lecodes-gl-lite 2.0.0
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/README.md +27 -0
- package/package.json +33 -0
- package/src/assets/decoders/README.md +13 -0
- package/src/assets/decoders/basis_transcoder.js +19 -0
- package/src/assets/decoders/basis_transcoder.wasm +0 -0
- package/src/assets/decoders/draco_decoder.wasm +0 -0
- package/src/assets/decoders/draco_wasm_wrapper.js +117 -0
- package/src/assets/neutral_ibl128.ktx +0 -0
- package/src/core/animBridge.ts +51 -0
- package/src/core/boneNames.ts +22 -0
- package/src/core/frame.ts +230 -0
- package/src/core/glbStructure.ts +100 -0
- package/src/core/headlessBridge.ts +92 -0
- package/src/core/mapView.ts +195 -0
- package/src/core/math3d.ts +275 -0
- package/src/core/particleBridge.ts +154 -0
- package/src/core/physicsBridge.ts +305 -0
- package/src/core/recording.ts +59 -0
- package/src/core/sceneCore.ts +833 -0
- package/src/core/serialize.ts +144 -0
- package/src/gen/unsupported.gen.ts +212 -0
- package/src/index.ts +5 -0
- package/src/vendor/creator-anim/dist/creatorAnim.d.ts +123 -0
- package/src/vendor/creator-anim/dist/creatorAnim.js +14 -0
- package/src/vendor/creator-anim/js/index.ts +656 -0
- package/src/vendor/creator-nav/dist/creatorNav.d.ts +56 -0
- package/src/vendor/creator-nav/dist/creatorNav.js +14 -0
- package/src/vendor/creator-nav/js/index.ts +400 -0
- package/src/vendor/creator-particles/dist/creatorParticles.d.ts +43 -0
- package/src/vendor/creator-particles/dist/creatorParticles.js +14 -0
- package/src/vendor/creator-particles/js/index.ts +137 -0
- package/src/vendor/creator-physics/dist/creatorPhysics.d.ts +107 -0
- package/src/vendor/creator-physics/dist/creatorPhysics.js +14 -0
- package/src/vendor/creator-physics/js/index.ts +515 -0
- package/src/vite-env.d.ts +1 -0
- package/src/webgl/batching.ts +453 -0
- package/src/webgl/createViewerLite.ts +1627 -0
- package/src/webgl/customMaterial.ts +228 -0
- package/src/webgl/decoders/basis.ts +123 -0
- package/src/webgl/decoders/draco.ts +110 -0
- package/src/webgl/decoders/index.ts +68 -0
- package/src/webgl/decoders/worker.ts +45 -0
- package/src/webgl/editor.ts +1297 -0
- package/src/webgl/glb.ts +593 -0
- package/src/webgl/ibl.ts +137 -0
- package/src/webgl/shaders.ts +779 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// Instanced batching for the lite engine's scene passes.
|
|
2
|
+
//
|
|
3
|
+
// A level built from a modular kit is thousands of nodes over a few hundred distinct meshes
|
|
4
|
+
// (Twig & Twine's map: 9 441 prefab placements of 152 GLBs → ~57 k drawables). Drawn one OGL Mesh at a
|
|
5
|
+
// time that is 57 k draw calls, each re-uploading every uniform of its program — the frame was
|
|
6
|
+
// 250 ms+ before a single triangle rasterized. Here every drawable that shares (geometry, program,
|
|
7
|
+
// mode, render order, shadow flags, winding) joins ONE batch and the batch draws with
|
|
8
|
+
// `drawElementsInstanced`, the per-instance world matrix coming from an `instanceMatrix` vertex
|
|
9
|
+
// attribute (shaders.ts: the `INSTANCED` define swaps `modelMatrix` for it). The per-node Mesh
|
|
10
|
+
// objects stay exactly as they were — picking, the selection outline, wireframe companions, bounds
|
|
11
|
+
// and the editor all keep reading them — they just stop being drawn one by one.
|
|
12
|
+
//
|
|
13
|
+
// `collect()` walks the OGL graph only when the host says the graph changed (a `graphVersion` the
|
|
14
|
+
// engine bumps on every matrix / parent / visibility / drawable mutation — every one of them goes
|
|
15
|
+
// through the core's backend hooks): the walk fills one instance stream per batch, `all` (every
|
|
16
|
+
// hierarchy-visible instance — the shadow casters), plus the instances' world bounding spheres
|
|
17
|
+
// and the list of singles. Every frame the camera moved, a flat pass over those cached spheres
|
|
18
|
+
// (no graph traversal — 57 k nodes are ~2 ms this way, ~40 ms as a recursive walk) fills `vis`,
|
|
19
|
+
// the camera-visible subset the main pass draws, and culls the singles. A static scene with a
|
|
20
|
+
// still camera does nothing at all. What can't batch — transparent programs (their draw order is
|
|
21
|
+
// depth-sorted per object), skinned meshes (per-mesh bone textures), morph targets (per-node
|
|
22
|
+
// weights), points, particle streams, programs whose vertex shader has no INSTANCED path — is a
|
|
23
|
+
// `single` and draws the way OGL's renderer draws it, in OGL's sort order.
|
|
24
|
+
|
|
25
|
+
import { Camera, Geometry, GLTFSkin, Mesh, Program, Renderer, Transform, type OGLRenderingContext } from 'ogl'
|
|
26
|
+
import { transformPoint } from '../core/math3d'
|
|
27
|
+
|
|
28
|
+
type GL = OGLRenderingContext & WebGL2RenderingContext
|
|
29
|
+
|
|
30
|
+
type Batch = {
|
|
31
|
+
key: string
|
|
32
|
+
source: Geometry
|
|
33
|
+
program: Program
|
|
34
|
+
/** The instanced twin of `program` (same uniforms object, INSTANCED vertex path). */
|
|
35
|
+
twin: Program
|
|
36
|
+
mode: number
|
|
37
|
+
renderOrder: number
|
|
38
|
+
castShadows: boolean
|
|
39
|
+
receiveShadows: boolean
|
|
40
|
+
/** Mirrored instances (negative world determinant) draw with the front face flipped. */
|
|
41
|
+
flip: boolean
|
|
42
|
+
all: Float32Array
|
|
43
|
+
allCount: number
|
|
44
|
+
allDirty: boolean
|
|
45
|
+
geoAll: Geometry
|
|
46
|
+
/** Per `all` instance: world bounding sphere (cx, cy, cz, r) — the cull pass reads these. */
|
|
47
|
+
sph: Float32Array
|
|
48
|
+
vis: Float32Array
|
|
49
|
+
visCount: number
|
|
50
|
+
visDirty: boolean
|
|
51
|
+
geoVis: Geometry
|
|
52
|
+
/** World AABB of `all` (min xyz, max xyz) — recomputed when `all` changes. */
|
|
53
|
+
bounds: Float32Array
|
|
54
|
+
lastUsed: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ZERO = { value: 0 }
|
|
58
|
+
const INITIAL_CAPACITY = 16
|
|
59
|
+
/** Frames a batch may sit empty before its GPU buffers go. */
|
|
60
|
+
const IDLE_FRAMES = 600
|
|
61
|
+
|
|
62
|
+
const maxScaleOnAxis = (m: ArrayLike<number>): number => Math.sqrt(Math.max(
|
|
63
|
+
m[0] * m[0] + m[1] * m[1] + m[2] * m[2],
|
|
64
|
+
m[4] * m[4] + m[5] * m[5] + m[6] * m[6],
|
|
65
|
+
m[8] * m[8] + m[9] * m[9] + m[10] * m[10],
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
const determinant3 = (m: ArrayLike<number>): number =>
|
|
69
|
+
m[0] * (m[5] * m[10] - m[6] * m[9]) - m[1] * (m[4] * m[10] - m[6] * m[8]) + m[2] * (m[4] * m[9] - m[5] * m[8])
|
|
70
|
+
|
|
71
|
+
export type Batcher = ReturnType<typeof createBatcher>
|
|
72
|
+
|
|
73
|
+
export const createBatcher = (gl: GL, renderer: Renderer) => {
|
|
74
|
+
const batches = new Map<string, Batch>()
|
|
75
|
+
/** This frame's camera-visible singles (the main pass). */
|
|
76
|
+
const singles: Mesh[] = []
|
|
77
|
+
/** Every hierarchy-visible single (the walk's output; the cull pass filters it into `singles`). */
|
|
78
|
+
const singlesAll: Mesh[] = []
|
|
79
|
+
/** The shadow-casting subset of `singlesAll`. */
|
|
80
|
+
const casterSingles: Mesh[] = []
|
|
81
|
+
let lastGraph = -1
|
|
82
|
+
const lastPV = new Float32Array(16)
|
|
83
|
+
let lastCull = true
|
|
84
|
+
/** Off = every drawable is a single (the per-mesh path, for A/B checks and debugging). */
|
|
85
|
+
let enabled = true
|
|
86
|
+
/** Source program → its instanced twin (null = the vertex shader has no INSTANCED path). */
|
|
87
|
+
const twins = new Map<Program, Program | null>()
|
|
88
|
+
let frame = 0
|
|
89
|
+
|
|
90
|
+
// ---- instanced program twins ----------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const twinFor = (src: Program): Program | null => {
|
|
93
|
+
const known = twins.get(src)
|
|
94
|
+
if (known !== undefined) return known
|
|
95
|
+
let twin: Program | null = null
|
|
96
|
+
const vs = gl.getShaderSource(src.vertexShader)
|
|
97
|
+
const fs = gl.getShaderSource(src.fragmentShader)
|
|
98
|
+
if (vs && fs && vs.startsWith('#version')) {
|
|
99
|
+
// the define goes right after the #version line (the only thing allowed before it)
|
|
100
|
+
const vertex = vs.replace(/^([^\n]*\n)/, '$1#define INSTANCED\n')
|
|
101
|
+
const t = new Program(gl, {
|
|
102
|
+
vertex, fragment: fs, uniforms: src.uniforms,
|
|
103
|
+
transparent: src.transparent, cullFace: src.cullFace as any, frontFace: src.frontFace,
|
|
104
|
+
depthTest: src.depthTest, depthWrite: src.depthWrite, depthFunc: src.depthFunc,
|
|
105
|
+
})
|
|
106
|
+
let hasInstanceAttr = false
|
|
107
|
+
t.attributeLocations.forEach((_loc: number, info: { name: string }) => { if (info.name === 'instanceMatrix') hasInstanceAttr = true })
|
|
108
|
+
if (hasInstanceAttr && gl.getProgramParameter(t.program, gl.LINK_STATUS)) twin = t
|
|
109
|
+
else t.remove()
|
|
110
|
+
}
|
|
111
|
+
twins.set(src, twin)
|
|
112
|
+
return twin
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The per-draw GL state lives on the source program (the SDK mutates it: depth flags, cull
|
|
116
|
+
* mode, blend) — mirror it onto the twin right before the batch draws. */
|
|
117
|
+
const syncState = (twin: Program, src: Program): void => {
|
|
118
|
+
twin.transparent = src.transparent
|
|
119
|
+
twin.cullFace = src.cullFace
|
|
120
|
+
twin.frontFace = src.frontFace
|
|
121
|
+
twin.depthTest = src.depthTest
|
|
122
|
+
twin.depthWrite = src.depthWrite
|
|
123
|
+
twin.depthFunc = src.depthFunc
|
|
124
|
+
twin.blendFunc = src.blendFunc
|
|
125
|
+
twin.blendEquation = src.blendEquation
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- geometry twins (shared vertex buffers + an instanceMatrix stream) -------------------------
|
|
129
|
+
|
|
130
|
+
const makeInstanceGeometry = (source: Geometry, capacity: number): Geometry => {
|
|
131
|
+
const attrs: Record<string, unknown> = {}
|
|
132
|
+
for (const [ name, a ] of Object.entries<any>(source.attributes)) {
|
|
133
|
+
attrs[name] = {
|
|
134
|
+
size: a.size, data: a.data, type: a.type, normalized: a.normalized,
|
|
135
|
+
stride: a.stride, offset: a.offset, buffer: a.buffer, count: a.count,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
attrs.instanceMatrix = { instanced: 1, size: 16, data: new Float32Array(capacity * 16), usage: gl.DYNAMIC_DRAW }
|
|
139
|
+
const g = new Geometry(gl, attrs as any)
|
|
140
|
+
g.drawRange.start = source.drawRange.start
|
|
141
|
+
g.drawRange.count = source.drawRange.count
|
|
142
|
+
return g
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const disposeInstanceGeometry = (g: Geometry): void => {
|
|
146
|
+
// ONLY the instance stream and the VAOs are ours — every other buffer belongs to the source
|
|
147
|
+
for (const vao of Object.values(g.VAOs)) renderer.deleteVertexArray(vao as WebGLVertexArrayObject)
|
|
148
|
+
g.VAOs = {}
|
|
149
|
+
const attr = (g.attributes as any).instanceMatrix
|
|
150
|
+
if (attr?.buffer) { gl.deleteBuffer(attr.buffer); attr.buffer = null }
|
|
151
|
+
;(renderer as any).currentGeometry = null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Grow the instance stream to hold `needed` matrices (the VAOs re-bind on the next draw). */
|
|
155
|
+
const growInstanceGeometry = (g: Geometry, needed: number): Float32Array => {
|
|
156
|
+
const attr = (g.attributes as any).instanceMatrix
|
|
157
|
+
let capacity = Math.max(INITIAL_CAPACITY, attr.count)
|
|
158
|
+
while (capacity < needed) capacity *= 2
|
|
159
|
+
const data = new Float32Array(capacity * 16)
|
|
160
|
+
data.set(attr.data)
|
|
161
|
+
disposeInstanceGeometry(g)
|
|
162
|
+
g.instancedCount = 0
|
|
163
|
+
g.addAttribute('instanceMatrix', { instanced: 1, size: 16, data, usage: gl.DYNAMIC_DRAW } as any)
|
|
164
|
+
return data
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const upload = (g: Geometry, data: Float32Array, count: number): void => {
|
|
168
|
+
const attr = (g.attributes as any).instanceMatrix
|
|
169
|
+
if (count > 0) {
|
|
170
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, attr.buffer)
|
|
171
|
+
;(renderer.state as any).boundBuffer = attr.buffer
|
|
172
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data, 0, count * 16)
|
|
173
|
+
}
|
|
174
|
+
g.setInstancedCount(count)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- batches ------------------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
const batchFor = (node: any, twin: Program, flip: boolean): Batch => {
|
|
180
|
+
const program: Program = node.program
|
|
181
|
+
const cast = node._castShadows !== false
|
|
182
|
+
const receive = node._receiveShadows !== false
|
|
183
|
+
const key = `${node.geometry.id}|${(program as any).id}|${node.mode}|${node.renderOrder}|${cast ? 1 : 0}${receive ? 1 : 0}${flip ? 1 : 0}`
|
|
184
|
+
let b = batches.get(key)
|
|
185
|
+
if (!b) {
|
|
186
|
+
const source: Geometry = node.geometry
|
|
187
|
+
b = {
|
|
188
|
+
key, source, program, twin, mode: node.mode, renderOrder: node.renderOrder,
|
|
189
|
+
castShadows: cast, receiveShadows: receive, flip,
|
|
190
|
+
all: new Float32Array(INITIAL_CAPACITY * 16), allCount: 0, allDirty: true,
|
|
191
|
+
geoAll: makeInstanceGeometry(source, INITIAL_CAPACITY),
|
|
192
|
+
sph: new Float32Array(INITIAL_CAPACITY * 4),
|
|
193
|
+
vis: new Float32Array(INITIAL_CAPACITY * 16), visCount: 0, visDirty: true,
|
|
194
|
+
geoVis: makeInstanceGeometry(source, INITIAL_CAPACITY),
|
|
195
|
+
bounds: new Float32Array(6),
|
|
196
|
+
lastUsed: frame,
|
|
197
|
+
}
|
|
198
|
+
batches.set(key, b)
|
|
199
|
+
}
|
|
200
|
+
return b
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Why a drawable can't join a batch — see the header. */
|
|
204
|
+
const batchable = (node: any): Program | null => {
|
|
205
|
+
if (!enabled) return null
|
|
206
|
+
if (node instanceof GLTFSkin || node._particles || node.mode === gl.POINTS) return null
|
|
207
|
+
const program: Program = node.program
|
|
208
|
+
if (program.transparent || !program.depthTest) return null
|
|
209
|
+
if (!node.geometry?.attributes?.position || node.geometry.isInstanced) return null
|
|
210
|
+
if ((program.uniforms as any).uMorphTex) return null
|
|
211
|
+
return twinFor(program)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const bounds = (node: any): { center: { x: number, y: number, z: number }, radius: number, min: any, max: any } | null => {
|
|
215
|
+
const g = node.geometry
|
|
216
|
+
if (!g.bounds || g.bounds.radius === Infinity) g.computeBoundingSphere()
|
|
217
|
+
return g.bounds && isFinite(g.bounds.radius) ? g.bounds : null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const sphereInFrustum = (camera: Camera, cx: number, cy: number, cz: number, r: number): boolean => {
|
|
221
|
+
const planes = camera.frustum as unknown as (ArrayLike<number> & { constant: number })[]
|
|
222
|
+
for (let i = 0; i < 6; i++) {
|
|
223
|
+
const p = planes[i]
|
|
224
|
+
if (p[0] * cx + p[1] * cy + p[2] * cz + p.constant < -r) return false
|
|
225
|
+
}
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Write matrix `w` at instance slot `i` of `data`; true when any value changed. */
|
|
230
|
+
const writeMatrix = (data: Float32Array, i: number, w: ArrayLike<number>): boolean => {
|
|
231
|
+
const o = i * 16
|
|
232
|
+
let changed = false
|
|
233
|
+
for (let k = 0; k < 16; k++) {
|
|
234
|
+
const v = w[k]
|
|
235
|
+
if (data[o + k] !== v) { data[o + k] = v; changed = true }
|
|
236
|
+
}
|
|
237
|
+
return changed
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** World AABB over a batch's `all` instances × its geometry's local AABB (8 corners each). */
|
|
241
|
+
const computeBounds = (b: Batch): void => {
|
|
242
|
+
const g = b.source
|
|
243
|
+
if (!g.bounds) g.computeBoundingBox()
|
|
244
|
+
const out = b.bounds
|
|
245
|
+
out[0] = out[1] = out[2] = Infinity; out[3] = out[4] = out[5] = -Infinity
|
|
246
|
+
if (!g.bounds || !isFinite(g.bounds.min.x)) return
|
|
247
|
+
const { min, max } = g.bounds
|
|
248
|
+
const corner = [ 0, 0, 0 ]
|
|
249
|
+
for (let i = 0; i < b.allCount; i++) {
|
|
250
|
+
const w = b.all.subarray(i * 16, i * 16 + 16)
|
|
251
|
+
for (let c = 0; c < 8; c++) {
|
|
252
|
+
transformPoint(corner, w, c & 1 ? max.x : min.x, c & 2 ? max.y : min.y, c & 4 ? max.z : min.z, 1)
|
|
253
|
+
for (let k = 0; k < 3; k++) {
|
|
254
|
+
if (corner[k] < out[k]) out[k] = corner[k]
|
|
255
|
+
if (corner[k] > out[k + 3]) out[k + 3] = corner[k]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- the per-frame walk -------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
const centre = [ 0, 0, 0 ]
|
|
264
|
+
|
|
265
|
+
/** The graph walk (world matrices must be current): rebuild every batch's `all` stream + spheres
|
|
266
|
+
* and the singles lists. Runs only when the graph version moved. */
|
|
267
|
+
const walk = (root: Transform): void => {
|
|
268
|
+
frame++
|
|
269
|
+
singlesAll.length = 0
|
|
270
|
+
casterSingles.length = 0
|
|
271
|
+
for (const b of batches.values()) b.allCount = 0
|
|
272
|
+
root.traverse((node: any) => {
|
|
273
|
+
if (!node.visible) return true
|
|
274
|
+
if (!node.draw) return false
|
|
275
|
+
const w = node.worldMatrix
|
|
276
|
+
const twin = batchable(node)
|
|
277
|
+
if (!twin) {
|
|
278
|
+
singlesAll.push(node)
|
|
279
|
+
if (node._castShadows !== false) casterSingles.push(node)
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
const flip = !!node.program.cullFace && determinant3(w) < 0
|
|
283
|
+
const b = batchFor(node, twin, flip)
|
|
284
|
+
b.lastUsed = frame
|
|
285
|
+
const i = b.allCount
|
|
286
|
+
if (i * 16 >= b.all.length) {
|
|
287
|
+
b.all = growInstanceGeometry(b.geoAll, i + 1)
|
|
288
|
+
const sph = new Float32Array(b.all.length / 4)
|
|
289
|
+
sph.set(b.sph)
|
|
290
|
+
b.sph = sph
|
|
291
|
+
}
|
|
292
|
+
if (writeMatrix(b.all, i, w)) b.allDirty = true
|
|
293
|
+
const bnd = bounds(node)
|
|
294
|
+
const o = i * 4
|
|
295
|
+
if (bnd && node.frustumCulled) {
|
|
296
|
+
transformPoint(centre, w, bnd.center.x, bnd.center.y, bnd.center.z, 1)
|
|
297
|
+
b.sph[o] = centre[0]; b.sph[o + 1] = centre[1]; b.sph[o + 2] = centre[2]
|
|
298
|
+
b.sph[o + 3] = bnd.radius * maxScaleOnAxis(w)
|
|
299
|
+
} else {
|
|
300
|
+
b.sph[o + 3] = -1 // never culled (frustumCulled off, or no bounds to test)
|
|
301
|
+
}
|
|
302
|
+
b.allCount++
|
|
303
|
+
return false
|
|
304
|
+
})
|
|
305
|
+
for (const [ key, b ] of batches) {
|
|
306
|
+
if (b.allCount !== b.geoAll.instancedCount) b.allDirty = true
|
|
307
|
+
if (b.allDirty) { upload(b.geoAll, b.all, b.allCount); computeBounds(b); b.allDirty = false }
|
|
308
|
+
if (b.allCount === 0 && frame - b.lastUsed > IDLE_FRAMES) {
|
|
309
|
+
disposeInstanceGeometry(b.geoAll)
|
|
310
|
+
disposeInstanceGeometry(b.geoVis)
|
|
311
|
+
batches.delete(key)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The cull pass: `all` → `vis` per batch from the cached spheres, `singlesAll` → `singles`.
|
|
317
|
+
* `cull` off copies everything through (a drawable with `frustumCulled = false` always
|
|
318
|
+
* passes, as in OGL). */
|
|
319
|
+
const cullPass = (camera: Camera, cull: boolean): void => {
|
|
320
|
+
camera.updateFrustum()
|
|
321
|
+
for (const b of batches.values()) {
|
|
322
|
+
b.visCount = 0
|
|
323
|
+
const n = b.allCount
|
|
324
|
+
for (let i = 0; i < n; i++) {
|
|
325
|
+
const o = i * 4
|
|
326
|
+
if (cull && b.sph[o + 3] >= 0 && !sphereInFrustum(camera, b.sph[o], b.sph[o + 1], b.sph[o + 2], b.sph[o + 3])) continue
|
|
327
|
+
const v = b.visCount
|
|
328
|
+
if (v * 16 >= b.vis.length) b.vis = growInstanceGeometry(b.geoVis, v + 1)
|
|
329
|
+
const src = i * 16, dst = v * 16
|
|
330
|
+
for (let k = 0; k < 16; k++) {
|
|
331
|
+
const x = b.all[src + k]
|
|
332
|
+
if (b.vis[dst + k] !== x) { b.vis[dst + k] = x; b.visDirty = true }
|
|
333
|
+
}
|
|
334
|
+
b.visCount++
|
|
335
|
+
}
|
|
336
|
+
if (b.visCount !== b.geoVis.instancedCount) b.visDirty = true
|
|
337
|
+
if (b.visDirty) { upload(b.geoVis, b.vis, b.visCount); b.visDirty = false }
|
|
338
|
+
}
|
|
339
|
+
singles.length = 0
|
|
340
|
+
for (const node of singlesAll) {
|
|
341
|
+
if (cull && node.frustumCulled && !camera.frustumIntersectsMesh(node)) continue
|
|
342
|
+
singles.push(node)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Refresh the frame's draw lists. `graphVersion` = the engine's mutation counter (a change
|
|
347
|
+
* re-walks the graph); the camera moving re-culls; otherwise nothing happens. `force` re-walks
|
|
348
|
+
* regardless (a caller that changed drawables behind the counter's back). */
|
|
349
|
+
const collect = (root: Transform, camera: Camera, cull: boolean, graphVersion = -2, force = false): void => {
|
|
350
|
+
const graphChanged = force || graphVersion !== lastGraph
|
|
351
|
+
if (graphChanged) { walk(root); lastGraph = graphVersion }
|
|
352
|
+
camera.updateMatrixWorld()
|
|
353
|
+
const pv = camera.projectionViewMatrix as unknown as ArrayLike<number>
|
|
354
|
+
let camChanged = cull !== lastCull
|
|
355
|
+
for (let k = 0; k < 16 && !camChanged; k++) if (lastPV[k] !== pv[k]) camChanged = true
|
|
356
|
+
if (!graphChanged && !camChanged) return
|
|
357
|
+
cullPass(camera, cull)
|
|
358
|
+
for (let k = 0; k < 16; k++) lastPV[k] = pv[k]
|
|
359
|
+
lastCull = cull
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ---- draws ------------------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
const sortedBatches = (): Batch[] => [ ...batches.values() ].sort((a, b) => a.renderOrder - b.renderOrder)
|
|
365
|
+
|
|
366
|
+
/** The main pass: batches (all opaque) in render-order, then the singles in OGL's order —
|
|
367
|
+
* opaque front-to-back, transparent back-to-front, depth-test-off last. `shadowEnabled` is the
|
|
368
|
+
* scene's live { value } slot; batches that don't receive shadows draw with it forced off (the
|
|
369
|
+
* same swap wireReceiveShadows does per single). */
|
|
370
|
+
const drawMain = (camera: Camera, shadowEnabled: { value: number }): void => {
|
|
371
|
+
for (const b of sortedBatches()) {
|
|
372
|
+
if (b.visCount === 0) continue
|
|
373
|
+
const u = b.twin.uniforms as Record<string, { value: unknown }>
|
|
374
|
+
;(u.projectionMatrix ??= { value: null }).value = camera.projectionMatrix
|
|
375
|
+
;(u.viewMatrix ??= { value: null }).value = camera.viewMatrix
|
|
376
|
+
;(u.cameraPosition ??= { value: null }).value = camera.worldPosition
|
|
377
|
+
// the uniforms object is the source program's too — swap the slot for this draw only, so
|
|
378
|
+
// a single drawn later through the same program still sees the scene's live value
|
|
379
|
+
const prevShadow = u.shadowEnabled
|
|
380
|
+
if (prevShadow) u.shadowEnabled = b.receiveShadows ? shadowEnabled : ZERO
|
|
381
|
+
syncState(b.twin, b.program)
|
|
382
|
+
b.twin.use({ flipFaces: b.flip })
|
|
383
|
+
b.geoVis.draw({ mode: b.mode, program: b.twin })
|
|
384
|
+
if (prevShadow) u.shadowEnabled = prevShadow
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const opaque: Mesh[] = [], transparent: Mesh[] = [], ui: Mesh[] = []
|
|
388
|
+
const pv = camera.projectionViewMatrix as unknown as ArrayLike<number>
|
|
389
|
+
for (const node of singles as any[]) {
|
|
390
|
+
if (!node.program.transparent) opaque.push(node)
|
|
391
|
+
else if (node.program.depthTest) transparent.push(node)
|
|
392
|
+
else ui.push(node)
|
|
393
|
+
node.zDepth = 0
|
|
394
|
+
if (node.renderOrder !== 0 || !node.program.depthTest) continue
|
|
395
|
+
const w = node.worldMatrix
|
|
396
|
+
const x = w[12], y = w[13], z = w[14]
|
|
397
|
+
const cw = pv[3] * x + pv[7] * y + pv[11] * z + pv[15] || 1
|
|
398
|
+
node.zDepth = (pv[2] * x + pv[6] * y + pv[10] * z + pv[14]) / cw
|
|
399
|
+
}
|
|
400
|
+
;(opaque as any[]).sort(renderer.sortOpaque)
|
|
401
|
+
;(transparent as any[]).sort(renderer.sortTransparent)
|
|
402
|
+
;(ui as any[]).sort(renderer.sortUI)
|
|
403
|
+
for (const node of opaque) node.draw({ camera })
|
|
404
|
+
for (const node of transparent) node.draw({ camera })
|
|
405
|
+
for (const node of ui) node.draw({ camera })
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The shadow pass for the batches: every `all` instance of every casting batch through the
|
|
409
|
+
* instanced depth program (`shadowViewProjection` already set by the caller). */
|
|
410
|
+
const drawShadow = (depthInstanced: Program): void => {
|
|
411
|
+
for (const b of batches.values()) {
|
|
412
|
+
if (!b.castShadows || b.allCount === 0) continue
|
|
413
|
+
depthInstanced.use({ flipFaces: b.flip })
|
|
414
|
+
b.geoAll.draw({ mode: b.mode, program: depthInstanced })
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Extend `min`/`max` by the world AABB of every casting batch (cached per batch). */
|
|
419
|
+
const casterBounds = (min: number[], max: number[]): void => {
|
|
420
|
+
for (const b of batches.values()) {
|
|
421
|
+
if (!b.castShadows || b.allCount === 0 || !isFinite(b.bounds[0])) continue
|
|
422
|
+
for (let k = 0; k < 3; k++) {
|
|
423
|
+
if (b.bounds[k] < min[k]) min[k] = b.bounds[k]
|
|
424
|
+
if (b.bounds[k + 3] > max[k]) max[k] = b.bounds[k + 3]
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const dispose = (): void => {
|
|
430
|
+
for (const b of batches.values()) { disposeInstanceGeometry(b.geoAll); disposeInstanceGeometry(b.geoVis) }
|
|
431
|
+
batches.clear()
|
|
432
|
+
for (const t of twins.values()) t?.remove()
|
|
433
|
+
twins.clear()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Whether a drawable is drawn through a batch (so a caller enumerating singles skips it). */
|
|
437
|
+
const isBatched = (node: Mesh): boolean => batchable(node) !== null
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
collect, drawMain, drawShadow, casterBounds, isBatched, dispose,
|
|
441
|
+
setEnabled: (v: boolean): void => { enabled = v; lastGraph = -1 },
|
|
442
|
+
/** This frame's individually drawn meshes (after `collect`). */
|
|
443
|
+
singles,
|
|
444
|
+
/** The hierarchy-visible singles that cast shadows (every one, not only the camera-visible). */
|
|
445
|
+
casterSingles,
|
|
446
|
+
/** Diagnostics: batch count / instance totals. */
|
|
447
|
+
stats: () => {
|
|
448
|
+
let all = 0, vis = 0, n = 0
|
|
449
|
+
for (const b of batches.values()) if (b.allCount > 0) { n++; all += b.allCount; vis += b.visCount }
|
|
450
|
+
return { batches: n, instances: all, visible: vis, singles: singles.length }
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
}
|