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,730 @@
|
|
|
1
|
+
import { BasePass } from "./BasePass.js"
|
|
2
|
+
import { Pipeline } from "../../Pipeline.js"
|
|
3
|
+
import { Texture } from "../../Texture.js"
|
|
4
|
+
import { Frustum } from "../../utils/Frustum.js"
|
|
5
|
+
import { transformBoundingSphere } from "../../utils/BoundingSphere.js"
|
|
6
|
+
|
|
7
|
+
import geometryWGSL from "../shaders/geometry.wgsl"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GBuffer textures container
|
|
11
|
+
*/
|
|
12
|
+
class GBuffer {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.isGBuffer = true
|
|
15
|
+
this.albedo = null // rgba8unorm - Base color
|
|
16
|
+
this.normal = null // rgba16float - World-space normals
|
|
17
|
+
this.arm = null // rgba8unorm - Ambient Occlusion, Roughness, Metallic
|
|
18
|
+
this.emission = null // rgba16float - Emissive color
|
|
19
|
+
this.velocity = null // rg16float - Motion vectors (screen-space pixels)
|
|
20
|
+
this.depth = null // depth32float - Scene depth
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static async create(engine, width, height) {
|
|
24
|
+
const gbuffer = new GBuffer()
|
|
25
|
+
gbuffer.albedo = await Texture.renderTarget(engine, 'rgba8unorm', width, height)
|
|
26
|
+
gbuffer.normal = await Texture.renderTarget(engine, 'rgba16float', width, height)
|
|
27
|
+
gbuffer.arm = await Texture.renderTarget(engine, 'rgba8unorm', width, height)
|
|
28
|
+
gbuffer.emission = await Texture.renderTarget(engine, 'rgba16float', width, height)
|
|
29
|
+
gbuffer.velocity = await Texture.renderTarget(engine, 'rg16float', width, height)
|
|
30
|
+
gbuffer.depth = await Texture.depth(engine, width, height)
|
|
31
|
+
gbuffer.width = width
|
|
32
|
+
gbuffer.height = height
|
|
33
|
+
return gbuffer
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getTargets() {
|
|
37
|
+
return [
|
|
38
|
+
{ format: "rgba8unorm" },
|
|
39
|
+
{ format: "rgba16float" },
|
|
40
|
+
{ format: "rgba8unorm" },
|
|
41
|
+
{ format: "rgba16float" },
|
|
42
|
+
{ format: "rg16float" },
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getColorAttachments() {
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
view: this.albedo.view,
|
|
50
|
+
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
|
51
|
+
loadOp: 'clear',
|
|
52
|
+
storeOp: 'store',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
view: this.normal.view,
|
|
56
|
+
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
|
57
|
+
loadOp: 'clear',
|
|
58
|
+
storeOp: 'store',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
view: this.arm.view,
|
|
62
|
+
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
|
63
|
+
loadOp: 'clear',
|
|
64
|
+
storeOp: 'store',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
view: this.emission.view,
|
|
68
|
+
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
|
|
69
|
+
loadOp: 'clear',
|
|
70
|
+
storeOp: 'store',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
view: this.velocity.view,
|
|
74
|
+
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
|
|
75
|
+
loadOp: 'clear',
|
|
76
|
+
storeOp: 'store',
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getDepthStencilAttachment() {
|
|
82
|
+
return {
|
|
83
|
+
view: this.depth.view,
|
|
84
|
+
depthClearValue: 1.0,
|
|
85
|
+
depthLoadOp: 'clear',
|
|
86
|
+
depthStoreOp: 'store',
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* GBufferPass - Renders scene geometry to GBuffer textures
|
|
93
|
+
*
|
|
94
|
+
* Pass 4 in the 7-pass pipeline.
|
|
95
|
+
* Outputs: Albedo, Normal, ARM, Emission, Depth
|
|
96
|
+
*/
|
|
97
|
+
class GBufferPass extends BasePass {
|
|
98
|
+
constructor(engine = null) {
|
|
99
|
+
super('GBuffer', engine)
|
|
100
|
+
|
|
101
|
+
this.gbuffer = null
|
|
102
|
+
this.pipelines = new Map() // materialId -> pipeline (ready)
|
|
103
|
+
this.skinnedPipelines = new Map() // materialId -> skinned pipeline (ready)
|
|
104
|
+
this.pendingPipelines = new Map() // materialId -> Promise<pipeline> (compiling)
|
|
105
|
+
|
|
106
|
+
// Clip plane for planar reflections
|
|
107
|
+
this.clipPlaneY = 0
|
|
108
|
+
this.clipPlaneEnabled = false
|
|
109
|
+
this.clipPlaneDirection = 1.0 // 1.0 = discard below, -1.0 = discard above
|
|
110
|
+
|
|
111
|
+
// Distance fade for preventing object popping at culling distance
|
|
112
|
+
this.distanceFadeStart = 0 // Distance where fade begins
|
|
113
|
+
this.distanceFadeEnd = 0 // Distance where fade completes (0 = disabled)
|
|
114
|
+
|
|
115
|
+
// Noise texture for alpha hashing
|
|
116
|
+
this.noiseTexture = null
|
|
117
|
+
this.noiseSize = 64
|
|
118
|
+
this.noiseAnimated = true
|
|
119
|
+
|
|
120
|
+
// HiZ pass reference for occlusion culling of legacy meshes
|
|
121
|
+
this.hizPass = null
|
|
122
|
+
|
|
123
|
+
// Frustum for legacy mesh culling
|
|
124
|
+
this.frustum = new Frustum()
|
|
125
|
+
|
|
126
|
+
// Billboard camera vectors (extracted from view matrix)
|
|
127
|
+
this._billboardCameraRight = [1, 0, 0]
|
|
128
|
+
this._billboardCameraUp = [0, 1, 0]
|
|
129
|
+
this._billboardCameraForward = [0, 0, -1]
|
|
130
|
+
|
|
131
|
+
// Culling stats for legacy meshes
|
|
132
|
+
this.legacyCullingStats = {
|
|
133
|
+
total: 0,
|
|
134
|
+
rendered: 0,
|
|
135
|
+
culledByFrustum: 0,
|
|
136
|
+
culledByDistance: 0,
|
|
137
|
+
culledByOcclusion: 0
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set the HiZ pass for occlusion culling of legacy meshes
|
|
143
|
+
* @param {HiZPass} hizPass - The HiZ pass instance
|
|
144
|
+
*/
|
|
145
|
+
setHiZPass(hizPass) {
|
|
146
|
+
this.hizPass = hizPass
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Test if a legacy mesh should be culled (per-instance occlusion culling)
|
|
151
|
+
* @param {Mesh} mesh - The mesh to test
|
|
152
|
+
* @param {Camera} camera - Current camera
|
|
153
|
+
* @param {boolean} canCull - Whether frustum/occlusion culling is available
|
|
154
|
+
* @returns {string|null} - Reason for culling or null if visible
|
|
155
|
+
*/
|
|
156
|
+
_shouldCullLegacyMesh(mesh, camera, canCull) {
|
|
157
|
+
// Skip culling if disabled or no HiZ pass
|
|
158
|
+
if (!canCull || !this.hizPass || !camera) {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Skip for entity-managed meshes - they're already culled by CullingSystem
|
|
163
|
+
// Only perform per-mesh occlusion culling for static (non-entity-managed) meshes
|
|
164
|
+
if (!mesh.static) {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const occlusionEnabled = this.settings?.occlusionCulling?.enabled
|
|
169
|
+
if (!occlusionEnabled) {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get local bounding sphere from geometry
|
|
174
|
+
const localBsphere = mesh.geometry?.getBoundingSphere?.()
|
|
175
|
+
if (!localBsphere || localBsphere.radius <= 0) {
|
|
176
|
+
this.legacyCullingStats.skippedNoBsphere = (this.legacyCullingStats.skippedNoBsphere || 0) + 1
|
|
177
|
+
return null // No valid bsphere, don't cull
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const instanceCount = mesh.geometry?.instanceCount || 0
|
|
181
|
+
if (instanceCount === 0) {
|
|
182
|
+
return null // No instances to test
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const instanceData = mesh.geometry?.instanceData
|
|
186
|
+
if (!instanceData) {
|
|
187
|
+
return null // No instance data
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Test each instance - if ANY is visible, mesh is visible
|
|
191
|
+
// Instance data layout: 20 floats per instance (4x4 matrix + 4 extra)
|
|
192
|
+
const floatsPerInstance = 20
|
|
193
|
+
let allOccluded = true
|
|
194
|
+
|
|
195
|
+
// Copy camera data to avoid mutable reference issues
|
|
196
|
+
const cameraPos = [camera.position[0], camera.position[1], camera.position[2]]
|
|
197
|
+
const viewProj = camera.viewProj
|
|
198
|
+
const near = camera.near
|
|
199
|
+
const far = camera.far
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
202
|
+
const offset = i * floatsPerInstance
|
|
203
|
+
|
|
204
|
+
// Extract 4x4 matrix from instance data
|
|
205
|
+
const matrix = instanceData.subarray(offset, offset + 16)
|
|
206
|
+
|
|
207
|
+
// Transform local bsphere by instance matrix
|
|
208
|
+
const worldBsphere = transformBoundingSphere(localBsphere, matrix)
|
|
209
|
+
|
|
210
|
+
// Test against HiZ - if visible, mesh is visible
|
|
211
|
+
const occluded = this.hizPass.testSphereOcclusion(
|
|
212
|
+
worldBsphere,
|
|
213
|
+
viewProj,
|
|
214
|
+
near,
|
|
215
|
+
far,
|
|
216
|
+
cameraPos
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if (!occluded) {
|
|
220
|
+
allOccluded = false
|
|
221
|
+
break // At least one instance visible, no need to test more
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return allOccluded ? 'occlusion' : null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract camera vectors from view matrix for billboarding
|
|
230
|
+
* The view matrix transforms world to view space. Camera basis vectors:
|
|
231
|
+
* - Right: first row of view matrix
|
|
232
|
+
* - Up: second row of view matrix
|
|
233
|
+
* - Forward: negative third row (camera looks down -Z in view space)
|
|
234
|
+
* @param {Float32Array|Array} viewMatrix - 4x4 view matrix
|
|
235
|
+
*/
|
|
236
|
+
_extractCameraVectors(viewMatrix) {
|
|
237
|
+
// View matrix is column-major, so row vectors are at indices:
|
|
238
|
+
// Row 0 (right): [0], [4], [8]
|
|
239
|
+
// Row 1 (up): [1], [5], [9]
|
|
240
|
+
// Row 2 (forward): [2], [6], [10] (negated because -Z is forward)
|
|
241
|
+
this._billboardCameraRight[0] = viewMatrix[0]
|
|
242
|
+
this._billboardCameraRight[1] = viewMatrix[4]
|
|
243
|
+
this._billboardCameraRight[2] = viewMatrix[8]
|
|
244
|
+
|
|
245
|
+
this._billboardCameraUp[0] = viewMatrix[1]
|
|
246
|
+
this._billboardCameraUp[1] = viewMatrix[5]
|
|
247
|
+
this._billboardCameraUp[2] = viewMatrix[9]
|
|
248
|
+
|
|
249
|
+
// Negate Z row for forward direction
|
|
250
|
+
this._billboardCameraForward[0] = -viewMatrix[2]
|
|
251
|
+
this._billboardCameraForward[1] = -viewMatrix[6]
|
|
252
|
+
this._billboardCameraForward[2] = -viewMatrix[10]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get billboard mode from material
|
|
257
|
+
* @param {Material} material - Material with optional billboardMode uniform
|
|
258
|
+
* @returns {number} Billboard mode: 0=none, 1=center, 2=bottom, 3=horizontal
|
|
259
|
+
*/
|
|
260
|
+
_getBillboardMode(material) {
|
|
261
|
+
const mode = material?.uniforms?.billboardMode
|
|
262
|
+
if (typeof mode === 'number') return mode
|
|
263
|
+
if (mode === 'center') return 1
|
|
264
|
+
if (mode === 'bottom') return 2
|
|
265
|
+
if (mode === 'horizontal') return 3
|
|
266
|
+
return 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Set the noise texture for alpha hashing
|
|
271
|
+
* @param {Texture} noise - Noise texture (blue noise or bayer dither)
|
|
272
|
+
* @param {number} size - Texture size
|
|
273
|
+
* @param {boolean} animated - Whether to animate noise offset each frame
|
|
274
|
+
*/
|
|
275
|
+
setNoise(noise, size = 64, animated = true) {
|
|
276
|
+
this.noiseTexture = noise
|
|
277
|
+
this.noiseSize = size
|
|
278
|
+
this.noiseAnimated = animated
|
|
279
|
+
// Mark all pipelines for rebuild (they need noise texture binding)
|
|
280
|
+
this.pipelines.clear()
|
|
281
|
+
this.skinnedPipelines.clear()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async _init() {
|
|
285
|
+
const { canvas } = this.engine
|
|
286
|
+
this.gbuffer = await GBuffer.create(this.engine, canvas.width, canvas.height)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get pipeline key for a mesh
|
|
291
|
+
*/
|
|
292
|
+
_getPipelineKey(mesh) {
|
|
293
|
+
const isSkinned = mesh.hasSkin && mesh.skin
|
|
294
|
+
const meshId = mesh.uid || mesh.geometry?.uid || 'default'
|
|
295
|
+
const forceEmissive = mesh.material?.forceEmissive ? '_emissive' : ''
|
|
296
|
+
const doubleSided = mesh.material?.doubleSided ? '_dbl' : ''
|
|
297
|
+
return `${mesh.material.uid}_${meshId}${isSkinned ? '_skinned' : ''}${forceEmissive}${doubleSided}`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Check if pipeline is ready for a mesh (non-blocking)
|
|
302
|
+
* @param {Mesh} mesh - The mesh to check
|
|
303
|
+
* @returns {Pipeline|null} The pipeline if ready, null if still compiling
|
|
304
|
+
*/
|
|
305
|
+
_getPipelineIfReady(mesh) {
|
|
306
|
+
const isSkinned = mesh.hasSkin && mesh.skin
|
|
307
|
+
const pipelinesMap = isSkinned ? this.skinnedPipelines : this.pipelines
|
|
308
|
+
const key = this._getPipelineKey(mesh)
|
|
309
|
+
return pipelinesMap.get(key) || null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check if pipeline is ready AND warmed up (stable for rendering)
|
|
314
|
+
* @param {Mesh} mesh - The mesh to check
|
|
315
|
+
* @returns {boolean} True if pipeline is ready and warmed up
|
|
316
|
+
*/
|
|
317
|
+
isPipelineStable(mesh) {
|
|
318
|
+
const pipeline = this._getPipelineIfReady(mesh)
|
|
319
|
+
return pipeline && (!pipeline._warmupFrames || pipeline._warmupFrames <= 0)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Start pipeline creation in background (non-blocking)
|
|
324
|
+
* @param {Mesh} mesh - The mesh to create pipeline for
|
|
325
|
+
*/
|
|
326
|
+
_startPipelineCreation(mesh) {
|
|
327
|
+
const isSkinned = mesh.hasSkin && mesh.skin
|
|
328
|
+
const pipelinesMap = isSkinned ? this.skinnedPipelines : this.pipelines
|
|
329
|
+
const key = this._getPipelineKey(mesh)
|
|
330
|
+
|
|
331
|
+
// Already ready or already pending
|
|
332
|
+
if (pipelinesMap.has(key) || this.pendingPipelines.has(key)) {
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Start async compilation without awaiting
|
|
337
|
+
const pipelinePromise = Pipeline.create(this.engine, {
|
|
338
|
+
label: `gbuffer-${key}`,
|
|
339
|
+
wgslSource: geometryWGSL,
|
|
340
|
+
geometry: mesh.geometry,
|
|
341
|
+
textures: mesh.material.textures,
|
|
342
|
+
renderTarget: this.gbuffer,
|
|
343
|
+
skin: isSkinned ? mesh.skin : null,
|
|
344
|
+
noiseTexture: this.noiseTexture,
|
|
345
|
+
doubleSided: mesh.material?.doubleSided ?? false,
|
|
346
|
+
}).then(pipeline => {
|
|
347
|
+
// Move from pending to ready
|
|
348
|
+
this.pendingPipelines.delete(key)
|
|
349
|
+
// Mark as warming up - needs 2 frames to stabilize
|
|
350
|
+
pipeline._warmupFrames = 2
|
|
351
|
+
pipelinesMap.set(key, pipeline)
|
|
352
|
+
return pipeline
|
|
353
|
+
}).catch(err => {
|
|
354
|
+
console.error(`Failed to create pipeline for ${key}:`, err)
|
|
355
|
+
this.pendingPipelines.delete(key)
|
|
356
|
+
return null
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
this.pendingPipelines.set(key, pipelinePromise)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get or create pipeline for a mesh (blocking - for batch system)
|
|
364
|
+
* @param {Mesh} mesh - The mesh to render
|
|
365
|
+
* @returns {Pipeline} The pipeline for this mesh
|
|
366
|
+
*/
|
|
367
|
+
async _getOrCreatePipeline(mesh) {
|
|
368
|
+
const isSkinned = mesh.hasSkin && mesh.skin
|
|
369
|
+
const pipelinesMap = isSkinned ? this.skinnedPipelines : this.pipelines
|
|
370
|
+
const key = this._getPipelineKey(mesh)
|
|
371
|
+
|
|
372
|
+
// Return if already ready
|
|
373
|
+
if (pipelinesMap.has(key)) {
|
|
374
|
+
return pipelinesMap.get(key)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Wait for pending if exists
|
|
378
|
+
if (this.pendingPipelines.has(key)) {
|
|
379
|
+
return await this.pendingPipelines.get(key)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Create new pipeline
|
|
383
|
+
const pipeline = await Pipeline.create(this.engine, {
|
|
384
|
+
label: `gbuffer-${key}`,
|
|
385
|
+
wgslSource: geometryWGSL,
|
|
386
|
+
geometry: mesh.geometry,
|
|
387
|
+
textures: mesh.material.textures,
|
|
388
|
+
renderTarget: this.gbuffer,
|
|
389
|
+
skin: isSkinned ? mesh.skin : null,
|
|
390
|
+
noiseTexture: this.noiseTexture,
|
|
391
|
+
doubleSided: mesh.material?.doubleSided ?? false,
|
|
392
|
+
})
|
|
393
|
+
// Mark as warming up - needs 2 frames to stabilize
|
|
394
|
+
pipeline._warmupFrames = 2
|
|
395
|
+
pipelinesMap.set(key, pipeline)
|
|
396
|
+
return pipeline
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Execute GBuffer pass
|
|
401
|
+
*
|
|
402
|
+
* @param {Object} context
|
|
403
|
+
* @param {Camera} context.camera - Current camera
|
|
404
|
+
* @param {Object} context.meshes - Legacy mesh dictionary (for backward compatibility)
|
|
405
|
+
* @param {Map} context.batches - Instance batches from InstanceManager (new system)
|
|
406
|
+
* @param {number} context.dt - Delta time for animation
|
|
407
|
+
* @param {HistoryBufferManager} context.historyManager - History buffer manager for motion vectors
|
|
408
|
+
*/
|
|
409
|
+
async _execute(context) {
|
|
410
|
+
const { device, canvas, options, stats } = this.engine
|
|
411
|
+
const { camera, meshes, batches, dt = 0, historyManager } = context
|
|
412
|
+
|
|
413
|
+
// Get previous frame camera matrices for motion vectors
|
|
414
|
+
// If no valid history, use current viewProj (zero motion)
|
|
415
|
+
const prevData = historyManager?.getPrevious()
|
|
416
|
+
const prevViewProjMatrix = prevData?.hasValidHistory
|
|
417
|
+
? prevData.viewProj
|
|
418
|
+
: camera.viewProj
|
|
419
|
+
|
|
420
|
+
// Get settings from engine (with fallbacks)
|
|
421
|
+
const emissionFactor = this.settings?.environment?.emissionFactor ?? [1.0, 1.0, 1.0, 4.0]
|
|
422
|
+
const mipBias = this.settings?.rendering?.mipBias ?? options.mipBias ?? 0
|
|
423
|
+
|
|
424
|
+
// Use absolute time for scene-loaded skins (same as entity animations)
|
|
425
|
+
// This ensures consistent timing regardless of frame rate fluctuations
|
|
426
|
+
const animationSpeed = this.settings?.animation?.speed ?? 1.0
|
|
427
|
+
const globalAnimTime = (performance.now() / 1000) * animationSpeed
|
|
428
|
+
|
|
429
|
+
stats.drawCalls = 0
|
|
430
|
+
stats.triangles = 0
|
|
431
|
+
|
|
432
|
+
// Update camera
|
|
433
|
+
camera.aspect = canvas.width / canvas.height
|
|
434
|
+
camera.updateMatrix()
|
|
435
|
+
camera.updateView()
|
|
436
|
+
|
|
437
|
+
// Extract camera vectors for billboarding
|
|
438
|
+
this._extractCameraVectors(camera.view)
|
|
439
|
+
|
|
440
|
+
let commandEncoder = null
|
|
441
|
+
let passEncoder = null
|
|
442
|
+
|
|
443
|
+
// Track which skins have been updated this frame (avoids duplicate updates)
|
|
444
|
+
const updatedSkins = new Set()
|
|
445
|
+
|
|
446
|
+
// New system: render batches from InstanceManager
|
|
447
|
+
if (batches && batches.size > 0) {
|
|
448
|
+
for (const [modelId, batch] of batches) {
|
|
449
|
+
const mesh = batch.mesh
|
|
450
|
+
if (!mesh) continue
|
|
451
|
+
|
|
452
|
+
// Update skin animation if skinned (skip if externally managed or already updated)
|
|
453
|
+
// Use absolute time (same as entities) for consistent animation speed
|
|
454
|
+
if (batch.hasSkin && batch.skin && !batch.skin.externallyManaged && !updatedSkins.has(batch.skin)) {
|
|
455
|
+
// Track animation start time per skin for absolute timing
|
|
456
|
+
if (batch.skin._animStartTime === undefined) {
|
|
457
|
+
batch.skin._animStartTime = globalAnimTime
|
|
458
|
+
}
|
|
459
|
+
const skinAnimTime = globalAnimTime - batch.skin._animStartTime
|
|
460
|
+
batch.skin.updateAtTime(skinAnimTime)
|
|
461
|
+
updatedSkins.add(batch.skin)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const pipeline = await this._getOrCreatePipeline(mesh)
|
|
465
|
+
|
|
466
|
+
// On first render after pipeline creation, force skin update to ensure proper state
|
|
467
|
+
if (pipeline._warmupFrames > 0) {
|
|
468
|
+
pipeline._warmupFrames--
|
|
469
|
+
if (batch.hasSkin && batch.skin) {
|
|
470
|
+
// Force immediate skin update to avoid stale joint matrices
|
|
471
|
+
batch.skin.update(0)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Update bind group if skinned
|
|
476
|
+
if (batch.hasSkin && batch.skin) {
|
|
477
|
+
pipeline.updateBindGroupForSkin(batch.skin)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Update geometry buffers
|
|
481
|
+
mesh.geometry.update()
|
|
482
|
+
|
|
483
|
+
// Set uniforms
|
|
484
|
+
const jitterFadeDistance = this.settings?.rendering?.jitterFadeDistance ?? 30.0
|
|
485
|
+
// Get alpha hash settings (per-material or global)
|
|
486
|
+
const alphaHashEnabled = mesh.material?.alphaHash ?? this.settings?.rendering?.alphaHash ?? false
|
|
487
|
+
const alphaHashScale = mesh.material?.alphaHashScale ?? this.settings?.rendering?.alphaHashScale ?? 1.0
|
|
488
|
+
const luminanceToAlpha = mesh.material?.luminanceToAlpha ?? this.settings?.rendering?.luminanceToAlpha ?? false
|
|
489
|
+
|
|
490
|
+
pipeline.uniformValues.set({
|
|
491
|
+
viewMatrix: camera.view,
|
|
492
|
+
projectionMatrix: camera.proj,
|
|
493
|
+
prevViewProjMatrix: prevViewProjMatrix,
|
|
494
|
+
mipBias: mipBias,
|
|
495
|
+
skinEnabled: batch.hasSkin ? 1.0 : 0.0,
|
|
496
|
+
numJoints: batch.hasSkin && batch.skin ? batch.skin.numJoints : 0,
|
|
497
|
+
near: camera.near || 0.05,
|
|
498
|
+
far: camera.far || 1000,
|
|
499
|
+
jitterFadeDistance: jitterFadeDistance,
|
|
500
|
+
jitterOffset: camera.jitterOffset || [0, 0],
|
|
501
|
+
screenSize: camera.screenSize || [canvas.width, canvas.height],
|
|
502
|
+
emissionFactor: emissionFactor,
|
|
503
|
+
clipPlaneY: this.clipPlaneY,
|
|
504
|
+
clipPlaneEnabled: this.clipPlaneEnabled ? 1.0 : 0.0,
|
|
505
|
+
clipPlaneDirection: this.clipPlaneDirection,
|
|
506
|
+
pixelRounding: this.settings?.rendering?.pixelRounding || 0.0,
|
|
507
|
+
pixelExpansion: this.settings?.rendering?.pixelExpansion ?? 0.05,
|
|
508
|
+
positionRounding: this.settings?.rendering?.positionRounding || 0.0,
|
|
509
|
+
alphaHashEnabled: alphaHashEnabled ? 1.0 : 0.0,
|
|
510
|
+
alphaHashScale: alphaHashScale,
|
|
511
|
+
luminanceToAlpha: luminanceToAlpha ? 1.0 : 0.0,
|
|
512
|
+
noiseSize: this.noiseSize,
|
|
513
|
+
// Always use static noise for alpha hash to avoid shimmer on cutout edges
|
|
514
|
+
noiseOffsetX: 0,
|
|
515
|
+
noiseOffsetY: 0,
|
|
516
|
+
cameraPosition: camera.position,
|
|
517
|
+
distanceFadeStart: this.distanceFadeStart,
|
|
518
|
+
distanceFadeEnd: this.distanceFadeEnd,
|
|
519
|
+
// Billboard uniforms
|
|
520
|
+
billboardMode: this._getBillboardMode(mesh.material),
|
|
521
|
+
billboardCameraRight: this._billboardCameraRight,
|
|
522
|
+
billboardCameraUp: this._billboardCameraUp,
|
|
523
|
+
billboardCameraForward: this._billboardCameraForward,
|
|
524
|
+
// Per-material specular boost (0-1, default 0 = disabled)
|
|
525
|
+
specularBoost: mesh.material?.specularBoost ?? 0,
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
// Render
|
|
529
|
+
if (commandEncoder) {
|
|
530
|
+
pipeline.render({
|
|
531
|
+
commandEncoder,
|
|
532
|
+
passEncoder,
|
|
533
|
+
dontFinish: true,
|
|
534
|
+
instanceBuffer: batch.buffer?.gpuBuffer,
|
|
535
|
+
instanceCount: batch.instanceCount
|
|
536
|
+
})
|
|
537
|
+
} else {
|
|
538
|
+
const result = pipeline.render({
|
|
539
|
+
dontFinish: true,
|
|
540
|
+
instanceBuffer: batch.buffer?.gpuBuffer,
|
|
541
|
+
instanceCount: batch.instanceCount
|
|
542
|
+
})
|
|
543
|
+
commandEncoder = result.commandEncoder
|
|
544
|
+
passEncoder = result.passEncoder
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Legacy system: render individual meshes with progressive loading
|
|
550
|
+
// Meshes appear as their shaders compile - no blocking wait
|
|
551
|
+
let totalInstances = 0
|
|
552
|
+
|
|
553
|
+
if (meshes && Object.keys(meshes).length > 0) {
|
|
554
|
+
// Update frustum for legacy mesh culling (only if camera has required properties)
|
|
555
|
+
const canCull = camera.view && camera.proj && camera.position && camera.direction
|
|
556
|
+
if (canCull) {
|
|
557
|
+
const fovRadians = (camera.fov || 60) * (Math.PI / 180)
|
|
558
|
+
this.frustum.update(
|
|
559
|
+
camera.view,
|
|
560
|
+
camera.proj,
|
|
561
|
+
camera.position,
|
|
562
|
+
camera.direction,
|
|
563
|
+
fovRadians,
|
|
564
|
+
camera.aspect || (canvas.width / canvas.height),
|
|
565
|
+
camera.near || 0.05,
|
|
566
|
+
camera.far || 1000,
|
|
567
|
+
canvas.width,
|
|
568
|
+
canvas.height
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Reset culling stats
|
|
573
|
+
this.legacyCullingStats.total = 0
|
|
574
|
+
this.legacyCullingStats.rendered = 0
|
|
575
|
+
this.legacyCullingStats.culledByFrustum = 0
|
|
576
|
+
this.legacyCullingStats.culledByDistance = 0
|
|
577
|
+
this.legacyCullingStats.culledByOcclusion = 0
|
|
578
|
+
this.legacyCullingStats.skippedNoBsphere = 0
|
|
579
|
+
|
|
580
|
+
// Start pipeline creation for ALL meshes (non-blocking)
|
|
581
|
+
// This kicks off parallel shader compilation in the background
|
|
582
|
+
for (const name in meshes) {
|
|
583
|
+
const mesh = meshes[name]
|
|
584
|
+
if (!mesh || !mesh.geometry || !mesh.material) continue
|
|
585
|
+
this._startPipelineCreation(mesh)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Render only meshes with READY pipelines (others will appear next frame)
|
|
589
|
+
for (const name in meshes) {
|
|
590
|
+
const mesh = meshes[name]
|
|
591
|
+
const instanceCount = mesh.geometry?.instanceCount || 0
|
|
592
|
+
totalInstances += instanceCount
|
|
593
|
+
|
|
594
|
+
// Skip meshes with no instances
|
|
595
|
+
if (instanceCount === 0) continue
|
|
596
|
+
|
|
597
|
+
this.legacyCullingStats.total++
|
|
598
|
+
|
|
599
|
+
// Apply culling to legacy meshes (frustum, distance, occlusion)
|
|
600
|
+
const cullReason = this._shouldCullLegacyMesh(mesh, camera, canCull)
|
|
601
|
+
if (cullReason) {
|
|
602
|
+
if (cullReason === 'frustum') this.legacyCullingStats.culledByFrustum++
|
|
603
|
+
else if (cullReason === 'distance') this.legacyCullingStats.culledByDistance++
|
|
604
|
+
else if (cullReason === 'occlusion') this.legacyCullingStats.culledByOcclusion++
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Check if pipeline is ready (non-blocking)
|
|
609
|
+
const pipeline = this._getPipelineIfReady(mesh)
|
|
610
|
+
if (!pipeline) continue // Still compiling, skip for now
|
|
611
|
+
|
|
612
|
+
// Track warmup frames (pipeline just became ready)
|
|
613
|
+
if (pipeline._warmupFrames > 0) {
|
|
614
|
+
pipeline._warmupFrames--
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this.legacyCullingStats.rendered++
|
|
618
|
+
|
|
619
|
+
// Update skin animation using absolute time (skip if externally managed or already updated)
|
|
620
|
+
if (mesh.skin && mesh.hasSkin && !mesh.skin.externallyManaged && !updatedSkins.has(mesh.skin)) {
|
|
621
|
+
// Track animation start time per skin for absolute timing
|
|
622
|
+
if (mesh.skin._animStartTime === undefined) {
|
|
623
|
+
mesh.skin._animStartTime = globalAnimTime
|
|
624
|
+
}
|
|
625
|
+
const skinAnimTime = globalAnimTime - mesh.skin._animStartTime
|
|
626
|
+
mesh.skin.updateAtTime(skinAnimTime)
|
|
627
|
+
updatedSkins.add(mesh.skin)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Ensure pipeline geometry matches mesh geometry
|
|
631
|
+
if (pipeline.geometry !== mesh.geometry) {
|
|
632
|
+
pipeline.geometry = mesh.geometry
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Update bind group if skinned
|
|
636
|
+
if (mesh.hasSkin && mesh.skin) {
|
|
637
|
+
pipeline.updateBindGroupForSkin(mesh.skin)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Update geometry buffers
|
|
641
|
+
mesh.geometry.update()
|
|
642
|
+
|
|
643
|
+
// Set uniforms
|
|
644
|
+
const jitterFadeDist = this.settings?.rendering?.jitterFadeDistance ?? 30.0
|
|
645
|
+
// Get alpha hash settings - check mesh material first, then global setting
|
|
646
|
+
const meshAlphaHash = mesh.material?.alphaHash ?? mesh.alphaHash
|
|
647
|
+
const alphaHashEnabled = meshAlphaHash ?? this.settings?.rendering?.alphaHash ?? false
|
|
648
|
+
const alphaHashScale = mesh.material?.alphaHashScale ?? this.settings?.rendering?.alphaHashScale ?? 1.0
|
|
649
|
+
const luminanceToAlpha = mesh.material?.luminanceToAlpha ?? this.settings?.rendering?.luminanceToAlpha ?? false
|
|
650
|
+
|
|
651
|
+
pipeline.uniformValues.set({
|
|
652
|
+
viewMatrix: camera.view,
|
|
653
|
+
projectionMatrix: camera.proj,
|
|
654
|
+
prevViewProjMatrix: prevViewProjMatrix,
|
|
655
|
+
mipBias: mipBias,
|
|
656
|
+
skinEnabled: mesh.hasSkin ? 1.0 : 0.0,
|
|
657
|
+
numJoints: mesh.hasSkin && mesh.skin ? mesh.skin.numJoints : 0,
|
|
658
|
+
near: camera.near || 0.05,
|
|
659
|
+
far: camera.far || 1000,
|
|
660
|
+
jitterFadeDistance: jitterFadeDist,
|
|
661
|
+
jitterOffset: camera.jitterOffset || [0, 0],
|
|
662
|
+
screenSize: camera.screenSize || [canvas.width, canvas.height],
|
|
663
|
+
emissionFactor: emissionFactor,
|
|
664
|
+
clipPlaneY: this.clipPlaneY,
|
|
665
|
+
clipPlaneEnabled: this.clipPlaneEnabled ? 1.0 : 0.0,
|
|
666
|
+
clipPlaneDirection: this.clipPlaneDirection,
|
|
667
|
+
pixelRounding: this.settings?.rendering?.pixelRounding || 0.0,
|
|
668
|
+
pixelExpansion: this.settings?.rendering?.pixelExpansion ?? 0.05,
|
|
669
|
+
positionRounding: this.settings?.rendering?.positionRounding || 0.0,
|
|
670
|
+
alphaHashEnabled: alphaHashEnabled ? 1.0 : 0.0,
|
|
671
|
+
alphaHashScale: alphaHashScale,
|
|
672
|
+
luminanceToAlpha: luminanceToAlpha ? 1.0 : 0.0,
|
|
673
|
+
noiseSize: this.noiseSize,
|
|
674
|
+
// Always use static noise for alpha hash to avoid shimmer on cutout edges
|
|
675
|
+
noiseOffsetX: 0,
|
|
676
|
+
noiseOffsetY: 0,
|
|
677
|
+
cameraPosition: camera.position,
|
|
678
|
+
distanceFadeStart: this.distanceFadeStart,
|
|
679
|
+
distanceFadeEnd: this.distanceFadeEnd,
|
|
680
|
+
// Billboard uniforms
|
|
681
|
+
billboardMode: this._getBillboardMode(mesh.material),
|
|
682
|
+
billboardCameraRight: this._billboardCameraRight,
|
|
683
|
+
billboardCameraUp: this._billboardCameraUp,
|
|
684
|
+
billboardCameraForward: this._billboardCameraForward,
|
|
685
|
+
// Per-material specular boost (0-1, default 0 = disabled)
|
|
686
|
+
specularBoost: mesh.material?.specularBoost ?? 0,
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
// Render
|
|
690
|
+
if (commandEncoder) {
|
|
691
|
+
pipeline.render({ commandEncoder, passEncoder, dontFinish: true })
|
|
692
|
+
} else {
|
|
693
|
+
const result = pipeline.render({ dontFinish: true })
|
|
694
|
+
commandEncoder = result.commandEncoder
|
|
695
|
+
passEncoder = result.passEncoder
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Finish the pass
|
|
701
|
+
if (passEncoder && commandEncoder) {
|
|
702
|
+
passEncoder.end()
|
|
703
|
+
device.queue.submit([commandEncoder.finish()])
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async _resize(width, height) {
|
|
708
|
+
// Recreate GBuffer at new size
|
|
709
|
+
this.gbuffer = await GBuffer.create(this.engine, width, height)
|
|
710
|
+
|
|
711
|
+
// Clear pipeline caches (they reference old GBuffer)
|
|
712
|
+
this.pipelines.clear()
|
|
713
|
+
this.skinnedPipelines.clear()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
_destroy() {
|
|
717
|
+
this.pipelines.clear()
|
|
718
|
+
this.skinnedPipelines.clear()
|
|
719
|
+
this.gbuffer = null
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Get the GBuffer for use by subsequent passes
|
|
724
|
+
*/
|
|
725
|
+
getGBuffer() {
|
|
726
|
+
return this.gbuffer
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export { GBuffer, GBufferPass }
|