topazcube 0.1.30 → 0.1.33

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 (96) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +18200 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +18183 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +94 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +71 -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} +173 -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 +572 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +61 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +643 -0
  38. package/src/renderer/Renderer.js +1324 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +359 -0
  42. package/src/renderer/core/CullingSystem.js +307 -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 +546 -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 +2064 -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 +417 -0
  58. package/src/renderer/rendering/passes/FogPass.js +419 -0
  59. package/src/renderer/rendering/passes/GBufferPass.js +706 -0
  60. package/src/renderer/rendering/passes/HiZPass.js +714 -0
  61. package/src/renderer/rendering/passes/LightingPass.js +739 -0
  62. package/src/renderer/rendering/passes/ParticlePass.js +835 -0
  63. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  64. package/src/renderer/rendering/passes/PostProcessPass.js +282 -0
  65. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  66. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  67. package/src/renderer/rendering/passes/SSGIPass.js +265 -0
  68. package/src/renderer/rendering/passes/SSGITilePass.js +296 -0
  69. package/src/renderer/rendering/passes/ShadowPass.js +1822 -0
  70. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  71. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  72. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  73. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  74. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  75. package/src/renderer/rendering/shaders/geometry.wgsl +550 -0
  76. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  77. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  78. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  79. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  80. package/src/renderer/rendering/shaders/particle_render.wgsl +525 -0
  81. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  82. package/src/renderer/rendering/shaders/postproc.wgsl +272 -0
  83. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  84. package/src/renderer/rendering/shaders/shadow.wgsl +76 -0
  85. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  86. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  87. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  88. package/src/renderer/utils/BoundingSphere.js +439 -0
  89. package/src/renderer/utils/Frustum.js +281 -0
  90. package/dist/client.d.cts +0 -211
  91. package/dist/client.d.ts +0 -211
  92. package/dist/server.d.cts +0 -120
  93. package/dist/server.d.ts +0 -120
  94. package/dist/terminal.d.cts +0 -64
  95. package/dist/terminal.d.ts +0 -64
  96. package/src/utils.ts +0 -403
