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,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
|
+
}
|