lecodes-sdk 0.20.2 → 1.0.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.
Files changed (77) hide show
  1. package/dist/global.d.ts +17 -5
  2. package/dist/types/canvas/Canvas.d.ts +2 -0
  3. package/dist/types/gl/DecalSet.d.ts +48 -3
  4. package/dist/types/gl/Geometry.d.ts +12 -0
  5. package/dist/types/gl/Lightmap.d.ts +9 -0
  6. package/dist/types/gl/Material.d.ts +4 -0
  7. package/dist/types/gl/Mesh.d.ts +7 -1
  8. package/dist/types/gl/Model.d.ts +34 -0
  9. package/dist/types/gl/Particles.d.ts +13 -0
  10. package/dist/types/gl/Scene.d.ts +1 -1
  11. package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
  12. package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
  13. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  14. package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
  15. package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
  16. package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
  17. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  18. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  19. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  20. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  21. package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
  22. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  23. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  24. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
  25. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  26. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  27. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  28. package/dist/types/inject.d.ts +28 -26
  29. package/dist/types/runtime/files.d.ts +24 -1
  30. package/dist/types/scene/defineScene.d.ts +10 -3
  31. package/dist/types.json +1 -1
  32. package/package.json +1 -1
  33. package/prompts/README.md +142 -142
  34. package/src/bridges.d.ts +1599 -1481
  35. package/src/canvas/Canvas.ts +21 -0
  36. package/src/compile/__tests__/compile.test.ts +11 -0
  37. package/src/compile/header.ts +6 -3
  38. package/src/compile/index.ts +1 -0
  39. package/src/compile/sceneEditor.ts +42 -1
  40. package/src/gl/DecalSet.ts +132 -5
  41. package/src/gl/Geometry.ts +348 -284
  42. package/src/gl/Lightmap.ts +32 -5
  43. package/src/gl/Material.ts +21 -0
  44. package/src/gl/Mesh.ts +120 -102
  45. package/src/gl/Model.ts +33 -1
  46. package/src/gl/Node.ts +1 -1
  47. package/src/gl/Particles.ts +21 -3
  48. package/src/gl/Scene.ts +1 -1
  49. package/src/gl/animation/DynamicBone.ts +459 -0
  50. package/src/gl/{IK.ts → animation/IK.ts} +4 -4
  51. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  52. package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
  53. package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
  54. package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
  55. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  56. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  57. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  58. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  59. package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +8 -6
  60. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  61. package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
  62. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  63. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
  64. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  65. package/src/gl/vehicle/Vehicle.ts +666 -0
  66. package/src/gl/vehicle/Wheel.ts +290 -0
  67. package/src/inject.ts +226 -224
  68. package/src/runtime/files.ts +32 -2
  69. package/src/scene/defineScene.ts +26 -10
  70. package/src/scene/level.ts +2 -2
  71. package/dist/types/gl/Gearbox.d.ts +0 -86
  72. package/dist/types/gl/Vehicle.d.ts +0 -191
  73. package/dist/types/gl/Wheel.d.ts +0 -95
  74. package/src/gl/Gearbox.ts +0 -212
  75. package/src/gl/Vehicle.ts +0 -473
  76. package/src/gl/Wheel.ts +0 -240
  77. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
@@ -71,6 +71,13 @@ export type LightmapLoadOptions = {
71
71
  * and its underside does not — the same shading the atlas gives the statics. Before that the volume
72
72
  * was one flat 0.5·E on every face and this knob was the workaround. Kept as a trim. */
73
73
  volumeBoost?: number
74
+ /** What an OCCLUDER-only static (a prop without lightmap UVs — traced into the receivers' atlas but
75
+ * carrying no rect of its own) does in real time. `"baked"` (default): its shadow is in the bake, so
76
+ * it stops casting and the shadow pass draws only the movers — on a level of thousands of foliage
77
+ * cards that is a third of the GPU frame. `"realtime"`: it keeps casting, which is the only way its
78
+ * shadow reaches OTHER non-receiver props (a rock under a tree); the honest fix for that is lightmap
79
+ * UVs on the props (`lecodes assets doctor --lightmap-uv`), which makes them receivers. */
80
+ occluderShadows?: "baked" | "realtime"
74
81
  }
75
82
 