@@ -0,0 +1,143 @@
1
+ // Shared lighting functions for deferred and forward rendering
2
+ // This file is imported as a string and concatenated with other shaders
3
+
4
+ const PI = 3.14159;
5
+ const MAX_LIGHTS = 768;
6
+ const CASCADE_COUNT = 3;
7
+ const MAX_LIGHTS_PER_TILE = 256;
8
+ const MAX_SPOT_SHADOWS = 16;
9
+
10
+ // Hard-coded spot shadow parameters
11
+ const SPOT_ATLAS_WIDTH: f32 = 2048.0;
12
+ const SPOT_ATLAS_HEIGHT: f32 = 2048.0;
13
+ const SPOT_TILE_SIZE: f32 = 512.0;
14
+ const SPOT_TILES_PER_ROW: i32 = 4;
15
+ const SPOT_FADE_START: f32 = 25.0;
16
+ const SPOT_MAX_DISTANCE: f32 = 30.0;
17
+ const SPOT_MIN_SHADOW: f32 = 0.5;
18
+
19
+ struct Light {
20
+ enabled: u32,
21
+ position: vec3f,
22
+ color: vec4f,
23
+ direction: vec3f,
24
+ geom: vec4f,
25
+ shadowIndex: i32,
26
+ }
27
+
28
+ struct SpotShadowMatrices {
29
+ matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,
30
+ }
31
+
32
+ struct CascadeMatrices {
33
+ matrices: array<mat4x4<f32>, CASCADE_COUNT>,
34
+ }
35
+
36
+ // ============================================
37
+ // Environment mapping functions
38
+ // ============================================
39
+
40
+ fn SphToUV(n: vec3<f32>) -> vec2<f32> {
41
+ var uv: vec2<f32>;
42
+ uv.x = atan2(-n.x, n.z);
43
+ uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);
44
+ uv.y = acos(n.y) / PI;
45
+ return uv;
46
+ }
47
+
48
+ fn octEncode(n: vec3<f32>) -> vec2<f32> {
49
+ var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));
50
+ if (n2.y < 0.0) {
51
+ let signX = select(-1.0, 1.0, n2.x >= 0.0);
52
+ let signZ = select(-1.0, 1.0, n2.z >= 0.0);
53
+ n2 = vec3f(
54
+ (1.0 - abs(n2.z)) * signX,
55
+ n2.y,
56
+ (1.0 - abs(n2.x)) * signZ
57
+ );
58
+ }
59
+ return n2.xz * 0.5 + 0.5;
60
+ }
61
+
62
+ fn octDecode(uv: vec2<f32>) -> vec3<f32> {
63
+ var uv2 = uv * 2.0 - 1.0;
64
+ var n = vec3f(uv2.x, 1.0 - abs(uv2.x) - abs(uv2.y), uv2.y);
65
+ if (n.y < 0.0) {
66
+ let signX = select(-1.0, 1.0, n.x >= 0.0);
67
+ let signZ = select(-1.0, 1.0, n.z >= 0.0);
68
+ n = vec3f(
69
+ (1.0 - abs(n.z)) * signX,
70
+ n.y,
71
+ (1.0 - abs(n.x)) * signZ
72
+ );
73
+ }
74
+ return normalize(n);
75
+ }
76
+
77
+ // ============================================
78
+ // Noise sampling functions
79
+ // ============================================
80
+
81
+ fn vogelDiskSample(sampleIndex: i32, numSamples: i32, rotation: f32) -> vec2f {
82
+ let goldenAngle = 2.399963229728653;
83
+ let r = sqrt((f32(sampleIndex) + 0.5) / f32(numSamples));
84
+ let theta = f32(sampleIndex) * goldenAngle + rotation;
85
+ return vec2f(r * cos(theta), r * sin(theta));
86
+ }
87
+
88
+ fn buildOrthonormalBasis(n: vec3f) -> mat3x3<f32> {
89
+ let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), abs(n.y) < 0.999);
90
+ let tangent = normalize(cross(up, n));
91
+ let bitangent = cross(n, tangent);
92
+ return mat3x3<f32>(tangent, bitangent, n);
93
+ }
94
+
95
+ // ============================================
96
+ // PBR BRDF functions
97
+ // ============================================
98
+
99
+ fn clampedDot(x: vec3<f32>, y: vec3<f32>) -> f32 {
100
+ return clamp(dot(x, y), 0.0, 1.0);
101
+ }
102
+
103
+ fn F_Schlick(f0: vec3<f32>, f90: vec3<f32>, VdotH: f32) -> vec3<f32> {
104
+ return f0 + (f90 - f0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0);
105
+ }
106
+
107
+ fn V_GGX(NdotL: f32, NdotV: f32, alphaRoughness: f32) -> f32 {
108
+ let alphaRoughnessSq = alphaRoughness * alphaRoughness;
109
+ let GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
110
+ let GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
111
+ let GGX = GGXV + GGXL;
112
+ if (GGX > 0.0) {
113
+ return 0.5 / GGX;
114
+ }
115
+ return 0.0;
116
+ }
117
+
118
+ fn D_GGX(NdotH: f32, alphaRoughness: f32) -> f32 {
119
+ let alphaRoughnessSq = alphaRoughness * alphaRoughness;
120
+ let f = (NdotH * NdotH) * (alphaRoughnessSq - 1.0) + 1.0;
121
+ return alphaRoughnessSq / (PI * f * f);
122
+ }
123
+
124
+ fn BRDF_lambertian(f0: vec3<f32>, f90: vec3<f32>, diffuseColor: vec3<f32>, specularWeight: f32, VdotH: f32) -> vec3<f32> {
125
+ return (1.0 - specularWeight * F_Schlick(f0, f90, VdotH)) * (diffuseColor / PI);
126
+ }
127
+
128
+ fn BRDF_specularGGX(f0: vec3<f32>, f90: vec3<f32>, alphaRoughness: f32, specularWeight: f32, VdotH: f32, NdotL: f32, NdotV: f32, NdotH: f32) -> vec3<f32> {
129
+ let F = F_Schlick(f0, f90, VdotH);
130
+ let Vis = V_GGX(NdotL, NdotV, alphaRoughness);
131
+ let D = D_GGX(NdotH, alphaRoughness);
132
+ return specularWeight * F * Vis * D;
133
+ }
134
+
135
+ // ============================================
136
+ // Squircle distance for cascade blending
137
+ // ============================================
138
+
139
+ fn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {
140
+ let normalized = offset / size;
141
+ let absNorm = abs(normalized);
142
+ return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);
143
+ }
@@ -0,0 +1,525 @@
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
+ fogPad2: vec2f,
89
+ }
90
+
91
+ struct CascadeMatrices {
92
+ matrices: array<mat4x4<f32>, CASCADE_COUNT>,
93
+ }
94
+
95
+ struct VertexOutput {
96
+ @builtin(position) position: vec4f,
97
+ @location(0) uv: vec2f,
98
+ @location(1) color: vec4f,
99
+ @location(2) viewZ: f32,
100
+ @location(3) linearDepth: f32, // For frag_depth output
101
+ @location(4) lighting: vec3f, // Pre-computed lighting from particle
102
+ @location(5) @interpolate(flat) emitterIdx: u32, // For per-emitter settings
103
+ @location(6) worldPos: vec3f, // For fog height fade
104
+ }
105
+
106
+ @group(0) @binding(0) var<uniform> uniforms: ParticleUniforms;
107
+ @group(0) @binding(1) var<storage, read> particles: array<Particle>;
108
+ @group(0) @binding(2) var particleTexture: texture_2d<f32>;
109
+ @group(0) @binding(3) var particleSampler: sampler;
110
+ @group(0) @binding(4) var depthTexture: texture_depth_2d;
111
+ @group(0) @binding(5) var shadowMapArray: texture_depth_2d_array;
112
+ @group(0) @binding(6) var shadowSampler: sampler_comparison;
113
+ @group(0) @binding(7) var<storage, read> cascadeMatrices: CascadeMatrices;
114
+ @group(0) @binding(8) var<storage, read> emitterSettings: array<EmitterRenderSettings, MAX_EMITTERS>;
115
+ @group(0) @binding(9) var envMap: texture_2d<f32>;
116
+ @group(0) @binding(10) var envSampler: sampler;
117
+ // Point/spot lights
118
+ @group(0) @binding(11) var<storage, read> lights: array<Light, MAX_LIGHTS>;
119
+ // Spot shadow atlas
120
+ @group(0) @binding(12) var spotShadowAtlas: texture_depth_2d;
121
+ @group(0) @binding(13) var spotShadowSampler: sampler_comparison;
122
+ @group(0) @binding(14) var<storage, read> spotMatrices: SpotShadowMatrices;
123
+
124
+ // Quad vertices: 0=bottom-left, 1=bottom-right, 2=top-left, 3=top-right
125
+ // Two triangles: 0,1,2 and 1,3,2
126
+ fn getQuadVertex(vertexId: u32) -> vec2f {
127
+ // Map 6 vertices to 4 quad corners
128
+ // Triangle 1: 0,1,2 -> BL, BR, TL
129
+ // Triangle 2: 3,4,5 -> BR, TR, TL
130
+ var corners = array<vec2f, 6>(
131
+ vec2f(-0.5, -0.5), // 0: BL
132
+ vec2f(0.5, -0.5), // 1: BR
133
+ vec2f(-0.5, 0.5), // 2: TL
134
+ vec2f(0.5, -0.5), // 3: BR
135
+ vec2f(0.5, 0.5), // 4: TR
136
+ vec2f(-0.5, 0.5) // 5: TL
137
+ );
138
+ return corners[vertexId];
139
+ }
140
+
141
+ fn getQuadUV(vertexId: u32) -> vec2f {
142
+ var uvs = array<vec2f, 6>(
143
+ vec2f(0.0, 1.0), // 0: BL
144
+ vec2f(1.0, 1.0), // 1: BR
145
+ vec2f(0.0, 0.0), // 2: TL
146
+ vec2f(1.0, 1.0), // 3: BR
147
+ vec2f(1.0, 0.0), // 4: TR
148
+ vec2f(0.0, 0.0) // 5: TL
149
+ );
150
+ return uvs[vertexId];
151
+ }
152
+
153
+ @vertex
154
+ fn vertexMain(
155
+ @builtin(vertex_index) vertexIndex: u32,
156
+ @builtin(instance_index) instanceIndex: u32
157
+ ) -> VertexOutput {
158
+ var output: VertexOutput;
159
+
160
+ // Get particle data
161
+ let particle = particles[instanceIndex];
162
+
163
+ // Get emitter index from flags (bits 8-15)
164
+ let emitterIdx = (particle.flags >> 8u) & 0xFFu;
165
+ output.emitterIdx = emitterIdx;
166
+
167
+ // Check if particle is alive
168
+ if ((particle.flags & 1u) == 0u || particle.lifetime <= 0.0) {
169
+ // Dead particle - render degenerate triangle
170
+ output.position = vec4f(0.0, 0.0, 0.0, 0.0);
171
+ output.uv = vec2f(0.0, 0.0);
172
+ output.color = vec4f(0.0, 0.0, 0.0, 0.0);
173
+ output.viewZ = 0.0;
174
+ output.linearDepth = 0.0;
175
+ output.lighting = vec3f(0.0);
176
+ output.worldPos = vec3f(0.0);
177
+ return output;
178
+ }
179
+
180
+ // Check blend mode: bit 1 of flags = additive (1) or alpha (0)
181
+ // uniforms.blendMode: 1.0 = additive, 0.0 = alpha
182
+ let particleIsAdditive = (particle.flags & 2u) != 0u;
183
+ let renderingAdditive = uniforms.blendMode > 0.5;
184
+ if (particleIsAdditive != renderingAdditive) {
185
+ // Wrong blend mode for this pass - skip
186
+ output.position = vec4f(0.0, 0.0, 0.0, 0.0);
187
+ output.uv = vec2f(0.0, 0.0);
188
+ output.color = vec4f(0.0, 0.0, 0.0, 0.0);
189
+ output.viewZ = 0.0;
190
+ output.linearDepth = 0.0;
191
+ output.lighting = vec3f(0.0);
192
+ output.worldPos = vec3f(0.0);
193
+ return output;
194
+ }
195
+
196
+ // Get quad vertex position and UV
197
+ let localVertexId = vertexIndex % 6u;
198
+ var quadPos = getQuadVertex(localVertexId);
199
+ output.uv = getQuadUV(localVertexId);
200
+
201
+ // Apply rotation to quad position
202
+ let cosR = cos(particle.rotation);
203
+ let sinR = sin(particle.rotation);
204
+ let rotatedPos = vec2f(
205
+ quadPos.x * cosR - quadPos.y * sinR,
206
+ quadPos.x * sinR + quadPos.y * cosR
207
+ );
208
+
209
+ // Billboard: create quad facing camera
210
+ let particleWorldPos = particle.position;
211
+
212
+ // Scale rotated quad by particle size
213
+ let scaledOffset = rotatedPos * particle.size;
214
+
215
+ // Create billboard position using camera vectors
216
+ let right = uniforms.cameraRight;
217
+ let up = uniforms.cameraUp;
218
+ let billboardPos = particleWorldPos + right * scaledOffset.x + up * scaledOffset.y;
219
+
220
+ // Apply z-offset along view direction to prevent z-fighting
221
+ let toCamera = normalize(uniforms.cameraPosition - particleWorldPos);
222
+ let offsetPos = billboardPos + toCamera * uniforms.zOffset;
223
+
224
+ // Transform to clip space
225
+ let viewPos = uniforms.viewMatrix * vec4f(offsetPos, 1.0);
226
+ output.position = uniforms.projectionMatrix * viewPos;
227
+ output.viewZ = -viewPos.z; // Positive depth
228
+
229
+ // Calculate linear depth matching GBuffer format: (z - near) / (far - near)
230
+ let z = -viewPos.z; // View space Z (positive into screen)
231
+ output.linearDepth = (z - uniforms.near) / (uniforms.far - uniforms.near);
232
+
233
+ // Pass pre-computed lighting from particle (calculated in compute shader)
234
+ output.lighting = particle.lighting;
235
+
236
+ // Pass through particle color
237
+ output.color = particle.color;
238
+
239
+ // Pass world position for fog
240
+ output.worldPos = particleWorldPos;
241
+
242
+ return output;
243
+ }
244
+
245
+ // Equirectangular UV from direction
246
+ fn SphToUV(n: vec3f) -> vec2f {
247
+ var uv: vec2f;
248
+ uv.x = atan2(-n.x, n.z);
249
+ uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);
250
+ uv.y = acos(n.y) / PI;
251
+ return uv;
252
+ }
253
+
254
+ // Octahedral encoding: direction to UV
255
+ fn octEncode(n: vec3f) -> vec2f {
256
+ var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));
257
+ if (n2.y < 0.0) {
258
+ let signX = select(-1.0, 1.0, n2.x >= 0.0);
259
+ let signZ = select(-1.0, 1.0, n2.z >= 0.0);
260
+ n2 = vec3f(
261
+ (1.0 - abs(n2.z)) * signX,
262
+ n2.y,
263
+ (1.0 - abs(n2.x)) * signZ
264
+ );
265
+ }
266
+ return n2.xz * 0.5 + 0.5;
267
+ }
268
+
269
+ // Get environment UV based on encoding type
270
+ fn getEnvUV(dir: vec3f) -> vec2f {
271
+ if (uniforms.envParams.z > 0.5) {
272
+ return octEncode(dir);
273
+ }
274
+ return SphToUV(dir);
275
+ }
276
+
277
+ // Sample IBL at direction with LOD
278
+ fn getIBLSample(dir: vec3f, lod: f32) -> vec3f {
279
+ let envRGBE = textureSampleLevel(envMap, envSampler, getEnvUV(dir), lod);
280
+ // RGBE decode
281
+ let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);
282
+ return envColor;
283
+ }
284
+
285
+ // Squircle distance - returns distance normalized to cascade size
286
+ fn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {
287
+ let normalized = offset / size;
288
+ let absNorm = abs(normalized);
289
+ return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);
290
+ }
291
+
292
+ // Sample shadow from a specific cascade (simplified for particles)
293
+ fn sampleCascadeShadow(worldPos: vec3f, normal: vec3f, cascadeIndex: i32) -> f32 {
294
+ let bias = uniforms.shadowBias;
295
+ let shadowMapSize = uniforms.shadowMapSize;
296
+
297
+ // Apply normal bias
298
+ let biasedPos = worldPos + normal * bias * 0.5;
299
+
300
+ // Get cascade matrix
301
+ let lightMatrix = cascadeMatrices.matrices[cascadeIndex];
302
+
303
+ // Transform to light space
304
+ let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
305
+ let projCoords = lightSpacePos.xyz / lightSpacePos.w;
306
+
307
+ // Transform to [0,1] UV space
308
+ let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
309
+ let currentDepth = projCoords.z - bias;
310
+
311
+ // Check bounds
312
+ let inBoundsX = shadowUV.x >= 0.0 && shadowUV.x <= 1.0;
313
+ let inBoundsY = shadowUV.y >= 0.0 && shadowUV.y <= 1.0;
314
+ let inBoundsZ = currentDepth >= 0.0 && currentDepth <= 1.0;
315
+
316
+ if (!inBoundsX || !inBoundsY || !inBoundsZ) {
317
+ return 1.0; // Out of bounds = lit
318
+ }
319
+
320
+ let clampedUV = clamp(shadowUV, vec2f(0.001), vec2f(0.999));
321
+ let clampedDepth = clamp(currentDepth, 0.001, 0.999);
322
+
323
+ // Simple 4-tap PCF for particles (fast)
324
+ let texelSize = 1.0 / shadowMapSize;
325
+ var shadow = 0.0;
326
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(-texelSize, 0.0), cascadeIndex, clampedDepth);
327
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(texelSize, 0.0), cascadeIndex, clampedDepth);
328
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, -texelSize), cascadeIndex, clampedDepth);
329
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, texelSize), cascadeIndex, clampedDepth);
330
+ shadow /= 4.0;
331
+
332
+ return shadow;
333
+ }
334
+
335
+ // Calculate cascaded shadow for particles (simplified cascade selection)
336
+ fn calculateParticleShadow(worldPos: vec3f, normal: vec3f) -> f32 {
337
+ let shadowStrength = uniforms.shadowStrength;
338
+
339
+ // Calculate XZ offset from camera
340
+ let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);
341
+ let posXZ = vec2f(worldPos.x, worldPos.z);
342
+ let offsetXZ = posXZ - camXZ;
343
+
344
+ // Cascade sizes
345
+ let cascade0Size = uniforms.cascadeSizes.x;
346
+ let cascade1Size = uniforms.cascadeSizes.y;
347
+ let cascade2Size = uniforms.cascadeSizes.z;
348
+
349
+ let dist0 = squircleDistanceXZ(offsetXZ, cascade0Size);
350
+ let dist1 = squircleDistanceXZ(offsetXZ, cascade1Size);
351
+ let dist2 = squircleDistanceXZ(offsetXZ, cascade2Size);
352
+
353
+ var shadow = 1.0;
354
+
355
+ // Simple cascade selection (no blending for performance)
356
+ if (dist0 < 0.95) {
357
+ shadow = sampleCascadeShadow(worldPos, normal, 0);
358
+ } else if (dist1 < 0.95) {
359
+ shadow = sampleCascadeShadow(worldPos, normal, 1);
360
+ } else if (dist2 < 0.95) {
361
+ shadow = sampleCascadeShadow(worldPos, normal, 2);
362
+ }
363
+
364
+ // Apply shadow strength
365
+ return mix(1.0 - shadowStrength, 1.0, shadow);
366
+ }
367
+
368
+ // Calculate spot light shadow (simplified for particles)
369
+ fn calculateSpotShadow(worldPos: vec3f, normal: vec3f, slotIndex: i32) -> f32 {
370
+ if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {
371
+ return 1.0; // No shadow
372
+ }
373
+
374
+ let bias = uniforms.shadowBias;
375
+ let normalBias = bias * 2.0; // Increased for spot lights
376
+
377
+ // Apply normal bias
378
+ let biasedPos = worldPos + normal * normalBias;
379
+
380
+ // Get the light matrix from storage buffer
381
+ let lightMatrix = spotMatrices.matrices[slotIndex];
382
+
383
+ // Transform to light space
384
+ let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
385
+
386
+ // Perspective divide
387
+ let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);
388
+ let projCoords = lightSpacePos.xyz / w;
389
+
390
+ // Check if outside frustum
391
+ if (projCoords.z < 0.0 || projCoords.z > 1.0 ||
392
+ abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0) {
393
+ return 1.0; // Outside shadow frustum
394
+ }
395
+
396
+ // Calculate tile position in atlas
397
+ let col = slotIndex % SPOT_TILES_PER_ROW;
398
+ let row = slotIndex / SPOT_TILES_PER_ROW;
399
+
400
+ // Transform to [0,1] UV within tile
401
+ let localUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
402
+
403
+ // Transform to atlas UV
404
+ let tileOffset = vec2f(f32(col), f32(row)) * SPOT_TILE_SIZE;
405
+ let atlasUV = (tileOffset + localUV * SPOT_TILE_SIZE) / vec2f(SPOT_ATLAS_WIDTH, SPOT_ATLAS_HEIGHT);
406
+
407
+ // Sample shadow with simple 4-tap PCF
408
+ let texelSize = 1.0 / SPOT_TILE_SIZE;
409
+ let currentDepth = clamp(projCoords.z - bias * 3.0, 0.001, 0.999);
410
+
411
+ var shadowSample = 0.0;
412
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(-texelSize, 0.0), currentDepth);
413
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(texelSize, 0.0), currentDepth);
414
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, -texelSize), currentDepth);
415
+ shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, texelSize), currentDepth);
416
+ shadowSample /= 4.0;
417
+
418
+ return shadowSample;
419
+ }
420
+
421
+ // Apply pre-computed lighting to particle color
422
+ // Lighting is calculated per-particle in the compute shader with temporal smoothing
423
+ fn applyLighting(baseColor: vec3f, lighting: vec3f) -> vec3f {
424
+ // Just multiply base color by pre-computed lighting
425
+ // Lighting already includes ambient, shadows, point/spot lights, and emissive
426
+ return baseColor * lighting;
427
+ }
428
+
429
+ struct FragmentOutput {
430
+ @location(0) color: vec4f,
431
+ @builtin(frag_depth) depth: f32,
432
+ }
433
+
434
+ // Calculate soft particle fade based on depth difference
435
+ fn calcSoftFade(fragPos: vec4f, particleLinearDepth: f32) -> f32 {
436
+ if (uniforms.softness <= 0.0) {
437
+ return 1.0;
438
+ }
439
+
440
+ // Get screen coordinates
441
+ let screenPos = vec2i(fragPos.xy);
442
+ let screenSize = vec2i(uniforms.screenSize);
443
+
444
+ // Bounds check
445
+ if (screenPos.x < 0 || screenPos.x >= screenSize.x ||
446
+ screenPos.y < 0 || screenPos.y >= screenSize.y) {
447
+ return 1.0;
448
+ }
449
+
450
+ // Sample scene depth (linear depth in 0-1 range from GBuffer)
451
+ let sceneDepthNorm = textureLoad(depthTexture, screenPos, 0);
452
+
453
+ // If depth is 0 (no valid data), skip soft fade
454
+ if (sceneDepthNorm <= 0.0) {
455
+ return 1.0;
456
+ }
457
+
458
+ // Convert both to world units for comparison
459
+ let sceneDepth = uniforms.near + sceneDepthNorm * (uniforms.far - uniforms.near);
460
+ let particleDepth = uniforms.near + particleLinearDepth * (uniforms.far - uniforms.near);
461
+
462
+ // Fade based on depth difference (in world units)
463
+ let depthDiff = sceneDepth - particleDepth;
464
+ return saturate(depthDiff / uniforms.softness);
465
+ }
466
+
467
+ // Fragment for alpha blend mode
468
+ @fragment
469
+ fn fragmentMainAlpha(input: VertexOutput) -> FragmentOutput {
470
+ var output: FragmentOutput;
471
+
472
+ // Sample texture directly
473
+ let texColor = textureSample(particleTexture, particleSampler, input.uv);
474
+
475
+ // Combine texture with particle color
476
+ var alpha = texColor.a * input.color.a;
477
+
478
+ // Apply soft particle fade
479
+ alpha *= calcSoftFade(input.position, input.linearDepth);
480
+
481
+ // Discard nearly transparent pixels
482
+ if (alpha < 0.001) {
483
+ discard;
484
+ }
485
+
486
+ // Apply pre-computed lighting from particle
487
+ let baseColor = texColor.rgb * input.color.rgb;
488
+ let litColor = applyLighting(baseColor, input.lighting);
489
+
490
+ // Fog is applied as post-process to the combined HDR buffer
491
+ output.color = vec4f(litColor, alpha);
492
+ output.depth = input.linearDepth;
493
+ return output;
494
+ }
495
+
496
+ // Fragment for additive mode
497
+ @fragment
498
+ fn fragmentMainAdditive(input: VertexOutput) -> FragmentOutput {
499
+ var output: FragmentOutput;
500
+
501
+ // Sample texture directly
502
+ let texColor = textureSample(particleTexture, particleSampler, input.uv);
503
+
504
+ // Combine texture with particle color
505
+ var alpha = texColor.a * input.color.a;
506
+
507
+ // Apply soft particle fade
508
+ alpha *= calcSoftFade(input.position, input.linearDepth);
509
+
510
+ // Discard nearly transparent pixels
511
+ if (alpha < 0.001) {
512
+ discard;
513
+ }
514
+
515
+ // Apply pre-computed lighting from particle
516
+ let baseColor = texColor.rgb * input.color.rgb;
517
+ let litColor = applyLighting(baseColor, input.lighting);
518
+
519
+ // Fog is applied as post-process to the combined HDR buffer
520
+ // Premultiply for additive blending
521
+ let rgb = litColor * alpha;
522
+ output.color = vec4f(rgb, alpha);
523
+ output.depth = input.linearDepth;
524
+ return output;
525
+ }