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,833 @@
|
|
|
1
|
+
// The GL-free scene core for the lite 3D engine — one CPU-side implementation of the `_creator`
|
|
2
|
+
// scene semantics (sdk/src/bridges.d.ts, mirroring creator-gl's scene.cpp /
|
|
3
|
+
// transformUtils.cpp / collider.cpp) that BOTH lite backends drive:
|
|
4
|
+
//
|
|
5
|
+
// • the WebGL backend (viewer-lite, browser) — builds OGL programs/geometry from core state, and
|
|
6
|
+
// • the headless backend (lecodes-headless) — records + serializes the same state to scene3d JSON.
|
|
7
|
+
//
|
|
8
|
+
// The core owns entities, hierarchy, local matrices, semantic materials/meshes/lights, GLB structure,
|
|
9
|
+
// colliders, and the animation playback clock. It creates NO GL objects: where GL work is needed it
|
|
10
|
+
// calls an optional `SceneBackend` hook, and the backend reacts at its edge (build a Program, sample
|
|
11
|
+
// a clip, invalidate a cached world matrix). Headless supplies only `allocMatrix`; every GL hook is a
|
|
12
|
+
// no-op there, so the recorded state IS the core state — that's why the two views can't disagree.
|
|
13
|
+
//
|
|
14
|
+
// Divergences between the old ports are resolved to creator-gl native here (see gl-lite-core-plan.md):
|
|
15
|
+
// destroyEntity destroys the whole subtree (scene.cpp), cloneEntity attaches to the source's parent +
|
|
16
|
+
// scene (scene.cpp), and the camera fov is the real engines' 60° (backend-configurable).
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
composeTRS, getEulerYXZDeg, getScale, identity, invertTRS, multiply, perspective,
|
|
20
|
+
setEulerOn, setQuaternionOn, setScaleOn, type Mat4, type Mat4Like,
|
|
21
|
+
} from "./math3d"
|
|
22
|
+
import { parseGlbStructure, type GlbAnimation } from "./glbStructure"
|
|
23
|
+
import { createAnimationSystem } from "./animBridge"
|
|
24
|
+
import { createPhysicsSystem } from "./physicsBridge"
|
|
25
|
+
import { createParticleRuntime, type ParticleFrame } from "./particleBridge"
|
|
26
|
+
import { nameMatchRank, shortName } from "./boneNames"
|
|
27
|
+
|
|
28
|
+
export type UniformRec =
|
|
29
|
+
| { kind: "rgb" | "rgba", v: number }
|
|
30
|
+
| { kind: "float", v: number }
|
|
31
|
+
| { kind: "bool", v: boolean }
|
|
32
|
+
| { kind: "texture", v: number }
|
|
33
|
+
| { kind: "array", v: number[] }
|
|
34
|
+
| { kind: "null" }
|
|
35
|
+
|
|
36
|
+
export type MaterialRec = { shader: string, uniforms: Record<string, UniformRec> }
|
|
37
|
+
|
|
38
|
+
export type MeshRec = {
|
|
39
|
+
kind: number
|
|
40
|
+
vertexCount: number
|
|
41
|
+
size?: [number, number, number]
|
|
42
|
+
center?: [number, number, number]
|
|
43
|
+
primitives?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TextureRec = { source?: string, width: number, height: number }
|
|
47
|
+
|
|
48
|
+
/** The animation playback clock — lives in the core; the backend only *samples* poses (onAnimationApply). */
|
|
49
|
+
|
|
50
|
+
export type Ent = {
|
|
51
|
+
id: number
|
|
52
|
+
name: string
|
|
53
|
+
/** Local transform (column-major), supplied by the backend so mutations flow to the renderer. */
|
|
54
|
+
m: Mat4Like
|
|
55
|
+
parent: number // 0 = none
|
|
56
|
+
children: number[]
|
|
57
|
+
visible: boolean
|
|
58
|
+
mesh?: MeshRec
|
|
59
|
+
materials: number[]
|
|
60
|
+
light?: { direction: [number, number, number], intensity: number, color: number, shadowsQuality: number, shadowDistance?: number, shadowCascades?: number }
|
|
61
|
+
/** `buffer` = the GLB bytes, retained on the root for mesh colliders built from the model
|
|
62
|
+
* (physicsBuildMeshFromEntity / glbTriangles) — the fetch store's copy may be gone by then. */
|
|
63
|
+
model?: { source?: string, systemId?: number, animations: GlbAnimation[], loaded: boolean, clipSet?: number, buffer?: ArrayBuffer }
|
|
64
|
+
/** A skin joint of a loaded GLB (the Animator prefers these over same-named plain nodes). */
|
|
65
|
+
skinJoint?: boolean
|
|
66
|
+
castShadows?: boolean
|
|
67
|
+
receiveShadows?: boolean
|
|
68
|
+
// --- headless recording surface (populated by recording.ts; the WebGL backend leaves these empty) ---
|
|
69
|
+
particles?: Record<string, unknown>
|
|
70
|
+
physics?: Record<string, unknown>
|
|
71
|
+
/** A Ragdoll's parts on the model entity (recording.ts ragdollCreate) + active/velocity while on. */
|
|
72
|
+
ragdoll?: Record<string, unknown>
|
|
73
|
+
collider?: { type: string, [k: string]: unknown }
|
|
74
|
+
anchor?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A scene's camera projection (setCameraProjection) — vertical fov in degrees + clip distances. */
|
|
78
|
+
export type Projection = { fov: number, near: number, far: number }
|
|
79
|
+
|
|
80
|
+
/** An inspection camera: eye + look-at point in world units (Y up), optional vertical fov (deg). */
|
|
81
|
+
export type CameraOverride = { eye: [number, number, number], target: [number, number, number], fov?: number }
|
|
82
|
+
|
|
83
|
+
export type SceneRec = {
|
|
84
|
+
id: number
|
|
85
|
+
entities: Set<number>
|
|
86
|
+
overlays: number[]
|
|
87
|
+
overlayOf?: number
|
|
88
|
+
camera?: number
|
|
89
|
+
/** Set by setCameraProjection; absent = the core defaults (fov option / 0.01 / 1000). */
|
|
90
|
+
projection?: Projection
|
|
91
|
+
skybox?: number
|
|
92
|
+
ibl?: number
|
|
93
|
+
bloom?: { strength: number }
|
|
94
|
+
antialias?: boolean
|
|
95
|
+
arMode?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The seam between the shared core and a concrete backend. Headless implements only `allocMatrix`. */
|
|
99
|
+
export interface SceneBackend {
|
|
100
|
+
/** Allocate an entity's local matrix. Headless: a fresh identity Float32Array. The WebGL backend
|
|
101
|
+
* hands back the OGL Transform's own matrix so the core mutates the renderable in place. */
|
|
102
|
+
allocMatrix(entityId: number): Mat4Like
|
|
103
|
+
/** A local matrix was mutated in place (WebGL: worldMatrixNeedsUpdate = true). */
|
|
104
|
+
localMatrixChanged?(entityId: number): void
|
|
105
|
+
/** Parent changed (WebGL: OGL setParent / reparent under the owning scene root). */
|
|
106
|
+
parentChanged?(childId: number, parentId: number): void
|
|
107
|
+
/** A scene was created (WebGL: create its root Transform). */
|
|
108
|
+
sceneCreated?(sceneId: number): void
|
|
109
|
+
/** Entity added (added=true) or removed from a scene (WebGL: reparent under / off the scene root). */
|
|
110
|
+
entitySceneChanged?(entityId: number, sceneId: number, added: boolean): void
|
|
111
|
+
/** Visibility toggled (WebGL: Transform.visible). */
|
|
112
|
+
visibleChanged?(entityId: number, visible: boolean): void
|
|
113
|
+
/** A material was created (WebGL: build the Program for this shader). `systemId` is present for
|
|
114
|
+
* createMaterial (a fetched shader — needed to load a custom .mat buffer), absent for a built-in
|
|
115
|
+
* one (createMaterialNamed: `rec.shader` is its name). */
|
|
116
|
+
materialCreated?(materialId: number, rec: MaterialRec, systemId?: number): void
|
|
117
|
+
/** A uniform was set with its SEMANTIC value (WebGL: convert hex→linear + set on the Program). */
|
|
118
|
+
uniformChanged?(materialId: number, uniform: string, u: UniformRec): void
|
|
119
|
+
/** A mesh was set (WebGL: build Geometry/Mesh from the raw arrays). */
|
|
120
|
+
meshSet?(entityId: number, materialId: number, mesh: MeshRec, arrays: {
|
|
121
|
+
vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, colors?: Uint8Array,
|
|
122
|
+
}): void
|
|
123
|
+
/** GLB entities were created from the structure (WebGL: run GLTFLoader + attach drawables by node
|
|
124
|
+
* index). `nodeEntityIds[i]` is the entity for glTF node i; `rootId` is the synthetic model root.
|
|
125
|
+
* May be async (createGlb awaits it before firing the SDK's onComplete). */
|
|
126
|
+
glbAttach?(systemId: number, rootId: number, nodeEntityIds: number[]): void | Promise<void>
|
|
127
|
+
/** An Animator sampled morph-target weights for an entity (WebGL: write the node's blendshape weights). */
|
|
128
|
+
morphWeights?(entityId: number, weights: Float32Array): void
|
|
129
|
+
/** An entity is being destroyed (WebGL: tear down its drawables). */
|
|
130
|
+
entityDestroyed?(entityId: number): void
|
|
131
|
+
/** A particle system was created on the entity with a material (WebGL: prepare its drawable). */
|
|
132
|
+
particleSystemCreated?(entityId: number, materialId: number): void
|
|
133
|
+
/** A particle system's vertex stream for this frame (WebGL: upload + draw; headless ignores it).
|
|
134
|
+
* `frame.bytes` is a view into the wasm heap — copy or upload before the next tick. */
|
|
135
|
+
particlesFrame?(entityId: number, frame: ParticleFrame): void
|
|
136
|
+
particleSystemDestroyed?(entityId: number): void
|
|
137
|
+
/** An entity was cloned (WebGL: clone drawables). Called after the core copies scalar state. */
|
|
138
|
+
entityCloned?(sourceId: number, cloneId: number): void
|
|
139
|
+
/** A whole `cloneEntity` finished; `map` is source entity → clone entity for every node of the
|
|
140
|
+
* subtree. `entityCloned` runs node by node in pre-order, so a skinned mesh is cloned before the
|
|
141
|
+
* clones of its joints exist — this is where a backend re-points such cross-node references. */
|
|
142
|
+
subtreeCloned?(sourceRootId: number, cloneRootId: number, map: ReadonlyMap<number, number>): void
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type SceneCoreOptions = {
|
|
146
|
+
getBuffer: (systemId: number) => ArrayBuffer | undefined
|
|
147
|
+
getSourceName?: (systemId: number) => string | undefined
|
|
148
|
+
/** Viewport in logical px (drives getDisplaySize + the camera projection). */
|
|
149
|
+
width?: number
|
|
150
|
+
height?: number
|
|
151
|
+
/** Vertical camera fov in degrees (creator-gl native / web-lite use 60). */
|
|
152
|
+
fov?: number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Copy up to `n` elements from `src` into `dst` in place — works for Float32Array and Array alike
|
|
156
|
+
* (the WebGL backend's matrix is an OGL Array subclass, which has no typed-array `.set`). */
|
|
157
|
+
const copyInto = (dst: Mat4Like, src: ArrayLike<number>, n = 16): void => {
|
|
158
|
+
for (let i = 0; i < n; i++) (dst as number[])[i] = src[i]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createSceneCore(backend: SceneBackend, opts: SceneCoreOptions) {
|
|
162
|
+
const width = opts.width ?? 390
|
|
163
|
+
const height = opts.height ?? 844
|
|
164
|
+
const fov = opts.fov ?? 60
|
|
165
|
+
/** Matches creator-gl's SceneStruct defaults (scene.cpp applyCameraProjection). */
|
|
166
|
+
const defaultProjection: Projection = { fov, near: 0.01, far: 1000 }
|
|
167
|
+
|
|
168
|
+
const entities = new Map<number, Ent>()
|
|
169
|
+
const scenes = new Map<number, SceneRec>()
|
|
170
|
+
const materials = new Map<number, MaterialRec>()
|
|
171
|
+
const textures = new Map<number, TextureRec>()
|
|
172
|
+
let nextEntity = 1
|
|
173
|
+
let nextMaterial = 1
|
|
174
|
+
let nextTexture = 1
|
|
175
|
+
let activeScene = -1
|
|
176
|
+
|
|
177
|
+
let earlyUpdate: ((dt: number) => void) | null = null
|
|
178
|
+
let lateUpdate: ((dt: number) => void) | null = null
|
|
179
|
+
let fixedUpdate: ((dt: number) => void) | null = null // the SDK's updateFixed: once per physics substep (creator-physics' frame protocol)
|
|
180
|
+
|
|
181
|
+
// The physics system (creator-physics' wasm — the same Jolt simulation creator-gl runs). Bodies and
|
|
182
|
+
// characters write world poses back into entity local matrices; the backend is told via localMatrixChanged.
|
|
183
|
+
const physics = createPhysicsSystem({
|
|
184
|
+
ent: (id) => ent(id),
|
|
185
|
+
get: (id) => entities.get(id),
|
|
186
|
+
world: (id) => worldMatrixF(id),
|
|
187
|
+
setWorldTRS: (id, x, y, z, qx, qy, qz, qw, sx, sy, sz) => writeWorldMatrix(id, composeTRS([x, y, z], [qx, qy, qz, qw], [sx, sy, sz])),
|
|
188
|
+
setWorldPosition: (id, x, y, z) => writeWorldPosition(id, x, y, z),
|
|
189
|
+
localMatrixChanged: (id) => backend.localMatrixChanged?.(id),
|
|
190
|
+
parentOf: (id) => entities.get(id)?.parent ?? 0,
|
|
191
|
+
glbBytes: (id) => entities.get(id)?.model?.buffer,
|
|
192
|
+
// a ragdoll let go with a blend: the animator transitions out of the fallen pose (`anim` is
|
|
193
|
+
// created right below; the hook only runs long after)
|
|
194
|
+
seedAnimator: (joint, blend) => anim.methods.animatorSeedPose(joint, blend),
|
|
195
|
+
// learned ragdoll limits: the clips' joint ranges from the same animator
|
|
196
|
+
jointRanges: (joint, pairs, frames) => anim.methods.animatorJointRanges(joint, pairs, frames),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// The particle runtime (creator-particles' wasm — the same simulation creator-gl runs): emitters
|
|
200
|
+
// read their world matrix here, the streams go to the backend's uploader, the counts to the record.
|
|
201
|
+
const particles = createParticleRuntime({
|
|
202
|
+
ent: (id) => ent(id),
|
|
203
|
+
world: (id) => worldMatrixF(id),
|
|
204
|
+
cameraPosition: () => {
|
|
205
|
+
const cam = scenes.get(activeScene)?.camera
|
|
206
|
+
if (cam === undefined) return null
|
|
207
|
+
const w = worldMatrix(cam)
|
|
208
|
+
return [w[12], w[13], w[14]]
|
|
209
|
+
},
|
|
210
|
+
created: (id, materialId) => backend.particleSystemCreated?.(id, materialId),
|
|
211
|
+
frame: (id, frame) => backend.particlesFrame?.(id, frame),
|
|
212
|
+
destroyed: (id) => backend.particleSystemDestroyed?.(id),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// The animation system (AnimationClip sets + Animators — creator-anim.s wasm, the same evaluator creator-gl runs).
|
|
216
|
+
// Joint writes go straight into entity local matrices; the backend is told via localMatrixChanged.
|
|
217
|
+
const anim = createAnimationSystem({
|
|
218
|
+
get: (id) => entities.get(id),
|
|
219
|
+
world: (id) => worldMatrixF(id),
|
|
220
|
+
localMatrixChanged: (id) => backend.localMatrixChanged?.(id),
|
|
221
|
+
getBuffer: (systemId) => opts.getBuffer(systemId),
|
|
222
|
+
setMorphWeights: (id, w) => backend.morphWeights?.(id, w),
|
|
223
|
+
applyRootMotion: (id, dx, dy, dz, dt, yaw) => {
|
|
224
|
+
const w = worldMatrixF(id) // node-local → world direction
|
|
225
|
+
return physics.applyRootMotion(id, w[0] * dx + w[4] * dy + w[8] * dz, w[2] * dx + w[6] * dy + w[10] * dz, dt, yaw)
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const makeEnt = (id: number): Ent => {
|
|
230
|
+
const e: Ent = {
|
|
231
|
+
id, name: "", m: backend.allocMatrix(id), parent: 0, children: [], visible: true, materials: [],
|
|
232
|
+
}
|
|
233
|
+
// allocMatrix may hand back an un-initialised buffer — ensure identity.
|
|
234
|
+
if (e.m[0] !== 1 || e.m[5] !== 1 || e.m[10] !== 1 || e.m[15] !== 1) copyInto(e.m, identity())
|
|
235
|
+
entities.set(id, e)
|
|
236
|
+
return e
|
|
237
|
+
}
|
|
238
|
+
const newEnt = (): Ent => makeEnt(nextEntity++)
|
|
239
|
+
// The SDK can hand back ids it saw from callbacks; be forgiving and materialize unknown ids.
|
|
240
|
+
const ent = (id: number): Ent => {
|
|
241
|
+
let e = entities.get(id)
|
|
242
|
+
if (!e) { e = makeEnt(id); if (id >= nextEntity) nextEntity = id + 1 }
|
|
243
|
+
return e
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const worldMatrix = (id: number): Mat4Like => {
|
|
247
|
+
const e = ent(id)
|
|
248
|
+
if (e.parent === 0) return e.m
|
|
249
|
+
return multiply(new Float32Array(16), worldMatrix(e.parent), e.m)
|
|
250
|
+
}
|
|
251
|
+
/** worldMatrix as a concrete Float32Array (for math that needs `.set`/`.subarray`). */
|
|
252
|
+
const worldMatrixF = (id: number): Float32Array => {
|
|
253
|
+
const w = worldMatrix(id)
|
|
254
|
+
return w instanceof Float32Array ? w : Float32Array.from(w as ArrayLike<number>)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const detach = (e: Ent): void => {
|
|
258
|
+
if (e.parent !== 0) {
|
|
259
|
+
const p = entities.get(e.parent)
|
|
260
|
+
if (p) p.children = p.children.filter((c) => c !== e.id)
|
|
261
|
+
e.parent = 0
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const setParentInternal = (childId: number, parentId: number, worldPositionStays: boolean): void => {
|
|
266
|
+
const e = ent(childId)
|
|
267
|
+
if (e.parent === parentId) return
|
|
268
|
+
if (worldPositionStays) {
|
|
269
|
+
const world = worldMatrixF(childId)
|
|
270
|
+
const local = parentId === 0 ? world : multiply(new Float32Array(16), invertTRS(worldMatrixF(parentId)), world)
|
|
271
|
+
copyInto(e.m, local)
|
|
272
|
+
}
|
|
273
|
+
detach(e)
|
|
274
|
+
if (parentId !== 0) {
|
|
275
|
+
e.parent = parentId
|
|
276
|
+
ent(parentId).children.push(childId)
|
|
277
|
+
}
|
|
278
|
+
backend.parentChanged?.(childId, parentId)
|
|
279
|
+
backend.localMatrixChanged?.(childId)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A world-space transform onto an entity (into its parent's space when parented) — the physics
|
|
283
|
+
// system's pose writers (creator-gl's writeWorldTransform / writeWorldPosition twins).
|
|
284
|
+
const writeWorldMatrix = (id: number, world: Mat4Like): void => {
|
|
285
|
+
const e = ent(id)
|
|
286
|
+
copyInto(e.m, e.parent === 0 ? world : multiply(new Float32Array(16), invertTRS(worldMatrixF(e.parent)), world))
|
|
287
|
+
backend.localMatrixChanged?.(id)
|
|
288
|
+
}
|
|
289
|
+
const writeWorldPosition = (id: number, x: number, y: number, z: number): void => {
|
|
290
|
+
const e = ent(id)
|
|
291
|
+
const m = e.m as number[]
|
|
292
|
+
if (e.parent === 0) { m[12] = x; m[13] = y; m[14] = z }
|
|
293
|
+
else {
|
|
294
|
+
const inv = invertTRS(worldMatrixF(e.parent))
|
|
295
|
+
m[12] = inv[0] * x + inv[4] * y + inv[8] * z + inv[12]
|
|
296
|
+
m[13] = inv[1] * x + inv[5] * y + inv[9] * z + inv[13]
|
|
297
|
+
m[14] = inv[2] * x + inv[6] * y + inv[10] * z + inv[14]
|
|
298
|
+
}
|
|
299
|
+
backend.localMatrixChanged?.(id)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** A scene's projection, falling back to the core defaults. Overlays follow their parent (they
|
|
303
|
+
* share its camera, exactly like createOverlayScene in scene.cpp). */
|
|
304
|
+
const projectionOf = (sceneId: number): Projection => {
|
|
305
|
+
const s = scenes.get(sceneId)
|
|
306
|
+
const owner = s?.overlayOf !== undefined ? scenes.get(s.overlayOf) : s
|
|
307
|
+
return owner?.projection ?? defaultProjection
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const sceneOf = (id: number): SceneRec | null => {
|
|
311
|
+
for (let cur = id; cur !== 0;) {
|
|
312
|
+
for (const s of scenes.values()) if (s.entities.has(cur)) return s
|
|
313
|
+
cur = ent(cur).parent
|
|
314
|
+
}
|
|
315
|
+
return null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const sourceName = (systemId: number): string | undefined => {
|
|
319
|
+
const name = opts.getSourceName?.(systemId)
|
|
320
|
+
if (!name) return undefined
|
|
321
|
+
// Local project assets arrive as lecodesfile://<abs-path>; keep just the file name.
|
|
322
|
+
const clean = name.startsWith("lecodesfile://") ? decodeURIComponent(name.slice("lecodesfile://".length)) : name
|
|
323
|
+
const isPathy = clean.includes("/") || clean.includes("\\")
|
|
324
|
+
return isPathy && !clean.startsWith("http") ? (clean.split(/[\\/]/).pop() ?? clean) : clean
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const material = (id: number): MaterialRec => {
|
|
328
|
+
let m = materials.get(id)
|
|
329
|
+
if (!m) { m = { shader: "unknown", uniforms: {} }; materials.set(id, m) }
|
|
330
|
+
return m
|
|
331
|
+
}
|
|
332
|
+
const setUniform = (materialId: number, uniform: string, u: UniformRec): void => {
|
|
333
|
+
material(materialId).uniforms[uniform] = u
|
|
334
|
+
backend.uniformChanged?.(materialId, uniform, u)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Stage 1 of GLB load (gl-free): parse the JSON structure and create the entity hierarchy (the
|
|
338
|
+
// synthetic model root + one entity per glTF node, in node-index order). Returns the ids so the
|
|
339
|
+
// backend's stage-2 glbAttach can attach drawables by node index (both derive from the same glTF
|
|
340
|
+
// JSON, so indices align). `entIds` is empty when the buffer isn't a parseable GLB.
|
|
341
|
+
const buildGlbEntities = (systemId: number): { rootId: number, entIds: number[] } => {
|
|
342
|
+
const buf = opts.getBuffer(systemId)
|
|
343
|
+
const structure = buf ? parseGlbStructure(buf) : null
|
|
344
|
+
const root = newEnt()
|
|
345
|
+
root.model = { source: sourceName(systemId), systemId, animations: structure?.animations ?? [], loaded: structure !== null, ...(buf && { buffer: buf }) }
|
|
346
|
+
// Embedded clips are parsed NOW (like native loadGlb): the fetched buffer is `useOnce` and may be
|
|
347
|
+
// gone by the time an Animator asks for the set. Only when the file has animations.
|
|
348
|
+
if (structure && structure.animations.length > 0 && buf) root.model.clipSet = anim.registerGlbClips(buf)
|
|
349
|
+
if (!structure) return { rootId: root.id, entIds: [] }
|
|
350
|
+
const entIds = structure.nodes.map(() => newEnt().id)
|
|
351
|
+
structure.nodes.forEach((n, i) => {
|
|
352
|
+
const e = ent(entIds[i])
|
|
353
|
+
e.name = n.name ?? ""
|
|
354
|
+
copyInto(e.m, n.matrix)
|
|
355
|
+
backend.localMatrixChanged?.(e.id)
|
|
356
|
+
if (n.skinJoint) e.skinJoint = true
|
|
357
|
+
if (n.mesh) {
|
|
358
|
+
e.mesh = { kind: 0, vertexCount: 0, primitives: n.mesh.primitives }
|
|
359
|
+
if (n.mesh.min && n.mesh.max) {
|
|
360
|
+
e.mesh.size = [n.mesh.max[0] - n.mesh.min[0], n.mesh.max[1] - n.mesh.min[1], n.mesh.max[2] - n.mesh.min[2]]
|
|
361
|
+
e.mesh.center = [(n.mesh.max[0] + n.mesh.min[0]) / 2, (n.mesh.max[1] + n.mesh.min[1]) / 2, (n.mesh.max[2] + n.mesh.min[2]) / 2]
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const c of n.children) setParentInternal(entIds[c], e.id, false)
|
|
365
|
+
})
|
|
366
|
+
for (const r of structure.roots) setParentInternal(entIds[r], root.id, false)
|
|
367
|
+
return { rootId: root.id, entIds }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// cloneEntity → creator-gl native: the clone attaches to the source's parent and joins the source's
|
|
371
|
+
// scene (scene.cpp cloneEntity). The subtree is cloned recursively (shared by both TS ports).
|
|
372
|
+
const cloneSubtree = (id: number, map: Map<number, number>): number => {
|
|
373
|
+
const src = ent(id)
|
|
374
|
+
const copy = newEnt()
|
|
375
|
+
map.set(src.id, copy.id)
|
|
376
|
+
copy.name = src.name
|
|
377
|
+
copyInto(copy.m, src.m)
|
|
378
|
+
copy.visible = src.visible
|
|
379
|
+
if (src.skinJoint) copy.skinJoint = true
|
|
380
|
+
if (src.light) copy.light = { ...src.light }
|
|
381
|
+
if (src.mesh) copy.mesh = { ...src.mesh }
|
|
382
|
+
copy.materials = [...src.materials]
|
|
383
|
+
if (src.model) copy.model = { ...src.model, animations: src.model.animations }
|
|
384
|
+
if (src.castShadows !== undefined) copy.castShadows = src.castShadows
|
|
385
|
+
if (src.receiveShadows !== undefined) copy.receiveShadows = src.receiveShadows
|
|
386
|
+
if (src.particles) copy.particles = structuredClone(src.particles)
|
|
387
|
+
if (src.physics) copy.physics = structuredClone(src.physics)
|
|
388
|
+
if (src.collider) copy.collider = structuredClone(src.collider)
|
|
389
|
+
if (src.anchor) copy.anchor = src.anchor
|
|
390
|
+
backend.localMatrixChanged?.(copy.id)
|
|
391
|
+
backend.entityCloned?.(src.id, copy.id)
|
|
392
|
+
for (const c of src.children) setParentInternal(cloneSubtree(c, map), copy.id, false)
|
|
393
|
+
return copy.id
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const destroySubtree = (id: number): void => {
|
|
397
|
+
const e = entities.get(id)
|
|
398
|
+
if (!e) return
|
|
399
|
+
for (const c of [...e.children]) destroySubtree(c)
|
|
400
|
+
detach(e)
|
|
401
|
+
for (const s of scenes.values()) s.entities.delete(id)
|
|
402
|
+
backend.entityDestroyed?.(id)
|
|
403
|
+
anim.onEntityDestroyed(id)
|
|
404
|
+
particles.onEntityDestroyed(id)
|
|
405
|
+
entities.delete(id)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Visible only if the whole ancestor chain is visible (native isVisible).
|
|
409
|
+
const visibleChain = (entityId: number): boolean => {
|
|
410
|
+
for (let id = entityId; id !== 0; id = ent(id).parent) if (!ent(id).visible) return false
|
|
411
|
+
return true
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** One render step: aspect update(dt) phases around the animator evaluation (creator-gl's
|
|
415
|
+
* render-loop order — early → physics → animators → late → particles). The WebGL backend drives this per frame with the
|
|
416
|
+
* real frame dt; headless drives it in fixed 16ms steps from `simulate`. */
|
|
417
|
+
// The engine clock multiplier (SDK Time.scale / Time.paused): the sims advance by dt × scale, the JS
|
|
418
|
+
// phases keep the raw dt — creator-gl's setTimeScale contract.
|
|
419
|
+
let timeScale = 1
|
|
420
|
+
const tick = (dt: number): void => {
|
|
421
|
+
const sim = dt * timeScale
|
|
422
|
+
earlyUpdate?.(dt)
|
|
423
|
+
// creator-gl's frame (init.cpp stepFrameUpdates): the physics steps right after the early phase
|
|
424
|
+
// (controllers feed it) with the fixed phase between the substeps — once per 1/60 of GAME time, at
|
|
425
|
+
// most 4 per frame, backlog dropped past the cap, dt always exactly 1/60 — then the render poses
|
|
426
|
+
// land on the entities, the animators run, and the active ragdolls overwrite their bones.
|
|
427
|
+
physics.frame(sim, fixedUpdate)
|
|
428
|
+
anim.evaluate(sim)
|
|
429
|
+
physics.syncRagdolls(sim)
|
|
430
|
+
lateUpdate?.(dt)
|
|
431
|
+
anim.lateSolve() // creator-gl's lateSolveAnimators: the IK chains on the joints as the late phase left them
|
|
432
|
+
// creator-gl's updateParticles — AFTER the late phase since 2026-09-20 (init.cpp stepFrameUpdates has the why): a
|
|
433
|
+
// world-space stream is written against the emitter's matrix, so it must be the matrix the draw uses, and an
|
|
434
|
+
// emitter moved by a default `update(dt)` aspect / system is moved in the late phase.
|
|
435
|
+
particles.update(sim)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Advance to `timeMs` in fixed 16ms steps (headless). 0 = don't step (the initial state). */
|
|
439
|
+
const simulate = (timeMs: number): void => {
|
|
440
|
+
const steps = Math.min(1000, Math.floor(timeMs / 16))
|
|
441
|
+
for (let i = 0; i < steps; i++) tick(0.016)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const hasActiveScene = (): boolean => activeScene >= 0 || scenes.size > 0
|
|
445
|
+
const activeSceneId = (): number => activeScene
|
|
446
|
+
|
|
447
|
+
// ---- the shared, backend-agnostic `_creator` methods --------------------------------------------
|
|
448
|
+
|
|
449
|
+
const methods = {
|
|
450
|
+
// --- scenes ---
|
|
451
|
+
createScene(): number {
|
|
452
|
+
const id = scenes.size
|
|
453
|
+
scenes.set(id, { id, entities: new Set(), overlays: [] })
|
|
454
|
+
backend.sceneCreated?.(id)
|
|
455
|
+
return id
|
|
456
|
+
},
|
|
457
|
+
createOverlayScene(parentId: number): number {
|
|
458
|
+
const id = scenes.size
|
|
459
|
+
// Overlays render through the parent scene's camera (the WebGL backend draws them with the main
|
|
460
|
+
// camera object), so we don't give the overlay its own camera — matching what serializes today.
|
|
461
|
+
scenes.set(id, { id, entities: new Set(), overlays: [], overlayOf: parentId })
|
|
462
|
+
scenes.get(parentId)?.overlays.push(id)
|
|
463
|
+
backend.sceneCreated?.(id)
|
|
464
|
+
return id
|
|
465
|
+
},
|
|
466
|
+
createGLView(): void {},
|
|
467
|
+
openScene(sceneId: number): void { activeScene = sceneId },
|
|
468
|
+
closeScene(): void { activeScene = -1 },
|
|
469
|
+
addEntityToScene(sceneId: number, entityId: number): void {
|
|
470
|
+
const s = scenes.get(sceneId)
|
|
471
|
+
if (!s) return
|
|
472
|
+
// An entity lives in exactly one scene (native scene.cpp move_elements): drop it from any other.
|
|
473
|
+
for (const other of scenes.values()) if (other !== s) other.entities.delete(entityId)
|
|
474
|
+
s.entities.add(entityId)
|
|
475
|
+
backend.entitySceneChanged?.(entityId, sceneId, true)
|
|
476
|
+
},
|
|
477
|
+
removeEntityFromScene(sceneId: number, entityId: number): void {
|
|
478
|
+
const s = scenes.get(sceneId)
|
|
479
|
+
if (!s) return
|
|
480
|
+
s.entities.delete(entityId)
|
|
481
|
+
backend.entitySceneChanged?.(entityId, sceneId, false)
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
// --- entities + hierarchy ---
|
|
485
|
+
createEntity: (): number => newEnt().id,
|
|
486
|
+
cloneEntity(entityId: number): number {
|
|
487
|
+
const src = ent(entityId)
|
|
488
|
+
const map = new Map<number, number>()
|
|
489
|
+
const cloneId = cloneSubtree(entityId, map)
|
|
490
|
+
// Native: attach to the source's parent; if the source is a bare scene member, join its scene.
|
|
491
|
+
if (src.parent !== 0) {
|
|
492
|
+
setParentInternal(cloneId, src.parent, false)
|
|
493
|
+
} else {
|
|
494
|
+
const scene = sceneOf(entityId)
|
|
495
|
+
if (scene) {
|
|
496
|
+
scene.entities.add(cloneId)
|
|
497
|
+
backend.entitySceneChanged?.(cloneId, scene.id, true)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
backend.subtreeCloned?.(entityId, cloneId, map)
|
|
501
|
+
return cloneId
|
|
502
|
+
},
|
|
503
|
+
destroyEntity(entityId: number): void { destroySubtree(entityId) },
|
|
504
|
+
|
|
505
|
+
setMatrix(entityId: number, mat: Float32Array): void {
|
|
506
|
+
const e = ent(entityId)
|
|
507
|
+
copyInto(e.m, mat)
|
|
508
|
+
backend.localMatrixChanged?.(entityId)
|
|
509
|
+
},
|
|
510
|
+
setWorldMatrix(entityId: number, mat: Float32Array): void {
|
|
511
|
+
const e = ent(entityId)
|
|
512
|
+
if (e.parent === 0) { copyInto(e.m, mat); backend.localMatrixChanged?.(entityId); return }
|
|
513
|
+
const local = multiply(new Float32Array(16), invertTRS(worldMatrixF(e.parent)), Float32Array.from(mat.subarray(0, 16)))
|
|
514
|
+
copyInto(e.m, local)
|
|
515
|
+
backend.localMatrixChanged?.(entityId)
|
|
516
|
+
},
|
|
517
|
+
getChildCount: (entityId: number): number => ent(entityId).children.length,
|
|
518
|
+
getChild: (entityId: number, index: number): number => ent(entityId).children[index] ?? 0,
|
|
519
|
+
|
|
520
|
+
setPosition(entityId: number, x: number, y: number, z: number): void {
|
|
521
|
+
const m = ent(entityId).m
|
|
522
|
+
m[12] = x; m[13] = y; m[14] = z
|
|
523
|
+
backend.localMatrixChanged?.(entityId)
|
|
524
|
+
},
|
|
525
|
+
setQuaternion(entityId: number, x: number, y: number, z: number, w: number): void {
|
|
526
|
+
setQuaternionOn(ent(entityId).m, x, y, z, w); backend.localMatrixChanged?.(entityId)
|
|
527
|
+
},
|
|
528
|
+
setEulerAngles(entityId: number, x: number, y: number, z: number, order: number): void {
|
|
529
|
+
setEulerOn(ent(entityId).m, x, y, z, order); backend.localMatrixChanged?.(entityId)
|
|
530
|
+
},
|
|
531
|
+
setScale(entityId: number, x: number, y: number, z: number): void {
|
|
532
|
+
setScaleOn(ent(entityId).m, x, y, z); backend.localMatrixChanged?.(entityId)
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
setParent(entityId: number, parentEntityId: number, worldPositionStays: boolean): void {
|
|
536
|
+
setParentInternal(entityId, parentEntityId, worldPositionStays)
|
|
537
|
+
},
|
|
538
|
+
setParentNull(entityId: number, worldPositionStays: boolean): void {
|
|
539
|
+
setParentInternal(entityId, 0, worldPositionStays)
|
|
540
|
+
},
|
|
541
|
+
getParent: (entityId: number): number => ent(entityId).parent,
|
|
542
|
+
traverse(entityId: number, callback: (entityId: number) => void): void {
|
|
543
|
+
// Descendants only (the SDK's Node.traverse visits `this` itself before delegating here).
|
|
544
|
+
const walk = (id: number): void => { callback(id); for (const c of ent(id).children) walk(c) }
|
|
545
|
+
for (const c of ent(entityId).children) walk(c)
|
|
546
|
+
},
|
|
547
|
+
findNode(entityId: number, name: string): number {
|
|
548
|
+
// Descendant by name with the Animator's binding rule (boneNames.ts): exact before short name,
|
|
549
|
+
// skin joints before plain nodes, first in tree order otherwise. 0 = absent. Native twin:
|
|
550
|
+
// creator-gl scene.cpp findNode.
|
|
551
|
+
const want = shortName(name)
|
|
552
|
+
let best = 0, bestRank = 4
|
|
553
|
+
const walk = (id: number): void => {
|
|
554
|
+
const e = ent(id)
|
|
555
|
+
const rank = nameMatchRank(e.name, !!e.skinJoint, name, want)
|
|
556
|
+
if (rank < bestRank && rank >= 0) { best = id; bestRank = rank }
|
|
557
|
+
for (const c of e.children) { if (bestRank === 0) return; walk(c) }
|
|
558
|
+
}
|
|
559
|
+
for (const c of ent(entityId).children) { if (bestRank === 0) break; walk(c) }
|
|
560
|
+
return best
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
// --- lights + environment ---
|
|
564
|
+
createSunLight(entityId: number, x: number, y: number, z: number, intensity: number, color: number, shadowsQuality: number, shadowDistance = 100, shadowCascades = 1): void {
|
|
565
|
+
ent(entityId).light = { direction: [x, y, z], intensity, color, shadowsQuality, shadowDistance, shadowCascades }
|
|
566
|
+
},
|
|
567
|
+
setDefaultIbl(sceneId: number, intensity: number): void { const s = scenes.get(sceneId); if (s) s.ibl = intensity },
|
|
568
|
+
setBloomOptions(sceneId: number, enabled: boolean, strength: number): void {
|
|
569
|
+
const s = scenes.get(sceneId)
|
|
570
|
+
if (s) s.bloom = enabled ? { strength } : undefined
|
|
571
|
+
},
|
|
572
|
+
setSkybox(sceneId: number, color: number): void { const s = scenes.get(sceneId); if (s) s.skybox = color },
|
|
573
|
+
setSceneMultiSampleAntiAliasing(sceneId: number, enabled: boolean): void {
|
|
574
|
+
const s = scenes.get(sceneId); if (s) s.antialias = enabled
|
|
575
|
+
},
|
|
576
|
+
// Recorded only: the lite engine draws at canvas resolution (web-lite) or reports JSON (headless) —
|
|
577
|
+
// neither scales its buffers. Kept so the call is not a missing-method error on older bundles.
|
|
578
|
+
setSceneRenderOptions(): void {},
|
|
579
|
+
setMaterialGlobalParameter(): void {},
|
|
580
|
+
getCameraFov(sceneId: number, fovType: number): number {
|
|
581
|
+
const { fov: vfov } = projectionOf(sceneId)
|
|
582
|
+
if (fovType !== 1) return vfov
|
|
583
|
+
const vert = (vfov * Math.PI) / 180
|
|
584
|
+
return (2 * Math.atan(Math.tan(vert / 2) * (width / height)) * 180) / Math.PI
|
|
585
|
+
},
|
|
586
|
+
setCameraProjection(sceneId: number, newFov: number, near: number, far: number): void {
|
|
587
|
+
const s = scenes.get(sceneId)
|
|
588
|
+
if (s) s.projection = { fov: newFov, near, far }
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
// --- transform reads ---
|
|
592
|
+
getMatrix(entityId: number, mat: Float32Array): boolean { mat.set(localMatrixF(entityId)); return true },
|
|
593
|
+
getWorldMatrix(entityId: number, mat: Float32Array): void { mat.set(worldMatrixF(entityId)) },
|
|
594
|
+
getWorldPosition(entityId: number, out: Float32Array): void {
|
|
595
|
+
const w = worldMatrix(entityId)
|
|
596
|
+
out[0] = w[12]; out[1] = w[13]; out[2] = w[14]
|
|
597
|
+
},
|
|
598
|
+
getWorldDirection(entityId: number, mode: number, out: Float32Array): void {
|
|
599
|
+
const w = worldMatrix(entityId)
|
|
600
|
+
out[0] = w[mode * 4]; out[1] = w[mode * 4 + 1]; out[2] = w[mode * 4 + 2]
|
|
601
|
+
},
|
|
602
|
+
getWorldMatrixInverse(entityId: number, mat: Float32Array): void { mat.set(invertTRS(worldMatrixF(entityId))) },
|
|
603
|
+
getCameraViewDirection(sceneId: number, screenX: number, screenY: number, out: Float32Array): void {
|
|
604
|
+
// A real unproject through the camera world matrix + the same projection we report.
|
|
605
|
+
const camId = scenes.get(sceneId)?.camera
|
|
606
|
+
const w = camId ? worldMatrix(camId) : identity()
|
|
607
|
+
const aspect = width / height
|
|
608
|
+
const t = Math.tan((projectionOf(sceneId).fov * Math.PI) / 360)
|
|
609
|
+
const cx = ((screenX / width) * 2 - 1) * t * aspect
|
|
610
|
+
const cy = (1 - (screenY / height) * 2) * t
|
|
611
|
+
const dx = w[0] * cx + w[4] * cy - w[8]
|
|
612
|
+
const dy = w[1] * cx + w[5] * cy - w[9]
|
|
613
|
+
const dz = w[2] * cx + w[6] * cy - w[10]
|
|
614
|
+
const len = Math.hypot(dx, dy, dz) || 1
|
|
615
|
+
out[0] = dx / len; out[1] = dy / len; out[2] = dz / len
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
// --- meshes ---
|
|
619
|
+
setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, _uv1?: Float32Array, colors?: Uint8Array): void {
|
|
620
|
+
const e = ent(entityId)
|
|
621
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
|
|
622
|
+
for (let i = 0; i + 2 < vertices.length; i += 3) {
|
|
623
|
+
const x = vertices[i], y = vertices[i + 1], z = vertices[i + 2]
|
|
624
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x
|
|
625
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y
|
|
626
|
+
if (z < minZ) minZ = z; if (z > maxZ) maxZ = z
|
|
627
|
+
}
|
|
628
|
+
const has = vertices.length >= 3
|
|
629
|
+
const mesh: MeshRec = {
|
|
630
|
+
kind: meshType,
|
|
631
|
+
vertexCount: Math.floor(vertices.length / 3),
|
|
632
|
+
size: has ? [maxX - minX, maxY - minY, maxZ - minZ] : undefined,
|
|
633
|
+
center: has ? [(maxX + minX) / 2, (maxY + minY) / 2, (maxZ + minZ) / 2] : undefined,
|
|
634
|
+
}
|
|
635
|
+
e.mesh = mesh
|
|
636
|
+
e.materials[0] = materialId
|
|
637
|
+
backend.meshSet?.(entityId, materialId, mesh, { vertices, normals, indices, uv, colors })
|
|
638
|
+
},
|
|
639
|
+
setCulling(): void {},
|
|
640
|
+
setRenderPriority(): void {},
|
|
641
|
+
setMaterialDepthTest(): void {},
|
|
642
|
+
setMaterialDepthWrite(): void {},
|
|
643
|
+
setMaterialCulling(): void {},
|
|
644
|
+
// no stencil in the lite renderer: a stencil test passes, a write is dropped (the outline recipe
|
|
645
|
+
// then draws its hull everywhere — an approximation, like lite's lighting)
|
|
646
|
+
setMaterialStencil(): void {},
|
|
647
|
+
setSceneStencil(): void {},
|
|
648
|
+
setCastShadows(entityId: number, v: boolean): void { ent(entityId).castShadows = v },
|
|
649
|
+
setReceiveShadows(entityId: number, v: boolean): void { ent(entityId).receiveShadows = v },
|
|
650
|
+
// creator-gl walks every renderable of the GLB instance; the twin walks the entity subtree
|
|
651
|
+
setGlbShadows(entityId: number, cast: boolean, receive: boolean): void {
|
|
652
|
+
const walk = (id: number): void => {
|
|
653
|
+
const e = ent(id)
|
|
654
|
+
e.castShadows = cast
|
|
655
|
+
e.receiveShadows = receive
|
|
656
|
+
for (const c of e.children) walk(c)
|
|
657
|
+
}
|
|
658
|
+
walk(entityId)
|
|
659
|
+
},
|
|
660
|
+
setCastShadowsGlobal(): void {},
|
|
661
|
+
setReceiveShadowsGlobal(): void {},
|
|
662
|
+
hasMesh: (entityId: number): boolean => ent(entityId).mesh !== undefined,
|
|
663
|
+
|
|
664
|
+
// --- materials + uniforms ---
|
|
665
|
+
createMaterial(systemId: number): number {
|
|
666
|
+
const id = nextMaterial++
|
|
667
|
+
const rec: MaterialRec = { shader: sourceName(systemId) ?? "unknown", uniforms: {} }
|
|
668
|
+
materials.set(id, rec)
|
|
669
|
+
backend.materialCreated?.(id, rec, systemId)
|
|
670
|
+
return id
|
|
671
|
+
},
|
|
672
|
+
/** A built-in material by NAME (`_creator.builtinMaterial`): this engine draws with its own programs,
|
|
673
|
+
* picked by that name — there is no shader file to find. */
|
|
674
|
+
createMaterialNamed(name: string): number {
|
|
675
|
+
const id = nextMaterial++
|
|
676
|
+
const rec: MaterialRec = { shader: name, uniforms: {} }
|
|
677
|
+
materials.set(id, rec)
|
|
678
|
+
backend.materialCreated?.(id, rec)
|
|
679
|
+
return id
|
|
680
|
+
},
|
|
681
|
+
setMaterial(entityId: number, materialId: number, index: number): void { ent(entityId).materials[index] = materialId },
|
|
682
|
+
getMaterial: (entityId: number, index: number): number => ent(entityId).materials[index] ?? 0,
|
|
683
|
+
|
|
684
|
+
setUniformRgb(materialId: number, uniform: string, color: number): void { setUniform(materialId, uniform, { kind: "rgb", v: color }) },
|
|
685
|
+
setUniformRgba(materialId: number, uniform: string, color: number): void { setUniform(materialId, uniform, { kind: "rgba", v: color }) },
|
|
686
|
+
setUniformFloat(materialId: number, uniform: string, value: number): void { setUniform(materialId, uniform, { kind: "float", v: value }) },
|
|
687
|
+
setUniformBoolean(materialId: number, uniform: string, value: boolean): void { setUniform(materialId, uniform, { kind: "bool", v: value }) },
|
|
688
|
+
setUniformTexture(materialId: number, uniform: string, systemId: number): void { setUniform(materialId, uniform, { kind: "texture", v: systemId }) },
|
|
689
|
+
setUniformArray(materialId: number, uniform: string, value: Float32Array): void { setUniform(materialId, uniform, { kind: "array", v: Array.from(value) }) },
|
|
690
|
+
setUniformNull(materialId: number, uniform: string): void { setUniform(materialId, uniform, { kind: "null" }) },
|
|
691
|
+
|
|
692
|
+
// --- textures ---
|
|
693
|
+
createTexture(systemId: number, onComplete: (id: number, width: number, height: number) => void, onReject: () => void): void {
|
|
694
|
+
const buf = opts.getBuffer(systemId)
|
|
695
|
+
if (!buf || buf.byteLength === 0) { onReject(); return }
|
|
696
|
+
const [w, h] = sniffImageSize(buf)
|
|
697
|
+
const id = nextTexture++
|
|
698
|
+
textures.set(id, { source: sourceName(systemId), width: w, height: h })
|
|
699
|
+
onComplete(id, w, h)
|
|
700
|
+
},
|
|
701
|
+
createTextureFromCanvas(surfaceId: number): number {
|
|
702
|
+
const id = nextTexture++
|
|
703
|
+
textures.set(id, { source: `canvas#${surfaceId}`, width: 0, height: 0 })
|
|
704
|
+
return id
|
|
705
|
+
},
|
|
706
|
+
updateTextureFromCanvas(): void {},
|
|
707
|
+
// Raw-pixel textures (Texture.fromPixels / a terrain's control map): the size is recorded, the
|
|
708
|
+
// WebGL backend overrides both with real uploads.
|
|
709
|
+
createTexturePixels(width: number, height: number, channels: number): number {
|
|
710
|
+
const id = nextTexture++
|
|
711
|
+
textures.set(id, { source: `pixels ${width}x${height}x${channels}`, width, height })
|
|
712
|
+
return id
|
|
713
|
+
},
|
|
714
|
+
updateTexturePixels(): void {},
|
|
715
|
+
createMediaPlayerTexture(playerId: number): number {
|
|
716
|
+
const id = nextTexture++
|
|
717
|
+
textures.set(id, { source: `mediaplayer#${playerId}`, width: 0, height: 0 })
|
|
718
|
+
return id
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
// --- camera ---
|
|
722
|
+
attachCameraToAR(sceneId: number, cameraEntityId: number, onProjectionChange: (mat: Float32Array, fov: number) => void): void {
|
|
723
|
+
const s = scenes.get(sceneId)
|
|
724
|
+
if (s) s.camera = cameraEntityId
|
|
725
|
+
// Headless (recording) reports the projection so a captured scene knows its camera; the WebGL
|
|
726
|
+
// backend (like the Filament viewer) ignores this callback and drives projection via the viewport.
|
|
727
|
+
const p = projectionOf(sceneId)
|
|
728
|
+
onProjectionChange(perspective(p.fov, width / height, p.near, p.far), p.fov)
|
|
729
|
+
},
|
|
730
|
+
getDisplaySize: (): Float32Array => Float32Array.of(width, height),
|
|
731
|
+
|
|
732
|
+
// --- visibility / names ---
|
|
733
|
+
setVisible(entityId: number, visible: boolean): void {
|
|
734
|
+
ent(entityId).visible = visible
|
|
735
|
+
backend.visibleChanged?.(entityId, visible)
|
|
736
|
+
// a hidden subtree's particle systems stop simulating (creator-gl renderable.cpp updateVisibleChild)
|
|
737
|
+
const walk = (id: number): void => {
|
|
738
|
+
const e = ent(id)
|
|
739
|
+
if (e.particles) particles.setVisible(id, visibleChain(id))
|
|
740
|
+
for (const c of e.children) walk(c)
|
|
741
|
+
}
|
|
742
|
+
walk(entityId)
|
|
743
|
+
},
|
|
744
|
+
isVisible: (entityId: number): boolean => visibleChain(entityId),
|
|
745
|
+
getName: (entityId: number): string => ent(entityId).name,
|
|
746
|
+
setName(entityId: number, name: string): void { ent(entityId).name = name },
|
|
747
|
+
|
|
748
|
+
// --- GLB models ---
|
|
749
|
+
// Two-stage: the core builds the entity structure (sync, gl-free), then the backend attaches
|
|
750
|
+
// drawables. Headless has no glbAttach → completes synchronously; the WebGL backend's glbAttach is
|
|
751
|
+
// async (GLTFLoader) → onComplete fires once geometry/skins are ready.
|
|
752
|
+
createGlb(systemId: number, onComplete: (entityId: number) => void, onReject?: (err: unknown) => void): void {
|
|
753
|
+
const { rootId, entIds } = buildGlbEntities(systemId)
|
|
754
|
+
if (!backend.glbAttach) { onComplete(rootId); return }
|
|
755
|
+
Promise.resolve(backend.glbAttach(systemId, rootId, entIds)).then(() => onComplete(rootId)).catch((e) => onReject?.(e))
|
|
756
|
+
},
|
|
757
|
+
setGlbCulling(): void {},
|
|
758
|
+
// The model's embedded clip set (docs/animation-plan.md), parsed at createGlb; clones share it. 0 = none.
|
|
759
|
+
getGlbClipSet: (entityId: number): number => entities.get(entityId)?.model?.clipSet ?? 0,
|
|
760
|
+
...anim.methods,
|
|
761
|
+
|
|
762
|
+
setEarlyUpdate(cb: (dt: number) => void): void { earlyUpdate = cb },
|
|
763
|
+
setLateUpdate(cb: (dt: number) => void): void { lateUpdate = cb },
|
|
764
|
+
setFixedUpdate(cb: ((dt: number) => void) | null): void { fixedUpdate = cb },
|
|
765
|
+
setTimeScale(scale: number): void { timeScale = Number.isFinite(scale) && scale > 0 ? scale : 0 },
|
|
766
|
+
...physics.methods,
|
|
767
|
+
...particles.methods,
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// The LOCAL matrix as a concrete Float32Array (native getMatrix reads local, not world).
|
|
771
|
+
function localMatrixF(entityId: number): Float32Array {
|
|
772
|
+
const m = ent(entityId).m
|
|
773
|
+
return m instanceof Float32Array ? m : Float32Array.from(m as ArrayLike<number>)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Inspection camera override (host tooling — the headless twin of creator-gl's setCameraOverride):
|
|
777
|
+
// while set, the frame report + serialized camera project through THIS eye/look-at (and fov)
|
|
778
|
+
// instead of the scene's camera entity. The entity itself is untouched — raycasts still use it.
|
|
779
|
+
let cameraOverride: CameraOverride | null = null
|
|
780
|
+
|
|
781
|
+
return {
|
|
782
|
+
// config
|
|
783
|
+
width, height, fov,
|
|
784
|
+
// state (read by serialize.ts; written by recording.ts)
|
|
785
|
+
entities, scenes, materials, textures,
|
|
786
|
+
cameraOverride: (): CameraOverride | null => cameraOverride,
|
|
787
|
+
setCameraOverride: (o: CameraOverride | null): void => { cameraOverride = o },
|
|
788
|
+
// helpers (used by recording.ts + the WebGL backend)
|
|
789
|
+
ent, newEnt, worldMatrix, worldMatrixF, sourceName, setParentInternal, sceneOf, projectionOf,
|
|
790
|
+
/** Repoint an entity's local matrix at a backend-owned buffer (WebGL uses this for GLB nodes so
|
|
791
|
+
* the entity IS the loader's animated transform — animation/skinning keep working). */
|
|
792
|
+
rebindMatrix: (entityId: number, m: Mat4Like): void => { ent(entityId).m = m },
|
|
793
|
+
// lifecycle
|
|
794
|
+
methods, simulate, tick, hasActiveScene, activeSceneId,
|
|
795
|
+
/** The physics world config the SDK set (serialize.ts), undefined if never configured. */
|
|
796
|
+
physicsConfig: physics.config,
|
|
797
|
+
/** A particle system's index pattern for its current capacity (the WebGL backend's index buffer). */
|
|
798
|
+
particleIndexPattern: particles.indexPattern,
|
|
799
|
+
nextTextureId: () => nextTexture++,
|
|
800
|
+
nextMaterialId: () => nextMaterial++,
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export type SceneCore = ReturnType<typeof createSceneCore>
|
|
805
|
+
|
|
806
|
+
/** Sniff PNG/JPEG pixel dimensions from encoded bytes (headless textures aren't decoded). */
|
|
807
|
+
export const sniffImageSize = (buf: ArrayBuffer): [number, number] => {
|
|
808
|
+
const b = new Uint8Array(buf)
|
|
809
|
+
if (b.length > 24 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) {
|
|
810
|
+
const dv = new DataView(buf)
|
|
811
|
+
return [dv.getUint32(16), dv.getUint32(20)]
|
|
812
|
+
}
|
|
813
|
+
// KTX2 («KTX 20» CR LF 0x1A LF): pixelWidth/pixelHeight little-endian @20/@24.
|
|
814
|
+
if (b.length >= 28 && b[0] === 0xab && b[1] === 0x4b && b[2] === 0x54 && b[3] === 0x58 && b[4] === 0x20 && b[5] === 0x32 && b[6] === 0x30 && b[7] === 0xbb) {
|
|
815
|
+
const dv = new DataView(buf)
|
|
816
|
+
return [dv.getUint32(20, true), dv.getUint32(24, true)]
|
|
817
|
+
}
|
|
818
|
+
if (b.length > 4 && b[0] === 0xff && b[1] === 0xd8) {
|
|
819
|
+
const dv = new DataView(buf)
|
|
820
|
+
let p = 2
|
|
821
|
+
while (p + 9 < b.length) {
|
|
822
|
+
if (b[p] !== 0xff) break
|
|
823
|
+
const marker = b[p + 1]
|
|
824
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
825
|
+
return [dv.getUint16(p + 7), dv.getUint16(p + 5)]
|
|
826
|
+
}
|
|
827
|
+
p += 2 + dv.getUint16(p + 2)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return [0, 0]
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export { copyInto }
|