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,932 @@
|
|
|
1
|
+
const PI = 3.14159;
|
|
2
|
+
const MAX_LIGHTS = 768;
|
|
3
|
+
const CASCADE_COUNT = 3;
|
|
4
|
+
const MAX_LIGHTS_PER_TILE = 256;
|
|
5
|
+
|
|
6
|
+
struct VertexOutput {
|
|
7
|
+
@builtin(position) position: vec4<f32>,
|
|
8
|
+
@location(0) uv: vec2<f32>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MAX_SPOT_SHADOWS = 16;
|
|
12
|
+
|
|
13
|
+
struct Light {
|
|
14
|
+
enabled: u32,
|
|
15
|
+
position: vec3f,
|
|
16
|
+
color: vec4f,
|
|
17
|
+
direction: vec3f,
|
|
18
|
+
geom: vec4f, // x = radius, y = inner cone, z = outer cone
|
|
19
|
+
shadowIndex: i32, // -1 if no shadow, 0-7 for spot shadow slot
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
struct Uniforms {
|
|
23
|
+
inverseViewProjection: mat4x4<f32>,
|
|
24
|
+
inverseProjection: mat4x4<f32>, // For logarithmic depth reconstruction
|
|
25
|
+
inverseView: mat4x4<f32>, // For logarithmic depth reconstruction
|
|
26
|
+
cameraPosition: vec3f,
|
|
27
|
+
canvasSize: vec2f,
|
|
28
|
+
lightDir: vec3f,
|
|
29
|
+
lightColor: vec4f,
|
|
30
|
+
ambientColor: vec4f,
|
|
31
|
+
environmentParams: vec4f, // x = diffuse level, y = specular level, z = env mip count, a = exposure
|
|
32
|
+
shadowParams: vec4f, // x = bias, y = normalBias, z = shadow strength, w = shadow map size
|
|
33
|
+
cascadeSizes: vec4f, // x, y, z = cascade half-widths in meters
|
|
34
|
+
tileParams: vec4u, // x = tile size, y = tile count X, z = max lights per tile, w = actual light count
|
|
35
|
+
noiseParams: vec4f, // x = noise texture size, y = noise offset X, z = noise offset Y, w = env encoding (0=equirect, 1=octahedral)
|
|
36
|
+
cameraParams: vec4f, // x = near, y = far, z = reflection mode (flip env Y), w = direct specular multiplier
|
|
37
|
+
specularBoost: vec4f, // x = intensity, y = roughness cutoff, z = unused, w = unused
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Hard-coded spot shadow parameters (to avoid uniform buffer alignment issues)
|
|
41
|
+
const SPOT_ATLAS_WIDTH: f32 = 2048.0;
|
|
42
|
+
const SPOT_ATLAS_HEIGHT: f32 = 2048.0;
|
|
43
|
+
const SPOT_TILE_SIZE: f32 = 512.0;
|
|
44
|
+
const SPOT_TILES_PER_ROW: i32 = 4;
|
|
45
|
+
const SPOT_FADE_START: f32 = 25.0;
|
|
46
|
+
const SPOT_MAX_DISTANCE: f32 = 30.0;
|
|
47
|
+
const SPOT_MIN_SHADOW: f32 = 0.5;
|
|
48
|
+
|
|
49
|
+
// Storage buffer for spotlight matrices (avoids uniform buffer size limits)
|
|
50
|
+
struct SpotShadowMatrices {
|
|
51
|
+
matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Storage buffer for cascade matrices
|
|
55
|
+
struct CascadeMatrices {
|
|
56
|
+
matrices: array<mat4x4<f32>, CASCADE_COUNT>,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
61
|
+
@group(0) @binding(12) var<storage, read> spotMatrices: SpotShadowMatrices;
|
|
62
|
+
@group(0) @binding(13) var<storage, read> cascadeMatrices: CascadeMatrices;
|
|
63
|
+
@group(0) @binding(14) var<storage, read> tileLightIndices: array<u32>;
|
|
64
|
+
@group(0) @binding(15) var<storage, read> lights: array<Light, MAX_LIGHTS>;
|
|
65
|
+
@group(0) @binding(1) var gAlbedo: texture_2d<f32>;
|
|
66
|
+
@group(0) @binding(2) var gNormal: texture_2d<f32>;
|
|
67
|
+
@group(0) @binding(3) var gArm: texture_2d<f32>;
|
|
68
|
+
@group(0) @binding(4) var gEmission: texture_2d<f32>;
|
|
69
|
+
@group(0) @binding(5) var gDepth: texture_depth_2d;
|
|
70
|
+
@group(0) @binding(6) var env: texture_2d<f32>;
|
|
71
|
+
@group(0) @binding(7) var envSampler: sampler;
|
|
72
|
+
@group(0) @binding(8) var shadowMapArray: texture_depth_2d_array;
|
|
73
|
+
@group(0) @binding(9) var shadowSampler: sampler_comparison;
|
|
74
|
+
@group(0) @binding(10) var spotShadowAtlas: texture_depth_2d;
|
|
75
|
+
@group(0) @binding(11) var spotShadowSampler: sampler_comparison;
|
|
76
|
+
@group(0) @binding(16) var noiseTexture: texture_2d<f32>;
|
|
77
|
+
@group(0) @binding(17) var noiseSampler: sampler;
|
|
78
|
+
@group(0) @binding(18) var aoTexture: texture_2d<f32>;
|
|
79
|
+
|
|
80
|
+
fn SphToUV(n: vec3<f32>) -> vec2<f32> {
|
|
81
|
+
var uv: vec2<f32>;
|
|
82
|
+
|
|
83
|
+
uv.x = atan2(-n.x, n.z);
|
|
84
|
+
uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);
|
|
85
|
+
|
|
86
|
+
uv.y = acos(n.y) / PI;
|
|
87
|
+
|
|
88
|
+
return uv;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Octahedral encoding: direction to UV
|
|
92
|
+
// Maps sphere to square: yaw around edges, pitch from center (top) to edges (bottom)
|
|
93
|
+
fn octEncode(n: vec3<f32>) -> vec2<f32> {
|
|
94
|
+
var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));
|
|
95
|
+
if (n2.y < 0.0) {
|
|
96
|
+
let signX = select(-1.0, 1.0, n2.x >= 0.0);
|
|
97
|
+
let signZ = select(-1.0, 1.0, n2.z >= 0.0);
|
|
98
|
+
n2 = vec3f(
|
|
99
|
+
(1.0 - abs(n2.z)) * signX,
|
|
100
|
+
n2.y,
|
|
101
|
+
(1.0 - abs(n2.x)) * signZ
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return n2.xz * 0.5 + 0.5;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Octahedral decoding: UV to direction
|
|
108
|
+
fn octDecode(uv: vec2<f32>) -> vec3<f32> {
|
|
109
|
+
var uv2 = uv * 2.0 - 1.0;
|
|
110
|
+
var n = vec3f(uv2.x, 1.0 - abs(uv2.x) - abs(uv2.y), uv2.y);
|
|
111
|
+
if (n.y < 0.0) {
|
|
112
|
+
let signX = select(-1.0, 1.0, n.x >= 0.0);
|
|
113
|
+
let signZ = select(-1.0, 1.0, n.z >= 0.0);
|
|
114
|
+
n = vec3f(
|
|
115
|
+
(1.0 - abs(n.z)) * signX,
|
|
116
|
+
n.y,
|
|
117
|
+
(1.0 - abs(n.x)) * signZ
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return normalize(n);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Sample noise texture at screen position with optional animation offset
|
|
124
|
+
// Uses textureLoad to avoid uniform control flow issues
|
|
125
|
+
fn sampleNoise(screenPos: vec2f) -> f32 {
|
|
126
|
+
let noiseSize = i32(uniforms.noiseParams.x);
|
|
127
|
+
let noiseOffsetX = i32(uniforms.noiseParams.y * f32(noiseSize));
|
|
128
|
+
let noiseOffsetY = i32(uniforms.noiseParams.z * f32(noiseSize));
|
|
129
|
+
|
|
130
|
+
// Tile the noise across screen space using modulo
|
|
131
|
+
let texCoord = vec2i(
|
|
132
|
+
(i32(screenPos.x) + noiseOffsetX) % noiseSize,
|
|
133
|
+
(i32(screenPos.y) + noiseOffsetY) % noiseSize
|
|
134
|
+
);
|
|
135
|
+
return textureLoad(noiseTexture, texCoord, 0).r;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Get 2D jitter offset from noise (-1 to 1 range)
|
|
139
|
+
// Uses textureLoad to avoid uniform control flow issues
|
|
140
|
+
fn getNoiseJitter(screenPos: vec2f) -> vec2f {
|
|
141
|
+
let noiseSize = i32(uniforms.noiseParams.x);
|
|
142
|
+
let noiseOffsetX = i32(uniforms.noiseParams.y * f32(noiseSize));
|
|
143
|
+
let noiseOffsetY = i32(uniforms.noiseParams.z * f32(noiseSize));
|
|
144
|
+
|
|
145
|
+
// Load at two offset positions for X and Y
|
|
146
|
+
let texCoord1 = vec2i(
|
|
147
|
+
(i32(screenPos.x) + noiseOffsetX) % noiseSize,
|
|
148
|
+
(i32(screenPos.y) + noiseOffsetY) % noiseSize
|
|
149
|
+
);
|
|
150
|
+
let texCoord2 = vec2i(
|
|
151
|
+
(i32(screenPos.x) + 37 + noiseOffsetX) % noiseSize,
|
|
152
|
+
(i32(screenPos.y) + 17 + noiseOffsetY) % noiseSize
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
let n1 = textureLoad(noiseTexture, texCoord1, 0).r;
|
|
156
|
+
let n2 = textureLoad(noiseTexture, texCoord2, 0).r;
|
|
157
|
+
|
|
158
|
+
return vec2f(n1, n2) * 2.0 - 1.0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Get 4-channel noise for IBL multisampling
|
|
162
|
+
fn getNoise4(screenPos: vec2f) -> vec4f {
|
|
163
|
+
let noiseSize = i32(uniforms.noiseParams.x);
|
|
164
|
+
let noiseOffsetX = i32(uniforms.noiseParams.y * f32(noiseSize));
|
|
165
|
+
let noiseOffsetY = i32(uniforms.noiseParams.z * f32(noiseSize));
|
|
166
|
+
|
|
167
|
+
let texCoord = vec2i(
|
|
168
|
+
(i32(screenPos.x) + noiseOffsetX) % noiseSize,
|
|
169
|
+
(i32(screenPos.y) + noiseOffsetY) % noiseSize
|
|
170
|
+
);
|
|
171
|
+
return textureLoad(noiseTexture, texCoord, 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Vogel disk sample pattern - gives good 2D distribution
|
|
175
|
+
fn vogelDiskSample(sampleIndex: i32, numSamples: i32, rotation: f32) -> vec2f {
|
|
176
|
+
let goldenAngle = 2.399963229728653; // pi * (3 - sqrt(5))
|
|
177
|
+
let r = sqrt((f32(sampleIndex) + 0.5) / f32(numSamples));
|
|
178
|
+
let theta = f32(sampleIndex) * goldenAngle + rotation;
|
|
179
|
+
return vec2f(r * cos(theta), r * sin(theta));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Build orthonormal basis around a direction (for cone sampling)
|
|
183
|
+
fn buildOrthonormalBasis(n: vec3f) -> mat3x3<f32> {
|
|
184
|
+
// Choose a vector not parallel to n
|
|
185
|
+
let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), abs(n.y) < 0.999);
|
|
186
|
+
let tangent = normalize(cross(up, n));
|
|
187
|
+
let bitangent = cross(n, tangent);
|
|
188
|
+
return mat3x3<f32>(tangent, bitangent, n);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn clampedDot(x: vec3<f32>, y: vec3<f32>) -> f32 {
|
|
192
|
+
return clamp(dot(x, y), 0.0, 1.0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn F_Schlick(f0: vec3<f32>, f90: vec3<f32>, VdotH: f32) -> vec3<f32> {
|
|
196
|
+
return f0 + (f90 - f0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn V_GGX(NdotL: f32, NdotV: f32, alphaRoughness: f32) -> f32 {
|
|
200
|
+
let alphaRoughnessSq = alphaRoughness * alphaRoughness;
|
|
201
|
+
|
|
202
|
+
let GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
|
|
203
|
+
let GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
|
|
204
|
+
|
|
205
|
+
let GGX = GGXV + GGXL;
|
|
206
|
+
if (GGX > 0.0) {
|
|
207
|
+
return 0.5 / GGX;
|
|
208
|
+
}
|
|
209
|
+
return 0.0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn D_GGX(NdotH: f32, alphaRoughness: f32) -> f32 {
|
|
213
|
+
let alphaRoughnessSq = alphaRoughness * alphaRoughness;
|
|
214
|
+
let f = (NdotH * NdotH) * (alphaRoughnessSq - 1.0) + 1.0;
|
|
215
|
+
return alphaRoughnessSq / (3.14159 * f * f);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn BRDF_lambertian(f0: vec3<f32>, f90: vec3<f32>, diffuseColor: vec3<f32>, specularWeight: f32, VdotH: f32) -> vec3<f32> {
|
|
219
|
+
return (1.0 - specularWeight * F_Schlick(f0, f90, VdotH)) * (diffuseColor / 3.14159);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn BRDF_specularGGX(f0: vec3<f32>, f90: vec3<f32>, alphaRoughness: f32, specularWeight: f32, VdotH: f32, NdotL: f32, NdotV: f32, NdotH: f32) -> vec3<f32> {
|
|
223
|
+
let F = F_Schlick(f0, f90, VdotH);
|
|
224
|
+
let Vis = V_GGX(NdotL, NdotV, alphaRoughness);
|
|
225
|
+
let D = D_GGX(NdotH, alphaRoughness);
|
|
226
|
+
|
|
227
|
+
return specularWeight * F * Vis * D;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Get environment UV based on encoding type (0=equirectangular, 1=octahedral)
|
|
231
|
+
fn getEnvUV(dir: vec3<f32>) -> vec2<f32> {
|
|
232
|
+
if (uniforms.noiseParams.w > 0.5) {
|
|
233
|
+
return octEncode(dir);
|
|
234
|
+
}
|
|
235
|
+
return SphToUV(dir);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn getIBLSample(reflection: vec3<f32>, lod: f32) -> vec3<f32> {
|
|
239
|
+
let envRGBE = textureSampleLevel(env, envSampler, getEnvUV(reflection), lod);
|
|
240
|
+
// Both equirectangular and octahedral textures are RGBE encoded
|
|
241
|
+
let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);
|
|
242
|
+
return envColor;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// IBL sample with noise-jittered UV to reduce banding
|
|
246
|
+
fn getIBLSampleJittered(reflection: vec3<f32>, lod: f32, screenPos: vec2f) -> vec3<f32> {
|
|
247
|
+
let baseUV = getEnvUV(reflection);
|
|
248
|
+
|
|
249
|
+
// Get 2D jitter from blue noise
|
|
250
|
+
let jitter = getNoiseJitter(screenPos);
|
|
251
|
+
|
|
252
|
+
// Jitter by 0.5 texels of the environment texture
|
|
253
|
+
let envSize = vec2f(textureDimensions(env, 0));
|
|
254
|
+
let jitterScale = 16 / envSize;
|
|
255
|
+
let jitteredUV = baseUV + jitter * jitterScale;
|
|
256
|
+
|
|
257
|
+
let envRGBE = textureSampleLevel(env, envSampler, jitteredUV, lod);
|
|
258
|
+
// Both equirectangular and octahedral textures are RGBE encoded
|
|
259
|
+
let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);
|
|
260
|
+
return envColor;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// IBL specular with multisampled cone jitter based on roughness
|
|
264
|
+
// Uses Vogel disk sampling with blue noise rotation (same technique as planar reflections)
|
|
265
|
+
fn getIBLRadianceGGX(n: vec3<f32>, v: vec3<f32>, roughness: f32, F0: vec3<f32>, specularWeight: f32, screenPos: vec2f) -> vec3<f32> {
|
|
266
|
+
let NdotV = clampedDot(n, v);
|
|
267
|
+
let lod = roughness * (uniforms.environmentParams.z - 1);
|
|
268
|
+
let reflection = normalize(reflect(-v, n));
|
|
269
|
+
|
|
270
|
+
// Fresnel calculation
|
|
271
|
+
let Fr = max(vec3<f32>(1.0 - roughness), F0) - F0;
|
|
272
|
+
let k_S = F0 + Fr * pow(1.0 - NdotV, 5.0);
|
|
273
|
+
|
|
274
|
+
// For very smooth surfaces (roughness < 0.1), use single sample
|
|
275
|
+
if (roughness < 0.1) {
|
|
276
|
+
let specularSample = getIBLSampleJittered(reflection, lod, screenPos);
|
|
277
|
+
return specularWeight * specularSample * k_S;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Multisample with roughness-based cone jitter
|
|
281
|
+
let numSamples = 4;
|
|
282
|
+
|
|
283
|
+
// Get blue noise for rotation and radius jitter
|
|
284
|
+
let noise = getNoise4(screenPos);
|
|
285
|
+
let rotationAngle = noise.r * 6.283185307; // 0 to 2*PI rotation
|
|
286
|
+
let radiusJitter = noise.g;
|
|
287
|
+
|
|
288
|
+
// Build orthonormal basis around reflection direction
|
|
289
|
+
let basis = buildOrthonormalBasis(reflection);
|
|
290
|
+
|
|
291
|
+
// Cone angle based on roughness (roughness^2 scaling, max ~30 degrees)
|
|
292
|
+
// Similar to planar reflection blur scaling
|
|
293
|
+
let maxConeAngle = 0.5; // ~30 degrees in radians
|
|
294
|
+
let coneAngle = roughness * roughness * maxConeAngle;
|
|
295
|
+
|
|
296
|
+
var colorSum = vec3f(0.0);
|
|
297
|
+
|
|
298
|
+
for (var i = 0; i < numSamples; i++) {
|
|
299
|
+
// Get Vogel disk sample position (unit disk, rotated by blue noise)
|
|
300
|
+
let diskSample = vogelDiskSample(i, numSamples, rotationAngle);
|
|
301
|
+
|
|
302
|
+
// Apply radius jitter per-sample
|
|
303
|
+
let sampleRadius = mix(0.5, 1.0, fract(radiusJitter + f32(i) * 0.618033988749895));
|
|
304
|
+
|
|
305
|
+
// Convert disk position to cone offset
|
|
306
|
+
let offset2D = diskSample * sampleRadius * coneAngle;
|
|
307
|
+
|
|
308
|
+
// Perturb reflection direction within cone
|
|
309
|
+
let perturbedDir = normalize(
|
|
310
|
+
basis[2] + // reflection direction (z-axis of basis)
|
|
311
|
+
basis[0] * offset2D.x + // tangent
|
|
312
|
+
basis[1] * offset2D.y // bitangent
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Sample environment at perturbed direction
|
|
316
|
+
let sampleColor = getIBLSample(perturbedDir, lod);
|
|
317
|
+
colorSum += sampleColor;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let specularLight = colorSum / f32(numSamples);
|
|
321
|
+
return specularWeight * specularLight * k_S;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Sample shadow from a specific cascade with blue noise jittered PCF
|
|
325
|
+
// Returns: x = shadow value (0=shadow, 1=lit), y = 1 if in bounds, 0 if out of bounds
|
|
326
|
+
fn sampleCascadeShadowWithBounds(worldPos: vec3<f32>, normal: vec3<f32>, cascadeIndex: i32, screenPos: vec2f) -> vec2f {
|
|
327
|
+
let baseBias = uniforms.shadowParams.x;
|
|
328
|
+
let baseNormalBias = uniforms.shadowParams.y;
|
|
329
|
+
let shadowMapSize = uniforms.shadowParams.w;
|
|
330
|
+
|
|
331
|
+
// Slightly increase bias for cascade 2 (larger coverage, lower resolution)
|
|
332
|
+
var biasScale = 1.0;
|
|
333
|
+
if (cascadeIndex == 2) {
|
|
334
|
+
biasScale = 1.5;
|
|
335
|
+
}
|
|
336
|
+
let bias = baseBias * biasScale;
|
|
337
|
+
let normalBias = baseNormalBias * biasScale;
|
|
338
|
+
|
|
339
|
+
// Apply normal bias
|
|
340
|
+
let biasedPos = worldPos + normal * normalBias;
|
|
341
|
+
|
|
342
|
+
// Get cascade matrix from storage buffer
|
|
343
|
+
let lightMatrix = cascadeMatrices.matrices[cascadeIndex];
|
|
344
|
+
|
|
345
|
+
// Transform to light space
|
|
346
|
+
let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
|
|
347
|
+
let projCoords = lightSpacePos.xyz / lightSpacePos.w;
|
|
348
|
+
|
|
349
|
+
// Transform to [0,1] UV space
|
|
350
|
+
let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
351
|
+
let currentDepth = projCoords.z - bias;
|
|
352
|
+
|
|
353
|
+
// Check bounds
|
|
354
|
+
let inBoundsX = shadowUV.x >= 0.0 && shadowUV.x <= 1.0;
|
|
355
|
+
let inBoundsY = shadowUV.y >= 0.0 && shadowUV.y <= 1.0;
|
|
356
|
+
let inBoundsZ = currentDepth >= 0.0 && currentDepth <= 1.0;
|
|
357
|
+
let inBounds = inBoundsX && inBoundsY && inBoundsZ;
|
|
358
|
+
|
|
359
|
+
if (!inBounds) {
|
|
360
|
+
return vec2f(1.0, 0.0); // Out of bounds
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let clampedUV = clamp(shadowUV, vec2f(0.001), vec2f(0.999));
|
|
364
|
+
let clampedDepth = clamp(currentDepth, 0.001, 0.999);
|
|
365
|
+
|
|
366
|
+
// Get blue noise jitter for this pixel
|
|
367
|
+
let jitter = getNoiseJitter(screenPos);
|
|
368
|
+
|
|
369
|
+
// Blue noise jittered PCF - rotated disk pattern
|
|
370
|
+
// Use noise to rotate and offset sample positions
|
|
371
|
+
let texelSize = 1.0 / shadowMapSize;
|
|
372
|
+
// More blur for closer cascades, less for distant ones
|
|
373
|
+
var cascadeBlurScale = 1.0; // Default for cascade 2
|
|
374
|
+
|
|
375
|
+
if (cascadeIndex == 0) {
|
|
376
|
+
cascadeBlurScale = 2.0;
|
|
377
|
+
} else if (cascadeIndex == 1) {
|
|
378
|
+
cascadeBlurScale = 1.7;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let filterRadius = texelSize * cascadeBlurScale;
|
|
382
|
+
|
|
383
|
+
// Create rotation matrix from noise
|
|
384
|
+
let angle = jitter.x * PI;
|
|
385
|
+
let cosA = cos(angle);
|
|
386
|
+
let sinA = sin(angle);
|
|
387
|
+
|
|
388
|
+
// Poisson disk sample offsets (pre-computed, 8 samples)
|
|
389
|
+
let offsets = array<vec2f, 8>(
|
|
390
|
+
vec2f(-0.94201624, -0.39906216),
|
|
391
|
+
vec2f(0.94558609, -0.76890725),
|
|
392
|
+
vec2f(-0.094184101, -0.92938870),
|
|
393
|
+
vec2f(0.34495938, 0.29387760),
|
|
394
|
+
vec2f(-0.91588581, 0.45771432),
|
|
395
|
+
vec2f(-0.81544232, -0.87912464),
|
|
396
|
+
vec2f(-0.38277543, 0.27676845),
|
|
397
|
+
vec2f(0.97484398, 0.75648379)
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
var shadow = 0.0;
|
|
401
|
+
for (var i = 0; i < 8; i++) {
|
|
402
|
+
// Rotate offset by noise angle
|
|
403
|
+
let offset = offsets[i];
|
|
404
|
+
let rotatedOffset = vec2f(
|
|
405
|
+
offset.x * cosA - offset.y * sinA,
|
|
406
|
+
offset.x * sinA + offset.y * cosA
|
|
407
|
+
) * filterRadius;
|
|
408
|
+
|
|
409
|
+
// Add subtle jitter offset
|
|
410
|
+
let jitteredOffset = rotatedOffset + jitter * texelSize * 0.15;
|
|
411
|
+
|
|
412
|
+
shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + jitteredOffset, cascadeIndex, clampedDepth);
|
|
413
|
+
}
|
|
414
|
+
shadow /= 8.0;
|
|
415
|
+
|
|
416
|
+
return vec2f(shadow, 1.0); // In bounds
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Wrapper for simple usage
|
|
420
|
+
fn sampleCascadeShadow(worldPos: vec3<f32>, normal: vec3<f32>, cascadeIndex: i32, screenPos: vec2f) -> f32 {
|
|
421
|
+
return sampleCascadeShadowWithBounds(worldPos, normal, cascadeIndex, screenPos).x;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Squircle distance function for smooth fade at edges
|
|
425
|
+
// Returns 0-1 where 1 is at the edge of the squircle
|
|
426
|
+
fn squircleDistance(uv: vec2<f32>, power: f32) -> f32 {
|
|
427
|
+
// Squircle: |x|^n + |y|^n = 1 where n > 2 gives squircle shape
|
|
428
|
+
let centered = (uv - 0.5) * 2.0; // Convert to -1 to 1
|
|
429
|
+
let absCentered = abs(centered);
|
|
430
|
+
return pow(pow(absCentered.x, power) + pow(absCentered.y, power), 1.0 / power);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Squircle distance - returns distance normalized to cascade size
|
|
434
|
+
// Power 4 gives nice rounded corners
|
|
435
|
+
fn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {
|
|
436
|
+
let normalized = offset / size;
|
|
437
|
+
let absNorm = abs(normalized);
|
|
438
|
+
return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Calculate cascaded shadow with smooth blending between cascades
|
|
442
|
+
fn calculateShadow(worldPos: vec3<f32>, normal: vec3<f32>, screenPos: vec2f) -> f32 {
|
|
443
|
+
let shadowStrength = uniforms.shadowParams.z;
|
|
444
|
+
|
|
445
|
+
// Calculate XZ offset from camera to world position
|
|
446
|
+
let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);
|
|
447
|
+
let posXZ = vec2f(worldPos.x, worldPos.z);
|
|
448
|
+
let offsetXZ = posXZ - camXZ;
|
|
449
|
+
|
|
450
|
+
// Cascade sizes from uniforms
|
|
451
|
+
let cascade0Size = uniforms.cascadeSizes.x;
|
|
452
|
+
let cascade1Size = uniforms.cascadeSizes.y;
|
|
453
|
+
let cascade2Size = uniforms.cascadeSizes.z;
|
|
454
|
+
|
|
455
|
+
// Use squircle distance for cascade selection - rounded square shape
|
|
456
|
+
let dist0 = squircleDistanceXZ(offsetXZ, cascade0Size);
|
|
457
|
+
let dist1 = squircleDistanceXZ(offsetXZ, cascade1Size);
|
|
458
|
+
let dist2 = squircleDistanceXZ(offsetXZ, cascade2Size);
|
|
459
|
+
|
|
460
|
+
// Determine which cascade(s) to sample based on squircle distance
|
|
461
|
+
var shadow = 1.0;
|
|
462
|
+
var cascadeUsed = -1;
|
|
463
|
+
var blendFactor = 0.0;
|
|
464
|
+
|
|
465
|
+
// Sample cascades with bounds checking (pass screen position for blue noise)
|
|
466
|
+
let sample0 = sampleCascadeShadowWithBounds(worldPos, normal, 0, screenPos);
|
|
467
|
+
let sample1 = sampleCascadeShadowWithBounds(worldPos, normal, 1, screenPos);
|
|
468
|
+
let sample2 = sampleCascadeShadowWithBounds(worldPos, normal, 2, screenPos);
|
|
469
|
+
|
|
470
|
+
let inBounds0 = sample0.y > 0.5;
|
|
471
|
+
let inBounds1 = sample1.y > 0.5;
|
|
472
|
+
let inBounds2 = sample2.y > 0.5;
|
|
473
|
+
|
|
474
|
+
// Cascade 0: highest detail, smallest area
|
|
475
|
+
if (dist0 < 1.0) {
|
|
476
|
+
if (inBounds0) {
|
|
477
|
+
// Blend towards cascade 1 at 90-100% of cascade 0 size
|
|
478
|
+
let blend0to1 = smoothstep(0.9, 1.0, dist0);
|
|
479
|
+
if (blend0to1 > 0.0 && inBounds1) {
|
|
480
|
+
shadow = mix(sample0.x, sample1.x, blend0to1);
|
|
481
|
+
} else {
|
|
482
|
+
shadow = sample0.x;
|
|
483
|
+
}
|
|
484
|
+
cascadeUsed = 0;
|
|
485
|
+
} else if (inBounds1) {
|
|
486
|
+
// Cascade 0 out of bounds, use cascade 1 100%
|
|
487
|
+
shadow = sample1.x;
|
|
488
|
+
cascadeUsed = 1;
|
|
489
|
+
} else if (inBounds2) {
|
|
490
|
+
// Cascade 0 and 1 out of bounds, use cascade 2 100%
|
|
491
|
+
shadow = sample2.x;
|
|
492
|
+
cascadeUsed = 2;
|
|
493
|
+
} else {
|
|
494
|
+
return mix(1.0 - shadowStrength, 1.0, 0.5);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Cascade 1: medium detail
|
|
498
|
+
else if (dist1 < 1.0) {
|
|
499
|
+
if (inBounds1) {
|
|
500
|
+
// Blend towards cascade 2 at 90-100% of cascade 1 size
|
|
501
|
+
let blend1to2 = smoothstep(0.9, 1.0, dist1);
|
|
502
|
+
if (blend1to2 > 0.0 && inBounds2) {
|
|
503
|
+
shadow = mix(sample1.x, sample2.x, blend1to2);
|
|
504
|
+
} else {
|
|
505
|
+
shadow = sample1.x;
|
|
506
|
+
}
|
|
507
|
+
cascadeUsed = 1;
|
|
508
|
+
} else if (inBounds2) {
|
|
509
|
+
// Cascade 1 out of bounds, use cascade 2 100%
|
|
510
|
+
shadow = sample2.x;
|
|
511
|
+
cascadeUsed = 2;
|
|
512
|
+
} else {
|
|
513
|
+
return mix(1.0 - shadowStrength, 1.0, 0.5);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Cascade 2: lowest detail, largest area
|
|
517
|
+
else if (dist2 < 1.0) {
|
|
518
|
+
if (inBounds2) {
|
|
519
|
+
shadow = sample2.x;
|
|
520
|
+
cascadeUsed = 2;
|
|
521
|
+
|
|
522
|
+
// Gradual fade for cascade 2:
|
|
523
|
+
// 50-95%: fade shadow towards half-lit
|
|
524
|
+
// 95-100%: fade from shadow to half-lit (0.5)
|
|
525
|
+
let lightnessFade = smoothstep(0.5, 0.95, dist2);
|
|
526
|
+
shadow = mix(shadow, 0.5, lightnessFade);
|
|
527
|
+
|
|
528
|
+
let edgeFade = smoothstep(0.95, 1.0, dist2);
|
|
529
|
+
shadow = mix(shadow, 0.5, edgeFade);
|
|
530
|
+
} else {
|
|
531
|
+
return mix(1.0 - shadowStrength, 1.0, 0.5);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Beyond all cascades: half-lit
|
|
535
|
+
else {
|
|
536
|
+
return mix(1.0 - shadowStrength, 1.0, 0.5);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Apply shadow strength
|
|
540
|
+
let shadowResult = mix(1.0 - shadowStrength, 1.0, shadow);
|
|
541
|
+
return shadowResult;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Calculate spot light shadow from atlas using storage buffer for matrices
|
|
545
|
+
// lightPos: spotlight position (for calculating distance to camera)
|
|
546
|
+
// slotIndex: shadow atlas slot (-1 if no shadow)
|
|
547
|
+
fn calculateSpotShadow(worldPos: vec3<f32>, normal: vec3<f32>, lightPos: vec3<f32>, slotIndex: i32, screenPos: vec2f) -> f32 {
|
|
548
|
+
// Use hard-coded constants to avoid uniform buffer alignment issues
|
|
549
|
+
let fadeStart = SPOT_FADE_START;
|
|
550
|
+
let maxDist = SPOT_MAX_DISTANCE;
|
|
551
|
+
let minShadow = SPOT_MIN_SHADOW;
|
|
552
|
+
let bias = uniforms.shadowParams.x;
|
|
553
|
+
let normalBias = uniforms.shadowParams.y;
|
|
554
|
+
let shadowStrength = uniforms.shadowParams.z;
|
|
555
|
+
|
|
556
|
+
// Calculate distance from spotlight to camera (not light to pixel!)
|
|
557
|
+
let lightToCam = lightPos - uniforms.cameraPosition;
|
|
558
|
+
let lightDistance = length(lightToCam);
|
|
559
|
+
|
|
560
|
+
// Get atlas parameters from constants
|
|
561
|
+
let atlasWidth = SPOT_ATLAS_WIDTH;
|
|
562
|
+
let atlasHeight = SPOT_ATLAS_HEIGHT;
|
|
563
|
+
let tileSize = SPOT_TILE_SIZE;
|
|
564
|
+
let tilesPerRow = SPOT_TILES_PER_ROW;
|
|
565
|
+
|
|
566
|
+
// Check validity
|
|
567
|
+
let hasValidSlot = f32(slotIndex >= 0 && slotIndex < MAX_SPOT_SHADOWS);
|
|
568
|
+
|
|
569
|
+
// Clamp slot index to valid range for matrix access
|
|
570
|
+
let safeSlot = clamp(slotIndex, 0, MAX_SPOT_SHADOWS - 1);
|
|
571
|
+
|
|
572
|
+
// Calculate tile position in atlas
|
|
573
|
+
let col = safeSlot % tilesPerRow;
|
|
574
|
+
let row = safeSlot / tilesPerRow;
|
|
575
|
+
|
|
576
|
+
// Apply normal bias (increased for spot lights due to perspective projection)
|
|
577
|
+
let spotNormalBias = normalBias * 2.0;
|
|
578
|
+
let biasedPos = worldPos + normal * spotNormalBias;
|
|
579
|
+
|
|
580
|
+
// Get the light matrix from storage buffer
|
|
581
|
+
let lightMatrix = spotMatrices.matrices[safeSlot];
|
|
582
|
+
|
|
583
|
+
// Transform to light space
|
|
584
|
+
let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
|
|
585
|
+
|
|
586
|
+
// Perspective divide (avoid divide by zero)
|
|
587
|
+
let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);
|
|
588
|
+
let projCoords = lightSpacePos.xyz / w;
|
|
589
|
+
|
|
590
|
+
// Calculate edge fade for soft frustum boundaries
|
|
591
|
+
// Use distance from center in clip space (max of abs x,y)
|
|
592
|
+
let edgeDist = max(abs(projCoords.x), abs(projCoords.y));
|
|
593
|
+
// Fade from 0.8 to 1.0 in clip space (soft edge)
|
|
594
|
+
let edgeFade = 1.0 - smoothstep(0.8, 1.0, edgeDist);
|
|
595
|
+
// Also fade based on depth (behind camera or too far)
|
|
596
|
+
let depthFade = smoothstep(-0.1, 0.1, projCoords.z) * (1.0 - smoothstep(0.9, 1.0, projCoords.z));
|
|
597
|
+
let frustumFade = edgeFade * depthFade;
|
|
598
|
+
|
|
599
|
+
// Transform to [0,1] UV space within the tile
|
|
600
|
+
let tileUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
601
|
+
|
|
602
|
+
// Calculate UV in atlas (normalized to atlas size)
|
|
603
|
+
let tileOffsetX = f32(col) * tileSize;
|
|
604
|
+
let tileOffsetY = f32(row) * tileSize;
|
|
605
|
+
let atlasUV = vec2f(
|
|
606
|
+
(tileOffsetX + tileUV.x * tileSize) / atlasWidth,
|
|
607
|
+
(tileOffsetY + tileUV.y * tileSize) / atlasHeight
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// Blue noise jittered PCF for spot shadows
|
|
611
|
+
let jitter = getNoiseJitter(screenPos);
|
|
612
|
+
let texelSize = 1.0 / tileSize;
|
|
613
|
+
let filterRadius = texelSize * 0.5; // Subtle filter for spot shadows
|
|
614
|
+
|
|
615
|
+
// Create rotation from noise
|
|
616
|
+
let angle = jitter.x * PI;
|
|
617
|
+
let cosA = cos(angle);
|
|
618
|
+
let sinA = sin(angle);
|
|
619
|
+
|
|
620
|
+
// 4-sample rotated PCF for spot shadows (less samples for performance)
|
|
621
|
+
let offsets = array<vec2f, 4>(
|
|
622
|
+
vec2f(-0.7, -0.7),
|
|
623
|
+
vec2f(0.7, -0.7),
|
|
624
|
+
vec2f(-0.7, 0.7),
|
|
625
|
+
vec2f(0.7, 0.7)
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Current depth with bias (increased for spot lights)
|
|
629
|
+
let spotBias = bias * 3.0;
|
|
630
|
+
let currentDepth = clamp(projCoords.z - spotBias, 0.001, 0.999);
|
|
631
|
+
|
|
632
|
+
var shadowSample = 0.0;
|
|
633
|
+
for (var i = 0; i < 4; i++) {
|
|
634
|
+
let offset = offsets[i];
|
|
635
|
+
let rotatedOffset = vec2f(
|
|
636
|
+
offset.x * cosA - offset.y * sinA,
|
|
637
|
+
offset.x * sinA + offset.y * cosA
|
|
638
|
+
) * filterRadius;
|
|
639
|
+
|
|
640
|
+
let sampleUV = clamp(atlasUV + rotatedOffset, vec2f(0.001), vec2f(0.999));
|
|
641
|
+
shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, sampleUV, currentDepth);
|
|
642
|
+
}
|
|
643
|
+
shadowSample /= 4.0;
|
|
644
|
+
|
|
645
|
+
// Apply shadow strength
|
|
646
|
+
let shadowWithStrength = mix(1.0 - shadowStrength, 1.0, shadowSample);
|
|
647
|
+
|
|
648
|
+
// Apply frustum edge fade (fade to 1.0 = no shadow at edges)
|
|
649
|
+
let edgeFadedShadow = mix(1.0, shadowWithStrength, frustumFade);
|
|
650
|
+
|
|
651
|
+
// Apply distance-based fade
|
|
652
|
+
let fadeT = clamp((lightDistance - fadeStart) / (maxDist - fadeStart), 0.0, 1.0);
|
|
653
|
+
let fadedShadow = mix(edgeFadedShadow, minShadow, fadeT);
|
|
654
|
+
|
|
655
|
+
// Calculate result for no-slot case (distance-based constant shadow)
|
|
656
|
+
let noSlotFadeT = clamp((lightDistance - fadeStart) / (maxDist - fadeStart), 0.0, 1.0);
|
|
657
|
+
let noSlotResult = mix(1.0, minShadow, noSlotFadeT);
|
|
658
|
+
|
|
659
|
+
// Select final result based on slot validity (frustum fade already applied)
|
|
660
|
+
let shadowResult = select(noSlotResult, fadedShadow, hasValidSlot > 0.5);
|
|
661
|
+
|
|
662
|
+
// Debug visualization stored in global for fragment shader to use
|
|
663
|
+
// Using projCoords.z as a debug value
|
|
664
|
+
|
|
665
|
+
return shadowResult;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
@vertex
|
|
669
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
670
|
+
var output : VertexOutput;
|
|
671
|
+
let x = f32(vertexIndex & 1u) * 4.0 - 1.0;
|
|
672
|
+
let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;
|
|
673
|
+
output.position = vec4<f32>(x, y, 0.0, 1.0);
|
|
674
|
+
output.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
|
|
675
|
+
return output;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
@fragment
|
|
679
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
680
|
+
let cs = uniforms.canvasSize;
|
|
681
|
+
let fuv = vec2i(floor(input.uv * cs));
|
|
682
|
+
let baseColor = textureLoad(gAlbedo, fuv, 0);
|
|
683
|
+
let normal = normalize(textureLoad(gNormal, fuv, 0).xyz);
|
|
684
|
+
let arm = textureLoad(gArm, fuv, 0);
|
|
685
|
+
let emissive = textureLoad(gEmission, fuv, 0).rgb;
|
|
686
|
+
let depth = textureLoad(gDepth, fuv, 0);
|
|
687
|
+
|
|
688
|
+
// Unpack ARM values (Ambient, Roughness, Metallic, per-material SpecularBoost)
|
|
689
|
+
var ao = arm.r;
|
|
690
|
+
var roughness = arm.g;
|
|
691
|
+
var metallic = arm.b;
|
|
692
|
+
let materialSpecularBoost = arm.a; // Per-material boost (0-1)
|
|
693
|
+
|
|
694
|
+
// Sample SSAO texture - this affects only ambient/environment lighting
|
|
695
|
+
let ssao = textureLoad(aoTexture, fuv, 0).r;
|
|
696
|
+
|
|
697
|
+
var specular = vec3<f32>(0.0);
|
|
698
|
+
var diffuse = vec3<f32>(0.0);
|
|
699
|
+
|
|
700
|
+
let ior = 1.5;
|
|
701
|
+
var f0 = vec3<f32>(0.04);
|
|
702
|
+
let f90 = vec3<f32>(1.0);
|
|
703
|
+
let specularWeight = 1.0;
|
|
704
|
+
|
|
705
|
+
// Reconstruct position from linear depth and UV
|
|
706
|
+
// Linear depth: depth = (z - near) / (far - near), so z = near + depth * (far - near)
|
|
707
|
+
let near = uniforms.cameraParams.x;
|
|
708
|
+
let far = uniforms.cameraParams.y;
|
|
709
|
+
let linearDepth = near + depth * (far - near);
|
|
710
|
+
|
|
711
|
+
// Get view-space ray direction from UV
|
|
712
|
+
var iuv = input.uv;
|
|
713
|
+
iuv.y = 1.0 - iuv.y;
|
|
714
|
+
let ndc = vec4f(iuv * 2.0 - 1.0, 0.0, 1.0);
|
|
715
|
+
let viewRay = uniforms.inverseProjection * ndc;
|
|
716
|
+
let rayDir = normalize(viewRay.xyz / viewRay.w);
|
|
717
|
+
|
|
718
|
+
// Reconstruct view-space position using linear depth
|
|
719
|
+
let viewPos = rayDir * (linearDepth / -rayDir.z);
|
|
720
|
+
|
|
721
|
+
// Transform to world space
|
|
722
|
+
let worldPos4 = uniforms.inverseView * vec4f(viewPos, 1.0);
|
|
723
|
+
let position = worldPos4.xyz;
|
|
724
|
+
let toCamera = uniforms.cameraPosition.xyz - position;
|
|
725
|
+
let v = normalize(toCamera);
|
|
726
|
+
|
|
727
|
+
let n = normal;
|
|
728
|
+
|
|
729
|
+
f0 = mix(f0, baseColor.rgb, metallic);
|
|
730
|
+
roughness = max(roughness, 0.04);
|
|
731
|
+
let alphaRoughness = roughness * roughness;
|
|
732
|
+
|
|
733
|
+
let c_diff = mix(baseColor.rgb, vec3<f32>(0.0), metallic);
|
|
734
|
+
|
|
735
|
+
// Combine material AO with SSAO for ambient/environment lighting only
|
|
736
|
+
let combinedAO = ao * ssao;
|
|
737
|
+
|
|
738
|
+
diffuse += combinedAO * uniforms.ambientColor.rgb * uniforms.ambientColor.a * BRDF_lambertian(f0, f90, c_diff, specularWeight, 1.0);
|
|
739
|
+
|
|
740
|
+
// Use jittered IBL sampling to reduce banding
|
|
741
|
+
let screenPos = input.position.xy;
|
|
742
|
+
let iblSample = getIBLSampleJittered(n, uniforms.environmentParams.z - 2, screenPos);
|
|
743
|
+
diffuse += combinedAO * uniforms.environmentParams.x * iblSample.rgb * BRDF_lambertian(f0, f90, c_diff, specularWeight, 1.0);
|
|
744
|
+
|
|
745
|
+
let reflection = normalize(reflect(-v, n));
|
|
746
|
+
specular += combinedAO * uniforms.environmentParams.y * getIBLRadianceGGX(n, v, roughness, f0, specularWeight, screenPos);
|
|
747
|
+
|
|
748
|
+
// calculate lighting
|
|
749
|
+
|
|
750
|
+
let onormal = n;
|
|
751
|
+
let l = normalize(uniforms.lightDir.xyz);
|
|
752
|
+
let h = normalize(l + v);
|
|
753
|
+
let NdotV = clampedDot(n, v);
|
|
754
|
+
let ONdotV = clampedDot(onormal, v);
|
|
755
|
+
let ONdotL = clampedDot(onormal, l);
|
|
756
|
+
let NdotLF = dot(n, l);
|
|
757
|
+
let NdotL = clamp(NdotLF, 0.0, 1.0);
|
|
758
|
+
let NdotLF_adjusted = NdotLF * 0.5 + 0.5;
|
|
759
|
+
let NdotH = clampedDot(n, h);
|
|
760
|
+
let LdotH = clampedDot(l, h);
|
|
761
|
+
let VdotH = clampedDot(v, h);
|
|
762
|
+
|
|
763
|
+
let intensity = uniforms.lightColor.a;
|
|
764
|
+
// Calculate shadow outside of non-uniform control flow
|
|
765
|
+
let shadow = calculateShadow(position, n, screenPos);
|
|
766
|
+
let fallOff = smoothstep(-0.1, 0.3, NdotLF); // Soft gradual falloff
|
|
767
|
+
|
|
768
|
+
// Apply lighting with shadow (using select to avoid non-uniform branching)
|
|
769
|
+
let lightActive = select(0.0, 1.0, (NdotL > 0.0 || NdotV > 0.0) && intensity > 0.0);
|
|
770
|
+
diffuse += lightActive * intensity * uniforms.lightColor.rgb * fallOff * shadow * BRDF_lambertian(f0, f90, c_diff, specularWeight, VdotH);
|
|
771
|
+
specular += lightActive * intensity * uniforms.lightColor.rgb * fallOff * shadow * BRDF_specularGGX(f0, f90, alphaRoughness, specularWeight, VdotH, fallOff, NdotV, NdotH) * uniforms.cameraParams.w;
|
|
772
|
+
|
|
773
|
+
// Tiled lighting: get tile index from pixel position
|
|
774
|
+
let tileSize = uniforms.tileParams.x;
|
|
775
|
+
let tileCountX = uniforms.tileParams.y;
|
|
776
|
+
let maxLightsPerTile = uniforms.tileParams.z;
|
|
777
|
+
let actualLightCount = uniforms.tileParams.w;
|
|
778
|
+
|
|
779
|
+
// Calculate tileCountY from canvas size
|
|
780
|
+
let tileCountY = (u32(uniforms.canvasSize.y) + tileSize - 1u) / tileSize;
|
|
781
|
+
|
|
782
|
+
let tileX = u32(floor(input.uv.x * uniforms.canvasSize.x)) / tileSize;
|
|
783
|
+
// Flip Y because UV.y increases downward but NDC Y (used in compute shader) increases upward
|
|
784
|
+
let rawTileY = u32(floor(input.uv.y * uniforms.canvasSize.y)) / tileSize;
|
|
785
|
+
let tileY = tileCountY - 1u - rawTileY;
|
|
786
|
+
let tileIndex = tileY * tileCountX + tileX;
|
|
787
|
+
let tileDataOffset = tileIndex * (maxLightsPerTile + 1u);
|
|
788
|
+
|
|
789
|
+
// Read number of lights affecting this tile
|
|
790
|
+
let tileLightCount = min(tileLightIndices[tileDataOffset], maxLightsPerTile);
|
|
791
|
+
|
|
792
|
+
// Process only lights that affect this tile
|
|
793
|
+
for (var i = 0u; i < tileLightCount; i++) {
|
|
794
|
+
let lightIndex = tileLightIndices[tileDataOffset + 1u + i];
|
|
795
|
+
if (lightIndex >= actualLightCount) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
let light = lights[lightIndex];
|
|
800
|
+
let isEnabled = f32(light.enabled);
|
|
801
|
+
|
|
802
|
+
// Calculate light direction and distance
|
|
803
|
+
let lightVec = light.position - position;
|
|
804
|
+
let dist = length(lightVec);
|
|
805
|
+
let lightDir = normalize(lightVec);
|
|
806
|
+
|
|
807
|
+
// Distance fade from CPU culling (stored in geom.w) - smooth fade to avoid popping
|
|
808
|
+
let distanceFade = light.geom.w;
|
|
809
|
+
|
|
810
|
+
// Attenuation (inverse square with radius falloff)
|
|
811
|
+
let radius = max(light.geom.x, 0.001);
|
|
812
|
+
let attenuation = max(0.0, 1.0 - dist / radius);
|
|
813
|
+
let attenuationSq = attenuation * attenuation * distanceFade;
|
|
814
|
+
|
|
815
|
+
// Spot light cone attenuation
|
|
816
|
+
let innerCone = light.geom.y;
|
|
817
|
+
let outerCone = light.geom.z;
|
|
818
|
+
let spotCos = dot(-lightDir, normalize(light.direction + vec3f(0.0001)));
|
|
819
|
+
let spotAttenuation = select(1.0, smoothstep(outerCone, innerCone, spotCos), outerCone > 0.0);
|
|
820
|
+
|
|
821
|
+
// Spot shadow calculation (only for spotlights, not point lights)
|
|
822
|
+
let isSpotlight = outerCone > 0.0;
|
|
823
|
+
let spotShadow = calculateSpotShadow(position, n, light.position, light.shadowIndex, screenPos);
|
|
824
|
+
let lightShadow = select(1.0, spotShadow, isSpotlight); // Point lights: no shadow
|
|
825
|
+
|
|
826
|
+
// Calculate lighting vectors
|
|
827
|
+
let pl = lightDir;
|
|
828
|
+
let ph = normalize(pl + v);
|
|
829
|
+
let pNdotL = clampedDot(n, pl);
|
|
830
|
+
let pNdotH = clampedDot(n, ph);
|
|
831
|
+
let pVdotH = clampedDot(v, ph);
|
|
832
|
+
|
|
833
|
+
// Light intensity from color.a, masked by enabled, attenuation, and shadow
|
|
834
|
+
let pIntensity = isEnabled * light.color.a * attenuationSq * spotAttenuation * lightShadow * pNdotL;
|
|
835
|
+
|
|
836
|
+
// Add light contribution (always compute, multiply by intensity mask)
|
|
837
|
+
// Use simpler diffuse calculation for point lights
|
|
838
|
+
diffuse += pIntensity * light.color.rgb * c_diff / PI;
|
|
839
|
+
specular += pIntensity * light.color.rgb * BRDF_specularGGX(f0, f90, alphaRoughness, specularWeight, pVdotH, pNdotL, NdotV, pNdotH) * uniforms.cameraParams.w;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Specular boost: 3 fake lights at 60° elevation, 120° apart
|
|
843
|
+
// Only affects materials with specularBoost >= 0.04 and roughness < cutoff, ignores shadows
|
|
844
|
+
let boostIntensity = uniforms.specularBoost.x;
|
|
845
|
+
let boostRoughnessCutoff = uniforms.specularBoost.y;
|
|
846
|
+
|
|
847
|
+
// Per-material specularBoost controls this effect (0 = disabled, 1 = full effect)
|
|
848
|
+
// Skip calculation entirely if material boost is below threshold (default 0 means no boost)
|
|
849
|
+
if (materialSpecularBoost >= 0.04 && boostIntensity > 0.0 && roughness < boostRoughnessCutoff && depth < 0.999999) {
|
|
850
|
+
|
|
851
|
+
// 3 light directions at 30° above horizon (sin30=0.5, cos30=0.866), 120° apart
|
|
852
|
+
// Pre-calculated normalized directions:
|
|
853
|
+
let boostDir0 = vec3f(0.0, 0.5, 0.866025); // 0° yaw
|
|
854
|
+
let boostDir1 = vec3f(0.75, 0.5, -0.433013); // 120° yaw
|
|
855
|
+
let boostDir2 = vec3f(-0.75, 0.5, -0.433013); // 240° yaw
|
|
856
|
+
|
|
857
|
+
// Fade based on roughness: full at 0, zero at cutoff
|
|
858
|
+
let roughnessFade = 1.0 - (roughness / boostRoughnessCutoff);
|
|
859
|
+
// Include per-material boost in final strength
|
|
860
|
+
let boostStrength = boostIntensity * roughnessFade * roughnessFade * materialSpecularBoost;
|
|
861
|
+
|
|
862
|
+
// Use sharper roughness for boost highlights (smaller, more concentrated)
|
|
863
|
+
// Square the roughness to make highlights much tighter
|
|
864
|
+
let boostRoughness = roughness * 0.5;
|
|
865
|
+
let boostAlphaRoughness = boostRoughness * 0.5;
|
|
866
|
+
|
|
867
|
+
// Calculate specular for each boost light
|
|
868
|
+
var boostSpecular = vec3f(0.0);
|
|
869
|
+
|
|
870
|
+
// Light 0
|
|
871
|
+
let bNdotL0 = max(dot(n, boostDir0), 0.0);
|
|
872
|
+
if (bNdotL0 > 0.0) {
|
|
873
|
+
let bH0 = normalize(boostDir0 + v);
|
|
874
|
+
let bNdotH0 = max(dot(n, bH0), 0.0);
|
|
875
|
+
let bVdotH0 = max(dot(v, bH0), 0.0);
|
|
876
|
+
boostSpecular += bNdotL0 * BRDF_specularGGX(f0, f90, boostAlphaRoughness, specularWeight, bVdotH0, bNdotL0, NdotV, bNdotH0);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Light 1
|
|
880
|
+
let bNdotL1 = max(dot(n, boostDir1), 0.0);
|
|
881
|
+
if (bNdotL1 > 0.0) {
|
|
882
|
+
let bH1 = normalize(boostDir1 + v);
|
|
883
|
+
let bNdotH1 = max(dot(n, bH1), 0.0);
|
|
884
|
+
let bVdotH1 = max(dot(v, bH1), 0.0);
|
|
885
|
+
boostSpecular += bNdotL1 * BRDF_specularGGX(f0, f90, boostAlphaRoughness, specularWeight, bVdotH1, bNdotL1, NdotV, bNdotH1);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Light 2
|
|
889
|
+
let bNdotL2 = max(dot(n, boostDir2), 0.0);
|
|
890
|
+
if (bNdotL2 > 0.0) {
|
|
891
|
+
let bH2 = normalize(boostDir2 + v);
|
|
892
|
+
let bNdotH2 = max(dot(n, bH2), 0.0);
|
|
893
|
+
let bVdotH2 = max(dot(v, bH2), 0.0);
|
|
894
|
+
boostSpecular += bNdotL2 * BRDF_specularGGX(f0, f90, boostAlphaRoughness, specularWeight, bVdotH2, bNdotL2, NdotV, bNdotH2);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Add boost specular (white light, same intensity as main light would have)
|
|
898
|
+
specular += boostSpecular * boostStrength;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// calculate final color
|
|
902
|
+
|
|
903
|
+
let specularColor = specular + emissive;
|
|
904
|
+
let diffuseColor = diffuse;
|
|
905
|
+
|
|
906
|
+
var color = diffuseColor + specularColor;
|
|
907
|
+
color *= uniforms.environmentParams.a;
|
|
908
|
+
|
|
909
|
+
if (depth > 0.999999) {
|
|
910
|
+
var bgDir = v;
|
|
911
|
+
if (uniforms.noiseParams.w > 0.5) {
|
|
912
|
+
// Octahedral encoding: captured probe uses standard coordinate system
|
|
913
|
+
// Just negate view direction to get world direction
|
|
914
|
+
bgDir = -v;
|
|
915
|
+
} else {
|
|
916
|
+
// Equirectangular encoding: flip to match IBL lighting orientation
|
|
917
|
+
bgDir.x *= -1.0;
|
|
918
|
+
bgDir.z *= -1.0;
|
|
919
|
+
bgDir.y *= -1.0;
|
|
920
|
+
}
|
|
921
|
+
// In reflection mode, flip Y to mirror the sky
|
|
922
|
+
if (uniforms.cameraParams.z > 0.5) {
|
|
923
|
+
bgDir.y *= -1.0;
|
|
924
|
+
}
|
|
925
|
+
// Background sky: only apply exposure, NOT diffuse/specular IBL levels
|
|
926
|
+
// Those levels are for PBR lighting, not for displaying the actual sky
|
|
927
|
+
let bgSample = getIBLSample(bgDir, 0.0) * uniforms.environmentParams.a;
|
|
928
|
+
color = bgSample;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return vec4f(color, 1.0);
|
|
932
|
+
}
|