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.
Files changed (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +170 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +563 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2258 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,672 @@
1
+ // Particle rendering shader
2
+ // Renders billboarded quads for each alive particle with soft depth fade
3
+ // Supports lighting, shadows, point/spot lights, IBL, and emissive brightness
4
+
5
+ const PI = 3.14159265359;
6
+ const CASCADE_COUNT = 3;
7
+ const MAX_EMITTERS = 16u;
8
+ const MAX_LIGHTS = 64u;
9
+ const MAX_SPOT_SHADOWS = 8;
10
+
11
+ // Spot shadow atlas constants (must match LightingPass)
12
+ const SPOT_ATLAS_WIDTH: f32 = 2048.0;
13
+ const SPOT_ATLAS_HEIGHT: f32 = 2048.0;
14
+ const SPOT_TILE_SIZE: f32 = 512.0;
15
+ const SPOT_TILES_PER_ROW: i32 = 4;
16
+
17
+ // Particle data structure (must match particle_simulate.wgsl)
18
+ struct Particle {
19
+ position: vec3f,
20
+ lifetime: f32,
21
+ velocity: vec3f,
22
+ maxLifetime: f32,
23
+ color: vec4f,
24
+ size: vec2f,
25
+ rotation: f32, // Current rotation in radians
26
+ flags: u32,
27
+ lighting: vec3f, // Pre-computed lighting (smoothed in compute shader)
28
+ lightingPad: f32,
29
+ }
30
+
31
+ // Light structure (must match LightingPass)
32
+ struct Light {
33
+ enabled: u32,
34
+ position: vec3f,
35
+ color: vec4f,
36
+ direction: vec3f,
37
+ geom: vec4f, // x = radius, y = inner cone, z = outer cone, w = distance fade
38
+ shadowIndex: i32, // -1 if no shadow, 0-7 for spot shadow slot
39
+ }
40
+
41
+ // Per-emitter settings (must match ParticlePass.js)
42
+ struct EmitterRenderSettings {
43
+ lit: f32, // 0 = unlit, 1 = lit
44
+ emissive: f32, // Brightness multiplier (1 = normal, >1 = glow)
45
+ softness: f32,
46
+ zOffset: f32,
47
+ }
48
+
49
+ // Spot shadow matrices
50
+ struct SpotShadowMatrices {
51
+ matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,
52
+ }
53
+
54
+ struct ParticleUniforms {
55
+ viewMatrix: mat4x4f,
56
+ projectionMatrix: mat4x4f,
57
+ cameraPosition: vec3f,
58
+ time: f32,
59
+ cameraRight: vec3f,
60
+ softness: f32,
61
+ cameraUp: vec3f,
62
+ zOffset: f32,
63
+ screenSize: vec2f,
64
+ near: f32,
65
+ far: f32,
66
+ blendMode: f32, // 0 = alpha, 1 = additive
67
+ lit: f32, // 0 = unlit, 1 = simple lighting (global fallback)
68
+ shadowBias: f32,
69
+ shadowStrength: f32,
70
+ // Lighting uniforms
71
+ lightDir: vec3f,
72
+ shadowMapSize: f32,
73
+ lightColor: vec4f,
74
+ ambientColor: vec4f,
75
+ cascadeSizes: vec4f, // x, y, z = cascade half-widths
76
+ // IBL uniforms
77
+ envParams: vec4f, // x = diffuse level, y = mip count, z = encoding (0=equirect, 1=octahedral), w = exposure
78
+ // Light count
79
+ lightParams: vec4u, // x = light count, y = unused, z = unused, w = unused
80
+ // Fog uniforms
81
+ fogColor: vec3f,
82
+ fogEnabled: f32,
83
+ fogDistances: vec3f, // [near, mid, far]
84
+ fogBrightResist: f32,
85
+ fogAlphas: vec3f, // [nearAlpha, midAlpha, farAlpha]
86
+ fogPad1: f32,
87
+ fogHeightFade: vec2f, // [bottomY, topY]
88
+ fogDebug: f32, // 0 = off, 2 = show distance
89
+ fogPad2: f32,
90
+ }
91
+
92
+ struct CascadeMatrices {
93
+ matrices: array<mat4x4<f32>, CASCADE_COUNT>,
94
+ }
95
+
96
+ struct VertexOutput {
97
+ @builtin(position) position: vec4f,
98
+ @location(0) uv: vec2f,
99
+ @location(1) color: vec4f,
100
+ @location(2) viewZ: f32,
101
+ @location(3) linearDepth: f32, // For frag_depth output
102
+ @location(4) lighting: vec3f, // Pre-computed lighting from particle
103
+ @location(5) @interpolate(flat) emitterIdx: u32, // For per-emitter settings
104
+ @location(6) worldPos: vec3f, // For fog height fade
105
+ @location(7) @interpolate(flat) centerViewZ: f32, // View-space Z of particle center (for fog)
106
+ }
107
+
108
+ @group(0) @binding(0) var<uniform> uniforms: ParticleUniforms;
109
+ @group(0) @binding(1) var<storage, read> particles: array<Particle>;
110
+ @group(0) @binding(2) var particleTexture: texture_2d<f32>;
111
+ @group(0) @binding(3) var particleSampler: sampler;
112
+ @group(0) @binding(4) var depthTexture: texture_depth_2d;
113
+ @group(0) @binding(5) var shadowMapArray: texture_depth_2d_array;
114
+ @group(0) @binding(6) var shadowSampler: sampler_comparison;
115
+ @group(0) @binding(7) var<storage, read> cascadeMatrices: CascadeMatrices;
116
+ @group(0) @binding(8) var<storage, read> emitterSettings: array<EmitterRenderSettings, MAX_EMITTERS>;
117
+ @group(0) @binding(9) var envMap: texture_2d<f32>;
118
+ @group(0) @binding(10) var envSampler: sampler;
119
+ // Point/spot lights
120
+ @group(0) @binding(11) var<storage, read> lights: array<Light, MAX_LIGHTS>;
121
+ // Spot shadow atlas
122
+ @group(0) @binding(12) var spotShadowAtlas: texture_depth_2d;
123
+ @group(0) @binding(13) var spotShadowSampler: sampler_comparison;
124
+ @group(0) @binding(14) var<storage, read> spotMatrices: SpotShadowMatrices;
125
+
126
+ // Quad vertices: 0=bottom-left, 1=bottom-right, 2=top-left, 3=top-right
127
+ // Two triangles: 0,1,2 and 1,3,2
128
+ fn getQuadVertex(vertexId: u32) -> vec2f {
129
+ // Map 6 vertices to 4 quad corners
130
+ // Triangle 1: 0,1,2 -> BL, BR, TL
131
+ // Triangle 2: 3,4,5 -> BR, TR, TL
132
+ var corners = array<vec2f, 6>(
133
+ vec2f(-0.5, -0.5), // 0: BL
134
+ vec2f(0.5, -0.5), // 1: BR
135
+ vec2f(-0.5, 0.5), // 2: TL
136
+ vec2f(0.5, -0.5), // 3: BR
137
+ vec2f(0.5, 0.5), // 4: TR
138
+ vec2f(-0.5, 0.5) // 5: TL
139
+ );
140
+ return corners[vertexId];
141
+ }
142
+
143
+ fn getQuadUV(vertexId: u32) -> vec2f {
144
+ var uvs = array<vec2f, 6>(
145
+ vec2f(0.0, 1.0), // 0: BL
146
+ vec2f(1.0, 1.0), // 1: BR
147
+ vec2f(0.0, 0.0), // 2: TL
148
+ vec2f(1.0, 1.0), // 3: BR
149
+ vec2f(1.0, 0.0), // 4: TR
150
+ vec2f(0.0, 0.0) // 5: TL
151
+ );
152
+ return uvs[vertexId];
153
+ }
154
+
155
+ @vertex
156
+ fn vertexMain(
157
+ @builtin(vertex_index) vertexIndex: u32,
158
+ @builtin(instance_index) instanceIndex: u32
159
+ ) -> VertexOutput {
160
+ var output: VertexOutput;
161
+
162
+ // Get particle data
163
+ let particle = particles[instanceIndex];
164
+
165
+ // Get emitter index from flags (bits 8-15)
166
+ let emitterIdx = (particle.flags >> 8u) & 0xFFu;
167
+ output.emitterIdx = emitterIdx;
168
+
169
+ // Check if particle is alive
170
+ if ((particle.flags & 1u) == 0u || particle.lifetime <= 0.0) {
171
+ // Dead particle - render degenerate triangle
172
+ output.position = vec4f(0.0, 0.0, 0.0, 0.0);
173
+ output.uv = vec2f(0.0, 0.0);
174
+ output.color = vec4f(0.0, 0.0, 0.0, 0.0);
175
+ output.viewZ = 0.0;
176
+ output.linearDepth = 0.0;
177
+ output.lighting = vec3f(0.0);
178
+ output.worldPos = vec3f(0.0);
179
+ output.centerViewZ = 0.0;
180
+ return output;
181
+ }
182
+
183
+ // Check blend mode: bit 1 of flags = additive (1) or alpha (0)
184
+ // uniforms.blendMode: 1.0 = additive, 0.0 = alpha
185
+ let particleIsAdditive = (particle.flags & 2u) != 0u;
186
+ let renderingAdditive = uniforms.blendMode > 0.5;
187
+
188
+ if (particleIsAdditive != renderingAdditive) {
189
+ // Wrong blend mode for this pass - skip
190
+ output.position = vec4f(0.0, 0.0, 0.0, 0.0);
191
+ output.uv = vec2f(0.0, 0.0);
192
+ output.color = vec4f(0.0, 0.0, 0.0, 0.0);
193
+ output.viewZ = 0.0;
194
+ output.linearDepth = 0.0;
195
+ output.lighting = vec3f(0.0);
196
+ output.worldPos = vec3f(0.0);
197
+ output.centerViewZ = 0.0;
198
+ return output;
199
+ }
200
+
201
+ // Get quad vertex position and UV
202
+ let localVertexId = vertexIndex % 6u;
203
+ var quadPos = getQuadVertex(localVertexId);
204
+ output.uv = getQuadUV(localVertexId);
205
+
206
+ // Apply rotation to quad position
207
+ let cosR = cos(particle.rotation);
208
+ let sinR = sin(particle.rotation);
209
+ let rotatedPos = vec2f(
210
+ quadPos.x * cosR - quadPos.y * sinR,
211
+ quadPos.x * sinR + quadPos.y * cosR
212
+ );
213
+
214
+ // Billboard: create quad facing camera
215
+ let particleWorldPos = particle.position;
216
+
217
+ // Scale rotated quad by particle size
218
+ let scaledOffset = rotatedPos * particle.size;
219
+
220
+ // Create billboard position using camera vectors
221
+ let right = uniforms.cameraRight;
222
+ let up = uniforms.cameraUp;
223
+ let billboardPos = particleWorldPos + right * scaledOffset.x + up * scaledOffset.y;
224
+
225
+ // Apply z-offset along view direction to prevent z-fighting
226
+ let toCamera = normalize(uniforms.cameraPosition - particleWorldPos);
227
+ let offsetPos = billboardPos + toCamera * uniforms.zOffset;
228
+
229
+ // Transform to clip space
230
+ let viewPos = uniforms.viewMatrix * vec4f(offsetPos, 1.0);
231
+ output.position = uniforms.projectionMatrix * viewPos;
232
+ output.viewZ = -viewPos.z; // Positive depth
233
+
234
+ // Calculate linear depth matching GBuffer format: (z - near) / (far - near)
235
+ let z = -viewPos.z; // View space Z (positive into screen)
236
+ output.linearDepth = (z - uniforms.near) / (uniforms.far - uniforms.near);
237
+
238
+ // Pass pre-computed lighting from particle (calculated in compute shader)
239
+ output.lighting = particle.lighting;
240
+
241
+ // Pass through particle color
242
+ output.color = particle.color;
243
+
244
+ // Pass world position for fog height
245
+ output.worldPos = particleWorldPos;
246
+
247
+ // Use the billboard's viewZ for fog distance
248
+ // This matches scene fog which uses view-space Z (linear depth)
249
+ // The billboard viewZ is already calculated correctly: -viewPos.z
250
+ output.centerViewZ = output.viewZ;
251
+
252
+ return output;
253
+ }
254
+
255
+ // Equirectangular UV from direction
256
+ fn SphToUV(n: vec3f) -> vec2f {
257
+ var uv: vec2f;
258
+ uv.x = atan2(-n.x, n.z);
259
+ uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);
260
+ uv.y = acos(n.y) / PI;
261
+ return uv;
262
+ }
263
+
264
+ // Octahedral encoding: direction to UV
265
+ fn octEncode(n: vec3f) -> vec2f {
266
+ var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));
267
+ if (n2.y < 0.0) {
268
+ let signX = select(-1.0, 1.0, n2.x >= 0.0);
269
+ let signZ = select(-1.0, 1.0, n2.z >= 0.0);
270
+ n2 = vec3f(
271
+ (1.0 - abs(n2.z)) * signX,
272
+ n2.y,
273
+ (1.0 - abs(n2.x)) * signZ
274
+ );
275
+ }
276
+ return n2.xz * 0.5 + 0.5;
277
+ }
278
+
279
+ // Get environment UV based on encoding type
280
+ fn getEnvUV(dir: vec3f) -> vec2f {
281
+ if (uniforms.envParams.z > 0.5) {
282
+ return octEncode(dir);
283
+ }
284
+ return SphToUV(dir);
285
+ }
286
+
287
+ // Sample IBL at direction with LOD
288
+ fn getIBLSample(dir: vec3f, lod: f32) -> vec3f {
289
+ let envRGBE = textureSampleLevel(envMap, envSampler, getEnvUV(dir), lod);
290
+ // RGBE decode
291
+ let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);
292
+ return envColor;
293
+ }
294
+
295
+ // Squircle distance - returns distance normalized to cascade size
296
+ fn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {
297
+ let normalized = offset / size;
298
+ let absNorm = abs(normalized);
299
+ return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);
300
+ }
301
+
302
+ // Sample shadow from a specific cascade (simplified for particles)
303
+ fn sampleCascadeShadow(worldPos: vec3f, normal: vec3f, cascadeIndex: i32) -> f32 {
304
+ let bias = uniforms.shadowBias;
305
+ let shadowMapSize = uniforms.shadowMapSize;
306
+
307
+ // Apply normal bias
308
+ let biasedPos = worldPos + normal * bias * 0.5;
309
+
310
+ // Get cascade matrix
311
+ let lightMatrix = cascadeMatrices.matrices[cascadeIndex];
312
+
313
+ // Transform to light space
314
+ let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
315
+ let projCoords = lightSpacePos.xyz / lightSpacePos.w;
316
+
317
+ // Transform to [0,1] UV space
318
+ let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
319
+ let currentDepth = projCoords.z - bias;
320
+
321
+ // Check bounds
322
+ let inBoundsX = shadowUV.x >= 0.0 && shadowUV.x <= 1.0;
323
+ let inBoundsY = shadowUV.y >= 0.0 && shadowUV.y <= 1.0;
324
+ let inBoundsZ = currentDepth >= 0.0 && currentDepth <= 1.0;
325
+
326
+ if (!inBoundsX || !inBoundsY || !inBoundsZ) {
327
+ return 1.0; // Out of bounds = lit
328
+ }
329
+
330
+ let clampedUV = clamp(shadowUV, vec2f(0.001), vec2f(0.999));
331
+ let clampedDepth = clamp(currentDepth, 0.001, 0.999);
332
+
333
+ // Simple 4-tap PCF for particles (fast)
334
+ let texelSize = 1.0 / shadowMapSize;
335
+ var shadow = 0.0;
336
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(-texelSize, 0.0), cascadeIndex, clampedDepth);
337
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(texelSize, 0.0), cascadeIndex, clampedDepth);
338
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, -texelSize), cascadeIndex, clampedDepth);
339
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, texelSize), cascadeIndex, clampedDepth);
340
+ shadow /= 4.0;
341
+
342
+ return shadow;
343
+ }
344
+
345
+ // Calculate cascaded shadow for particles (simplified cascade selection)
346
+ fn calculateParticleShadow(worldPos: vec3f, normal: vec3f) -> f32 {
347
+ let shadowStrength = uniforms.shadowStrength;
348
+
349
+ // Calculate XZ offset from camera
350
+ let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);
351
+ let posXZ = vec2f(worldPos.x, worldPos.z);
352
+ let offsetXZ = posXZ - camXZ;
353
+
354
+ // Cascade sizes
355
+ let cascade0Size = uniforms.cascadeSizes.x;
356
+ let cascade1Size = uniforms.cascadeSizes.y;
357
+ let cascade2Size = uniforms.cascadeSizes.z;
358
+
359
+ let dist0 = squircleDistanceXZ(offsetXZ, cascade0Size);
360
+ let dist1 = squircleDistanceXZ(offsetXZ, cascade1Size);
361
+ let dist2 = squircleDistanceXZ(offsetXZ, cascade2Size);
362
+
363
+ var shadow = 1.0;
364
+
365
+ // Simple cascade selection (no blending for performance)
366
+ if (dist0 < 0.95) {
367
+ shadow = sampleCascadeShadow(worldPos, normal, 0);
368
+ } else if (dist1 < 0.95) {
369
+ shadow = sampleCascadeShadow(worldPos, normal, 1);
370
+ } else if (dist2 < 0.95) {
371
+ shadow = sampleCascadeShadow(worldPos, normal, 2);
372
+ }
373
+
374
+ // Apply shadow strength
375
+ return mix(1.0 - shadowStrength, 1.0, shadow);
376
+ }
377
+
378
+ // Calculate spot light shadow (simplified for particles)
379
+ fn calculateSpotShadow(worldPos: vec3f, normal: vec3f, slotIndex: i32) -> f32 {
380
+ if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {
381
+ return 1.0; // No shadow
382
+ }
383
+
384
+ let bias = uniforms.shadowBias;
385
+ let normalBias = bias * 2.0; // Increased for spot lights
386
+
387
+ // Apply normal bias
388
+ let biasedPos = worldPos + normal * normalBias;
389
+
390
+ // Get the light matrix from storage buffer
391
+ let lightMatrix = spotMatrices.matrices[slotIndex];
392
+
393
+ // Transform to light space
394
+ let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
395
+
396
+ // Perspective divide
397
+ let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);
398
+ let projCoords = lightSpacePos.xyz / w;
399
+
400
+ // Check if outside frustum
401
+ if (projCoords.z < 0.0 || projCoords.z > 1.0 ||
402
+ abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0) {
403
+ return 1.0; // Outside shadow frustum
404
+ }
405
+
406
+ // Calculate tile position in atlas
407
+ let col = slotIndex % SPOT_TILES_PER_ROW;
408
+ let row = slotIndex / SPOT_TILES_PER_ROW;
409
+
410
+ // Transform to [0,1] UV within tile
411
+ let localUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
412
+
413
+ // Transform to atlas UV
414
+ let tileOffset = vec2f(f32(col), f32(row)) * SPOT_TILE_SIZE;
415
+ let atlasUV = (tileOffset + localUV * SPOT_TILE_SIZE) / vec2f(SPOT_ATLAS_WIDTH, SPOT_ATLAS_HEIGHT);
416
+
417
+ // Sample shadow with simple 4-tap PCF
418
+ let texelSize = 1.0 / SPOT_TILE_SIZE;
419
+ let currentDepth = clamp(projCoords.z - bias * 3.0, 0.001, 0.999);
420
+
421
+ var shadowSample = 0.0;
422
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(-texelSize, 0.0), currentDepth);
423
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(texelSize, 0.0), currentDepth);
424
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, -texelSize), currentDepth);
425
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, texelSize), currentDepth);
426
+ shadowSample /= 4.0;
427
+
428
+ return shadowSample;
429
+ }
430
+
431
+ // Apply pre-computed lighting to particle color
432
+ // Lighting is calculated per-particle in the compute shader with temporal smoothing
433
+ fn applyLighting(baseColor: vec3f, lighting: vec3f) -> vec3f {
434
+ // Just multiply base color by pre-computed lighting
435
+ // Lighting already includes ambient, shadows, point/spot lights, and emissive
436
+ return baseColor * lighting;
437
+ }
438
+
439
+ struct FragmentOutput {
440
+ @location(0) color: vec4f,
441
+ @builtin(frag_depth) depth: f32,
442
+ }
443
+
444
+ // Calculate soft particle fade based on depth difference
445
+ fn calcSoftFade(fragPos: vec4f, particleLinearDepth: f32) -> f32 {
446
+ if (uniforms.softness <= 0.0) {
447
+ return 1.0;
448
+ }
449
+
450
+ // Get screen coordinates
451
+ let screenPos = vec2i(fragPos.xy);
452
+ let screenSize = vec2i(uniforms.screenSize);
453
+
454
+ // Bounds check
455
+ if (screenPos.x < 0 || screenPos.x >= screenSize.x ||
456
+ screenPos.y < 0 || screenPos.y >= screenSize.y) {
457
+ return 1.0;
458
+ }
459
+
460
+ // Sample scene depth (linear depth in 0-1 range from GBuffer)
461
+ let sceneDepthNorm = textureLoad(depthTexture, screenPos, 0);
462
+
463
+ // If depth is 0 (no valid data), skip soft fade
464
+ if (sceneDepthNorm <= 0.0) {
465
+ return 1.0;
466
+ }
467
+
468
+ // Convert both to world units for comparison
469
+ let sceneDepth = uniforms.near + sceneDepthNorm * (uniforms.far - uniforms.near);
470
+ let particleDepth = uniforms.near + particleLinearDepth * (uniforms.far - uniforms.near);
471
+
472
+ // Fade based on depth difference (in world units)
473
+ let depthDiff = sceneDepth - particleDepth;
474
+ return saturate(depthDiff / uniforms.softness);
475
+ }
476
+
477
+ // Calculate fog based on particle's world position (not scene depth)
478
+ // This allows particles to fog correctly even over sky
479
+ fn calcFog(cameraDistance: f32, worldPosY: f32) -> f32 {
480
+ if (uniforms.fogEnabled < 0.5) {
481
+ return 0.0;
482
+ }
483
+
484
+ // Distance fog - two gradients
485
+ var distanceFog: f32;
486
+ let d0 = uniforms.fogDistances.x;
487
+ let d1 = uniforms.fogDistances.y;
488
+ let d2 = uniforms.fogDistances.z;
489
+ let a0 = uniforms.fogAlphas.x;
490
+ let a1 = uniforms.fogAlphas.y;
491
+ let a2 = uniforms.fogAlphas.z;
492
+
493
+ if (cameraDistance <= d0) {
494
+ distanceFog = a0;
495
+ } else if (cameraDistance <= d1) {
496
+ let t = (cameraDistance - d0) / max(d1 - d0, 0.001);
497
+ distanceFog = mix(a0, a1, t);
498
+ } else if (cameraDistance <= d2) {
499
+ let t = (cameraDistance - d1) / max(d2 - d1, 0.001);
500
+ distanceFog = mix(a1, a2, t);
501
+ } else {
502
+ distanceFog = a2;
503
+ }
504
+
505
+ // Height fade - full fog at bottomY, zero fog at topY
506
+ let bottomY = uniforms.fogHeightFade.x;
507
+ let topY = uniforms.fogHeightFade.y;
508
+ var heightFactor = clamp((worldPosY - bottomY) / max(topY - bottomY, 0.001), 0.0, 1.0);
509
+ if (worldPosY < bottomY) {
510
+ heightFactor = 0.0;
511
+ }
512
+
513
+ return distanceFog * (1.0 - heightFactor);
514
+ }
515
+
516
+ // Apply fog to color (blend toward fog color)
517
+ // Particles use same fog calculation as scene
518
+ fn applyFog(color: vec3f, cameraDistance: f32, worldPosY: f32) -> vec3f {
519
+ let fogAlpha = calcFog(cameraDistance, worldPosY);
520
+ if (fogAlpha <= 0.0) {
521
+ return color;
522
+ }
523
+
524
+ return mix(color, uniforms.fogColor, fogAlpha);
525
+ }
526
+
527
+ // Fragment for alpha blend mode
528
+ @fragment
529
+ fn fragmentMainAlpha(input: VertexOutput) -> FragmentOutput {
530
+ var output: FragmentOutput;
531
+
532
+ // Sample texture directly
533
+ let texColor = textureSample(particleTexture, particleSampler, input.uv);
534
+
535
+ // Combine texture with particle color
536
+ var alpha = texColor.a * input.color.a;
537
+
538
+ // Apply soft particle fade
539
+ alpha *= calcSoftFade(input.position, input.linearDepth);
540
+
541
+ // Discard nearly transparent pixels
542
+ if (alpha < 0.001) {
543
+ discard;
544
+ }
545
+
546
+ // Apply pre-computed lighting from particle
547
+ let baseColor = texColor.rgb * input.color.rgb;
548
+ let litColor = applyLighting(baseColor, input.lighting);
549
+
550
+ // Use pre-computed center view-space Z from vertex shader (flat interpolated)
551
+ let centerViewZ = input.centerViewZ;
552
+
553
+ // Debug modes using centerViewZ (flat interpolated = same value across whole particle)
554
+ // Use full alpha to avoid blending artifacts in debug view
555
+ if (uniforms.fogDebug > 0.5) {
556
+ // Debug mode 1: show centerViewZ/100 as grayscale (should match scene fog)
557
+ if (uniforms.fogDebug < 1.5) {
558
+ let dist = clamp(centerViewZ / 100.0, 0.0, 1.0);
559
+ output.color = vec4f(vec3f(dist), 1.0);
560
+ }
561
+ // Debug mode 2: show centerViewZ at 3 scales to find correct range
562
+ // R = /10, G = /100, B = /1000
563
+ else if (uniforms.fogDebug < 2.5) {
564
+ let r = clamp(centerViewZ / 10.0, 0.0, 1.0);
565
+ let g = clamp(centerViewZ / 100.0, 0.0, 1.0);
566
+ let b = clamp(centerViewZ / 1000.0, 0.0, 1.0);
567
+ output.color = vec4f(r, g, b, 1.0);
568
+ }
569
+ // Debug mode 3: compare viewZ (interpolated) vs centerViewZ (flat)
570
+ // R = centerViewZ/100, G = viewZ/100, B = difference
571
+ else if (uniforms.fogDebug < 3.5) {
572
+ let center = clamp(centerViewZ / 100.0, 0.0, 1.0);
573
+ let vertex = clamp(input.viewZ / 100.0, 0.0, 1.0);
574
+ let diff = abs(center - vertex);
575
+ output.color = vec4f(center, vertex, diff * 10.0, 1.0);
576
+ }
577
+ // Debug mode 4: show worldPos
578
+ else if (uniforms.fogDebug < 4.5) {
579
+ let r = clamp(abs(input.worldPos.x) / 100.0, 0.0, 1.0);
580
+ let g = clamp(abs(input.worldPos.y) / 100.0, 0.0, 1.0);
581
+ let b = clamp(abs(input.worldPos.z) / 100.0, 0.0, 1.0);
582
+ output.color = vec4f(r, g, b, 1.0);
583
+ }
584
+ // Debug mode 5: show the actual fog color being used
585
+ else {
586
+ output.color = vec4f(uniforms.fogColor, 1.0);
587
+ }
588
+ output.depth = input.linearDepth;
589
+ return output;
590
+ }
591
+
592
+ let foggedColor = applyFog(litColor, centerViewZ, input.worldPos.y);
593
+
594
+ output.color = vec4f(foggedColor, alpha);
595
+ output.depth = input.linearDepth;
596
+ return output;
597
+ }
598
+
599
+ // Fragment for additive mode
600
+ @fragment
601
+ fn fragmentMainAdditive(input: VertexOutput) -> FragmentOutput {
602
+ var output: FragmentOutput;
603
+
604
+ // Sample texture directly
605
+ let texColor = textureSample(particleTexture, particleSampler, input.uv);
606
+
607
+ // Combine texture with particle color
608
+ var alpha = texColor.a * input.color.a;
609
+
610
+ // Apply soft particle fade
611
+ alpha *= calcSoftFade(input.position, input.linearDepth);
612
+
613
+ // Discard nearly transparent pixels
614
+ if (alpha < 0.001) {
615
+ discard;
616
+ }
617
+
618
+ // Apply pre-computed lighting from particle
619
+ let baseColor = texColor.rgb * input.color.rgb;
620
+ let litColor = applyLighting(baseColor, input.lighting);
621
+
622
+ // Use pre-computed center view-space Z from vertex shader (flat interpolated)
623
+ let centerViewZ = input.centerViewZ;
624
+
625
+ // Debug modes using centerViewZ (flat interpolated = same value across whole particle)
626
+ // Use full alpha to avoid blending artifacts in debug view
627
+ if (uniforms.fogDebug > 0.5) {
628
+ // Debug mode 1: show centerViewZ/100 as grayscale (should match scene fog)
629
+ if (uniforms.fogDebug < 1.5) {
630
+ let dist = clamp(centerViewZ / 100.0, 0.0, 1.0);
631
+ output.color = vec4f(vec3f(dist), 1.0);
632
+ }
633
+ // Debug mode 2: show centerViewZ at 3 scales
634
+ else if (uniforms.fogDebug < 2.5) {
635
+ let r = clamp(centerViewZ / 10.0, 0.0, 1.0);
636
+ let g = clamp(centerViewZ / 100.0, 0.0, 1.0);
637
+ let b = clamp(centerViewZ / 1000.0, 0.0, 1.0);
638
+ output.color = vec4f(r, g, b, 1.0);
639
+ }
640
+ // Debug mode 3: compare viewZ vs centerViewZ
641
+ else if (uniforms.fogDebug < 3.5) {
642
+ let center = clamp(centerViewZ / 100.0, 0.0, 1.0);
643
+ let vertex = clamp(input.viewZ / 100.0, 0.0, 1.0);
644
+ let diff = abs(center - vertex);
645
+ output.color = vec4f(center, vertex, diff * 10.0, 1.0);
646
+ }
647
+ // Debug mode 4: show worldPos
648
+ else if (uniforms.fogDebug < 4.5) {
649
+ let r = clamp(abs(input.worldPos.x) / 100.0, 0.0, 1.0);
650
+ let g = clamp(abs(input.worldPos.y) / 100.0, 0.0, 1.0);
651
+ let b = clamp(abs(input.worldPos.z) / 100.0, 0.0, 1.0);
652
+ output.color = vec4f(r, g, b, 1.0);
653
+ }
654
+ // Debug mode 5: show the actual fog color being used
655
+ else {
656
+ output.color = vec4f(uniforms.fogColor, 1.0);
657
+ }
658
+ output.depth = input.linearDepth;
659
+ return output;
660
+ }
661
+
662
+ // For additive particles, fog should fade the contribution to zero
663
+ // (not mix with fog color, which would add fog color to the already-fogged background)
664
+ let fogAlpha = calcFog(centerViewZ, input.worldPos.y);
665
+ let fadedColor = litColor * (1.0 - fogAlpha);
666
+
667
+ // Premultiply for additive blending
668
+ let rgb = fadedColor * alpha;
669
+ output.color = vec4f(rgb, alpha);
670
+ output.depth = input.linearDepth;
671
+ return output;
672
+ }