@forgeax/engine-vfx-render 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +55 -10
  2. package/dist/__tests__/persistent-ticks.integration.test.d.ts +2 -0
  3. package/dist/__tests__/persistent-ticks.integration.test.d.ts.map +1 -0
  4. package/dist/feature/event-resources.d.ts +10 -3
  5. package/dist/feature/event-resources.d.ts.map +1 -1
  6. package/dist/feature/gpu-particle-feature.d.ts +18 -5
  7. package/dist/feature/gpu-particle-feature.d.ts.map +1 -1
  8. package/dist/feature/particle-resources.d.ts +43 -6
  9. package/dist/feature/particle-resources.d.ts.map +1 -1
  10. package/dist/host/data-interface-providers.d.ts +4 -1
  11. package/dist/host/data-interface-providers.d.ts.map +1 -1
  12. package/dist/host/vfx-runtime-host.d.ts +21 -2
  13. package/dist/host/vfx-runtime-host.d.ts.map +1 -1
  14. package/dist/index.d.ts +4 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.mjs +1027 -307
  17. package/dist/index.mjs.map +1 -1
  18. package/package.json +11 -9
  19. package/src/__tests__/billboard-advanced.integration.test.ts +1 -1
  20. package/src/__tests__/data-interface-providers.unit.test.ts +4 -0
  21. package/src/__tests__/data-interface-public-api.test-d.ts +1 -0
  22. package/src/__tests__/gpu-host.integration.test.ts +1471 -59
  23. package/src/__tests__/particle-resources.unit.test.ts +75 -0
  24. package/src/__tests__/persistent-ticks.integration.test.ts +162 -0
  25. package/src/__tests__/render-vocabulary-owner.test-d.ts +7 -7
  26. package/src/feature/event-resources.ts +18 -4
  27. package/src/feature/gpu-particle-feature.ts +1153 -376
  28. package/src/feature/particle-resources.ts +223 -11
  29. package/src/host/data-interface-providers.ts +19 -0
  30. package/src/host/vfx-runtime-host.ts +82 -2
  31. package/src/index.ts +8 -0
  32. package/src/shaders/beam-inputs.wgsl +40 -0
  33. package/src/shaders/beam.wgsl +4 -3
  34. package/src/shaders/billboard-inputs.wgsl +85 -0
  35. package/src/shaders/mesh-inputs.wgsl +95 -0
  36. package/src/shaders/mesh-shadow.wgsl +18 -0
  37. package/src/shaders/mesh.wgsl +64 -40
  38. package/src/shaders/ribbon-inputs.wgsl +39 -0
  39. package/src/shaders/trail-inputs.wgsl +45 -0
  40. package/src/shaders/trail.wgsl +9 -3
