1
2use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq)]
13pub enum VfxAttributeType {
14 Float,
15 Float2,
16 Float3,
17 Float4,
18 Int,
19 Uint,
20 Bool,
21 Color,
22 Matrix4x4,
23 Mesh,
24 Texture2D,
25 Texture3D,
26 TextureCube,
27 AnimationCurve,
28 Gradient,
29 SdfVolume,
30 PointCache,
31}
32
33impl VfxAttributeType {
34 pub fn label(&self) -> &'static str {
35 match self {
36 Self::Float => "Float",
37 Self::Float2 => "Vector2",
38 Self::Float3 => "Vector3",
39 Self::Float4 => "Vector4",
40 Self::Int => "Int",
41 Self::Uint => "UInt",
42 Self::Bool => "Bool",
43 Self::Color => "Color",
44 Self::Matrix4x4 => "Matrix4x4",
45 Self::Mesh => "Mesh",
46 Self::Texture2D => "Texture2D",
47 Self::Texture3D => "Texture3D",
48 Self::TextureCube => "TextureCube",
49 Self::AnimationCurve => "AnimationCurve",
50 Self::Gradient => "Gradient",
51 Self::SdfVolume => "SDF Volume",
52 Self::PointCache => "Point Cache",
53 }
54 }
55
56 pub fn port_color(&self) -> Vec4 {
57 match self {
58 Self::Float => Vec4::new(0.55, 0.8, 0.55, 1.0),
59 Self::Float2 => Vec4::new(0.55, 0.72, 0.87, 1.0),
60 Self::Float3 => Vec4::new(0.55, 0.55, 0.87, 1.0),
61 Self::Float4 => Vec4::new(0.72, 0.55, 0.87, 1.0),
62 Self::Int => Vec4::new(0.5, 0.8, 0.5, 1.0),
63 Self::Uint => Vec4::new(0.4, 0.75, 0.4, 1.0),
64 Self::Bool => Vec4::new(0.87, 0.55, 0.55, 1.0),
65 Self::Color => Vec4::new(0.87, 0.87, 0.25, 1.0),
66 Self::Matrix4x4 => Vec4::new(0.75, 0.65, 0.45, 1.0),
67 Self::Mesh => Vec4::new(0.87, 0.6, 0.35, 1.0),
68 Self::Texture2D | Self::Texture3D | Self::TextureCube => Vec4::new(0.65, 0.45, 0.75, 1.0),
69 Self::AnimationCurve => Vec4::new(0.35, 0.65, 0.65, 1.0),
70 Self::Gradient => Vec4::new(0.85, 0.65, 0.35, 1.0),
71 Self::SdfVolume => Vec4::new(0.35, 0.85, 0.65, 1.0),
72 Self::PointCache => Vec4::new(0.65, 0.85, 0.35, 1.0),
73 }
74 }
75
76 pub fn default_value(&self) -> VfxValue {
77 match self {
78 Self::Float => VfxValue::Float(0.0),
79 Self::Float2 => VfxValue::Float2(Vec2::ZERO),
80 Self::Float3 => VfxValue::Float3(Vec3::ZERO),
81 Self::Float4 => VfxValue::Float4(Vec4::ZERO),
82 Self::Int => VfxValue::Int(0),
83 Self::Uint => VfxValue::Uint(0),
84 Self::Bool => VfxValue::Bool(false),
85 Self::Color => VfxValue::Color(Vec4::ONE),
86 Self::Matrix4x4 => VfxValue::Matrix4x4(Mat4::IDENTITY),
87 _ => VfxValue::None,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
97pub enum VfxValue {
98 None,
99 Float(f32),
100 Float2(Vec2),
101 Float3(Vec3),
102 Float4(Vec4),
103 Int(i32),
104 Uint(u32),
105 Bool(bool),
106 Color(Vec4),
107 Matrix4x4(Mat4),
108}
109
110impl VfxValue {
111 pub fn as_float(&self) -> f32 {
112 match self {
113 Self::Float(v) => *v,
114 Self::Int(v) => *v as f32,
115 Self::Uint(v) => *v as f32,
116 Self::Bool(v) => if *v { 1.0 } else { 0.0 },
117 _ => 0.0,
118 }
119 }
120
121 pub fn as_float3(&self) -> Vec3 {
122 match self {
123 Self::Float3(v) => *v,
124 Self::Float4(v) => Vec3::new(v.x, v.y, v.z),
125 Self::Color(v) => Vec3::new(v.x, v.y, v.z),
126 Self::Float(v) => Vec3::splat(*v),
127 _ => Vec3::ZERO,
128 }
129 }
130
131 pub fn as_color(&self) -> Vec4 {
132 match self {
133 Self::Color(v) => *v,
134 Self::Float4(v) => *v,
135 Self::Float3(v) => Vec4::new(v.x, v.y, v.z, 1.0),
136 _ => Vec4::ONE,
137 }
138 }
139
140 pub fn type_of(&self) -> VfxAttributeType {
141 match self {
142 Self::None => VfxAttributeType::Bool,
143 Self::Float(_) => VfxAttributeType::Float,
144 Self::Float2(_) => VfxAttributeType::Float2,
145 Self::Float3(_) => VfxAttributeType::Float3,
146 Self::Float4(_) => VfxAttributeType::Float4,
147 Self::Int(_) => VfxAttributeType::Int,
148 Self::Uint(_) => VfxAttributeType::Uint,
149 Self::Bool(_) => VfxAttributeType::Bool,
150 Self::Color(_) => VfxAttributeType::Color,
151 Self::Matrix4x4(_) => VfxAttributeType::Matrix4x4,
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161pub enum BuiltinAttribute {
162 Position,
163 Velocity,
164 OldPosition,
165 Direction,
166 Size,
167 SizeX,
168 SizeY,
169 SizeZ,
170 Scale,
171 Color,
172 Alpha,
173 AliveTime,
174 TotalLifetime,
175 Age,
176 AgeNormalized,
177 Mass,
178 AngleX,
179 AngleY,
180 AngleZ,
181 AngularVelocityX,
182 AngularVelocityY,
183 AngularVelocityZ,
184 TexIndex,
185 PivotX,
186 PivotY,
187 PivotZ,
188 Random,
189 ParticleId,
190 SpawnIndex,
191 StripIndex,
192}
193
194impl BuiltinAttribute {
195 pub fn label(&self) -> &'static str {
196 match self {
197 Self::Position => "position",
198 Self::Velocity => "velocity",
199 Self::OldPosition => "oldPosition",
200 Self::Direction => "direction",
201 Self::Size => "size",
202 Self::SizeX => "sizeX",
203 Self::SizeY => "sizeY",
204 Self::SizeZ => "sizeZ",
205 Self::Scale => "scale",
206 Self::Color => "color",
207 Self::Alpha => "alpha",
208 Self::AliveTime => "alive",
209 Self::TotalLifetime => "lifetime",
210 Self::Age => "age",
211 Self::AgeNormalized => "ageNormalized",
212 Self::Mass => "mass",
213 Self::AngleX => "angleX",
214 Self::AngleY => "angleY",
215 Self::AngleZ => "angleZ",
216 Self::AngularVelocityX => "angularVelocityX",
217 Self::AngularVelocityY => "angularVelocityY",
218 Self::AngularVelocityZ => "angularVelocityZ",
219 Self::TexIndex => "texIndex",
220 Self::PivotX => "pivotX",
221 Self::PivotY => "pivotY",
222 Self::PivotZ => "pivotZ",
223 Self::Random => "random",
224 Self::ParticleId => "particleId",
225 Self::SpawnIndex => "spawnIndex",
226 Self::StripIndex => "stripIndex",
227 }
228 }
229
230 pub fn attribute_type(&self) -> VfxAttributeType {
231 match self {
232 Self::Position | Self::Velocity | Self::OldPosition | Self::Direction => VfxAttributeType::Float3,
233 Self::Color => VfxAttributeType::Color,
234 Self::Alpha | Self::Size | Self::SizeX | Self::SizeY | Self::SizeZ | Self::Scale => VfxAttributeType::Float,
235 Self::AliveTime | Self::TotalLifetime | Self::Age | Self::AgeNormalized | Self::Mass => VfxAttributeType::Float,
236 Self::AngleX | Self::AngleY | Self::AngleZ => VfxAttributeType::Float,
237 Self::AngularVelocityX | Self::AngularVelocityY | Self::AngularVelocityZ => VfxAttributeType::Float,
238 Self::TexIndex | Self::ParticleId | Self::SpawnIndex | Self::StripIndex => VfxAttributeType::Uint,
239 Self::PivotX | Self::PivotY | Self::PivotZ | Self::Random => VfxAttributeType::Float,
240 }
241 }
242}
243
244#[derive(Debug, Clone)]
249pub struct VfxPort {
250 pub name: String,
251 pub data_type: VfxAttributeType,
252 pub is_input: bool,
253 pub optional: bool,
254 pub default_value: VfxValue,
255 pub tooltip: String,
256}
257
258impl VfxPort {
259 pub fn input(name: &str, data_type: VfxAttributeType) -> Self {
260 let default_value = data_type.default_value();
261 Self {
262 name: name.to_string(),
263 data_type,
264 is_input: true,
265 optional: false,
266 default_value,
267 tooltip: String::new(),
268 }
269 }
270
271 pub fn output(name: &str, data_type: VfxAttributeType) -> Self {
272 let default_value = data_type.default_value();
273 Self {
274 name: name.to_string(),
275 data_type,
276 is_input: false,
277 optional: false,
278 default_value,
279 tooltip: String::new(),
280 }
281 }
282
283 pub fn optional(mut self) -> Self { self.optional = true; self }
284 pub fn with_tooltip(mut self, tip: &str) -> Self { self.tooltip = tip.to_string(); self }
285}
286
287#[derive(Debug, Clone, PartialEq)]
292pub enum VfxNodeKind {
293 SpawnContext,
295 InitializeContext,
296 UpdateContext,
297 OutputParticleQuad,
298 OutputParticleMesh,
299 OutputParticleStrip,
300
301 ConstantRate,
303 BurstSpawn,
304 PeriodicBurst,
305 OnCollision,
306 OnTrigger,
307 SpawnOnDeath,
308 SpawnFromPointCache,
309
310 SetPositionShape,
312 SetPositionSphere,
313 SetPositionCone,
314 SetPositionBox,
315 SetPositionLine,
316 SetPositionTorus,
317 SetPositionMeshSurface,
318 SetVelocityRandom,
319 SetVelocityTangent,
320 SetLifetime,
321 SetSize,
322 SetColor,
323 SetColorFromGradient,
324 SetAlpha,
325 SetMass,
326 SetAngle,
327 SetTexIndex,
328 InheritSourceVelocity,
329 InheritSourceColor,
330 InheritSourcePosition,
331
332 Gravity,
334 Drag,
335 Turbulence,
336 VelocityField,
337 ConformToSphere,
338 ConformToSdf,
339 OrbitForce,
340 LinearDrag,
341 AngularDrag,
342 AttractToPosition,
343 FlipbookAnimation,
344 RotateOverTime,
345 ScaleOverLife,
346 ColorOverLife,
347 AlphaOverLife,
348 SpeedLimiter,
349 Collision,
350 KillOnCollision,
351 KillOnBounds,
352 TriggerOnDeath,
353 UpdatePosition,
354 EulerIntegration,
355 Noise3D,
356 CurlNoise,
357
358 Add,
360 Subtract,
361 Multiply,
362 Divide,
363 Power,
364 Sqrt,
365 Abs,
366 Sin,
367 Cos,
368 Tan,
369 Atan2,
370 Floor,
371 Ceil,
372 Round,
373 Frac,
374 Clamp,
375 Lerp,
376 Step,
377 SmoothStep,
378 Min,
379 Max,
380 Remap,
381 Dot,
382 Cross,
383 Normalize,
384 Length,
385 Distance,
386 Swizzle,
387 Combine,
388 Split,
389 Negate,
390 OneMinus,
391 Reciprocal,
392
393 SampleGradient,
395 SampleCurve,
396 SampleTexture2D,
397 SampleTexture3D,
398 SampleSdf,
399 SampleMesh,
400 SampleNoise2D,
401 SampleNoise3D,
402
403 GetAttribute,
405 SetAttribute,
406 GetBuiltin,
407
408 Branch,
410 Loop,
411 Random,
412 RandomPerComponent,
413 Sequence,
414
415 Comment,
417 Sticky,
418 ExposedParameter,
419 CustomHlsl,
420}
421
422impl VfxNodeKind {
423 pub fn label(&self) -> &'static str {
424 match self {
425 Self::SpawnContext => "Spawn",
426 Self::InitializeContext => "Initialize",
427 Self::UpdateContext => "Update",
428 Self::OutputParticleQuad => "Output Particle (Quad)",
429 Self::OutputParticleMesh => "Output Particle (Mesh)",
430 Self::OutputParticleStrip => "Output Particle (Strip)",
431 Self::ConstantRate => "Constant Rate",
432 Self::BurstSpawn => "Burst",
433 Self::PeriodicBurst => "Periodic Burst",
434 Self::OnCollision => "On Collision",
435 Self::OnTrigger => "On Trigger",
436 Self::SpawnOnDeath => "Spawn on Death",
437 Self::SpawnFromPointCache => "Spawn from Point Cache",
438 Self::SetPositionShape => "Set Position (Shape)",
439 Self::SetPositionSphere => "Set Position (Sphere)",
440 Self::SetPositionCone => "Set Position (Cone)",
441 Self::SetPositionBox => "Set Position (Box)",
442 Self::SetPositionLine => "Set Position (Line)",
443 Self::SetPositionTorus => "Set Position (Torus)",
444 Self::SetPositionMeshSurface => "Set Position (Mesh Surface)",
445 Self::SetVelocityRandom => "Set Velocity (Random)",
446 Self::SetVelocityTangent => "Set Velocity (Tangent)",
447 Self::SetLifetime => "Set Lifetime",
448 Self::SetSize => "Set Size",
449 Self::SetColor => "Set Color",
450 Self::SetColorFromGradient => "Set Color (Gradient)",
451 Self::SetAlpha => "Set Alpha",
452 Self::SetMass => "Set Mass",
453 Self::SetAngle => "Set Angle",
454 Self::SetTexIndex => "Set Tex Index",
455 Self::InheritSourceVelocity => "Inherit Source Velocity",
456 Self::InheritSourceColor => "Inherit Source Color",
457 Self::InheritSourcePosition => "Inherit Source Position",
458 Self::Gravity => "Gravity",
459 Self::Drag => "Drag",
460 Self::Turbulence => "Turbulence",
461 Self::VelocityField => "Velocity Field",
462 Self::ConformToSphere => "Conform to Sphere",
463 Self::ConformToSdf => "Conform to SDF",
464 Self::OrbitForce => "Orbit Force",
465 Self::LinearDrag => "Linear Drag",
466 Self::AngularDrag => "Angular Drag",
467 Self::AttractToPosition => "Attract to Position",
468 Self::FlipbookAnimation => "Flipbook",
469 Self::RotateOverTime => "Rotate over Time",
470 Self::ScaleOverLife => "Scale over Life",
471 Self::ColorOverLife => "Color over Life",
472 Self::AlphaOverLife => "Alpha over Life",
473 Self::SpeedLimiter => "Speed Limiter",
474 Self::Collision => "Collision",
475 Self::KillOnCollision => "Kill on Collision",
476 Self::KillOnBounds => "Kill on Bounds",
477 Self::TriggerOnDeath => "Trigger on Death",
478 Self::UpdatePosition => "Update Position",
479 Self::EulerIntegration => "Euler Integration",
480 Self::Noise3D => "Noise 3D",
481 Self::CurlNoise => "Curl Noise",
482 Self::Add => "Add",
483 Self::Subtract => "Subtract",
484 Self::Multiply => "Multiply",
485 Self::Divide => "Divide",
486 Self::Power => "Power",
487 Self::Sqrt => "Sqrt",
488 Self::Abs => "Abs",
489 Self::Sin => "Sin",
490 Self::Cos => "Cos",
491 Self::Tan => "Tan",
492 Self::Atan2 => "Atan2",
493 Self::Floor => "Floor",
494 Self::Ceil => "Ceil",
495 Self::Round => "Round",
496 Self::Frac => "Frac",
497 Self::Clamp => "Clamp",
498 Self::Lerp => "Lerp",
499 Self::Step => "Step",
500 Self::SmoothStep => "Smooth Step",
501 Self::Min => "Min",
502 Self::Max => "Max",
503 Self::Remap => "Remap",
504 Self::Dot => "Dot Product",
505 Self::Cross => "Cross Product",
506 Self::Normalize => "Normalize",
507 Self::Length => "Length",
508 Self::Distance => "Distance",
509 Self::Swizzle => "Swizzle",
510 Self::Combine => "Combine",
511 Self::Split => "Split",
512 Self::Negate => "Negate",
513 Self::OneMinus => "One Minus",
514 Self::Reciprocal => "Reciprocal",
515 Self::SampleGradient => "Sample Gradient",
516 Self::SampleCurve => "Sample Curve",
517 Self::SampleTexture2D => "Sample Texture 2D",
518 Self::SampleTexture3D => "Sample Texture 3D",
519 Self::SampleSdf => "Sample SDF",
520 Self::SampleMesh => "Sample Mesh",
521 Self::SampleNoise2D => "Sample Noise 2D",
522 Self::SampleNoise3D => "Sample Noise 3D",
523 Self::GetAttribute => "Get Attribute",
524 Self::SetAttribute => "Set Attribute",
525 Self::GetBuiltin => "Get Builtin Attribute",
526 Self::Branch => "Branch",
527 Self::Loop => "Loop",
528 Self::Random => "Random",
529 Self::RandomPerComponent => "Random (Per Component)",
530 Self::Sequence => "Sequence",
531 Self::Comment => "Comment",
532 Self::Sticky => "Sticky Note",
533 Self::ExposedParameter => "Exposed Parameter",
534 Self::CustomHlsl => "Custom HLSL",
535 }
536 }
537
538 pub fn category(&self) -> &'static str {
539 match self {
540 Self::SpawnContext | Self::InitializeContext | Self::UpdateContext
541 | Self::OutputParticleQuad | Self::OutputParticleMesh | Self::OutputParticleStrip => "Context",
542 Self::ConstantRate | Self::BurstSpawn | Self::PeriodicBurst
543 | Self::OnCollision | Self::OnTrigger | Self::SpawnOnDeath | Self::SpawnFromPointCache => "Spawn",
544 Self::SetPositionShape | Self::SetPositionSphere | Self::SetPositionCone
545 | Self::SetPositionBox | Self::SetPositionLine | Self::SetPositionTorus
546 | Self::SetPositionMeshSurface | Self::SetVelocityRandom | Self::SetVelocityTangent
547 | Self::SetLifetime | Self::SetSize | Self::SetColor | Self::SetColorFromGradient
548 | Self::SetAlpha | Self::SetMass | Self::SetAngle | Self::SetTexIndex
549 | Self::InheritSourceVelocity | Self::InheritSourceColor | Self::InheritSourcePosition => "Initialize",
550 Self::Gravity | Self::Drag | Self::Turbulence | Self::VelocityField
551 | Self::ConformToSphere | Self::ConformToSdf | Self::OrbitForce | Self::LinearDrag
552 | Self::AngularDrag | Self::AttractToPosition | Self::FlipbookAnimation
553 | Self::RotateOverTime | Self::ScaleOverLife | Self::ColorOverLife | Self::AlphaOverLife
554 | Self::SpeedLimiter | Self::Collision | Self::KillOnCollision | Self::KillOnBounds
555 | Self::TriggerOnDeath | Self::UpdatePosition | Self::EulerIntegration
556 | Self::Noise3D | Self::CurlNoise => "Update",
557 Self::Add | Self::Subtract | Self::Multiply | Self::Divide | Self::Power
558 | Self::Sqrt | Self::Abs | Self::Sin | Self::Cos | Self::Tan | Self::Atan2
559 | Self::Floor | Self::Ceil | Self::Round | Self::Frac | Self::Clamp
560 | Self::Lerp | Self::Step | Self::SmoothStep | Self::Min | Self::Max
561 | Self::Remap | Self::Dot | Self::Cross | Self::Normalize | Self::Length
562 | Self::Distance | Self::Swizzle | Self::Combine | Self::Split
563 | Self::Negate | Self::OneMinus | Self::Reciprocal => "Math",
564 Self::SampleGradient | Self::SampleCurve | Self::SampleTexture2D
565 | Self::SampleTexture3D | Self::SampleSdf | Self::SampleMesh
566 | Self::SampleNoise2D | Self::SampleNoise3D => "Sample",
567 Self::GetAttribute | Self::SetAttribute | Self::GetBuiltin => "Attribute",
568 Self::Branch | Self::Loop | Self::Random | Self::RandomPerComponent | Self::Sequence => "Flow",
569 Self::Comment | Self::Sticky | Self::ExposedParameter | Self::CustomHlsl => "Utility",
570 }
571 }
572
573 pub fn header_color(&self) -> Vec4 {
574 match self.category() {
575 "Context" => Vec4::new(0.16, 0.29, 0.48, 1.0),
576 "Spawn" => Vec4::new(0.29, 0.48, 0.16, 1.0),
577 "Initialize" => Vec4::new(0.16, 0.48, 0.35, 1.0),
578 "Update" => Vec4::new(0.48, 0.29, 0.16, 1.0),
579 "Math" => Vec4::new(0.35, 0.22, 0.48, 1.0),
580 "Sample" => Vec4::new(0.48, 0.16, 0.35, 1.0),
581 "Attribute" => Vec4::new(0.22, 0.40, 0.48, 1.0),
582 "Flow" => Vec4::new(0.48, 0.40, 0.16, 1.0),
583 _ => Vec4::new(0.30, 0.30, 0.30, 1.0),
584 }
585 }
586
587 pub fn default_ports(&self) -> (Vec<VfxPort>, Vec<VfxPort>) {
588 match self {
589 Self::ConstantRate => (
590 vec![VfxPort::input("Rate", VfxAttributeType::Float)],
591 vec![VfxPort::output("Spawn Count", VfxAttributeType::Uint)],
592 ),
593 Self::SetLifetime => (
594 vec![VfxPort::input("Lifetime", VfxAttributeType::Float)],
595 vec![],
596 ),
597 Self::SetColor => (
598 vec![VfxPort::input("Color", VfxAttributeType::Color)],
599 vec![],
600 ),
601 Self::SetSize => (
602 vec![VfxPort::input("Size", VfxAttributeType::Float)],
603 vec![],
604 ),
605 Self::Add | Self::Subtract | Self::Multiply | Self::Divide => (
606 vec![
607 VfxPort::input("A", VfxAttributeType::Float),
608 VfxPort::input("B", VfxAttributeType::Float),
609 ],
610 vec![VfxPort::output("Result", VfxAttributeType::Float)],
611 ),
612 Self::Lerp => (
613 vec![
614 VfxPort::input("A", VfxAttributeType::Float),
615 VfxPort::input("B", VfxAttributeType::Float),
616 VfxPort::input("T", VfxAttributeType::Float),
617 ],
618 vec![VfxPort::output("Result", VfxAttributeType::Float)],
619 ),
620 Self::Clamp => (
621 vec![
622 VfxPort::input("Value", VfxAttributeType::Float),
623 VfxPort::input("Min", VfxAttributeType::Float),
624 VfxPort::input("Max", VfxAttributeType::Float),
625 ],
626 vec![VfxPort::output("Result", VfxAttributeType::Float)],
627 ),
628 Self::SampleGradient => (
629 vec![
630 VfxPort::input("Gradient", VfxAttributeType::Gradient),
631 VfxPort::input("T", VfxAttributeType::Float),
632 ],
633 vec![VfxPort::output("Color", VfxAttributeType::Color)],
634 ),
635 Self::SampleCurve => (
636 vec![
637 VfxPort::input("Curve", VfxAttributeType::AnimationCurve),
638 VfxPort::input("T", VfxAttributeType::Float),
639 ],
640 vec![VfxPort::output("Value", VfxAttributeType::Float)],
641 ),
642 Self::GetBuiltin => (
643 vec![],
644 vec![VfxPort::output("Value", VfxAttributeType::Float)],
645 ),
646 Self::Gravity => (
647 vec![VfxPort::input("Gravity", VfxAttributeType::Float3).optional()],
648 vec![],
649 ),
650 Self::Noise3D => (
651 vec![
652 VfxPort::input("Position", VfxAttributeType::Float3),
653 VfxPort::input("Frequency", VfxAttributeType::Float),
654 VfxPort::input("Octaves", VfxAttributeType::Int).optional(),
655 ],
656 vec![
657 VfxPort::output("Noise", VfxAttributeType::Float),
658 VfxPort::output("Noise3D", VfxAttributeType::Float3),
659 ],
660 ),
661 Self::Branch => (
662 vec![
663 VfxPort::input("Condition", VfxAttributeType::Bool),
664 VfxPort::input("True", VfxAttributeType::Float),
665 VfxPort::input("False", VfxAttributeType::Float),
666 ],
667 vec![VfxPort::output("Result", VfxAttributeType::Float)],
668 ),
669 Self::Combine => (
670 vec![
671 VfxPort::input("X", VfxAttributeType::Float),
672 VfxPort::input("Y", VfxAttributeType::Float),
673 VfxPort::input("Z", VfxAttributeType::Float).optional(),
674 VfxPort::input("W", VfxAttributeType::Float).optional(),
675 ],
676 vec![VfxPort::output("Vector", VfxAttributeType::Float4)],
677 ),
678 Self::Split => (
679 vec![VfxPort::input("Vector", VfxAttributeType::Float4)],
680 vec![
681 VfxPort::output("X", VfxAttributeType::Float),
682 VfxPort::output("Y", VfxAttributeType::Float),
683 VfxPort::output("Z", VfxAttributeType::Float),
684 VfxPort::output("W", VfxAttributeType::Float),
685 ],
686 ),
687 _ => (vec![], vec![]),
688 }
689 }
690}
691
692static mut VFX_NODE_ID_COUNTER: u64 = 1;
697fn next_vfx_node_id() -> u64 {
698 unsafe {
699 let id = VFX_NODE_ID_COUNTER;
700 VFX_NODE_ID_COUNTER += 1;
701 id
702 }
703}
704
705#[derive(Debug, Clone)]
706pub struct VfxNode {
707 pub id: u64,
708 pub kind: VfxNodeKind,
709 pub position: Vec2,
710 pub size: Vec2,
711 pub title_override: Option<String>,
712 pub inputs: Vec<VfxPort>,
713 pub outputs: Vec<VfxPort>,
714 pub param_values: HashMap<String, VfxValue>,
715 pub comment: String,
716 pub collapsed: bool,
717 pub enabled: bool,
718 pub preview_enabled: bool,
719 pub error_message: Option<String>,
720 pub custom_hlsl_code: String,
721 pub exposed_param_name: String,
722 pub builtin_attribute: BuiltinAttribute,
723}
724
725impl VfxNode {
726 pub fn new(kind: VfxNodeKind, position: Vec2) -> Self {
727 let (inputs, outputs) = kind.default_ports();
728 let mut node = Self {
729 id: next_vfx_node_id(),
730 kind,
731 position,
732 size: Vec2::new(200.0, 80.0),
733 title_override: None,
734 inputs,
735 outputs,
736 param_values: HashMap::new(),
737 comment: String::new(),
738 collapsed: false,
739 enabled: true,
740 preview_enabled: false,
741 error_message: None,
742 custom_hlsl_code: String::new(),
743 exposed_param_name: String::new(),
744 builtin_attribute: BuiltinAttribute::Age,
745 };
746 node.recalculate_size();
747 node
748 }
749
750 pub fn label(&self) -> &str {
751 if let Some(ref t) = self.title_override {
752 t.as_str()
753 } else {
754 self.kind.label()
755 }
756 }
757
758 pub fn recalculate_size(&mut self) {
759 let port_count = self.inputs.len().max(self.outputs.len());
760 let header_h = 28.0;
761 let port_h = 22.0;
762 let footer_h = 4.0;
763 self.size.x = 200.0;
764 self.size.y = header_h + port_count as f32 * port_h + footer_h;
765 if self.size.y < 60.0 { self.size.y = 60.0; }
766 }
767
768 pub fn input_port_position(&self, index: usize) -> Vec2 {
769 let y = self.position.y + 28.0 + index as f32 * 22.0 + 11.0;
770 Vec2::new(self.position.x, y)
771 }
772
773 pub fn output_port_position(&self, index: usize) -> Vec2 {
774 let y = self.position.y + 28.0 + index as f32 * 22.0 + 11.0;
775 Vec2::new(self.position.x + self.size.x, y)
776 }
777
778 pub fn contains_point(&self, pt: Vec2) -> bool {
779 pt.x >= self.position.x && pt.x <= self.position.x + self.size.x
780 && pt.y >= self.position.y && pt.y <= self.position.y + self.size.y
781 }
782
783 pub fn set_param(&mut self, name: &str, value: VfxValue) {
784 self.param_values.insert(name.to_string(), value);
785 }
786
787 pub fn get_param(&self, name: &str) -> Option<&VfxValue> {
788 self.param_values.get(name)
789 }
790}
791
792#[derive(Debug, Clone)]
797pub struct VfxConnection {
798 pub id: u64,
799 pub from_node: u64,
800 pub from_port: usize,
801 pub to_node: u64,
802 pub to_port: usize,
803}
804
805#[derive(Debug, Clone, Copy, PartialEq)]
810pub enum VfxContextKind {
811 Spawn,
812 Initialize,
813 Update,
814 Output,
815}
816
817#[derive(Debug, Clone)]
818pub struct VfxContext {
819 pub id: u64,
820 pub kind: VfxContextKind,
821 pub node_id: u64,
822 pub operator_node_ids: Vec<u64>,
823 pub enabled: bool,
824 pub capacity: u32,
825 pub label: String,
826}
827
828impl VfxContext {
829 pub fn new(kind: VfxContextKind, node_id: u64) -> Self {
830 let label = match kind {
831 VfxContextKind::Spawn => "Spawn".to_string(),
832 VfxContextKind::Initialize => "Initialize".to_string(),
833 VfxContextKind::Update => "Update".to_string(),
834 VfxContextKind::Output => "Output".to_string(),
835 };
836 Self {
837 id: next_vfx_node_id(),
838 kind,
839 node_id,
840 operator_node_ids: Vec::new(),
841 enabled: true,
842 capacity: 4096,
843 label,
844 }
845 }
846}
847
848#[derive(Debug, Clone)]
853pub struct ExposedParameter {
854 pub name: String,
855 pub data_type: VfxAttributeType,
856 pub value: VfxValue,
857 pub min: f32,
858 pub max: f32,
859 pub tooltip: String,
860 pub exposed: bool,
861}
862
863impl ExposedParameter {
864 pub fn new(name: &str, data_type: VfxAttributeType) -> Self {
865 let value = data_type.default_value();
866 Self {
867 name: name.to_string(),
868 data_type,
869 value,
870 min: 0.0,
871 max: 1.0,
872 tooltip: String::new(),
873 exposed: true,
874 }
875 }
876}
877
878#[derive(Debug, Clone)]
883pub struct VfxGraph {
884 pub name: String,
885 pub nodes: Vec<VfxNode>,
886 pub connections: Vec<VfxConnection>,
887 pub contexts: Vec<VfxContext>,
888 pub exposed_parameters: Vec<ExposedParameter>,
889 pub next_conn_id: u64,
890 pub dirty: bool,
891 pub last_compile_error: Option<String>,
892 pub compile_warnings: Vec<String>,
893 pub particle_capacity: u32,
894 pub simulation_space: SimulationSpace,
895 pub culling_mode: CullingMode,
896}
897
898#[derive(Debug, Clone, Copy, PartialEq)]
899pub enum SimulationSpace { Local, World }
900#[derive(Debug, Clone, Copy, PartialEq)]
901pub enum CullingMode { Automatic, AlwaysSimulate, StopSimulating, Pause }
902
903impl VfxGraph {
904 pub fn new(name: &str) -> Self {
905 Self {
906 name: name.to_string(),
907 nodes: Vec::new(),
908 connections: Vec::new(),
909 contexts: Vec::new(),
910 exposed_parameters: Vec::new(),
911 next_conn_id: 1,
912 dirty: false,
913 last_compile_error: None,
914 compile_warnings: Vec::new(),
915 particle_capacity: 4096,
916 simulation_space: SimulationSpace::World,
917 culling_mode: CullingMode::Automatic,
918 }
919 }
920
921 pub fn add_node(&mut self, node: VfxNode) -> u64 {
922 let id = node.id;
923 self.nodes.push(node);
924 self.dirty = true;
925 id
926 }
927
928 pub fn remove_node(&mut self, id: u64) {
929 self.nodes.retain(|n| n.id != id);
930 self.connections.retain(|c| c.from_node != id && c.to_node != id);
931 for ctx in &mut self.contexts {
932 ctx.operator_node_ids.retain(|&nid| nid != id);
933 }
934 self.dirty = true;
935 }
936
937 pub fn find_node(&self, id: u64) -> Option<&VfxNode> {
938 self.nodes.iter().find(|n| n.id == id)
939 }
940
941 pub fn find_node_mut(&mut self, id: u64) -> Option<&mut VfxNode> {
942 self.nodes.iter_mut().find(|n| n.id == id)
943 }
944
945 pub fn connect(&mut self, from_node: u64, from_port: usize, to_node: u64, to_port: usize) -> u64 {
946 self.connections.retain(|c| !(c.to_node == to_node && c.to_port == to_port));
948 let id = self.next_conn_id;
949 self.next_conn_id += 1;
950 self.connections.push(VfxConnection { id, from_node, from_port, to_node, to_port });
951 self.dirty = true;
952 id
953 }
954
955 pub fn disconnect(&mut self, conn_id: u64) {
956 self.connections.retain(|c| c.id != conn_id);
957 self.dirty = true;
958 }
959
960 pub fn validate(&mut self) -> Vec<String> {
961 let mut errors = Vec::new();
962 let node_ids: std::collections::HashSet<u64> = self.nodes.iter().map(|n| n.id).collect();
963 for conn in &self.connections {
964 if !node_ids.contains(&conn.from_node) {
965 errors.push(format!("Connection {} references missing node {}", conn.id, conn.from_node));
966 }
967 if !node_ids.contains(&conn.to_node) {
968 errors.push(format!("Connection {} references missing node {}", conn.id, conn.to_node));
969 }
970 }
971 let connected_inputs: std::collections::HashSet<(u64, usize)> =
973 self.connections.iter().map(|c| (c.to_node, c.to_port)).collect();
974 for node in &self.nodes {
975 for (i, port) in node.inputs.iter().enumerate() {
976 if !port.optional && !connected_inputs.contains(&(node.id, i)) {
977 self.compile_warnings.push(format!(
979 "Node '{}' port '{}' has no connection (will use default)",
980 node.label(), port.name
981 ));
982 }
983 }
984 }
985 if errors.is_empty() {
986 self.last_compile_error = None;
987 } else {
988 self.last_compile_error = Some(errors.join("; "));
989 }
990 errors
991 }
992
993 pub fn topological_order(&self) -> Vec<u64> {
994 let mut in_degree: HashMap<u64, usize> = self.nodes.iter().map(|n| (n.id, 0)).collect();
995 let mut adjacency: HashMap<u64, Vec<u64>> = self.nodes.iter().map(|n| (n.id, Vec::new())).collect();
996 for conn in &self.connections {
997 *in_degree.entry(conn.to_node).or_insert(0) += 1;
998 adjacency.entry(conn.from_node).or_default().push(conn.to_node);
999 }
1000 let mut queue: std::collections::VecDeque<u64> = in_degree.iter()
1001 .filter(|(_, °)| deg == 0)
1002 .map(|(&id, _)| id)
1003 .collect();
1004 let mut order = Vec::new();
1005 while let Some(id) = queue.pop_front() {
1006 order.push(id);
1007 if let Some(neighbors) = adjacency.get(&id) {
1008 for &next in neighbors {
1009 let deg = in_degree.entry(next).or_insert(0);
1010 if *deg > 0 { *deg -= 1; }
1011 if *deg == 0 { queue.push_back(next); }
1012 }
1013 }
1014 }
1015 order
1016 }
1017
1018 pub fn generate_hlsl(&self) -> String {
1019 let order = self.topological_order();
1020 let mut lines = Vec::new();
1021 lines.push("// Auto-generated VFX HLSL".to_string());
1022 lines.push("struct ParticleData {".to_string());
1023 lines.push(" float3 position;".to_string());
1024 lines.push(" float3 velocity;".to_string());
1025 lines.push(" float4 color;".to_string());
1026 lines.push(" float size;".to_string());
1027 lines.push(" float age;".to_string());
1028 lines.push(" float lifetime;".to_string());
1029 lines.push("};".to_string());
1030 lines.push(String::new());
1031 lines.push("[numthreads(64, 1, 1)]".to_string());
1032 lines.push("void VFXMain(uint id : SV_DispatchThreadID) {".to_string());
1033 lines.push(" ParticleData p = particleBuffer[id];".to_string());
1034 for &node_id in &order {
1035 if let Some(node) = self.find_node(node_id) {
1036 let snippet = self.node_to_hlsl(node);
1037 if !snippet.is_empty() {
1038 lines.push(format!(" // {}", node.label()));
1039 lines.push(format!(" {}", snippet));
1040 }
1041 }
1042 }
1043 lines.push(" particleBuffer[id] = p;".to_string());
1044 lines.push("}".to_string());
1045 lines.join("\n")
1046 }
1047
1048 fn node_to_hlsl(&self, node: &VfxNode) -> String {
1049 match node.kind {
1050 VfxNodeKind::Gravity => {
1051 let g = node.get_param("Gravity")
1052 .map(|v| v.as_float3())
1053 .unwrap_or(Vec3::new(0.0, -9.81, 0.0));
1054 format!("p.velocity += float3({}, {}, {}) * deltaTime;", g.x, g.y, g.z)
1055 }
1056 VfxNodeKind::Drag => {
1057 let drag = node.get_param("Drag")
1058 .map(|v| v.as_float())
1059 .unwrap_or(0.1);
1060 format!("p.velocity *= (1.0 - {:.4} * deltaTime);", drag)
1061 }
1062 VfxNodeKind::EulerIntegration => {
1063 "p.position += p.velocity * deltaTime;".to_string()
1064 }
1065 VfxNodeKind::ScaleOverLife => {
1066 "p.size = lerp(p.size, 0.0, saturate(p.age / p.lifetime));".to_string()
1067 }
1068 VfxNodeKind::AlphaOverLife => {
1069 "p.color.a = saturate(1.0 - p.age / p.lifetime);".to_string()
1070 }
1071 VfxNodeKind::CustomHlsl => node.custom_hlsl_code.clone(),
1072 _ => String::new(),
1073 }
1074 }
1075
1076 pub fn estimate_memory_bytes(&self) -> usize {
1077 self.particle_capacity as usize * 52
1079 }
1080
1081 pub fn build_fire_effect() -> Self {
1082 let mut graph = Self::new("Fire");
1083 graph.particle_capacity = 2048;
1084
1085 let spawn = graph.add_node(VfxNode::new(VfxNodeKind::SpawnContext, Vec2::new(20.0, 20.0)));
1086 let rate = graph.add_node(VfxNode::new(VfxNodeKind::ConstantRate, Vec2::new(20.0, 80.0)));
1087 if let Some(n) = graph.find_node_mut(rate) {
1088 n.set_param("Rate", VfxValue::Float(50.0));
1089 }
1090
1091 let init = graph.add_node(VfxNode::new(VfxNodeKind::InitializeContext, Vec2::new(300.0, 20.0)));
1092 let set_pos = graph.add_node(VfxNode::new(VfxNodeKind::SetPositionCone, Vec2::new(300.0, 80.0)));
1093 let set_life = graph.add_node(VfxNode::new(VfxNodeKind::SetLifetime, Vec2::new(300.0, 140.0)));
1094 if let Some(n) = graph.find_node_mut(set_life) {
1095 n.set_param("Lifetime", VfxValue::Float(1.5));
1096 }
1097 let set_color = graph.add_node(VfxNode::new(VfxNodeKind::SetColor, Vec2::new(300.0, 200.0)));
1098 if let Some(n) = graph.find_node_mut(set_color) {
1099 n.set_param("Color", VfxValue::Color(Vec4::new(1.0, 0.4, 0.0, 1.0)));
1100 }
1101
1102 let update = graph.add_node(VfxNode::new(VfxNodeKind::UpdateContext, Vec2::new(580.0, 20.0)));
1103 let gravity_node = graph.add_node(VfxNode::new(VfxNodeKind::Gravity, Vec2::new(580.0, 80.0)));
1104 if let Some(n) = graph.find_node_mut(gravity_node) {
1105 n.set_param("Gravity", VfxValue::Float3(Vec3::new(0.0, -2.0, 0.0)));
1106 }
1107 let turbulence = graph.add_node(VfxNode::new(VfxNodeKind::Turbulence, Vec2::new(580.0, 140.0)));
1108 let color_life = graph.add_node(VfxNode::new(VfxNodeKind::ColorOverLife, Vec2::new(580.0, 200.0)));
1109 let alpha_life = graph.add_node(VfxNode::new(VfxNodeKind::AlphaOverLife, Vec2::new(580.0, 260.0)));
1110 let euler = graph.add_node(VfxNode::new(VfxNodeKind::EulerIntegration, Vec2::new(580.0, 320.0)));
1111
1112 let output = graph.add_node(VfxNode::new(VfxNodeKind::OutputParticleQuad, Vec2::new(860.0, 20.0)));
1113
1114 graph.connect(rate, 0, spawn, 0);
1116 graph.connect(spawn, 0, init, 0);
1118 graph.connect(init, 0, update, 0);
1119 graph.connect(update, 0, output, 0);
1120 graph.connect(set_pos, 0, init, 1);
1122 graph.connect(set_life, 0, init, 2);
1123 graph.connect(set_color, 0, init, 3);
1124 graph.connect(gravity_node, 0, update, 1);
1125 graph.connect(turbulence, 0, update, 2);
1126 graph.connect(color_life, 0, update, 3);
1127 graph.connect(alpha_life, 0, update, 4);
1128 graph.connect(euler, 0, update, 5);
1129
1130 graph
1131 }
1132
1133 pub fn build_smoke_effect() -> Self {
1134 let mut graph = Self::new("Smoke");
1135 graph.particle_capacity = 1024;
1136
1137 let spawn = graph.add_node(VfxNode::new(VfxNodeKind::SpawnContext, Vec2::new(20.0, 20.0)));
1138 let rate = graph.add_node(VfxNode::new(VfxNodeKind::ConstantRate, Vec2::new(20.0, 80.0)));
1139 if let Some(n) = graph.find_node_mut(rate) {
1140 n.set_param("Rate", VfxValue::Float(15.0));
1141 }
1142
1143 let init = graph.add_node(VfxNode::new(VfxNodeKind::InitializeContext, Vec2::new(300.0, 20.0)));
1144 let set_pos = graph.add_node(VfxNode::new(VfxNodeKind::SetPositionSphere, Vec2::new(300.0, 80.0)));
1145 let set_life = graph.add_node(VfxNode::new(VfxNodeKind::SetLifetime, Vec2::new(300.0, 140.0)));
1146 if let Some(n) = graph.find_node_mut(set_life) {
1147 n.set_param("Lifetime", VfxValue::Float(4.0));
1148 }
1149 let set_size = graph.add_node(VfxNode::new(VfxNodeKind::SetSize, Vec2::new(300.0, 200.0)));
1150 if let Some(n) = graph.find_node_mut(set_size) {
1151 n.set_param("Size", VfxValue::Float(0.5));
1152 }
1153 let set_color = graph.add_node(VfxNode::new(VfxNodeKind::SetColor, Vec2::new(300.0, 260.0)));
1154 if let Some(n) = graph.find_node_mut(set_color) {
1155 n.set_param("Color", VfxValue::Color(Vec4::new(0.6, 0.6, 0.6, 0.8)));
1156 }
1157
1158 let update = graph.add_node(VfxNode::new(VfxNodeKind::UpdateContext, Vec2::new(580.0, 20.0)));
1159 let gravity_node = graph.add_node(VfxNode::new(VfxNodeKind::Gravity, Vec2::new(580.0, 80.0)));
1160 if let Some(n) = graph.find_node_mut(gravity_node) {
1161 n.set_param("Gravity", VfxValue::Float3(Vec3::new(0.0, 0.5, 0.0)));
1162 }
1163 let scale_life = graph.add_node(VfxNode::new(VfxNodeKind::ScaleOverLife, Vec2::new(580.0, 140.0)));
1164 let alpha_life = graph.add_node(VfxNode::new(VfxNodeKind::AlphaOverLife, Vec2::new(580.0, 200.0)));
1165 let euler = graph.add_node(VfxNode::new(VfxNodeKind::EulerIntegration, Vec2::new(580.0, 260.0)));
1166
1167 let output = graph.add_node(VfxNode::new(VfxNodeKind::OutputParticleQuad, Vec2::new(860.0, 20.0)));
1168
1169 graph.connect(rate, 0, spawn, 0);
1170 graph.connect(spawn, 0, init, 0);
1171 graph.connect(init, 0, update, 0);
1172 graph.connect(update, 0, output, 0);
1173 graph.connect(set_pos, 0, init, 1);
1174 graph.connect(set_life, 0, init, 2);
1175 graph.connect(set_size, 0, init, 3);
1176 graph.connect(set_color, 0, init, 4);
1177 graph.connect(gravity_node, 0, update, 1);
1178 graph.connect(scale_life, 0, update, 2);
1179 graph.connect(alpha_life, 0, update, 3);
1180 graph.connect(euler, 0, update, 4);
1181
1182 graph
1183 }
1184
1185 pub fn build_sparks_effect() -> Self {
1186 let mut graph = Self::new("Sparks");
1187 graph.particle_capacity = 8192;
1188
1189 let spawn = graph.add_node(VfxNode::new(VfxNodeKind::SpawnContext, Vec2::new(20.0, 20.0)));
1190 let burst = graph.add_node(VfxNode::new(VfxNodeKind::BurstSpawn, Vec2::new(20.0, 80.0)));
1191 if let Some(n) = graph.find_node_mut(burst) {
1192 n.set_param("Count", VfxValue::Uint(200));
1193 }
1194
1195 let init = graph.add_node(VfxNode::new(VfxNodeKind::InitializeContext, Vec2::new(300.0, 20.0)));
1196 let set_pos = graph.add_node(VfxNode::new(VfxNodeKind::SetPositionShape, Vec2::new(300.0, 80.0)));
1197 let set_vel = graph.add_node(VfxNode::new(VfxNodeKind::SetVelocityRandom, Vec2::new(300.0, 140.0)));
1198 let set_life = graph.add_node(VfxNode::new(VfxNodeKind::SetLifetime, Vec2::new(300.0, 200.0)));
1199 if let Some(n) = graph.find_node_mut(set_life) {
1200 n.set_param("Lifetime", VfxValue::Float(0.8));
1201 }
1202 let set_size = graph.add_node(VfxNode::new(VfxNodeKind::SetSize, Vec2::new(300.0, 260.0)));
1203 if let Some(n) = graph.find_node_mut(set_size) {
1204 n.set_param("Size", VfxValue::Float(0.05));
1205 }
1206 let set_color = graph.add_node(VfxNode::new(VfxNodeKind::SetColor, Vec2::new(300.0, 320.0)));
1207 if let Some(n) = graph.find_node_mut(set_color) {
1208 n.set_param("Color", VfxValue::Color(Vec4::new(1.0, 0.9, 0.3, 1.0)));
1209 }
1210
1211 let update = graph.add_node(VfxNode::new(VfxNodeKind::UpdateContext, Vec2::new(580.0, 20.0)));
1212 let gravity_node = graph.add_node(VfxNode::new(VfxNodeKind::Gravity, Vec2::new(580.0, 80.0)));
1213 let drag_node = graph.add_node(VfxNode::new(VfxNodeKind::Drag, Vec2::new(580.0, 140.0)));
1214 if let Some(n) = graph.find_node_mut(drag_node) {
1215 n.set_param("Drag", VfxValue::Float(2.0));
1216 }
1217 let alpha_life = graph.add_node(VfxNode::new(VfxNodeKind::AlphaOverLife, Vec2::new(580.0, 200.0)));
1218 let euler = graph.add_node(VfxNode::new(VfxNodeKind::EulerIntegration, Vec2::new(580.0, 260.0)));
1219
1220 let output = graph.add_node(VfxNode::new(VfxNodeKind::OutputParticleStrip, Vec2::new(860.0, 20.0)));
1221
1222 graph.connect(burst, 0, spawn, 0);
1223 graph.connect(spawn, 0, init, 0);
1224 graph.connect(init, 0, update, 0);
1225 graph.connect(update, 0, output, 0);
1226 graph.connect(set_pos, 0, init, 1);
1227 graph.connect(set_vel, 0, init, 2);
1228 graph.connect(set_life, 0, init, 3);
1229 graph.connect(set_size, 0, init, 4);
1230 graph.connect(set_color, 0, init, 5);
1231 graph.connect(gravity_node, 0, update, 1);
1232 graph.connect(drag_node, 0, update, 2);
1233 graph.connect(alpha_life, 0, update, 3);
1234 graph.connect(euler, 0, update, 4);
1235
1236 graph
1237 }
1238}
1239
1240#[derive(Debug, Clone, Copy, PartialEq)]
1245pub enum VfxEditorTool {
1246 Select,
1247 Connect,
1248 Comment,
1249 Pan,
1250}
1251
1252#[derive(Debug, Clone, Copy, PartialEq)]
1253pub enum VfxEditorPanel {
1254 Graph,
1255 Inspector,
1256 Preview,
1257 Blackboard,
1258 Profiler,
1259}
1260
1261#[derive(Debug, Clone)]
1262pub struct VfxEditorView {
1263 pub zoom: f32,
1264 pub pan: Vec2,
1265 pub min_zoom: f32,
1266 pub max_zoom: f32,
1267 pub grid_size: f32,
1268 pub show_grid: bool,
1269 pub show_node_minimap: bool,
1270}
1271
1272impl Default for VfxEditorView {
1273 fn default() -> Self {
1274 Self {
1275 zoom: 1.0,
1276 pan: Vec2::ZERO,
1277 min_zoom: 0.1,
1278 max_zoom: 4.0,
1279 grid_size: 20.0,
1280 show_grid: true,
1281 show_node_minimap: true,
1282 }
1283 }
1284}
1285
1286impl VfxEditorView {
1287 pub fn screen_to_graph(&self, screen: Vec2) -> Vec2 {
1288 (screen - self.pan) / self.zoom
1289 }
1290
1291 pub fn graph_to_screen(&self, graph: Vec2) -> Vec2 {
1292 graph * self.zoom + self.pan
1293 }
1294
1295 pub fn zoom_at(&mut self, focus: Vec2, delta: f32) {
1296 let old_zoom = self.zoom;
1297 self.zoom = (self.zoom * (1.0 + delta * 0.1)).clamp(self.min_zoom, self.max_zoom);
1298 let scale = self.zoom / old_zoom;
1299 self.pan = focus + (self.pan - focus) * scale;
1300 }
1301
1302 pub fn snap_to_grid(&self, pos: Vec2) -> Vec2 {
1303 let g = self.grid_size;
1304 Vec2::new((pos.x / g).round() * g, (pos.y / g).round() * g)
1305 }
1306
1307 pub fn fit_to_graph(&mut self, nodes: &[VfxNode], viewport_size: Vec2) {
1308 if nodes.is_empty() { return; }
1309 let min_x = nodes.iter().map(|n| n.position.x).fold(f32::MAX, f32::min);
1310 let min_y = nodes.iter().map(|n| n.position.y).fold(f32::MAX, f32::min);
1311 let max_x = nodes.iter().map(|n| n.position.x + n.size.x).fold(f32::MIN, f32::max);
1312 let max_y = nodes.iter().map(|n| n.position.y + n.size.y).fold(f32::MIN, f32::max);
1313 let content_w = max_x - min_x + 80.0;
1314 let content_h = max_y - min_y + 80.0;
1315 let scale_x = viewport_size.x / content_w;
1316 let scale_y = viewport_size.y / content_h;
1317 self.zoom = scale_x.min(scale_y).clamp(self.min_zoom, self.max_zoom);
1318 let center = Vec2::new((min_x + max_x) * 0.5, (min_y + max_y) * 0.5);
1319 self.pan = viewport_size * 0.5 - center * self.zoom;
1320 }
1321}
1322
1323#[derive(Debug, Clone)]
1328pub enum VfxDragState {
1329 None,
1330 DraggingNode { node_id: u64, offset: Vec2 },
1331 DraggingConnection { from_node: u64, from_port: usize, is_input: bool, current_pos: Vec2 },
1332 BoxSelect { start: Vec2, current: Vec2 },
1333 Panning { last: Vec2 },
1334}
1335
1336#[derive(Debug, Clone, Default)]
1341pub struct VfxProfilerSample {
1342 pub graph_name: String,
1343 pub spawn_ms: f32,
1344 pub init_ms: f32,
1345 pub update_ms: f32,
1346 pub render_ms: f32,
1347 pub particle_count: u32,
1348 pub alive_count: u32,
1349 pub memory_bytes: usize,
1350 pub draw_calls: u32,
1351}
1352
1353impl VfxProfilerSample {
1354 pub fn total_ms(&self) -> f32 {
1355 self.spawn_ms + self.init_ms + self.update_ms + self.render_ms
1356 }
1357
1358 pub fn memory_kb(&self) -> f32 {
1359 self.memory_bytes as f32 / 1024.0
1360 }
1361}
1362
1363#[derive(Debug, Clone)]
1364pub struct VfxProfiler {
1365 pub samples: Vec<VfxProfilerSample>,
1366 pub history_len: usize,
1367 pub paused: bool,
1368}
1369
1370impl Default for VfxProfiler {
1371 fn default() -> Self {
1372 Self {
1373 samples: Vec::new(),
1374 history_len: 128,
1375 paused: false,
1376 }
1377 }
1378}
1379
1380impl VfxProfiler {
1381 pub fn push_sample(&mut self, sample: VfxProfilerSample) {
1382 if self.paused { return; }
1383 self.samples.push(sample);
1384 if self.samples.len() > self.history_len {
1385 self.samples.remove(0);
1386 }
1387 }
1388
1389 pub fn avg_total_ms(&self) -> f32 {
1390 if self.samples.is_empty() { return 0.0; }
1391 self.samples.iter().map(|s| s.total_ms()).sum::<f32>() / self.samples.len() as f32
1392 }
1393
1394 pub fn peak_particle_count(&self) -> u32 {
1395 self.samples.iter().map(|s| s.alive_count).max().unwrap_or(0)
1396 }
1397}
1398
1399#[derive(Debug, Clone)]
1404pub struct VfxBlackboardEntry {
1405 pub name: String,
1406 pub value: VfxValue,
1407 pub description: String,
1408 pub exposed: bool,
1409}
1410
1411#[derive(Debug, Clone, Default)]
1412pub struct VfxBlackboard {
1413 pub entries: Vec<VfxBlackboardEntry>,
1414}
1415
1416impl VfxBlackboard {
1417 pub fn add(&mut self, name: &str, value: VfxValue) {
1418 self.entries.push(VfxBlackboardEntry {
1419 name: name.to_string(),
1420 value,
1421 description: String::new(),
1422 exposed: false,
1423 });
1424 }
1425
1426 pub fn get(&self, name: &str) -> Option<&VfxValue> {
1427 self.entries.iter().find(|e| e.name == name).map(|e| &e.value)
1428 }
1429
1430 pub fn set(&mut self, name: &str, value: VfxValue) {
1431 if let Some(e) = self.entries.iter_mut().find(|e| e.name == name) {
1432 e.value = value;
1433 } else {
1434 self.add(name, value);
1435 }
1436 }
1437}
1438
1439#[derive(Debug, Clone)]
1444pub struct VfxInstance {
1445 pub graph_name: String,
1446 pub position: Vec3,
1447 pub rotation: Quat,
1448 pub scale: Vec3,
1449 pub playing: bool,
1450 pub looping: bool,
1451 pub play_time: f32,
1452 pub duration: f32,
1453 pub speed: f32,
1454 pub prewarm: bool,
1455 pub seed: u32,
1456 pub attached_to_entity: Option<u64>,
1457}
1458
1459impl VfxInstance {
1460 pub fn new(graph_name: &str) -> Self {
1461 Self {
1462 graph_name: graph_name.to_string(),
1463 position: Vec3::ZERO,
1464 rotation: Quat::IDENTITY,
1465 scale: Vec3::ONE,
1466 playing: false,
1467 looping: true,
1468 play_time: 0.0,
1469 duration: 5.0,
1470 speed: 1.0,
1471 prewarm: false,
1472 seed: 12345,
1473 attached_to_entity: None,
1474 }
1475 }
1476
1477 pub fn play(&mut self) { self.playing = true; }
1478 pub fn stop(&mut self) { self.playing = false; self.play_time = 0.0; }
1479 pub fn pause(&mut self) { self.playing = false; }
1480
1481 pub fn update(&mut self, dt: f32) {
1482 if !self.playing { return; }
1483 self.play_time += dt * self.speed;
1484 if self.looping && self.play_time >= self.duration {
1485 self.play_time -= self.duration;
1486 } else if self.play_time >= self.duration {
1487 self.playing = false;
1488 self.play_time = self.duration;
1489 }
1490 }
1491
1492 pub fn normalized_time(&self) -> f32 {
1493 if self.duration > 0.0 { (self.play_time / self.duration).clamp(0.0, 1.0) } else { 0.0 }
1494 }
1495
1496 pub fn transform_matrix(&self) -> Mat4 {
1497 Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
1498 }
1499}
1500
1501#[derive(Debug)]
1506pub struct VfxGraphEditor {
1507 pub graphs: Vec<VfxGraph>,
1508 pub active_graph: usize,
1509 pub view: VfxEditorView,
1510 pub tool: VfxEditorTool,
1511 pub active_panel: VfxEditorPanel,
1512 pub selected_nodes: Vec<u64>,
1513 pub clipboard: Vec<VfxNode>,
1514 pub drag_state: VfxDragState,
1515 pub hovered_node: Option<u64>,
1516 pub search_query: String,
1517 pub show_block_library: bool,
1518 pub blackboard: VfxBlackboard,
1519 pub profiler: VfxProfiler,
1520 pub instances: Vec<VfxInstance>,
1521 pub preview_playing: bool,
1522 pub preview_time: f32,
1523 pub show_grid: bool,
1524 pub snap_to_grid: bool,
1525 pub history: Vec<Vec<VfxNode>>,
1526 pub history_pos: usize,
1527 pub category_filter: Option<String>,
1528 pub show_warnings: bool,
1529}
1530
1531impl Default for VfxGraphEditor {
1532 fn default() -> Self {
1533 let fire = VfxGraph::build_fire_effect();
1534 let smoke = VfxGraph::build_smoke_effect();
1535 let sparks = VfxGraph::build_sparks_effect();
1536
1537 let mut blackboard = VfxBlackboard::default();
1538 blackboard.add("DeltaTime", VfxValue::Float(0.016));
1539 blackboard.add("ElapsedTime", VfxValue::Float(0.0));
1540 blackboard.add("Gravity", VfxValue::Float3(Vec3::new(0.0, -9.81, 0.0)));
1541
1542 let instances = vec![
1543 VfxInstance::new("Fire"),
1544 VfxInstance::new("Smoke"),
1545 VfxInstance::new("Sparks"),
1546 ];
1547
1548 Self {
1549 graphs: vec![fire, smoke, sparks],
1550 active_graph: 0,
1551 view: VfxEditorView::default(),
1552 tool: VfxEditorTool::Select,
1553 active_panel: VfxEditorPanel::Graph,
1554 selected_nodes: Vec::new(),
1555 clipboard: Vec::new(),
1556 drag_state: VfxDragState::None,
1557 hovered_node: None,
1558 search_query: String::new(),
1559 show_block_library: false,
1560 blackboard,
1561 profiler: VfxProfiler::default(),
1562 instances,
1563 preview_playing: false,
1564 preview_time: 0.0,
1565 show_grid: true,
1566 snap_to_grid: true,
1567 history: Vec::new(),
1568 history_pos: 0,
1569 category_filter: None,
1570 show_warnings: true,
1571 }
1572 }
1573}
1574
1575impl VfxGraphEditor {
1576 pub fn active_graph(&self) -> &VfxGraph {
1577 &self.graphs[self.active_graph]
1578 }
1579
1580 pub fn active_graph_mut(&mut self) -> &mut VfxGraph {
1581 &mut self.graphs[self.active_graph]
1582 }
1583
1584 pub fn snapshot(&mut self) {
1585 let nodes = self.active_graph().nodes.clone();
1586 self.history.truncate(self.history_pos);
1587 self.history.push(nodes);
1588 self.history_pos = self.history.len();
1589 }
1590
1591 pub fn undo(&mut self) {
1592 if self.history_pos > 1 {
1593 self.history_pos -= 1;
1594 let nodes = self.history[self.history_pos - 1].clone();
1595 self.active_graph_mut().nodes = nodes;
1596 }
1597 }
1598
1599 pub fn redo(&mut self) {
1600 if self.history_pos < self.history.len() {
1601 let nodes = self.history[self.history_pos].clone();
1602 self.active_graph_mut().nodes = nodes;
1603 self.history_pos += 1;
1604 }
1605 }
1606
1607 pub fn select_node(&mut self, id: u64) {
1608 self.selected_nodes = vec![id];
1609 }
1610
1611 pub fn add_to_selection(&mut self, id: u64) {
1612 if !self.selected_nodes.contains(&id) {
1613 self.selected_nodes.push(id);
1614 }
1615 }
1616
1617 pub fn deselect_all(&mut self) {
1618 self.selected_nodes.clear();
1619 }
1620
1621 pub fn delete_selected(&mut self) {
1622 self.snapshot();
1623 let ids = self.selected_nodes.clone();
1624 for id in ids {
1625 self.active_graph_mut().remove_node(id);
1626 }
1627 self.selected_nodes.clear();
1628 }
1629
1630 pub fn copy_selected(&mut self) {
1631 let ids: std::collections::HashSet<u64> = self.selected_nodes.iter().copied().collect();
1632 self.clipboard = self.active_graph().nodes.iter()
1633 .filter(|n| ids.contains(&n.id))
1634 .cloned()
1635 .collect();
1636 }
1637
1638 pub fn paste(&mut self) {
1639 self.snapshot();
1640 let nodes_to_paste: Vec<VfxNode> = self.clipboard.clone();
1641 let graph = self.active_graph_mut();
1642 let mut new_ids = Vec::new();
1643 for mut node in nodes_to_paste {
1644 node.id = next_vfx_node_id();
1645 node.position += Vec2::new(20.0, 20.0);
1646 let id = node.id;
1647 graph.nodes.push(node);
1648 new_ids.push(id);
1649 }
1650 self.selected_nodes = new_ids;
1651 }
1652
1653 pub fn duplicate_selected(&mut self) {
1654 self.copy_selected();
1655 self.paste();
1656 }
1657
1658 pub fn add_node_at(&mut self, kind: VfxNodeKind, pos: Vec2) -> u64 {
1659 self.snapshot();
1660 let graph_pos = if self.snap_to_grid {
1661 self.view.snap_to_grid(pos)
1662 } else {
1663 pos
1664 };
1665 let node = VfxNode::new(kind, graph_pos);
1666 self.active_graph_mut().add_node(node)
1667 }
1668
1669 pub fn hit_test(&self, screen_pos: Vec2) -> Option<u64> {
1670 let graph_pos = self.view.screen_to_graph(screen_pos);
1671 for node in self.active_graph().nodes.iter().rev() {
1673 if node.contains_point(graph_pos) {
1674 return Some(node.id);
1675 }
1676 }
1677 None
1678 }
1679
1680 pub fn begin_drag_node(&mut self, node_id: u64, screen_pos: Vec2) {
1681 let graph_pos = self.view.screen_to_graph(screen_pos);
1682 if let Some(node) = self.active_graph().find_node(node_id) {
1683 let offset = node.position - graph_pos;
1684 self.drag_state = VfxDragState::DraggingNode { node_id, offset };
1685 }
1686 }
1687
1688 pub fn update_drag(&mut self, screen_pos: Vec2) {
1689 let graph_pos = self.view.screen_to_graph(screen_pos);
1690 match self.drag_state.clone() {
1691 VfxDragState::DraggingNode { node_id, offset } => {
1692 let new_pos = if self.snap_to_grid {
1693 self.view.snap_to_grid(graph_pos + offset)
1694 } else {
1695 graph_pos + offset
1696 };
1697 if let Some(node) = self.active_graph_mut().find_node_mut(node_id) {
1698 node.position = new_pos;
1699 }
1700 }
1701 VfxDragState::Panning { last } => {
1702 self.view.pan += screen_pos - last;
1703 self.drag_state = VfxDragState::Panning { last: screen_pos };
1704 }
1705 VfxDragState::BoxSelect { start, .. } => {
1706 self.drag_state = VfxDragState::BoxSelect { start, current: graph_pos };
1707 }
1708 VfxDragState::DraggingConnection { from_node, from_port, is_input, .. } => {
1709 self.drag_state = VfxDragState::DraggingConnection {
1710 from_node, from_port, is_input, current_pos: screen_pos,
1711 };
1712 }
1713 _ => {}
1714 }
1715 }
1716
1717 pub fn end_drag(&mut self) {
1718 if let VfxDragState::BoxSelect { start, current } = &self.drag_state.clone() {
1719 let min = Vec2::new(start.x.min(current.x), start.y.min(current.y));
1720 let max = Vec2::new(start.x.max(current.x), start.y.max(current.y));
1721 let newly_selected: Vec<u64> = self.active_graph().nodes.iter()
1722 .filter(|n| {
1723 let cx = n.position.x + n.size.x * 0.5;
1724 let cy = n.position.y + n.size.y * 0.5;
1725 cx >= min.x && cx <= max.x && cy >= min.y && cy <= max.y
1726 })
1727 .map(|n| n.id)
1728 .collect();
1729 self.selected_nodes = newly_selected;
1730 }
1731 self.drag_state = VfxDragState::None;
1732 }
1733
1734 pub fn zoom(&mut self, screen_focus: Vec2, delta: f32) {
1735 self.view.zoom_at(screen_focus, delta);
1736 }
1737
1738 pub fn frame_all(&mut self, viewport: Vec2) {
1739 let nodes = self.active_graph().nodes.clone();
1740 self.view.fit_to_graph(&nodes, viewport);
1741 }
1742
1743 pub fn search_blocks(&self, query: &str) -> Vec<VfxNodeKind> {
1744 let q = query.to_lowercase();
1745 let all: Vec<VfxNodeKind> = vec![
1746 VfxNodeKind::ConstantRate, VfxNodeKind::BurstSpawn, VfxNodeKind::SetLifetime,
1747 VfxNodeKind::SetSize, VfxNodeKind::SetColor, VfxNodeKind::SetVelocityRandom,
1748 VfxNodeKind::Gravity, VfxNodeKind::Drag, VfxNodeKind::Turbulence,
1749 VfxNodeKind::EulerIntegration, VfxNodeKind::ColorOverLife, VfxNodeKind::AlphaOverLife,
1750 VfxNodeKind::ScaleOverLife, VfxNodeKind::Add, VfxNodeKind::Multiply,
1751 VfxNodeKind::Lerp, VfxNodeKind::Clamp, VfxNodeKind::Noise3D, VfxNodeKind::CurlNoise,
1752 VfxNodeKind::GetBuiltin, VfxNodeKind::SetAttribute, VfxNodeKind::Branch,
1753 VfxNodeKind::SampleGradient, VfxNodeKind::SampleCurve, VfxNodeKind::CustomHlsl,
1754 ];
1755 if q.is_empty() {
1756 all
1757 } else {
1758 all.into_iter().filter(|k| {
1759 k.label().to_lowercase().contains(&q) || k.category().to_lowercase().contains(&q)
1760 }).collect()
1761 }
1762 }
1763
1764 pub fn update_preview(&mut self, dt: f32) {
1765 if self.preview_playing {
1766 self.preview_time += dt;
1767 for inst in &mut self.instances {
1768 inst.update(dt);
1769 }
1770 let sample = VfxProfilerSample {
1772 graph_name: self.active_graph().name.clone(),
1773 spawn_ms: 0.05 + (self.preview_time.sin() * 0.01).abs(),
1774 init_ms: 0.02,
1775 update_ms: 0.4 + (self.preview_time.cos() * 0.05).abs(),
1776 render_ms: 0.3,
1777 particle_count: self.active_graph().particle_capacity,
1778 alive_count: (self.active_graph().particle_capacity as f32 * 0.7) as u32,
1779 memory_bytes: self.active_graph().estimate_memory_bytes(),
1780 draw_calls: 2,
1781 };
1782 self.profiler.push_sample(sample);
1783 }
1784 }
1785
1786 pub fn validate_active(&mut self) -> Vec<String> {
1787 self.active_graph_mut().validate()
1788 }
1789
1790 pub fn generate_hlsl_for_active(&self) -> String {
1791 self.active_graph().generate_hlsl()
1792 }
1793}