lecodes-sdk 1.1.0 → 1.2.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/dist/inject.js +260 -361
- package/dist/types/gl/Light.d.ts +3 -0
- package/dist/types/gl/Particles.d.ts +15 -0
- package/dist/types/gl/animation/Locomotion.d.ts +52 -3
- package/dist/types/gl/terrain/Terrain.d.ts +8 -0
- package/dist/types/runtime/input.d.ts +7 -0
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/src/animate/tween/Animation.ts +378 -378
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +100 -100
- package/src/animate/tween/spec.ts +479 -479
- package/src/audio/audio.ts +161 -161
- package/src/bridges.d.ts +10 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Light.ts +3 -0
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +167 -167
- package/src/gl/Particles.ts +19 -0
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +72 -8
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +29 -0
- package/src/inject.ts +236 -236
- package/src/runtime/input.ts +11 -0
- package/src/scene/gizmos.ts +148 -148
package/src/gl/Mesh.ts
CHANGED
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
// A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/capsule/plane) and
|
|
2
|
-
// Mesh.from for a custom Geometry. A GLB import is a different kind: see Model.load.
|
|
3
|
-
|
|
4
|
-
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
5
|
-
import {
|
|
6
|
-
Geometry,
|
|
7
|
-
type CapsuleOptions,
|
|
8
|
-
type CylinderOptions,
|
|
9
|
-
type PlaneOptions,
|
|
10
|
-
type SphereOptions,
|
|
11
|
-
} from "./Geometry"
|
|
12
|
-
import { Material } from "./Material"
|
|
13
|
-
import { Node } from "./Node"
|
|
14
|
-
|
|
15
|
-
/** Common transform/render options every primitive factory accepts. */
|
|
16
|
-
export type MeshOptions = {
|
|
17
|
-
material?: Material
|
|
18
|
-
position?: Vec3Like
|
|
19
|
-
eulerAngles?: Vec3Like
|
|
20
|
-
scale?: Vec3Like | number
|
|
21
|
-
name?: string
|
|
22
|
-
castShadows?: boolean
|
|
23
|
-
receiveShadows?: boolean
|
|
24
|
-
/** Coarse draw order, 0 (first) … 7 (last); default 4. See `Mesh.renderPriority`. */
|
|
25
|
-
renderPriority?: number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class Mesh extends Node {
|
|
29
|
-
private _geometry?: Geometry
|
|
30
|
-
|
|
31
|
-
constructor(geometry?: Geometry, material?: Material) {
|
|
32
|
-
super()
|
|
33
|
-
if (geometry) {
|
|
34
|
-
const mat = material ?? Material.unlit()
|
|
35
|
-
this._geometry = geometry
|
|
36
|
-
this._materials = [ mat ]
|
|
37
|
-
_creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
override get geometry(): Geometry | undefined { return this._geometry }
|
|
42
|
-
/** Replace the geometry in place — the node, its transform and its material stay, the vertex and
|
|
43
|
-
* index buffers are rebuilt. What an editor overlay or a debug drawer redraws with. */
|
|
44
|
-
setGeometry(geometry: Geometry): this {
|
|
45
|
-
this._geometry = geometry
|
|
46
|
-
_creator.setMesh(this.id, Material.idOf(this._materials![0]!), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
|
|
47
|
-
// the host rebuilds the renderable for a new geometry (creator-gl createMesh destroys + builds), which
|
|
48
|
-
// resets its priority / culling / shadow flags to the defaults — an overlay redrawn every frame lost its
|
|
49
|
-
// renderPriority 7 after one frame and hid inside the model. What was set on this mesh is set again.
|
|
50
|
-
if (this._renderPriority !== undefined) this.renderPriority = this._renderPriority
|
|
51
|
-
if (this._culling !== undefined) this.culling = this._culling
|
|
52
|
-
if (this._castShadows !== undefined) this.castShadows = this._castShadows
|
|
53
|
-
if (this._receiveShadows !== undefined) this.receiveShadows = this._receiveShadows
|
|
54
|
-
return this
|
|
55
|
-
}
|
|
56
|
-
private _renderPriority?: number
|
|
57
|
-
private _culling?: boolean
|
|
58
|
-
private _castShadows?: boolean
|
|
59
|
-
private _receiveShadows?: boolean
|
|
60
|
-
|
|
61
|
-
/** A Mesh always carries a material (slot 0) — see Node.setMaterial for the slot API. */
|
|
62
|
-
override get material(): Material { return this._materials![0]! }
|
|
63
|
-
override set material(m: Material) { this.setMaterial(m, 0) }
|
|
64
|
-
|
|
65
|
-
set culling(v: boolean) { this._culling = v; _creator.setCulling(this.id, v) }
|
|
66
|
-
/** Coarse draw order within the frame: 0 draws first, 7 last, 4 is the default (Filament's
|
|
67
|
-
* renderable priority; within one priority opaque draws sort front-to-back, blended back-to-front).
|
|
68
|
-
* Something drawn over the scene with `Material.depthTest = false` goes in 7, so nothing drawn
|
|
69
|
-
* after it can cover it. Write-only; a host that predates the call ignores it. */
|
|
70
|
-
set renderPriority(v: number) {
|
|
71
|
-
this._renderPriority = v
|
|
72
|
-
if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
|
|
73
|
-
else if (!Mesh._warnedPriority) { Mesh._warnedPriority = true; console.warn("[mesh] this host has no setRenderPriority — renderPriority ignored") }
|
|
74
|
-
}
|
|
75
|
-
private static _warnedPriority = false
|
|
76
|
-
set castShadows(v: boolean) { this._castShadows = v; _creator.setCastShadows(this.id, v) }
|
|
77
|
-
set receiveShadows(v: boolean) { this._receiveShadows = v; _creator.setReceiveShadows(this.id, v) }
|
|
78
|
-
|
|
79
|
-
// --- factories ---
|
|
80
|
-
|
|
81
|
-
static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
|
|
82
|
-
// the size goes into the geometry (not a scale after): the lightmap chart is laid out per face area
|
|
83
|
-
return apply(new Mesh(Geometry.box(options.size ?? 1), options.material), options)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
|
|
87
|
-
return apply(new Mesh(Geometry.sphere(options), options.material), options)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
|
|
91
|
-
return apply(new Mesh(Geometry.cylinder(options), options.material), options)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** A capsule along Y (`radius`, `length` between the cap centres) — a character's or a collider's shape in one mesh. */
|
|
95
|
-
static capsule(options: MeshOptions & CapsuleOptions = {}): Mesh {
|
|
96
|
-
return apply(new Mesh(Geometry.capsule(options), options.material), options)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
|
|
100
|
-
return apply(new Mesh(Geometry.plane(options), options.material), options)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Wrap a custom Geometry. */
|
|
104
|
-
static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
|
|
105
|
-
return apply(new Mesh(geometry, options.material), options)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])
|
|
110
|
-
|
|
111
|
-
const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
|
|
112
|
-
if (o.position) mesh.position = o.position
|
|
113
|
-
if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
|
|
114
|
-
if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
|
|
115
|
-
if (o.name !== undefined) mesh.name = o.name
|
|
116
|
-
if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
|
|
117
|
-
if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
|
|
118
|
-
if (o.renderPriority !== undefined) mesh.renderPriority = o.renderPriority
|
|
119
|
-
return mesh
|
|
120
|
-
}
|
|
1
|
+
// A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/capsule/plane) and
|
|
2
|
+
// Mesh.from for a custom Geometry. A GLB import is a different kind: see Model.load.
|
|
3
|
+
|
|
4
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
5
|
+
import {
|
|
6
|
+
Geometry,
|
|
7
|
+
type CapsuleOptions,
|
|
8
|
+
type CylinderOptions,
|
|
9
|
+
type PlaneOptions,
|
|
10
|
+
type SphereOptions,
|
|
11
|
+
} from "./Geometry"
|
|
12
|
+
import { Material } from "./Material"
|
|
13
|
+
import { Node } from "./Node"
|
|
14
|
+
|
|
15
|
+
/** Common transform/render options every primitive factory accepts. */
|
|
16
|
+
export type MeshOptions = {
|
|
17
|
+
material?: Material
|
|
18
|
+
position?: Vec3Like
|
|
19
|
+
eulerAngles?: Vec3Like
|
|
20
|
+
scale?: Vec3Like | number
|
|
21
|
+
name?: string
|
|
22
|
+
castShadows?: boolean
|
|
23
|
+
receiveShadows?: boolean
|
|
24
|
+
/** Coarse draw order, 0 (first) … 7 (last); default 4. See `Mesh.renderPriority`. */
|
|
25
|
+
renderPriority?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Mesh extends Node {
|
|
29
|
+
private _geometry?: Geometry
|
|
30
|
+
|
|
31
|
+
constructor(geometry?: Geometry, material?: Material) {
|
|
32
|
+
super()
|
|
33
|
+
if (geometry) {
|
|
34
|
+
const mat = material ?? Material.unlit()
|
|
35
|
+
this._geometry = geometry
|
|
36
|
+
this._materials = [ mat ]
|
|
37
|
+
_creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override get geometry(): Geometry | undefined { return this._geometry }
|
|
42
|
+
/** Replace the geometry in place — the node, its transform and its material stay, the vertex and
|
|
43
|
+
* index buffers are rebuilt. What an editor overlay or a debug drawer redraws with. */
|
|
44
|
+
setGeometry(geometry: Geometry): this {
|
|
45
|
+
this._geometry = geometry
|
|
46
|
+
_creator.setMesh(this.id, Material.idOf(this._materials![0]!), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
|
|
47
|
+
// the host rebuilds the renderable for a new geometry (creator-gl createMesh destroys + builds), which
|
|
48
|
+
// resets its priority / culling / shadow flags to the defaults — an overlay redrawn every frame lost its
|
|
49
|
+
// renderPriority 7 after one frame and hid inside the model. What was set on this mesh is set again.
|
|
50
|
+
if (this._renderPriority !== undefined) this.renderPriority = this._renderPriority
|
|
51
|
+
if (this._culling !== undefined) this.culling = this._culling
|
|
52
|
+
if (this._castShadows !== undefined) this.castShadows = this._castShadows
|
|
53
|
+
if (this._receiveShadows !== undefined) this.receiveShadows = this._receiveShadows
|
|
54
|
+
return this
|
|
55
|
+
}
|
|
56
|
+
private _renderPriority?: number
|
|
57
|
+
private _culling?: boolean
|
|
58
|
+
private _castShadows?: boolean
|
|
59
|
+
private _receiveShadows?: boolean
|
|
60
|
+
|
|
61
|
+
/** A Mesh always carries a material (slot 0) — see Node.setMaterial for the slot API. */
|
|
62
|
+
override get material(): Material { return this._materials![0]! }
|
|
63
|
+
override set material(m: Material) { this.setMaterial(m, 0) }
|
|
64
|
+
|
|
65
|
+
set culling(v: boolean) { this._culling = v; _creator.setCulling(this.id, v) }
|
|
66
|
+
/** Coarse draw order within the frame: 0 draws first, 7 last, 4 is the default (Filament's
|
|
67
|
+
* renderable priority; within one priority opaque draws sort front-to-back, blended back-to-front).
|
|
68
|
+
* Something drawn over the scene with `Material.depthTest = false` goes in 7, so nothing drawn
|
|
69
|
+
* after it can cover it. Write-only; a host that predates the call ignores it. */
|
|
70
|
+
set renderPriority(v: number) {
|
|
71
|
+
this._renderPriority = v
|
|
72
|
+
if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
|
|
73
|
+
else if (!Mesh._warnedPriority) { Mesh._warnedPriority = true; console.warn("[mesh] this host has no setRenderPriority — renderPriority ignored") }
|
|
74
|
+
}
|
|
75
|
+
private static _warnedPriority = false
|
|
76
|
+
set castShadows(v: boolean) { this._castShadows = v; _creator.setCastShadows(this.id, v) }
|
|
77
|
+
set receiveShadows(v: boolean) { this._receiveShadows = v; _creator.setReceiveShadows(this.id, v) }
|
|
78
|
+
|
|
79
|
+
// --- factories ---
|
|
80
|
+
|
|
81
|
+
static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
|
|
82
|
+
// the size goes into the geometry (not a scale after): the lightmap chart is laid out per face area
|
|
83
|
+
return apply(new Mesh(Geometry.box(options.size ?? 1), options.material), options)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
|
|
87
|
+
return apply(new Mesh(Geometry.sphere(options), options.material), options)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
|
|
91
|
+
return apply(new Mesh(Geometry.cylinder(options), options.material), options)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A capsule along Y (`radius`, `length` between the cap centres) — a character's or a collider's shape in one mesh. */
|
|
95
|
+
static capsule(options: MeshOptions & CapsuleOptions = {}): Mesh {
|
|
96
|
+
return apply(new Mesh(Geometry.capsule(options), options.material), options)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
|
|
100
|
+
return apply(new Mesh(Geometry.plane(options), options.material), options)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Wrap a custom Geometry. */
|
|
104
|
+
static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
|
|
105
|
+
return apply(new Mesh(geometry, options.material), options)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])
|
|
110
|
+
|
|
111
|
+
const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
|
|
112
|
+
if (o.position) mesh.position = o.position
|
|
113
|
+
if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
|
|
114
|
+
if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
|
|
115
|
+
if (o.name !== undefined) mesh.name = o.name
|
|
116
|
+
if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
|
|
117
|
+
if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
|
|
118
|
+
if (o.renderPriority !== undefined) mesh.renderPriority = o.renderPriority
|
|
119
|
+
return mesh
|
|
120
|
+
}
|
package/src/gl/Model.ts
CHANGED
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
// A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
|
|
2
|
-
// distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
|
|
3
|
-
// model.anim (an Animator over the GLB's clips — crossfades, blend spaces, layers when you need them):
|
|
4
|
-
// const hero = await Model.load(asset('./hero.glb'))
|
|
5
|
-
// hero.anim.play('Run', { loop: true })
|
|
6
|
-
|
|
7
|
-
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
8
|
-
import { Node } from "./Node"
|
|
9
|
-
import { Animator, _pushLod, type LodMode } from "./animation/Animator"
|
|
10
|
-
import { Material } from "./Material"
|
|
11
|
-
|
|
12
|
-
/** One polygon under a screen point — what `Model.pickTriangle` returns. `bones` are the vertex's raw
|
|
13
|
-
* JOINTS_0 / WEIGHTS_0 pairs (weight > 0; empty = unweighted), `bind` its position in mesh space,
|
|
14
|
-
* `world` its skinned position this frame. */
|
|
15
|
-
export interface TrianglePick {
|
|
16
|
-
/** the Model's root entity, and the mesh node's own entity (0 when the host has no entity for it) */
|
|
17
|
-
entity: number
|
|
18
|
-
node: number
|
|
19
|
-
nodeName: string
|
|
20
|
-
mesh: string
|
|
21
|
-
primitive: number
|
|
22
|
-
material: string
|
|
23
|
-
/** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
|
|
24
|
-
triangle: number
|
|
25
|
-
backface: boolean
|
|
26
|
-
distance: number
|
|
27
|
-
point: [number, number, number]
|
|
28
|
-
bary: [number, number, number]
|
|
29
|
-
skin: string
|
|
30
|
-
vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class Model extends Node {
|
|
34
|
-
/** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
|
|
35
|
-
* more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
|
|
36
|
-
declare readonly anim: Animator
|
|
37
|
-
/** @internal loaded through the lightmap material (a baked static); clones inherit it. */
|
|
38
|
-
_lightmapped = false
|
|
39
|
-
/** @internal loaded through the foliage tier (`Foliage`); clones inherit it. */
|
|
40
|
-
_foliage = false
|
|
41
|
-
/** @internal the shadow pair, mirrored here because the bridge writes both at once. */
|
|
42
|
-
_castShadows = true
|
|
43
|
-
/** @internal */
|
|
44
|
-
_receiveShadows = true
|
|
45
|
-
/** @internal */
|
|
46
|
-
_culling = true
|
|
47
|
-
/** @internal -1 = auto (see `lod`) */
|
|
48
|
-
_lodMesh = -1
|
|
49
|
-
|
|
50
|
-
constructor(internalId: number) {
|
|
51
|
-
super(internalId)
|
|
52
|
-
this.aspect(Animator)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Does this model cast a real-time shadow? Unlike Mesh (one renderable) a GLB is a whole
|
|
56
|
-
* hierarchy, so the flag goes to EVERY renderable of the instance. A first-person viewmodel —
|
|
57
|
-
* arms, weapon, attachments — sets it false: it lives in front of the camera and its shadow
|
|
58
|
-
* is never wanted. */
|
|
59
|
-
get castShadows(): boolean { return this._castShadows }
|
|
60
|
-
set castShadows(v: boolean) { this._castShadows = v; this._applyShadows() }
|
|
61
|
-
|
|
62
|
-
/** Is this model lit by other casters' shadows? Same instance-wide reach as castShadows. */
|
|
63
|
-
get receiveShadows(): boolean { return this._receiveShadows }
|
|
64
|
-
set receiveShadows(v: boolean) { this._receiveShadows = v; this._applyShadows() }
|
|
65
|
-
|
|
66
|
-
/** Frustum culling for the whole instance: a model the camera cannot see skips the draw and the
|
|
67
|
-
* shadow pass. ON by default where the host keeps a skinned mesh's bounds honest — the engine refits
|
|
68
|
-
* them to the joints every frame, so a walking, kneeling or ragdolled body is never culled while on
|
|
69
|
-
* screen (`_creator.skinnedCullingSupported`); OFF (always draw) on hosts without that, where
|
|
70
|
-
* Filament would cull an animated body by its bind-pose box. `false` = always draw (a skybox-sized
|
|
71
|
-
* mesh, a debugging aid); `true` forces it on regardless of the host. */
|
|
72
|
-
get culling(): boolean { return this._culling }
|
|
73
|
-
set culling(v: boolean) { this._culling = v; _creator.setGlbCulling(this.id, v) }
|
|
74
|
-
/** Level of detail (docs/lod-plan.md). `'auto'` (default): the engine shows the `_LOD<n>` mesh level
|
|
75
|
-
* that fits the model's size on screen (`lecodes assets doctor --lod` makes them) and scales the
|
|
76
|
-
* animation rate with it; a number 0–3 pins that level for both — `0` = always full detail (a hero,
|
|
77
|
-
* a showcase), `2`/`3` = always cheap (a crowd filler). `model.anim.lod = 'full'` keeps the animation
|
|
78
|
-
* exact while the mesh still switches. No-op on hosts without the LOD pass. */
|
|
79
|
-
get lod(): LodMode { return this._lodMesh < 0 ? "auto" : (this._lodMesh as 0 | 1 | 2 | 3) }
|
|
80
|
-
set lod(v: LodMode) { this._lodMesh = v === "auto" ? -1 : Math.max(0, Math.min(3, Math.round(v))); _pushLod(this) }
|
|
81
|
-
|
|
82
|
-
/** @internal the default for a fresh instance (load / clone), decided once per host. */
|
|
83
|
-
static _cullingDefault(): boolean {
|
|
84
|
-
if (Model._cullDefault === undefined) Model._cullDefault = !!_creator.skinnedCullingSupported?.()
|
|
85
|
-
return Model._cullDefault
|
|
86
|
-
}
|
|
87
|
-
private static _cullDefault: boolean | undefined
|
|
88
|
-
|
|
89
|
-
/** @internal both flags travel together — the bridge walks the instance once. */
|
|
90
|
-
_applyShadows(): void { _creator.setGlbShadows?.(this.id, this._castShadows, this._receiveShadows) }
|
|
91
|
-
|
|
92
|
-
/** DEBUG: the closest polygon of this model under a screen point (logical px — `Input.mouse.position`,
|
|
93
|
-
* a touch event's clientX/Y), tested against the CPU-skinned CURRENT pose, both faces. Names the
|
|
94
|
-
* triangle, its three vertices (bind + skinned positions) and their raw bone weights, so a stretched
|
|
95
|
-
* or misbound polygon can be traced to its binding. One full CPU skin of the model per call: click-rate
|
|
96
|
-
* only. null = miss, or a host without the pick (desktop today). */
|
|
97
|
-
pickTriangle(screenX: number, screenY: number): TrianglePick | null {
|
|
98
|
-
const json = _creator.pickTriangle?.(this.id, screenX, screenY)
|
|
99
|
-
return json ? JSON.parse(json) as TrianglePick : null
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
|
|
103
|
-
* the same parent and scene and sharing this model's current transform. The clone has its own
|
|
104
|
-
* independent animation state (reach it via clone.anim). Mirrors this model's culling flag. */
|
|
105
|
-
clone(): Model {
|
|
106
|
-
const id = _creator.cloneEntity(this.id)
|
|
107
|
-
const m = new Model(id)
|
|
108
|
-
m._lightmapped = this._lightmapped
|
|
109
|
-
m._foliage = this._foliage
|
|
110
|
-
if (!this._culling) { m._culling = false; _creator.setGlbCulling(id, false) } // the engine default is on
|
|
111
|
-
if (this._lodMesh >= 0) { m._lodMesh = this._lodMesh; _pushLod(m) }
|
|
112
|
-
// the clone is a FRESH gltfio instance — it comes back with the asset's own shadow flags,
|
|
113
|
-
// not this model's, so a cleared flag has to be re-applied
|
|
114
|
-
m._castShadows = this._castShadows
|
|
115
|
-
m._receiveShadows = this._receiveShadows
|
|
116
|
-
if (!this._castShadows || !this._receiveShadows) m._applyShadows()
|
|
117
|
-
return m
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
|
|
121
|
-
static load(
|
|
122
|
-
source: string | FetchResponse,
|
|
123
|
-
options: {
|
|
124
|
-
/** Frustum culling — see `culling` (default: on where the host refits skinned bounds, else off). */
|
|
125
|
-
culling?: boolean
|
|
126
|
-
onProgress?: (p: { loaded: number, total?: number }) => void
|
|
127
|
-
/** Baked lighting (packages/creator-bake). `true` = a STATIC: loads through the lightmap material so
|
|
128
|
-
* `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
|
|
129
|
-
* --lightmap-uv`) and takes nothing from the real-time lights once the bake applies. Omitted / `false` =
|
|
130
|
-
* the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
|
|
131
|
-
lightmap?: boolean
|
|
132
|
-
/** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
|
|
133
|
-
* per-copy tint. Hosts without the tier fall back to the standard shader. */
|
|
134
|
-
foliage?: boolean
|
|
135
|
-
} = {},
|
|
136
|
-
): Promise<Model> {
|
|
137
|
-
if (typeof source === "string") {
|
|
138
|
-
return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
|
|
139
|
-
if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`))
|
|
140
|
-
return Model.load(resp, options)
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
return new Promise<Model>((resolve, reject) => {
|
|
144
|
-
// the provider path: a foliage model takes the vegetation tier's pair when the host ships it (fetchLocal
|
|
145
|
-
// id 0 = it does not), a static the lightmap material + its masked twin; anything else the ubershader
|
|
146
|
-
const foliageWanted = !!options.foliage && !!_creator.setNextGlbLightmapped
|
|
147
|
-
let foliage = false
|
|
148
|
-
if (foliageWanted) {
|
|
149
|
-
const fol = Material.idOf(Material._foliageTemplate())
|
|
150
|
-
const folMasked = fol ? Material.idOf(Material._foliageMaskedTemplate()) : 0
|
|
151
|
-
foliage = fol > 0 && folMasked > 0
|
|
152
|
-
if (foliage) _creator.setNextGlbLightmapped!(fol, folMasked)
|
|
153
|
-
}
|
|
154
|
-
const lightmapped = !foliage && !!options.lightmap && !!_creator.setNextGlbLightmapped
|
|
155
|
-
if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
|
|
156
|
-
_creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
|
|
157
|
-
if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
|
|
158
|
-
const model = new Model(entityId)
|
|
159
|
-
model._lightmapped = lightmapped
|
|
160
|
-
model._foliage = foliage
|
|
161
|
-
const culling = options.culling ?? Model._cullingDefault()
|
|
162
|
-
if (!culling) { model._culling = false; _creator.setGlbCulling(entityId, false) }
|
|
163
|
-
resolve(model)
|
|
164
|
-
}, reject)
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
}
|
|
1
|
+
// A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
|
|
2
|
+
// distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
|
|
3
|
+
// model.anim (an Animator over the GLB's clips — crossfades, blend spaces, layers when you need them):
|
|
4
|
+
// const hero = await Model.load(asset('./hero.glb'))
|
|
5
|
+
// hero.anim.play('Run', { loop: true })
|
|
6
|
+
|
|
7
|
+
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
8
|
+
import { Node } from "./Node"
|
|
9
|
+
import { Animator, _pushLod, type LodMode } from "./animation/Animator"
|
|
10
|
+
import { Material } from "./Material"
|
|
11
|
+
|
|
12
|
+
/** One polygon under a screen point — what `Model.pickTriangle` returns. `bones` are the vertex's raw
|
|
13
|
+
* JOINTS_0 / WEIGHTS_0 pairs (weight > 0; empty = unweighted), `bind` its position in mesh space,
|
|
14
|
+
* `world` its skinned position this frame. */
|
|
15
|
+
export interface TrianglePick {
|
|
16
|
+
/** the Model's root entity, and the mesh node's own entity (0 when the host has no entity for it) */
|
|
17
|
+
entity: number
|
|
18
|
+
node: number
|
|
19
|
+
nodeName: string
|
|
20
|
+
mesh: string
|
|
21
|
+
primitive: number
|
|
22
|
+
material: string
|
|
23
|
+
/** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
|
|
24
|
+
triangle: number
|
|
25
|
+
backface: boolean
|
|
26
|
+
distance: number
|
|
27
|
+
point: [number, number, number]
|
|
28
|
+
bary: [number, number, number]
|
|
29
|
+
skin: string
|
|
30
|
+
vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Model extends Node {
|
|
34
|
+
/** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
|
|
35
|
+
* more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
|
|
36
|
+
declare readonly anim: Animator
|
|
37
|
+
/** @internal loaded through the lightmap material (a baked static); clones inherit it. */
|
|
38
|
+
_lightmapped = false
|
|
39
|
+
/** @internal loaded through the foliage tier (`Foliage`); clones inherit it. */
|
|
40
|
+
_foliage = false
|
|
41
|
+
/** @internal the shadow pair, mirrored here because the bridge writes both at once. */
|
|
42
|
+
_castShadows = true
|
|
43
|
+
/** @internal */
|
|
44
|
+
_receiveShadows = true
|
|
45
|
+
/** @internal */
|
|
46
|
+
_culling = true
|
|
47
|
+
/** @internal -1 = auto (see `lod`) */
|
|
48
|
+
_lodMesh = -1
|
|
49
|
+
|
|
50
|
+
constructor(internalId: number) {
|
|
51
|
+
super(internalId)
|
|
52
|
+
this.aspect(Animator)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Does this model cast a real-time shadow? Unlike Mesh (one renderable) a GLB is a whole
|
|
56
|
+
* hierarchy, so the flag goes to EVERY renderable of the instance. A first-person viewmodel —
|
|
57
|
+
* arms, weapon, attachments — sets it false: it lives in front of the camera and its shadow
|
|
58
|
+
* is never wanted. */
|
|
59
|
+
get castShadows(): boolean { return this._castShadows }
|
|
60
|
+
set castShadows(v: boolean) { this._castShadows = v; this._applyShadows() }
|
|
61
|
+
|
|
62
|
+
/** Is this model lit by other casters' shadows? Same instance-wide reach as castShadows. */
|
|
63
|
+
get receiveShadows(): boolean { return this._receiveShadows }
|
|
64
|
+
set receiveShadows(v: boolean) { this._receiveShadows = v; this._applyShadows() }
|
|
65
|
+
|
|
66
|
+
/** Frustum culling for the whole instance: a model the camera cannot see skips the draw and the
|
|
67
|
+
* shadow pass. ON by default where the host keeps a skinned mesh's bounds honest — the engine refits
|
|
68
|
+
* them to the joints every frame, so a walking, kneeling or ragdolled body is never culled while on
|
|
69
|
+
* screen (`_creator.skinnedCullingSupported`); OFF (always draw) on hosts without that, where
|
|
70
|
+
* Filament would cull an animated body by its bind-pose box. `false` = always draw (a skybox-sized
|
|
71
|
+
* mesh, a debugging aid); `true` forces it on regardless of the host. */
|
|
72
|
+
get culling(): boolean { return this._culling }
|
|
73
|
+
set culling(v: boolean) { this._culling = v; _creator.setGlbCulling(this.id, v) }
|
|
74
|
+
/** Level of detail (docs/lod-plan.md). `'auto'` (default): the engine shows the `_LOD<n>` mesh level
|
|
75
|
+
* that fits the model's size on screen (`lecodes assets doctor --lod` makes them) and scales the
|
|
76
|
+
* animation rate with it; a number 0–3 pins that level for both — `0` = always full detail (a hero,
|
|
77
|
+
* a showcase), `2`/`3` = always cheap (a crowd filler). `model.anim.lod = 'full'` keeps the animation
|
|
78
|
+
* exact while the mesh still switches. No-op on hosts without the LOD pass. */
|
|
79
|
+
get lod(): LodMode { return this._lodMesh < 0 ? "auto" : (this._lodMesh as 0 | 1 | 2 | 3) }
|
|
80
|
+
set lod(v: LodMode) { this._lodMesh = v === "auto" ? -1 : Math.max(0, Math.min(3, Math.round(v))); _pushLod(this) }
|
|
81
|
+
|
|
82
|
+
/** @internal the default for a fresh instance (load / clone), decided once per host. */
|
|
83
|
+
static _cullingDefault(): boolean {
|
|
84
|
+
if (Model._cullDefault === undefined) Model._cullDefault = !!_creator.skinnedCullingSupported?.()
|
|
85
|
+
return Model._cullDefault
|
|
86
|
+
}
|
|
87
|
+
private static _cullDefault: boolean | undefined
|
|
88
|
+
|
|
89
|
+
/** @internal both flags travel together — the bridge walks the instance once. */
|
|
90
|
+
_applyShadows(): void { _creator.setGlbShadows?.(this.id, this._castShadows, this._receiveShadows) }
|
|
91
|
+
|
|
92
|
+
/** DEBUG: the closest polygon of this model under a screen point (logical px — `Input.mouse.position`,
|
|
93
|
+
* a touch event's clientX/Y), tested against the CPU-skinned CURRENT pose, both faces. Names the
|
|
94
|
+
* triangle, its three vertices (bind + skinned positions) and their raw bone weights, so a stretched
|
|
95
|
+
* or misbound polygon can be traced to its binding. One full CPU skin of the model per call: click-rate
|
|
96
|
+
* only. null = miss, or a host without the pick (desktop today). */
|
|
97
|
+
pickTriangle(screenX: number, screenY: number): TrianglePick | null {
|
|
98
|
+
const json = _creator.pickTriangle?.(this.id, screenX, screenY)
|
|
99
|
+
return json ? JSON.parse(json) as TrianglePick : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
|
|
103
|
+
* the same parent and scene and sharing this model's current transform. The clone has its own
|
|
104
|
+
* independent animation state (reach it via clone.anim). Mirrors this model's culling flag. */
|
|
105
|
+
clone(): Model {
|
|
106
|
+
const id = _creator.cloneEntity(this.id)
|
|
107
|
+
const m = new Model(id)
|
|
108
|
+
m._lightmapped = this._lightmapped
|
|
109
|
+
m._foliage = this._foliage
|
|
110
|
+
if (!this._culling) { m._culling = false; _creator.setGlbCulling(id, false) } // the engine default is on
|
|
111
|
+
if (this._lodMesh >= 0) { m._lodMesh = this._lodMesh; _pushLod(m) }
|
|
112
|
+
// the clone is a FRESH gltfio instance — it comes back with the asset's own shadow flags,
|
|
113
|
+
// not this model's, so a cleared flag has to be re-applied
|
|
114
|
+
m._castShadows = this._castShadows
|
|
115
|
+
m._receiveShadows = this._receiveShadows
|
|
116
|
+
if (!this._castShadows || !this._receiveShadows) m._applyShadows()
|
|
117
|
+
return m
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
|
|
121
|
+
static load(
|
|
122
|
+
source: string | FetchResponse,
|
|
123
|
+
options: {
|
|
124
|
+
/** Frustum culling — see `culling` (default: on where the host refits skinned bounds, else off). */
|
|
125
|
+
culling?: boolean
|
|
126
|
+
onProgress?: (p: { loaded: number, total?: number }) => void
|
|
127
|
+
/** Baked lighting (packages/creator-bake). `true` = a STATIC: loads through the lightmap material so
|
|
128
|
+
* `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
|
|
129
|
+
* --lightmap-uv`) and takes nothing from the real-time lights once the bake applies. Omitted / `false` =
|
|
130
|
+
* the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
|
|
131
|
+
lightmap?: boolean
|
|
132
|
+
/** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
|
|
133
|
+
* per-copy tint. Hosts without the tier fall back to the standard shader. */
|
|
134
|
+
foliage?: boolean
|
|
135
|
+
} = {},
|
|
136
|
+
): Promise<Model> {
|
|
137
|
+
if (typeof source === "string") {
|
|
138
|
+
return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
|
|
139
|
+
if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`))
|
|
140
|
+
return Model.load(resp, options)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
return new Promise<Model>((resolve, reject) => {
|
|
144
|
+
// the provider path: a foliage model takes the vegetation tier's pair when the host ships it (fetchLocal
|
|
145
|
+
// id 0 = it does not), a static the lightmap material + its masked twin; anything else the ubershader
|
|
146
|
+
const foliageWanted = !!options.foliage && !!_creator.setNextGlbLightmapped
|
|
147
|
+
let foliage = false
|
|
148
|
+
if (foliageWanted) {
|
|
149
|
+
const fol = Material.idOf(Material._foliageTemplate())
|
|
150
|
+
const folMasked = fol ? Material.idOf(Material._foliageMaskedTemplate()) : 0
|
|
151
|
+
foliage = fol > 0 && folMasked > 0
|
|
152
|
+
if (foliage) _creator.setNextGlbLightmapped!(fol, folMasked)
|
|
153
|
+
}
|
|
154
|
+
const lightmapped = !foliage && !!options.lightmap && !!_creator.setNextGlbLightmapped
|
|
155
|
+
if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
|
|
156
|
+
_creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
|
|
157
|
+
if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
|
|
158
|
+
const model = new Model(entityId)
|
|
159
|
+
model._lightmapped = lightmapped
|
|
160
|
+
model._foliage = foliage
|
|
161
|
+
const culling = options.culling ?? Model._cullingDefault()
|
|
162
|
+
if (!culling) { model._culling = false; _creator.setGlbCulling(entityId, false) }
|
|
163
|
+
resolve(model)
|
|
164
|
+
}, reject)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|