@@ -0,0 +1,95 @@
1
+ #define_import_path forgeax::vfx-render.particles.mesh-inputs
2
+ #import forgeax_view::common::{View, view}
3
+ #import forgeax_scene_temporal::{sceneViewZ}
4
+ #import forgeax_material::standard_surface::{VsOut, StandardSurfaceFactors, evaluateStandardSurface, material}
5
+ #import forgeax_material::standard_surface::{materialTextureFilteringWitness}
6
+
7
+ #pragma variant_axis STORAGE_BUFFER_AVAILABLE
8
+ #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
9
+ #pragma variant_axis EXTENDED_LIGHTING_AVAILABLE
10
+ #pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE
11
+ #pragma variant_axis PROJECTOR_AVAILABLE
12
+
13
+ // Retain the full declared material ABI in reflection, including inactive inputs.
14
+ fn materialInterfaceWitness() { materialTextureFilteringWitness(); }
15
+
16
+ struct VertexOutput {
17
+ @builtin(position) position: vec4<f32>,
18
+ @location(0) color: vec4<f32>,
19
+ @location(1) normal: vec3<f32>,
20
+ @location(2) @interpolate(flat) render_controls: vec2<f32>,
21
+ @location(4) world_position: vec3<f32>,
22
+ @location(5) uv: vec2<f32>,
23
+ @location(6) tangent: vec4<f32>,
24
+ };
25
+
26
+ struct VertexInput {
27
+ @location(0) geometry_position: vec3<f32>,
28
+ @location(1) geometry_normal: vec3<f32>,
29
+ @location(2) geometry_uv: vec2<f32>,
30
+ @location(3) geometry_tangent: vec4<f32>,
31
+ @location(4) center: vec3<f32>,
32
+ @location(5) right: vec3<f32>,
33
+ @location(6) up: vec3<f32>,
34
+ @location(7) forward: vec3<f32>,
35
+ @location(8) particle_color: vec4<f32>,
36
+ @location(9) render_controls: vec2<f32>,
37
+ @location(10) particle_inputs: vec4<f32>,
38
+ };
39
+
40
+ fn safeNormalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
41
+ let magnitude = length(value);
42
+ if (magnitude > 0.000001) { return value / magnitude; }
43
+ return fallback;
44
+ }
45
+
46
+ @vertex
47
+ fn vs_main(input: VertexInput) -> VertexOutput {
48
+ var output: VertexOutput;
49
+ output.world_position = input.center + input.right * input.geometry_position.x +
50
+ input.up * input.geometry_position.y + input.forward * input.geometry_position.z;
51
+ output.position = view.worldViewProj * vec4<f32>(output.world_position, 1.0);
52
+ let heat = clamp(input.particle_inputs.x, 0.0, 1.0);
53
+ output.color = input.particle_color * (0.35 + 0.65 * heat);
54
+ let normalBasisX = cross(input.up, input.forward);
55
+ let normalBasisY = cross(input.forward, input.right);
56
+ let normalBasisZ = cross(input.right, input.up);
57
+ // Cofactors require determinant sign for reflected/non-uniform Scale3.
58
+ let handedness = select(-1.0, 1.0, dot(input.right, normalBasisX) >= 0.0);
59
+ output.normal = safeNormalize(handedness * (
60
+ normalBasisX * input.geometry_normal.x + normalBasisY * input.geometry_normal.y +
61
+ normalBasisZ * input.geometry_normal.z), vec3<f32>(0.0, 1.0, 0.0));
62
+ let tangent = input.right * input.geometry_tangent.x + input.up * input.geometry_tangent.y +
63
+ input.forward * input.geometry_tangent.z;
64
+ output.tangent = vec4<f32>(safeNormalize(tangent - output.normal * dot(output.normal, tangent),
65
+ vec3<f32>(1.0, 0.0, 0.0)), input.geometry_tangent.w * handedness);
66
+ output.uv = input.geometry_uv;
67
+ output.render_controls = input.render_controls;
68
+ return output;
69
+ }
70
+
71
+ @fragment
72
+ fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
73
+ var surface: VsOut;
74
+ let clip = view.worldViewProj * vec4<f32>(input.world_position, 1.0);
75
+ surface.clip = clip;
76
+ surface.worldPos = input.world_position;
77
+ surface.worldNormal = input.normal;
78
+ surface.worldTangent = input.tangent;
79
+ surface.uv = input.uv;
80
+ surface.uvOne = input.uv;
81
+ surface.uvTwo = input.uv;
82
+ surface.uvThree = input.uv;
83
+ surface.uvFour = input.uv;
84
+ surface.uvFive = input.uv;
85
+ surface.uvSix = input.uv;
86
+ surface.uvSeven = input.uv;
87
+ surface.ndc = vec4<f32>(clip.xyz / clip.w, 0.0);
88
+ surface.viewZ = sceneViewZ(clip, view.temporalProjection);
89
+ return evaluateStandardSurface(surface, StandardSurfaceFactors(
90
+ input.color * material.baseColor, material.metallic, material.roughness,
91
+ material.emissive, material.emissiveIntensity,
92
+ material.clearcoat, material.clearcoatRoughness,
93
+ input.render_controls.x != 0.0, input.render_controls.y != 0.0,
94
+ )).color;
95
+ }
@@ -0,0 +1,18 @@
1
+ #define_import_path forgeax::vfx-render.particles.mesh-shadow
2
+ #pragma variant_axis STORAGE_BUFFER_AVAILABLE
3
+ #import forgeax_shadow::surface::{projectShadowPosition}
4
+
5
+ struct MeshShadowInput {
6
+ @location(0) position: vec3<f32>,
7
+ @location(4) center: vec3<f32>,
8
+ @location(5) right: vec3<f32>,
9
+ @location(6) up: vec3<f32>,
10
+ @location(7) forward: vec3<f32>,
11
+ };
12
+
13
+ @vertex
14
+ fn vs_main(input: MeshShadowInput) -> @builtin(position) vec4<f32> {
15
+ let worldPosition = input.center + input.right * input.position.x +
16
+ input.up * input.position.y + input.forward * input.position.z;
17
+ return projectShadowPosition(vec4<f32>(worldPosition, 1.0));
18
+ }
@@ -1,12 +1,26 @@
1
1
  #define_import_path forgeax::vfx-render.particles.mesh
