topazcube 0.1.31 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +170 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +563 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2258 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,831 @@
1
+ import { BasePass } from "./BasePass.js"
2
+ import { Texture } from "../../Texture.js"
3
+ import { Frustum } from "../../utils/Frustum.js"
4
+
5
+ import lightingCommonWGSL from "../shaders/lighting_common.wgsl"
6
+
7
+ /**
8
+ * TransparentPass - Forward rendering pass for transparent objects
9
+ *
10
+ * Renders transparent meshes with full PBR lighting on top of the HDR buffer.
11
+ * Uses back-to-front sorting for correct alpha blending.
12
+ * Reads depth from GBuffer for occlusion testing but does NOT write depth,
13
+ * so fog pass uses scene depth (what's behind transparent objects).
14
+ */
15
+ class TransparentPass extends BasePass {
16
+ constructor(engine = null) {
17
+ super('Transparent', engine)
18
+
19
+ this.pipeline = null
20
+ this.bindGroupLayout = null
21
+ this.uniformBuffer = null
22
+ this.pipelineCache = new Map()
23
+
24
+ // References to shared resources
25
+ this.gbuffer = null
26
+ this.lightingUniforms = null
27
+ this.shadowPass = null
28
+ this.environmentMap = null
29
+ this.noiseTexture = null
30
+ this.noiseSize = 64
31
+ this.noiseAnimated = true
32
+
33
+ // Light buffers
34
+ this.tileLightBuffer = null
35
+ this.lightBuffer = null
36
+
37
+ // HiZ pass reference for occlusion culling
38
+ this.hizPass = null
39
+
40
+ // Frustum for transparent mesh culling
41
+ this.frustum = new Frustum()
42
+
43
+ // Distance fade for preventing object popping at culling distance
44
+ this.distanceFadeStart = 0 // Distance where fade begins
45
+ this.distanceFadeEnd = 0 // Distance where fade completes (0 = disabled)
46
+
47
+ // Culling stats for transparent meshes
48
+ this.cullingStats = {
49
+ total: 0,
50
+ rendered: 0,
51
+ culledByFrustum: 0,
52
+ culledByDistance: 0,
53
+ culledByOcclusion: 0
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Set the HiZ pass for occlusion culling
59
+ * @param {HiZPass} hizPass - The HiZ pass instance
60
+ */
61
+ setHiZPass(hizPass) {
62
+ this.hizPass = hizPass
63
+ }
64
+
65
+ /**
66
+ * Test if a transparent mesh should be culled
67
+ * @param {Mesh} mesh - The mesh to test
68
+ * @param {Camera} camera - Current camera
69
+ * @param {boolean} canCull - Whether frustum/occlusion culling is available
70
+ * @returns {string|null} - Reason for culling or null if visible
71
+ */
72
+ _shouldCullMesh(mesh, camera, canCull) {
73
+ // Transparent mesh culling is disabled - same issues as legacy mesh culling
74
+ // where bounding spheres may be in local space or represent root nodes
75
+ return null
76
+ }
77
+
78
+ async _init() {
79
+ const { device } = this.engine
80
+
81
+ // Create uniform buffer (same layout as lighting pass + material params)
82
+ this.uniformBuffer = device.createBuffer({
83
+ size: 512, // Plenty of space for uniforms
84
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
85
+ label: 'Transparent Uniforms'
86
+ })
87
+
88
+ // Create placeholder resources for missing bindings
89
+ await this._createPlaceholders()
90
+ }
91
+
92
+ /**
93
+ * Set the GBuffer for depth testing
94
+ */
95
+ setGBuffer(gbuffer) {
96
+ this.gbuffer = gbuffer
97
+ }
98
+
99
+ /**
100
+ * Set the HDR output texture to render onto
101
+ */
102
+ setOutputTexture(texture) {
103
+ this.outputTexture = texture
104
+ }
105
+
106
+ /**
107
+ * Set shadow pass reference
108
+ */
109
+ setShadowPass(shadowPass) {
110
+ this.shadowPass = shadowPass
111
+ }
112
+
113
+ /**
114
+ * Set environment map
115
+ */
116
+ setEnvironmentMap(envMap, encoding = 'equirect') {
117
+ this.environmentMap = envMap
118
+ this.environmentEncoding = encoding
119
+ }
120
+
121
+ /**
122
+ * Set noise texture for effects
123
+ */
124
+ setNoise(noise, size = 64, animated = true) {
125
+ this.noiseTexture = noise
126
+ this.noiseSize = size
127
+ this.noiseAnimated = animated
128
+ }
129
+
130
+ /**
131
+ * Set light buffers for tiled lighting
132
+ */
133
+ setLightBuffers(tileLightBuffer, lightBuffer) {
134
+ this.tileLightBuffer = tileLightBuffer
135
+ this.lightBuffer = lightBuffer
136
+ }
137
+
138
+ /**
139
+ * Create or get pipeline for a mesh
140
+ */
141
+ async _getOrCreatePipeline(mesh) {
142
+ const { device } = this.engine
143
+ const key = `transparent_${mesh.material.uid}`
144
+
145
+ if (this.pipelineCache.has(key)) {
146
+ return this.pipelineCache.get(key)
147
+ }
148
+
149
+ // Build the shader
150
+ const shaderCode = this._buildShaderCode()
151
+
152
+ const shaderModule = device.createShaderModule({
153
+ label: 'Transparent Shader',
154
+ code: shaderCode
155
+ })
156
+
157
+ // Bind group layout
158
+ const bindGroupLayout = device.createBindGroupLayout({
159
+ label: 'Transparent BindGroup Layout',
160
+ entries: [
161
+ // Uniforms
162
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
163
+ // Albedo texture
164
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
165
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
166
+ // Normal texture
167
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
168
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
169
+ // ARM texture
170
+ { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
171
+ { binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
172
+ // Environment map
173
+ { binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
174
+ { binding: 8, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
175
+ // Shadow map array (cascades)
176
+ { binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth', viewDimension: '2d-array' } },
177
+ { binding: 10, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
178
+ // Cascade matrices
179
+ { binding: 11, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
180
+ // Noise texture
181
+ { binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
182
+ ]
183
+ })
184
+
185
+ const pipelineLayout = device.createPipelineLayout({
186
+ bindGroupLayouts: [bindGroupLayout]
187
+ })
188
+
189
+ // Vertex buffer layout (same as geometry pass)
190
+ const vertexBufferLayout = {
191
+ arrayStride: 80,
192
+ attributes: [
193
+ { format: "float32x3", offset: 0, shaderLocation: 0 }, // position
194
+ { format: "float32x2", offset: 12, shaderLocation: 1 }, // uv
195
+ { format: "float32x3", offset: 20, shaderLocation: 2 }, // normal
196
+ { format: "float32x4", offset: 32, shaderLocation: 3 }, // color
197
+ { format: "float32x4", offset: 48, shaderLocation: 4 }, // weights
198
+ { format: "uint32x4", offset: 64, shaderLocation: 5 }, // joints
199
+ ],
200
+ stepMode: 'vertex'
201
+ }
202
+
203
+ const instanceBufferLayout = {
204
+ arrayStride: 112, // 28 floats: matrix(16) + posRadius(4) + uvTransform(4) + color(4)
205
+ stepMode: 'instance',
206
+ attributes: [
207
+ { format: "float32x4", offset: 0, shaderLocation: 6 },
208
+ { format: "float32x4", offset: 16, shaderLocation: 7 },
209
+ { format: "float32x4", offset: 32, shaderLocation: 8 },
210
+ { format: "float32x4", offset: 48, shaderLocation: 9 },
211
+ { format: "float32x4", offset: 64, shaderLocation: 10 },
212
+ { format: "float32x4", offset: 80, shaderLocation: 11 }, // uvTransform
213
+ { format: "float32x4", offset: 96, shaderLocation: 12 }, // color
214
+ ]
215
+ }
216
+
217
+ const pipeline = await device.createRenderPipelineAsync({
218
+ label: 'Transparent Pipeline',
219
+ layout: pipelineLayout,
220
+ vertex: {
221
+ module: shaderModule,
222
+ entryPoint: 'vertexMain',
223
+ buffers: [vertexBufferLayout, instanceBufferLayout]
224
+ },
225
+ fragment: {
226
+ module: shaderModule,
227
+ entryPoint: 'fragmentMain',
228
+ targets: [{
229
+ format: 'rgba16float',
230
+ blend: {
231
+ color: {
232
+ srcFactor: 'src-alpha',
233
+ dstFactor: 'one-minus-src-alpha',
234
+ operation: 'add'
235
+ },
236
+ alpha: {
237
+ srcFactor: 'one',
238
+ dstFactor: 'one-minus-src-alpha',
239
+ operation: 'add'
240
+ }
241
+ }
242
+ }]
243
+ },
244
+ depthStencil: {
245
+ format: 'depth32float',
246
+ depthWriteEnabled: false, // Don't write depth - fog needs scene depth behind transparent objects
247
+ depthCompare: 'less',
248
+ },
249
+ primitive: {
250
+ topology: 'triangle-list',
251
+ cullMode: 'none', // No culling for transparent objects
252
+ }
253
+ })
254
+
255
+ this.pipelineCache.set(key, { pipeline, bindGroupLayout })
256
+ return { pipeline, bindGroupLayout }
257
+ }
258
+
259
+ /**
260
+ * Build the forward transparent shader
261
+ */
262
+ _buildShaderCode() {
263
+ return `
264
+ ${lightingCommonWGSL}
265
+
266
+ struct TransparentUniforms {
267
+ viewMatrix: mat4x4f,
268
+ projectionMatrix: mat4x4f,
269
+ cameraPosition: vec3f,
270
+ opacity: f32,
271
+ lightDir: vec3f,
272
+ lightIntensity: f32,
273
+ lightColor: vec3f,
274
+ ambientIntensity: f32,
275
+ ambientColor: vec3f,
276
+ envMipCount: f32,
277
+ envDiffuse: f32,
278
+ envSpecular: f32,
279
+ exposure: f32,
280
+ envEncoding: f32, // 0=equirect, 1=octahedral
281
+ shadowBias: f32,
282
+ shadowNormalBias: f32,
283
+ shadowStrength: f32,
284
+ cascadeSize0: f32,
285
+ cascadeSize1: f32,
286
+ cascadeSize2: f32,
287
+ noiseSize: f32,
288
+ noiseOffsetX: f32,
289
+ noiseOffsetY: f32,
290
+ distanceFadeStart: f32, // Distance where fade begins
291
+ distanceFadeEnd: f32, // Distance where fade completes (0 = disabled)
292
+ fogEnabled: f32, // Whether fog is enabled
293
+ fogPad1: f32, // Padding for vec3 alignment
294
+ fogPad2: f32,
295
+ fogPad3: f32,
296
+ fogColor: vec3f, // Fog color
297
+ fogBrightResist: f32, // HDR brightness resistance
298
+ fogDistances: vec3f, // Distance thresholds [near, mid, far]
299
+ fogPad4: f32,
300
+ fogAlphas: vec3f, // Alpha values at distances
301
+ fogPad5: f32,
302
+ fogHeightFade: vec2f, // [bottomY, topY]
303
+ cameraNear: f32,
304
+ cameraFar: f32,
305
+ }
306
+
307
+ struct VertexInput {
308
+ @location(0) position: vec3f,
309
+ @location(1) uv: vec2f,
310
+ @location(2) normal: vec3f,
311
+ @location(3) color: vec4f,
312
+ @location(4) weights: vec4f,
313
+ @location(5) joints: vec4u,
314
+ @location(6) model0: vec4f,
315
+ @location(7) model1: vec4f,
316
+ @location(8) model2: vec4f,
317
+ @location(9) model3: vec4f,
318
+ @location(10) posRadius: vec4f,
319
+ }
320
+
321
+ struct VertexOutput {
322
+ @builtin(position) position: vec4f,
323
+ @location(0) worldPos: vec3f,
324
+ @location(1) uv: vec2f,
325
+ @location(2) normal: vec3f,
326
+ }
327
+
328
+ @group(0) @binding(0) var<uniform> uniforms: TransparentUniforms;
329
+ @group(0) @binding(1) var albedoTexture: texture_2d<f32>;
330
+ @group(0) @binding(2) var albedoSampler: sampler;
331
+ @group(0) @binding(3) var normalTexture: texture_2d<f32>;
332
+ @group(0) @binding(4) var normalSampler: sampler;
333
+ @group(0) @binding(5) var armTexture: texture_2d<f32>;
334
+ @group(0) @binding(6) var armSampler: sampler;
335
+ @group(0) @binding(7) var envTexture: texture_2d<f32>;
336
+ @group(0) @binding(8) var envSampler: sampler;
337
+ @group(0) @binding(9) var shadowMapArray: texture_depth_2d_array;
338
+ @group(0) @binding(10) var shadowSampler: sampler_comparison;
339
+ @group(0) @binding(11) var<storage, read> cascadeMatrices: CascadeMatrices;
340
+ @group(0) @binding(12) var noiseTexture: texture_2d<f32>;
341
+
342
+ fn getEnvUV(dir: vec3f) -> vec2f {
343
+ if (uniforms.envEncoding > 0.5) {
344
+ return octEncode(dir);
345
+ }
346
+ return SphToUV(dir);
347
+ }
348
+
349
+ fn sampleNoise(screenPos: vec2f) -> f32 {
350
+ let noiseSize = i32(uniforms.noiseSize);
351
+ let noiseOffsetX = i32(uniforms.noiseOffsetX * f32(noiseSize));
352
+ let noiseOffsetY = i32(uniforms.noiseOffsetY * f32(noiseSize));
353
+ let texCoord = vec2i(
354
+ (i32(screenPos.x) + noiseOffsetX) % noiseSize,
355
+ (i32(screenPos.y) + noiseOffsetY) % noiseSize
356
+ );
357
+ return textureLoad(noiseTexture, texCoord, 0).r;
358
+ }
359
+
360
+ fn getNoiseJitter(screenPos: vec2f) -> vec2f {
361
+ let noiseSize = i32(uniforms.noiseSize);
362
+ let noiseOffsetX = i32(uniforms.noiseOffsetX * f32(noiseSize));
363
+ let noiseOffsetY = i32(uniforms.noiseOffsetY * f32(noiseSize));
364
+ let texCoord1 = vec2i(
365
+ (i32(screenPos.x) + noiseOffsetX) % noiseSize,
366
+ (i32(screenPos.y) + noiseOffsetY) % noiseSize
367
+ );
368
+ let texCoord2 = vec2i(
369
+ (i32(screenPos.x) + 37 + noiseOffsetX) % noiseSize,
370
+ (i32(screenPos.y) + 17 + noiseOffsetY) % noiseSize
371
+ );
372
+ let n1 = textureLoad(noiseTexture, texCoord1, 0).r;
373
+ let n2 = textureLoad(noiseTexture, texCoord2, 0).r;
374
+ return vec2f(n1, n2) * 2.0 - 1.0;
375
+ }
376
+
377
+ fn getIBLSample(reflection: vec3f, lod: f32) -> vec3f {
378
+ let envRGBE = textureSampleLevel(envTexture, envSampler, getEnvUV(reflection), lod);
379
+ let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);
380
+ return envColor;
381
+ }
382
+
383
+ // Sample cascade shadow
384
+ fn sampleCascadeShadow(worldPos: vec3f, normal: vec3f, cascadeIndex: i32, screenPos: vec2f) -> f32 {
385
+ let bias = uniforms.shadowBias;
386
+ let normalBias = uniforms.shadowNormalBias;
387
+ let biasedPos = worldPos + normal * normalBias;
388
+ let lightMatrix = cascadeMatrices.matrices[cascadeIndex];
389
+ let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
390
+ let projCoords = lightSpacePos.xyz / lightSpacePos.w;
391
+ let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
392
+ let currentDepth = projCoords.z - bias;
393
+
394
+ if (shadowUV.x < 0.0 || shadowUV.x > 1.0 || shadowUV.y < 0.0 || shadowUV.y > 1.0) {
395
+ return 1.0;
396
+ }
397
+
398
+ // Simple shadow sampling with jitter
399
+ let jitter = getNoiseJitter(screenPos);
400
+ let texelSize = 1.0 / 2048.0;
401
+ var shadow = 0.0;
402
+ for (var i = 0; i < 4; i++) {
403
+ let offset = vogelDiskSample(i, 4, jitter.x * PI) * texelSize * 2.0;
404
+ shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, shadowUV + offset, cascadeIndex, currentDepth);
405
+ }
406
+ return shadow / 4.0;
407
+ }
408
+
409
+ fn calculateShadow(worldPos: vec3f, normal: vec3f, screenPos: vec2f) -> f32 {
410
+ let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);
411
+ let posXZ = vec2f(worldPos.x, worldPos.z);
412
+ let offsetXZ = posXZ - camXZ;
413
+
414
+ let dist0 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSize0);
415
+ let dist1 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSize1);
416
+ let dist2 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSize2);
417
+
418
+ var shadow = 1.0;
419
+ if (dist0 < 1.0) {
420
+ shadow = sampleCascadeShadow(worldPos, normal, 0, screenPos);
421
+ } else if (dist1 < 1.0) {
422
+ shadow = sampleCascadeShadow(worldPos, normal, 1, screenPos);
423
+ } else if (dist2 < 1.0) {
424
+ shadow = sampleCascadeShadow(worldPos, normal, 2, screenPos);
425
+ }
426
+
427
+ return mix(1.0 - uniforms.shadowStrength, 1.0, shadow);
428
+ }
429
+
430
+ @vertex
431
+ fn vertexMain(input: VertexInput) -> VertexOutput {
432
+ var output: VertexOutput;
433
+
434
+ let modelMatrix = mat4x4f(
435
+ input.model0,
436
+ input.model1,
437
+ input.model2,
438
+ input.model3
439
+ );
440
+
441
+ let worldPos = (modelMatrix * vec4f(input.position, 1.0)).xyz;
442
+ let viewPos = uniforms.viewMatrix * vec4f(worldPos, 1.0);
443
+ output.position = uniforms.projectionMatrix * viewPos;
444
+ output.worldPos = worldPos;
445
+ output.uv = input.uv;
446
+
447
+ let normalMatrix = mat3x3f(
448
+ modelMatrix[0].xyz,
449
+ modelMatrix[1].xyz,
450
+ modelMatrix[2].xyz
451
+ );
452
+ output.normal = normalize(normalMatrix * input.normal);
453
+
454
+ return output;
455
+ }
456
+
457
+ @fragment
458
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
459
+ // Sample textures
460
+ let albedoSample = textureSample(albedoTexture, albedoSampler, input.uv);
461
+ let armSample = textureSample(armTexture, armSampler, input.uv);
462
+
463
+ // Calculate alpha (albedo alpha * material opacity)
464
+ var alpha = albedoSample.a * uniforms.opacity;
465
+
466
+ // Distance fade: modulate alpha to prevent popping at culling distance
467
+ if (uniforms.distanceFadeEnd > 0.0) {
468
+ let distToCamera = length(input.worldPos - uniforms.cameraPosition);
469
+ if (distToCamera >= uniforms.distanceFadeEnd) {
470
+ discard; // Beyond fade end - fully invisible
471
+ }
472
+ if (distToCamera > uniforms.distanceFadeStart) {
473
+ // Calculate fade factor: 1.0 at fadeStart, 0.0 at fadeEnd
474
+ let fadeRange = uniforms.distanceFadeEnd - uniforms.distanceFadeStart;
475
+ let fadeFactor = 1.0 - (distToCamera - uniforms.distanceFadeStart) / fadeRange;
476
+ alpha *= fadeFactor;
477
+ }
478
+ }
479
+
480
+ // Discard very transparent fragments
481
+ if (alpha < 0.01) {
482
+ discard;
483
+ }
484
+
485
+ let baseColor = albedoSample.rgb;
486
+ let ao = armSample.r;
487
+ let roughness = max(armSample.g, 0.04);
488
+ let metallic = armSample.b;
489
+ let alphaRoughness = roughness * roughness;
490
+
491
+ // Normal mapping
492
+ let normalSample = textureSample(normalTexture, normalSampler, input.uv).rgb;
493
+ let tangentNormal = normalize(normalSample * 2.0 - 1.0);
494
+ let N = normalize(input.normal);
495
+ let refVec = select(vec3f(0.0, 1.0, 0.0), vec3f(1.0, 0.0, 0.0), abs(N.y) > 0.9);
496
+ let T = normalize(cross(N, refVec));
497
+ let B = cross(N, T);
498
+ let TBN = mat3x3f(T, B, N);
499
+ let n = normalize(TBN * tangentNormal);
500
+
501
+ // View direction
502
+ let v = normalize(uniforms.cameraPosition - input.worldPos);
503
+ let NdotV = clampedDot(n, v);
504
+
505
+ // PBR setup
506
+ var f0 = vec3f(0.04);
507
+ f0 = mix(f0, baseColor, metallic);
508
+ let f90 = vec3f(1.0);
509
+ let c_diff = mix(baseColor, vec3f(0.0), metallic);
510
+ let specularWeight = 1.0;
511
+
512
+ var diffuse = vec3f(0.0);
513
+ var specular = vec3f(0.0);
514
+
515
+ // Ambient lighting
516
+ diffuse += ao * uniforms.ambientColor * uniforms.ambientIntensity * BRDF_lambertian(f0, f90, c_diff, specularWeight, 1.0);
517
+
518
+ // Environment diffuse
519
+ let envDiffuseSample = getIBLSample(n, uniforms.envMipCount - 2.0);
520
+ diffuse += ao * uniforms.envDiffuse * envDiffuseSample * BRDF_lambertian(f0, f90, c_diff, specularWeight, 1.0);
521
+
522
+ // Environment specular
523
+ let reflection = normalize(reflect(-v, n));
524
+ let lod = roughness * (uniforms.envMipCount - 1.0);
525
+ let Fr = max(vec3f(1.0 - roughness), f0) - f0;
526
+ let k_S = f0 + Fr * pow(1.0 - NdotV, 5.0);
527
+ let envSpecSample = getIBLSample(reflection, lod);
528
+ specular += ao * uniforms.envSpecular * envSpecSample * k_S;
529
+
530
+ // Directional light
531
+ let l = normalize(uniforms.lightDir);
532
+ let h = normalize(l + v);
533
+ let NdotL = clampedDot(n, l);
534
+ let NdotH = clampedDot(n, h);
535
+ let VdotH = clampedDot(v, h);
536
+
537
+ let shadow = calculateShadow(input.worldPos, n, input.position.xy);
538
+ let lightContrib = uniforms.lightIntensity * uniforms.lightColor * NdotL * shadow;
539
+
540
+ diffuse += lightContrib * BRDF_lambertian(f0, f90, c_diff, specularWeight, VdotH);
541
+ specular += lightContrib * BRDF_specularGGX(f0, f90, alphaRoughness, specularWeight, VdotH, NdotL, NdotV, NdotH);
542
+
543
+ // Final color
544
+ var color = (diffuse + specular) * uniforms.exposure;
545
+
546
+ // Fog is applied as post-process after all transparent/particle rendering
547
+
548
+ // For glass-like materials, reduce color intensity based on transparency
549
+ // More transparent = more of background shows through
550
+ color *= alpha;
551
+
552
+ return vec4f(color, alpha);
553
+ }
554
+ `
555
+ }
556
+
557
+ /**
558
+ * Execute the transparent pass
559
+ */
560
+ async _execute(context) {
561
+ const { device, canvas, stats } = this.engine
562
+ const { camera, meshes, mainLight } = context
563
+
564
+ // Initialize transparent stats
565
+ stats.transparentDrawCalls = 0
566
+ stats.transparentTriangles = 0
567
+
568
+ if (!this.outputTexture || !this.gbuffer) {
569
+ return
570
+ }
571
+
572
+ // Update frustum for transparent mesh culling (only if camera has required properties)
573
+ const canCull = camera.view && camera.proj && camera.position && camera.direction
574
+ if (canCull) {
575
+ const fovRadians = (camera.fov || 60) * (Math.PI / 180)
576
+ this.frustum.update(
577
+ camera.view,
578
+ camera.proj,
579
+ camera.position,
580
+ camera.direction,
581
+ fovRadians,
582
+ camera.aspect || (canvas.width / canvas.height),
583
+ camera.near || 0.05,
584
+ camera.far || 1000,
585
+ canvas.width,
586
+ canvas.height
587
+ )
588
+ }
589
+
590
+ // Reset culling stats
591
+ this.cullingStats.total = 0
592
+ this.cullingStats.rendered = 0
593
+ this.cullingStats.culledByFrustum = 0
594
+ this.cullingStats.culledByDistance = 0
595
+ this.cullingStats.culledByOcclusion = 0
596
+
597
+ // Collect transparent meshes with culling
598
+ const transparentMeshes = []
599
+ for (const name in meshes) {
600
+ const mesh = meshes[name]
601
+ if (mesh.material?.transparent && mesh.geometry?.instanceCount > 0) {
602
+ this.cullingStats.total++
603
+
604
+ // Apply culling (frustum, distance, occlusion)
605
+ const cullReason = this._shouldCullMesh(mesh, camera, canCull)
606
+ if (cullReason) {
607
+ if (cullReason === 'frustum') this.cullingStats.culledByFrustum++
608
+ else if (cullReason === 'distance') this.cullingStats.culledByDistance++
609
+ else if (cullReason === 'occlusion') this.cullingStats.culledByOcclusion++
610
+ continue
611
+ }
612
+
613
+ this.cullingStats.rendered++
614
+ transparentMeshes.push({ name, mesh })
615
+ }
616
+ }
617
+
618
+ if (transparentMeshes.length === 0) {
619
+ return
620
+ }
621
+
622
+ // Sort back-to-front by distance to camera
623
+ transparentMeshes.sort((a, b) => {
624
+ // Get center position from first instance (simplified)
625
+ const aPos = a.mesh.geometry.instanceData ?
626
+ [a.mesh.geometry.instanceData[12], a.mesh.geometry.instanceData[13], a.mesh.geometry.instanceData[14]] :
627
+ [0, 0, 0]
628
+ const bPos = b.mesh.geometry.instanceData ?
629
+ [b.mesh.geometry.instanceData[12], b.mesh.geometry.instanceData[13], b.mesh.geometry.instanceData[14]] :
630
+ [0, 0, 0]
631
+
632
+ const aDist = (aPos[0] - camera.position[0]) ** 2 +
633
+ (aPos[1] - camera.position[1]) ** 2 +
634
+ (aPos[2] - camera.position[2]) ** 2
635
+ const bDist = (bPos[0] - camera.position[0]) ** 2 +
636
+ (bPos[1] - camera.position[1]) ** 2 +
637
+ (bPos[2] - camera.position[2]) ** 2
638
+
639
+ return bDist - aDist // Back to front
640
+ })
641
+
642
+ // Render each transparent mesh
643
+ const commandEncoder = device.createCommandEncoder({ label: 'Transparent Pass' })
644
+
645
+ const passEncoder = commandEncoder.beginRenderPass({
646
+ colorAttachments: [{
647
+ view: this.outputTexture.view,
648
+ loadOp: 'load', // Preserve existing content
649
+ storeOp: 'store',
650
+ }],
651
+ depthStencilAttachment: {
652
+ view: this.gbuffer.depth.view,
653
+ depthLoadOp: 'load',
654
+ depthStoreOp: 'store',
655
+ }
656
+ })
657
+
658
+ for (const { mesh } of transparentMeshes) {
659
+ const { pipeline, bindGroupLayout } = await this._getOrCreatePipeline(mesh)
660
+
661
+ // Update uniforms
662
+ const uniformData = new Float32Array(96)
663
+ uniformData.set(camera.view, 0)
664
+ uniformData.set(camera.proj, 16)
665
+ uniformData.set(camera.position, 32)
666
+ uniformData[35] = mesh.material.opacity ?? 1.0
667
+
668
+ // Light direction
669
+ const lightDir = mainLight?.direction || [-1, 1, -0.5]
670
+ uniformData.set(lightDir, 36)
671
+ uniformData[39] = mainLight?.color?.[3] ?? 1.0 // intensity
672
+
673
+ // Light color
674
+ const lightColor = mainLight?.color || [1, 1, 1]
675
+ uniformData.set(lightColor, 40)
676
+ uniformData[43] = this.settings?.environment?.ambientIntensity ?? 0.3
677
+
678
+ // Ambient color
679
+ const ambientColor = this.settings?.environment?.ambientColor || [0.5, 0.5, 0.6]
680
+ uniformData.set(ambientColor, 44)
681
+ uniformData[47] = this.settings?.environment?.envMipCount ?? 8 // envMipCount
682
+
683
+ // Environment params
684
+ uniformData[48] = this.settings?.environment?.diffuseLevel ?? 0.5
685
+ uniformData[49] = this.settings?.environment?.specularLevel ?? 1.0
686
+ uniformData[50] = this.settings?.environment?.exposure ?? 1.0
687
+ uniformData[51] = this.environmentEncoding === 'octahedral' ? 1.0 : 0.0
688
+
689
+ // Shadow params
690
+ uniformData[52] = this.settings?.shadow?.bias ?? 0.001
691
+ uniformData[53] = this.settings?.shadow?.normalBias ?? 0.02
692
+ uniformData[54] = this.settings?.shadow?.strength ?? 0.7
693
+
694
+ // Cascade sizes
695
+ const cascadeSizes = this.shadowPass?.getCascadeSizes() || [20, 60, 300]
696
+ uniformData[55] = cascadeSizes[0]
697
+ uniformData[56] = cascadeSizes[1]
698
+ uniformData[57] = cascadeSizes[2]
699
+
700
+ // Noise params
701
+ uniformData[58] = this.noiseSize
702
+ uniformData[59] = this.noiseAnimated ? Math.random() : 0
703
+ uniformData[60] = this.noiseAnimated ? Math.random() : 0
704
+
705
+ // Distance fade params
706
+ uniformData[61] = this.distanceFadeStart
707
+ uniformData[62] = this.distanceFadeEnd
708
+
709
+ // Fog params
710
+ const fogSettings = this.settings?.environment?.fog
711
+ uniformData[63] = fogSettings?.enabled ? 1.0 : 0.0
712
+ // Padding at 64-66 for vec3 alignment (indices 64, 65, 66 are pad)
713
+ // fogColor at indices 68-70 (aligned to 16 bytes at byte 272)
714
+ const fogColor = fogSettings?.color ?? [0.8, 0.85, 0.9]
715
+ uniformData[68] = fogColor[0]
716
+ uniformData[69] = fogColor[1]
717
+ uniformData[70] = fogColor[2]
718
+ uniformData[71] = fogSettings?.brightResist ?? 0.8
719
+ // fogDistances at indices 72-74
720
+ const fogDistances = fogSettings?.distances ?? [0, 50, 200]
721
+ uniformData[72] = fogDistances[0]
722
+ uniformData[73] = fogDistances[1]
723
+ uniformData[74] = fogDistances[2]
724
+ // fogAlphas at indices 76-78
725
+ const fogAlphas = fogSettings?.alpha ?? [0, 0.3, 0.8]
726
+ uniformData[76] = fogAlphas[0]
727
+ uniformData[77] = fogAlphas[1]
728
+ uniformData[78] = fogAlphas[2]
729
+ // fogHeightFade at indices 80-81
730
+ const fogHeightFade = fogSettings?.heightFade ?? [-10, 100]
731
+ uniformData[80] = fogHeightFade[0]
732
+ uniformData[81] = fogHeightFade[1]
733
+ // cameraNear/Far at indices 82-83
734
+ uniformData[82] = camera.near || 0.1
735
+ uniformData[83] = camera.far || 1000
736
+
737
+ device.queue.writeBuffer(this.uniformBuffer, 0, uniformData)
738
+
739
+ // Create bind group
740
+ const bindGroup = device.createBindGroup({
741
+ layout: bindGroupLayout,
742
+ entries: [
743
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
744
+ { binding: 1, resource: mesh.material.textures[0]?.view || this._placeholderTexture.view },
745
+ { binding: 2, resource: mesh.material.textures[0]?.sampler || this._placeholderSampler },
746
+ { binding: 3, resource: mesh.material.textures[1]?.view || this._placeholderNormal.view },
747
+ { binding: 4, resource: mesh.material.textures[1]?.sampler || this._placeholderSampler },
748
+ { binding: 5, resource: mesh.material.textures[3]?.view || this._placeholderTexture.view },
749
+ { binding: 6, resource: mesh.material.textures[3]?.sampler || this._placeholderSampler },
750
+ { binding: 7, resource: this.environmentMap?.view || this._placeholderTexture.view },
751
+ { binding: 8, resource: this.environmentMap?.sampler || this._placeholderSampler },
752
+ { binding: 9, resource: this.shadowPass?.getShadowMapView() || this._placeholderDepth.view },
753
+ { binding: 10, resource: this.shadowPass?.getShadowSampler() || this._placeholderComparisonSampler },
754
+ { binding: 11, resource: { buffer: this.shadowPass?.getCascadeMatricesBuffer() || this._placeholderBuffer } },
755
+ { binding: 12, resource: this.noiseTexture?.view || this._placeholderTexture.view },
756
+ ]
757
+ })
758
+
759
+ // Update geometry
760
+ mesh.geometry.update()
761
+
762
+ // Render
763
+ passEncoder.setPipeline(pipeline)
764
+ passEncoder.setBindGroup(0, bindGroup)
765
+ passEncoder.setVertexBuffer(0, mesh.geometry.vertexBuffer)
766
+ passEncoder.setVertexBuffer(1, mesh.geometry.instanceBuffer)
767
+ passEncoder.setIndexBuffer(mesh.geometry.indexBuffer, 'uint32')
768
+ passEncoder.drawIndexed(mesh.geometry.indexArray.length, mesh.geometry.instanceCount)
769
+
770
+ // Track stats
771
+ stats.transparentDrawCalls++
772
+ stats.transparentTriangles += (mesh.geometry.indexArray.length / 3) * mesh.geometry.instanceCount
773
+ }
774
+
775
+ passEncoder.end()
776
+ device.queue.submit([commandEncoder.finish()])
777
+ }
778
+
779
+ /**
780
+ * Create placeholder resources for missing bindings
781
+ */
782
+ async _createPlaceholders() {
783
+ const { device } = this.engine
784
+
785
+ // 1x1 white texture
786
+ this._placeholderTexture = await Texture.fromRGBA(this.engine, 1, 1, 1, 1)
787
+
788
+ // 1x1 normal texture (pointing up)
789
+ this._placeholderNormal = await Texture.fromColor(this.engine, "#8080FF")
790
+
791
+ // Sampler
792
+ this._placeholderSampler = device.createSampler({
793
+ magFilter: 'linear',
794
+ minFilter: 'linear',
795
+ })
796
+
797
+ // Comparison sampler
798
+ this._placeholderComparisonSampler = device.createSampler({
799
+ compare: 'less',
800
+ magFilter: 'linear',
801
+ minFilter: 'linear',
802
+ })
803
+
804
+ // 1x1 depth texture
805
+ this._placeholderDepthTexture = device.createTexture({
806
+ size: [1, 1, 1],
807
+ format: 'depth32float',
808
+ usage: GPUTextureUsage.TEXTURE_BINDING,
809
+ dimension: '2d',
810
+ })
811
+ this._placeholderDepth = {
812
+ view: this._placeholderDepthTexture.createView({ dimension: '2d-array', arrayLayerCount: 1 })
813
+ }
814
+
815
+ // Placeholder buffer
816
+ this._placeholderBuffer = device.createBuffer({
817
+ size: 256,
818
+ usage: GPUBufferUsage.STORAGE,
819
+ })
820
+ }
821
+
822
+ async _resize(width, height) {
823
+ // Nothing to resize
824
+ }
825
+
826
+ _destroy() {
827
+ this.pipelineCache.clear()
828
+ }
829
+ }
830
+
831
+ export { TransparentPass }