@plasius/gpu-particles 0.1.0

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 (49) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +203 -0
  3. package/README.md +82 -0
  4. package/dist/effects/fire/physics.job.wgsl +84 -0
  5. package/dist/effects/fire/prelude.wgsl +41 -0
  6. package/dist/effects/fire/render.job.wgsl +8 -0
  7. package/dist/effects/firework/prelude.wgsl +41 -0
  8. package/dist/effects/firework/render.job.wgsl +8 -0
  9. package/dist/effects/firework/update.job.wgsl +154 -0
  10. package/dist/effects/rain/prelude.wgsl +35 -0
  11. package/dist/effects/rain/render.job.wgsl +8 -0
  12. package/dist/effects/rain/update.job.wgsl +51 -0
  13. package/dist/effects/snow/prelude.wgsl +35 -0
  14. package/dist/effects/snow/render.job.wgsl +8 -0
  15. package/dist/effects/snow/update.job.wgsl +53 -0
  16. package/dist/effects/sparks/prelude.wgsl +41 -0
  17. package/dist/effects/sparks/render.job.wgsl +8 -0
  18. package/dist/effects/sparks/update.job.wgsl +53 -0
  19. package/dist/effects/text/layout.job.wgsl +57 -0
  20. package/dist/effects/text/prelude.wgsl +39 -0
  21. package/dist/effects/text/render.job.wgsl +8 -0
  22. package/dist/index.cjs +286 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.js +244 -0
  25. package/dist/index.js.map +1 -0
  26. package/legal/CLA-REGISTRY.csv +2 -0
  27. package/legal/CLA.md +22 -0
  28. package/legal/CORPORATE_CLA.md +57 -0
  29. package/legal/INDIVIDUAL_CLA.md +91 -0
  30. package/package.json +74 -0
  31. package/src/effects/fire/physics.job.wgsl +84 -0
  32. package/src/effects/fire/prelude.wgsl +41 -0
  33. package/src/effects/fire/render.job.wgsl +8 -0
  34. package/src/effects/firework/prelude.wgsl +41 -0
  35. package/src/effects/firework/render.job.wgsl +8 -0
  36. package/src/effects/firework/update.job.wgsl +154 -0
  37. package/src/effects/rain/prelude.wgsl +35 -0
  38. package/src/effects/rain/render.job.wgsl +8 -0
  39. package/src/effects/rain/update.job.wgsl +51 -0
  40. package/src/effects/snow/prelude.wgsl +35 -0
  41. package/src/effects/snow/render.job.wgsl +8 -0
  42. package/src/effects/snow/update.job.wgsl +53 -0
  43. package/src/effects/sparks/prelude.wgsl +41 -0
  44. package/src/effects/sparks/render.job.wgsl +8 -0
  45. package/src/effects/sparks/update.job.wgsl +53 -0
  46. package/src/effects/text/layout.job.wgsl +57 -0
  47. package/src/effects/text/prelude.wgsl +39 -0
  48. package/src/effects/text/render.job.wgsl +8 -0
  49. package/src/index.js +251 -0