2
+ #import forgeax_view::common::{View, view}
3
+ #import forgeax_scene_temporal::{sceneViewZ}
4
+ #import forgeax_material::standard_surface::{VsOut, StandardSurfaceFactors, evaluateStandardSurface, material}
5
+ #import forgeax_material::standard_surface::{materialTextureFilteringWitness}
6
+
7
+ #pragma variant_axis STORAGE_BUFFER_AVAILABLE
8
+ #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
9
+ #pragma variant_axis EXTENDED_LIGHTING_AVAILABLE
10
+ #pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE
11
+ #pragma variant_axis PROJECTOR_AVAILABLE
12
+
13
+ // Retain the full declared material ABI in reflection, including inactive inputs.
14
+ fn materialInterfaceWitness() { materialTextureFilteringWitness(); }
2
15
 
3
16
  struct VertexOutput {
4
17
  @builtin(position) position: vec4<f32>,
5
18
  @location(0) color: vec4<f32>,
6
19
  @location(1) normal: vec3<f32>,
7
- @location(2) emissive_intensity: vec4<f32>,
8
- @location(3) surface: vec4<f32>,
9
- @location(4) clip_position: vec3<f32>,
20
+ @location(2) @interpolate(flat) render_controls: vec2<f32>,
21
+ @location(4) world_position: vec3<f32>,
22
+ @location(5) uv: vec2<f32>,
23
+ @location(6) tangent: vec4<f32>,
10
24
  };
11
25
 
12
26
  struct VertexInput {
@@ -19,51 +33,61 @@ struct VertexInput {
19
33
  @location(6) up: vec3<f32>,
20
34
  @location(7) forward: vec3<f32>,
21
35
  @location(8) particle_color: vec4<f32>,
22
- @location(9) base_color: vec4<f32>,
23
- @location(10) emissive_intensity: vec4<f32>,
24
- @location(11) surface: vec4<f32>,
36
+ @location(9) render_controls: vec2<f32>,
25
37
  };
26
38
 
39
+ fn safeNormalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
40
+ let magnitude = length(value);
41
+ if (magnitude > 0.000001) { return value / magnitude; }
42
+ return fallback;
43
+ }
44
+
27
45
  @vertex
28
46
  fn vs_main(input: VertexInput) -> VertexOutput {
29
47
  var output: VertexOutput;
30
- let offset =
31
- input.right * input.geometry_position.x +
32
- input.up * input.geometry_position.y +
33
- input.forward * input.geometry_position.z;
34
- let clipPosition = input.center + offset;
35
- output.position = vec4<f32>(clipPosition, 1.0);
36
- output.clip_position = clipPosition;
37
- output.color = input.particle_color * input.base_color;
38
- output.normal = normalize(
39
- input.right * input.geometry_normal.x +
40
- input.up * input.geometry_normal.y +
41
- input.forward * input.geometry_normal.z
42
- );
43
- output.emissive_intensity = input.emissive_intensity;
44
- output.surface = input.surface;
48
+ output.world_position = input.center + input.right * input.geometry_position.x +
49
+ input.up * input.geometry_position.y + input.forward * input.geometry_position.z;
50
+ output.position = view.worldViewProj * vec4<f32>(output.world_position, 1.0);
51
+ output.color = input.particle_color;
52
+ let normalBasisX = cross(input.up, input.forward);
53
+ let normalBasisY = cross(input.forward, input.right);
54
+ let normalBasisZ = cross(input.right, input.up);
55
+ // Cofactors require determinant sign for reflected/non-uniform Scale3.
56
+ let handedness = select(-1.0, 1.0, dot(input.right, normalBasisX) >= 0.0);
57
+ output.normal = safeNormalize(handedness * (
58
+ normalBasisX * input.geometry_normal.x + normalBasisY * input.geometry_normal.y +
59
+ normalBasisZ * input.geometry_normal.z), vec3<f32>(0.0, 1.0, 0.0));
60
+ let tangent = input.right * input.geometry_tangent.x + input.up * input.geometry_tangent.y +
61
+ input.forward * input.geometry_tangent.z;
62
+ output.tangent = vec4<f32>(safeNormalize(tangent - output.normal * dot(output.normal, tangent),
63
+ vec3<f32>(1.0, 0.0, 0.0)), input.geometry_tangent.w * handedness);
64
+ output.uv = input.geometry_uv;
65
+ output.render_controls = input.render_controls;
45
66
  return output;
46
67
  }
