lecodes-sdk 1.0.0 → 1.1.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/global.d.ts +18 -4
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +40 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +4 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +226 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Model.ts +167 -156
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +61 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -459
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/terrain/Terrain.ts +4 -2
- package/src/inject.ts +12 -2
- package/src/scene/defineScene.ts +72 -62
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
package/src/gl/Material.ts
CHANGED
|
@@ -161,8 +161,6 @@ export type LitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
|
|
|
161
161
|
metallic?: number
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** `Material.lightmapShading` tiers — see the setter. */
|
|
165
|
-
export type LightmapShading = "full" | "baked" | "baked-lite"
|
|
166
164
|
|
|
167
165
|
export class Material {
|
|
168
166
|
/** @internal native material-instance handle. */
|
|
@@ -328,54 +326,50 @@ export class Material {
|
|
|
328
326
|
return m
|
|
329
327
|
}
|
|
330
328
|
|
|
331
|
-
/** The lightmap material (
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* `
|
|
335
|
-
*
|
|
329
|
+
/** The lightmap material (packages/creator-bake): a lit PBR material whose whole DIFFUSE light is the
|
|
330
|
+
* baked irradiance atlas on UV1 (`lightmapLight`, lux) — the real-time sun, lamps and diffuse IBL do not
|
|
331
|
+
* touch it; the specular IBL stays, occluded by the baked sky visibility. Models take it through
|
|
332
|
+
* `Model.load(…, { lightmap: true })`; a static Mesh gets one from here. Parameters use gltfio's names
|
|
333
|
+
* (`baseColorFactor`, `baseColorMap`, `roughnessFactor`, `metallicFactor`) plus `lightmapLight`, `lightmapAux`,
|
|
334
|
+
* `lightmapST`, `lightScale` (the level-wide value `Lightmap.load` sets) and the debug view knobs. */
|
|
336
335
|
static lightmap(): Material {
|
|
337
|
-
// Inline
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
: Material.lightmapShading === "baked-lite"
|
|
341
|
-
? _creatorUtils.fetchLocal("lightmap-baked-lite.filamat")
|
|
342
|
-
: _creatorUtils.fetchLocal("lightmap.filamat")
|
|
343
|
-
const m = new Material({ _id: id } as any)
|
|
344
|
-
m.set("baseColorFactor", "#ffffffff").set("roughnessFactor", 1).set("metallicFactor", 0)
|
|
345
|
-
m.set("lightmapST", [ 1, 1, 0, 0 ]).set("ambientScale", 1).set("sunStrength", 1).set("hasNormalMap", 0)
|
|
346
|
-
// lightScale 0 = no baked point lights until Lightmap.load binds a light atlas / volume
|
|
347
|
-
m.set("bakedAo", 1).set("lightScale", 0)
|
|
348
|
-
m.set("probeMin", [ 0, 0, 0, 0 ]).set("probeInvSize", [ 0, 0, 0, 0 ])
|
|
336
|
+
// Inline literal: the compiler's preload header captures the shader name from it.
|
|
337
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("lightmap.filamat") } as any)
|
|
338
|
+
Material._lightmapDefaults(m)
|
|
349
339
|
return m
|
|
350
340
|
}
|
|
351
341
|
|
|
352
342
|
/** `Material.lightmap()`'s masked twin (`blending: masked`, same shader and parameters): what the engine
|
|
353
343
|
* gives a lightmapped model's alpha-MASK materials. The cutoff comes from the glTF material. */
|
|
354
344
|
static lightmapMasked(): Material {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
: Material.lightmapShading === "baked-lite"
|
|
358
|
-
? _creatorUtils.fetchLocal("lightmap-baked-lite-masked.filamat")
|
|
359
|
-
: _creatorUtils.fetchLocal("lightmap-masked.filamat")
|
|
360
|
-
const m = new Material({ _id: id } as any)
|
|
361
|
-
m.set("baseColorFactor", "#ffffffff").set("roughnessFactor", 1).set("metallicFactor", 0)
|
|
362
|
-
m.set("lightmapST", [ 1, 1, 0, 0 ]).set("ambientScale", 1).set("sunStrength", 1).set("hasNormalMap", 0)
|
|
363
|
-
m.set("bakedAo", 1).set("lightScale", 0)
|
|
364
|
-
m.set("probeMin", [ 0, 0, 0, 0 ]).set("probeInvSize", [ 0, 0, 0, 0 ])
|
|
345
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("lightmap-masked.filamat") } as any)
|
|
346
|
+
Material._lightmapDefaults(m)
|
|
365
347
|
return m
|
|
366
348
|
}
|
|
367
349
|
|
|
368
350
|
/** The terrain splat material (docs/terrain-plan.md §1.5): four albedo (+ normal-map) layers blended by
|
|
369
351
|
* a control map on the terrain's own grid, per-layer `tiling` (metres per repeat) / `roughness` /
|
|
370
|
-
* `normalScale` / `triplanar
|
|
371
|
-
*
|
|
372
|
-
* unset layer is white and an unbaked terrain is fully lit. */
|
|
352
|
+
* `normalScale` / `triplanar`. `Terrain` builds and owns one per terrain; the uniforms are primed here so an
|
|
353
|
+
* unset layer is white. Lit real-time; a BAKED terrain takes `Material.terrainLightmap()`. */
|
|
373
354
|
static terrain(): Material {
|
|
374
355
|
// Inline literal: the compiler's preload header captures the shader name from it.
|
|
375
356
|
const m = new Material({ _id: _creatorUtils.fetchLocal("terrain.filamat") } as any)
|
|
376
357
|
m.set("tiling", [ 8, 8, 8, 8 ]).set("roughness", [ 1, 1, 1, 1 ]).set("normalScale", [ 0, 0, 0, 0 ]).set("triplanar", [ 0, 0, 0, 0 ])
|
|
377
358
|
m.set("gridSize", [ 2, 2 ]).set("cellSize", 1).set("tint", "#ffffff")
|
|
378
|
-
m
|
|
359
|
+
return m
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** `Material.terrain()` for a BAKED terrain (terrain-lightmap.mat): the same layers and uniforms, shaded like
|
|
363
|
+
* `Material.lightmap()` — the whole diffuse light is the baked atlas on the terrain's UV1 (one rect), which
|
|
364
|
+
* `Lightmap.load` binds. A scene file's `terrain:` node that is a lightmap static is built with it; without a
|
|
365
|
+
* bound bake it draws black, like any lightmapped static. No reflection probes (the sampler budget): it reflects the
|
|
366
|
+
* sky through the baked sky visibility. */
|
|
367
|
+
static terrainLightmap(): Material {
|
|
368
|
+
// Inline literal: the compiler's preload header captures the shader name from it.
|
|
369
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("terrain-lightmap.filamat") } as any)
|
|
370
|
+
m.set("tiling", [ 8, 8, 8, 8 ]).set("roughness", [ 1, 1, 1, 1 ]).set("normalScale", [ 0, 0, 0, 0 ]).set("triplanar", [ 0, 0, 0, 0 ])
|
|
371
|
+
m.set("gridSize", [ 2, 2 ]).set("cellSize", 1).set("tint", "#ffffff")
|
|
372
|
+
m.set("lightmapST", [ 1, 1, 0, 0 ]).set("lightScale", 1).set("debugMode", 0).set("debugParams", [ 0.001, 0, 5, 0 ])
|
|
379
373
|
return m
|
|
380
374
|
}
|
|
381
375
|
|
|
@@ -387,23 +381,41 @@ export class Material {
|
|
|
387
381
|
* materials (foliage cards) take it instead of falling back to the ubershader's full lit path. */
|
|
388
382
|
static _lightmapMaskedTemplate(): Material { return (Material._lmMaskedTemplate ??= Material.lightmapMasked()) }
|
|
389
383
|
private static _lmMaskedTemplate: Material | undefined
|
|
384
|
+
/** The FOLIAGE tier (`Model.load(…, { foliage: true })`, see `Foliage`): an unlit shader with its own cheap
|
|
385
|
+
* lighting plus a vertex shader that sways in the wind, bends away from benders and thins out with distance,
|
|
386
|
+
* and a per-copy tint. glTF parameters like `Material.lightmap()` plus `wind` / `sway` / `fade` / `benders`,
|
|
387
|
+
* which the engine writes itself. Not baked by the rewritten bake yet. */
|
|
388
|
+
static foliage(): Material {
|
|
389
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("foliage.filamat") } as any)
|
|
390
|
+
Material._glbDefaults(m)
|
|
391
|
+
return m
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** `Material.foliage()`'s masked twin (`blending: masked`): what the engine gives a foliage model's alpha-MASK
|
|
395
|
+
* materials — the cards. */
|
|
396
|
+
static foliageMasked(): Material {
|
|
397
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("foliage-masked.filamat") } as any)
|
|
398
|
+
Material._glbDefaults(m)
|
|
399
|
+
return m
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** the glTF-side identity values every provider-handed material starts from */
|
|
403
|
+
private static _glbDefaults(m: Material): void {
|
|
404
|
+
m.set("baseColorFactor", "#ffffffff").set("roughnessFactor", 1).set("metallicFactor", 0).set("hasNormalMap", 0)
|
|
405
|
+
}
|
|
390
406
|
|
|
391
|
-
static
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
* shadows on dynamic objects). `"baked"` keeps the baked light and an approximated ambient but drops
|
|
395
|
-
* the lit path: ~40 % cheaper per pixel on a fill-bound GPU. `"baked-lite"` is that minus the normal,
|
|
396
|
-
* metallic/roughness and occlusion map reads (the factors stand in, the atlas keeps the baked AO;
|
|
397
|
-
* emissive still glows): +20 % more at 1080p on the same GPU. Read when a lightmapped model LOADS, so set
|
|
398
|
-
* it before the level (a settings menu applies it on the next level load), like `Texture.maxSize`. */
|
|
399
|
-
static get lightmapShading(): LightmapShading { return Material._lightmapShading }
|
|
400
|
-
static set lightmapShading(mode: LightmapShading) {
|
|
401
|
-
if (mode === Material._lightmapShading) return
|
|
402
|
-
Material._lightmapShading = mode
|
|
403
|
-
Material._lmTemplate = undefined // the next load builds a template on the new shader
|
|
404
|
-
Material._lmMaskedTemplate = undefined
|
|
407
|
+
private static _lightmapDefaults(m: Material): void {
|
|
408
|
+
Material._glbDefaults(m)
|
|
409
|
+
m.set("lightmapST", [ 1, 1, 0, 0 ]).set("lightScale", 1).set("debugMode", 0).set("debugParams", [ 0.001, 0, 5, 0 ])
|
|
405
410
|
}
|
|
406
411
|
|
|
412
|
+
/** @internal the foliage template pair, like the lightmap one (a host that lacks the tier returns id 0 from
|
|
413
|
+
* fetchLocal — `Model.load` then falls back to the lightmap tier). */
|
|
414
|
+
static _foliageTemplate(): Material { return (Material._folTemplate ??= Material.foliage()) }
|
|
415
|
+
private static _folTemplate: Material | undefined
|
|
416
|
+
static _foliageMaskedTemplate(): Material { return (Material._folMaskedTemplate ??= Material.foliageMasked()) }
|
|
417
|
+
private static _folMaskedTemplate: Material | undefined
|
|
418
|
+
|
|
407
419
|
/** Shadow-catcher material (transparent except where shadows fall). */
|
|
408
420
|
static shadow(color: ColorInput = "#000000aa"): Material {
|
|
409
421
|
const m = new Material({ _id: _creatorUtils.fetchLocal("shadow.filamat") } as any)
|
package/src/gl/Model.ts
CHANGED
|
@@ -1,156 +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 (
|
|
38
|
-
_lightmapped = false
|
|
39
|
-
/** @internal
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
m.
|
|
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 (
|
|
128
|
-
* `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
|
|
129
|
-
* --lightmap-uv`)
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// the
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
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
|
+
}
|
package/src/gl/Node.ts
CHANGED
|
@@ -16,6 +16,19 @@ import { ensurePhysicsEvents } from "./physics/physicsEvents"
|
|
|
16
16
|
import { Material } from "./Material"
|
|
17
17
|
import type { CompAxis, CompWriter } from "../core/compWrite"
|
|
18
18
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
19
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
20
|
+
import { CLOCK_GAME, DOM_GL_NODE, eulerValue, quatValue, vec3Value, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
21
|
+
import { tweenBag } from "../animate/tween/Timeline"
|
|
22
|
+
|
|
23
|
+
/** Animatable transform props of a 3D node: a value tweens from the current one, an ARRAY OF
|
|
24
|
+
* VALUES is a keyframe list (`position: [[0,0,0], [0,2,0]]`). */
|
|
25
|
+
export type NodeTweenProps = {
|
|
26
|
+
position?: Vec3Like | Vec3Like[],
|
|
27
|
+
scale?: number | Vec3Like | (number | Vec3Like)[],
|
|
28
|
+
quaternion?: QuatLike | QuatLike[],
|
|
29
|
+
/** Degrees, YXZ; interpolated per axis without shortest-arc, so `[0, 720, 0]` spins twice. */
|
|
30
|
+
eulerAngles?: Vec3Like | Vec3Like[],
|
|
31
|
+
}
|
|
19
32
|
|
|
20
33
|
/** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
|
|
21
34
|
export const nodeRegistry = new Registry<Node>()
|
|
@@ -197,6 +210,32 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
|
|
|
197
210
|
if (this._xf) this._setOwnedRotation()
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
// --- keyframe animation (docs/timeline-plan.md) ---
|
|
214
|
+
/** Tween the transform to the given values — `node.animateTo({ position: [0, 2, 0], duration: 800,
|
|
215
|
+
* easing: 'inOutCubic' })`; arrays of values are keyframes. Runs on the game clock (pauses with
|
|
216
|
+
* the game) unless `clock: 'ui'`. Returns the {@link Animation} handle. Phase 1: plain nodes —
|
|
217
|
+
* a physics-owned node (a body / character) is not routed through its engine object yet. */
|
|
218
|
+
animateTo(props: NodeTweenProps & TweenMeta): Animation {
|
|
219
|
+
return tweenBag(this, props as Record<string, unknown>, true)
|
|
220
|
+
}
|
|
221
|
+
/** Tween FROM the given values to the node's current transform (an entrance). */
|
|
222
|
+
animateFrom(props: NodeTweenProps & TweenMeta): Animation {
|
|
223
|
+
return tweenBag(this, props as Record<string, unknown>, false)
|
|
224
|
+
}
|
|
225
|
+
/** @internal keyframe-core target protocol */
|
|
226
|
+
readonly _tweenClock: number = CLOCK_GAME
|
|
227
|
+
/** @internal */
|
|
228
|
+
_tweenChannel(prop: string): TweenChannel | null {
|
|
229
|
+
const id = () => this.id
|
|
230
|
+
switch (prop) {
|
|
231
|
+
case "position": return { domain: DOM_GL_NODE, id, value: vec3Value }
|
|
232
|
+
case "scale": return { domain: DOM_GL_NODE, id, value: vec3Value }
|
|
233
|
+
case "quaternion": return { domain: DOM_GL_NODE, id, value: quatValue }
|
|
234
|
+
case "eulerAngles": return { domain: DOM_GL_NODE, id, value: eulerValue }
|
|
235
|
+
default: return null
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
200
239
|
get eulerAngles(): Vec3 { return new Mat4(this._sync()).eulerAngles }
|
|
201
240
|
// order 1 = YXZ — the SDK's euler convention (math/quat.ts); the getter also extracts YXZ, so
|
|
202
241
|
// the pair round-trips. (Historically this passed 0/XYZ AND the engine stored the euler matrix
|