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,550 @@
1
+ struct VertexInput {
2
+ @location(0) position: vec3f,
3
+ @location(1) uv: vec2f,
4
+ @location(2) normal: vec3f,
5
+ @location(3) color: vec4f,
6
+ @location(4) weights: vec4f,
7
+ @location(5) joints: vec4u,
8
+ }
9
+
10
+ struct VertexOutput {
11
+ @invariant @builtin(position) position: vec4f, // @invariant ensures shared edges compute identical positions
12
+ @location(0) worldPos: vec3f,
13
+ @location(1) uv: vec2f,
14
+ @location(2) normal: vec3f,
15
+ @location(3) viewZ: f32, // Linear view-space depth for logarithmic encoding
16
+ @location(4) currClipPos: vec4f, // Current frame clip position (for motion vectors)
17
+ @location(5) prevClipPos: vec4f, // Previous frame clip position (for motion vectors)
18
+ @location(6) instanceColor: vec4f, // Per-instance color tint for sprites
19
+ @location(7) anchorY: f32, // Entity anchor Y position (for billboard clip plane testing)
20
+ }
21
+
22
+ struct Uniforms {
23
+ viewMatrix: mat4x4f,
24
+ projectionMatrix: mat4x4f,
25
+ prevViewProjMatrix: mat4x4f, // Previous frame view-projection for motion vectors
26
+ mipBias: f32,
27
+ skinEnabled: f32, // 1.0 if skinning enabled, 0.0 otherwise
28
+ numJoints: f32, // Number of joints in the skin
29
+ near: f32, // Camera near plane
30
+ far: f32, // Camera far plane
31
+ jitterFadeDistance: f32, // Distance at which jitter fades to 0 (meters)
32
+ jitterOffset: vec2f, // TAA jitter offset in pixels
33
+ screenSize: vec2f, // Screen dimensions for pixel-to-clip conversion
34
+ emissionFactor: vec4f,
35
+ clipPlaneY: f32, // Y level for clip plane
36
+ clipPlaneEnabled: f32, // 1.0 to enable clip plane, 0.0 to disable
37
+ clipPlaneDirection: f32, // 1.0 = discard below clipPlaneY, -1.0 = discard above clipPlaneY
38
+ pixelRounding: f32, // Pixel grid size (0=off, 1=every pixel, 2=every 2px, etc.)
39
+ pixelExpansion: f32, // Sub-pixel expansion to convert gaps to overlaps (0=off, 0.05=default)
40
+ positionRounding: f32, // If > 0, round view-space position to this precision (simulates fixed-point)
41
+ alphaHashEnabled: f32, // 1.0 to enable alpha hashing/dithering for cutout transparency
42
+ alphaHashScale: f32, // Scale factor for alpha hash (default 1.0, higher = more opaque)
43
+ luminanceToAlpha: f32, // 1.0 to derive alpha from base color luminance (black=transparent)
44
+ noiseSize: f32, // Size of noise texture (8 for bayer, 64/128 for blue noise)
45
+ noiseOffsetX: f32, // Animated noise offset X
46
+ noiseOffsetY: f32, // Animated noise offset Y
47
+ cameraPosition: vec3f, // World-space camera position for distance fade
48
+ distanceFadeStart: f32, // Distance where fade begins (A)
49
+ distanceFadeEnd: f32, // Distance where fade completes (B) - object invisible
50
+ // Billboard/sprite uniforms
51
+ billboardMode: f32, // 0=none, 1=center (spherical), 2=bottom (cylindrical), 3=horizontal
52
+ billboardCameraRight: vec3f, // Camera right vector for billboarding
53
+ _pad1: f32,
54
+ billboardCameraUp: vec3f, // Camera up vector for billboarding
55
+ _pad2: f32,
56
+ billboardCameraForward: vec3f, // Camera forward vector for billboarding
57
+ specularBoost: f32, // Per-material specular boost (0-1), scales the 3-light specular effect
58
+ }
59
+
60
+ struct InstanceData {
61
+ modelMatrix: mat4x4f,
62
+ posRadius: vec4f,
63
+ }
64
+
65
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
66
+ @group(0) @binding(1) var albedoTexture: texture_2d<f32>;
67
+ @group(0) @binding(2) var albedoSampler: sampler;
68
+ @group(0) @binding(3) var normalTexture: texture_2d<f32>;
69
+ @group(0) @binding(4) var normalSampler: sampler;
70
+ @group(0) @binding(5) var ambientTexture: texture_2d<f32>;
71
+ @group(0) @binding(6) var ambientSampler: sampler;
72
+ @group(0) @binding(7) var rmTexture: texture_2d<f32>;
73
+ @group(0) @binding(8) var rmSampler: sampler;
74
+ @group(0) @binding(9) var emissionTexture: texture_2d<f32>;
75
+ @group(0) @binding(10) var emissionSampler: sampler;
76
+ @group(0) @binding(11) var jointTexture: texture_2d<f32>;
77
+ @group(0) @binding(12) var jointSampler: sampler;
78
+ @group(0) @binding(13) var prevJointTexture: texture_2d<f32>; // Previous frame joint matrices for skinned motion vectors
79
+ @group(0) @binding(14) var noiseTexture: texture_2d<f32>; // Noise texture for alpha hashing (blue noise or bayer)
80
+
81
+ // Sample noise at screen position (tiled with animated offset)
82
+ fn sampleNoise(screenPos: vec2f) -> f32 {
83
+ let noiseSize = i32(uniforms.noiseSize);
84
+ let noiseOffsetX = i32(uniforms.noiseOffsetX * f32(noiseSize));
85
+ let noiseOffsetY = i32(uniforms.noiseOffsetY * f32(noiseSize));
86
+
87
+ let texCoord = vec2i(
88
+ (i32(screenPos.x) + noiseOffsetX) % noiseSize,
89
+ (i32(screenPos.y) + noiseOffsetY) % noiseSize
90
+ );
91
+ return textureLoad(noiseTexture, texCoord, 0).r;
92
+ }
93
+
94
+ // Get a 4x4 matrix from the joint texture
95
+ // Each row of the texture stores one joint's matrix (4 RGBA pixels = 16 floats)
96
+ fn getJointMatrix(jointIndex: u32) -> mat4x4f {
97
+ let row = i32(jointIndex);
98
+ let col0 = textureLoad(jointTexture, vec2i(0, row), 0);
99
+ let col1 = textureLoad(jointTexture, vec2i(1, row), 0);
100
+ let col2 = textureLoad(jointTexture, vec2i(2, row), 0);
101
+ let col3 = textureLoad(jointTexture, vec2i(3, row), 0);
102
+ return mat4x4f(col0, col1, col2, col3);
103
+ }
104
+
105
+ // Get a 4x4 matrix from the previous frame joint texture (for motion vectors)
106
+ fn getPrevJointMatrix(jointIndex: u32) -> mat4x4f {
107
+ let row = i32(jointIndex);
108
+ let col0 = textureLoad(prevJointTexture, vec2i(0, row), 0);
109
+ let col1 = textureLoad(prevJointTexture, vec2i(1, row), 0);
110
+ let col2 = textureLoad(prevJointTexture, vec2i(2, row), 0);
111
+ let col3 = textureLoad(prevJointTexture, vec2i(3, row), 0);
112
+ return mat4x4f(col0, col1, col2, col3);
113
+ }
114
+
115
+ // Apply skinning to a position
116
+ fn applySkinning(position: vec3f, joints: vec4u, weights: vec4f) -> vec3f {
117
+ var skinnedPos = vec3f(0.0);
118
+
119
+ let m0 = getJointMatrix(joints.x);
120
+ let m1 = getJointMatrix(joints.y);
121
+ let m2 = getJointMatrix(joints.z);
122
+ let m3 = getJointMatrix(joints.w);
123
+
124
+ skinnedPos += (m0 * vec4f(position, 1.0)).xyz * weights.x;
125
+ skinnedPos += (m1 * vec4f(position, 1.0)).xyz * weights.y;
126
+ skinnedPos += (m2 * vec4f(position, 1.0)).xyz * weights.z;
127
+ skinnedPos += (m3 * vec4f(position, 1.0)).xyz * weights.w;
128
+
129
+ return skinnedPos;
130
+ }
131
+
132
+ // Apply skinning with previous frame joint matrices (for motion vectors)
133
+ fn applySkinningPrev(position: vec3f, joints: vec4u, weights: vec4f) -> vec3f {
134
+ var skinnedPos = vec3f(0.0);
135
+
136
+ let m0 = getPrevJointMatrix(joints.x);
137
+ let m1 = getPrevJointMatrix(joints.y);
138
+ let m2 = getPrevJointMatrix(joints.z);
139
+ let m3 = getPrevJointMatrix(joints.w);
140
+
141
+ skinnedPos += (m0 * vec4f(position, 1.0)).xyz * weights.x;
142
+ skinnedPos += (m1 * vec4f(position, 1.0)).xyz * weights.y;
143
+ skinnedPos += (m2 * vec4f(position, 1.0)).xyz * weights.z;
144
+ skinnedPos += (m3 * vec4f(position, 1.0)).xyz * weights.w;
145
+
146
+ return skinnedPos;
147
+ }
148
+
149
+ // Apply skinning to a normal (using 3x3 rotation part of matrices)
150
+ fn applySkinningNormal(normal: vec3f, joints: vec4u, weights: vec4f) -> vec3f {
151
+ var skinnedNormal = vec3f(0.0);
152
+
153
+ let m0 = getJointMatrix(joints.x);
154
+ let m1 = getJointMatrix(joints.y);
155
+ let m2 = getJointMatrix(joints.z);
156
+ let m3 = getJointMatrix(joints.w);
157
+
158
+ // Extract 3x3 rotation matrices
159
+ let r0 = mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz);
160
+ let r1 = mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz);
161
+ let r2 = mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz);
162
+ let r3 = mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz);
163
+
164
+ skinnedNormal += (r0 * normal) * weights.x;
165
+ skinnedNormal += (r1 * normal) * weights.y;
166
+ skinnedNormal += (r2 * normal) * weights.z;
167
+ skinnedNormal += (r3 * normal) * weights.w;
168
+
169
+ return normalize(skinnedNormal);
170
+ }
171
+
172
+ @vertex
173
+ fn vertexMain(
174
+ input: VertexInput,
175
+ @builtin(instance_index) instanceIdx : u32,
176
+ @location(6) instanceModelMatrix0: vec4f,
177
+ @location(7) instanceModelMatrix1: vec4f,
178
+ @location(8) instanceModelMatrix2: vec4f,
179
+ @location(9) instanceModelMatrix3: vec4f,
180
+ @location(10) posRadius: vec4f,
181
+ @location(11) uvTransform: vec4f, // UV offset (xy) and scale (zw) for sprite sheets
182
+ @location(12) instanceColor: vec4f, // Per-instance color tint
183
+ ) -> VertexOutput {
184
+ var output: VertexOutput;
185
+
186
+ // Construct instance model matrix
187
+ let instanceModelMatrix = mat4x4f(
188
+ instanceModelMatrix0,
189
+ instanceModelMatrix1,
190
+ instanceModelMatrix2,
191
+ instanceModelMatrix3
192
+ );
193
+
194
+ var localPos = input.position;
195
+ var prevLocalPos = input.position; // For motion vectors
196
+ var localNormal = input.normal;
197
+ var billboardWorldPos: vec3f;
198
+ var useBillboardWorldPos = false;
199
+
200
+ // Apply billboard transform if enabled
201
+ if (uniforms.billboardMode > 0.5) {
202
+ // Extract entity position (translation) from instance matrix
203
+ let entityPos = instanceModelMatrix[3].xyz;
204
+
205
+ // Extract scale from matrix (length of each column's xyz)
206
+ let scaleX = length(instanceModelMatrix[0].xyz);
207
+ let scaleY = length(instanceModelMatrix[1].xyz);
208
+
209
+ if (uniforms.billboardMode < 1.5) {
210
+ // Mode 1: Center (spherical billboard) - faces camera, centered on entity
211
+ // Calculate from camera position and world-up to avoid view matrix flip issues
212
+ let toCamera = uniforms.cameraPosition - entityPos;
213
+ let toCameraDist = length(toCamera);
214
+
215
+ var right: vec3f;
216
+ var up: vec3f;
217
+
218
+ if (toCameraDist > 0.001) {
219
+ let forward = toCamera / toCameraDist;
220
+ // Right = cross(worldUp, forward)
221
+ right = cross(vec3f(0.0, 1.0, 0.0), forward);
222
+ let rightLen = length(right);
223
+ if (rightLen < 0.001) {
224
+ // Camera directly above/below - use camera right as fallback
225
+ right = uniforms.billboardCameraRight;
226
+ up = vec3f(0.0, 0.0, 1.0); // Use Z as up when looking straight down
227
+ } else {
228
+ right = right / rightLen;
229
+ // Up is perpendicular to both forward and right
230
+ up = cross(forward, right);
231
+ }
232
+ } else {
233
+ right = uniforms.billboardCameraRight;
234
+ up = vec3f(0.0, 1.0, 0.0);
235
+ }
236
+
237
+ let offset = right * input.position.x * scaleX + up * input.position.y * scaleY;
238
+ billboardWorldPos = entityPos + offset;
239
+ useBillboardWorldPos = true;
240
+ localNormal = vec3f(0.0, 1.0, 0.0); // Normal points up
241
+
242
+ } else if (uniforms.billboardMode < 2.5) {
243
+ // Mode 2: Bottom (face camera position) - tilts toward camera, pivots at bottom
244
+ // Like a sunflower facing the sun - when viewed from above, nearly horizontal
245
+ let toCamera = uniforms.cameraPosition - entityPos;
246
+ let toCameraDist = length(toCamera);
247
+
248
+ var right: vec3f;
249
+ var billUp: vec3f;
250
+
251
+ if (toCameraDist > 0.001) {
252
+ let forward = toCamera / toCameraDist; // Direction toward camera
253
+ // Right = cross(worldUp, forward)
254
+ right = cross(vec3f(0.0, 1.0, 0.0), forward);
255
+ let rightLen = length(right);
256
+ if (rightLen < 0.001) {
257
+ // Camera directly above/below - use camera right as fallback
258
+ right = uniforms.billboardCameraRight;
259
+ billUp = uniforms.billboardCameraUp;
260
+ } else {
261
+ right = right / rightLen;
262
+ // Billboard "up" is perpendicular to both forward and right
263
+ billUp = cross(forward, right);
264
+ }
265
+ } else {
266
+ // Camera at entity position - use camera vectors as fallback
267
+ right = uniforms.billboardCameraRight;
268
+ billUp = uniforms.billboardCameraUp;
269
+ }
270
+
271
+ let offset = right * input.position.x * scaleX + billUp * input.position.y * scaleY;
272
+ billboardWorldPos = entityPos + offset;
273
+ useBillboardWorldPos = true;
274
+ localNormal = vec3f(0.0, 1.0, 0.0); // Normal points up
275
+
276
+ } else {
277
+ // Mode 3: Horizontal - fixed in world space on XZ plane
278
+ // Uses entity's model matrix rotation (yaw only in practice)
279
+ // Don't modify localPos - let model matrix apply the transform
280
+ // Geometry is already on XZ plane from Geometry.billboardQuad('horizontal')
281
+ localNormal = vec3f(0.0, 1.0, 0.0); // Normal points up
282
+ }
283
+ }
284
+
285
+ // Apply skinning if enabled
286
+ if (uniforms.skinEnabled > 0.5) {
287
+ // Check if weights sum to something meaningful
288
+ let weightSum = input.weights.x + input.weights.y + input.weights.z + input.weights.w;
289
+ if (weightSum > 0.001) {
290
+ localPos = applySkinning(input.position, input.joints, input.weights);
291
+ prevLocalPos = applySkinningPrev(input.position, input.joints, input.weights);
292
+ localNormal = applySkinningNormal(input.normal, input.joints, input.weights);
293
+ }
294
+ }
295
+
296
+ // Apply instance transform
297
+ // For billboard modes 1 and 2, we computed worldPos directly (bypassing matrix rotation)
298
+ // For billboard mode 3 and non-billboard, use standard matrix transform
299
+ var worldPos: vec3f;
300
+ if (useBillboardWorldPos) {
301
+ worldPos = billboardWorldPos;
302
+ } else {
303
+ worldPos = (instanceModelMatrix * vec4f(localPos, 1.0)).xyz;
304
+ }
305
+ var viewPos = uniforms.viewMatrix * vec4f(worldPos, 1.0);
306
+
307
+ // Check if this instance allows rounding (negative radius = no rounding)
308
+ let allowRounding = posRadius.w >= 0.0;
309
+
310
+ // Position rounding: simulate fixed-point by rounding view-space position
311
+ // Only round X and Y - rounding Z can cause clipping issues when vertices are near camera
312
+ if (allowRounding && uniforms.positionRounding > 0.0) {
313
+ let snap = uniforms.positionRounding;
314
+ viewPos = vec4f(
315
+ floor(viewPos.x / snap) * snap,
316
+ floor(viewPos.y / snap) * snap,
317
+ viewPos.z,
318
+ viewPos.w
319
+ );
320
+ }
321
+
322
+ var clipPos = uniforms.projectionMatrix * viewPos;
323
+
324
+ // Compute previous world position (using same instance matrix - static objects)
325
+ // For moving objects, we'd need prevInstanceModelMatrix in the instance buffer
326
+ // For billboards, use same worldPos (billboard faces camera each frame)
327
+ var prevWorldPos: vec3f;
328
+ if (useBillboardWorldPos) {
329
+ prevWorldPos = billboardWorldPos; // Billboard - same as current (TODO: proper motion vectors)
330
+ } else {
331
+ prevWorldPos = (instanceModelMatrix * vec4f(prevLocalPos, 1.0)).xyz;
332
+ }
333
+ let prevClipPos = uniforms.prevViewProjMatrix * vec4f(prevWorldPos, 1.0);
334
+
335
+ // Apply TAA jitter with distance-based fade
336
+ // Full jitter near camera, fading to 0 at jitterFadeDistance
337
+ let viewDist = -viewPos.z; // Positive distance from camera
338
+ let jitterFade = saturate(1.0 - viewDist / uniforms.jitterFadeDistance);
339
+
340
+ // Convert pixel offset to clip space and apply with fade
341
+ // In clip space, 1 pixel = 2/screenSize (since clip space is -1 to 1)
342
+ let jitterClip = uniforms.jitterOffset * 2.0 / uniforms.screenSize * jitterFade;
343
+ clipPos.x += jitterClip.x * clipPos.w;
344
+ clipPos.y += jitterClip.y * clipPos.w;
345
+
346
+ // Pixel rounding: snap vertices to pixel grid for retro/PSX aesthetic
347
+ // pixelRounding value = grid size in pixels (1.0 = every pixel, 2.0 = every 2 pixels, etc.)
348
+ if (allowRounding && uniforms.pixelRounding > 0.5) {
349
+ let gridSize = uniforms.pixelRounding; // Grid size in pixels
350
+ let ndc = clipPos.xy / clipPos.w;
351
+
352
+ // Convert NDC (-1..1) to pixel coordinates (0..screenSize)
353
+ let pixelCoords = (ndc + 1.0) * 0.5 * uniforms.screenSize;
354
+
355
+ // Snap to coarse grid (every gridSize pixels)
356
+ var snappedPixel = floor(pixelCoords / gridSize) * gridSize;
357
+
358
+ // Expansion to convert gaps to overlaps (push vertices slightly outward from screen center)
359
+ if (uniforms.pixelExpansion > 0.0) {
360
+ let screenCenter = uniforms.screenSize * 0.5;
361
+ let fromCenter = snappedPixel - screenCenter;
362
+ snappedPixel += sign(fromCenter) * uniforms.pixelExpansion;
363
+ }
364
+
365
+ // Convert back to NDC
366
+ let snappedNDC = (snappedPixel / uniforms.screenSize) * 2.0 - 1.0;
367
+
368
+ // Convert back to clip space
369
+ clipPos.x = snappedNDC.x * clipPos.w;
370
+ clipPos.y = snappedNDC.y * clipPos.w;
371
+ }
372
+
373
+ output.position = clipPos;
374
+ output.worldPos = worldPos;
375
+ // Apply UV transform for sprite sheets: uv * scale + offset
376
+ output.uv = input.uv * uvTransform.zw + uvTransform.xy;
377
+ output.viewZ = viewDist;
378
+ output.currClipPos = clipPos;
379
+ output.prevClipPos = prevClipPos;
380
+ output.instanceColor = instanceColor;
381
+
382
+ // For billboard sprites, use entity position Y for clip plane testing
383
+ // In planar reflections, the billboard camera vectors are flipped, which causes
384
+ // worldPos to be computed at wrong Y values. Using entityPos directly is simpler
385
+ // and more reliable - if the entity is above ground, the sprite shows.
386
+ if (uniforms.billboardMode > 0.5) {
387
+ let entityPos = instanceModelMatrix[3].xyz;
388
+ output.anchorY = entityPos.y;
389
+ } else {
390
+ output.anchorY = worldPos.y; // For regular meshes, use fragment world Y
391
+ }
392
+
393
+ // Apply instance transform to normal
394
+ // For billboard sprites, the normal is already in world space (0, 1, 0)
395
+ if (uniforms.billboardMode > 0.5) {
396
+ output.normal = localNormal; // Already world-space (0, 1, 0) for all billboard modes
397
+ } else {
398
+ let normalMatrix = mat3x3f(
399
+ instanceModelMatrix[0].xyz,
400
+ instanceModelMatrix[1].xyz,
401
+ instanceModelMatrix[2].xyz
402
+ );
403
+ output.normal = normalMatrix * localNormal;
404
+ }
405
+ return output;
406
+ }
407
+
408
+ struct GBufferOutput {
409
+ @location(0) albedo: vec4f,
410
+ @location(1) normal: vec4f,
411
+ @location(2) arm: vec4f, // Ambient, Roughness, Metallic
412
+ @location(3) emission: vec4f,
413
+ @location(4) velocity: vec2f, // Motion vectors in pixels
414
+ @builtin(frag_depth) depth: f32, // Linear depth
415
+ }
416
+
417
+ // Interleaved Gradient Noise for alpha hashing
418
+ // From: http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
419
+ fn interleavedGradientNoise(screenPos: vec2f) -> f32 {
420
+ let magic = vec3f(0.06711056, 0.00583715, 52.9829189);
421
+ return fract(magic.z * fract(dot(screenPos, magic.xy)));
422
+ }
423
+
424
+ // Alternative: simple hash for variety
425
+ fn screenHash(screenPos: vec2f) -> f32 {
426
+ let p = fract(screenPos * vec2f(0.1031, 0.1030));
427
+ let p3 = p.xyx * (p.yxy + 33.33);
428
+ return fract((p3.x + p3.y) * p3.z);
429
+ }
430
+
431
+ @fragment
432
+ fn fragmentMain(input: VertexOutput) -> GBufferOutput {
433
+ // Clip plane: discard fragments based on clip plane Y level and direction
434
+ // Used for planar reflections to avoid rendering geometry on the wrong side of the water
435
+ // Direction > 0: discard below clipPlaneY (camera above water, show above-water objects)
436
+ // Direction < 0: discard above clipPlaneY (camera below water, show below-water objects)
437
+ // For billboard sprites (anchorY != worldPos.y), skip clip plane - entity placement handles it
438
+ if (uniforms.clipPlaneEnabled > 0.5 && uniforms.billboardMode < 0.5) {
439
+ let clipDist = (input.worldPos.y - uniforms.clipPlaneY) * uniforms.clipPlaneDirection;
440
+ if (clipDist < 0.0) {
441
+ discard;
442
+ }
443
+ }
444
+
445
+ // Distance fade: noise-based dithered fade to prevent popping at culling distance
446
+ // Fade from fully visible at distanceFadeStart to invisible at distanceFadeEnd
447
+ if (uniforms.distanceFadeEnd > 0.0) {
448
+ let distToCamera = length(input.worldPos - uniforms.cameraPosition);
449
+ if (distToCamera >= uniforms.distanceFadeEnd) {
450
+ discard; // Beyond fade end - fully invisible
451
+ }
452
+ if (distToCamera > uniforms.distanceFadeStart) {
453
+ // Calculate fade factor: 1.0 at fadeStart, 0.0 at fadeEnd
454
+ let fadeRange = uniforms.distanceFadeEnd - uniforms.distanceFadeStart;
455
+ let fadeFactor = 1.0 - (distToCamera - uniforms.distanceFadeStart) / fadeRange;
456
+ // Use noise-based dithering for smooth fade
457
+ let noise = sampleNoise(input.position.xy);
458
+ if (fadeFactor < noise) {
459
+ discard; // Dithered fade out
460
+ }
461
+ }
462
+ }
463
+
464
+ var output: GBufferOutput;
465
+
466
+ // Write albedo with mip offset
467
+ let mipBias = uniforms.mipBias; // Offset mip level
468
+ output.albedo = textureSampleBias(albedoTexture, albedoSampler, input.uv, mipBias);
469
+
470
+ // Apply instance color tint (for sprites)
471
+ output.albedo = output.albedo * input.instanceColor;
472
+
473
+ // Luminance to alpha: derive alpha from base color brightness (for old game assets where black=transparent)
474
+ // Only pure black (luminance < 1/255) becomes transparent - HARD discard, no noise
475
+ if (uniforms.luminanceToAlpha > 0.5) {
476
+ let luminance = dot(output.albedo.rgb, vec3f(0.299, 0.587, 0.114));
477
+ if (luminance < 0.004) {
478
+ discard; // Hard discard for pure black - no noise dithering
479
+ }
480
+ output.albedo.a = 1.0; // Everything else is fully opaque
481
+ }
482
+
483
+ // Alpha hashing: screen-space dithered alpha test (only for non-luminanceToAlpha materials)
484
+ // Uses noise texture (blue noise or bayer) for stable, temporally coherent cutout
485
+ if (uniforms.alphaHashEnabled > 0.5 && uniforms.luminanceToAlpha < 0.5) {
486
+ let alpha = output.albedo.a * uniforms.alphaHashScale;
487
+ let noise = sampleNoise(input.position.xy);
488
+
489
+ // Hard cutoff for very transparent areas (avoid random dots)
490
+ if (alpha < 0.5) {
491
+ discard;
492
+ }
493
+ // Hash only the semi-transparent edge (0.5 to 1.0 range)
494
+ // Remap alpha from [0.5, 1.0] to [0.0, 1.0] for noise comparison
495
+ let remappedAlpha = (alpha - 0.5) * 2.0;
496
+ if (remappedAlpha < noise) {
497
+ discard;
498
+ }
499
+ }
500
+
501
+
502
+ // Write world-space normal
503
+
504
+ // Sample normal map and convert from [0,1] to [-1,1] range
505
+ let nsample = textureSampleBias(normalTexture, normalSampler, input.uv, mipBias).rgb;
506
+ let tangentNormal = normalize(nsample * 2.0 - 1.0);
507
+
508
+ // Create TBN matrix using world normal
509
+ let N = normalize(input.normal);
510
+ // Use different reference vector when N is close to up/down to avoid zero cross product
511
+ let refVec = select(vec3f(0.0, 1.0, 0.0), vec3f(1.0, 0.0, 0.0), abs(N.y) > 0.9);
512
+ let T = normalize(cross(N, refVec));
513
+ let B = cross(N, T);
514
+ let TBN = mat3x3f(T, B, N);
515
+
516
+ // Transform tangent space normal to world space
517
+ let normal = normalize(TBN * tangentNormal);
518
+ // Store world Y position in .w for planar reflection distance fade
519
+ output.normal = vec4f(normal, input.worldPos.y);
520
+
521
+ // Write ARM values (Ambient, Roughness, Metallic, SpecularBoost)
522
+ var ambient = textureSampleBias(ambientTexture, ambientSampler, input.uv, mipBias).rgb;
523
+ if (ambient.r < 0.04) {
524
+ ambient.r = 1.0;
525
+ }
526
+ let rm = textureSampleBias(rmTexture, rmSampler, input.uv, mipBias).rgb;
527
+ output.arm = vec4f(ambient.r, rm.g, rm.b, uniforms.specularBoost);
528
+
529
+ // Write emission
530
+ output.emission = textureSampleBias(emissionTexture, emissionSampler, input.uv, mipBias) * uniforms.emissionFactor;
531
+
532
+ // Compute motion vectors (velocity) in pixels
533
+ // Convert clip positions to NDC
534
+ let currNDC = input.currClipPos.xy / input.currClipPos.w;
535
+ let prevNDC = input.prevClipPos.xy / input.prevClipPos.w;
536
+
537
+ // Convert NDC difference to pixel velocity
538
+ // NDC is -1 to 1, screen is 0 to screenSize
539
+ // velocity = (currNDC - prevNDC) * screenSize / 2
540
+ let velocityNDC = currNDC - prevNDC;
541
+ output.velocity = velocityNDC * uniforms.screenSize * 0.5;
542
+
543
+ // Linear depth: maps [near, far] to [0, 1]
544
+ let near = uniforms.near;
545
+ let far = uniforms.far;
546
+ let z = input.viewZ;
547
+ output.depth = (z - near) / (far - near);
548
+
549
+ return output;
550
+ }
@@ -0,0 +1,114 @@
1
+ // HiZ Reduce Compute Shader
2
+ // Reduces the depth buffer to MIN and MAX depth per 64x64 tile for occlusion culling
3
+ // MIN depth = closest geometry in tile (occluder surface)
4
+ // MAX depth = farthest geometry in tile (if < 1.0, tile is fully covered)
5
+ // Tile thickness = MAX - MIN (thin for walls, thick for angled ground)
6
+
7
+ struct Uniforms {
8
+ screenWidth: f32,
9
+ screenHeight: f32,
10
+ tileCountX: f32,
11
+ tileCountY: f32,
12
+ tileSize: f32,
13
+ near: f32,
14
+ far: f32,
15
+ clearValue: f32, // 1.0 to clear, 0.0 to accumulate
16
+ }
17
+
18
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
19
+ @group(0) @binding(1) var depthTexture: texture_depth_2d;
20
+ @group(0) @binding(2) var<storage, read_write> hizBuffer: array<f32>;
21
+
22
+ // Workgroup size: 8x8 threads, each thread processes 8x8 pixels = 64x64 per workgroup
23
+ var<workgroup> sharedMinDepth: array<f32, 64>; // 8x8 = 64 threads
24
+ var<workgroup> sharedMaxDepth: array<f32, 64>;
25
+
26
+ @compute @workgroup_size(8, 8, 1)
27
+ fn main(
28
+ @builtin(global_invocation_id) globalId: vec3u,
29
+ @builtin(local_invocation_id) localId: vec3u,
30
+ @builtin(workgroup_id) workgroupId: vec3u
31
+ ) {
32
+ let tileX = workgroupId.x;
33
+ let tileY = workgroupId.y;
34
+ let tileIndex = tileY * u32(uniforms.tileCountX) + tileX;
35
+
36
+ // Check if this tile is within bounds
37
+ if (tileX >= u32(uniforms.tileCountX) || tileY >= u32(uniforms.tileCountY)) {
38
+ return;
39
+ }
40
+
41
+ // Each thread processes an 8x8 block within the 64x64 tile
42
+ let localIndex = localId.y * 8u + localId.x;
43
+ let blockStartX = tileX * 64u + localId.x * 8u;
44
+ let blockStartY = tileY * 64u + localId.y * 8u;
45
+
46
+ // Find MIN and MAX depth in this thread's 8x8 block
47
+ var minDepth: f32 = 1.0; // Start at far plane
48
+ var maxDepth: f32 = 0.0; // Start at near plane
49
+
50
+ for (var y = 0u; y < 8u; y = y + 1u) {
51
+ for (var x = 0u; x < 8u; x = x + 1u) {
52
+ let pixelX = blockStartX + x;
53
+ let pixelY = blockStartY + y;
54
+
55
+ // Skip pixels outside screen bounds
56
+ if (pixelX < u32(uniforms.screenWidth) && pixelY < u32(uniforms.screenHeight)) {
57
+ let depth = textureLoad(depthTexture, vec2u(pixelX, pixelY), 0);
58
+ minDepth = min(minDepth, depth);
59
+ maxDepth = max(maxDepth, depth);
60
+ }
61
+ }
62
+ }
63
+
64
+ // Store in shared memory
65
+ sharedMinDepth[localIndex] = minDepth;
66
+ sharedMaxDepth[localIndex] = maxDepth;
67
+ workgroupBarrier();
68
+
69
+ // Parallel reduction in shared memory
70
+ // Step 1: 64 -> 32
71
+ if (localIndex < 32u) {
72
+ sharedMinDepth[localIndex] = min(sharedMinDepth[localIndex], sharedMinDepth[localIndex + 32u]);
73
+ sharedMaxDepth[localIndex] = max(sharedMaxDepth[localIndex], sharedMaxDepth[localIndex + 32u]);
74
+ }
75
+ workgroupBarrier();
76
+
77
+ // Step 2: 32 -> 16
78
+ if (localIndex < 16u) {
79
+ sharedMinDepth[localIndex] = min(sharedMinDepth[localIndex], sharedMinDepth[localIndex + 16u]);
80
+ sharedMaxDepth[localIndex] = max(sharedMaxDepth[localIndex], sharedMaxDepth[localIndex + 16u]);
81
+ }
82
+ workgroupBarrier();
83
+
84
+ // Step 3: 16 -> 8
85
+ if (localIndex < 8u) {
86
+ sharedMinDepth[localIndex] = min(sharedMinDepth[localIndex], sharedMinDepth[localIndex + 8u]);
87
+ sharedMaxDepth[localIndex] = max(sharedMaxDepth[localIndex], sharedMaxDepth[localIndex + 8u]);
88
+ }
89
+ workgroupBarrier();
90
+
91
+ // Step 4: 8 -> 4
92
+ if (localIndex < 4u) {
93
+ sharedMinDepth[localIndex] = min(sharedMinDepth[localIndex], sharedMinDepth[localIndex + 4u]);
94
+ sharedMaxDepth[localIndex] = max(sharedMaxDepth[localIndex], sharedMaxDepth[localIndex + 4u]);
95
+ }
96
+ workgroupBarrier();
97
+
98
+ // Step 5: 4 -> 2
99
+ if (localIndex < 2u) {
100
+ sharedMinDepth[localIndex] = min(sharedMinDepth[localIndex], sharedMinDepth[localIndex + 2u]);
101
+ sharedMaxDepth[localIndex] = max(sharedMaxDepth[localIndex], sharedMaxDepth[localIndex + 2u]);
102
+ }
103
+ workgroupBarrier();
104
+
105
+ // Step 6: 2 -> 1 (final result)
106
+ if (localIndex == 0u) {
107
+ let finalMinDepth = min(sharedMinDepth[0], sharedMinDepth[1]);
108
+ let finalMaxDepth = max(sharedMaxDepth[0], sharedMaxDepth[1]);
109
+
110
+ // Store both min and max depth (2 floats per tile, interleaved)
111
+ hizBuffer[tileIndex * 2u] = finalMinDepth;
112
+ hizBuffer[tileIndex * 2u + 1u] = finalMaxDepth;
113
+ }
114
+ }