topazcube 0.1.31 → 0.1.35
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.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- package/src/utils.ts +0 -403
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// Particle simulation compute shader
|
|
2
|
+
// Updates particle positions, velocities, lifetimes, and handles spawning
|
|
3
|
+
// Also calculates per-particle lighting with temporal smoothing
|
|
4
|
+
|
|
5
|
+
const WORKGROUP_SIZE: u32 = 64u;
|
|
6
|
+
const MAX_EMITTERS: u32 = 16u;
|
|
7
|
+
const CASCADE_COUNT = 3;
|
|
8
|
+
const MAX_LIGHTS = 64u;
|
|
9
|
+
const MAX_SPOT_SHADOWS = 8;
|
|
10
|
+
const LIGHTING_FADE_TIME: f32 = 0.3; // Seconds to smooth lighting changes
|
|
11
|
+
|
|
12
|
+
// Spot shadow atlas constants
|
|
13
|
+
const SPOT_ATLAS_WIDTH: f32 = 2048.0;
|
|
14
|
+
const SPOT_ATLAS_HEIGHT: f32 = 2048.0;
|
|
15
|
+
const SPOT_TILE_SIZE: f32 = 512.0;
|
|
16
|
+
const SPOT_TILES_PER_ROW: i32 = 4;
|
|
17
|
+
|
|
18
|
+
// Particle data structure (80 bytes, matches JS PARTICLE_STRIDE)
|
|
19
|
+
// flags: bit 0 = alive, bit 1 = additive, bits 8-15 = emitter index
|
|
20
|
+
struct Particle {
|
|
21
|
+
position: vec3f, // World position
|
|
22
|
+
lifetime: f32, // Remaining life (seconds), <=0 = dead
|
|
23
|
+
velocity: vec3f, // Movement per second
|
|
24
|
+
maxLifetime: f32, // Initial lifetime for fade calculation
|
|
25
|
+
color: vec4f, // RGBA with computed alpha
|
|
26
|
+
size: vec2f, // Current width/height
|
|
27
|
+
rotation: f32, // Current rotation in radians
|
|
28
|
+
flags: u32, // Bit 0 = alive, bit 1 = additive, bits 8-15 = emitter index
|
|
29
|
+
lighting: vec3f, // Pre-computed lighting (smoothed over time)
|
|
30
|
+
lightingPad: f32, // Padding for alignment
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Light structure (must match LightingPass)
|
|
34
|
+
struct Light {
|
|
35
|
+
enabled: u32,
|
|
36
|
+
position: vec3f,
|
|
37
|
+
color: vec4f,
|
|
38
|
+
direction: vec3f,
|
|
39
|
+
geom: vec4f, // x = radius, y = inner cone, z = outer cone, w = distance fade
|
|
40
|
+
shadowIndex: i32,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
struct CascadeMatrices {
|
|
44
|
+
matrices: array<mat4x4<f32>, CASCADE_COUNT>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
struct SpotShadowMatrices {
|
|
48
|
+
matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Spawn request structure (64 bytes, matches JS SPAWN_REQUEST_STRIDE)
|
|
52
|
+
struct SpawnRequest {
|
|
53
|
+
position: vec3f,
|
|
54
|
+
lifetime: f32,
|
|
55
|
+
velocity: vec3f,
|
|
56
|
+
maxLifetime: f32,
|
|
57
|
+
color: vec4f,
|
|
58
|
+
startSize: f32,
|
|
59
|
+
endSize: f32,
|
|
60
|
+
rotation: f32, // Initial rotation (random)
|
|
61
|
+
flags: u32, // Bit 0 = alive, bit 1 = additive, bits 8-15 = emitter index
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Per-emitter simulation settings (64 bytes each)
|
|
65
|
+
struct EmitterSettings {
|
|
66
|
+
gravity: vec3f,
|
|
67
|
+
drag: f32,
|
|
68
|
+
turbulence: f32,
|
|
69
|
+
fadeIn: f32,
|
|
70
|
+
fadeOut: f32,
|
|
71
|
+
rotationSpeed: f32,
|
|
72
|
+
startSize: f32,
|
|
73
|
+
endSize: f32,
|
|
74
|
+
baseAlpha: f32,
|
|
75
|
+
padding: f32,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
struct SimulationUniforms {
|
|
79
|
+
dt: f32,
|
|
80
|
+
time: f32,
|
|
81
|
+
maxParticles: u32,
|
|
82
|
+
emitterCount: u32,
|
|
83
|
+
// Lighting parameters
|
|
84
|
+
cameraPosition: vec3f,
|
|
85
|
+
shadowBias: f32,
|
|
86
|
+
lightDir: vec3f,
|
|
87
|
+
shadowStrength: f32,
|
|
88
|
+
lightColor: vec4f,
|
|
89
|
+
ambientColor: vec4f,
|
|
90
|
+
cascadeSizes: vec4f,
|
|
91
|
+
lightCount: u32,
|
|
92
|
+
pad1: u32,
|
|
93
|
+
pad2: u32,
|
|
94
|
+
pad3: u32,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
struct Counters {
|
|
98
|
+
aliveCount: atomic<u32>,
|
|
99
|
+
nextFreeIndex: atomic<u32>,
|
|
100
|
+
spawnCount: u32,
|
|
101
|
+
frameCount: u32,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Core bindings
|
|
105
|
+
@group(0) @binding(0) var<uniform> uniforms: SimulationUniforms;
|
|
106
|
+
@group(0) @binding(1) var<storage, read_write> particles: array<Particle>;
|
|
107
|
+
@group(0) @binding(2) var<storage, read_write> counters: Counters;
|
|
108
|
+
@group(0) @binding(3) var<storage, read> spawnRequests: array<SpawnRequest>;
|
|
109
|
+
@group(0) @binding(4) var<storage, read> emitterSettings: array<EmitterSettings>;
|
|
110
|
+
|
|
111
|
+
// Lighting bindings
|
|
112
|
+
@group(0) @binding(5) var<storage, read> emitterRenderSettings: array<EmitterRenderSettings>;
|
|
113
|
+
@group(0) @binding(6) var shadowMapArray: texture_depth_2d_array;
|
|
114
|
+
@group(0) @binding(7) var shadowSampler: sampler_comparison;
|
|
115
|
+
@group(0) @binding(8) var<storage, read> cascadeMatrices: CascadeMatrices;
|
|
116
|
+
@group(0) @binding(9) var<storage, read> lights: array<Light, MAX_LIGHTS>;
|
|
117
|
+
@group(0) @binding(10) var spotShadowAtlas: texture_depth_2d;
|
|
118
|
+
@group(0) @binding(11) var<storage, read> spotMatrices: SpotShadowMatrices;
|
|
119
|
+
|
|
120
|
+
// Emitter render settings (lit, emissive)
|
|
121
|
+
struct EmitterRenderSettings {
|
|
122
|
+
lit: f32,
|
|
123
|
+
emissive: f32,
|
|
124
|
+
softness: f32,
|
|
125
|
+
zOffset: f32,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Simple noise function for turbulence
|
|
129
|
+
fn hash(p: vec3f) -> f32 {
|
|
130
|
+
var p3 = fract(p * 0.1031);
|
|
131
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
132
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn noise3D(p: vec3f) -> vec3f {
|
|
136
|
+
let i = floor(p);
|
|
137
|
+
let f = fract(p);
|
|
138
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
139
|
+
|
|
140
|
+
return vec3f(
|
|
141
|
+
mix(hash(i), hash(i + vec3f(1.0, 0.0, 0.0)), u.x),
|
|
142
|
+
mix(hash(i + vec3f(0.0, 1.0, 0.0)), hash(i + vec3f(1.0, 1.0, 0.0)), u.x),
|
|
143
|
+
mix(hash(i + vec3f(0.0, 0.0, 1.0)), hash(i + vec3f(1.0, 0.0, 1.0)), u.x)
|
|
144
|
+
) * 2.0 - 1.0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============= LIGHTING FUNCTIONS =============
|
|
148
|
+
|
|
149
|
+
// Squircle distance for cascade selection
|
|
150
|
+
fn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {
|
|
151
|
+
let normalized = offset / size;
|
|
152
|
+
let absNorm = abs(normalized);
|
|
153
|
+
return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Sample shadow from cascade (single tap for compute shader performance)
|
|
157
|
+
fn sampleCascadeShadow(worldPos: vec3f, cascadeIndex: i32) -> f32 {
|
|
158
|
+
let bias = uniforms.shadowBias;
|
|
159
|
+
let lightMatrix = cascadeMatrices.matrices[cascadeIndex];
|
|
160
|
+
let lightSpacePos = lightMatrix * vec4f(worldPos, 1.0);
|
|
161
|
+
let projCoords = lightSpacePos.xyz / lightSpacePos.w;
|
|
162
|
+
let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
163
|
+
let currentDepth = projCoords.z - bias;
|
|
164
|
+
|
|
165
|
+
// Check bounds
|
|
166
|
+
if (shadowUV.x < 0.0 || shadowUV.x > 1.0 || shadowUV.y < 0.0 || shadowUV.y > 1.0 ||
|
|
167
|
+
currentDepth < 0.0 || currentDepth > 1.0) {
|
|
168
|
+
return 1.0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Single tap shadow for compute shader (faster)
|
|
172
|
+
return textureSampleCompareLevel(shadowMapArray, shadowSampler, shadowUV, cascadeIndex, currentDepth);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Calculate cascade shadow for particle
|
|
176
|
+
fn calculateParticleShadow(worldPos: vec3f) -> f32 {
|
|
177
|
+
let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);
|
|
178
|
+
let posXZ = vec2f(worldPos.x, worldPos.z);
|
|
179
|
+
let offsetXZ = posXZ - camXZ;
|
|
180
|
+
|
|
181
|
+
let dist0 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSizes.x);
|
|
182
|
+
let dist1 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSizes.y);
|
|
183
|
+
let dist2 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSizes.z);
|
|
184
|
+
|
|
185
|
+
var shadow = 1.0;
|
|
186
|
+
if (dist0 < 0.95) {
|
|
187
|
+
shadow = sampleCascadeShadow(worldPos, 0);
|
|
188
|
+
} else if (dist1 < 0.95) {
|
|
189
|
+
shadow = sampleCascadeShadow(worldPos, 1);
|
|
190
|
+
} else if (dist2 < 0.95) {
|
|
191
|
+
shadow = sampleCascadeShadow(worldPos, 2);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return mix(1.0 - uniforms.shadowStrength, 1.0, shadow);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Calculate spot shadow (single tap)
|
|
198
|
+
fn calculateSpotShadow(worldPos: vec3f, slotIndex: i32) -> f32 {
|
|
199
|
+
if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {
|
|
200
|
+
return 1.0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let lightMatrix = spotMatrices.matrices[slotIndex];
|
|
204
|
+
let lightSpacePos = lightMatrix * vec4f(worldPos, 1.0);
|
|
205
|
+
let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);
|
|
206
|
+
let projCoords = lightSpacePos.xyz / w;
|
|
207
|
+
|
|
208
|
+
if (projCoords.z < 0.0 || projCoords.z > 1.0 || abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0) {
|
|
209
|
+
return 1.0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let col = slotIndex % SPOT_TILES_PER_ROW;
|
|
213
|
+
let row = slotIndex / SPOT_TILES_PER_ROW;
|
|
214
|
+
let localUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
215
|
+
let tileOffset = vec2f(f32(col), f32(row)) * SPOT_TILE_SIZE;
|
|
216
|
+
let atlasUV = (tileOffset + localUV * SPOT_TILE_SIZE) / vec2f(SPOT_ATLAS_WIDTH, SPOT_ATLAS_HEIGHT);
|
|
217
|
+
let currentDepth = clamp(projCoords.z - uniforms.shadowBias * 3.0, 0.001, 0.999);
|
|
218
|
+
|
|
219
|
+
return textureSampleCompareLevel(spotShadowAtlas, shadowSampler, atlasUV, currentDepth);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Calculate full lighting for a particle at given position
|
|
223
|
+
fn calculateParticleLighting(worldPos: vec3f, emitterIdx: u32) -> vec3f {
|
|
224
|
+
let renderSettings = emitterRenderSettings[min(emitterIdx, MAX_EMITTERS - 1u)];
|
|
225
|
+
|
|
226
|
+
// Unlit particles - just return emissive multiplier as 1.0
|
|
227
|
+
if (renderSettings.lit < 0.5) {
|
|
228
|
+
return vec3f(renderSettings.emissive);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Particle normal is always up
|
|
232
|
+
let normal = vec3f(0.0, 1.0, 0.0);
|
|
233
|
+
|
|
234
|
+
// Start with ambient
|
|
235
|
+
var lighting = uniforms.ambientColor.rgb * uniforms.ambientColor.a;
|
|
236
|
+
|
|
237
|
+
// Main directional light with shadow
|
|
238
|
+
let shadow = calculateParticleShadow(worldPos);
|
|
239
|
+
let NdotL = max(dot(normal, uniforms.lightDir), 0.0);
|
|
240
|
+
lighting += uniforms.lightColor.rgb * uniforms.lightColor.a * NdotL * shadow;
|
|
241
|
+
|
|
242
|
+
// Point and spot lights
|
|
243
|
+
let lightCount = uniforms.lightCount;
|
|
244
|
+
for (var i = 0u; i < min(lightCount, MAX_LIGHTS); i++) {
|
|
245
|
+
let light = lights[i];
|
|
246
|
+
if (light.enabled == 0u) { continue; }
|
|
247
|
+
|
|
248
|
+
let lightVec = light.position - worldPos;
|
|
249
|
+
let dist = length(lightVec);
|
|
250
|
+
let lightDir = normalize(lightVec);
|
|
251
|
+
let distanceFade = light.geom.w;
|
|
252
|
+
|
|
253
|
+
// Attenuation
|
|
254
|
+
let radius = max(light.geom.x, 0.001);
|
|
255
|
+
let attenuation = max(0.0, 1.0 - dist / radius);
|
|
256
|
+
let attenuationSq = attenuation * attenuation * distanceFade;
|
|
257
|
+
if (attenuationSq <= 0.001) { continue; }
|
|
258
|
+
|
|
259
|
+
// Spot cone
|
|
260
|
+
let innerCone = light.geom.y;
|
|
261
|
+
let outerCone = light.geom.z;
|
|
262
|
+
var spotAttenuation = 1.0;
|
|
263
|
+
let isSpotlight = outerCone > 0.0;
|
|
264
|
+
if (isSpotlight) {
|
|
265
|
+
let spotCos = dot(-lightDir, normalize(light.direction));
|
|
266
|
+
spotAttenuation = smoothstep(outerCone, innerCone, spotCos);
|
|
267
|
+
}
|
|
268
|
+
if (spotAttenuation <= 0.001) { continue; }
|
|
269
|
+
|
|
270
|
+
// Spot shadow
|
|
271
|
+
var lightShadow = 1.0;
|
|
272
|
+
if (isSpotlight && light.shadowIndex >= 0) {
|
|
273
|
+
lightShadow = calculateSpotShadow(worldPos, light.shadowIndex);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Add contribution
|
|
277
|
+
let pNdotL = max(dot(normal, lightDir), 0.0);
|
|
278
|
+
let pIntensity = light.color.a * attenuationSq * spotAttenuation * lightShadow * pNdotL;
|
|
279
|
+
lighting += pIntensity * light.color.rgb;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return lighting * renderSettings.emissive;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Spawn pass: initialize new particles from spawn requests
|
|
286
|
+
@compute @workgroup_size(WORKGROUP_SIZE)
|
|
287
|
+
fn spawn(@builtin(global_invocation_id) globalId: vec3u) {
|
|
288
|
+
let idx = globalId.x;
|
|
289
|
+
|
|
290
|
+
// Check if this thread should process a spawn request
|
|
291
|
+
if (idx >= counters.spawnCount) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Find a free particle slot (try multiple slots if needed)
|
|
296
|
+
let maxAttempts = 8u; // Limit search to avoid infinite loop
|
|
297
|
+
var particleIdx = 0u;
|
|
298
|
+
var foundSlot = false;
|
|
299
|
+
|
|
300
|
+
for (var attempt = 0u; attempt < maxAttempts; attempt++) {
|
|
301
|
+
let rawIdx = atomicAdd(&counters.nextFreeIndex, 1u);
|
|
302
|
+
particleIdx = rawIdx % uniforms.maxParticles;
|
|
303
|
+
|
|
304
|
+
// Check if this slot is free (particle is dead)
|
|
305
|
+
let existing = particles[particleIdx];
|
|
306
|
+
if ((existing.flags & 1u) == 0u || existing.lifetime <= 0.0) {
|
|
307
|
+
foundSlot = true;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// If no free slot found, skip this spawn
|
|
313
|
+
if (!foundSlot) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Read spawn request
|
|
318
|
+
let req = spawnRequests[idx];
|
|
319
|
+
|
|
320
|
+
// Initialize particle from spawn request
|
|
321
|
+
var p: Particle;
|
|
322
|
+
p.position = req.position;
|
|
323
|
+
p.lifetime = req.lifetime;
|
|
324
|
+
p.velocity = req.velocity;
|
|
325
|
+
p.maxLifetime = req.maxLifetime;
|
|
326
|
+
p.color = req.color;
|
|
327
|
+
p.size = vec2f(req.startSize, req.startSize);
|
|
328
|
+
p.rotation = req.rotation; // Random initial rotation
|
|
329
|
+
p.flags = req.flags;
|
|
330
|
+
// Initialize lighting to ambient (will smooth towards actual lighting)
|
|
331
|
+
p.lighting = uniforms.ambientColor.rgb * uniforms.ambientColor.a;
|
|
332
|
+
p.lightingPad = 0.0;
|
|
333
|
+
|
|
334
|
+
// Store particle
|
|
335
|
+
particles[particleIdx] = p;
|
|
336
|
+
|
|
337
|
+
// Increment alive count
|
|
338
|
+
atomicAdd(&counters.aliveCount, 1u);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Simulate pass: update existing particles
|
|
342
|
+
@compute @workgroup_size(WORKGROUP_SIZE)
|
|
343
|
+
fn simulate(@builtin(global_invocation_id) globalId: vec3u) {
|
|
344
|
+
let idx = globalId.x;
|
|
345
|
+
|
|
346
|
+
// Bounds check
|
|
347
|
+
if (idx >= uniforms.maxParticles) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
var p = particles[idx];
|
|
352
|
+
|
|
353
|
+
// Skip dead particles
|
|
354
|
+
if ((p.flags & 1u) == 0u || p.lifetime <= 0.0) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Get emitter index from flags (bits 8-15)
|
|
359
|
+
let emitterIdx = (p.flags >> 8u) & 0xFFu;
|
|
360
|
+
let settings = emitterSettings[min(emitterIdx, MAX_EMITTERS - 1u)];
|
|
361
|
+
|
|
362
|
+
let dt = uniforms.dt;
|
|
363
|
+
|
|
364
|
+
// Update lifetime
|
|
365
|
+
p.lifetime -= dt;
|
|
366
|
+
|
|
367
|
+
// Check if particle died
|
|
368
|
+
if (p.lifetime <= 0.0) {
|
|
369
|
+
p.flags = 0u; // Mark as dead
|
|
370
|
+
p.lifetime = 0.0;
|
|
371
|
+
atomicSub(&counters.aliveCount, 1u);
|
|
372
|
+
particles[idx] = p;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Calculate life progress (0 = just born, 1 = about to die)
|
|
377
|
+
let lifeProgress = 1.0 - (p.lifetime / p.maxLifetime);
|
|
378
|
+
|
|
379
|
+
// Apply gravity (per-emitter)
|
|
380
|
+
p.velocity += settings.gravity * dt;
|
|
381
|
+
|
|
382
|
+
// Apply drag (per-emitter)
|
|
383
|
+
let dragFactor = 1.0 - settings.drag * dt;
|
|
384
|
+
p.velocity *= max(dragFactor, 0.0);
|
|
385
|
+
|
|
386
|
+
// Apply turbulence (per-emitter)
|
|
387
|
+
if (settings.turbulence > 0.0) {
|
|
388
|
+
let noisePos = p.position * 0.5 + vec3f(uniforms.time * 0.1, 0.0, 0.0);
|
|
389
|
+
let turbulenceForce = noise3D(noisePos + vec3f(p.rotation * 10.0)) * settings.turbulence;
|
|
390
|
+
p.velocity += turbulenceForce * dt * 10.0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Update position
|
|
394
|
+
p.position += p.velocity * dt;
|
|
395
|
+
|
|
396
|
+
// Interpolate size over lifetime (per-emitter)
|
|
397
|
+
let currentSize = mix(settings.startSize, settings.endSize, lifeProgress);
|
|
398
|
+
p.size = vec2f(currentSize, currentSize);
|
|
399
|
+
|
|
400
|
+
// Update rotation (per-emitter)
|
|
401
|
+
let rotationDir = select(-1.0, 1.0, p.rotation >= 0.0);
|
|
402
|
+
p.rotation += rotationDir * settings.rotationSpeed * dt;
|
|
403
|
+
|
|
404
|
+
// Calculate alpha with fade in/out (per-emitter)
|
|
405
|
+
var alpha = settings.baseAlpha;
|
|
406
|
+
|
|
407
|
+
// Fade in
|
|
408
|
+
let fadeInDuration = settings.fadeIn / p.maxLifetime;
|
|
409
|
+
if (lifeProgress < fadeInDuration && fadeInDuration > 0.0) {
|
|
410
|
+
alpha *= lifeProgress / fadeInDuration;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fade out
|
|
414
|
+
let fadeOutStart = 1.0 - (settings.fadeOut / p.maxLifetime);
|
|
415
|
+
if (lifeProgress > fadeOutStart && fadeOutStart < 1.0) {
|
|
416
|
+
let fadeOutProgress = (lifeProgress - fadeOutStart) / (1.0 - fadeOutStart);
|
|
417
|
+
alpha *= 1.0 - fadeOutProgress;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Only update alpha, preserve RGB from spawn
|
|
421
|
+
p.color.a = alpha;
|
|
422
|
+
|
|
423
|
+
// Calculate target lighting at particle center
|
|
424
|
+
let targetLighting = calculateParticleLighting(p.position, emitterIdx);
|
|
425
|
+
|
|
426
|
+
// Smooth lighting over time (exponential moving average)
|
|
427
|
+
// lerpFactor = 1 - exp(-dt / fadeTime) ≈ dt / fadeTime for small dt
|
|
428
|
+
let lerpFactor = clamp(dt / LIGHTING_FADE_TIME, 0.0, 1.0);
|
|
429
|
+
p.lighting = mix(p.lighting, targetLighting, lerpFactor);
|
|
430
|
+
|
|
431
|
+
// Write updated particle
|
|
432
|
+
particles[idx] = p;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Reset counters (run once per frame before spawn)
|
|
436
|
+
@compute @workgroup_size(1)
|
|
437
|
+
fn resetCounters() {
|
|
438
|
+
// Reset spawn-related counter but preserve alive count
|
|
439
|
+
atomicStore(&counters.nextFreeIndex, 0u);
|
|
440
|
+
}
|