lecodes-cli 0.17.0 → 0.17.1
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/index.js +68 -10
- package/package.json +2 -2
- package/runtime/sdk/gl/IK.ts +174 -170
- package/runtime/sdk/gl/InstancedMesh.ts +132 -0
- package/runtime/sdk/gl/Node.ts +285 -272
- package/runtime/sdk/gl/Particles.ts +625 -546
- package/runtime/sdk/inject.ts +162 -160
- package/runtime/sdk-types.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// One geometry drawn many times — grass tufts, crates, debris, coins, a crowd of identical props —
|
|
2
|
+
// with a transform per instance and a handful of draw calls. Hardware instancing (Filament's
|
|
3
|
+
// InstanceBuffer): the engine applies each instance's local matrix, so any regular material works.
|
|
4
|
+
//
|
|
5
|
+
// Transforms live in a CPU-side Float32Array (16 floats per instance, column-major, local to this
|
|
6
|
+
// node). `setAt` / `hideAt` / `setMatrixAt` edit it and track the dirty range; `commit()` uploads
|
|
7
|
+
// that range in one bridge call — call it once per frame after your edits, not per instance.
|
|
8
|
+
// The native side splits the instances into chunks of the platform's per-draw cap (64 native,
|
|
9
|
+
// 8 on WebGL) on child entities, and keeps a bounding box per chunk so frustum culling stays on.
|
|
10
|
+
|
|
11
|
+
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
12
|
+
import { Quat, type QuatLike } from "../math/quat"
|
|
13
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
14
|
+
import type { Geometry } from "./Geometry"
|
|
15
|
+
import { Material } from "./Material"
|
|
16
|
+
import type { MeshOptions } from "./Mesh"
|
|
17
|
+
import { Node } from "./Node"
|
|
18
|
+
|
|
19
|
+
/** One instance's placement, local to the InstancedMesh node. Omitted fields mean identity. */
|
|
20
|
+
export type InstanceTransform = {
|
|
21
|
+
position?: Vec3Like
|
|
22
|
+
/** Quaternion; wins over `eulerAngles`. */
|
|
23
|
+
rotation?: QuatLike
|
|
24
|
+
/** Degrees (YXZ), like `Node.eulerAngles`. */
|
|
25
|
+
eulerAngles?: Vec3Like
|
|
26
|
+
scale?: Vec3Like | number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEG = Math.PI / 180
|
|
30
|
+
|
|
31
|
+
export class InstancedMesh extends Node {
|
|
32
|
+
/** Instance capacity, fixed at construction. Unset instances are hidden. */
|
|
33
|
+
readonly count: number
|
|
34
|
+
private _geometry: Geometry
|
|
35
|
+
private _material: Material
|
|
36
|
+
private _data: Float32Array
|
|
37
|
+
private _lo = Infinity
|
|
38
|
+
private _hi = -1
|
|
39
|
+
|
|
40
|
+
constructor(geometry: Geometry, count: number, options: MeshOptions = {}) {
|
|
41
|
+
super()
|
|
42
|
+
if (!(count >= 1)) throw new Error("InstancedMesh: count must be ≥ 1")
|
|
43
|
+
this.count = Math.floor(count)
|
|
44
|
+
this._geometry = geometry
|
|
45
|
+
this._material = options.material ?? Material.unlit()
|
|
46
|
+
this._data = new Float32Array(16 * this.count)
|
|
47
|
+
_creator.createInstancedMesh(this.id, Material.idOf(this._material), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, this.count)
|
|
48
|
+
if (options.position) this.position = options.position
|
|
49
|
+
if (options.eulerAngles) this.eulerAngles = options.eulerAngles
|
|
50
|
+
if (options.scale !== undefined) this.scale = typeof options.scale === "number" ? [ options.scale, options.scale, options.scale ] : options.scale
|
|
51
|
+
if (options.name !== undefined) this.name = options.name
|
|
52
|
+
if (options.castShadows !== undefined || options.receiveShadows !== undefined) {
|
|
53
|
+
_creator.setInstancedMeshShadows(this.id, options.castShadows ?? true, options.receiveShadows ?? true)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override get geometry(): Geometry { return this._geometry }
|
|
58
|
+
|
|
59
|
+
get material(): Material { return this._material }
|
|
60
|
+
set material(m: Material) {
|
|
61
|
+
this._material = m
|
|
62
|
+
_creator.setInstancedMeshMaterial(this.id, Material.idOf(m))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Shadow flags for all instances (both at once — the engine sets them per draw). */
|
|
66
|
+
setShadows(cast: boolean, receive = cast): this {
|
|
67
|
+
_creator.setInstancedMeshShadows(this.id, cast, receive)
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private _touch(i: number): void {
|
|
72
|
+
if (i < this._lo) this._lo = i
|
|
73
|
+
if (i > this._hi) this._hi = i
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Place instance `i`. Pending until `commit()`. */
|
|
77
|
+
setAt(i: number, t: InstanceTransform): this {
|
|
78
|
+
if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
|
|
79
|
+
const rot = t.rotation ?? (t.eulerAngles
|
|
80
|
+
? Quat.fromEuler(cx(t.eulerAngles) * DEG, cy(t.eulerAngles) * DEG, cz(t.eulerAngles) * DEG)
|
|
81
|
+
: Quat.identity)
|
|
82
|
+
const m = Mat4.compose(t.position ?? [ 0, 0, 0 ], rot, t.scale ?? 1).toArray()
|
|
83
|
+
this._data.set(m, i * 16)
|
|
84
|
+
this._touch(i)
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Place instance `i` from a 4×4 column-major matrix (local to this node). */
|
|
89
|
+
setMatrixAt(i: number, m: Mat4Like): this {
|
|
90
|
+
if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
|
|
91
|
+
const a = m instanceof Mat4 ? m.toArray() : m
|
|
92
|
+
for (let k = 0; k < 16; k++) this._data[i * 16 + k] = a[k]
|
|
93
|
+
this._touch(i)
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Hide instance `i` (a zero matrix — no geometry is emitted for it). */
|
|
98
|
+
hideAt(i: number): this {
|
|
99
|
+
if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
|
|
100
|
+
this._data.fill(0, i * 16, i * 16 + 16)
|
|
101
|
+
this._touch(i)
|
|
102
|
+
return this
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Bulk write: `matrices` holds 16 floats per instance, starting at instance `offset`. */
|
|
106
|
+
setMatrices(matrices: ArrayLike<number>, offset = 0): this {
|
|
107
|
+
const n = Math.min(Math.floor(matrices.length / 16), this.count - offset)
|
|
108
|
+
if (n <= 0) return this
|
|
109
|
+
this._data.set(Array.prototype.slice.call(matrices, 0, n * 16), offset * 16)
|
|
110
|
+
this._touch(offset)
|
|
111
|
+
this._touch(offset + n - 1)
|
|
112
|
+
return this
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The 4×4 of instance `i` as stored (a copy). */
|
|
116
|
+
matrixAt(i: number): Mat4 {
|
|
117
|
+
return Mat4.from(this._data.subarray(i * 16, i * 16 + 16))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Upload every change since the last commit (one bridge call covering the dirty range). */
|
|
121
|
+
commit(): this {
|
|
122
|
+
if (this._hi < this._lo) return this
|
|
123
|
+
const lo = this._lo, hi = this._hi
|
|
124
|
+
this._lo = Infinity
|
|
125
|
+
this._hi = -1
|
|
126
|
+
_creator.setInstanceTransforms(this.id, this._data.subarray(lo * 16, (hi + 1) * 16), lo)
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Whether edits are waiting for `commit()`. */
|
|
131
|
+
get dirty(): boolean { return this._hi >= this._lo }
|
|
132
|
+
}
|