47
68
 
48
69
  @fragment
49
70
  fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
50
- let normal = normalize(input.normal);
51
- let light_direction = normalize(vec3<f32>(0.35, 0.7, 0.55));
52
- let view_direction = vec3<f32>(0.0, 0.0, 1.0);
53
- let half_direction = normalize(light_direction + view_direction);
54
- let diffuse = 0.2 + 0.8 * max(dot(normal, light_direction), 0.0);
55
- let metallic = clamp(input.surface.x, 0.0, 1.0);
56
- let roughness = clamp(input.surface.y, 0.04, 1.0);
57
- let clearcoat = clamp(input.surface.z, 0.0, 1.0);
58
- let clearcoat_roughness = clamp(input.surface.w, 0.04, 1.0);
59
- let specular_power = mix(96.0, 4.0, roughness);
60
- let coat_power = mix(192.0, 8.0, clearcoat_roughness);
61
- let specular = pow(max(dot(normal, half_direction), 0.0), specular_power);
62
- let coat = clearcoat * pow(max(dot(normal, half_direction), 0.0), coat_power);
63
- let dielectric = vec3<f32>(0.04);
64
- let specular_color = mix(dielectric, input.color.rgb, metallic);
65
- let lit = input.color.rgb * diffuse * (1.0 - metallic * 0.55);
66
- let emissive = input.emissive_intensity.rgb * input.emissive_intensity.a;
67
- let rgb = lit + specular_color * specular + vec3<f32>(coat) + emissive;
68
- return vec4<f32>(rgb * input.color.a, input.color.a);
71
+ var surface: VsOut;
72
+ let clip = view.worldViewProj * vec4<f32>(input.world_position, 1.0);
73
+ surface.clip = clip;
74
+ surface.worldPos = input.world_position;
75
+ surface.worldNormal = input.normal;
76
+ surface.worldTangent = input.tangent;
77
+ surface.uv = input.uv;
78
+ surface.uvOne = input.uv;
79
+ surface.uvTwo = input.uv;
80
+ surface.uvThree = input.uv;
81
+ surface.uvFour = input.uv;
82
+ surface.uvFive = input.uv;
83
+ surface.uvSix = input.uv;
84
+ surface.uvSeven = input.uv;
85
+ surface.ndc = vec4<f32>(clip.xyz / clip.w, 0.0);
86
+ surface.viewZ = sceneViewZ(clip, view.temporalProjection);
87
+ return evaluateStandardSurface(surface, StandardSurfaceFactors(
88
+ input.color * material.baseColor, material.metallic, material.roughness,
89
+ material.emissive, material.emissiveIntensity,
90
+ material.clearcoat, material.clearcoatRoughness,
91
+ input.render_controls.x != 0.0, input.render_controls.y != 0.0,
92
+ )).color;
69
93
  }
