Setting the file. One moment.
Powder · Frost Sequence Camera Orbit · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Three Mesh BVH LICENSE
source/src/powder/Powder.ts
source/src/powder/ Powder.ts
TypeScript · 1,775 lines · 72 KB
11 positionView ,
12 reflect ,
13 vec2 ,
14 Fn ,
15 vec3 ,
16 vec4 ,
17 float ,
18 uniform ,
19 instancedArray ,
20 storage ,
21 instanceIndex ,
22 If ,
23 Loop ,
24 int ,
25 uint ,
26 uvec3 ,
27 ivec3 ,
28 atomicAdd ,
29 atomicSub ,
30 atomicLoad ,
31 atomicStore ,
32 max ,
33 min ,
34 abs ,
35 dot ,
36 length ,
37 normalize ,
38 exp ,
39 mix ,
40 smoothstep ,
41 clamp ,
42 select ,
43 floor ,
44 fract ,
45 sin ,
46 cos ,
47 sqrt ,
48 texture3D ,
49 textureStore ,
50 positionLocal ,
51 normalLocal ,
52 transformNormalToView ,
53 varying ,
54 cameraPosition ,
55 screenUV ,
56 mat3 ,
57 mat4 ,
58 pow ,
59 Break ,
60 Continue ,
61 cross ,
62 step ,
63 negate ,
64 uv ,
65 texture ,
66 uniformArray ,
67 cameraViewMatrix ,
68 } = tsl;
69 import type { ShapeSpec } from "../shape/sdf" ;
70 import { sampleInterior, sdfNormalNode } from "../shape/sdf" ;
71 import { buildGrainVariants } from "../shape/geometry" ;
72 import { SHARD_ATLAS_URL, SHARD_GRID, SHARD_CELLS } from "./shardAtlas" ;
73 import type { ErosionField } from "../erosion/ErosionField" ;
74 import { AssemblyField, assemblyReach } from "../erosion/AssemblyField" ;
75 import {
76 assemblyFrontPhase,
77 returnGroupPhase,
78 returnReserve,
79 RETURN_GROUP_DEFAULTS,
80 } from "./returnGroups" ;
81 import type { Interaction } from "../core/interaction" ;
82 import { curlNoise, vnoise3, hash11, hash31, hashSeed, rotateAxis, saturate } from "../tsl/noise" ;
83 import { D } from "../dials/store" ;
84 import { backdropColorAt, type BackdropUniforms } from "../scene/Backdrop" ;
85 import { iceSurface, iceSmudgeDirections } from "../ice/SharedSurface" ;
86 import { iceEnvironment } from "../ice/EnvironmentSampling" ;
87 import { iceDetail, iceFrost, iceInclusions } from "../ice/SurfaceDetail" ;
88 import { ICE_VARIANT } from "../ice/variant" ;
89 import { sim } from "../core/state" ;
90
91 type N = any ;
92
93 /** Shard sprite atlas (generated from the Higgsfield shard sheet): loaded once, shared by every rebuild. */
94 let shardAtlas : THREE . Texture | null = null ;
95 function getShardAtlas () : THREE . Texture {
96 if ( ! shardAtlas) {
97 const t = new THREE . TextureLoader (). load ( SHARD_ATLAS_URL );
98 t.flipY = false ;
99 t.colorSpace = THREE .NoColorSpace;
100 t.wrapS = t.wrapT = THREE .ClampToEdgeWrapping;
101 t.minFilter = THREE .LinearMipmapLinearFilter;
102 t.magFilter = THREE .LinearFilter;
103 t.generateMipmaps = true ;
104 t.anisotropy = 4 ;
105 shardAtlas = t;
106 }
107 return shardAtlas;
108 }
109 // GHOST = eroded but gated out by `amount`: never rendered, still heals its voxel when the wave arrives
110 // WAITING = landed, but its break cell still has grains in flight: it sits at rest (invisible) until the
111 // cell refills, and only leaves again if the brush cuts its voxel
112 const DORMANT = 0 ,
113 ACTIVE = 1 ,
114 HEALING = 2 ,
115 STRAY = 3 ,
116 GHOST = 4 ,
117 WAITING = 5 ;
118
119 export interface PowderOptions {
120 renderer : THREE . WebGPURenderer ;
121 scene : THREE . Scene ;
122 shape : ShapeSpec ;
123 erosion : ErosionField ;
124 seed : number ;
125 rand : () => number ;
126 count : number ;
127 variants : number ;
128 strays : number ;
129 densityRes : number ;
130 clumpCount : number ;
131 objectGroup : THREE . Object3D ;
132 enabled ?: boolean ;
133 /** 'fast' swaps the 6-sample finite-difference curl for a one-sample swirl. */
134 turbulence ?: "full" | "fast" ;
135 /** shared atomic uint buffer: [density G^3 | 16 counters | heal grid]. */
136 atomics : any ;
137 countersOffset : number ;
138 healOffset : number ;
139 cellOffset : number ;
140 flightOffset : number ;
141 fieldRes : number ;
142 /** backdrop gradient uniforms (the translucent grains show the backdrop through their centres). */
143 backdrop : BackdropUniforms ;
144 fractureDetail ?: THREE . Texture ;
145 iceUniforms : Record < string , any >;
146 iceFeatures : Record < string , any >;
147 environment : THREE . Texture ;
148 }
149
150 export class Powder {
151 readonly total : number ;
152 /** FROST: grains that belong to the shape (the rest are strays) and the initial rest buffer (thresholds, strays). */
153 readonly count : number ;
154 readonly restInit : Float32Array ;
155 readonly meshes : THREE . Mesh [] = [];
156 readonly group = new THREE . Group ();
157 readonly densityTex : THREE . Storage3DTexture ;
158 private assembly ?: AssemblyField ;
159 assemblyEnabled = true ;
160 beforeIntegrate ?: () => void ;
161 beforeAssembly ?: () => void ;
162 private buffers : Record < string , any > = {};
163 private nodes : Record < string , any > = {};
164 private material !: THREE . MeshStandardNodeMaterial ;
165 private prevModel = new THREE . Matrix4 ();
166 private readonly inversePrevModel = new THREE . Matrix4 ();
167 private statsTimer = 0 ;
168 private densityDirty = true ;
169 private fragmentShadowMap = true ;
170 private settingsVersion = - 1 ;
171 readonly u = {
172 rigEnabled: uniform ( 0 ),
173 rigStrength: uniform ( 1 ),
174 surfaceFrost: uniform ( 1 ),
175 surfaceDetail: uniform ( 1 ),
176 dt: uniform ( 0 ),
177 time: uniform ( 0 ),
178 model: uniform ( new THREE . Matrix4 ()),
179 previousModel: uniform ( new THREE . Matrix4 ()),
180 modelDelta: uniform ( new THREE . Matrix4 ()),
181 normalMat: uniform ( new THREE . Matrix3 ()),
182 strokeDir: uniform ( new THREE . Vector3 ( 1 , 0 , 0 )),
183 strokeSpeed: uniform ( 0 ),
184 heal: uniform ( 0 ),
185 ejectSpeed: uniform ( 1.1 ),
186 ejectSpread: uniform ( 0.5 ),
187 ejectTurb: uniform ( 0.8 ),
188 backwardRatio: uniform ( 0.25 ),
189 clumpJitter: uniform ( 0.5 ),
190 drag: uniform ( 1.4 ),
191 gravity: uniform ( 0.12 ),
192 turbulence: uniform ( 0.9 ),
193 turbScale: uniform ( 1.8 ),
194 turbDecay: uniform ( 0.6 ),
195 cohesion: uniform ( 2.4 ),
196 settleTime: uniform ( 3.2 ),
197 settledDrift: uniform ( 0.012 ),
198 tumble: uniform ( 2.5 ),
199 returnDuration: uniform ( 2.4 ),
200 returnCurve: uniform ( 0.35 ),
201 inherit: uniform ( 1 ),
202 inheritTime: uniform ( 0.22 ),
203 straySpeed: uniform ( 0.06 ),
204 fade: uniform ( 1 ),
205 sizeMul: uniform ( 1 ),
206 amount: uniform ( 0.3 ),
207 dustScale: uniform ( 0.0055 ),
208 grainScale: uniform ( 0.011 ),
209 clumpScale: uniform ( 0.022 ),
210 fragmentScale: uniform ( 0.05 ),
211 sizeJitter: uniform ( 0.45 ),
212 densityExtent: uniform ( 4.2 ),
213 densityScale: uniform ( 0.03 ),
214 shadowStrength: uniform ( 0.75 ),
215 aoStrength: uniform ( 0.55 ),
216 lightDir: uniform ( new THREE . Vector3 ( 0 , 1 , 0 )),
217 tone: uniform ( new THREE . Color ( "#dcdcdc" )),
218 wrap: uniform ( 0.45 ),
219 keyIntensity: uniform ( 3.2 ),
220 keyColor: uniform ( new THREE . Color ( "#ffffff" )),
221 colorByState: uniform ( 0 ),
222 colorBySize: uniform ( 0 ),
223 colorByAge: uniform ( 0 ),
224 camPos: uniform ( new THREE . Vector3 ()),
225 invProj: uniform ( new THREE . Matrix4 ()),
226 camWorld: uniform ( new THREE . Matrix4 ()),
227 hazeSteps: uniform ( 28 ),
228 strayBase: uniform ( 1 ),
229 holeRadius: uniform ( 0.55 ),
230 bound: uniform ( 1.5 ),
231 fieldActive: uniform ( 1 ),
232 hazeOn: uniform ( 1 ),
233 waveTime: uniform ( 2 ),
234 waveJitter: uniform ( 0.6 ),
235 minEject: uniform ( 1 ),
236 minPixel: uniform ( 1.5 ),
237 pixelWorld: uniform ( 0.001 ),
238 ghostDist: uniform ( 0 ),
239 ghostFrac: uniform ( 0.45 ),
240 waveReach: uniform ( 3 ),
241 depositRadius: uniform ( 2 ),
242 returnMode: uniform ( 1 ),
243 springK: uniform ( 6 ),
244 springDamp: uniform ( 0.9 ),
245 springRamp: uniform ( 0.6 ),
246 landRadius: uniform ( 0.03 ),
247 returnAfter: uniform ( 3 ),
248 maxSpeed: uniform ( 8 ),
249 returnDrag: uniform ( 0.25 ),
250 returnMaxSpeed: uniform ( 12 ),
251 lostRadius: uniform ( 30 ),
252 stragglers: uniform ( 0 ),
253 spriteSee: uniform ( 0 ),
254 frontDuration: uniform ( 3.2 ),
255 frontX: uniform ( - 0.65 ),
256 frontY: uniform ( 0.55 ),
257 frontAngle: uniform ( - 35 ),
258 frontSpread: uniform ( 0.65 ),
259 frontNoise: uniform ( 0.35 ),
260 speedVariation: uniform ( 0.65 ),
261 pathBend: uniform ( 1.2 ),
262 pathSwirl: uniform ( 1.1 ),
263 landingVariation: uniform ( 0.8 ),
264 groupNoise: uniform ( 2 ),
265 assemblyEnd: uniform ( 0 ),
266 groupStagger: uniform ( 0 ),
267 groupScale: uniform ( 0.65 ),
268 groupSeed: uniform ( 7 ),
269 alignAmt: uniform ( 1 ),
270 alignDist: uniform ( 0.6 ),
271 alignCurve: uniform ( 1 ),
272 landedFade: uniform ( 0.25 ),
273 landShrink: uniform ( 0 ),
274 translucency: uniform ( 0.6 ),
275 throughTint: uniform ( new THREE . Color ( "#aeb6bb" )),
276 fresnelPower: uniform ( 3 ),
277 fragRough: uniform ( 0.32 ),
278 fragClearcoat: uniform ( 0.6 ),
279 fragSpecular: uniform ( 1 ),
280 sparkle: uniform ( 0.6 ),
281 sparkleFraction: uniform ( 0.35 ),
282 sparkleSpread: uniform ( 0.5 ),
283 lightDirView: uniform ( new THREE . Vector3 ( 0 , 1 , 0 )),
284 spriteSize: uniform ( 1.4 ),
285 spriteTilt: uniform ( 0.5 ),
286 spriteNormal: uniform ( 1 ),
287 spriteFrost: uniform ( 1 ),
288 spriteFrostBoost: uniform ( 0.35 ),
289 spriteFrostRough: uniform ( 0.7 ),
290 spriteEdge: uniform ( 0.6 ),
291 spriteCut: uniform ( 0.2 ),
292 repel: uniform ( 0 ),
293 repelStrength: uniform ( 6 ),
294 repelRange: uniform ( 0.35 ),
295 repelRadial: uniform ( 12 ),
296 repelRadialRange: uniform ( 1.6 ),
297 modelInv: uniform ( new THREE . Matrix4 ()),
298 };
299
300 constructor ( readonly o : PowderOptions ) {
301 const { count , strays , rand , shape } = o;
302 this .total = count + strays;
303 const N = this .total;
304 const P = D .powder;
305 // ---- CPU init
306 const samples = sampleInterior (shape, count, P .nearSurfaceFraction, P .nearSurfaceDepth, rand);
307 const pos = new Float32Array ( N * 4 ),
308 vel = new Float32Array ( N * 4 ),
309 rest = new Float32Array ( N * 4 ),
310 meta = new Float32Array ( N * 4 ),
311 heal = new Float32Array ( N * 4 );
312 const ratios = [
313 P .grainSizes.tinyDustRatio,
314 P .grainSizes.smallGrainRatio,
315 P .grainSizes.mediumClumpRatio,
316 0.01 ,
317 ];
318 const rs = ratios. reduce (( a , b ) => a + b, 0 );
319 const cum = [
320 ratios[ 0 ] / rs,
321 (ratios[ 0 ] + ratios[ 1 ]) / rs,
322 (ratios[ 0 ] + ratios[ 1 ] + ratios[ 2 ]) / rs,
323 ];
324 // clump leaders via spatial cells
325 const cellSize = Math. cbrt (( 8 * shape.bound ** 3 ) / Math. max ( 100 , o.clumpCount));
326 const halfCells = Math. ceil (shape.bound / cellSize) + 1 ;
327 const gridSide = halfCells * 2 + 1 ;
328 const leaders = new Map < number , number >();
329 for ( let i = 0 ; i < count; i ++ ) {
330 const i3 = i * 3 ,
331 i4 = i * 4 ;
332 const x = samples.positions[i3],
333 y = samples.positions[i3 + 1 ],
334 z = samples.positions[i3 + 2 ];
335 const depth = samples.depths[i];
336 const r = rand ();
337 let cls = r < cum[ 0 ] ? 0 : r < cum[ 1 ] ? 1 : r < cum[ 2 ] ? 2 : 3 ;
338 if (cls === 3 && depth > 0.2 ) cls = 2 ;
339 const cx = Math. floor (x / cellSize) + halfCells,
340 cy = Math. floor (y / cellSize) + halfCells,
341 cz = Math. floor (z / cellSize) + halfCells;
342 const key = cx + cy * gridSide + cz * gridSide * gridSide;
343 let leader = leaders. get (key);
344 if (leader === undefined ) {
345 leader = i;
346 leaders. set (key, i);
347 }
348 rest[i4] = x;
349 rest[i4 + 1 ] = y;
350 rest[i4 + 2 ] = z;
351 rest[i4 + 3 ] = 0.35 + rand () * 0.4 ;
352 pos[i4] = x;
353 pos[i4 + 1 ] = y;
354 pos[i4 + 2 ] = z;
355 meta[i4] = DORMANT ;
356 meta[i4 + 1 ] = rand ();
357 meta[i4 + 2 ] = cls;
358 meta[i4 + 3 ] = leader;
359 }
360 // strays: at rest in / around the hole
361 for ( let s = 0 ; s < strays; s ++ ) {
362 const i = count + s;
363 const a = rand () * Math. PI * 2 ;
364 const rr =
365 shape.name === "torus"
366 ? (shape.size - shape.size * shape.tubeRatio) * ( 0.15 + rand () * 0.75 )
367 : shape.bound * ( 1.05 + rand () * 0.35 );
368 const x = Math. cos (a) * rr,
369 y = Math. sin (a) * rr,
370 z = ( rand () - 0.5 ) * shape.thickness * 1.2 ;
371 const i4 = i * 4 ;
372 rest[i4] = x;
373 rest[i4 + 1 ] = y;
374 rest[i4 + 2 ] = z;
375 rest[i4 + 3 ] = 2 ;
376 pos[i4] = x;
377 pos[i4 + 1 ] = y;
378 pos[i4 + 2 ] = z;
379 pos[i4 + 3 ] = 0.01 ;
380 vel[i4 + 3 ] = rand () * 10 ;
381 meta[i4] = STRAY ;
382 meta[i4 + 1 ] = rand ();
383 meta[i4 + 2 ] = rand () < 0.8 ? 0 : 1 ;
384 meta[i4 + 3 ] = i;
385 }
386 this .count = count;
387 this .restInit = rest. slice ();
388 const B = this .buffers;
389 B .pos = instancedArray (pos, "vec4" );
390 B .vel = instancedArray (vel, "vec4" );
391 B .rest = instancedArray (rest, "vec4" );
392 B .meta = instancedArray (meta, "vec4" );
393 B .heal = instancedArray (heal, "vec4" );
394 B .leaders = instancedArray ( N , "vec4" );
395 const V = o.variants;
396 const G = o.densityRes;
397 B .atomics = o.atomics; // density voxels at 0, counters at countersOffset, heal grid at healOffset
398 this .densityTex = new THREE . Storage3DTexture ( G , G , G );
399 this .densityTex.type = THREE .HalfFloatType;
400 this .densityTex.format = THREE .RGBAFormat;
401 this .densityTex.minFilter = this .densityTex.magFilter = THREE .LinearFilter;
402 this .densityTex.wrapS =
403 this .densityTex.wrapT =
404 this .densityTex.wrapR =
405 THREE .ClampToEdgeWrapping;
406 this .u.bound.value = shape.bound;
407 this .u.holeRadius.value =
408 shape.name === "torus" ? shape.size - shape.size * shape.tubeRatio : shape.bound;
409 if (o.enabled !== false ) {
410 this . buildCompute ();
411 this .assembly = new AssemblyField (o.erosion, count, B , this .u);
412 this . buildMeshes ();
413 o.scene. add ( this .group);
414 }
415 this .prevModel. copy (o.objectGroup.matrixWorld);
416 }
417
418 // --------------------------------------------------------------------------------------------
419 private buildCompute () {
420 const { count , shape , erosion , variants : V } = this .o;
421 const N = this .total,
422 u = this .u,
423 B = this .buffers;
424 const G = this .o.densityRes;
425 const i = instanceIndex;
426 void count;
427
428 const CO = this .o.countersOffset,
429 HO = this .o.healOffset,
430 CLO = this .o.cellOffset,
431 FLO = this .o.flightOffset,
432 FR = this .o.fieldRes; // FR: erosion field resolution (R is the rest buffer element below)
433 const cellTexNode = texture3D (erosion.cellTex);
434 const counter = ( v : N ) => B .atomics. element ( uint ( CO ). add ( uint (v)));
435 const densityAt = ( idx : N ) => B .atomics. element (idx);
436 const healAt = ( idx : N ) => B .atomics. element ( uint ( HO ). add (idx));
437 const classScale = ( cls : N ) =>
438 select (
439 cls. lessThan ( 0.5 ),
440 u.dustScale,
441 select (
442 cls. lessThan ( 1.5 ),
443 u.grainScale,
444 select (cls. lessThan ( 2.5 ), u.clumpScale, u.fragmentScale),
445 ),
446 );
447 const classWeight = ( cls : N ) =>
448 select (
449 cls. lessThan ( 0.5 ),
450 uint ( 1 ),
451 select (cls. lessThan ( 1.5 ), uint ( 2 ), select (cls. lessThan ( 2.5 ), uint ( 5 ), uint ( 12 ))),
452 );
453 // Read a frozen step: live neighbor reads race other workgroups writing their state.
454 this .nodes.snapshotLeaders = Fn (() => {
455 B .leaders. element (i). assign ( vec4 ( B .pos. element (i).xyz, B .meta. element (i).x));
456 })(). compute ( N , [ 64 ]);
457 const expoInOut = ( x : N ) => {
458 const xc = clamp (x, 0.0 , 1.0 );
459 return select (
460 xc. lessThan ( 0.5 ),
461 pow ( 2.0 , xc. mul ( 20.0 ). sub ( 10.0 )). div ( 2.0 ),
462 float ( 2 )
463 . sub ( pow ( 2.0 , xc. mul ( - 20.0 ). add ( 10.0 )))
464 . div ( 2.0 ),
465 );
466 };
467
468 this .nodes.resetCounters = Fn (() => {
469 Loop ( 8 , ({ i : k } : any ) => {
470 atomicStore ( counter (k), uint ( 0 ));
471 });
472 })(). compute ( 1 , [ 1 ]);
473
474 const curl =
475 this .o.turbulence === "fast"
476 ? ( q : N ) => cross ( vnoise3 (q), vnoise3 (q. add ( vec3 ( 31.7 , 11.3 , 57.9 )))). mul ( 1.6 )
477 : ( q : N ) => curlNoise (q);
478
479 // normalize() of a zero vector is NaN on the GPU and a NaN grain is lost for good (the logo SDF is flat
480 // outside its grid, so its gradient is zero there): every direction is normalised safely
481 const safeNormalize = ( v : N ) => v. div ( max ( length (v), 1e-6 ));
482 // one update kernel, instantiated twice: all particles, and (idle) the strays only
483 const buildUpdate = ( indexNode : N , dispatchCount : number ) =>
484 Fn (() => {
485 const i = indexNode;
486 const P = B .pos. element (i),
487 Vv = B .vel. element (i),
488 R = B .rest. element (i),
489 M = B .meta. element (i),
490 H = B .heal. element (i);
491 // every value shared between the state branches is pinned to a variable here (TSL assigns
492 // unpinned nodes at first use, which may be inside another branch -> uninitialised reads)
493 const state = M .x. toVar ();
494 const seed = M .y. toVar (),
495 cls = M .z. toVar (),
496 leader = M .w. toVar ();
497 const pos = P .xyz. toVar ();
498 const size = P .w. toVar ();
499 const vel = Vv.xyz. toVar ();
500 const age = Vv.w. toVar ();
501 const rest = R .xyz. toVar ();
502 const threshold = R .w. toVar ();
503 const h1 = hashSeed (seed, 1 ). toVar (),
504 h2 = hashSeed (seed, 2 ). toVar (),
505 h3 = hashSeed (seed, 3 ). toVar ();
506 const baseSize = classScale (cls)
507 . mul ( float ( 1 ). sub (u.sizeJitter. mul ( 0.5 )). add (u.sizeJitter. mul (h1)))
508 . mul (u.sizeMul)
509 . toVar ();
510 const restWorld = u.model. mul ( vec4 (rest, 1.0 )).xyz. toVar ();
511 const restNormalWorld = safeNormalize (
512 u.normalMat. mul ( sdfNormalNode (shape, rest, 0.004 )),
513 ). toVar ();
514 const centreWorld = u.model. mul ( vec4 ( 0.0 , 0.0 , 0.0 , 1.0 )).xyz. toVar ();
515
516 const erSample = erosion. sample (rest). toVar ();
517 const erRest = erSample.r. toVar ();
518 const hitRecent = erSample.a. greaterThan ( 0.01 ); // the brush is cutting this grain's rest voxel right now
519 const gate = hash11 (seed. mul ( 211.7 ). add ( 5.0 )). lessThan (u.amount);
520 const healScheduled = H .w. greaterThan ( 0.5 );
521 // deposit into the heal grid around the rest voxel (the field refills there next frame); the radius
522 // lets one returning grain stand in for the material that vanished on breakup (powder.amount < 1)
523 // break cell of the rest voxel (nearest voxel, like the erosion step reads it)
524 const cellIdOf = () => {
525 const gc = ivec3 ( floor (erosion. uvw (rest). mul ( float ( FR ))));
526 const cellHash = cellTexNode. load ( clamp (gc, ivec3 ( 0 ), ivec3 ( FR - 1 ))). level ( 0 ).w;
527 return uint ( clamp (cellHash. mul ( 65535.0 ), 0.0 , 65535.0 ));
528 };
529 // leaving: one more grain of this cell in flight
530 const leave = () => {
531 atomicAdd ( B .atomics. element ( uint ( FLO ). add ( cellIdOf ())), uint ( 1 ));
532 };
533 // arriving: one fewer; the last one home (allowing `cellStragglers`) triggers the cell restore
534 const arrive = ( trigger : N ) => {
535 const cellId = cellIdOf (). toVar ();
536 const before = atomicSub ( B .atomics. element ( uint ( FLO ). add (cellId)), uint ( 1 )). toVar ();
537 If (trigger. and (before. lessThanEqual ( uint ( 1 ). add ( uint (u.stragglers)))), () => {
538 atomicAdd ( B .atomics. element ( uint ( CLO ). add (cellId)), uint ( 1 ));
539 });
540 };
541 const deposit = () => {
542 // the small voxel sphere around the rest (the cell itself refills once all its grains are home)
543 const gp = rest
544 . add ( this .o.erosion.bound)
545 . div ( this .o.erosion.bound * 2 )
546 . mul ( float ( FR ));
547 const gi = ivec3 ( floor (gp)). toVar ();
548 const rf = u.depositRadius. toVar ();
549 const r = int (rf. ceil ()). toVar ();
550 const rng = { start: r. negate (), end: r, condition: "<=" };
551 Loop (rng, rng, rng, ({ i : dx , j : dy , k : dz } : any ) => {
552 const cc = gi. add ( ivec3 (dx, dy, dz));
553 const inSphere = float (dx. mul (dx). add (dy. mul (dy)). add (dz. mul (dz))). lessThanEqual (
554 rf. mul (rf). add ( 0.01 ),
555 );
556 If (
557 inSphere
558 . and (cc.x. greaterThanEqual ( 0 ))
559 . and (cc.y. greaterThanEqual ( 0 ))
560 . and (cc.z. greaterThanEqual ( 0 ))
561 . and (cc.x. lessThan ( FR ))
562 . and (cc.y. lessThan ( FR ))
563 . and (cc.z. lessThan ( FR )),
564 () => {
565 const hidx = uint (cc.x)
566 . add ( uint (cc.y). mul ( uint ( FR )))
567 . add ( uint (cc.z). mul ( uint ( FR * FR )));
568 atomicAdd ( healAt (hidx), uint ( 1 ));
569 },
570 );
571 });
572 };
573 // return order: grains nearest the object first, the far plume last (ghost dust spreads evenly)
574 const scheduleTime = () => {
575 const distance = length (pos. sub (restWorld));
576 const wave = clamp (distance. div (u.waveReach), 0.0 , 1.0 ). mul (u.waveTime);
577 const regional = erosion.u.reconstruct
578 . greaterThan ( 0.5 )
579 . and (u.groupNoise. greaterThan ( 0.5 ));
580 const jitter = select (regional, h3. mul (u.waveJitter). mul ( 0.04 ), h3. mul (u.waveJitter));
581 const base = wave. add (jitter);
582 const growing = u.groupNoise. greaterThan ( 1.5 );
583 const delay = float ( 0 ). toVar ();
584 If (
585 erosion.u.reconstruct. greaterThan ( 0.5 ). and (u.groupStagger. greaterThan ( 0 ). or (growing)),
586 () => {
587 const reserve = select (
588 u.returnMode. greaterThan ( 0.5 ),
589 returnReserve (distance, length (vel), u),
590 u.returnDuration. add ( 0.2 ),
591 );
592 const available = max ( 0 , u.assemblyEnd. sub (u.time). sub (base). sub (reserve));
593 // Scale the whole front to the available window, rather than clipping
594 // all late regions to the same departure time.
595 delay. assign (
596 min ( select (growing, u.frontDuration, u.groupStagger), available). mul (
597 select (
598 u.rigEnabled. greaterThan ( 0.5 ),
599 assemblyFrontPhase (rest, u),
600 max ( 0 , threshold. sub ( 2 )),
601 ),
602 ),
603 );
604 },
605 );
606 return u.time. add (wave). add (jitter). add (delay);
607 };
608 const ghostScheduleTime = () => u.time. add (h1. mul (u.waveTime)). add (h3. mul (u.waveJitter));
609
610 // leave the surface: velocity along the stroke (a share backwards), spread along the normal, turbulence
611 const eject = () => {
612 const pw = restWorld;
613 const nw = restNormalWorld;
614 const leaderSeed = B .meta. element ( uint (leader)).y;
615 const clumpRand = float ( 1 )
616 . sub (u.clumpJitter. mul ( 0.5 ))
617 . add (u.clumpJitter. mul ( hash11 (leaderSeed. mul ( 77.7 ))));
618 const backward = select (
619 hash11 (leaderSeed. mul ( 31.3 ). add ( 1.0 )). lessThan (u.backwardRatio),
620 float ( - 1 ),
621 float ( 1 ),
622 );
623 const spd = max (u.strokeSpeed, u.minEject);
624 const dir = u.strokeDir. mul (backward);
625 const turb = curl (pw. mul (u.turbScale). add (u.time. mul ( 0.2 )));
626 const v0 = dir
627 . mul (spd. mul (u.ejectSpeed). mul (clumpRand))
628 . add (nw. mul (u.ejectSpread). mul ( float ( 0.5 ). add (h2)). mul (spd. mul ( 0.4 )))
629 . add (turb. mul (u.ejectTurb). mul (spd. mul ( 0.25 )). mul (h3. add ( 0.5 )));
630 const fragScale = select (cls. greaterThan ( 2.5 ), float ( 0.35 ), float ( 1.0 ));
631 vel. assign (v0. mul (fragScale));
632 pos. assign (pw. add (nw. mul ( 0.002 )));
633 age. assign ( 0.0 );
634 size. assign (baseSize);
635 state. assign ( ACTIVE );
636 H .w. assign ( 0.0 );
637 leave ();
638 };
639 If (state. lessThan ( 0.5 ), () => {
640 // DORMANT: sample the erosion field at rest (skipped while the field is static). The state
641 // chain below must stay intact, so the idle gate is nested rather than part of the condition.
642 If (u.fieldActive. greaterThan ( 0.5 ), () => {
643 If (erRest. greaterThan (threshold), () => {
644 If (gate, () => {
645 eject ();
646 }). Else (() => {
647 state. assign ( GHOST );
648 H .w. assign ( 0.0 );
649 });
650 });
651 });
652 })
653 . ElseIf (state. lessThan ( 1.5 ), () => {
654 // ACTIVE
655 age. addAssign (u.dt);
656 const settle = smoothstep (u.settleTime. mul ( 0.45 ), u.settleTime, age);
657 const turb = curl (pos. mul (u.turbScale). add (u.time. mul ( 0.15 )))
658 . mul (u.turbulence)
659 . mul ( exp (age. mul (u.turbDecay). negate ()));
660 const leaderP = B .leaders. element ( uint (leader));
661 const leaderState = leaderP.w;
662 const coh = select (
663 leaderState. greaterThan ( 0.5 ). and (leaderState. lessThan ( 1.5 )),
664 leaderP.xyz. sub (pos). mul (u.cohesion). mul ( float ( 1 ). sub (settle)),
665 vec3 ( 0 ),
666 );
667 // optional repulsion from the object: full strength inside the surface, smooth falloff outside
668 const repelF = vec3 ( 0 ). toVar ();
669 If (u.repel. greaterThan ( 0.5 ), () => {
670 const pObj = u.modelInv. mul ( vec4 (pos, 1.0 )).xyz. toVar ();
671 const dObj = shape. sdfNode (pObj). toVar ();
672 If (dObj. lessThan (u.repelRange), () => {
673 const wgt = saturate ( float ( 1 ). sub (dObj. div (u.repelRange)));
674 const nW = safeNormalize (u.normalMat. mul ( sdfNormalNode (shape, pObj, 0.004 )));
675 repelF. assign (nW. mul (u.repelStrength). mul (wgt. mul (wgt)));
676 });
677 // radial push from the object's centre: the only direction that always leads out of a pocket
678 const toOut = pos. sub (centreWorld);
679 const rn = length (toOut). div (u.bound. mul (u.repelRadialRange));
680 If (rn. lessThan ( 1.0 ), () => {
681 const w2 = float ( 1 ). sub (rn. mul (rn));
682 const dirOut = safeNormalize (
683 toOut. add ( vec3 (h1. sub ( 0.5 ), h2. sub ( 0.5 ), h3. sub ( 0.5 )). mul ( 0.05 )),
684 );
685 repelF. addAssign (dirOut. mul (u.repelRadial). mul (w2. mul (w2)));
686 });
687 });
688 vel. addAssign (
689 turb
690 . add (coh)
691 . add (repelF)
692 . add ( vec3 ( 0 , u.gravity. negate (), 0 ))
693 . mul (u.dt),
694 );
695 vel. mulAssign ( exp (u.drag. mul (u.dt). negate ()));
696 vel. mulAssign ( float ( 1 ). sub (settle. mul ( min ( 1.0 , u.dt. mul ( 3.0 )))));
697 pos. addAssign (vel. mul (u.dt));
698 pos. addAssign (
699 curl (pos. mul ( 0.9 ). add (u.time. mul ( 0.05 )))
700 . mul (u.settledDrift)
701 . mul (settle)
702 . mul (u.dt),
703 );
704 If (u.inherit. greaterThan ( 0.5 ). and (age. lessThan (u.inheritTime)), () => {
705 const follow = float ( 1 ). sub (
706 smoothstep (u.inheritTime. mul ( 0.75 ), max (u.inheritTime, 0.001 ), age),
707 );
708 pos. assign ( mix (pos, u.modelDelta. mul ( vec4 (pos, 1.0 )).xyz, follow));
709 vel. assign ( mix (vel, mat3 (u.modelDelta). mul (vel), follow));
710 });
711 // hard speed cap: nothing ever flings a grain off-screen
712 const spdNow = length (vel);
713 If (spdNow. greaterThan (u.maxSpeed), () => {
714 vel. mulAssign (u.maxSpeed. div (spdNow));
715 });
716 // healing: scheduled when the cursor is idle, or on the grain's own timer (returnAfter)
717 const ownTimer = u.returnAfter. greaterThan ( 0.001 ). and (age. greaterThan (u.returnAfter));
718 If (u.heal. greaterThan ( 0.5 ). or (ownTimer), () => {
719 If (healScheduled. not (), () => {
720 H .w. assign ( scheduleTime ());
721 });
722 If ( H .w. greaterThan ( 0.5 ). and (u.time. greaterThanEqual ( H .w)), () => {
723 state. assign ( HEALING );
724 // FROST: the flight start lives in H.w; `age` keeps counting so the tumble phase never jumps
725 H .xyz. assign (pos);
726 H .w. assign (u.time);
727 });
728 }). Else (() => {
729 H .w. assign ( 0.0 );
730 });
731 })
732 . ElseIf (state. lessThan ( 2.5 ), () => {
733 // HEALING
734 const isGhost = H .w. lessThan ( - 0.5 );
735 // FROST: flight start from H.w (ghosts keep the experiment's age-based start); age keeps counting
736 const t0 = select (isGhost, age, H .w). toVar ();
737 If (isGhost. not (), () => {
738 age. addAssign (u.dt);
739 });
740 const start = H .xyz;
741 const target = restWorld;
742 const nw = restNormalWorld;
743 If (u.returnMode. greaterThan ( 0.5 ), () => {
744 // spring return: a damped spring toward the rest that ramps in over `springRamp`, so the grain
745 // decelerates, turns and comes back in one continuous motion; lands when it reaches the rest
746 const tt = u.time. sub (t0);
747 const ramp = smoothstep ( 0.0 , max (u.springRamp, 0.001 ), tt);
748 // Ease inherited frame motion into the spring instead of dropping it at return start.
749 const follow = u.inherit
750 . mul ( float ( 1 ). sub (ramp))
751 . mul (
752 float ( 1 ). sub ( smoothstep (u.inheritTime. mul ( 0.75 ), max (u.inheritTime, 0.001 ), age)),
753 );
754 pos. assign ( mix (pos, u.modelDelta. mul ( vec4 (pos, 1.0 )).xyz, follow));
755 vel. assign ( mix (vel, mat3 (u.modelDelta). mul (vel), follow));
756 const d = target. sub (pos);
757 const dist = length (d). toVar ();
758 const organic = select (
759 erosion.u.reconstruct. greaterThan ( 0.5 ). and (u.groupNoise. greaterThan ( 1.5 )),
760 float ( 1 ),
761 float ( 0 ),
762 );
763 const speedFactor = mix (
764 float ( 1 ),
765 mix ( float ( 0.55 ), float ( 1.8 ), h1),
766 u.speedVariation. mul (organic),
767 );
768 const k = u.springK. mul (u.rigStrength). mul (speedFactor. mul (speedFactor)). mul (ramp);
769 const c = sqrt (u.springK. mul (u.rigStrength))
770 . mul (speedFactor)
771 . mul ( 2.0 )
772 . mul (u.springDamp)
773 . mul (ramp);
774 // A moving guide creates different approach angles. It collapses onto
775 // the true home near contact and after a bounded time, so arcs settle.
776 const flightDistance = length (target. sub (start));
777 const homeFraction = clamp (dist. div ( max (flightDistance, 0.05 )), 0 , 1 );
778 const guideLife = float ( 1 ). sub ( smoothstep ( 0.5 , 2.4 , tt. mul (speedFactor)));
779 const side = safeNormalize ( cross (nw, vec3 (h2, h3, h1). sub ( 0.5 )). add ( 0.001 ));
780 const other = cross (nw, side);
781 const twist = tt. mul (u.pathSwirl). mul (h2. mul ( 2 ). sub ( 1 )). add (h3. mul ( 6.283185 ));
782 const arc = side
783 . mul ( cos (twist))
784 . add (other. mul ( sin (twist)))
785 . add (nw. mul (h2. sub ( 0.5 )));
786 const guide = arc
787 . mul ( min (flightDistance, 3 ))
788 . mul (u.pathBend)
789 . mul ( 0.5 )
790 . mul (homeFraction. mul ( 0.3 ). add ( sin (homeFraction. mul (Math. PI )). mul ( 0.7 )))
791 . mul (guideLife)
792 . mul (organic);
793 const previousTarget = u.previousModel. mul ( vec4 (rest, 1.0 )).xyz;
794 const targetVelocity = target. sub (previousTarget). div ( max (u.dt, 1e-5 ));
795 const acc = d
796 . add (guide)
797 . mul (k)
798 . sub (vel. sub (targetVelocity). mul (c))
799 . add ( vec3 ( 0 , u.gravity. negate (). mul ( float ( 1 ). sub (ramp)), 0 ));
800 vel. addAssign (acc. mul (u.dt));
801 vel. mulAssign ( exp (u.drag. mul (u.returnDrag). mul (u.dt). negate ()));
802 const spdBack = length (vel);
803 const speedLimit = u.returnMaxSpeed. mul (speedFactor);
804 If (spdBack. greaterThan (speedLimit), () => {
805 vel. mulAssign (speedLimit. div (spdBack));
806 });
807 // never overshoot the rest in one step
808 const step = vel. mul (u.dt);
809 const stepLen = length (step);
810 pos. addAssign ( select (stepLen. greaterThan (dist), d, step));
811 size. assign (
812 baseSize
813 . mul (
814 select (
815 u.landShrink. greaterThan ( 0.001 ),
816 saturate (dist. div ( max (u.landShrink, 0.001 ))),
817 float ( 1 ),
818 ),
819 )
820 . mul ( select (isGhost, smoothstep ( 0.0 , 0.25 , tt), float ( 1 ))),
821 );
822 const landed = dist. lessThan (u.landRadius);
823 If (landed, () => {
824 If (isGhost. not (), () => {
825 arrive (hitRecent. not (). and (erosion.u.reconstruct. lessThan ( 0.5 )));
826 If (hitRecent. not (). and (erosion.u.reconstruct. lessThan ( 0.5 )), () => {
827 deposit ();
828 });
829 });
830 state. assign ( select (isGhost, float ( DORMANT ), float ( WAITING )));
831 pos. assign (target);
832 vel. assign ( vec3 ( 0 ));
833 H .w. assign ( 0.0 );
834 size. assign (
835 select (isGhost. or (erosion.u.reconstruct. greaterThan ( 0.5 )), float ( 0 ), baseSize),
836 );
837 });
838 }). Else (() => {
839 // path return: eased curved flight, shrinking into the surface; deposits at 85% of the flight
840 const uu = clamp (u.time. sub (t0). div ( max (u.returnDuration, 0.05 )), 0.0 , 1.0 ). toVar ();
841 const uuPrev = clamp (
842 u.time. sub (u.dt). sub (t0). div ( max (u.returnDuration, 0.05 )),
843 0.0 ,
844 1.0 ,
845 );
846 const e = expoInOut (uu);
847 const mid = mix (start, target, 0.5 )
848 . add (
849 nw
850 . mul (u.returnCurve)
851 . mul ( length (target. sub (start)))
852 . mul ( 0.5 ),
853 )
854 . add ( vec3 (h2, h3, h1). sub ( 0.5 ). mul (u.returnCurve). mul ( 0.4 ));
855 const a = mix (start, mid, e),
856 b = mix (mid, target, e);
857 pos. assign ( mix (a, b, e));
858 const fadeIn = select (
859 isGhost,
860 smoothstep ( float ( 1 ). sub (u.ghostFrac), float ( 1 ). sub (u.ghostFrac). add ( 0.08 ), uu),
861 float ( 1 ),
862 );
863 size. assign (baseSize. mul ( float ( 1 ). sub ( smoothstep ( 0.82 , 1.0 , uu))). mul (fadeIn));
864 If (uuPrev. lessThan ( 0.85 ). and (uu. greaterThanEqual ( 0.85 )). and (isGhost. not ()), () => {
865 arrive (hitRecent. not (). and (erosion.u.reconstruct. lessThan ( 0.5 )));
866 If (hitRecent. not (). and (erosion.u.reconstruct. lessThan ( 0.5 )), () => {
867 deposit ();
868 });
869 });
870 If (uu. greaterThanEqual ( 1.0 ), () => {
871 state. assign ( select (isGhost, float ( DORMANT ), float ( WAITING )));
872 pos. assign (target);
873 H .w. assign ( 0.0 );
874 });
875 });
876 // The same spatial contact controls both return modes. Once home, disarm this
877 // shard until the next breakup even if a neighbouring owner is still approaching.
878 If (erosion.u.reconstruct. greaterThan ( 0.5 ). and (isGhost. not ()), () => {
879 const variation = select (u.groupNoise. greaterThan ( 1.5 ), u.landingVariation, float ( 0 ));
880 const reach = float ( assemblyReach (erosion.bound)). mul (
881 mix ( float ( 1 ), mix ( float ( 0.35 ), float ( 1.65 ), h2), variation),
882 );
883 const contactFade = smoothstep (
884 u.landRadius,
885 max (u.landRadius. add ( 0.001 ), reach),
886 length (target. sub (pos)),
887 );
888 size. assign (
889 baseSize. mul (
890 pow (contactFade, mix ( float ( 1 ), mix ( float ( 0.45 ), float ( 2.2 ), h3), variation)),
891 ),
892 );
893 If (state. greaterThan ( 4.5 ), () => {
894 R .w. assign ( 2.0 );
895 });
896 });
897 // a stroke elsewhere never touches a grain in flight: only the brush passing over its own rest voxel
898 // matters, and that is handled on landing (no deposit, and it leaves the surface again next frame)
899 })
900 . ElseIf (state. lessThan ( 3.5 ), () => {
901 // STRAY: slow orbit in / around the hole, object space -> world
902 const ang = u.time. mul (u.straySpeed). mul ( float ( 0.5 ). add (h1)). add (seed. mul ( 6.2831 ));
903 const r = length (rest.xy);
904 const zz = rest.z. add ( sin (u.time. mul ( 0.3 ). add (seed. mul ( 12.0 ))). mul ( 0.05 ));
905 const local = vec3 ( cos (ang). mul (r), sin (ang). mul (r), zz);
906 pos. assign (u.model. mul ( vec4 (local, 1.0 )).xyz);
907 size. assign ( classScale (cls). mul ( 0.9 ). mul (u.sizeMul));
908 age. assign (age. add (u.dt));
909 })
910 . ElseIf (state. lessThan ( 4.5 ), () => {
911 // GHOST: vanished on breakup. It never rebuilds anything (only grains that flew out do); on the
912 // wave it just becomes dormant again, or optionally shows up as extra dust flying in.
913 If (u.heal. greaterThan ( 0.5 ), () => {
914 If (healScheduled. not (), () => {
915 H .w. assign (
916 ghostScheduleTime (). add (u.returnDuration. mul ( float ( 1 ). sub (u.ghostFrac))),
917 );
918 });
919 If ( H .w. greaterThan ( 0.5 ). and (u.time. greaterThanEqual ( H .w)), () => {
920 If (u.ghostDist. greaterThan ( 0.001 ), () => {
921 const nw = restNormalWorld;
922 const tangent = safeNormalize (
923 cross (nw, vec3 (h1. sub ( 0.5 ), h2. sub ( 0.5 ), h3. sub ( 0.5 )). add ( 0.001 )),
924 );
925 const startPos = restWorld
926 . add (nw. mul (u.ghostDist). mul ( float ( 0.4 ). add (h1. mul ( 0.6 ))))
927 . add (tangent. mul (u.ghostDist). mul (h2. sub ( 0.5 )). mul ( 0.8 ));
928 H .xyz. assign (startPos);
929 H .w. assign ( - 1.0 ); // -1: ghost-origin return (fades in, no deposit)
930 pos. assign (startPos);
931 age. assign (u.time. sub (u.returnDuration. mul ( float ( 1 ). sub (u.ghostFrac))));
932 size. assign ( 0.0 );
933 state. assign ( HEALING );
934 }). Else (() => {
935 state. assign ( DORMANT );
936 H .w. assign ( 0.0 );
937 });
938 });
939 }). Else (() => {
940 H .w. assign ( 0.0 );
941 });
942 })
943 . Else (() => {
944 // WAITING: home, lying on the surface until its voxel is solid again (the cell waits for its other
945 // grains), then it fades over `landedFade`. Cut again by the brush -> leaves again.
946 pos. assign (restWorld);
947 If (erosion.u.reconstruct. greaterThan ( 0.5 ), () => {
948 state. assign ( DORMANT );
949 H .w. assign ( 0 );
950 size. assign ( 0 );
951 })
952 . ElseIf (erRest. lessThan (threshold), () => {
953 If ( H .w. lessThan ( 0.5 ), () => {
954 H .w. assign (u.time);
955 });
956 const f = saturate (u.time. sub ( H .w). div ( max (u.landedFade, 0.01 )));
957 size. assign (baseSize. mul ( float ( 1 ). sub (f)));
958 If (f. greaterThanEqual ( 1.0 ), () => {
959 state. assign ( DORMANT );
960 H .w. assign ( 0.0 );
961 size. assign ( 0.0 );
962 });
963 })
964 . Else (() => {
965 size. assign (baseSize);
966 H .w. assign ( 0.0 );
967 If (hitRecent. and (u.fieldActive. greaterThan ( 0.5 )), () => {
968 eject ();
969 });
970 });
971 });
972
973 // lost-grain guard: a grain that is not a finite number any more, or has drifted far off screen, is put
974 // back near its rest so the state machine can land it (nothing is ever lost for good)
975 If (state. greaterThan ( 0.5 ). and (state. lessThan ( 2.5 )), () => {
976 const finite = abs (pos.x)
977 . lessThan ( 1e30 )
978 . and ( abs (pos.y). lessThan ( 1e30 ))
979 . and ( abs (pos.z). lessThan ( 1e30 ))
980 . and ( abs (vel.x). lessThan ( 1e30 ))
981 . and ( abs (vel.y). lessThan ( 1e30 ))
982 . and ( abs (vel.z). lessThan ( 1e30 ));
983 const far = length (pos. sub (centreWorld)). greaterThan (u.lostRadius);
984 If (finite. not (). or (far), () => {
985 pos. assign (restWorld. add (restNormalWorld. mul ( 0.05 )));
986 vel. assign ( vec3 ( 0.0 ));
987 });
988 });
989 P .xyz. assign (pos);
990 P .w. assign (size);
991 Vv.xyz. assign (vel);
992 Vv.w. assign (age);
993 M .x. assign (state);
994
995 // Active-particle statistics and density accumulation (ghosts are never drawn)
996 If (state. greaterThan ( 0.5 ). and (state. lessThan ( 3.5 )). or (state. greaterThan ( 4.5 )), () => {
997 const variant = uint ( floor (seed. mul ( 1000.0 ))). mod ( uint ( V ));
998 atomicAdd ( counter (variant), uint ( 1 ));
999 const gp = pos. add (u.densityExtent). div (u.densityExtent. mul ( 2.0 )). mul ( float ( G ));
1000 const gi = ivec3 ( floor (gp));
1001 If (
1002 gi.x
1003 . greaterThanEqual ( 0 )
1004 . and (gi.y. greaterThanEqual ( 0 ))
1005 . and (gi.z. greaterThanEqual ( 0 ))
1006 . and (gi.x. lessThan ( G ))
1007 . and (gi.y. lessThan ( G ))
1008 . and (gi.z. lessThan ( G )),
1009 () => {
1010 const idx = uint (gi.x)
1011 . add ( uint (gi.y). mul ( uint ( G )))
1012 . add ( uint (gi.z). mul ( uint ( G * G )));
1013 atomicAdd ( densityAt (idx), classWeight (cls));
1014 },
1015 );
1016 });
1017 })(). compute (dispatchCount, [ 64 ]);
1018 this .nodes.update = buildUpdate (instanceIndex, N );
1019 this .nodes.updateStrays =
1020 this .o.strays > 0 ? buildUpdate (instanceIndex. add ( uint (count)), this .o.strays) : null ;
1021
1022 // density resolve: density, light transmittance, ao -> 3D texture; then clear the atomic grid
1023 const gCoord = () => uvec3 (i. mod ( uint ( G )), i. div ( uint ( G )). mod ( uint ( G )), i. div ( uint ( G * G )));
1024 const loadD = ( c : N ) => {
1025 const cc = clamp ( ivec3 (c), ivec3 ( 0 ), ivec3 ( G - 1 ));
1026 const idx = uint (cc.x)
1027 . add ( uint (cc.y). mul ( uint ( G )))
1028 . add ( uint (cc.z). mul ( uint ( G * G )));
1029 return float ( atomicLoad ( densityAt (idx))). mul (u.densityScale);
1030 };
1031 this .nodes.resolve = Fn (() => {
1032 const c = gCoord ();
1033 const d = loadD ( ivec3 (c)). toVar ();
1034 const dens = float ( 1 ). sub ( exp (d. negate ()));
1035 // transmittance toward the key light (6 steps in grid space)
1036 const Lg = u.lightDir. mul ( float ( G )). div (u.densityExtent. mul ( 2.0 )). toVar ();
1037 const acc = float ( 0 ). toVar ();
1038 const q = vec3 (c). add ( 0.5 ). toVar ();
1039 Loop ( 6 , () => {
1040 q. addAssign (Lg. mul ( 0.9 ));
1041 acc. addAssign ( loadD ( ivec3 ( floor (q))));
1042 });
1043 const T = exp (acc. mul ( 0.9 ). negate ());
1044 // local density for ao
1045 const nb = loadD ( ivec3 (c). add ( ivec3 ( 1 , 0 , 0 )))
1046 . add ( loadD ( ivec3 (c). add ( ivec3 ( - 1 , 0 , 0 ))))
1047 . add ( loadD ( ivec3 (c). add ( ivec3 ( 0 , 1 , 0 ))))
1048 . add ( loadD ( ivec3 (c). add ( ivec3 ( 0 , - 1 , 0 ))))
1049 . add ( loadD ( ivec3 (c). add ( ivec3 ( 0 , 0 , 1 ))))
1050 . add ( loadD ( ivec3 (c). add ( ivec3 ( 0 , 0 , - 1 ))));
1051 const ao = float ( 1 ). sub ( exp (nb. div ( 6.0 ). add (d). mul ( 1.2 ). negate ()));
1052 textureStore ( this .densityTex, c, vec4 (dens, T , ao, 1.0 )). toWriteOnly ();
1053 })(). compute ( G * G * G , [ 64 ]);
1054 this .nodes.clearDensity = Fn (() => {
1055 atomicStore ( densityAt (i), uint ( 0 ));
1056 })(). compute ( G * G * G , [ 64 ]);
1057
1058 // FROST: after a retarget, in-flight grains (active/healing) count toward their new cell so the last one home
1059 // restores it; landed (waiting) grains go dormant at their new home; dormant/ghost grains sit in the eroded
1060 // shape, threshold raised to keep the filled field from ejecting them (restoreThresholds() re-arms).
1061 this .nodes.retargetCount = Fn (() => {
1062 const M = B .meta. element (i),
1063 R = B .rest. element (i),
1064 P = B .pos. element (i);
1065 const state = M .x. toVar ();
1066 If (state. greaterThan ( 4.5 ), () => {
1067 M .x. assign ( DORMANT );
1068 P .w. assign ( 0.0 );
1069 B .heal. element (i). assign ( vec4 ( 0.0 ));
1070 state. assign ( DORMANT );
1071 });
1072 If (state. greaterThan ( 0.5 ). and (state. lessThan ( 2.5 )), () => {
1073 // While returning, the ejection threshold is unused. Pack the stable group
1074 // phase above 2 here, avoiding a ninth storage binding on default WebGPU.
1075 // restoreThresholds() reinstates the original value before the next breakup.
1076 If (
1077 erosion.u.reconstruct
1078 . greaterThan ( 0.5 )
1079 . and (u.groupStagger. greaterThan ( 0 ). or (u.groupNoise. greaterThan ( 1.5 ))),
1080 () => {
1081 R .w. assign (
1082 float ( 2 ). add (
1083 select (
1084 u.groupNoise. greaterThan ( 1.5 ),
1085 assemblyFrontPhase ( R .xyz, u),
1086 returnGroupPhase ( R .xyz, u.groupScale, u.groupSeed, u.groupNoise),
1087 ),
1088 ),
1089 );
1090 },
1091 );
1092 const gc = ivec3 ( floor (erosion. uvw ( R .xyz). mul ( float ( FR ))));
1093 const cellHash = cellTexNode. load ( clamp (gc, ivec3 ( 0 ), ivec3 ( FR - 1 ))). level ( 0 ).w;
1094 atomicAdd (
1095 B .atomics. element ( uint ( FLO ). add ( uint ( clamp (cellHash. mul ( 65535.0 ), 0.0 , 65535.0 )))),
1096 uint ( 1 ),
1097 );
1098 }). ElseIf (state. lessThan ( 0.5 ). or (state. greaterThan ( 3.5 ). and (state. lessThan ( 4.5 ))), () => {
1099 R .w. assign ( 2.0 );
1100 });
1101 })(). compute ( N , [ 64 ]);
1102
1103 this .nodes.reset = Fn (() => {
1104 const M = B .meta. element (i),
1105 Vv = B .vel. element (i),
1106 P = B .pos. element (i),
1107 R = B .rest. element (i);
1108 If ( M .x. lessThan ( 2.5 ). or ( M .x. greaterThan ( 3.5 )), () => {
1109 M .x. assign ( DORMANT );
1110 Vv. assign ( vec4 ( 0.0 ));
1111 P . assign ( vec4 ( R .xyz, 0.0 ));
1112 B .heal. element (i). assign ( vec4 ( 0.0 ));
1113 });
1114 })(). compute ( N , [ 64 ]);
1115 void count;
1116 void shape;
1117 }
1118
1119 // --------------------------------------------------------------------------------------------
1120 private buildMeshes () {
1121 const { variants : V , rand } = this .o;
1122 const N = this .total,
1123 u = this .u,
1124 B = this .buffers;
1125 const SP = D .powder.sprites;
1126 const sprite = !! SP .enabled;
1127 // sprite grains: one camera-facing quad per grain, textured from the shard atlas (silhouette, normal,
1128 // frost structure, thickness); mesh grains: the low-poly variants
1129 const geos = sprite
1130 ? Array. from ({ length: V }, () => new THREE . PlaneGeometry ( 2 , 2 ))
1131 : buildGrainVariants ( V , rand, D .powder.facetedGrains);
1132 const atlasTex = sprite ? getShardAtlas () : null ;
1133 const cellsArr = sprite
1134 ? uniformArray ( SHARD_CELLS . map (( c ) => new THREE . Vector2 (c[ 0 ], c[ 1 ])))
1135 : null ;
1136 const posRO = storage ( B .pos.value, "vec4" , N ). toReadOnly ();
1137 const velRO = storage ( B .vel.value, "vec4" , N ). toReadOnly ();
1138 const metaRO = storage ( B .meta.value, "vec4" , N ). toReadOnly ();
1139 const restRO = storage ( B .rest.value, "vec4" , N ). toReadOnly ();
1140 const healRO = storage ( B .heal.value, "vec4" , N ). toReadOnly ();
1141 const dens = texture3D ( this .densityTex);
1142
1143 for ( let v = 0 ; v < V ; v ++ ) {
1144 const geo = new THREE . InstancedBufferGeometry (). copy (
1145 geos[v] as any ,
1146 ) as THREE . InstancedBufferGeometry ;
1147 // Draw a stable partition of the particle buffer. Dormant/ghost grains
1148 // collapse in the vertex shader, so visibility never depends on the
1149 // compute-generated compact list or indirect argument buffer.
1150 geo.instanceCount = Math. max ( 0 , Math. ceil (( N - v) / V ));
1151 const mat = new THREE . MeshPhysicalNodeMaterial ();
1152 mat.metalness = 0 ;
1153 mat.side = THREE .DoubleSide;
1154 mat.forceSinglePass = true ;
1155 const pIdx = instanceIndex. mul ( uint ( V )). add ( uint (v));
1156 const P = posRO. element (pIdx),
1157 Vv = velRO. element (pIdx),
1158 M = metaRO. element (pIdx);
1159 const seed = M .y,
1160 cls = M .z,
1161 state = M .x,
1162 age = Vv.w;
1163 const axis = normalize (
1164 vec3 ( hashSeed (seed, 11 ), hashSeed (seed, 12 ), hashSeed (seed, 13 )). sub ( 0.5 ),
1165 );
1166 const spin = hashSeed (seed, 14 )
1167 . mul ( 6.2831 )
1168 . add (age. mul (u.tumble). mul ( float ( 0.4 ). add ( hashSeed (seed, 15 ))));
1169 const camDist = length ( P .xyz. sub (cameraPosition));
1170 const drawable = state. greaterThan ( 0.5 ). and (state. lessThan ( 3.5 )). or (state. greaterThan ( 4.5 ));
1171 const size = select (
1172 drawable. and ( P .w. greaterThan ( 1e-5 )),
1173 max ( P .w, camDist. mul (u.pixelWorld). mul (u.minPixel)),
1174 float ( 0 ),
1175 ). mul (u.fade);
1176 // density shading (varying: sample at the grain centre)
1177 const guv = P .xyz. add (u.densityExtent). div (u.densityExtent. mul ( 2.0 ));
1178 const dSample = varying (dens. sample (guv). level ( 0 ));
1179 const shadow = mix ( float ( 1 ), dSample.g, u.shadowStrength);
1180 const ao = float ( 1 ). sub (dSample.b. mul (u.aoStrength));
1181 let vSeed : N ;
1182 const I = this .o.iceUniforms,
1183 features = this .o.iceFeatures;
1184 const surfaceFeatures = {
1185 ... features,
1186 // The solid's diagnostic bump switch preserves the existing shard finish.
1187 surfaceBumps: float ( 1 ),
1188 frost: features.frost. mul (features.shardFrost),
1189 crystals: features.crystals. mul (features.shardNormals),
1190 grain: features.grain. mul (features.shardNormals),
1191 micro: features.micro. mul (features.shardNormals),
1192 ripples: features.ripples. mul (features.shardNormals),
1193 };
1194 const homeGradient = sdfNormalNode ( this .o.shape, restRO. element (pIdx).xyz, 0.02 );
1195 const homeInfo = varying (
1196 vec4 ( select ( length (homeGradient). greaterThan ( 0.001 ), homeGradient, vec3 ( 0 , 0 , 1 )), size),
1197 );
1198 const homeNormal = homeInfo.xyz;
1199 const patch = varying (restRO. element (pIdx).xyz. add ( attribute ( "position" , "vec3" ). mul (size)));
1200 const objectSeed = vec3 ( this .o.seed * 0.731 , this .o.seed * 0.137 , this .o.seed * 0.529 );
1201 const surface = iceSurface (
1202 patch,
1203 homeNormal,
1204 objectSeed,
1205 iceSmudgeDirections ( this .o.seed),
1206 this .o.erosion,
1207 I ,
1208 surfaceFeatures,
1209 );
1210 const inheritedFrost = surface.frost;
1211 const inheritedDetail =
1212 ICE_VARIANT === "photographic"
1213 ? iceInclusions (
1214 this .o.fractureDetail,
1215 patch,
1216 homeNormal,
1217 homeNormal. negate (),
1218 homeInfo.w,
1219 I .inclusionScale,
1220 I .inclusionAmount,
1221 ). mul (u.surfaceDetail)
1222 : float ( 0 );
1223 const viewDir = normalize (positionView. negate ());
1224 let nView : N ,
1225 wrapL : N ,
1226 frost : N = float ( 0 ),
1227 thick : N = float ( 1 );
1228 if (sprite) {
1229 // --- quad in view space: in-plane spin (tumble) + a random tilt off the camera plane, mirrored at random
1230 const cellIdx = int ( floor ( hashSeed (seed, 31 ). mul ( SHARD_GRID * SHARD_GRID )));
1231 const ext = cellsArr. element (cellIdx);
1232 const flipX = select ( hashSeed (seed, 32 ). lessThan ( 0.5 ), float ( - 1 ), float ( 1 ));
1233 const flipY = select ( hashSeed (seed, 33 ). lessThan ( 0.5 ), float ( - 1 ), float ( 1 ));
1234 const tiltAng = hashSeed (seed, 34 ). mul (u.spriteTilt);
1235 const phi = hashSeed (seed, 35 ). mul ( 6.2831 );
1236 const tiltAxis = vec3 ( cos (phi), sin (phi), 0.0 );
1237 const rot = ( q : N ) => rotateAxis ( rotateAxis (q, vec3 ( 0 , 0 , 1 ), spin), tiltAxis, tiltAng);
1238 // camera-facing basis (view space); the mirror is folded into the signs
1239 const bR0 = rot ( vec3 (flipX, 0.0 , 0.0 )),
1240 bF0 = rot ( vec3 ( 0.0 , 0.0 , 1.0 ));
1241 // on the way home the shard turns to lie on the surface it lands on: the basis blends toward the
1242 // tangent frame of the SDF normal at its rest point (a landed, WAITING shard is fully aligned)
1243 const R4 = restRO. element (pIdx);
1244 const restW = u.model. mul ( vec4 ( R4 .xyz, 1.0 )).xyz;
1245 const nRestW0 = u.normalMat. mul ( sdfNormalNode ( this .o.shape, R4 .xyz, 0.004 ));
1246 const nRestW = nRestW0. div ( max ( length (nRestW0), 1e-6 ));
1247 const nRestV0 = normalize (
1248 mat3 (cameraViewMatrix)
1249 . mul (nRestW)
1250 . add ( vec3 ( 0.0 , 0.0 , 1e-4 )),
1251 );
1252 const nRestV = select (nRestV0.z. lessThan ( 0.0 ), nRestV0. negate (), nRestV0); // the side the camera sees
1253 const healingNow = state. greaterThan ( 1.5 ). and (state. lessThan ( 2.5 ));
1254 const waitingNow = state. greaterThan ( 4.5 );
1255 const distHome = length (restW. sub ( P .xyz));
1256 // FROST: a returning shard turns to lie on the surface over its whole flight home (fraction of the start
1257 // distance covered, shaped by alignCurve), instead of snapping within alignDist of the surface
1258 const H4 = healRO. element (pIdx);
1259 const startDist = max ( length (restW. sub ( H4 .xyz)), 0.05 );
1260 const homeFrac = saturate ( float ( 1 ). sub (distHome. div (startDist)));
1261 const align = select (
1262 waitingNow,
1263 float ( 1 ),
1264 select (healingNow, pow (homeFrac, u.alignCurve), float ( 0 )),
1265 ). mul (u.alignAmt);
1266 const bRt = normalize (bR0. sub (nRestV. mul ( dot (bR0, nRestV))). add ( vec3 ( 1e-4 , 0.0 , 0.0 )));
1267 const bF1 = normalize ( mix (bF0, nRestV, align));
1268 const bR1a = normalize ( mix (bR0, bRt, align));
1269 const bR1 = normalize (bR1a. sub (bF1. mul ( dot (bR1a, bF1))));
1270 const bU1 = cross (bF1, bR1). mul (flipX. mul (flipY));
1271 const offV = bR1
1272 . mul (positionLocal.x. mul (ext.x). mul (flipX))
1273 . add (bU1. mul (positionLocal.y. mul (ext.y). mul (flipY)))
1274 . mul (size)
1275 . mul (u.spriteSize);
1276 mat.positionNode = P .xyz. add ( mat3 (u.camWorld). mul (offV));
1277 const bR = varying (bR1);
1278 const bU = varying (bU1);
1279 const bF = varying (bF1);
1280 // One location instead of four scalar varyings. Leave room for the
1281 // physical lighting and motion-vector outputs on 16-location GPUs.
1282 const atlasInfo = varying ( vec4 ( float (cellIdx), flipX, flipY, seed));
1283 const vCell = atlasInfo.x,
1284 vFx = atlasInfo.y,
1285 vFy = atlasInfo.z;
1286 vSeed = atlasInfo.w;
1287 // --- atlas fetch
1288 const uvL = uv ();
1289 const uS = mix (uvL.x, float ( 1 ). sub (uvL.x), step (vFx, 0.0 ));
1290 const vS = mix (uvL.y, float ( 1 ). sub (uvL.y), step (vFy, 0.0 ));
1291 const ci = int (vCell. add ( 0.5 ));
1292 const cx = float (ci. mod ( int ( SHARD_GRID ))),
1293 cy = float (ci. div ( int ( SHARD_GRID )));
1294 const extF = cellsArr. element (ci);
1295 const auv = vec2 (
1296 cx. add ( 0.5 ). add (uS. sub ( 0.5 ). mul (extF.x)),
1297 cy. add ( 0.5 ). add (vS. sub ( 0.5 ). mul (extF.y)),
1298 ). div ( SHARD_GRID );
1299 const smp = texture (atlasTex, auv);
1300 const nxy = smp.rg. mul ( 2.0 ). sub ( 1.0 ). mul (u.spriteNormal);
1301 const nz = sqrt ( saturate ( float ( 1 ). sub ( dot (nxy, nxy))));
1302 nView = normalize (bR. mul (nxy.x). add (bU. mul (nxy.y)). add (bF. mul (nz)));
1303 // Rotate the inherited surface perturbation into the shard's facet frame.
1304 const detailNormal = surface.nSurface. sub (homeNormal);
1305 nView = normalize (
1306 nView. add (bR. mul (detailNormal.x)). add (bU. mul (detailNormal.y)). add (bF. mul (detailNormal.z)),
1307 );
1308 mat.normalNode = nView;
1309 mat.maskNode = smp.a. greaterThan (u.spriteCut);
1310 frost = clamp (
1311 inheritedFrost
1312 . mul ( I .frostDiffuse)
1313 . add (surface.smudge. mul ( I .smudgeWhite))
1314 . add (inheritedDetail. mul ( 0.65 )),
1315 0 ,
1316 0.65 ,
1317 );
1318 thick = saturate (smp.a. sub ( 0.35 ). div ( 0.65 ));
1319 if ( SP .seeThrough > 0 ) {
1320 // real see-through: the clear parts of a shard blend over whatever is behind them (ice included)
1321 mat.transparent = true ;
1322 mat.depthWrite = false ;
1323 const fresS = pow ( saturate ( float ( 1 ). sub ( saturate ( dot (nView, viewDir)))), u.fresnelPower);
1324 // Thin-sheet optical coverage, not opaque white atlas paint.
1325 const opticalCover = mix ( float ( 0.42 ), float ( 0.85 ), frost)
1326 . add (fresS. mul ( 0.45 ))
1327 . add ( float ( 1 ). sub (thick). mul ( 0.18 ));
1328 mat.opacityNode = mix (
1329 float ( 1 ),
1330 saturate (opticalCover),
1331 u.spriteSee. mul (features.transmission). mul (features.shardTransmission),
1332 );
1333 }
1334 wrapL = saturate ( dot (nView, u.lightDirView). add (u.wrap). div ( float ( 1 ). add (u.wrap)));
1335 } else {
1336 vSeed = varying (seed);
1337 const local = rotateAxis (positionLocal. mul (size), axis, spin);
1338 mat.positionNode = P .xyz. add (local);
1339 const nLocal = rotateAxis (normalLocal, axis, spin);
1340 mat.normalNode = transformNormalToView (nLocal);
1341 nView = normalize ( transformNormalToView (nLocal));
1342 wrapL = saturate ( dot ( normalize (nLocal), u.lightDir). add (u.wrap). div ( float ( 1 ). add (u.wrap)));
1343 }
1344 if ( ! sprite)
1345 frost = clamp (
1346 inheritedFrost
1347 . mul ( I .frostDiffuse)
1348 . add (surface.smudge. mul ( I .smudgeWhite))
1349 . add (inheritedDetail. mul ( 0.65 )),
1350 0 ,
1351 0.65 ,
1352 );
1353 const f0 = pow ( I .ior. sub ( 1 ). div ( I .ior. add ( 1 )), 2 );
1354 const fres = f0. add (
1355 float ( 1 )
1356 . sub (f0)
1357 . mul ( pow ( saturate ( float ( 1 ). sub ( saturate ( dot (nView, viewDir)))), 5 )),
1358 );
1359 const surfaceDensity =
1360 ICE_VARIANT === "photographic"
1361 ? iceDetail ( this .o.fractureDetail, patch, homeNormal, I .inclusionScale). mul (
1362 surfaceFeatures.frost,
1363 )
1364 : float ( 0 );
1365 const roughness = clamp (surface.roughness. add (surfaceDensity. mul ( 0.22 )), 0.015 , 0.8 );
1366 mat.roughnessNode = roughness;
1367 mat.iorNode = I .ior;
1368 mat.clearcoatNode = I .clearcoat. mul (features.clearcoat). mul (features.shardReflections);
1369 mat.clearcoatRoughnessNode = I .clearcoatRough;
1370 mat.specularIntensityNode = I .specularIntensity
1371 . mul (features.reflections)
1372 . mul (features.shardReflections);
1373 const reflectionOn = features.reflections. mul (features.shardReflections);
1374 const transmissionOn = features.transmission. mul (features.shardTransmission);
1375 const viewToWorld = ( q : N ) => normalize ( mat3 (u.camWorld). mul (q));
1376 const env = ( q : N ) => iceEnvironment ( this .o.environment, I .envStrength, q, roughness);
1377 const ray = viewDir. negate ();
1378 const transmittedRay = refract (ray, nView, float ( 1 ). div ( max ( I .ior, 1.001 )));
1379 // Detached pieces are thin volumes: use atlas thickness and world size,
1380 // rather than tracing the intact object's SDF after they have left it.
1381 const opticalDepth = max (homeInfo.w. mul (thick). mul ( I .thicknessScale), 0.001 );
1382 const attenuation = exp (
1383 log ( max ( I .attColor, vec3 ( 0.001 )))
1384 . mul (opticalDepth. div ( max ( I .attDist, 0.01 )))
1385 . mul (features.absorption),
1386 );
1387 const refractedUV = clamp (
1388 screenUV. add (transmittedRay.xy. sub (ray.xy). mul (opticalDepth). mul ( 0.08 )),
1389 0 ,
1390 1 ,
1391 );
1392 const plate = backdropColorAt ( this .o.backdrop, refractedUV)
1393 . add ( env ( viewToWorld (transmittedRay)). mul ( I .backlight))
1394 . mul (attenuation);
1395 const reflected = env ( viewToWorld ( reflect (ray, nView)))
1396 . mul (fres)
1397 . mul (reflectionOn)
1398 . mul ( I .specularIntensity);
1399 const cover = max (frost, float ( 1 ). sub (transmissionOn));
1400 const scatter = I .keyColor
1401 . mul (
1402 surface.surfCrack
1403 . mul ( I .crackBright)
1404 . mul ( 0.3 )
1405 . add (inheritedDetail. mul ( I .backlight). mul ( 0.8 )),
1406 )
1407 . mul (features.scatter);
1408 mat.colorNode = I .baseColor;
1409 mat.emissiveNode = I .keyColor
1410 . mul (cover. mul ( I .interiorScatter). mul ( 0.3 ))
1411 . mul (features.scatter);
1412 mat.outputNode = Fn (() => {
1413 const clear = reflected. add (plate. mul ( float ( 1 ). sub (fres. mul (reflectionOn))));
1414 const radiance = clear. mul ( float ( 1 ). sub (cover)). add (output.rgb. mul (cover)). add (scatter);
1415 // Preserve physical radiance through the thin-sheet blend.
1416 const a = max (output.a, 0.001 );
1417 const background = backdropColorAt ( this .o.backdrop, screenUV);
1418 return vec4 ( max (radiance. sub (background. mul ( float ( 1 ). sub (a))), vec3 ( 0 )). div (a), output.a);
1419 })();
1420 const mesh = new THREE . Mesh (geo, mat);
1421 mesh.frustumCulled = false ;
1422 mesh.castShadow = true ;
1423 mesh.receiveShadow = true ;
1424 this .meshes. push (mesh);
1425 this .group. add (mesh);
1426 if (v === 0 ) this .material = mat;
1427 }
1428 }
1429
1430 /** Additive volumetric haze over the density grid (evaluated in the post graph). */
1431 hazeNode ( depthNode : N , viewZ : N , _camera : THREE . PerspectiveCamera ) : N {
1432 const u = this .u;
1433 const dens = texture3D ( this .densityTex);
1434 return Fn (() => {
1435 // screenUV is y-down on the WebGPU post quad: NDC y must be negated or the march mirrors vertically
1436 const ndc0 = screenUV. mul ( 2.0 ). sub ( 1.0 );
1437 const ndc = vec2 (ndc0.x, ndc0.y. negate ());
1438 const clip = u.invProj. mul ( vec4 (ndc.x, ndc.y, 1.0 , 1.0 ));
1439 const vView = clip.xyz. div (clip.w);
1440 const dirView = normalize (vView);
1441 const dir = normalize ( mat3 (u.camWorld). mul (dirView));
1442 const ro = u.camPos;
1443 // ray-box
1444 const ext = u.densityExtent;
1445 const inv = vec3 ( 1 ). div (dir);
1446 const t0 = ext. negate (). sub (ro). mul (inv),
1447 t1 = ext. sub (ro). mul (inv);
1448 const tmin = max ( max ( min (t0.x, t1.x), min (t0.y, t1.y)), min (t0.z, t1.z));
1449 const tmax = min ( min ( max (t0.x, t1.x), max (t0.y, t1.y)), max (t0.z, t1.z));
1450 // scene depth along the ray
1451 const zRatio = float ( 1 ). div ( max (dirView.z. negate (), 1e-4 ));
1452 const tScene = viewZ. negate (). mul (zRatio);
1453 const tEnd = min (tmax, tScene);
1454 const acc = float ( 0 ). toVar ();
1455 If (tEnd. greaterThan ( max (tmin, 0.0 )). and (u.hazeOn. greaterThan ( 0.5 )), () => {
1456 const tStart = max (tmin, 0.0 );
1457 const steps = max (u.hazeSteps, 4.0 );
1458 const dtt = tEnd. sub (tStart). div (steps);
1459 const jitter = hash31 ( vec3 (screenUV. mul ( 1234.0 ), u.time)). mul (dtt);
1460 const tt = tStart. add (jitter). toVar ();
1461 Loop ({ start: int ( 0 ), end: int (steps), type: "int" , condition: "<" }, () => {
1462 const pw = ro. add (dir. mul (tt));
1463 const guv = pw. add (ext). div (ext. mul ( 2.0 ));
1464 const s = dens. sample (guv). level ( 0 );
1465 acc. addAssign (s.r. mul (s.g). mul (dtt));
1466 tt. addAssign (dtt);
1467 });
1468 });
1469 const col = u.tone. mul (u.keyColor). mul (u.keyIntensity). mul (acc). mul ( 0.35 );
1470 return vec4 (col, 0.0 );
1471 })();
1472 void depthNode;
1473 }
1474
1475 // --------------------------------------------------------------------------------------------
1476 step (
1477 renderer : THREE . WebGPURenderer ,
1478 t : number ,
1479 dt : number ,
1480 inter : Interaction ,
1481 key : THREE . DirectionalLight ,
1482 idle : { fieldActive : boolean ; powderActive : boolean },
1483 ) {
1484 const P = D .powder,
1485 H = D .healing,
1486 u = this .u;
1487 u.fieldActive.value = idle.fieldActive ? 1 : 0 ;
1488 u.hazeOn.value = idle.powderActive ? 1 : 0 ;
1489 u.dt.value = Math. min (dt, 1 / 30 );
1490 u.time.value = t;
1491 const model = this .o.objectGroup.matrixWorld;
1492 u.model.value. copy (model);
1493 u.previousModel.value. copy ( this .prevModel);
1494 u.normalMat.value. getNormalMatrix (model);
1495 u.modelDelta.value. copy (model). multiply ( this .inversePrevModel. copy ( this .prevModel). invert ());
1496 this .prevModel. copy (model);
1497 u.strokeDir.value. copy (inter.strokeDir);
1498 u.strokeSpeed.value = inter.strokeSpeed;
1499 u.heal.value = sim.healing ? 1 : 0 ;
1500 u.fade.value = sim.fade;
1501 u.lightDir.value. copy (key.position). sub (key.target.position). normalize ();
1502 u.modelInv.value. copy (model). invert ();
1503 if ( D .version !== this .settingsVersion) {
1504 this .settingsVersion = D .version;
1505 u.ejectSpeed.value = P .ejectSpeed;
1506 u.ejectSpread.value = P .ejectSpread;
1507 u.ejectTurb.value = P .ejectTurbulence;
1508 u.backwardRatio.value = P .backwardRatio;
1509 u.clumpJitter.value = P .clumpSpeedJitter;
1510 u.drag.value = P .drag;
1511 u.gravity.value = P .gravity;
1512 u.turbulence.value = P .turbulence;
1513 u.turbScale.value = P .turbulenceScale;
1514 u.turbDecay.value = P .turbulenceDecay;
1515 u.cohesion.value = P .clumpCohesion;
1516 u.settleTime.value = P .settleTime;
1517 u.settledDrift.value = P .settledDrift;
1518 u.tumble.value = P .tumble;
1519 u.returnDuration.value = H .returnDuration;
1520 u.returnCurve.value = H .returnCurve;
1521 u.inherit.value = P .inheritRotation ? 1 : 0 ;
1522 u.inheritTime.value = P .inheritTime / 1000 ;
1523 u.straySpeed.value = P .straySpeed;
1524 u.dustScale.value = P .grainSizes.tinyDustSize;
1525 u.grainScale.value = P .grainSizes.smallGrainSize;
1526 u.clumpScale.value = P .grainSizes.mediumClumpSize;
1527 u.fragmentScale.value = P .grainSizes.largeFragmentSize;
1528 u.sizeJitter.value = P .grainSizes.sizeJitter;
1529 u.sizeMul.value = P .grainSizeMultiplier;
1530 u.amount.value = P .amount;
1531 u.minEject.value = P .minEjectSpeed;
1532 u.minPixel.value = P .minPixelSize;
1533 u.waveTime.value = H .waveTime;
1534 u.waveJitter.value = H .waveJitter;
1535 u.ghostDist.value = H .ghostReturnDistance;
1536 u.ghostFrac.value = H .ghostFlightFraction;
1537 u.waveReach.value = H .waveReach;
1538 u.depositRadius.value = H .depositRadius;
1539 u.returnMode.value = H .returnMode === "spring" ? 1 : 0 ;
1540 u.springK.value = H .returnSpring;
1541 u.springDamp.value = H .returnDamping;
1542 u.springRamp.value = H .returnRamp;
1543 u.landRadius.value = H .landRadius;
1544 u.returnAfter.value = H .returnAfter;
1545 u.maxSpeed.value = P .maxSpeed;
1546 u.returnDrag.value = H .returnDrag;
1547 u.returnMaxSpeed.value = H .returnMaxSpeed;
1548 u.lostRadius.value = P .lostRadius;
1549 u.stragglers.value = H .cellStragglers;
1550 u.spriteSee.value = P .sprites.seeThrough;
1551 u.alignAmt.value = H .alignToSurface;
1552 u.alignDist.value = H .alignDistance;
1553 u.alignCurve.value = ( H as any ).alignCurve ?? 1 ;
1554 u.landedFade.value = H .landedFade;
1555 u.landShrink.value = H .landShrink;
1556 u.densityExtent.value = P .densityExtent;
1557 u.shadowStrength.value = P .densityShadowStrength;
1558 u.aoStrength.value = P .densityAOStrength;
1559 u.densityScale.value = 0.02 * ( this .o.densityRes / 64 ) ** 3 * ( 1_000_000 / this .total) ** 0.5 ;
1560 u.tone.value. set ( P .baseTone);
1561 u.wrap.value = P .wrap;
1562 u.keyIntensity.value = D .lighting.key.intensity;
1563 u.keyColor.value. set ( D .lighting.key.color);
1564 const F = P .fragments;
1565 u.translucency.value = F .translucency;
1566 u.throughTint.value. set ( F .throughTint);
1567 u.fresnelPower.value = F .fresnelPower;
1568 u.fragRough.value = F .roughness;
1569 u.fragClearcoat.value = F .clearcoat;
1570 u.fragSpecular.value = F .specular;
1571 u.sparkle.value = F .sparkle;
1572 u.sparkleFraction.value = F .sparkleFraction;
1573 u.sparkleSpread.value = F .sparkleSpread;
1574 const SPu = P .sprites;
1575 u.spriteSize.value = SPu.sizeScale;
1576 u.spriteTilt.value = THREE .MathUtils. degToRad (SPu.tilt);
1577 u.spriteNormal.value = SPu.normalStrength;
1578 u.spriteFrost.value = SPu.frostFromAtlas;
1579 u.spriteFrostBoost.value = SPu.frostBoost;
1580 u.spriteFrostRough.value = SPu.frostRoughness;
1581 u.spriteEdge.value = SPu.edgeLight;
1582 u.spriteCut.value = SPu.alphaCut;
1583 // Material-panel switches change shading only; saved strengths and simulation survive.
1584 const features = D .ice.features;
1585 u.surfaceFrost.value = Number (features.shardFrost && features.frost);
1586 u.surfaceDetail.value = Number (features.shardFrost && features.scatter);
1587 if ( ! features.shardNormals) u.spriteNormal.value = 0 ;
1588 if ( ! features.shardFrost) {
1589 u.spriteFrost.value = 0 ;
1590 u.spriteFrostBoost.value = 0 ;
1591 }
1592 if ( ! features.shardTransmission) {
1593 u.translucency.value = 0 ;
1594 u.spriteSee.value = 0 ;
1595 }
1596 if ( ! features.shardReflections) {
1597 u.fragSpecular.value = 0 ;
1598 u.fragClearcoat.value = 0 ;
1599 }
1600 if ( ! features.shardSparkle) u.sparkle.value = 0 ;
1601 if ( ! features.shardEdges) u.spriteEdge.value = 0 ;
1602 u.repel.value = P .repelFromObject ? 1 : 0 ;
1603 u.repelStrength.value = P .repelStrength;
1604 u.repelRange.value = P .repelRange;
1605 u.repelRadial.value = P .repelRadial;
1606 u.repelRadialRange.value = P .repelRadialRange;
1607 u.colorByState.value = D .debug.colorByState ? 1 : 0 ;
1608 u.colorBySize.value = D .debug.colorBySize ? 1 : 0 ;
1609 u.colorByAge.value = D .debug.colorByAge ? 1 : 0 ;
1610 u.hazeSteps.value = P .hazeSteps;
1611 if ( P .fragmentShadowMap !== this .fragmentShadowMap) {
1612 this .fragmentShadowMap = P .fragmentShadowMap;
1613 for ( const m of this .meshes) m.castShadow = P .fragmentShadowMap;
1614 }
1615 }
1616
1617 this . beforeIntegrate ?.();
1618 if (dt <= 0 ) return ;
1619 renderer. compute ( this .nodes.resetCounters);
1620 if (idle.powderActive || idle.fieldActive || ! this .nodes.updateStrays) {
1621 renderer. compute ( this .nodes.snapshotLeaders);
1622 renderer. compute ( this .nodes.update);
1623 renderer. compute ( this .nodes.resolve);
1624 renderer. compute ( this .nodes.clearDensity);
1625 this .densityDirty = true ;
1626 } else {
1627 // Nothing can move except the strays: update their positions and density counters only.
1628 renderer. compute ( this .nodes.updateStrays);
1629 if ( this .densityDirty) {
1630 renderer. compute ( this .nodes.resolve);
1631 renderer. compute ( this .nodes.clearDensity);
1632 this .densityDirty = false ;
1633 }
1634 }
1635
1636 this . beforeAssembly ?.();
1637 if ( this .assemblyEnabled && this .o.erosion.u.reconstruct.value > 0.5 && idle.fieldActive)
1638 this .assembly?. update (renderer);
1639
1640 // stats (async readback, throttled)
1641 this .statsTimer += dt;
1642 if ( this .statsTimer > 0.5 && ( D .debug.stats || D .performance.idleSkip)) {
1643 this .statsTimer = 0 ;
1644 const CO = this .o.countersOffset;
1645 renderer
1646 . getArrayBufferAsync ( this .buffers.atomics.value, null , CO * 4 , 16 * 4 )
1647 . then (( buf : ArrayBuffer ) => {
1648 const c = new Uint32Array (buf);
1649 let active = 0 ;
1650 for ( let v = 0 ; v < this .o.variants; v ++ ) active += c[v];
1651 sim.counts.total = this .total;
1652 sim.counts.active = Math. max ( 0 , active - this .o.strays);
1653 sim.counts.dormant = Math. max ( 0 , this .total - active);
1654 })
1655 . catch (() => {});
1656 }
1657 }
1658
1659 /** Update camera uniforms used by the haze raymarch (call before post render). */
1660 updateCamera ( camera : THREE . PerspectiveCamera , viewportHeightPx : number ) {
1661 this .u.pixelWorld.value =
1662 ( 2 * Math. tan ( THREE .MathUtils. degToRad (camera.fov / 2 ))) / Math. max ( 1 , viewportHeightPx);
1663 this .u.lightDirView.value
1664 . copy ( this .u.lightDir.value)
1665 . transformDirection (camera.matrixWorldInverse);
1666 this .u.camPos.value. copy (camera.position);
1667 this .u.invProj.value. copy (camera.projectionMatrixInverse);
1668 this .u.camWorld.value. copy (camera.matrixWorld);
1669 }
1670
1671 reset ( renderer : THREE . WebGPURenderer ) {
1672 renderer. compute ( this .nodes.reset);
1673 }
1674
1675 /** FROST: the object frame jumped on purpose (facing offset at a retarget): no inherited motion for that frame. */
1676 syncModel () {
1677 this .prevModel. copy ( this .o.objectGroup.matrixWorld);
1678 }
1679
1680 /**
1681 * FROST: give every grain a new home (object space, N x vec4, thresholds and strays preserved by the caller).
1682 * Call after the erosion field was re-baked for the new shape: the in-flight counters are rebuilt from it.
1683 */
1684 retarget ( renderer : THREE . WebGPURenderer , rest : Float32Array ) {
1685 const attr = this .buffers.rest.value;
1686 (attr.array as Float32Array ). set (rest);
1687 this . upload (renderer, attr);
1688 this .u.groupNoise.value = Number ( D .healing.returnNoiseAmount ?? 2 );
1689 this .u.frontDuration.value = Number (
1690 D .healing.assemblyFrontDuration ?? RETURN_GROUP_DEFAULTS .assemblyFrontDuration,
1691 );
1692 this .u.frontX.value = Number (
1693 D .healing.assemblyOriginX ?? RETURN_GROUP_DEFAULTS .assemblyOriginX,
1694 );
1695 this .u.frontY.value = Number (
1696 D .healing.assemblyOriginY ?? RETURN_GROUP_DEFAULTS .assemblyOriginY,
1697 );
1698 this .u.frontAngle.value = Number (
1699 D .healing.assemblyAngle ?? RETURN_GROUP_DEFAULTS .assemblyAngle,
1700 );
1701 this .u.frontSpread.value = Number (
1702 D .healing.assemblySpread ?? RETURN_GROUP_DEFAULTS .assemblySpread,
1703 );
1704 this .u.frontNoise.value = Number (
1705 D .healing.assemblyFrontNoise ?? RETURN_GROUP_DEFAULTS .assemblyFrontNoise,
1706 );
1707 this .u.speedVariation.value = Number (
1708 D .healing.assemblySpeedVariation ?? RETURN_GROUP_DEFAULTS .assemblySpeedVariation,
1709 );
1710 this .u.pathBend.value = Number ( D .healing.assemblyBend ?? RETURN_GROUP_DEFAULTS .assemblyBend);
1711 this .u.pathSwirl.value = Number ( D .healing.assemblySwirl ?? RETURN_GROUP_DEFAULTS .assemblySwirl);
1712 this .u.landingVariation.value = Number (
1713 D .healing.assemblyLandingVariation ?? RETURN_GROUP_DEFAULTS .assemblyLandingVariation,
1714 );
1715
1716 this .u.groupStagger.value =
1717 D .healing.returnGroupStagger ?? RETURN_GROUP_DEFAULTS .returnGroupStagger;
1718 this .u.groupScale.value = D .healing.returnGroupScale ?? RETURN_GROUP_DEFAULTS .returnGroupScale;
1719 this .u.groupSeed.value = D .healing.returnGroupSeed ?? RETURN_GROUP_DEFAULTS .returnGroupSeed;
1720 renderer. compute ( this .nodes.retargetCount);
1721 if ( this .o.erosion.u.reconstruct.value > 0.5 ) this .assembly?. prepare (renderer);
1722 }
1723
1724 prepareAssembly ( renderer : THREE . WebGPURenderer ) {
1725 this .assembly?. prepare (renderer);
1726 }
1727
1728 /** FROST: every grain may leave again (the thresholds retarget() raised come back from the initial buffer). */
1729 restoreThresholds ( renderer : THREE . WebGPURenderer ) {
1730 const attr = this .buffers.rest.value,
1731 a = attr.array as Float32Array ;
1732 for ( let i = 3 ; i < a. length ; i += 4 ) a[i] = this .restInit[i];
1733 this . upload (renderer, attr);
1734 }
1735
1736 private upload ( renderer : THREE . WebGPURenderer , attr : any ) {
1737 attr.needsUpdate = true ;
1738 // the storage binding is updated lazily by three; the compute below must already see the new homes
1739 const backend : any = (renderer as any ).backend;
1740 const gpu = backend?. get ?.(attr);
1741 if (gpu?.buffer && backend.device)
1742 backend.device.queue. writeBuffer (
1743 gpu.buffer,
1744 0 ,
1745 attr.array.buffer,
1746 attr.array.byteOffset,
1747 attr.array.byteLength,
1748 );
1749 }
1750
1751 dispose () {
1752 this .o.scene. remove ( this .group);
1753 for ( const m of this .meshes) {
1754 m.geometry. dispose ();
1755 (m.material as THREE . Material ). dispose ();
1756 }
1757 this .densityTex. dispose ();
1758 this .assembly?. dispose ();
1759 for ( const k of Object. keys ( this .buffers)) {
1760 const b = this .buffers[k];
1761 if (b?.value?.dispose) b.value. dispose ?.();
1762 }
1763 }
1764 }
1765 void abs;
1766 void sqrt;
1767 void fract;
1768 void Break;
1769 void Continue;
1770 void cross;
1771 void step;
1772 void negate;
1773 void mat4;
1774 void cameraPosition;
1775 void int;