@@ -0,0 +1,41 @@
1
+ // Fire effect shared WGSL definitions.
2
+
3
+ struct Particle {
4
+ pos: vec4<f32>, // x, y, life, seed
5
+ vel: vec4<f32>, // vx, vy, kind, value
6
+ };
7
+
8
+ struct EffectParams {
9
+ time: f32,
10
+ dt: f32,
11
+ intensity: f32,
12
+ spawn_radius: f32,
13
+ bounds_min: vec2<f32>,
14
+ bounds_max: vec2<f32>,
15
+ origin: vec2<f32>,
16
+ drift: vec2<f32>,
17
+ };
18
+
19
+ @group(1) @binding(0) var<storage, read_write> particles: array<Particle>;
20
+ @group(1) @binding(1) var<uniform> effect_params: EffectParams;
21
+
22
+ const TWO_PI: f32 = 6.2831853;
23
+
24
+ fn hash_u32(x: u32) -> u32 {
25
+ var v = x;
26
+ v = v ^ (v >> 16u);
27
+ v = v * 0x7feb352du;
28
+ v = v ^ (v >> 15u);
29
+ v = v * 0x846ca68bu;
30
+ v = v ^ (v >> 16u);
31
+ return v;
32
+ }
33
+
34
+ fn rand01(seed: u32) -> f32 {
35
+ let v = hash_u32(seed) & 0x00ffffffu;
36
+ return f32(v) / 16777216.0;
37
+ }
38
+
39
+ fn rand_signed(seed: u32) -> f32 {
40
+ return rand01(seed) * 2.0 - 1.0;
41
+ }
@@ -0,0 +1,8 @@
1
+ // Fire render job placeholder (rendering handled on the CPU in the demo).
2
+
3
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
4
+ let _ignore = job_index + job_type + payload_words;
5
+ if (_ignore == 0u) {
6
+ return;
7
+ }
8
+ }
@@ -0,0 +1,41 @@
1
+ // Firework effect shared WGSL definitions.
2
+
3
+ struct Particle {
4
+ pos: vec4<f32>,
5
+ vel: vec4<f32>,
6
+ };
7
+
8
+ struct EffectParams {
9
+ time: f32,
10
+ dt: f32,
11
+ intensity: f32,
12
+ spawn_radius: f32,
13
+ bounds_min: vec2<f32>,
14
+ bounds_max: vec2<f32>,
15
+ origin: vec2<f32>,
16
+ drift: vec2<f32>,
17
+ };
18
+
19
+ @group(1) @binding(0) var<storage, read_write> particles: array<Particle>;
20
+ @group(1) @binding(1) var<uniform> effect_params: EffectParams;
21
+
22
+ const TWO_PI: f32 = 6.2831853;
23
+
24
+ fn hash_u32(x: u32) -> u32 {
25
+ var v = x;
26
+ v = v ^ (v >> 16u);
27
+ v = v * 0x7feb352du;
28
+ v = v ^ (v >> 15u);
29
+ v = v * 0x846ca68bu;
30
+ v = v ^ (v >> 16u);
31
+ return v;
32
+ }
33
+
34
+ fn rand01(seed: u32) -> f32 {
35
+ let v = hash_u32(seed) & 0x00ffffffu;
36
+ return f32(v) / 16777216.0;
37
+ }
38
+
39
+ fn rand_signed(seed: u32) -> f32 {
40
+ return rand01(seed) * 2.0 - 1.0;
41
+ }
@@ -0,0 +1,8 @@
1
+ // Firework render job placeholder.
2
+
3
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
4
+ let _ignore = job_index + job_type + payload_words;
5
+ if (_ignore == 0u) {
6
+ return;
7
+ }
8
+ }
@@ -0,0 +1,154 @@
1
+ // Firework update job: explosions with sparks, smoke, and ash.
2
+
3
+ const KIND_SPARK: f32 = 0.0;
4
+ const KIND_SMOKE: f32 = 1.0;
5
+ const KIND_ASH: f32 = 2.0;
6
+
7
+ const PATHS: array<vec2<f32>, 20> = array<vec2<f32>, 20>(
8
+ vec2<f32>(1.0, 0.0),
9
+ vec2<f32>(0.92, 0.38),
10
+ vec2<f32>(0.71, 0.71),
11
+ vec2<f32>(0.38, 0.92),
12
+ vec2<f32>(0.0, 1.0),
13
+ vec2<f32>(-0.38, 0.92),
14
+ vec2<f32>(-0.71, 0.71),
15
+ vec2<f32>(-0.92, 0.38),
16
+ vec2<f32>(-1.0, 0.0),
17
+ vec2<f32>(-0.92, -0.38),
18
+ vec2<f32>(-0.71, -0.71),
19
+ vec2<f32>(-0.38, -0.92),
20
+ vec2<f32>(0.0, -1.0),
21
+ vec2<f32>(0.38, -0.92),
22
+ vec2<f32>(0.71, -0.71),
23
+ vec2<f32>(0.92, -0.38),
24
+ vec2<f32>(0.98, 0.2),
25
+ vec2<f32>(0.2, 0.98),
26
+ vec2<f32>(-0.98, 0.2),
27
+ vec2<f32>(-0.2, 0.98)
28
+ );
29
+
30
+ fn respawn_firework(seed: u32, burst_seed: u32) -> Particle {
31
+ let r2 = rand01(seed ^ 0x9e3779b9u);
32
+ let radius = effect_params.spawn_radius * sqrt(r2);
33
+ let intensity = clamp(effect_params.intensity, 0.1, 2.0);
34
+ let scale = 0.2 + rand01(seed ^ 0x3c6ef372u) * 0.8;
35
+ let size = clamp(scale * intensity, 0.15, 0.95);
36
+ let raw_count = 5u + u32(floor(size * 15.0));
37
+ let path_count = min(20u, max(5u, raw_count));
38
+ let path_index = hash_u32(seed ^ burst_seed) % path_count;
39
+ let path_dir = normalize(PATHS[path_index]);
40
+ let offset = path_dir * (radius * 0.15);
41
+ let base = effect_params.origin + offset;
42
+
43
+ let kind_roll = rand01(seed ^ 0x85ebca6bu);
44
+ var kind = KIND_SPARK;
45
+ var life = 0.9 + rand01(seed ^ 0x27d4eb2du) * 1.1;
46
+ var speed = (0.8 + rand01(seed ^ 0xc2b2ae35u) * 2.2) * scale * (0.7 + intensity * 0.3);
47
+ var vel = path_dir * speed;
48
+
49
+ if (kind_roll > 0.78) {
50
+ kind = KIND_SMOKE;
51
+ life = 1.8 + rand01(seed ^ 0x51a3c1u) * 2.0;
52
+ speed = 0.08 + rand01(seed ^ 0x94d049bdu) * 0.25;
53
+ vel = vec2<f32>(
54
+ rand_signed(seed ^ 0x7f4a7c15u) * 0.06,
55
+ 0.06 + rand01(seed ^ 0x165667b1u) * 0.18
56
+ );
57
+ } else if (kind_roll > 0.6) {
58
+ kind = KIND_ASH;
59
+ life = 1.2 + rand01(seed ^ 0x846ca68bu) * 1.4;
60
+ speed = 0.12 + rand01(seed ^ 0x6c8e9cf5u) * 0.35;
61
+ vel = vec2<f32>(
62
+ rand_signed(seed ^ 0x1b873593u) * 0.18,
63
+ 0.18 + rand01(seed ^ 0x27d4eb2du) * 0.28
64
+ );
65
+ }
66
+
67
+ let perp = vec2<f32>(-path_dir.y, path_dir.x);
68
+ vel = vel + perp * rand_signed(seed ^ 0x632be5abu) * 0.08 * scale;
69
+ vel.y = vel.y + (0.45 + rand01(seed ^ 0x632be5abu) * 0.55) * scale;
70
+
71
+ let color_roll = rand01(seed ^ 0x9e3779b9u);
72
+ var color_index = floor(color_roll * 3.0);
73
+ if (color_roll > 0.92) {
74
+ color_index = 3.0;
75
+ }
76
+
77
+ return Particle(vec4<f32>(base, life, f32(seed)), vec4<f32>(vel, kind, color_index + size));
78
+ }
79
+
80
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
81
+ if (payload_words == 0u) {
82
+ return;
83
+ }
84
+ let particle_index = payload_word(job_index, 0u);
85
+ if (particle_index >= arrayLength(&particles)) {
86
+ return;
87
+ }
88
+
89
+ var particle = particles[particle_index];
90
+ var life = particle.pos.z;
91
+ let seed = u32(particle.pos.w) ^ (particle_index * 173u);
92
+
93
+ let burst_id = u32(effect_params.time * 0.6);
94
+ let burst_seed = burst_id * 0x9e3779b9u;
95
+ let burst_phase = fract(effect_params.time * 0.6);
96
+
97
+ if (life <= 0.0) {
98
+ let chance = rand01(seed ^ burst_seed ^ 0x85ebca6bu);
99
+ let window = burst_phase < 0.08;
100
+ let threshold = select(0.995, 0.7 - effect_params.intensity * 0.2, window);
101
+ if (chance > threshold) {
102
+ particle = respawn_firework(seed + burst_seed, burst_seed);
103
+ particles[particle_index] = particle;
104
+ }
105
+ return;
106
+ }
107
+
108
+ let kind = particle.vel.z;
109
+ let color_index = floor(particle.vel.w);
110
+ let size = clamp(particle.vel.w - color_index, 0.0, 1.5);
111
+ var pos = particle.pos.xy;
112
+ var vel = particle.vel.xy;
113
+
114
+ if (kind == KIND_SPARK) {
115
+ var gravity = 1.1 + color_index * 0.2;
116
+ let drag = 0.985 - size * 0.01;
117
+ if (color_index >= 3.0) {
118
+ gravity = 0.85;
119
+ vel.x = vel.x + sin(effect_params.time * 3.0 + f32(seed) * 0.01) * 0.02;
120
+ }
121
+ vel.y = vel.y - gravity * effect_params.dt;
122
+ vel = vel * drag;
123
+ life = life - effect_params.dt * (1.1 + size * 0.4);
124
+ } else if (kind == KIND_SMOKE) {
125
+ let sway = rand_signed(seed ^ u32(effect_params.time * 900.0)) * 0.02;
126
+ vel.x = vel.x + (effect_params.drift.x * 0.15 + sway) * effect_params.dt;
127
+ vel.y = vel.y - 0.05 * effect_params.dt;
128
+ vel = vel * 0.989;
129
+ life = life - effect_params.dt * 0.55;
130
+ } else {
131
+ vel.y = vel.y - (0.7 + color_index * 0.05) * effect_params.dt;
132
+ vel.x = vel.x + effect_params.drift.x * effect_params.dt * 0.3;
133
+ vel = vel * 0.99;
134
+ life = life - effect_params.dt * 0.75;
135
+ }
136
+
137
+ pos = pos + vel * effect_params.dt;
138
+
139
+ if (pos.y < effect_params.bounds_min.y - 0.2 || pos.y > effect_params.bounds_max.y + 0.3 || pos.x < effect_params.bounds_min.x - 0.2 || pos.x > effect_params.bounds_max.x + 0.2) {
140
+ life = -1.0;
141
+ }
142
+
143
+ particle.pos = vec4<f32>(pos, life, f32(seed));
144
+ particle.vel = vec4<f32>(vel, kind, particle.vel.w);
145
+
146
+ if (life <= 0.0) {
147
+ let chance = rand01(seed ^ burst_seed ^ 0x27d4eb2du);
148
+ if (chance > 0.985) {
149
+ particle = respawn_firework(seed + 59u + burst_seed, burst_seed);
150
+ }
151
+ }
152
+
153
+ particles[particle_index] = particle;
154
+ }
@@ -0,0 +1,35 @@
1
+ // Rain effect shared WGSL definitions.
2
+
3
+ struct Particle {
4
+ pos: vec4<f32>,
5
+ vel: vec4<f32>,
6
+ };
7
+
8
+ struct EffectParams {
9
+ time: f32,
10
+ dt: f32,
11
+ intensity: f32,
12
+ spawn_radius: f32,
13
+ bounds_min: vec2<f32>,
14
+ bounds_max: vec2<f32>,
15
+ origin: vec2<f32>,
16
+ drift: vec2<f32>,
17
+ };
18
+
19
+ @group(1) @binding(0) var<storage, read_write> particles: array<Particle>;
20
+ @group(1) @binding(1) var<uniform> effect_params: EffectParams;
21
+
22
+ fn hash_u32(x: u32) -> u32 {
23
+ var v = x;
24
+ v = v ^ (v >> 16u);
25
+ v = v * 0x7feb352du;
26
+ v = v ^ (v >> 15u);
27
+ v = v * 0x846ca68bu;
28
+ v = v ^ (v >> 16u);
29
+ return v;
30
+ }
31
+
32
+ fn rand01(seed: u32) -> f32 {
33
+ let v = hash_u32(seed) & 0x00ffffffu;
34
+ return f32(v) / 16777216.0;
35
+ }
@@ -0,0 +1,8 @@
1
+ // Rain render job placeholder.
2
+
3
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
4
+ let _ignore = job_index + job_type + payload_words;
5
+ if (_ignore == 0u) {
6
+ return;
7
+ }
8
+ }
@@ -0,0 +1,51 @@
1
+ // Rain update job.
2
+
3
+ fn rand_signed(seed: u32) -> f32 {
4
+ return rand01(seed) * 2.0 - 1.0;
5
+ }
6
+
7
+ fn respawn_rain(seed: u32) -> Particle {
8
+ let x = effect_params.origin.x + rand_signed(seed) * effect_params.spawn_radius;
9
+ let y = effect_params.origin.y + rand01(seed ^ 0x9e3779b9u) * 0.1;
10
+ let speed = 0.7 + rand01(seed ^ 0x85ebca6bu) * 1.2;
11
+ let vel = vec2<f32>(effect_params.drift.x * 0.2, -speed);
12
+ return Particle(vec4<f32>(vec2<f32>(x, y), 1.0, f32(seed)), vec4<f32>(vel, 0.0, 0.0));
13
+ }
14
+
15
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
16
+ if (payload_words == 0u) {
17
+ return;
18
+ }
19
+ let particle_index = payload_word(job_index, 0u);
20
+ if (particle_index >= arrayLength(&particles)) {
21
+ return;
22
+ }
23
+
24
+ var particle = particles[particle_index];
25
+ var life = particle.pos.z;
26
+ let seed = u32(particle.pos.w) ^ (particle_index * 97u);
27
+
28
+ if (life <= 0.0) {
29
+ particle = respawn_rain(seed + u32(effect_params.time * 400.0));
30
+ particles[particle_index] = particle;
31
+ return;
32
+ }
33
+
34
+ var pos = particle.pos.xy;
35
+ var vel = particle.vel.xy;
36
+ vel.x = vel.x + effect_params.drift.x * effect_params.dt * 0.3;
37
+ pos = pos + vel * effect_params.dt;
38
+
39
+ if (pos.y < effect_params.bounds_min.y - 0.1) {
40
+ life = -1.0;
41
+ }
42
+
43
+ particle.pos = vec4<f32>(pos, life, f32(seed));
44
+ particle.vel = vec4<f32>(vel, particle.vel.z, particle.vel.w);
45
+
46
+ if (life <= 0.0) {
47
+ particle = respawn_rain(seed + 13u + u32(effect_params.time * 700.0));
48
+ }
49
+
50
+ particles[particle_index] = particle;
51
+ }
@@ -0,0 +1,35 @@
1
+ // Snow effect shared WGSL definitions.
2
+
3
+ struct Particle {
4
+ pos: vec4<f32>,
5
+ vel: vec4<f32>,
6
+ };
7
+
8
+ struct EffectParams {
9
+ time: f32,
10
+ dt: f32,
11
+ intensity: f32,
12
+ spawn_radius: f32,
13
+ bounds_min: vec2<f32>,
14
+ bounds_max: vec2<f32>,
15
+ origin: vec2<f32>,
16
+ drift: vec2<f32>,
17
+ };
18
+
19
+ @group(1) @binding(0) var<storage, read_write> particles: array<Particle>;
20
+ @group(1) @binding(1) var<uniform> effect_params: EffectParams;
21
+
22
+ fn hash_u32(x: u32) -> u32 {
23
+ var v = x;
24
+ v = v ^ (v >> 16u);
25
+ v = v * 0x7feb352du;
26
+ v = v ^ (v >> 15u);
27
+ v = v * 0x846ca68bu;
28
+ v = v ^ (v >> 16u);
29
+ return v;
30
+ }
31
+
32
+ fn rand01(seed: u32) -> f32 {
33
+ let v = hash_u32(seed) & 0x00ffffffu;
34
+ return f32(v) / 16777216.0;
35
+ }
@@ -0,0 +1,8 @@
1
+ // Snow render job placeholder.
2
+
3
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
4
+ let _ignore = job_index + job_type + payload_words;
5
+ if (_ignore == 0u) {
6
+ return;
7
+ }
8
+ }
@@ -0,0 +1,53 @@
1
+ // Snow update job.
2
+
3
+ fn rand_signed(seed: u32) -> f32 {
4
+ return rand01(seed) * 2.0 - 1.0;
5
+ }
6
+
7
+ fn respawn_snow(seed: u32) -> Particle {
8
+ let x = effect_params.origin.x + rand_signed(seed) * effect_params.spawn_radius;
9
+ let y = effect_params.origin.y + rand01(seed ^ 0x9e3779b9u) * 0.1;
10
+ let speed = 0.12 + rand01(seed ^ 0x85ebca6bu) * 0.2;
11
+ let drift = rand_signed(seed ^ 0x27d4eb2du) * 0.05;
12
+ let vel = vec2<f32>(drift + effect_params.drift.x * 0.2, -speed);
13
+ return Particle(vec4<f32>(vec2<f32>(x, y), 1.0, f32(seed)), vec4<f32>(vel, 0.0, 0.0));
14
+ }
15
+
16
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
17
+ if (payload_words == 0u) {
18
+ return;
19
+ }
20
+ let particle_index = payload_word(job_index, 0u);
21
+ if (particle_index >= arrayLength(&particles)) {
22
+ return;
23
+ }
24
+
25
+ var particle = particles[particle_index];
26
+ var life = particle.pos.z;
27
+ let seed = u32(particle.pos.w) ^ (particle_index * 67u);
28
+
29
+ if (life <= 0.0) {
30
+ particle = respawn_snow(seed + u32(effect_params.time * 200.0));
31
+ particles[particle_index] = particle;
32
+ return;
33
+ }
34
+
35
+ var pos = particle.pos.xy;
36
+ var vel = particle.vel.xy;
37
+ let sway = sin(effect_params.time * 0.8 + f32(seed) * 0.01) * 0.02;
38
+ vel.x = vel.x + sway + effect_params.drift.x * effect_params.dt * 0.4;
39
+ pos = pos + vel * effect_params.dt;
40
+
41
+ if (pos.y < effect_params.bounds_min.y - 0.1) {
42
+ life = -1.0;
43
+ }
44
+
45
+ particle.pos = vec4<f32>(pos, life, f32(seed));
46
+ particle.vel = vec4<f32>(vel, particle.vel.z, particle.vel.w);
47
+
48
+ if (life <= 0.0) {
49
+ particle = respawn_snow(seed + 29u + u32(effect_params.time * 300.0));
50
+ }
51
+
52
+ particles[particle_index] = particle;
53
+ }
@@ -0,0 +1,41 @@
1
+ // Sparks effect shared WGSL definitions.
2
+
3
+ struct Particle {
4
+ pos: vec4<f32>,
5
+ vel: vec4<f32>,
6
+ };
7
+
8
+ struct EffectParams {
9
+ time: f32,
10
+ dt: f32,
11
+ intensity: f32,
12
+ spawn_radius: f32,
13
+ bounds_min: vec2<f32>,
14
+ bounds_max: vec2<f32>,
15
+ origin: vec2<f32>,
16
+ drift: vec2<f32>,
17
+ };
18
+
19
+ @group(1) @binding(0) var<storage, read_write> particles: array<Particle>;
20
+ @group(1) @binding(1) var<uniform> effect_params: EffectParams;
21
+
22
+ const TWO_PI: f32 = 6.2831853;
23
+
24
+ fn hash_u32(x: u32) -> u32 {
25
+ var v = x;
26
+ v = v ^ (v >> 16u);
27
+ v = v * 0x7feb352du;
28
+ v = v ^ (v >> 15u);
29
+ v = v * 0x846ca68bu;
30
+ v = v ^ (v >> 16u);
31
+ return v;
32
+ }
33
+
34
+ fn rand01(seed: u32) -> f32 {
35
+ let v = hash_u32(seed) & 0x00ffffffu;
36
+ return f32(v) / 16777216.0;
37
+ }
38
+
39
+ fn rand_signed(seed: u32) -> f32 {
40
+ return rand01(seed) * 2.0 - 1.0;
41
+ }
@@ -0,0 +1,8 @@
1
+ // Sparks render job placeholder.
2
+
3
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
4
+ let _ignore = job_index + job_type + payload_words;
5
+ if (_ignore == 0u) {
6
+ return;
7
+ }
8
+ }
@@ -0,0 +1,53 @@
1
+ // Sparks update job.
2
+
3
+ fn respawn_spark(seed: u32) -> Particle {
4
+ let angle = rand01(seed) * TWO_PI;
5
+ let speed = 0.5 + rand01(seed ^ 0x9e3779b9u) * 1.6;
6
+ let radius = effect_params.spawn_radius * rand01(seed ^ 0x85ebca6bu);
7
+ let offset = vec2<f32>(cos(angle), sin(angle)) * radius;
8
+ let pos = effect_params.origin + offset;
9
+ let vel = vec2<f32>(cos(angle), sin(angle)) * speed;
10
+ let life = 0.3 + rand01(seed ^ 0x27d4eb2du) * 0.7;
11
+ return Particle(vec4<f32>(pos, life, f32(seed)), vec4<f32>(vel, 1.0, 0.0));
12
+ }
13
+
14
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
15
+ if (payload_words == 0u) {
16
+ return;
17
+ }
18
+ let particle_index = payload_word(job_index, 0u);
19
+ if (particle_index >= arrayLength(&particles)) {
20
+ return;
21
+ }
22
+
23
+ var particle = particles[particle_index];
24
+ var life = particle.pos.z;
25
+ let seed = u32(particle.pos.w) ^ (particle_index * 131u);
26
+
27
+ if (life <= 0.0) {
28
+ particle = respawn_spark(seed + u32(effect_params.time * 500.0));
29
+ particles[particle_index] = particle;
30
+ return;
31
+ }
32
+
33
+ var vel = particle.vel.xy;
34
+ var pos = particle.pos.xy;
35
+ vel.y = vel.y - 1.4 * effect_params.dt + effect_params.drift.y * effect_params.dt;
36
+ vel.x = vel.x + effect_params.drift.x * effect_params.dt;
37
+ vel = vel * 0.985;
38
+ pos = pos + vel * effect_params.dt;
39
+ life = life - effect_params.dt * 1.5;
40
+
41
+ if (pos.y < effect_params.bounds_min.y - 0.2 || pos.y > effect_params.bounds_max.y + 0.2) {
42
+ life = -1.0;
43
+ }
44
+
45
+ particle.pos = vec4<f32>(pos, life, f32(seed));
46
+ particle.vel = vec4<f32>(vel, particle.vel.z, particle.vel.w);
47
+
48
+ if (life <= 0.0) {
49
+ particle = respawn_spark(seed + 19u + u32(effect_params.time * 700.0));
50
+ }
51
+
52
+ particles[particle_index] = particle;
53
+ }
@@ -0,0 +1,57 @@
1
+ // Text layout job.
2
+
3
+ fn respawn_text(seed: u32) -> Particle {
4
+ let x = effect_params.origin.x + rand_signed(seed) * effect_params.spawn_radius;
5
+ let y = effect_params.origin.y + rand01(seed ^ 0x9e3779b9u) * 0.05;
6
+ let value = f32(hash_u32(seed) % 10000u);
7
+ let lateral = rand_signed(seed ^ 0x85ebca6bu) * 0.12;
8
+ let upward = 0.55 + rand01(seed ^ 0x27d4eb2du) * 0.45;
9
+ let vel = vec2<f32>(lateral, upward);
10
+ let life = 2.2 + rand01(seed ^ 0x51a3c1u) * 1.2;
11
+ return Particle(vec4<f32>(vec2<f32>(x, y), life, f32(seed)), vec4<f32>(vel, 0.0, value));
12
+ }
13
+
14
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
15
+ if (payload_words == 0u) {
16
+ return;
17
+ }
18
+ let particle_index = payload_word(job_index, 0u);
19
+ if (particle_index >= arrayLength(&particles)) {
20
+ return;
21
+ }
22
+
23
+ var particle = particles[particle_index];
24
+ var life = particle.pos.z;
25
+ let seed = u32(particle.pos.w) ^ (particle_index * 113u);
26
+
27
+ if (life <= 0.0) {
28
+ let burst = rand01(seed ^ u32(effect_params.time * 120.0));
29
+ if (burst > 0.985) {
30
+ particle = respawn_text(seed + u32(effect_params.time * 600.0));
31
+ particles[particle_index] = particle;
32
+ }
33
+ return;
34
+ }
35
+
36
+ var pos = particle.pos.xy;
37
+ var vel = particle.vel.xy;
38
+ let dir = select(-1.0, 1.0, pos.x >= effect_params.origin.x);
39
+ vel.x = vel.x + dir * 0.18 * effect_params.dt;
40
+ vel.x = vel.x + effect_params.drift.x * effect_params.dt * 0.05;
41
+ vel.y = vel.y - 0.95 * effect_params.dt;
42
+ vel = vel * 0.995;
43
+ pos = pos + vel * effect_params.dt;
44
+
45
+ if (pos.y < effect_params.bounds_min.y - 0.1 || pos.x < effect_params.bounds_min.x - 0.1 || pos.x > effect_params.bounds_max.x + 0.1) {
46
+ life = -1.0;
47
+ }
48
+
49
+ particle.pos = vec4<f32>(pos, life, f32(seed));
50
+ particle.vel = vec4<f32>(vel, particle.vel.z, particle.vel.w);
51
+
52
+ if (life <= 0.0) {
53
+ particle = respawn_text(seed + 37u + u32(effect_params.time * 500.0));
54
+ }
55
+
56
+ particles[particle_index] = particle;
57
+ }
@@ -0,0 +1,39 @@
1
+ // Text overlay effect shared WGSL definitions.
2
+
3
+ struct Particle {
4
+ pos: vec4<f32>,
5
+ vel: vec4<f32>,
6
+ };
7
+
8
+ struct EffectParams {
9
+ time: f32,
10
+ dt: f32,
11
+ intensity: f32,
12
+ spawn_radius: f32,
13
+ bounds_min: vec2<f32>,
14
+ bounds_max: vec2<f32>,
15
+ origin: vec2<f32>,
16
+ drift: vec2<f32>,
17
+ };
18
+
19
+ @group(1) @binding(0) var<storage, read_write> particles: array<Particle>;
20
+ @group(1) @binding(1) var<uniform> effect_params: EffectParams;
21
+
22
+ fn hash_u32(x: u32) -> u32 {
23
+ var v = x;
24
+ v = v ^ (v >> 16u);
25
+ v = v * 0x7feb352du;
26
+ v = v ^ (v >> 15u);
27
+ v = v * 0x846ca68bu;
28
+ v = v ^ (v >> 16u);
29
+ return v;
30
+ }
31
+
32
+ fn rand01(seed: u32) -> f32 {
33
+ let v = hash_u32(seed) & 0x00ffffffu;
34
+ return f32(v) / 16777216.0;
35
+ }
36
+
37
+ fn rand_signed(seed: u32) -> f32 {
38
+ return rand01(seed) * 2.0 - 1.0;
39
+ }
@@ -0,0 +1,8 @@
1
+ // Text render job placeholder.
2
+
3
+ fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
4
+ let _ignore = job_index + job_type + payload_words;
5
+ if (_ignore == 0u) {
6
+ return;
7
+ }
8
+ }