lecodes-sdk 2.3.0 → 2.4.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 +4 -0
- package/dist/types/gl/Foliage.d.ts +5 -0
- package/dist/types/gl/Lightmap.d.ts +5 -3
- package/dist/types/gl/Material.d.ts +6 -0
- package/dist/types/gl/Scatter.d.ts +25 -0
- package/dist/types/gl/terrain/Terrain.d.ts +5 -0
- package/dist/types/inject.d.ts +2 -0
- package/dist/types/runtime/device.d.ts +20 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/audio/support.ts +3 -1
- package/src/bridges/audio.d.ts +23 -18
- package/src/bridges/device.d.ts +19 -0
- package/src/bridges/gl.d.ts +11 -0
- package/src/compile/serverSplit.ts +8 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +7 -0
- package/src/gl/Light.ts +257 -257
- package/src/gl/Lightmap.ts +8 -3
- package/src/gl/Material.ts +10 -1
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Scatter.ts +67 -0
- package/src/gl/animation/DynamicBone.ts +486 -486
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +17 -0
- package/src/inject.ts +2 -0
- package/src/runtime/device.ts +29 -0
- package/src/version.ts +1 -1
package/src/gl/Lightmap.ts
CHANGED
|
@@ -221,13 +221,18 @@ export class Lightmap {
|
|
|
221
221
|
/** The bake bound last (`load`): what `probe` reads rects from. */
|
|
222
222
|
private static bound: Bound | null = null
|
|
223
223
|
|
|
224
|
-
/** Register static geometry — a Model, a Mesh, or any node whose subtree holds them. Statics are
|
|
225
|
-
* both receivers and occluders in the bake
|
|
226
|
-
*
|
|
224
|
+
/** Register static geometry — a Model, a Mesh, a Terrain's node, or any node whose subtree holds them. Statics are
|
|
225
|
+
* both receivers and occluders in the bake (a model without lightmap UVs, or with materials the lightmap shader
|
|
226
|
+
* declines — alpha cards — is an occluder alone; a terrain is ONE rect, built with `Material.terrainLightmap()`).
|
|
227
|
+
* `key` names the entry in the .bake (default: the node's name + a running number, `container#3`); pass one when
|
|
228
|
+
* names are not stable. */
|
|
227
229
|
static add(node: Node, key?: string): Node {
|
|
228
230
|
const base = key ?? `${node.name || "node"}#${Lightmap.next(node.name || "node")}`
|
|
229
231
|
const found: Entry[] = []
|
|
230
232
|
const walk = (n: Node): void => {
|
|
233
|
+
// the node `Terrain.create` returned carries its terrain (Terrain.of reads the same field): the host keeps the
|
|
234
|
+
// terrain's triangles under this node's id for the bake and binds the rect to its chunks
|
|
235
|
+
if ((n as unknown as { _terrain?: unknown })._terrain) { found.push({ key: base, ground: n }); return }
|
|
231
236
|
if (n instanceof Model) { found.push({ key: base, model: n }); return }
|
|
232
237
|
if (n instanceof Mesh) { found.push({ key: base, mesh: n }); return }
|
|
233
238
|
for (const c of n.children) walk(c)
|
package/src/gl/Material.ts
CHANGED
|
@@ -451,8 +451,17 @@ export class Material {
|
|
|
451
451
|
return Material._folTemplate
|
|
452
452
|
}
|
|
453
453
|
private static _folTemplate: Material | null | undefined
|
|
454
|
+
/** Which material a foliage model's CARDS (its alpha-MASK materials) take: `"full"` = `foliage-masked`, the
|
|
455
|
+
* shader with the card's own plane, normal map, edge fade and the ground's baked light; `"lean"` =
|
|
456
|
+
* `foliage-cards`, the same wind with the lighting moved to the vertex (one texture read, the sun's shadow
|
|
457
|
+
* kept) — a third of a forest frame less on a phone-class GPU (2026-10-18). Read when the first foliage
|
|
458
|
+
* model loads; set it before. */
|
|
459
|
+
static foliageCards: "full" | "lean" = "full"
|
|
454
460
|
static _foliageMaskedTemplate(): Material | null {
|
|
455
|
-
if (Material._folMaskedTemplate === undefined)
|
|
461
|
+
if (Material._folMaskedTemplate === undefined) {
|
|
462
|
+
const lean = Material.foliageCards === "lean" ? _creator.builtinMaterial("foliage-cards") : null
|
|
463
|
+
Material._folMaskedTemplate = Material._glbTemplate(lean ?? _creator.builtinMaterial("foliage-masked"))
|
|
464
|
+
}
|
|
456
465
|
return Material._folMaskedTemplate
|
|
457
466
|
}
|
|
458
467
|
private static _folMaskedTemplate: Material | null | undefined
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// A loaded model drawn many times — grass, ground cover, a bush kind — by hardware instancing (2026-10-18, the
|
|
2
|
+
// Forest's grass): `new Scatter(kind, placements)` lays the kind's level-0 meshes out at every placement with the
|
|
3
|
+
// kind's OWN material instances, so a foliage kind keeps its wind, its touch, the ground's baked light and its
|
|
4
|
+
// distance fade, and 1 778 clumps cost ~60 draws instead of 1 778. The engine buckets the placements into cells
|
|
5
|
+
// and splits a cell into chunks of the platform's per-draw cap (64 native); a cell past the kind's fade distance
|
|
6
|
+
// leaves the scene. The chunks are children of this node: add it to a scene, move it, destroy it like any node.
|
|
7
|
+
//
|
|
8
|
+
// const grass = await Model.load(asset('./kinds/grass.glb'), { foliage: true }) // the KIND: loaded, not added
|
|
9
|
+
// scene.add(new Scatter(grass, clumps)) // clumps: { position, yaw, scale }[]
|
|
10
|
+
//
|
|
11
|
+
// The kind itself need not be in a scene. Placements are fixed at construction — a carpet, not a crowd that moves
|
|
12
|
+
// (InstancedMesh is the one with live transforms). On a host without the engine call the placements become clones
|
|
13
|
+
// of the model under this node, the same picture at the old cost.
|
|
14
|
+
|
|
15
|
+
import { Node } from "./Node"
|
|
16
|
+
import type { Model } from "./Model"
|
|
17
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
18
|
+
|
|
19
|
+
/** One placement: where the kind's origin stands, its yaw about +Y in degrees, its uniform scale. */
|
|
20
|
+
export type ScatterPlacement = {
|
|
21
|
+
position: Vec3Like
|
|
22
|
+
/** Degrees, default 0. */
|
|
23
|
+
yaw?: number
|
|
24
|
+
/** Uniform, default 1. */
|
|
25
|
+
scale?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ScatterOptions = {
|
|
29
|
+
/** The cell edge in metres the placements are bucketed by (one draw per 64 placements of a cell). Default 16. */
|
|
30
|
+
cell?: number
|
|
31
|
+
/** Whether the copies cast the sun's shadow. Default false: ground cover never does. */
|
|
32
|
+
castShadows?: boolean
|
|
33
|
+
name?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class Scatter extends Node {
|
|
37
|
+
/** Placements given. */
|
|
38
|
+
readonly count: number
|
|
39
|
+
/** Renderables the engine made (0 on a host that cloned instead). */
|
|
40
|
+
readonly draws: number
|
|
41
|
+
|
|
42
|
+
constructor(kind: Model, placements: readonly ScatterPlacement[], options: ScatterOptions = {}) {
|
|
43
|
+
super()
|
|
44
|
+
this.count = placements.length
|
|
45
|
+
if (options.name !== undefined) this.name = options.name
|
|
46
|
+
const data = new Float32Array(placements.length * 5)
|
|
47
|
+
for (let i = 0; i < placements.length; i++) {
|
|
48
|
+
const p = placements[i]
|
|
49
|
+
data[i * 5] = cx(p.position); data[i * 5 + 1] = cy(p.position); data[i * 5 + 2] = cz(p.position)
|
|
50
|
+
data[i * 5 + 3] = p.yaw ?? 0
|
|
51
|
+
data[i * 5 + 4] = p.scale ?? 1
|
|
52
|
+
}
|
|
53
|
+
const made = placements.length > 0 ? _creator.createGlbScatter?.(this.id, kind.id, data, options.cell ?? 16, options.castShadows ?? false) : 0
|
|
54
|
+
this.draws = made ?? 0
|
|
55
|
+
if (made === undefined && placements.length > 0) {
|
|
56
|
+
// no engine call on this host: the old way, a clone per placement under this node
|
|
57
|
+
for (const p of placements) {
|
|
58
|
+
const m = kind.clone()
|
|
59
|
+
m.position = p.position
|
|
60
|
+
m.eulerAngles = [ 0, p.yaw ?? 0, 0 ]
|
|
61
|
+
m.scale = p.scale ?? 1
|
|
62
|
+
if (!(options.castShadows ?? false)) m.castShadows = false
|
|
63
|
+
this.add(m)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|