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,394 @@
|
|
|
1
|
+
import { loadGltfData, loadGltf } from "../gltf.js"
|
|
2
|
+
import { Geometry } from "../Geometry.js"
|
|
3
|
+
import { Material } from "../Material.js"
|
|
4
|
+
import { Mesh } from "../Mesh.js"
|
|
5
|
+
import { Texture } from "../Texture.js"
|
|
6
|
+
import { calculateBoundingSphere } from "../utils/BoundingSphere.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* AssetManager - Lazy loading and caching of assets
|
|
10
|
+
*
|
|
11
|
+
* Asset keys:
|
|
12
|
+
* - "path/to/model.glb" - Raw GLTF data (gltf object, ready state)
|
|
13
|
+
* - "path/to/model.glb|meshName" - Processed mesh (geometry, material, skin, bsphere)
|
|
14
|
+
*
|
|
15
|
+
* Assets structure:
|
|
16
|
+
* {
|
|
17
|
+
* "models/fox.glb": { gltf: {...}, meshNames: [...], ready: true, loading: false }
|
|
18
|
+
* "models/fox.glb|fox1": { geometry, material, skin, bsphere, ready: true }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
class AssetManager {
|
|
22
|
+
constructor(engine = null) {
|
|
23
|
+
this.engine = engine
|
|
24
|
+
|
|
25
|
+
// Asset storage
|
|
26
|
+
this.assets = {}
|
|
27
|
+
|
|
28
|
+
// Loading promises for deduplication
|
|
29
|
+
this._loadingPromises = {}
|
|
30
|
+
|
|
31
|
+
// Callbacks for when assets become ready
|
|
32
|
+
this._readyCallbacks = {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a ModelID into path and mesh name
|
|
37
|
+
* @param {string} modelId - Format: "path/to/model.glb|meshName"
|
|
38
|
+
* @returns {{ path: string, meshName: string|null }}
|
|
39
|
+
*/
|
|
40
|
+
parseModelId(modelId) {
|
|
41
|
+
const parts = modelId.split("|")
|
|
42
|
+
return {
|
|
43
|
+
path: parts[0],
|
|
44
|
+
meshName: parts[1] || null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a ModelID from path and mesh name
|
|
50
|
+
*/
|
|
51
|
+
createModelId(path, meshName) {
|
|
52
|
+
return `${path}|${meshName}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if an asset exists and is ready
|
|
57
|
+
*/
|
|
58
|
+
isReady(assetKey) {
|
|
59
|
+
return this.assets[assetKey]?.ready === true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if an asset is currently loading
|
|
64
|
+
*/
|
|
65
|
+
isLoading(assetKey) {
|
|
66
|
+
return this.assets[assetKey]?.loading === true || this._loadingPromises[assetKey] !== undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get an asset if ready, otherwise return null
|
|
71
|
+
*/
|
|
72
|
+
get(assetKey) {
|
|
73
|
+
const asset = this.assets[assetKey]
|
|
74
|
+
if (asset?.ready) {
|
|
75
|
+
return asset
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get or load a GLTF file
|
|
82
|
+
* @param {string} path - Path to the GLTF file
|
|
83
|
+
* @param {Object} options - Loading options
|
|
84
|
+
* @returns {Promise<Object>} The loaded GLTF asset
|
|
85
|
+
*/
|
|
86
|
+
async loadGltfFile(path, options = {}) {
|
|
87
|
+
// Check if already loaded
|
|
88
|
+
if (this.assets[path]?.ready) {
|
|
89
|
+
return this.assets[path]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if already loading (deduplicate)
|
|
93
|
+
if (this._loadingPromises[path]) {
|
|
94
|
+
return this._loadingPromises[path]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Mark as loading
|
|
98
|
+
this.assets[path] = { ready: false, loading: true }
|
|
99
|
+
|
|
100
|
+
// Create loading promise
|
|
101
|
+
this._loadingPromises[path] = (async () => {
|
|
102
|
+
try {
|
|
103
|
+
const result = await loadGltf(this.engine, path, options)
|
|
104
|
+
|
|
105
|
+
// Store the full result
|
|
106
|
+
const meshNames = Object.keys(result.meshes)
|
|
107
|
+
|
|
108
|
+
// For skinned models, compute a combined bounding sphere for ALL meshes
|
|
109
|
+
// This prevents individual submeshes from being culled independently
|
|
110
|
+
// Include all meshes (skinned and rigid parts) in the combined sphere
|
|
111
|
+
let combinedBsphere = null
|
|
112
|
+
const hasAnySkin = Object.values(result.meshes).some(m => m.hasSkin)
|
|
113
|
+
|
|
114
|
+
if (hasAnySkin) {
|
|
115
|
+
// Collect all vertex positions from ALL meshes (not just skinned ones)
|
|
116
|
+
// This ensures rigid parts attached to skinned models share the same bounds
|
|
117
|
+
const allPositions = []
|
|
118
|
+
for (const mesh of Object.values(result.meshes)) {
|
|
119
|
+
if (mesh.geometry?.attributes?.position) {
|
|
120
|
+
const positions = mesh.geometry.attributes.position
|
|
121
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
122
|
+
allPositions.push(positions[i], positions[i + 1], positions[i + 2])
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (allPositions.length > 0) {
|
|
128
|
+
combinedBsphere = calculateBoundingSphere(new Float32Array(allPositions))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Store the parent GLTF asset with combined bsphere for entities using parent path
|
|
133
|
+
this.assets[path] = {
|
|
134
|
+
gltf: result,
|
|
135
|
+
meshes: result.meshes,
|
|
136
|
+
skins: result.skins,
|
|
137
|
+
animations: result.animations,
|
|
138
|
+
nodes: result.nodes,
|
|
139
|
+
meshNames: meshNames,
|
|
140
|
+
bsphere: combinedBsphere, // Combined bsphere for parent path entities
|
|
141
|
+
hasSkin: hasAnySkin, // Flag for skinned model detection
|
|
142
|
+
ready: true,
|
|
143
|
+
loading: false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Auto-register individual meshes
|
|
147
|
+
for (const meshName of meshNames) {
|
|
148
|
+
const mesh = result.meshes[meshName]
|
|
149
|
+
// Use combined bsphere for ALL meshes when model has any skinning
|
|
150
|
+
// This ensures all submeshes are culled together as a unit
|
|
151
|
+
const bsphere = (hasAnySkin && combinedBsphere) ? combinedBsphere : null
|
|
152
|
+
await this._registerMesh(path, meshName, mesh, bsphere)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Trigger ready callbacks
|
|
156
|
+
this._triggerReady(path)
|
|
157
|
+
|
|
158
|
+
return this.assets[path]
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`Failed to load GLTF: ${path}`, error)
|
|
161
|
+
this.assets[path] = { ready: false, loading: false, error: error.message }
|
|
162
|
+
throw error
|
|
163
|
+
} finally {
|
|
164
|
+
delete this._loadingPromises[path]
|
|
165
|
+
}
|
|
166
|
+
})()
|
|
167
|
+
|
|
168
|
+
return this._loadingPromises[path]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Register a mesh asset (internal)
|
|
173
|
+
* @param {string} path - GLTF file path
|
|
174
|
+
* @param {string} meshName - Mesh name
|
|
175
|
+
* @param {Object} mesh - Mesh object
|
|
176
|
+
* @param {Object|null} overrideBsphere - Optional bounding sphere (for skinned mesh combined sphere)
|
|
177
|
+
*/
|
|
178
|
+
async _registerMesh(path, meshName, mesh, overrideBsphere = null) {
|
|
179
|
+
const modelId = this.createModelId(path, meshName)
|
|
180
|
+
|
|
181
|
+
// Use override bsphere if provided, otherwise calculate from geometry
|
|
182
|
+
const bsphere = overrideBsphere || calculateBoundingSphere(mesh.geometry.attributes.position)
|
|
183
|
+
|
|
184
|
+
this.assets[modelId] = {
|
|
185
|
+
mesh: mesh,
|
|
186
|
+
geometry: mesh.geometry,
|
|
187
|
+
material: mesh.material,
|
|
188
|
+
skin: mesh.skin || null,
|
|
189
|
+
hasSkin: mesh.hasSkin || false,
|
|
190
|
+
bsphere: bsphere,
|
|
191
|
+
ready: true,
|
|
192
|
+
loading: false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Trigger ready callbacks
|
|
196
|
+
this._triggerReady(modelId)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get or load a specific mesh from a GLTF file
|
|
201
|
+
* @param {string} modelId - Format: "path/to/model.glb|meshName"
|
|
202
|
+
* @param {Object} options - Loading options
|
|
203
|
+
* @returns {Promise<Object>} The mesh asset
|
|
204
|
+
*/
|
|
205
|
+
async loadMesh(modelId, options = {}) {
|
|
206
|
+
const { path, meshName } = this.parseModelId(modelId)
|
|
207
|
+
|
|
208
|
+
// If mesh is already loaded, return it
|
|
209
|
+
if (this.assets[modelId]?.ready) {
|
|
210
|
+
return this.assets[modelId]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Load the GLTF file first (will auto-register meshes)
|
|
214
|
+
await this.loadGltfFile(path, options)
|
|
215
|
+
|
|
216
|
+
// Now get the mesh
|
|
217
|
+
const meshAsset = this.assets[modelId]
|
|
218
|
+
if (!meshAsset) {
|
|
219
|
+
throw new Error(`Mesh "${meshName}" not found in "${path}"`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return meshAsset
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Preload multiple assets
|
|
227
|
+
* @param {string[]} assetKeys - List of asset keys to preload
|
|
228
|
+
* @param {Object} options - Loading options
|
|
229
|
+
* @returns {Promise<void>}
|
|
230
|
+
*/
|
|
231
|
+
async preload(assetKeys, options = {}) {
|
|
232
|
+
const promises = assetKeys.map(key => {
|
|
233
|
+
const { path, meshName } = this.parseModelId(key)
|
|
234
|
+
if (meshName) {
|
|
235
|
+
return this.loadMesh(key, options)
|
|
236
|
+
} else {
|
|
237
|
+
return this.loadGltfFile(key, options)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
await Promise.all(promises)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Register a callback for when an asset becomes ready
|
|
246
|
+
*/
|
|
247
|
+
onReady(assetKey, callback) {
|
|
248
|
+
// If already ready, call immediately
|
|
249
|
+
if (this.assets[assetKey]?.ready) {
|
|
250
|
+
callback(this.assets[assetKey])
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Register callback
|
|
255
|
+
if (!this._readyCallbacks[assetKey]) {
|
|
256
|
+
this._readyCallbacks[assetKey] = []
|
|
257
|
+
}
|
|
258
|
+
this._readyCallbacks[assetKey].push(callback)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Trigger ready callbacks
|
|
263
|
+
*/
|
|
264
|
+
_triggerReady(assetKey) {
|
|
265
|
+
const callbacks = this._readyCallbacks[assetKey]
|
|
266
|
+
if (callbacks) {
|
|
267
|
+
for (const callback of callbacks) {
|
|
268
|
+
callback(this.assets[assetKey])
|
|
269
|
+
}
|
|
270
|
+
delete this._readyCallbacks[assetKey]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get all loaded mesh names for a GLTF file
|
|
276
|
+
*/
|
|
277
|
+
getMeshNames(path) {
|
|
278
|
+
const asset = this.assets[path]
|
|
279
|
+
return asset?.meshNames || []
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get all unique GLTF paths currently loaded
|
|
284
|
+
*/
|
|
285
|
+
getLoadedPaths() {
|
|
286
|
+
return Object.keys(this.assets).filter(key => !key.includes("|") && this.assets[key].ready)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get all ModelIDs currently loaded
|
|
291
|
+
*/
|
|
292
|
+
getLoadedModelIds() {
|
|
293
|
+
return Object.keys(this.assets).filter(key => key.includes("|") && this.assets[key].ready)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get bounding sphere for a model
|
|
298
|
+
*/
|
|
299
|
+
getBoundingSphere(modelId) {
|
|
300
|
+
const asset = this.assets[modelId]
|
|
301
|
+
return asset?.bsphere || null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Create a clone of a mesh (shares geometry/material, but separate instance buffers)
|
|
306
|
+
*/
|
|
307
|
+
cloneMesh(modelId) {
|
|
308
|
+
const asset = this.assets[modelId]
|
|
309
|
+
if (!asset?.ready) {
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Create a new Mesh with the same geometry and material
|
|
314
|
+
const clone = new Mesh(asset.geometry, asset.material)
|
|
315
|
+
if (asset.skin) {
|
|
316
|
+
clone.skin = asset.skin
|
|
317
|
+
clone.hasSkin = true
|
|
318
|
+
}
|
|
319
|
+
return clone
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Unload an asset to free memory
|
|
324
|
+
*/
|
|
325
|
+
unload(assetKey) {
|
|
326
|
+
const asset = this.assets[assetKey]
|
|
327
|
+
if (!asset) return
|
|
328
|
+
|
|
329
|
+
// If this is a GLTF file, also unload all its meshes
|
|
330
|
+
if (asset.meshNames) {
|
|
331
|
+
for (const meshName of asset.meshNames) {
|
|
332
|
+
const modelId = this.createModelId(assetKey, meshName)
|
|
333
|
+
delete this.assets[modelId]
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
delete this.assets[assetKey]
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Clear all assets
|
|
342
|
+
*/
|
|
343
|
+
clear() {
|
|
344
|
+
this.assets = {}
|
|
345
|
+
this._loadingPromises = {}
|
|
346
|
+
this._readyCallbacks = {}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get loading status for all assets
|
|
351
|
+
*/
|
|
352
|
+
getStatus() {
|
|
353
|
+
const ready = []
|
|
354
|
+
const loading = []
|
|
355
|
+
const failed = []
|
|
356
|
+
|
|
357
|
+
for (const key in this.assets) {
|
|
358
|
+
const asset = this.assets[key]
|
|
359
|
+
if (asset.ready) {
|
|
360
|
+
ready.push(key)
|
|
361
|
+
} else if (asset.loading) {
|
|
362
|
+
loading.push(key)
|
|
363
|
+
} else if (asset.error) {
|
|
364
|
+
failed.push({ key, error: asset.error })
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { ready, loading, failed }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Register a manually created mesh (for procedural geometry)
|
|
373
|
+
*/
|
|
374
|
+
registerMesh(modelId, mesh, bsphere = null) {
|
|
375
|
+
if (!bsphere) {
|
|
376
|
+
bsphere = calculateBoundingSphere(mesh.geometry.attributes.position)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.assets[modelId] = {
|
|
380
|
+
mesh: mesh,
|
|
381
|
+
geometry: mesh.geometry,
|
|
382
|
+
material: mesh.material,
|
|
383
|
+
skin: mesh.skin || null,
|
|
384
|
+
hasSkin: mesh.hasSkin || false,
|
|
385
|
+
bsphere: bsphere,
|
|
386
|
+
ready: true,
|
|
387
|
+
loading: false
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this._triggerReady(modelId)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export { AssetManager }
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Frustum } from "../utils/Frustum.js"
|
|
2
|
+
import { transformBoundingSphere } from "../utils/BoundingSphere.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CullingSystem - Manages visibility culling for entities
|
|
6
|
+
*
|
|
7
|
+
* Performs cone-based frustum culling, distance filtering,
|
|
8
|
+
* and HiZ occlusion culling with configurable limits per pass type.
|
|
9
|
+
*/
|
|
10
|
+
class CullingSystem {
|
|
11
|
+
constructor(engine = null) {
|
|
12
|
+
// Reference to engine for settings access
|
|
13
|
+
this.engine = engine
|
|
14
|
+
|
|
15
|
+
// Frustum for culling
|
|
16
|
+
this.frustum = new Frustum()
|
|
17
|
+
|
|
18
|
+
// HiZ pass reference for occlusion culling
|
|
19
|
+
this.hizPass = null
|
|
20
|
+
|
|
21
|
+
// Current camera data for HiZ testing
|
|
22
|
+
this._viewProj = null
|
|
23
|
+
this._near = 0.05
|
|
24
|
+
this._far = 1000
|
|
25
|
+
this._cameraPos = null
|
|
26
|
+
|
|
27
|
+
// Stats for occlusion culling
|
|
28
|
+
this._occlusionStats = {
|
|
29
|
+
tested: 0,
|
|
30
|
+
culled: 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Cached visible entity lists per pass
|
|
34
|
+
this._visibleCache = {
|
|
35
|
+
shadow: null,
|
|
36
|
+
reflection: null,
|
|
37
|
+
planarReflection: null,
|
|
38
|
+
main: null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Frame counter for cache invalidation
|
|
42
|
+
this._frameId = 0
|
|
43
|
+
this._cacheFrameId = -1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Set the HiZ pass for occlusion culling
|
|
48
|
+
* @param {HiZPass} hizPass - The HiZ pass instance
|
|
49
|
+
*/
|
|
50
|
+
setHiZPass(hizPass) {
|
|
51
|
+
this.hizPass = hizPass
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Culling config is now a getter that reads from engine.settings.culling
|
|
55
|
+
get config() {
|
|
56
|
+
return this.engine.settings.culling
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update frustum from camera
|
|
61
|
+
* @param {Camera} camera - Camera object
|
|
62
|
+
* @param {number} screenWidth - Screen width in pixels
|
|
63
|
+
* @param {number} screenHeight - Screen height in pixels
|
|
64
|
+
*/
|
|
65
|
+
updateFrustum(camera, screenWidth, screenHeight) {
|
|
66
|
+
// Camera uses: view, proj, fov (degrees), aspect, near, far, position, direction
|
|
67
|
+
const fovRadians = camera.fov * (Math.PI / 180)
|
|
68
|
+
this.frustum.update(
|
|
69
|
+
camera.view,
|
|
70
|
+
camera.proj,
|
|
71
|
+
camera.position,
|
|
72
|
+
camera.direction,
|
|
73
|
+
fovRadians,
|
|
74
|
+
camera.aspect,
|
|
75
|
+
camera.near,
|
|
76
|
+
camera.far,
|
|
77
|
+
screenWidth,
|
|
78
|
+
screenHeight
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Store camera data for HiZ testing
|
|
82
|
+
// Copy position to avoid issues with mutable references
|
|
83
|
+
this._viewProj = camera.viewProj
|
|
84
|
+
this._near = camera.near
|
|
85
|
+
this._far = camera.far
|
|
86
|
+
this._cameraPos = [camera.position[0], camera.position[1], camera.position[2]]
|
|
87
|
+
|
|
88
|
+
// Reset occlusion stats
|
|
89
|
+
this._occlusionStats.tested = 0
|
|
90
|
+
this._occlusionStats.culled = 0
|
|
91
|
+
|
|
92
|
+
this._frameId++
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Set culling configuration for a pass type
|
|
97
|
+
* @param {string} passType - 'shadow', 'reflection', or 'main'
|
|
98
|
+
* @param {Object} config - { maxDistance, maxSkinned }
|
|
99
|
+
*/
|
|
100
|
+
setConfig(passType, config) {
|
|
101
|
+
const cullingConfig = this.engine?.settings?.culling
|
|
102
|
+
if (cullingConfig && cullingConfig[passType]) {
|
|
103
|
+
Object.assign(cullingConfig[passType], config)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Cull entities for a specific pass type
|
|
109
|
+
*
|
|
110
|
+
* @param {EntityManager} entityManager - Entity manager
|
|
111
|
+
* @param {AssetManager} assetManager - Asset manager
|
|
112
|
+
* @param {string} passType - 'shadow', 'reflection', or 'main'
|
|
113
|
+
* @returns {{ visible: Array, skinnedCount: number }}
|
|
114
|
+
*/
|
|
115
|
+
cull(entityManager, assetManager, passType = 'main') {
|
|
116
|
+
const config = this.config[passType] || this.config.main
|
|
117
|
+
const visible = []
|
|
118
|
+
let skinnedCount = 0
|
|
119
|
+
let skippedNoVisible = 0
|
|
120
|
+
let skippedNoModel = 0
|
|
121
|
+
|
|
122
|
+
entityManager.forEach((id, entity) => {
|
|
123
|
+
// Skip if not visible
|
|
124
|
+
if (!entity._visible) {
|
|
125
|
+
skippedNoVisible++
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip if no model
|
|
130
|
+
if (!entity.model) {
|
|
131
|
+
skippedNoModel++
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Get bounding sphere from asset and transform by entity matrix
|
|
136
|
+
const asset = assetManager.get(entity.model)
|
|
137
|
+
let bsphere
|
|
138
|
+
|
|
139
|
+
if (asset?.bsphere) {
|
|
140
|
+
// Transform asset's bsphere by entity's current matrix
|
|
141
|
+
// Note: For skinned models, bsphere is pre-computed as combined sphere of all submeshes
|
|
142
|
+
bsphere = transformBoundingSphere(asset.bsphere, entity._matrix)
|
|
143
|
+
// Cache it on entity for other uses
|
|
144
|
+
entity._bsphere = bsphere
|
|
145
|
+
} else if (entity._bsphere && entity._bsphere.radius > 0) {
|
|
146
|
+
// Use existing bsphere if available
|
|
147
|
+
bsphere = entity._bsphere
|
|
148
|
+
} else {
|
|
149
|
+
// No bsphere available, include by default
|
|
150
|
+
visible.push({ id, entity, distance: 0 })
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if culling is enabled
|
|
155
|
+
const globalCullingEnabled = this.engine?.settings?.culling?.frustumEnabled !== false
|
|
156
|
+
const passFrustumEnabled = config.frustum !== false
|
|
157
|
+
|
|
158
|
+
// For planar reflection, mirror the bounding sphere across the ground level
|
|
159
|
+
// This ensures we cull based on where the object appears in the reflection
|
|
160
|
+
let cullBsphere = bsphere
|
|
161
|
+
if (passType === 'planarReflection') {
|
|
162
|
+
const groundLevel = this.engine?.settings?.planarReflection?.groundLevel ?? 0
|
|
163
|
+
// Mirror Y position: mirroredY = 2 * groundLevel - originalY
|
|
164
|
+
cullBsphere = {
|
|
165
|
+
center: [
|
|
166
|
+
bsphere.center[0],
|
|
167
|
+
2 * groundLevel - bsphere.center[1],
|
|
168
|
+
bsphere.center[2]
|
|
169
|
+
],
|
|
170
|
+
radius: bsphere.radius
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Distance test (always apply when global culling is enabled)
|
|
175
|
+
const distance = this.frustum.getDistance(cullBsphere)
|
|
176
|
+
if (globalCullingEnabled && distance - cullBsphere.radius > config.maxDistance) {
|
|
177
|
+
return // Too far
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Pixel size test (always apply when global culling is enabled)
|
|
181
|
+
if (globalCullingEnabled && config.minPixelSize > 0) {
|
|
182
|
+
const projectedSize = this.frustum.getProjectedSize(cullBsphere, distance)
|
|
183
|
+
if (projectedSize < config.minPixelSize) {
|
|
184
|
+
return // Too small to see
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Frustum test (only when both global AND per-pass frustum culling is enabled)
|
|
189
|
+
if (globalCullingEnabled && passFrustumEnabled && !this.frustum.testSpherePlanes(cullBsphere)) {
|
|
190
|
+
return // Outside frustum
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// HiZ occlusion culling for entities
|
|
194
|
+
if (passType === 'main' && this.hizPass && this._viewProj && this._cameraPos) {
|
|
195
|
+
const occlusionEnabled = this.engine?.settings?.occlusionCulling?.enabled
|
|
196
|
+
if (occlusionEnabled) {
|
|
197
|
+
this._occlusionStats.tested++
|
|
198
|
+
if (this.hizPass.testSphereOcclusion(bsphere, this._viewProj, this._near, this._far, this._cameraPos)) {
|
|
199
|
+
this._occlusionStats.culled++
|
|
200
|
+
return // Occluded by previous frame's geometry
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check skinned limit (asset already fetched above)
|
|
206
|
+
const isSkinned = asset?.hasSkin === true
|
|
207
|
+
|
|
208
|
+
if (isSkinned) {
|
|
209
|
+
if (skinnedCount >= config.maxSkinned) {
|
|
210
|
+
return // Too many skinned already
|
|
211
|
+
}
|
|
212
|
+
skinnedCount++
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
visible.push({
|
|
216
|
+
id,
|
|
217
|
+
entity,
|
|
218
|
+
distance,
|
|
219
|
+
isSkinned
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Sort by distance for front-to-back rendering (reduces overdraw)
|
|
224
|
+
visible.sort((a, b) => a.distance - b.distance)
|
|
225
|
+
|
|
226
|
+
return { visible, skinnedCount }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Group visible entities by model for instancing
|
|
231
|
+
*
|
|
232
|
+
* @param {Array} visibleEntities - Array from cull()
|
|
233
|
+
* @returns {Map<string, Array>} Map of modelId -> entities
|
|
234
|
+
*/
|
|
235
|
+
groupByModel(visibleEntities) {
|
|
236
|
+
const groups = new Map()
|
|
237
|
+
|
|
238
|
+
for (const item of visibleEntities) {
|
|
239
|
+
const modelId = item.entity.model
|
|
240
|
+
if (!groups.has(modelId)) {
|
|
241
|
+
groups.set(modelId, [])
|
|
242
|
+
}
|
|
243
|
+
groups.get(modelId).push(item)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return groups
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Group visible entities by model and animation for skinned meshes
|
|
251
|
+
* Entities with same animation can potentially share animation state
|
|
252
|
+
*
|
|
253
|
+
* @param {Array} visibleEntities - Array from cull()
|
|
254
|
+
* @param {number} phaseQuantization - Quantize phase to this step (default 0.05 = 20 groups per animation)
|
|
255
|
+
* @returns {Map<string, Array>} Map of "modelId|animation|quantizedPhase" -> entities
|
|
256
|
+
*/
|
|
257
|
+
groupByModelAndAnimation(visibleEntities, phaseQuantization = 0.05) {
|
|
258
|
+
const groups = new Map()
|
|
259
|
+
|
|
260
|
+
for (const item of visibleEntities) {
|
|
261
|
+
const entity = item.entity
|
|
262
|
+
let key = entity.model
|
|
263
|
+
|
|
264
|
+
if (item.isSkinned && entity.animation) {
|
|
265
|
+
const quantizedPhase = Math.floor(entity.phase / phaseQuantization) * phaseQuantization
|
|
266
|
+
key = `${entity.model}|${entity.animation}|${quantizedPhase.toFixed(2)}`
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!groups.has(key)) {
|
|
270
|
+
groups.set(key, [])
|
|
271
|
+
}
|
|
272
|
+
groups.get(key).push(item)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return groups
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get statistics about culling
|
|
280
|
+
*/
|
|
281
|
+
getStats(entityManager, assetManager) {
|
|
282
|
+
const total = entityManager.count
|
|
283
|
+
const { visible } = this.cull(entityManager, assetManager, 'main')
|
|
284
|
+
const culled = total - visible.length
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
total,
|
|
288
|
+
visible: visible.length,
|
|
289
|
+
culled,
|
|
290
|
+
cullPercent: total > 0 ? ((culled / total) * 100).toFixed(1) : 0
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get occlusion culling statistics
|
|
296
|
+
*/
|
|
297
|
+
getOcclusionStats() {
|
|
298
|
+
return {
|
|
299
|
+
tested: this._occlusionStats.tested,
|
|
300
|
+
culled: this._occlusionStats.culled,
|
|
301
|
+
cullPercent: this._occlusionStats.tested > 0
|
|
302
|
+
? ((this._occlusionStats.culled / this._occlusionStats.tested) * 100).toFixed(1)
|
|
303
|
+
: 0
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export { CullingSystem }
|