Context
PR #3060 fixes Size over Lifetime TwoCurves, but the current 168-byte particle instance layout has no unused random component. a_Random0.z is therefore shared by Noise strength and Size over Lifetime; when both modules are enabled, Noise takes precedence and both shaders consume the same random factor. This makes two independent modules visibly correlated.
The instance buffer currently sends three vec4 random attributes (48 bytes per particle). Some modules require independent XYZ factors, so reclaiming a float by silently sharing another slot would only move the coupling elsewhere.
Goal
Give particle modules stable, independent random values while reducing or at least not increasing instance-buffer bandwidth and vertex attribute usage.
Options to evaluate
1. Per-particle seed + salted GPU derivation
Store one particle seed and derive module/axis values using stable salts, for example:
- Noise:
hash(seed + NOISE_SALT)
- Size:
hash(seed + SIZE_SALT)
- Velocity XYZ:
hash3(seed + VELOCITY_SALT)
- Force XYZ:
hash3(seed + FORCE_SALT)
This matches the ownership model used by Unity: one seed per particle, with module/curve IDs offsetting randomness.
Risks to validate:
- Vertex shader hash cost is paid per vertex, not once per instance; mesh particles may amplify the ALU cost substantially.
- WebGL1 lacks the integer/bitwise facilities used by common 32-bit hashes.
- CPU paths such as sub-emitter inherited size/color/rotation must reproduce the same derived values.
- Results must remain stable when unrelated modules are enabled or disabled.
2. Pack CPU-generated random factors
Keep generating factors once on the CPU, but store them as NormalizedUShort4 or NormalizedUByte4 instead of 32-bit floats.
A possible layout is:
StartSpeedGravity: two floats
- packed random 0: Color, Noise, Size, Rotation
- packed random 1: Texture Sheet, Velocity X/Y/Z
- packed random 2: Force X/Y/Z, Limit Velocity
This gives Size an independent channel without increasing attribute count. Estimated instance stride:
NormalizedUShort4: about 148 bytes, down from 168 bytes (~12%)
NormalizedUByte4: about 136 bytes, down from 168 bytes (~19%), with 8-bit interpolation precision
Benchmark matrix
Compare the current float attributes, packed factors, and seed/hash variants across:
- Billboard and mesh render modes
- Low- and high-vertex meshes
- Representative particle counts
- WebGL1 and WebGL2
- Mobile TBDR and desktop GPUs where available
Measure:
- CPU particle emission/update time
- Instance-buffer upload bytes and time
- Vertex GPU time / frame time
- Visual distribution and determinism
- CPU/GPU agreement for sub-emitter inheritance
Acceptance criteria
- Noise and Size over Lifetime have independent random factors when both are enabled.
- Enabling an unrelated module does not change another module's random sequence.
- No regression in WebGL1 compatibility.
- No increase in required vertex attribute count.
- The selected design is supported by benchmark evidence rather than bandwidth or ALU assumptions alone.
Related: #3060
Context
PR #3060 fixes Size over Lifetime
TwoCurves, but the current 168-byte particle instance layout has no unused random component.a_Random0.zis therefore shared by Noise strength and Size over Lifetime; when both modules are enabled, Noise takes precedence and both shaders consume the same random factor. This makes two independent modules visibly correlated.The instance buffer currently sends three
vec4random attributes (48 bytes per particle). Some modules require independent XYZ factors, so reclaiming a float by silently sharing another slot would only move the coupling elsewhere.Goal
Give particle modules stable, independent random values while reducing or at least not increasing instance-buffer bandwidth and vertex attribute usage.
Options to evaluate
1. Per-particle seed + salted GPU derivation
Store one particle seed and derive module/axis values using stable salts, for example:
hash(seed + NOISE_SALT)hash(seed + SIZE_SALT)hash3(seed + VELOCITY_SALT)hash3(seed + FORCE_SALT)This matches the ownership model used by Unity: one seed per particle, with module/curve IDs offsetting randomness.
Risks to validate:
2. Pack CPU-generated random factors
Keep generating factors once on the CPU, but store them as
NormalizedUShort4orNormalizedUByte4instead of 32-bit floats.A possible layout is:
StartSpeedGravity: two floatsThis gives Size an independent channel without increasing attribute count. Estimated instance stride:
NormalizedUShort4: about 148 bytes, down from 168 bytes (~12%)NormalizedUByte4: about 136 bytes, down from 168 bytes (~19%), with 8-bit interpolation precisionBenchmark matrix
Compare the current float attributes, packed factors, and seed/hash variants across:
Measure:
Acceptance criteria
Related: #3060