76
83
  export type LightmapInfo = {
@@ -87,6 +94,8 @@ export type LightmapInfo = {
87
94
  applied: number
88
95
  total: number
89
96
  missing: string[]
97
+ /** Occluder-only statics whose real-time shadow was switched off (`occluderShadows: "baked"`). */
98
+ occluders: number
90
99
  }
91
100
 
92
101
  export type LightmapFiles = {
@@ -98,7 +107,7 @@ export type LightmapFiles = {
98
107
  volume?: string
99
108
  }
100
109
 
101
- import type { Terrain } from "./Terrain"
110
+ import type { Terrain } from "./terrain/Terrain"
102
111
 
103
112
  type Entry = { key: string, model?: Model, mesh?: Mesh, terrain?: Terrain }
104
113
 
@@ -274,12 +283,29 @@ export class Lightmap {
274
283
  // the light volume next: it claims every lightmap-material instance, the atlas rects below win back the statics
275
284
  const volume = files.volume ? await Lightmap.loadVolume(files.volume, volumeScale, lightGamma, ambientScale, strength, aoStrength) : null
276
285
  const rects = new Map<string, { st: number[], page: number }>()
277
- for (const inst of data.instances) if (inst.receiver && inst.st) rects.set(inst.key, { st: inst.st, page: inst.page ?? 0 })
278
- let applied = 0
286
+ // an OCCLUDER-only static (no lightmap UVs): the bake traced its shadow onto every receiver, so it
287
+ // has nothing left to cast in real time — the shadow pass then draws only the movers. Twig & Twine's
288
+ // 9 441 props (5 M alpha-masked spruce cards) cost the GPU a third of the frame as live casters.
289
+ const occluders = new Set<string>()
290
+ for (const inst of data.instances) {
291
+ if (inst.receiver && inst.st) rects.set(inst.key, { st: inst.st, page: inst.page ?? 0 })
292
+ else if (!inst.receiver) occluders.add(inst.key)
293
+ }
294
+ const bakedOccluders = (options.occluderShadows ?? "baked") === "baked"
295
+ let applied = 0, shadowless = 0
279
296
  const missing: string[] = []
280
297
  for (const e of Lightmap.entries) {
281
298
  const r = rects.get(e.key)
282
- if (!r || r.page >= textures.length) { missing.push(e.key); continue }
299
+ if (!r || r.page >= textures.length) {
300
+ if (occluders.has(e.key)) {
301
+ if (bakedOccluders) {
302
+ if (e.model) e.model.castShadows = false
303
+ else if (e.mesh) e.mesh.castShadows = false
304
+ shadowless++
305
+ }
306
+ } else missing.push(e.key)
307
+ continue
308
+ }
283
309
  const st = r.st
284
310
  const texture = textures[r.page]
285
311
  const light = r.page < lightTextures.length ? lightTextures[r.page] : null
@@ -304,7 +330,8 @@ export class Lightmap {
304
330
  // the lights the bake carries go dark: the atlas holds them for the statics, the volume for the movers
305
331
  let lights = 0
306
332
  if (lightTextures.length > 0 && data.lights?.length) lights = Lightmap.switchOffBaked(data.lights)
307
- const info: LightmapInfo = { size: data.size, texel: data.texel, pages, applied, total: rects.size, missing, lights }
333
+ if (shadowless > 0) console.log(`[lightmap] ${shadowless} occluder-only static(s) cast no real-time shadow (baked); the shadow pass draws the movers only`)
334
+ const info: LightmapInfo = { size: data.size, texel: data.texel, pages, applied, total: rects.size, missing, lights, occluders: shadowless }
308
335
  if (volume) info.volume = { dims: volume.dims, cell: volume.cell }
309
336
  return info
310
337
  }
@@ -349,6 +349,22 @@ export class Material {
349
349
  return m
350
350
  }
351
351
 
352
+ /** `Material.lightmap()`'s masked twin (`blending: masked`, same shader and parameters): what the engine
353
+ * gives a lightmapped model's alpha-MASK materials. The cutoff comes from the glTF material. */
354
+ static lightmapMasked(): Material {
355
+ const id = Material.lightmapShading === "baked"
356
+ ? _creatorUtils.fetchLocal("lightmap-baked-masked.filamat")
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 ])
365
+ return m
366
+ }
367
+
352
368
  /** The terrain splat material (docs/terrain-plan.md §1.5): four albedo (+ normal-map) layers blended by
353
369
  * a control map on the terrain's own grid, per-layer `tiling` (metres per repeat) / `roughness` /
354
370
  * `normalScale` / `triplanar`, lightmap-aware (the same `lightmap` / `lightmapST` block as
@@ -367,6 +383,10 @@ export class Material {
367
383
  * the GLB provider instantiates; the template itself never draws). */
368
384
  static _lightmapTemplate(): Material { return (Material._lmTemplate ??= Material.lightmap()) }
369
385
  private static _lmTemplate: Material | undefined
386
+ /** @internal The tier's MASKED twin, handed to the engine next to the template: a model's glTF MASK
387
+ * materials (foliage cards) take it instead of falling back to the ubershader's full lit path. */
388
+ static _lightmapMaskedTemplate(): Material { return (Material._lmMaskedTemplate ??= Material.lightmapMasked()) }
389
+ private static _lmMaskedTemplate: Material | undefined
370
390
 
371
391
  static _lightmapShading: LightmapShading = "full"
372
392
  /** Which shader lightmapped models take — a graphics-quality tier, engine-wide. `"full"` is filament's
@@ -381,6 +401,7 @@ export class Material {
381
401
  if (mode === Material._lightmapShading) return
382
402
  Material._lightmapShading = mode
383
403
  Material._lmTemplate = undefined // the next load builds a template on the new shader
404
+ Material._lmMaskedTemplate = undefined
384
405
  }
385
406
 
386
407
  /** Shadow-catcher material (transparent except where shadows fall). */
package/src/gl/Mesh.ts CHANGED
@@ -1,102 +1,120 @@
1
- // A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/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 CylinderOptions,
8
- type PlaneOptions,
9
- type SphereOptions,
10
- } from "./Geometry"
11
- import { Material } from "./Material"
12
- import { Node } from "./Node"
13
-
14
- /** Common transform/render options every primitive factory accepts. */
15
- export type MeshOptions = {
16
- material?: Material
17
- position?: Vec3Like
18
- eulerAngles?: Vec3Like
19
- scale?: Vec3Like | number
20
- name?: string
21
- castShadows?: boolean
22
- receiveShadows?: boolean
23
- /** Coarse draw order, 0 (first) … 7 (last); default 4. See `Mesh.renderPriority`. */
24
- renderPriority?: number
25
- }
26
-
27
- export class Mesh extends Node {
28
- private _geometry?: Geometry
29
-
30
- constructor(geometry?: Geometry, material?: Material) {
31
- super()
32
- if (geometry) {
33
- const mat = material ?? Material.unlit()
34
- this._geometry = geometry
35
- this._materials = [ mat ]
36
- _creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
37
- }
38
- }
39
-
40
- override get geometry(): Geometry | undefined { return this._geometry }
41
- /** Replace the geometry in place — the node, its transform and its material stay, the vertex and
42
- * index buffers are rebuilt. What an editor overlay or a debug drawer redraws with. */
43
- setGeometry(geometry: Geometry): this {
44
- this._geometry = geometry
45
- _creator.setMesh(this.id, Material.idOf(this._materials![0]!), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
46
- return this
47
- }
48
-
49
- /** A Mesh always carries a material (slot 0) — see Node.setMaterial for the slot API. */
50
- override get material(): Material { return this._materials![0]! }
51
- override set material(m: Material) { this.setMaterial(m, 0) }
52
-
53
- set culling(v: boolean) { _creator.setCulling(this.id, v) }
54
- /** Coarse draw order within the frame: 0 draws first, 7 last, 4 is the default (Filament's
55
- * renderable priority; within one priority opaque draws sort front-to-back, blended back-to-front).
56
- * Something drawn over the scene with `Material.depthTest = false` goes in 7, so nothing drawn
57
- * after it can cover it. Write-only; a host that predates the call ignores it. */
58
- set renderPriority(v: number) {
59
- if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
60
- else if (!Mesh._warnedPriority) { Mesh._warnedPriority = true; console.warn("[mesh] this host has no setRenderPriority — renderPriority ignored") }
61
- }
62
- private static _warnedPriority = false
63
- set castShadows(v: boolean) { _creator.setCastShadows(this.id, v) }
64
- set receiveShadows(v: boolean) { _creator.setReceiveShadows(this.id, v) }
65
-
66
- // --- factories ---
67
-
68
- static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
69
- // the size goes into the geometry (not a scale after): the lightmap chart is laid out per face area
70
- return apply(new Mesh(Geometry.box(options.size ?? 1), options.material), options)
71
- }
72
-
73
- static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
74
- return apply(new Mesh(Geometry.sphere(options), options.material), options)
75
- }
76
-
77
- static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
78
- return apply(new Mesh(Geometry.cylinder(options), options.material), options)
79
- }
80
-
81
- static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
82
- return apply(new Mesh(Geometry.plane(options), options.material), options)
83
- }
84
-
85
- /** Wrap a custom Geometry. */
86
- static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
87
- return apply(new Mesh(geometry, options.material), options)
88
- }
89
- }
90
-
91
- const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])
92
-
93
- const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
94
- if (o.position) mesh.position = o.position
95
- if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
96
- if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
97
- if (o.name !== undefined) mesh.name = o.name
98
- if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
99
- if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
100
- if (o.renderPriority !== undefined) mesh.renderPriority = o.renderPriority
101
- return mesh
102
- }
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
@@ -9,6 +9,27 @@ import { Node } from "./Node"
9
9
  import { Animator, _pushLod, type LodMode } from "./animation/Animator"
