载入中...
搜索中...
未找到
ParticleGraph.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <map>
6#include <set>
7#include <utility>
8
10namespace {
11
12void error(ParticleGraphCompileResult& result, const char* rule, std::string message) {
14 DiagnosticSeverity::Error, std::move(message)));
15}
16
17const EditorValue* field(const EditorValue& value, const char* key) {
18 const auto* object = value.getIf<EditorValue::Object>();
19 if (!object) return nullptr;
20 const auto found = object->find(key);
21 return found == object->end() ? nullptr : &found->second;
22}
23
24GraphNodeRecord node(const GraphNodeId& id, std::string type, EditorValue::Object properties,
25 bool input = true, bool output = true) {
26 GraphNodeRecord result;
27 result.id = id;
28 result.type = std::move(type);
29 result.properties = EditorValue(std::move(properties));
30 if (input)
31 result.pins.push_back({GraphPinId(id.value() + ".in"), id, "particle.flow",
32 GraphPinDirection::Input});
33 if (output)
34 result.pins.push_back({GraphPinId(id.value() + ".out"), id, "particle.flow",
35 GraphPinDirection::Output});
36 return result;
37}
38
39bool number(const EditorValue& properties, const char* key, double minimum, double maximum) {
40 const EditorValue* value = field(properties, key);
41 const auto* typed = value ? value->getIf<double>() : nullptr;
42 return typed && std::isfinite(*typed) && *typed >= minimum && *typed <= maximum;
43}
44
45bool integer(const EditorValue& properties, const char* key, int64_t minimum, int64_t maximum) {
46 const EditorValue* value = field(properties, key);
47 const auto* typed = value ? value->getIf<int64_t>() : nullptr;
48 return typed && *typed >= minimum && *typed <= maximum;
49}
50
51bool boolean(const EditorValue& properties, const char* key) {
52 const EditorValue* value = field(properties, key);
53 return value && value->getIf<bool>();
54}
55
56bool choice(const EditorValue& properties, const char* key,
57 std::initializer_list<const char*> choices) {
58 const EditorValue* value = field(properties, key);
59 const auto* typed = value ? value->getIf<std::string>() : nullptr;
60 if (!typed) return false;
61 return std::any_of(choices.begin(), choices.end(), [&](const char* item) { return *typed == item; });
62}
63
64bool tuple(const EditorValue& properties, const char* key, std::size_t size,
65 bool nonNegative = false) {
66 const EditorValue* value = field(properties, key);
67 const auto* typed = value ? value->getIf<EditorValue::Array>() : nullptr;
68 if (!typed || typed->size() != size) return false;
69 return std::all_of(typed->begin(), typed->end(), [&](const EditorValue& component) {
70 const auto* item = component.getIf<double>();
71 return item && std::isfinite(*item) && (!nonNegative || *item >= 0.0);
72 });
73}
74
75} // namespace
76
78 const GraphPinRecord& to) const {
80 result.allowed = from.direction == GraphPinDirection::Output &&
81 to.direction == GraphPinDirection::Input && from.type == "particle.flow" &&
82 to.type == "particle.flow" && from.node != to.node;
83 if (!result.allowed)
85 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.particles.invalid-connection"),
86 DiagnosticSeverity::Error, "Particle graph requires flow output-to-input between distinct nodes"));
87 return result;
88}
89
91 const std::string& type) const {
92 if (id.empty())
93 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Rejected, RuleId("editor.particles.node-id"),
94 "Particle node id is required");
96 if (type == "emission") {
97 properties["rate"] = 10.0;
98 properties["lifetime"] = EditorValue::Array{1.0, 1.0};
99 properties["area"] = "none";
100 properties["areaSize"] = EditorValue::Array{0.0, 0.0};
101 properties["randomSeed"] = 0;
102 properties["autoRandomSeed"] = false;
103 properties["looping"] = true;
104 properties["emitterLife"] = -1.0;
105 return eve::editing::applied<GraphNodeRecord>(node(id, type, std::move(properties), false, true));
106 }
107 if (type == "motion") {
108 properties["direction"] = 0.0;
109 properties["spread"] = 0.0;
110 properties["speed"] = EditorValue::Array{0.0, 0.0};
111 properties["gravity"] = EditorValue::Array{0.0, 0.0};
112 properties["damping"] = 0.0;
113 properties["simulationSpace"] = "world";
114 } else if (type == "collision") {
115 properties["mode"] = "none";
116 properties["radius"] = 0.0;
117 properties["restitution"] = 0.6;
118 properties["lifetimeLoss"] = 0.0;
119 properties["world"] = false;
120 } else if (type == "renderer") {
121 properties["mode"] = "billboard";
122 properties["material"] = "unlit";
123 properties["texture"] = "";
124 properties["size"] = EditorValue::Array{8.0, 8.0};
125 properties["sort"] = "none";
126 properties["gpu"] = false;
127 properties["softParticles"] = false;
128 properties["softDepth"] = 0.5;
129 properties["softFade"] = 0.05;
130 } else if (type == "output") {
131 properties["bufferSize"] = 1000;
132 properties["priority"] = 0;
133 properties["minimumQuality"] = 0;
134 properties["maxSpawnPerFrame"] = 0;
135 properties["overflow"] = "drop";
136 return eve::editing::applied<GraphNodeRecord>(node(id, type, std::move(properties), true, false));
137 } else {
138 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Unsupported, RuleId("editor.particles.node-type"),
139 "Unknown particle node type: " + type);
140 }
141 return eve::editing::applied<GraphNodeRecord>(node(id, type, std::move(properties)));
142}
143
146 result.documentRevision = graph.revision;
147 if (graph.domain != domain()) {
148 result.status = EditorStatus::Rejected;
149 error(result, "editor.particles.wrong-domain", "Graph is not a particle emitter document");
150 return result;
151 }
152 if (graph.schemaVersion != 1) {
153 result.status = EditorStatus::Unsupported;
154 error(result, "editor.particles.unsupported-schema", "Particle graph schema version is unsupported");
155 return result;
156 }
157 std::map<GraphNodeId, const GraphNodeRecord*> nodes;
158 std::map<GraphPinId, GraphNodeId> pins;
159 std::map<GraphPinId, const GraphPinRecord*> pinRecords;
160 int emissionCount = 0;
161 int rendererCount = 0;
162 int outputCount = 0;
163 for (const GraphNodeRecord& item : graph.nodes) {
164 if (!nodes.emplace(item.id, &item).second)
165 error(result, "editor.particles.duplicate-node", "Duplicate particle node id: " + item.id.value());
166 for (const GraphPinRecord& pin : item.pins) {
167 pins.emplace(pin.id, item.id);
168 pinRecords.emplace(pin.id, &pin);
169 }
170 emissionCount += item.type == "emission";
171 rendererCount += item.type == "renderer";
172 outputCount += item.type == "output";
173 }
174 if (emissionCount != 1 || rendererCount != 1 || outputCount != 1)
175 error(result, "editor.particles.required-modules",
176 "Particle graph requires exactly one emission, renderer and output node");
177 std::map<GraphNodeId, GraphNodeId> next;
178 std::map<GraphNodeId, int> incoming;
179 for (const GraphEdgeRecord& edge : graph.edges) {
180 if (!pins.contains(edge.from) || !pins.contains(edge.to)) {
181 error(result, "editor.particles.dangling-edge", "Particle edge references a missing pin");
182 continue;
183 }
184 if (!canConnect(*pinRecords.at(edge.from), *pinRecords.at(edge.to)).allowed) {
185 error(result, "editor.particles.edge-direction", "Particle edge has an invalid direction or type");
186 continue;
187 }
188 const GraphNodeId from = pins[edge.from];
189 const GraphNodeId to = pins[edge.to];
190 if (!next.emplace(from, to).second || ++incoming[to] > 1)
191 error(result, "editor.particles.branching", "Particle module chain cannot branch or merge");
192 }
193 const auto emission = std::find_if(nodes.begin(), nodes.end(), [](const auto& entry) {
194 return entry.second->type == "emission";
195 });
196 std::set<GraphNodeId> visited;
197 if (emission != nodes.end()) {
198 GraphNodeId current = emission->first;
199 while (visited.insert(current).second && next.contains(current)) current = next[current];
200 if (visited.size() != nodes.size() || (next.contains(current) && visited.contains(next[current])))
201 error(result, "editor.particles.chain", "Particle modules must form one acyclic emission-to-output chain");
202 else if (nodes.at(current)->type != "output")
203 error(result, "editor.particles.output-terminal", "Particle module chain must terminate at output");
204 }
205
206 EditorValue::Object config;
207 for (const auto& [id, item] : nodes) {
208 (void)id;
209 const EditorValue& p = item->properties;
210 if (item->type == "emission") {
211 if (!number(p, "rate", 0.0, 10000000.0) || !tuple(p, "lifetime", 2, true) ||
212 !choice(p, "area", {"none", "ellipse", "rect"}) || !tuple(p, "areaSize", 2, true) ||
213 !integer(p, "randomSeed", -2147483648LL, 2147483647LL) ||
214 !boolean(p, "autoRandomSeed") || !boolean(p, "looping") ||
215 !number(p, "emitterLife", -1.0, 86400.0))
216 error(result, "editor.particles.emission-properties", "Emission properties are invalid");
217 config["emission"] = p;
218 } else if (item->type == "motion") {
219 if (!number(p, "direction", -360000.0, 360000.0) || !number(p, "spread", 0.0, 360.0) ||
220 !tuple(p, "speed", 2, true) || !tuple(p, "gravity", 2) ||
221 !number(p, "damping", 0.0, 1.0) ||
222 !choice(p, "simulationSpace", {"world", "local"}))
223 error(result, "editor.particles.motion-properties", "Motion properties are invalid");
224 config["motion"] = p;
225 } else if (item->type == "collision") {
226 if (!choice(p, "mode", {"none", "kill", "bounce", "stop"}) ||
227 !number(p, "radius", 0.0, 1000000.0) ||
228 !number(p, "restitution", 0.0, 1.0) ||
229 !number(p, "lifetimeLoss", 0.0, 1.0) || !boolean(p, "world"))
230 error(result, "editor.particles.collision-properties", "Collision properties are invalid");
231 config["collision"] = p;
232 } else if (item->type == "renderer") {
233 if (!choice(p, "mode", {"billboard", "axis", "stretched", "ribbon"}) ||
234 !choice(p, "material", {"unlit", "lit", "distortion"}) ||
235 !field(p, "texture") || !field(p, "texture")->getIf<std::string>() ||
236 !tuple(p, "size", 2, true) || !choice(p, "sort", {"none", "oldest", "youngest", "distance"}) ||
237 !boolean(p, "gpu") || !boolean(p, "softParticles") ||
238 !number(p, "softDepth", 0.0, 1.0) || !number(p, "softFade", 0.0, 1.0))
239 error(result, "editor.particles.renderer-properties", "Renderer properties are invalid");
240 config["renderer"] = p;
241 } else if (item->type == "output") {
242 if (!integer(p, "bufferSize", 1, 10000000) || !integer(p, "priority", -100000, 100000) ||
243 !integer(p, "minimumQuality", 0, 3) || !integer(p, "maxSpawnPerFrame", 0, 10000000) ||
244 !choice(p, "overflow", {"drop", "pause", "warn"}))
245 error(result, "editor.particles.output-properties", "Output/budget properties are invalid");
246 config["output"] = p;
247 } else {
248 error(result, "editor.particles.unknown-node", "Unknown particle module: " + item->type);
249 }
250 }
251 if (!result.diagnostics.empty()) {
252 result.status = EditorStatus::Failed;
253 return result;
254 }
255 result.status = EditorStatus::Applied;
256 result.configuration = EditorValue(std::move(config));
257 return result;
258}
259
261 double fixedStep, int particleBudget,
262 int spawnBudgetPerFrame) const {
264 result.documentRevision = graph.revision;
265 result.simulatedSeconds = seconds;
266 const auto compiled = compile(graph);
267 result.diagnostics = compiled.diagnostics;
268 if (compiled.status != EditorStatus::Applied || !std::isfinite(seconds) ||
269 !std::isfinite(fixedStep) || seconds < 0.0 || seconds > 600.0 || fixedStep <= 0.0 ||
270 seconds / fixedStep > 1000000.0 ||
271 particleBudget < 1 || spawnBudgetPerFrame < 0) {
272 result.status = EditorStatus::Rejected;
273 if (compiled.status == EditorStatus::Applied)
275 eve::DiagnosticCode::InvalidArgument, RuleId("editor.particles.preview-input"),
276 DiagnosticSeverity::Error, "Preview time, step, and budgets are invalid"));
277 return result;
278 }
279 const EditorValue* emission = field(compiled.configuration, "emission");
280 const EditorValue* output = field(compiled.configuration, "output");
281 const double rate = *field(*emission, "rate")->getIf<double>();
282 const auto& lifetime = *field(*emission, "lifetime")->getIf<EditorValue::Array>();
283 const double maxLifetime = *lifetime[1].getIf<double>();
284 const int bufferSize = static_cast<int>(*field(*output, "bufferSize")->getIf<int64_t>());
285 const int configuredFrameCap = static_cast<int>(*field(*output, "maxSpawnPerFrame")->getIf<int64_t>());
286 const int frames = static_cast<int>(std::ceil(seconds / fixedStep));
287 const int requested = static_cast<int>(std::floor(rate * seconds));
288 int frameCap = configuredFrameCap;
289 if (spawnBudgetPerFrame > 0 && (frameCap == 0 || spawnBudgetPerFrame < frameCap))
290 frameCap = spawnBudgetPerFrame;
291 const int admittedByFrames = frameCap > 0 ? std::min(requested, frames * frameCap) : requested;
292 result.spawnedParticles = std::min(admittedByFrames, particleBudget);
293 result.droppedParticles = requested - result.spawnedParticles;
294 result.peakLiveParticles = std::min({result.spawnedParticles, bufferSize, particleBudget,
295 static_cast<int>(std::ceil(rate * maxLifetime))});
296 if (result.droppedParticles > 0)
298 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.particles.preview-budget"),
299 DiagnosticSeverity::Warning,
300 "Preview dropped spawns because a particle or per-frame budget was exceeded"));
301 if (result.peakLiveParticles >= bufferSize && requested > bufferSize)
303 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.particles.preview-buffer"),
304 DiagnosticSeverity::Warning, "Preview reaches the emitter buffer capacity"));
305 result.status = EditorStatus::Applied;
306 return result;
307}
308
309} // namespace eve::particles_editing
double value
std::string output
std::string from
glm::vec4 p[6]
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
float maximum[3]
float minimum[3]
std::uint32_t key
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::map< std::string, PropertyRule > properties
std::set< std::string > choices
std::vector< BvhNode > nodes
std::string error
Definition Package.cpp:60
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
std::weak_ptr< const void > lifetime
std::string id
Definition PlayHost.cpp:108
const RoadNode * node
const RoadEdge * edge
double number
bool found
double current
bool boolean
float size
Definition TreeMesh.cpp:156
Move-only operation result carrying either a value or Status.
Definition Result.h:155
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
ParticleGraphPreviewResult preview(const GraphDocumentData &graph, double seconds, double fixedStep=1.0/60.0, int particleBudget=100000, int spawnBudgetPerFrame=0) const
Estimate a deterministic preview and enforce particle/frame budgets.
GraphConnectionDecision canConnect(const GraphPinRecord &from, const GraphPinRecord &to) const override
Can connect.
ParticleGraphCompileResult compile(const GraphDocumentData &graph) const
Compile one acyclic module chain into a runtime-neutral configuration object.
Result< GraphNodeRecord > makeNode(const GraphNodeId &id, const std::string &type) const
Construct a node of type emission, motion, collision, renderer, or output.
std::string domain() const override
Domain.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
Result of domain-specific connection validation.
std::vector< Diagnostic > diagnostics
Immutable graph document value sent to a domain compiler.
Directed connection between an output and input pin.
Generic graph node independent of its domain.
std::vector< GraphPinRecord > pins
Stable typed pin owned by a graph node.
Validated particle configuration compiled from a module graph.
std::vector< EditorDiagnostic > diagnostics
Deterministic, revision-tagged particle preview estimate.
std::vector< EditorDiagnostic > diagnostics