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.
@@ -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. `key` names the entry in the .bake (default:
226
- * the node's name + a running number, `container#3`); pass one when names are not stable. */
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)
@@ -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) Material._folMaskedTemplate = Material._glbTemplate(_creator.builtinMaterial("foliage-masked"))
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
+ }