lecodes-cli 0.17.0 → 0.17.2

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.
@@ -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
+ }
@@ -45,6 +45,15 @@ export type ParticlesMaterialOptions = {
45
45
  sheet?: number | [number, number]
46
46
  /** Extra brightness multiplier for a glow look (0 = off). */
47
47
  emissive?: number
48
+ /** (`render` quad / stretch) a second sheet on the same grid, ADDED as `emissiveMap × emissiveColor`
49
+ * — keyed by `map`'s alpha, not tinted by the particle colour. A fireball = a smoke `map` whose
50
+ * colour curve goes dark + a fire `emissiveMap` that keeps glowing while alpha lasts. */
51
+ emissiveMap?: Texture | null
52
+ /** Multiplier for `emissiveMap`; an `[r, g, b]` array may exceed 1 (HDR glow). Default 0 (off). */
53
+ emissiveColor?: ColorInput
54
+ /** (`render` quad / stretch) crossfade each flipbook frame into the next by the fractional frame
55
+ * index instead of snapping — smooth sheet playback at any frame rate. Default false. */
56
+ frameBlend?: boolean
48
57
  /** Compositing: `"alpha"` (default — smoke, dust) or `"add"` (fire, sparks, magic). */
49
58
  blend?: "alpha" | "add"
50
59
  /** Soft radial falloff toward the sprite edge. Defaults to true without a map, false with one. */
@@ -163,6 +172,9 @@ export class Material {
163
172
  if (quad) {
164
173
  m.uniforms.mode = options.render === "ribbon" ? 2 : options.render === "stretch" ? 1 : 0
165
174
  m.uniforms.stretch = options.stretch ?? (options.render === "stretch" ? 0.05 : 0)
175
+ if (options.emissiveMap) m.uniforms.emissiveMap = options.emissiveMap
176
+ if (options.emissiveColor !== undefined) m.uniforms.emissiveColor = Color.toRgb01(options.emissiveColor)
177
+ if (options.frameBlend) m.uniforms.frameBlend = 1
166
178
  }
167
179
  if (options.map) m.map = options.map
168
180
  return m