lecodes-sdk 1.0.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/global.d.ts +18 -4
- package/dist/inject.js +260 -361
- 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 +28 -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 +55 -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/Locomotion.d.ts +52 -3
- 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 +12 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/runtime/input.d.ts +7 -0
- 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/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- 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/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/audio/audio.ts +161 -161
- package/src/bridges.d.ts +235 -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/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +49 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +23 -12
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +80 -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 +35 -12
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Locomotion.ts +72 -8
- 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/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +33 -2
- package/src/inject.ts +236 -226
- package/src/runtime/input.ts +11 -0
- package/src/scene/defineScene.ts +72 -62
- package/src/scene/gizmos.ts +148 -148
- 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/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
|
@@ -34,11 +34,10 @@ export class Model extends Node {
|
|
|
34
34
|
/** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
|
|
35
35
|
* more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
|
|
36
36
|
declare readonly anim: Animator
|
|
37
|
-
/** @internal loaded through the lightmap material (
|
|
37
|
+
/** @internal loaded through the lightmap material (a baked static); clones inherit it. */
|
|
38
38
|
_lightmapped = false
|
|
39
|
-
/** @internal
|
|
40
|
-
|
|
41
|
-
static _lightmapDefault: false | "dynamic" = false
|
|
39
|
+
/** @internal loaded through the foliage tier (`Foliage`); clones inherit it. */
|
|
40
|
+
_foliage = false
|
|
42
41
|
/** @internal the shadow pair, mirrored here because the bridge writes both at once. */
|
|
43
42
|
_castShadows = true
|
|
44
43
|
/** @internal */
|
|
@@ -107,6 +106,7 @@ export class Model extends Node {
|
|
|
107
106
|
const id = _creator.cloneEntity(this.id)
|
|
108
107
|
const m = new Model(id)
|
|
109
108
|
m._lightmapped = this._lightmapped
|
|
109
|
+
m._foliage = this._foliage
|
|
110
110
|
if (!this._culling) { m._culling = false; _creator.setGlbCulling(id, false) } // the engine default is on
|
|
111
111
|
if (this._lodMesh >= 0) { m._lodMesh = this._lodMesh; _pushLod(m) }
|
|
112
112
|
// the clone is a FRESH gltfio instance — it comes back with the asset's own shadow flags,
|
|
@@ -124,12 +124,14 @@ export class Model extends Node {
|
|
|
124
124
|
/** Frustum culling — see `culling` (default: on where the host refits skinned bounds, else off). */
|
|
125
125
|
culling?: boolean
|
|
126
126
|
onProgress?: (p: { loaded: number, total?: number }) => void
|
|
127
|
-
/** Baked lighting (
|
|
127
|
+
/** Baked lighting (packages/creator-bake). `true` = a STATIC: loads through the lightmap material so
|
|
128
128
|
* `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
|
|
129
|
-
* --lightmap-uv`)
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
133
135
|
} = {},
|
|
134
136
|
): Promise<Model> {
|
|
135
137
|
if (typeof source === "string") {
|
|
@@ -139,14 +141,23 @@ export class Model extends Node {
|
|
|
139
141
|
})
|
|
140
142
|
}
|
|
141
143
|
return new Promise<Model>((resolve, reject) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
145
155
|
if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
|
|
146
156
|
_creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
|
|
147
157
|
if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
|
|
148
158
|
const model = new Model(entityId)
|
|
149
159
|
model._lightmapped = lightmapped
|
|
160
|
+
model._foliage = foliage
|
|
150
161
|
const culling = options.culling ?? Model._cullingDefault()
|
|
151
162
|
if (!culling) { model._culling = false; _creator.setGlbCulling(entityId, false) }
|
|
152
163
|
resolve(model)
|
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
|