@@ -0,0 +1,39 @@
1
+ #define_import_path forgeax::vfx-render.particles.ribbon-inputs
2
+
3
+ struct SegmentInput {
4
+ @location(0) start: vec3<f32>,
5
+ @location(1) endpoint: vec3<f32>,
6
+ @location(2) color: vec4<f32>,
7
+ @location(3) properties: vec2<f32>,
8
+ @location(4) particle_inputs: vec4<f32>,
9
+ }
10
+
11
+ struct VertexOutput {
12
+ @builtin(position) position: vec4<f32>,
13
+ @location(0) color: vec4<f32>,
14
+ }
15
+
16
+ @vertex
17
+ fn vs_main(input: SegmentInput, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
18
+ let corners = array<vec2<f32>, 6>(
19
+ vec2<f32>(0.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
20
+ vec2<f32>(0.0, -1.0), vec2<f32>(1.0, 1.0), vec2<f32>(0.0, 1.0)
21
+ );
22
+ let corner = corners[vertexIndex];
23
+ let delta = input.endpoint.xy - input.start.xy;
24
+ let normal = normalize(vec2<f32>(-delta.y, delta.x) + vec2<f32>(0.000001, 0.0));
25
+ let point = mix(input.start, input.endpoint, corner.x);
26
+ let clipPosition = vec3<f32>(point.xy + normal * corner.y * input.properties.x, point.z);
27
+ var output: VertexOutput;
28
+ output.position = vec4<f32>(clipPosition, 1.0);
29
+ let heat = clamp(input.particle_inputs.x, 0.0, 1.0);
30
+ output.color = input.color * (0.35 + 0.65 * heat);
31
+ return output;
32
+ }
33
+
34
+ @fragment
35
+ fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
36
+ let base = vec4<f32>(0.2, 0.7, 1.0, 0.9);
37
+ let alpha = base.a * input.color.a;
38
+ return vec4<f32>(base.rgb * input.color.rgb * alpha, alpha);
39
+ }
@@ -0,0 +1,45 @@
1
+ #define_import_path forgeax::vfx-render.particles.trail-inputs
2
+
3
+ struct SegmentInput {
4
+ @location(0) start: vec3<f32>,
5
+ @location(1) endpoint: vec3<f32>,
6
+ @location(2) color: vec4<f32>,
7
+ @location(3) properties: vec2<f32>,
8
+ @location(4) particle_inputs: vec4<f32>,
9
+ }
10
+
11
+ struct VertexOutput {
12
+ @builtin(position) position: vec4<f32>,
13
+ @location(0) color: vec4<f32>,
14
+ @location(1) uv: vec2<f32>,
15
+ }
16
+
17
+ @vertex
18
+ fn vs_main(input: SegmentInput, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
19
+ let corners = array<vec2<f32>, 6>(
20
+ vec2<f32>(0.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
21
+ vec2<f32>(0.0, -1.0), vec2<f32>(1.0, 1.0), vec2<f32>(0.0, 1.0)
22
+ );
23
+ let corner = corners[vertexIndex];
24
+ let delta = input.endpoint.xy - input.start.xy;
25
+ let normal = normalize(vec2<f32>(-delta.y, delta.x) + vec2<f32>(0.000001, 0.0));
26
+ let tangent = normalize(delta + vec2<f32>(0.000001, 0.0));
27
+ let taper = max(0.0, 1.0 - input.properties.y);
28
+ let point = mix(input.start, input.endpoint, corner.x) + vec3<f32>(
29
+ tangent * ((corner.x * 2.0 - 1.0) * input.properties.x), 0.0,
30
+ );
31
+ let clipPosition = vec3<f32>(point.xy + normal * corner.y * input.properties.x * taper, point.z);
32
+ var output: VertexOutput;
33
+ output.position = vec4<f32>(clipPosition, 1.0);
34
+ let heat = clamp(input.particle_inputs.x, 0.0, 1.0);
35
+ output.color = input.color * (0.35 + 0.65 * heat);
36
+ output.uv = corner;
37
+ return output;
38
+ }
39
+
40
+ @fragment
41
+ fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
42
+ let edge = 1.0 - smoothstep(0.58, 1.0, abs(input.uv.y));
43
+ let alpha = input.color.a * edge;
44
+ return vec4<f32>(input.color.rgb * alpha, alpha);
45
+ }
@@ -11,6 +11,7 @@ struct VertexOutput {
11
11
  @builtin(position) position: vec4<f32>,
12
12
  @location(0) color: vec4<f32>,
13
13
  @location(1) clip_position: vec3<f32>,
14
+ @location(2) uv: vec2<f32>,
14
15
  }
15
16
 
16
17
  @vertex
@@ -36,12 +37,17 @@ fn vs_main(input: SegmentInput, @builtin(vertex_index) vertexIndex: u32) -> Vert
36
37
  output.position = vec4<f32>(clipPosition, 1.0);
37
38
  output.color = input.color;
38
39
  output.clip_position = clipPosition;
40
+ output.uv = corner;
39
41
  return output;
40
42
  }
41
43
 
42
44
  @fragment
43
45
  fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
44
- let base = vec4<f32>(0.8, 0.35, 0.1, 1.0);
45
- let alpha = base.a * input.color.a;
46
- return vec4<f32>(base.rgb * input.color.rgb * alpha, alpha);
46
+ // Keep the topology's authored particle color authoritative and feather
47
+ // the strip edge. A solid quad makes short history segments read as a
48
+ // block when several particles overlap; this analytic cross-section keeps
49
+ // the existing clip-space ABI while providing a continuous wake.
50
+ let edge = 1.0 - smoothstep(0.58, 1.0, abs(input.uv.y));
51
+ let alpha = input.color.a * edge;
52
+ return vec4<f32>(input.color.rgb * alpha, alpha);
47
53
  }