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,593 @@
|
|
|
1
|
+
import { BasePass } from "./BasePass.js"
|
|
2
|
+
import { Texture } from "../../Texture.js"
|
|
3
|
+
import { GBufferPass } from "./GBufferPass.js"
|
|
4
|
+
import { LightingPass } from "./LightingPass.js"
|
|
5
|
+
import { mat4, vec3 } from "../../math.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AmbientCapturePass - Captures 6 directional ambient light samples for sky-aware GI
|
|
9
|
+
*
|
|
10
|
+
* Renders simplified views in 6 directions (up, down, left, right, front, back)
|
|
11
|
+
* to capture sky visibility and distant lighting. This provides ambient lighting
|
|
12
|
+
* that responds to sky visibility (blue tint from sky when outdoors, darker when
|
|
13
|
+
* under a roof).
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - 64x64 resolution per direction (configurable)
|
|
17
|
+
* - Staggered updates: 2 faces per frame (full cycle in 3 frames)
|
|
18
|
+
* - Direct lights + emissive + skybox only (no IBL on geometry)
|
|
19
|
+
* - Aggressive distance culling (25m default)
|
|
20
|
+
* - Output: 6 average colors applied in RenderPost based on surface normal
|
|
21
|
+
*
|
|
22
|
+
* Settings (from engine.settings.ambientCapture):
|
|
23
|
+
* - enabled: boolean - Enable/disable ambient capture
|
|
24
|
+
* - maxDistance: number - Distance culling (default 25m)
|
|
25
|
+
* - intensity: number - Output intensity multiplier
|
|
26
|
+
* - resolution: number - Capture resolution (default 64)
|
|
27
|
+
*/
|
|
28
|
+
class AmbientCapturePass extends BasePass {
|
|
29
|
+
constructor(engine = null) {
|
|
30
|
+
super('AmbientCapture', engine)
|
|
31
|
+
|
|
32
|
+
// Capture settings
|
|
33
|
+
this.captureSize = 64
|
|
34
|
+
this.maxDistance = 25
|
|
35
|
+
this.currentFaceIndex = 0 // cycles 0-5, update 2 per frame
|
|
36
|
+
|
|
37
|
+
// 6 face directions (calculated each frame from camera)
|
|
38
|
+
// Order: up, down, left, right, front, back
|
|
39
|
+
this.directions = [
|
|
40
|
+
[0, 1, 0],
|
|
41
|
+
[0, -1, 0],
|
|
42
|
+
[-1, 0, 0],
|
|
43
|
+
[1, 0, 0],
|
|
44
|
+
[0, 0, 1],
|
|
45
|
+
[0, 0, -1]
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
// Output: 6 average colors (vec4 each)
|
|
49
|
+
this.faceColors = new Float32Array(6 * 4)
|
|
50
|
+
this.faceColorsBuffer = null // GPU storage buffer
|
|
51
|
+
|
|
52
|
+
// Internal passes
|
|
53
|
+
this.gbufferPass = null
|
|
54
|
+
this.lightingPass = null
|
|
55
|
+
|
|
56
|
+
// Render targets (reused for each face)
|
|
57
|
+
this.captureTexture = null
|
|
58
|
+
this.dummyAO = null
|
|
59
|
+
|
|
60
|
+
// Compute pipeline for reduction
|
|
61
|
+
this.reducePipeline = null
|
|
62
|
+
this.reduceBindGroupLayout = null
|
|
63
|
+
this.reduceBindGroups = [] // One per face
|
|
64
|
+
|
|
65
|
+
// Camera matrices (reused)
|
|
66
|
+
this.faceView = mat4.create()
|
|
67
|
+
this.faceProj = mat4.create()
|
|
68
|
+
|
|
69
|
+
// Textures pending destruction
|
|
70
|
+
this._pendingDestroyRing = [[], [], []]
|
|
71
|
+
this._pendingDestroyIndex = 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async _init() {
|
|
75
|
+
const { device } = this.engine
|
|
76
|
+
|
|
77
|
+
// Get settings
|
|
78
|
+
this.captureSize = this.settings?.ambientCapture?.resolution ?? 64
|
|
79
|
+
this.maxDistance = this.settings?.ambientCapture?.maxDistance ?? 25
|
|
80
|
+
|
|
81
|
+
await this._createResources()
|
|
82
|
+
await this._createComputePipeline()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async _createResources() {
|
|
86
|
+
const { device } = this.engine
|
|
87
|
+
|
|
88
|
+
// Create capture render target
|
|
89
|
+
this.captureTexture = await Texture.renderTarget(this.engine, 'rgba16float', this.captureSize, this.captureSize)
|
|
90
|
+
this.captureTexture.label = 'ambientCaptureRT'
|
|
91
|
+
|
|
92
|
+
// Create internal GBuffer pass
|
|
93
|
+
this.gbufferPass = new GBufferPass(this.engine)
|
|
94
|
+
await this.gbufferPass.initialize()
|
|
95
|
+
await this.gbufferPass.resize(this.captureSize, this.captureSize)
|
|
96
|
+
|
|
97
|
+
// Create internal Lighting pass
|
|
98
|
+
this.lightingPass = new LightingPass(this.engine)
|
|
99
|
+
await this.lightingPass.initialize()
|
|
100
|
+
await this.lightingPass.resize(this.captureSize, this.captureSize)
|
|
101
|
+
this.lightingPass.exposureOverride = 1.0
|
|
102
|
+
|
|
103
|
+
// Disable IBL on geometry for ambient capture (skybox still renders as background)
|
|
104
|
+
this.lightingPass.ambientCaptureMode = true
|
|
105
|
+
|
|
106
|
+
// Create dummy AO texture (white = no occlusion)
|
|
107
|
+
const dummyAOTexture = device.createTexture({
|
|
108
|
+
label: 'ambientCaptureAO',
|
|
109
|
+
size: [this.captureSize, this.captureSize],
|
|
110
|
+
format: 'r8unorm',
|
|
111
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST
|
|
112
|
+
})
|
|
113
|
+
const whiteData = new Uint8Array(this.captureSize * this.captureSize).fill(255)
|
|
114
|
+
device.queue.writeTexture(
|
|
115
|
+
{ texture: dummyAOTexture },
|
|
116
|
+
whiteData,
|
|
117
|
+
{ bytesPerRow: this.captureSize },
|
|
118
|
+
{ width: this.captureSize, height: this.captureSize }
|
|
119
|
+
)
|
|
120
|
+
this.dummyAO = {
|
|
121
|
+
texture: dummyAOTexture,
|
|
122
|
+
view: dummyAOTexture.createView()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Wire up passes
|
|
126
|
+
await this.lightingPass.setGBuffer(this.gbufferPass.getGBuffer())
|
|
127
|
+
this.lightingPass.setAOTexture(this.dummyAO)
|
|
128
|
+
|
|
129
|
+
// Create output buffer for 6 face colors
|
|
130
|
+
this.faceColorsBuffer = device.createBuffer({
|
|
131
|
+
label: 'ambientCaptureFaceColors',
|
|
132
|
+
size: 6 * 4 * 4, // 6 vec4f
|
|
133
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Initialize with distinct colors per direction for debugging
|
|
137
|
+
// up=cyan, down=brown, left=red, right=green, front=blue, back=yellow
|
|
138
|
+
const initColors = new Float32Array(6 * 4)
|
|
139
|
+
// Up: sky blue
|
|
140
|
+
initColors[0] = 0.4; initColors[1] = 0.6; initColors[2] = 1.0; initColors[3] = 1.0
|
|
141
|
+
// Down: brown/ground
|
|
142
|
+
initColors[4] = 0.3; initColors[5] = 0.2; initColors[6] = 0.1; initColors[7] = 1.0
|
|
143
|
+
// Left (-X): dark
|
|
144
|
+
initColors[8] = 0.2; initColors[9] = 0.2; initColors[10] = 0.2; initColors[11] = 1.0
|
|
145
|
+
// Right (+X): dark
|
|
146
|
+
initColors[12] = 0.2; initColors[13] = 0.2; initColors[14] = 0.2; initColors[15] = 1.0
|
|
147
|
+
// Front (+Z): dark
|
|
148
|
+
initColors[16] = 0.2; initColors[17] = 0.2; initColors[18] = 0.2; initColors[19] = 1.0
|
|
149
|
+
// Back (-Z): dark
|
|
150
|
+
initColors[20] = 0.2; initColors[21] = 0.2; initColors[22] = 0.2; initColors[23] = 1.0
|
|
151
|
+
device.queue.writeBuffer(this.faceColorsBuffer, 0, initColors)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async _createComputePipeline() {
|
|
155
|
+
const { device } = this.engine
|
|
156
|
+
|
|
157
|
+
// Bind group layout for reduction compute
|
|
158
|
+
this.reduceBindGroupLayout = device.createBindGroupLayout({
|
|
159
|
+
label: 'ambientReduceBindGroupLayout',
|
|
160
|
+
entries: [
|
|
161
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'float' } },
|
|
162
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
|
163
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }
|
|
164
|
+
]
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Create compute shader for reduction with temporal smoothing
|
|
168
|
+
const shaderCode = `
|
|
169
|
+
// Reduce a texture to a single average color using parallel reduction
|
|
170
|
+
// Includes temporal smoothing to blend with previous frame's value
|
|
171
|
+
// Workgroup: 8x8 threads, each samples region of the texture
|
|
172
|
+
// Uses shared memory for efficient reduction
|
|
173
|
+
|
|
174
|
+
struct ReduceParams {
|
|
175
|
+
faceIndex: u32,
|
|
176
|
+
textureSize: u32,
|
|
177
|
+
blendFactor: f32, // 0 = keep old, 1 = use new
|
|
178
|
+
emissiveBoost: f32,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
182
|
+
@group(0) @binding(1) var<storage, read_write> outputColors: array<vec4f>;
|
|
183
|
+
@group(0) @binding(2) var<uniform> params: ReduceParams;
|
|
184
|
+
|
|
185
|
+
var<workgroup> sharedColors: array<vec4f, 64>;
|
|
186
|
+
|
|
187
|
+
@compute @workgroup_size(8, 8, 1)
|
|
188
|
+
fn main(
|
|
189
|
+
@builtin(local_invocation_id) localId: vec3u,
|
|
190
|
+
@builtin(local_invocation_index) localIndex: u32
|
|
191
|
+
) {
|
|
192
|
+
let faceIndex = params.faceIndex;
|
|
193
|
+
let textureSize = params.textureSize;
|
|
194
|
+
let tilesPerThread = textureSize / 8u;
|
|
195
|
+
|
|
196
|
+
// Each thread samples a region and accumulates
|
|
197
|
+
var sum = vec4f(0.0);
|
|
198
|
+
let baseX = localId.x * tilesPerThread;
|
|
199
|
+
let baseY = localId.y * tilesPerThread;
|
|
200
|
+
|
|
201
|
+
for (var dy = 0u; dy < tilesPerThread; dy++) {
|
|
202
|
+
for (var dx = 0u; dx < tilesPerThread; dx++) {
|
|
203
|
+
let coord = vec2i(i32(baseX + dx), i32(baseY + dy));
|
|
204
|
+
var sample = textureLoad(inputTexture, coord, 0);
|
|
205
|
+
|
|
206
|
+
// Clamp max luminance to prevent sun/bright HDR from dominating
|
|
207
|
+
// This preserves color ratios while limiting brightness
|
|
208
|
+
var lum = max(sample.r, max(sample.g, sample.b));
|
|
209
|
+
let maxLum = 2.0; // Cap brightness (sun can be 100+)
|
|
210
|
+
if (lum > maxLum) {
|
|
211
|
+
sample = sample * (maxLum / lum);
|
|
212
|
+
lum = maxLum;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Gentle boost for emissive after clamping (logarithmic)
|
|
216
|
+
if (lum > 0.5 && params.emissiveBoost > 0.0) {
|
|
217
|
+
let boost = 1.0 + log2(lum + 1.0) * params.emissiveBoost;
|
|
218
|
+
sample = sample * boost;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
sum += sample;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Average this thread's samples
|
|
226
|
+
let pixelCount = f32(tilesPerThread * tilesPerThread);
|
|
227
|
+
sharedColors[localIndex] = sum / pixelCount;
|
|
228
|
+
|
|
229
|
+
workgroupBarrier();
|
|
230
|
+
|
|
231
|
+
// Parallel reduction: 64 -> 32 -> 16 -> 8 -> 4 -> 2 -> 1
|
|
232
|
+
if (localIndex < 32u) {
|
|
233
|
+
sharedColors[localIndex] += sharedColors[localIndex + 32u];
|
|
234
|
+
}
|
|
235
|
+
workgroupBarrier();
|
|
236
|
+
|
|
237
|
+
if (localIndex < 16u) {
|
|
238
|
+
sharedColors[localIndex] += sharedColors[localIndex + 16u];
|
|
239
|
+
}
|
|
240
|
+
workgroupBarrier();
|
|
241
|
+
|
|
242
|
+
if (localIndex < 8u) {
|
|
243
|
+
sharedColors[localIndex] += sharedColors[localIndex + 8u];
|
|
244
|
+
}
|
|
245
|
+
workgroupBarrier();
|
|
246
|
+
|
|
247
|
+
if (localIndex < 4u) {
|
|
248
|
+
sharedColors[localIndex] += sharedColors[localIndex + 4u];
|
|
249
|
+
}
|
|
250
|
+
workgroupBarrier();
|
|
251
|
+
|
|
252
|
+
if (localIndex < 2u) {
|
|
253
|
+
sharedColors[localIndex] += sharedColors[localIndex + 2u];
|
|
254
|
+
}
|
|
255
|
+
workgroupBarrier();
|
|
256
|
+
|
|
257
|
+
// Final reduction with temporal smoothing
|
|
258
|
+
if (localIndex == 0u) {
|
|
259
|
+
let finalSum = sharedColors[0] + sharedColors[1];
|
|
260
|
+
let newColor = finalSum / 64.0;
|
|
261
|
+
|
|
262
|
+
// Blend with previous value for temporal smoothing
|
|
263
|
+
let oldColor = outputColors[faceIndex];
|
|
264
|
+
let blendFactor = params.blendFactor;
|
|
265
|
+
outputColors[faceIndex] = mix(oldColor, newColor, blendFactor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
`
|
|
269
|
+
|
|
270
|
+
const shaderModule = device.createShaderModule({
|
|
271
|
+
label: 'ambientReduceShader',
|
|
272
|
+
code: shaderCode
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
this.reducePipeline = await device.createComputePipelineAsync({
|
|
276
|
+
label: 'ambientReducePipeline',
|
|
277
|
+
layout: device.createPipelineLayout({
|
|
278
|
+
bindGroupLayouts: [this.reduceBindGroupLayout]
|
|
279
|
+
}),
|
|
280
|
+
compute: {
|
|
281
|
+
module: shaderModule,
|
|
282
|
+
entryPoint: 'main'
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// Create uniform buffers for each face (stores faceIndex, textureSize, blendFactor, emissiveBoost)
|
|
287
|
+
this.reduceUniformBuffers = []
|
|
288
|
+
this.reduceUniformData = []
|
|
289
|
+
for (let i = 0; i < 6; i++) {
|
|
290
|
+
const buffer = device.createBuffer({
|
|
291
|
+
label: `ambientReduceParams_${i}`,
|
|
292
|
+
size: 16, // u32 + u32 + f32 + f32
|
|
293
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
294
|
+
})
|
|
295
|
+
// Initialize with default values
|
|
296
|
+
const data = new ArrayBuffer(16)
|
|
297
|
+
const view = new DataView(data)
|
|
298
|
+
view.setUint32(0, i, true) // faceIndex
|
|
299
|
+
view.setUint32(4, this.captureSize, true) // textureSize
|
|
300
|
+
view.setFloat32(8, 1.0, true) // blendFactor (1.0 = instant, for first frame)
|
|
301
|
+
view.setFloat32(12, 2.0, true) // emissiveBoost
|
|
302
|
+
device.queue.writeBuffer(buffer, 0, data)
|
|
303
|
+
this.reduceUniformBuffers.push(buffer)
|
|
304
|
+
this.reduceUniformData.push(data)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Smoothing time in seconds
|
|
308
|
+
this.smoothingTime = 1.0
|
|
309
|
+
this.lastUpdateTime = performance.now() / 1000
|
|
310
|
+
|
|
311
|
+
// Create bind groups for each face
|
|
312
|
+
this._createReduceBindGroups()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
_createReduceBindGroups() {
|
|
316
|
+
const { device } = this.engine
|
|
317
|
+
|
|
318
|
+
this.reduceBindGroups = []
|
|
319
|
+
const lightingOutput = this.lightingPass.getOutputTexture()
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < 6; i++) {
|
|
322
|
+
const bindGroup = device.createBindGroup({
|
|
323
|
+
label: `ambientReduceBindGroup_${i}`,
|
|
324
|
+
layout: this.reduceBindGroupLayout,
|
|
325
|
+
entries: [
|
|
326
|
+
{ binding: 0, resource: lightingOutput.view },
|
|
327
|
+
{ binding: 1, resource: { buffer: this.faceColorsBuffer } },
|
|
328
|
+
{ binding: 2, resource: { buffer: this.reduceUniformBuffers[i] } }
|
|
329
|
+
]
|
|
330
|
+
})
|
|
331
|
+
this.reduceBindGroups.push(bindGroup)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Set dependencies from main render pipeline
|
|
337
|
+
*/
|
|
338
|
+
setDependencies(options) {
|
|
339
|
+
const { environmentMap, encoding, shadowPass, noise, noiseSize } = options
|
|
340
|
+
|
|
341
|
+
if (environmentMap) {
|
|
342
|
+
this.lightingPass.setEnvironmentMap(environmentMap, encoding ?? 0)
|
|
343
|
+
}
|
|
344
|
+
if (shadowPass) {
|
|
345
|
+
this.lightingPass.setShadowPass(shadowPass)
|
|
346
|
+
}
|
|
347
|
+
if (noise) {
|
|
348
|
+
this.gbufferPass.setNoise(noise, noiseSize, false)
|
|
349
|
+
this.lightingPass.setNoise(noise, noiseSize, false)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Calculate 6 world-space directions for ambient capture
|
|
355
|
+
* Uses fixed world axes so shader can apply using world-space normals
|
|
356
|
+
*/
|
|
357
|
+
_calculateDirections(camera) {
|
|
358
|
+
// 6 directions in world space: up, down, left (-X), right (+X), front (+Z), back (-Z)
|
|
359
|
+
// These match the shader's normal-direction mapping
|
|
360
|
+
this.directions = [
|
|
361
|
+
[0, 1, 0], // up (+Y)
|
|
362
|
+
[0, -1, 0], // down (-Y)
|
|
363
|
+
[-1, 0, 0], // left (-X)
|
|
364
|
+
[1, 0, 0], // right (+X)
|
|
365
|
+
[0, 0, 1], // front (+Z)
|
|
366
|
+
[0, 0, -1] // back (-Z)
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Create a camera looking in a specific direction
|
|
372
|
+
*/
|
|
373
|
+
_createFaceCamera(camera, direction, faceIndex) {
|
|
374
|
+
const position = camera.position
|
|
375
|
+
|
|
376
|
+
// Create look-at view matrix
|
|
377
|
+
// Target = position + direction
|
|
378
|
+
const target = [
|
|
379
|
+
position[0] + direction[0],
|
|
380
|
+
position[1] + direction[1],
|
|
381
|
+
position[2] + direction[2]
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
// Up vector: for up/down faces, use world +Z; otherwise use world up
|
|
385
|
+
let up
|
|
386
|
+
if (faceIndex === 0) {
|
|
387
|
+
// Looking up (+Y): use +Z as up
|
|
388
|
+
up = [0, 0, 1]
|
|
389
|
+
} else if (faceIndex === 1) {
|
|
390
|
+
// Looking down (-Y): use +Z as up
|
|
391
|
+
up = [0, 0, 1]
|
|
392
|
+
} else {
|
|
393
|
+
// Horizontal directions: use world up (+Y)
|
|
394
|
+
up = [0, 1, 0]
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
mat4.lookAt(this.faceView, position, target, up)
|
|
398
|
+
|
|
399
|
+
// 90 degree FOV perspective projection
|
|
400
|
+
const fov = Math.PI / 2 // 90 degrees
|
|
401
|
+
const aspect = 1.0
|
|
402
|
+
const near = 0.1
|
|
403
|
+
const far = this.maxDistance
|
|
404
|
+
mat4.perspective(this.faceProj, fov, aspect, near, far)
|
|
405
|
+
|
|
406
|
+
// Create inverse matrices
|
|
407
|
+
const iView = mat4.create()
|
|
408
|
+
const iProj = mat4.create()
|
|
409
|
+
const viewProj = mat4.create()
|
|
410
|
+
const iViewProj = mat4.create()
|
|
411
|
+
mat4.invert(iView, this.faceView)
|
|
412
|
+
mat4.invert(iProj, this.faceProj)
|
|
413
|
+
mat4.multiply(viewProj, this.faceProj, this.faceView)
|
|
414
|
+
mat4.invert(iViewProj, viewProj)
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
view: this.faceView,
|
|
418
|
+
proj: this.faceProj,
|
|
419
|
+
iView,
|
|
420
|
+
iProj,
|
|
421
|
+
iViewProj,
|
|
422
|
+
viewProj,
|
|
423
|
+
position,
|
|
424
|
+
near,
|
|
425
|
+
far,
|
|
426
|
+
aspect,
|
|
427
|
+
jitterEnabled: false,
|
|
428
|
+
jitterOffset: [0, 0],
|
|
429
|
+
screenSize: [this.captureSize, this.captureSize],
|
|
430
|
+
forward: direction,
|
|
431
|
+
updateMatrix: () => {},
|
|
432
|
+
updateView: () => {}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Execute ambient capture pass
|
|
438
|
+
*/
|
|
439
|
+
async _execute(context) {
|
|
440
|
+
const { device, stats } = this.engine
|
|
441
|
+
const { camera, meshes, lights, mainLight, dt = 0.016 } = context
|
|
442
|
+
|
|
443
|
+
// Process deferred texture destruction
|
|
444
|
+
this._pendingDestroyIndex = (this._pendingDestroyIndex + 1) % 3
|
|
445
|
+
const toDestroy = this._pendingDestroyRing[this._pendingDestroyIndex]
|
|
446
|
+
for (const tex of toDestroy) {
|
|
447
|
+
tex.destroy()
|
|
448
|
+
}
|
|
449
|
+
this._pendingDestroyRing[this._pendingDestroyIndex] = []
|
|
450
|
+
|
|
451
|
+
// Check if enabled
|
|
452
|
+
if (!this.settings?.ambientCapture?.enabled) {
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Update settings
|
|
457
|
+
this.maxDistance = this.settings?.ambientCapture?.maxDistance ?? 25
|
|
458
|
+
this.smoothingTime = this.settings?.ambientCapture?.smoothingTime ?? 1.0
|
|
459
|
+
|
|
460
|
+
// Calculate directions based on camera orientation
|
|
461
|
+
this._calculateDirections(camera)
|
|
462
|
+
|
|
463
|
+
// Copy lights to internal lighting pass
|
|
464
|
+
this.lightingPass.lights = lights
|
|
465
|
+
|
|
466
|
+
// Determine which 2 faces to update this frame
|
|
467
|
+
const facesToUpdate = [
|
|
468
|
+
this.currentFaceIndex,
|
|
469
|
+
(this.currentFaceIndex + 1) % 6
|
|
470
|
+
]
|
|
471
|
+
|
|
472
|
+
// Render and reduce each face with temporal smoothing
|
|
473
|
+
for (const faceIndex of facesToUpdate) {
|
|
474
|
+
await this._renderFace(context, faceIndex)
|
|
475
|
+
this._reduceFace(faceIndex, dt)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Advance to next pair of faces
|
|
479
|
+
this.currentFaceIndex = (this.currentFaceIndex + 2) % 6
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async _renderFace(context, faceIndex) {
|
|
483
|
+
const { camera, meshes, lights, mainLight, dt } = context
|
|
484
|
+
|
|
485
|
+
const direction = this.directions[faceIndex]
|
|
486
|
+
const faceCamera = this._createFaceCamera(camera, direction, faceIndex)
|
|
487
|
+
|
|
488
|
+
// Execute GBuffer pass
|
|
489
|
+
await this.gbufferPass.execute({
|
|
490
|
+
camera: faceCamera,
|
|
491
|
+
meshes,
|
|
492
|
+
dt,
|
|
493
|
+
// Custom culling for ambient capture
|
|
494
|
+
cullingOverride: {
|
|
495
|
+
maxDistance: this.maxDistance,
|
|
496
|
+
frustum: true,
|
|
497
|
+
hiZ: false
|
|
498
|
+
}
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
// Execute Lighting pass
|
|
502
|
+
await this.lightingPass.execute({
|
|
503
|
+
camera: faceCamera,
|
|
504
|
+
meshes,
|
|
505
|
+
dt,
|
|
506
|
+
lights,
|
|
507
|
+
mainLight
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
_reduceFace(faceIndex, dt) {
|
|
512
|
+
const { device } = this.engine
|
|
513
|
+
|
|
514
|
+
// Calculate blend factor for temporal smoothing
|
|
515
|
+
// blend = 1 - exp(-dt / smoothingTime) gives exponential smoothing
|
|
516
|
+
// With smoothingTime=1s, after 1s we've blended ~63% of the way to target
|
|
517
|
+
const blendFactor = 1.0 - Math.exp(-dt / this.smoothingTime)
|
|
518
|
+
|
|
519
|
+
// Get emissive boost from settings
|
|
520
|
+
const emissiveBoost = this.settings?.ambientCapture?.emissiveBoost ?? 2.0
|
|
521
|
+
|
|
522
|
+
// Update uniform buffer with blend factor and emissive boost
|
|
523
|
+
const data = this.reduceUniformData[faceIndex]
|
|
524
|
+
const view = new DataView(data)
|
|
525
|
+
view.setFloat32(8, blendFactor, true) // blendFactor
|
|
526
|
+
view.setFloat32(12, emissiveBoost, true) // emissiveBoost
|
|
527
|
+
device.queue.writeBuffer(this.reduceUniformBuffers[faceIndex], 0, data)
|
|
528
|
+
|
|
529
|
+
const commandEncoder = device.createCommandEncoder({
|
|
530
|
+
label: `ambientReduce_face${faceIndex}`
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const computePass = commandEncoder.beginComputePass({
|
|
534
|
+
label: `ambientReducePass_face${faceIndex}`
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
computePass.setPipeline(this.reducePipeline)
|
|
538
|
+
computePass.setBindGroup(0, this.reduceBindGroups[faceIndex])
|
|
539
|
+
computePass.dispatchWorkgroups(1) // Single workgroup handles entire 64x64 texture
|
|
540
|
+
computePass.end()
|
|
541
|
+
|
|
542
|
+
device.queue.submit([commandEncoder.finish()])
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get the face colors buffer for RenderPost
|
|
547
|
+
*/
|
|
548
|
+
getFaceColorsBuffer() {
|
|
549
|
+
return this.faceColorsBuffer
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get the 6 directions (for RenderPost to know orientation)
|
|
554
|
+
*/
|
|
555
|
+
getDirections() {
|
|
556
|
+
return this.directions
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async _resize(width, height) {
|
|
560
|
+
// Ambient capture doesn't resize with screen - fixed resolution
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
_destroy() {
|
|
564
|
+
if (this.gbufferPass) {
|
|
565
|
+
this.gbufferPass.destroy()
|
|
566
|
+
this.gbufferPass = null
|
|
567
|
+
}
|
|
568
|
+
if (this.lightingPass) {
|
|
569
|
+
this.lightingPass.destroy()
|
|
570
|
+
this.lightingPass = null
|
|
571
|
+
}
|
|
572
|
+
if (this.dummyAO?.texture) {
|
|
573
|
+
this.dummyAO.texture.destroy()
|
|
574
|
+
this.dummyAO = null
|
|
575
|
+
}
|
|
576
|
+
if (this.faceColorsBuffer) {
|
|
577
|
+
this.faceColorsBuffer.destroy()
|
|
578
|
+
this.faceColorsBuffer = null
|
|
579
|
+
}
|
|
580
|
+
for (const buffer of this.reduceUniformBuffers || []) {
|
|
581
|
+
buffer.destroy()
|
|
582
|
+
}
|
|
583
|
+
this.reduceUniformBuffers = []
|
|
584
|
+
for (const slot of this._pendingDestroyRing) {
|
|
585
|
+
for (const tex of slot) {
|
|
586
|
+
tex.destroy()
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
this._pendingDestroyRing = [[], [], []]
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export { AmbientCapturePass }
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BasePass - Abstract base class for render passes
|
|
3
|
+
*
|
|
4
|
+
* All render passes in the 7-pass pipeline inherit from this.
|
|
5
|
+
*/
|
|
6
|
+
class BasePass {
|
|
7
|
+
constructor(name, engine = null) {
|
|
8
|
+
this.name = name
|
|
9
|
+
this.engine = engine
|
|
10
|
+
this.enabled = true
|
|
11
|
+
this._initialized = false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Convenience getter for settings (with fallback for passes without engine)
|
|
15
|
+
get settings() {
|
|
16
|
+
return this.engine?.settings
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the pass (create pipelines, textures, etc.)
|
|
21
|
+
* Called once before first use.
|
|
22
|
+
* @returns {Promise<void>}
|
|
23
|
+
*/
|
|
24
|
+
async initialize() {
|
|
25
|
+
if (this._initialized) return
|
|
26
|
+
await this._init()
|
|
27
|
+
this._initialized = true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Override in subclass to perform initialization
|
|
32
|
+
* @protected
|
|
33
|
+
*/
|
|
34
|
+
async _init() {
|
|
35
|
+
// Override in subclass
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute the render pass
|
|
40
|
+
* @param {Object} context - Render context with camera, entities, etc.
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async execute(context) {
|
|
44
|
+
if (!this.enabled) return
|
|
45
|
+
if (!this._initialized) {
|
|
46
|
+
await this.initialize()
|
|
47
|
+
}
|
|
48
|
+
await this._execute(context)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Override in subclass to perform rendering
|
|
53
|
+
* @param {Object} context - Render context
|
|
54
|
+
* @protected
|
|
55
|
+
*/
|
|
56
|
+
async _execute(context) {
|
|
57
|
+
// Override in subclass
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resize pass resources (called on window resize)
|
|
62
|
+
* @param {number} width - New width
|
|
63
|
+
* @param {number} height - New height
|
|
64
|
+
*/
|
|
65
|
+
async resize(width, height) {
|
|
66
|
+
await this._resize(width, height)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Override in subclass to handle resize
|
|
71
|
+
* @protected
|
|
72
|
+
*/
|
|
73
|
+
async _resize(width, height) {
|
|
74
|
+
// Override in subclass
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Destroy pass resources
|
|
79
|
+
*/
|
|
80
|
+
destroy() {
|
|
81
|
+
this._destroy()
|
|
82
|
+
this._initialized = false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Override in subclass to clean up resources
|
|
87
|
+
* @protected
|
|
88
|
+
*/
|
|
89
|
+
_destroy() {
|
|
90
|
+
// Override in subclass
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get debug name for profiling
|
|
95
|
+
*/
|
|
96
|
+
getDebugName() {
|
|
97
|
+
return `Pass: ${this.name}`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { BasePass }
|