10
10
  import { Material } from "./Material"
11
11
 
12
+ /** One polygon under a screen point — what `Model.pickTriangle` returns. `bones` are the vertex's raw
13
+ * JOINTS_0 / WEIGHTS_0 pairs (weight > 0; empty = unweighted), `bind` its position in mesh space,
14
+ * `world` its skinned position this frame. */
15
+ export interface TrianglePick {
16
+ /** the Model's root entity, and the mesh node's own entity (0 when the host has no entity for it) */
17
+ entity: number
18
+ node: number
19
+ nodeName: string
20
+ mesh: string
21
+ primitive: number
22
+ material: string
23
+ /** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
24
+ triangle: number
25
+ backface: boolean
26
+ distance: number
27
+ point: [number, number, number]
28
+ bary: [number, number, number]
29
+ skin: string
30
+ vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
31
+ }
32
+
12
33
  export class Model extends Node {
13
34
  /** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
14
35
  * more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
@@ -69,6 +90,16 @@ export class Model extends Node {
69
90
  /** @internal both flags travel together — the bridge walks the instance once. */
70
91
  _applyShadows(): void { _creator.setGlbShadows?.(this.id, this._castShadows, this._receiveShadows) }
71
92
 
93
+ /** DEBUG: the closest polygon of this model under a screen point (logical px — `Input.mouse.position`,
94
+ * a touch event's clientX/Y), tested against the CPU-skinned CURRENT pose, both faces. Names the
95
+ * triangle, its three vertices (bind + skinned positions) and their raw bone weights, so a stretched
96
+ * or misbound polygon can be traced to its binding. One full CPU skin of the model per call: click-rate
97
+ * only. null = miss, or a host without the pick (desktop today). */
98
+ pickTriangle(screenX: number, screenY: number): TrianglePick | null {
99
+ const json = _creator.pickTriangle?.(this.id, screenX, screenY)
100
+ return json ? JSON.parse(json) as TrianglePick : null
101
+ }
102
+
72
103
  /** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
73
104
  * the same parent and scene and sharing this model's current transform. The clone has its own
74
105
  * independent animation state (reach it via clone.anim). Mirrors this model's culling flag. */
@@ -110,7 +141,8 @@ export class Model extends Node {
110
141
  return new Promise<Model>((resolve, reject) => {
111
142
  const mode = options.lightmap ?? Model._lightmapDefault
112
143
  const lightmapped = !!mode && !!_creator.setNextGlbLightmapped
113
- if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()))
144
+ // the tier + its masked twin (alpha-MASK materials — foliage — take the lightmap shader too, 2026-09-11)
145
+ if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
114
146
  _creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
115
147
  if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
116
148
  const model = new Model(entityId)
package/src/gl/Node.ts CHANGED
@@ -12,7 +12,7 @@ import { Quat, cw, type QuatLike } from "../math/quat"
12
12
  import { Mat4, type Mat4Like } from "../math/mat4"
13
13
  import type { Geometry } from "./Geometry"
14
14
  import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
15
- import { ensurePhysicsEvents } from "./physicsEvents"
15
+ 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"
@@ -231,7 +231,15 @@ type Shape =
231
231
  * birth direction (a burst that decays: `curve(4).to(0)`), `x`/`y`/`z` drift in the simulation
232
232
  * space (rising smoke: `y: curve(2).from(0).to(1)`). Unity's velocityOverLifetime. */
233
233
  export type VelocityOverLife = { radial?: ParticleValue, x?: ParticleValue, y?: ParticleValue, z?: ParticleValue }
234
- type Noise = { strength?: number, frequency?: number, speed?: number }
234
+ type Noise = {
235
+ strength?: number, frequency?: number, speed?: number,
236
+ /** How much of the displacement a particle earns with AGE (default 0 = all of it from birth).
237
+ * The field is sampled by position, so at 0 every particle born at the same spot is pushed the
238
+ * same way and a plume's root wanders off its emitter by up to `strength` metres; at 1 a particle
239
+ * is born exactly where the emitter put it and drifts into the field over its life — what you
240
+ * want whenever the source is visible (an exhaust pipe, a contact patch, a muzzle). */
241
+ ramp?: number,
242
+ }
235
243
 
236
244
  /** Ground plane for bouncing particles (all render modes). `height` is in the simulation space:
237
245
  * world y with `space: 'world'`, emitter-local y otherwise. */
@@ -283,6 +291,11 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
283
291
  * without this the engine picks who covers whom per frame (smoke popping over a fireball).
284
292
  * Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
285
293
  order?: number
294
+ /** Coarse draw order among ALL blended draws, 0 (first) … 7 (last) — see `Mesh.renderPriority`.
295
+ * Default 5: meshes sit at 4 and decals at 3, so smoke covers a car's glass and its skid marks
296
+ * whatever the camera does (the engine's depth sort compares object centres, and an emitter's
297
+ * centre says nothing about where its cloud is). `order` breaks ties inside one priority. */
298
+ renderPriority?: number
286
299
  shape?: Shape
287
300
  /** Extra velocity over life (radial burst that decays, axis drift) — see `VelocityOverLife`. */
288
301
  velocityOverLife?: VelocityOverLife
@@ -437,6 +450,7 @@ export class Particles extends Node {
437
450
  super()
438
451
  this._material = options.material ?? (options.mesh ? Material.lit() : Material.particles(options))
439
452
  _creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxParticles ?? 0)
453
+ this.renderPriority = options.renderPriority ?? 5
440
454
 
441
455
  const id = this.id
442
456
  this.custom = new Proxy([] as ParticleValue[], {
@@ -496,7 +510,7 @@ export class Particles extends Node {
496
510
  }
497
511
  if (options.noise !== undefined) {
498
512
  const n = options.noise
499
- out.push(TAG_NOISE, 3, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0)
513
+ out.push(TAG_NOISE, 4, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0, n ? n.ramp ?? 0 : 0)
500
514
  }
501
515
  if (options.seed !== undefined) out.push(TAG_SEED, 1, options.seed)
502
516
 
@@ -517,6 +531,10 @@ export class Particles extends Node {
517
531
  set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
518
532
  set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
519
533
  set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
534
+ /** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
535
+ set renderPriority(v: number) {
536
+ if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
537
+ }
520
538
 
521
539
  set velocityOverLife(v: VelocityOverLife) {
522
540
  const out: number[] = []
@@ -585,7 +603,7 @@ export class Particles extends Node {
585
603
 
586
604
  set noise(noise: Noise | null) {
587
605
  this._send(noise
588
- ? [ TAG_NOISE, 3, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1 ]
606
+ ? [ TAG_NOISE, 4, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1, noise.ramp ?? 0 ]
589
607
  : [ TAG_NOISE, 3, 0, 0, 0 ])
590
608
  }
591
609
  }
package/src/gl/Scene.ts CHANGED
@@ -13,7 +13,7 @@ import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
13
13
  import type { FetchResponse } from "../runtime/fetch"
14
14
  import { _requestCameraPermission } from "../plugins/permission"
15
15
  import { Camera } from "./Camera"
16
- import { SceneAudio } from "./SceneAudio"
16
+ import { SceneAudio } from "./audio/SceneAudio"
17
17
  import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
18
18
  import { Material } from "./Material"
19
19
  import { Texture } from "./Texture"