lecodes-sdk 1.2.0 → 2.0.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.
Files changed (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
@@ -1,4 +1,4 @@
1
- // Baked lighting for static 3D geometry (packages/creator-bake). A scene FILE does all of this by
1
+ // Baked lighting for static 3D geometry (engines/bake). A scene FILE does all of this by
2
2
  // itself: `env: { lightmap: { data, light, aux } }` marks the level as baked, every model/mesh node
3
3
  // that no Physics aspect moves is a static (keys = node paths, `lightmap: true|false` overrides),
4
4
  // and the runtime applies the bake when the level finishes loading (scene/defineScene.ts).
@@ -39,12 +39,13 @@
39
39
  // is untouched — it is the same material reading other pages). The bake's `--debug-dir` keeps the
40
40
  // float layers `lecodes lightmap inspect` reads.
41
41
 
42
+ import { Color } from "../core/color"
42
43
  import { fetch } from "../runtime/fetch"
43
44
  import { Texture } from "./Texture"
44
45
  import { Material } from "./Material"
45
46
  import { Mesh } from "./Mesh"
46
47
  import { Model } from "./Model"
47
- import { Node, nodeRegistry } from "./Node"
48
+ import { Node, nodes } from "./Node"
48
49
  import { Light } from "./Light"
49
50
  import type { Scene } from "./Scene"
50
51
 
@@ -266,12 +267,13 @@ export class Lightmap {
266
267
  // by entity id through the bridge, not `node.children`: the bundler drops that accessor from a build where
267
268
  // nothing else reads it (the leash in lightcheck, 2026-09-18: "children" was undefined on a Mesh)
268
269
  const walk = (id: number): void => {
269
- const n = nodeRegistry.get(id)
270
+ const n = nodes.get(id)
270
271
  if (n instanceof Model || n instanceof Mesh) {
271
272
  if (on) Lightmap.movers.add(n); else Lightmap.movers.delete(n)
272
273
  if (Lightmap.volumeOn || !on) _creator.lightVolumeTrack?.(id, on)
273
274
  }
274
- for (const c of _creator.getChildren(id)) walk(c)
275
+ const count = _creator.getChildCount(id)
276
+ for (let i = 0; i < count; i++) walk(_creator.getChild(id, i))
275
277
  }
276
278
  walk(node.id)
277
279
  }
@@ -285,8 +287,7 @@ export class Lightmap {
285
287
  try {
286
288
  const resp = await fetch(files.volume, { useOnce: true })
287
289
  if (resp.status >= 400) { console.warn(`Lightmap: no light grid at ${files.volume} (HTTP ${resp.status})`); return }
288
- const ok = _creator.lightVolumeLoad((resp as unknown as { _id: number })._id)
289
- resp.dispose()
290
+ const ok = _creator.lightVolumeLoad((resp as unknown as { _h: Handle })._h.id)
290
291
  if (!ok) { console.warn(`Lightmap: ${files.volume} is not a light grid — rebake`); return }
291
292
  } catch (e) {
292
293
  console.warn(`Lightmap: could not read ${files.volume}: ${String(e)}`)
@@ -313,10 +314,31 @@ export class Lightmap {
313
314
  Lightmap.bound = null
314
315
  if (Lightmap.balls) { Lightmap.balls.destroy(); Lightmap.balls = null }
315
316
  _creator.lightmapSetOptions?.(1, 0)
317
+ Lightmap.sunArgs = null
316
318
  _creator.lightmapSunSet?.(0, 1, 0, 0, 0, 0, false)
317
319
  _creator.lightmapDebug?.(0)
318
320
  }
319
321
 
322
+ /** THE MOVERS' SUN SHADOWS ON THE BAKED STATICS (a bot, a barrel, the player's rifle darkening the ground under
323
+ * it): the lightmap shader reads the real-time shadow map and the bake's sun map for it, per pixel. Off, a baked
324
+ * surface takes no real-time shadow at all — the cheapest lighting tier (some 4 ms of a 16 ms 720p frame on an
325
+ * integrated GPU, the yard 2026-09-26); a mover still shades ITSELF and the other movers. Live: it rebinds the
326
+ * level's material instances at once. */
327
+ static get moverShadows(): boolean { return Lightmap._moverShadows }
328
+ static set moverShadows(on: boolean) {
329
+ if (Lightmap._moverShadows === on) return
330
+ Lightmap._moverShadows = on
331
+ Lightmap.applySun()
332
+ }
333
+ private static _moverShadows = true
334
+ /** The loaded bake's sun (TO the sun ×3, its illuminance rgb, the sun map's levels), null without a sun map. */
335
+ private static sunArgs: [number, number, number, number, number, number, number] | null = null
336
+ private static applySun(): void {
337
+ const a = Lightmap.sunArgs
338
+ if (a && Lightmap._moverShadows) _creator.lightmapSunSet?.(a[0], a[1], a[2], a[3], a[4], a[5], true, a[6])
339
+ else _creator.lightmapSunSet?.(0, 1, 0, 0, 0, 0, false)
340
+ }
341
+
320
342
  /** Apply a bake — or, under `lecodes lightmap bake`, run it. Resolves to null when nothing was
321
343
  * applied (no bake yet, a host without the feature, bake mode). */
322
344
  private static scene: Scene | null = null
@@ -333,7 +355,6 @@ export class Lightmap {
333
355
  const resp = await fetch(files.data, { useOnce: true })
334
356
  if (resp.status >= 400) { console.warn(`Lightmap: no bake at ${files.data} (HTTP ${resp.status}) — run \`lecodes lightmap bake\``); return null }
335
357
  data = resp.json()
336
- resp.dispose()
337
358
  } catch (e) {
338
359
  console.warn(`Lightmap: could not read ${files.data}: ${String(e)}`)
339
360
  return null
@@ -357,8 +378,8 @@ export class Lightmap {
357
378
  _creator.lightmapSetOptions?.(lightScale, data.emissiveNits ?? 0)
358
379
  // a mover's sun shadow on the statics: only a bake that wrote the sun map under its aux pages
359
380
  const sun = data.sunMap ? data.sun : null
360
- if (sun) _creator.lightmapSunSet?.(-sun.direction[0], -sun.direction[1], -sun.direction[2], sun.color[0] * sun.lux, sun.color[1] * sun.lux, sun.color[2] * sun.lux, true, data.sunMapLevels ?? 1)
361
- else _creator.lightmapSunSet?.(0, 1, 0, 0, 0, 0, false)
381
+ Lightmap.sunArgs = sun ? [-sun.direction[0], -sun.direction[1], -sun.direction[2], sun.color[0] * sun.lux, sun.color[1] * sun.lux, sun.color[2] * sun.lux, data.sunMapLevels ?? 1] : null
382
+ Lightmap.applySun()
362
383
  // the reflection probes: the atlas loads like a page, the grid travels as numbers; level-wide on the host
363
384
  let probes = 0
364
385
  let probeAtlas: Texture | undefined
@@ -370,7 +391,7 @@ export class Lightmap {
370
391
  else {
371
392
  probeAtlas = await Texture.load(files.probes, { fullSize: true, srgb: false })
372
393
  // which probes a pixel reflects is in the aux pages (their probe maps); the host needs the atlas and the records' range
373
- probes = _creator.lightmapProbesSet(probeAtlas._id, pb.oct, pb.levels, pb.perRow, pb.count,
394
+ probes = _creator.lightmapProbesSet(probeAtlas._h.id, pb.oct, pb.levels, pb.perRow, pb.count,
374
395
  pb.encode[0], pb.encode[1], pb.encode[2], pb.encode[3], pb.encode[4], pb.encode[5])
375
396
  if (probes !== pb.count) console.warn(`Lightmap: the host took ${probes} of ${pb.count} probe(s) — rebake`)
376
397
  }
@@ -400,9 +421,9 @@ export class Lightmap {
400
421
  for (const [ k, inst ] of groups) {
401
422
  if (inst.small && inst.light && _creator.lightmapObjectApply && n > 0) {
402
423
  const l = inst.light
403
- took += _creator.lightmapObjectApply(node.id, light[0]._id, aux[0]._id, Float32Array.from([ ...l.cube.flat(), probeId(l.probes[0]), probeId(l.probes[1]), l.probes[2] ]), k)
424
+ took += _creator.lightmapObjectApply(node.id, light[0]._h.id, aux[0]._h.id, Float32Array.from([ ...l.cube.flat(), probeId(l.probes[0]), probeId(l.probes[1]), l.probes[2] ]), k)
404
425
  } else if (inst.receiver && inst.st && inst.page !== undefined && inst.page < n) {
405
- took += _creator.lightmapApply(node.id, light[inst.page]._id, aux[inst.page]._id, inst.st[0], inst.st[1], inst.st[2], inst.st[3], k)
426
+ took += _creator.lightmapApply(node.id, light[inst.page]._h.id, aux[inst.page]._h.id, inst.st[0], inst.st[1], inst.st[2], inst.st[3], k)
406
427
  }
407
428
  }
408
429
  if (took === 0) console.warn(`Lightmap: ${e.key} (${groups.size} groups) was not loaded lightmapped, or the host binds no groups (rebuild it)`)
@@ -417,7 +438,7 @@ export class Lightmap {
417
438
  if (!l || !_creator.lightmapObjectApply || n === 0) continue
418
439
  if (e.mesh) e.mesh.material = Lightmap.meshMaterial(e.mesh)
419
440
  const d = Float32Array.from([ ...l.cube.flat(), probeId(l.probes[0]), probeId(l.probes[1]), l.probes[2] ])
420
- if (_creator.lightmapObjectApply(node.id, light[0]._id, aux[0]._id, d) > 0) smallLit++
441
+ if (_creator.lightmapObjectApply(node.id, light[0]._h.id, aux[0]._h.id, d) > 0) smallLit++
421
442
  continue
422
443
  }
423
444
  if (!r && declined.has(e.key)) continue // glass / a declined material: the bake knows it as an occluder - the light grid lights it (loadVolume)
@@ -425,7 +446,7 @@ export class Lightmap {
425
446
  const st = r.st!
426
447
  // a Mesh receiver: its lit material becomes a lightmap material carrying the same colour / map / factors
427
448
  if (e.mesh) e.mesh.material = Lightmap.meshMaterial(e.mesh)
428
- const took = _creator.lightmapApply(node.id, light[r.page!]._id, aux[r.page!]._id, st[0], st[1], st[2], st[3])
449
+ const took = _creator.lightmapApply(node.id, light[r.page!]._h.id, aux[r.page!]._h.id, st[0], st[1], st[2], st[3])
429
450
  if (took === 0) { console.warn(`Lightmap: ${e.key} was not loaded lightmapped (Model.load { lightmap: true } / Material.lightmap())`); continue }
430
451
  applied++
431
452
  }
@@ -479,12 +500,12 @@ export class Lightmap {
479
500
  const parts = Lightmap.groupsOf(b.data.instances)
480
501
  for (const [ id, e ] of b.byEntity) {
481
502
  for (const [ k, inst ] of parts.get(e.key) ?? []) {
482
- if (inst.receiver && inst.st && inst.page !== undefined && inst.page < b.pages) _creator.lightmapApply(id, tex[inst.page]._id, b.aux[inst.page]._id, inst.st[0], inst.st[1], inst.st[2], inst.st[3], k)
503
+ if (inst.receiver && inst.st && inst.page !== undefined && inst.page < b.pages) _creator.lightmapApply(id, tex[inst.page]._h.id, b.aux[inst.page]._h.id, inst.st[0], inst.st[1], inst.st[2], inst.st[3], k)
483
504
  }
484
505
  const r = b.rects.get(e.key)
485
506
  if (!r || r.page! >= b.pages) continue
486
507
  const st = r.st!
487
- _creator.lightmapApply(id, tex[r.page!]._id, b.aux[r.page!]._id, st[0], st[1], st[2], st[3])
508
+ _creator.lightmapApply(id, tex[r.page!]._h.id, b.aux[r.page!]._h.id, st[0], st[1], st[2], st[3])
488
509
  }
489
510
  console.log(`[lightmap] showing the ${kind} pages`)
490
511
  }
@@ -508,7 +529,7 @@ export class Lightmap {
508
529
  ball.name = `probe#${k}`
509
530
  ball.position = [ x, y, z ]
510
531
  holder.add(ball)
511
- _creator.lightmapApply(ball.id, b.light[0]._id, b.aux[0]._id, 0, 0, 0, 0)
532
+ _creator.lightmapApply(ball.id, b.light[0]._h.id, b.aux[0]._h.id, 0, 0, 0, 0)
512
533
  m.set("probeBall", k + 1) // probe k alone, unoccluded
513
534
  n++
514
535
  }
@@ -527,15 +548,13 @@ export class Lightmap {
527
548
  static probe(screenX: number, screenY: number): LightmapProbe | null {
528
549
  const b = Lightmap.bound
529
550
  if (!b || !_creator.pickTriangle) return null
530
- type Hit = { entity: number, nodeName: string, material: string, point: number[], distance: number, uv1?: number[], lightmapGroup?: number }
551
+ type Hit = { entity: number, nodeName: string, material: string, point: number[], distance: number, uv1?: [number, number], lightmapGroup?: number }
531
552
  // the nearest hit over the registered statics: one pick per entry (the all-instances pick, entity 0, walks
532
553
  // every GLB instance including the off-scene templates and is what a scene file's clones confuse)
533
554
  let hit: Hit | null = null
534
555
  for (const id of b.byEntity.keys()) {
535
- const j = _creator.pickTriangle(id, screenX, screenY)
536
- if (!j) continue
537
- const h = JSON.parse(j) as Hit
538
- if (!hit || h.distance < hit.distance) hit = h
556
+ const h: Hit | null = _creator.pickTriangle(id, screenX, screenY)
557
+ if (h && (!hit || h.distance < hit.distance)) hit = h
539
558
  }
540
559
  if (!hit) { console.log(`[probe] nothing baked under ${screenX.toFixed(0)},${screenY.toFixed(0)}`); return null }
541
560
  const entry = b.byEntity.get(hit.entity)
@@ -553,19 +572,16 @@ export class Lightmap {
553
572
  }
554
573
  }
555
574
 
556
- /** A hex colour as the 9-char form the float4 uniform path expects ("#rrggbbaa"). */
557
- private static hex8(c: unknown): string {
558
- if (typeof c !== "string") return "#ffffffff"
559
- if (c.length === 7) return c + "ff"
560
- if (c.length === 4) return "#" + c[1] + c[1] + c[2] + c[2] + c[3] + c[3] + "ff"
561
- return c
575
+ /** A material's colour uniform as "#rrggbbaa" (the float4 uniform path sets its alpha); white without one. */
576
+ private static colorUniform(c: unknown): string {
577
+ return typeof c === "string" ? Color.toHexString(c) : "#ffffffff"
562
578
  }
563
579
 
564
580
  /** The lightmap material for a static Mesh, from whatever lit material it carried (colour, map, roughness, metallic). */
565
581
  private static meshMaterial(mesh: Mesh): Material {
566
582
  const u = mesh.material?.uniforms ?? {}
567
583
  const m = Material.lightmap()
568
- m.set("baseColorFactor", Lightmap.hex8(u.baseColor ?? u.color))
584
+ m.set("baseColorFactor", Lightmap.colorUniform(u.baseColor ?? u.color))
569
585
  if (u.baseColorMap instanceof Texture) m.set("baseColorMap", u.baseColorMap)
570
586
  if (typeof u.roughness === "number") m.set("roughnessFactor", u.roughness)
571
587
  if (typeof u.metallic === "number") m.set("metallicFactor", u.metallic)
@@ -15,16 +15,25 @@ const isCanvasSource = (v: unknown): v is Canvas =>
15
15
 
16
16
  type UniformValue = string | Texture | number | boolean | number[] | Float32Array | null
17
17
 
18
+ // A color string → the RGB or the RGBA slot. The arity is semantic (engines/gl material.cpp): RGB on a
19
+ // float4 keeps the instance's alpha, RGBA sets it — so a color that carries alpha (below 1, or written
20
+ // '#rgba' / '#rrggbbaa') goes RGBA, every other one RGB. Not a color → throws with the value.
21
+ const applyColorUniform = (id: number, key: string, value: string): void => {
22
+ const rgba = Color.toPackedRgba(value)
23
+ const t = value.trim()
24
+ if ((rgba & 0xff) !== 0xff || (t[0] === "#" && (t.length === 5 || t.length === 9))) _creator.setUniformRgba(id, key, rgba)
25
+ else _creator.setUniformRgb(id, key, rgba >>> 8)
26
+ }
27
+
18
28
  const applyUniform = (id: number, key: string, value: UniformValue): void => {
19
29
  if (typeof value === "string") {
20
- if (value.length > 7) _creator.setUniformRgba(id, key, parseInt(value.slice(1), 16))
21
- else _creator.setUniformRgb(id, key, parseInt(value.slice(1), 16))
30
+ applyColorUniform(id, key, value)
22
31
  } else if (typeof value === "number") {
23
32
  _creator.setUniformFloat(id, key, value)
24
33
  } else if (typeof value === "boolean") {
25
34
  _creator.setUniformBoolean(id, key, value)
26
35
  } else if (value instanceof Texture) {
27
- _creator.setUniformTexture(id, key, value._id, value.wrapS, value.wrapT)
36
+ _creator.setUniformTexture(id, key, value._h.id, value.wrapS, value.wrapT)
28
37
  } else if (value instanceof Float32Array) {
29
38
  _creator.setUniformArray(id, key, value)
30
39
  } else if (Array.isArray(value)) {
@@ -163,26 +172,30 @@ export type LitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
163
172
 
164
173
 
165
174
  export class Material {
166
- /** @internal native material-instance handle. */
167
- _id: number
175
+ /** @internal The material instance's owned handle (`_h.id` = the engine id). An instance the
176
+ * engine created itself (a glTF part's) is wrapped from its number and never freed by us. */
177
+ _h: Handle
168
178
  readonly shader: FetchResponse | "unknown"
169
179
  readonly uniforms: Record<string, UniformValue>
170
180
 
171
181
  private _colorKey: string | null = null
172
182
 
173
- constructor(shader: FetchResponse | number) {
183
+ constructor(shader: FetchResponse | Handle | number) {
174
184
  if (typeof shader === "number") {
175
185
  this.shader = "unknown"
176
- this._id = shader
186
+ this._h = { id: shader } // an engine-owned instance: not ours to free
187
+ } else if ("_h" in (shader as any)) {
188
+ this.shader = shader as FetchResponse
189
+ this._h = _creator.createMaterial((shader as any)._h.id) // owned: dropped and unused → freed
177
190
  } else {
178
- this.shader = shader
179
- this._id = _creator.createMaterial((shader as any)._id)
191
+ this.shader = "unknown"
192
+ this._h = shader as Handle // an instance the runtime already wrapped for us
180
193
  }
181
194
  const store: Record<string, UniformValue> = {}
182
- const id = () => this._id
195
+ const id = () => this._h.id
183
196
  this.uniforms = new Proxy(store, {
184
197
  get: (t, k: string) => t[k],
185
- set: (t, k: string, v: UniformValue) => { t[k] = v; applyUniform(id(), k, v); return true },
198
+ set: (t, k: string, v: UniformValue) => { applyUniform(id(), k, v); t[k] = v; return true },
186
199
  })
187
200
  }
188
201
 
@@ -199,18 +212,18 @@ export class Material {
199
212
  /** Depth test against the scene (write-only; see `MaterialDepthOptions`). A host that predates
200
213
  * the call leaves the material as the shader has it. */
201
214
  set depthTest(on: boolean) {
202
- if (_creator.setMaterialDepthTest) _creator.setMaterialDepthTest(this._id, on)
215
+ if (_creator.setMaterialDepthTest) _creator.setMaterialDepthTest(this._h.id, on)
203
216
  else warnOnce("depthTest", "[material] this host has no setMaterialDepthTest — depthTest ignored")
204
217
  }
205
218
  /** Depth write (write-only; see `MaterialStateOptions`). */
206
219
  set depthWrite(on: boolean) {
207
- if (_creator.setMaterialDepthWrite) _creator.setMaterialDepthWrite(this._id, on)
220
+ if (_creator.setMaterialDepthWrite) _creator.setMaterialDepthWrite(this._h.id, on)
208
221
  else warnOnce("depthWrite", "[material] this host has no setMaterialDepthWrite — depthWrite ignored")
209
222
  }
210
223
  /** Both faces drawn (write-only; see `MaterialStateOptions`). */
211
224
  set doubleSided(on: boolean) {
212
225
  // 0 = no culling, 2 = back faces culled (the shader's usual default)
213
- if (_creator.setMaterialCulling) _creator.setMaterialCulling(this._id, on ? 0 : 2)
226
+ if (_creator.setMaterialCulling) _creator.setMaterialCulling(this._h.id, on ? 0 : 2)
214
227
  else warnOnce("doubleSided", "[material] this host has no setMaterialCulling — doubleSided ignored")
215
228
  }
216
229
  /** The stencil test / write (write-only; `null` = back to none). See `MaterialStencil`. */
@@ -218,7 +231,7 @@ export class Material {
218
231
  if (!_creator.setMaterialStencil) { warnOnce("stencil", "[material] this host has no setMaterialStencil — stencil ignored"); return }
219
232
  const o = s ?? {}
220
233
  const clamp = (v: number | undefined, d: number): number => Math.max(0, Math.min(255, Math.round(v ?? d)))
221
- _creator.setMaterialStencil(this._id, !!o.write, STENCIL_TEST.indexOf(o.test ?? "always"), clamp(o.ref, 0),
234
+ _creator.setMaterialStencil(this._h.id, !!o.write, STENCIL_TEST.indexOf(o.test ?? "always"), clamp(o.ref, 0),
222
235
  STENCIL_OP.indexOf(o.onPass ?? "keep"), STENCIL_OP.indexOf(o.onFail ?? "keep"), STENCIL_OP.indexOf(o.onDepthFail ?? "keep"),
223
236
  clamp(o.readMask, 255), clamp(o.writeMask, 255))
224
237
  }
@@ -238,11 +251,20 @@ export class Material {
238
251
  }
239
252
 
240
253
  // --- factories ---
254
+ // The engine's own materials are asked for by NAME (`_creator.builtinMaterial`): the runtime finds the
255
+ // shader — in the archive its host ships, or in the host's file of that name. Every call below passes
256
+ // a string LITERAL: the compile reads a bundle's preload list out of them (compile/header.ts), so a
257
+ // name that reaches the call through a variable is a material a web host never fetches.
258
+
259
+ /** The material an instance handle is of; a host that carries none under `name` is an error naming it. */
260
+ private static _builtin(h: Handle | null, name: string): Material {
261
+ if (!h) throw new Error(`Material: this host carries no "${name}" material`)
262
+ return new Material(h)
263
+ }
241
264
 
242
265
  /** PBR lit material. */
243
266
  static lit(options: LitMaterialOptions = {}): Material {
244
- // The fetchLocal literal must be inline so the compiler's preload header captures the shader name.
245
- const m = new Material({ _id: _creatorUtils.fetchLocal("lit.filamat") } as any)
267
+ const m = Material._builtin(_creator.builtinMaterial("lit"), "lit")
246
268
  m._colorKey = "baseColor"
247
269
  if (options.color !== undefined) m.color = options.color
248
270
  if (options.map) m.map = options.map
@@ -255,16 +277,13 @@ export class Material {
255
277
  /** Unlit (flat) material — opaque unless `transparent` asks for alpha blending. */
256
278
  static unlit(options: UnlitMaterialOptions = {}): Material {
257
279
  // Blending is baked into the shader package (a material instance can't change it), so the
258
- // blended twin is its own file. Both fetchLocal literals stay inline — that is what puts the
259
- // shader names in the compiler's preload header. A host that predates unlit-transparent answers
260
- // -1; fall back to the opaque one rather than handing createMaterial a bad handle.
261
- let id = -1
280
+ // blended twin is its own material. A host without it draws opaque.
281
+ let h: Handle | null = null
262
282
  if (options.transparent) {
263
- id = _creatorUtils.fetchLocal("unlit-transparent.filamat")
264
- if (id < 0) console.warn("[material] this host ships no unlit-transparent.filamat — drawing opaque")
283
+ h = _creator.builtinMaterial("unlit-transparent")
284
+ if (!h) console.warn("[material] this host carries no unlit-transparent material — drawing opaque")
265
285
  }
266
- if (id < 0) id = _creatorUtils.fetchLocal("unlit.filamat")
267
- const m = new Material({ _id: id } as any)
286
+ const m = Material._builtin(h ?? _creator.builtinMaterial("unlit"), "unlit")
268
287
  m._colorKey = "baseColor"
269
288
  if (options.color !== undefined) m.color = options.color
270
289
  if (options.map) m.map = options.map
@@ -277,10 +296,9 @@ export class Material {
277
296
  * knob is primed here; JS writes after construction override them. */
278
297
  static particles(options: ParticlesMaterialOptions = {}): Material {
279
298
  const quad = options.render !== undefined && options.render !== "point"
280
- // Both fetchLocal literals inline so the compiler's preload header captures the shader names.
281
299
  const m = quad
282
- ? new Material({ _id: _creatorUtils.fetchLocal("particles-quad.filamat") } as any)
283
- : new Material({ _id: _creatorUtils.fetchLocal("particles.filamat") } as any)
300
+ ? Material._builtin(_creator.builtinMaterial("particles-quad"), "particles-quad")
301
+ : Material._builtin(_creator.builtinMaterial("particles"), "particles")
284
302
  m.uniforms.sheetSize = _sheetColsRows(options.sheet)
285
303
  m.uniforms.emissive = options.emissive ?? 0
286
304
  m.uniforms.additive = options.blend === "add" ? 1 : 0
@@ -301,10 +319,9 @@ export class Material {
301
319
  * scene behind it. Blended, no depth write, no shadows — the engine keeps the scene depth bound
302
320
  * while a set with live decals is on screen. */
303
321
  static decal(options: DecalMaterialOptions = {}): Material {
304
- // Both fetchLocal literals inline so the compiler's preload header captures the shader names.
305
322
  const m = options.normalMap
306
- ? new Material({ _id: _creatorUtils.fetchLocal("decal-relief.filamat") } as any)
307
- : new Material({ _id: _creatorUtils.fetchLocal("decal.filamat") } as any)
323
+ ? Material._builtin(_creator.builtinMaterial("decal-relief"), "decal-relief")
324
+ : Material._builtin(_creator.builtinMaterial("decal"), "decal")
308
325
  m.uniforms.time = 0
309
326
  m.uniforms.edge = options.edge ?? 0.3
310
327
  m.uniforms.angleFade = options.angleFade ?? 0.3
@@ -321,20 +338,21 @@ export class Material {
321
338
 
322
339
  /** Material that samples a VideoPlayer's texture. */
323
340
  static video(map?: Texture): Material {
324
- const m = new Material({ _id: _creatorUtils.fetchLocal("video.filamat") } as any)
341
+ const m = Material._builtin(_creator.builtinMaterial("video"), "video")
325
342
  if (map) m.uniforms.baseColorMap = map
326
343
  return m
327
344
  }
328
345
 
329
- /** The lightmap material (packages/creator-bake): a lit PBR material whose whole DIFFUSE light is the
346
+ /** The lightmap material (engines/bake): a lit PBR material whose whole DIFFUSE light is the
330
347
  * baked irradiance atlas on UV1 (`lightmapLight`, lux) — the real-time sun, lamps and diffuse IBL do not
331
348
  * touch it; the specular IBL stays, occluded by the baked sky visibility. Models take it through
332
349
  * `Model.load(…, { lightmap: true })`; a static Mesh gets one from here. Parameters use gltfio's names
333
350
  * (`baseColorFactor`, `baseColorMap`, `roughnessFactor`, `metallicFactor`) plus `lightmapLight`, `lightmapAux`,
334
351
  * `lightmapST`, `lightScale` (the level-wide value `Lightmap.load` sets) and the debug view knobs. */
335
352
  static lightmap(): Material {
336
- // Inline literal: the compiler's preload header captures the shader name from it.
337
- const m = new Material({ _id: _creatorUtils.fetchLocal("lightmap.filamat") } as any)
353
+ // The lite material is only asked for (200 KB of shader) when the tier wants it.
354
+ const lite = Material.lightmapShading === "lite" ? Material._liteTier(_creator.builtinMaterial("lightmap-lite")) : null
355
+ const m = Material._builtin(lite ?? _creator.builtinMaterial("lightmap"), "lightmap")
338
356
  Material._lightmapDefaults(m)
339
357
  return m
340
358
  }
@@ -342,18 +360,33 @@ export class Material {
342
360
  /** `Material.lightmap()`'s masked twin (`blending: masked`, same shader and parameters): what the engine
343
361
  * gives a lightmapped model's alpha-MASK materials. The cutoff comes from the glTF material. */
344
362
  static lightmapMasked(): Material {
345
- const m = new Material({ _id: _creatorUtils.fetchLocal("lightmap-masked.filamat") } as any)
363
+ const lite = Material.lightmapShading === "lite" ? Material._liteTier(_creator.builtinMaterial("lightmap-masked-lite")) : null
364
+ const m = Material._builtin(lite ?? _creator.builtinMaterial("lightmap-masked"), "lightmap-masked")
346
365
  Material._lightmapDefaults(m)
347
366
  return m
348
367
  }
349
368
 
369
+ /** THE LIGHTMAP SHADER TIER, engine-wide: `"full"` = the target's own blob (desktop: the bicubic atlas fetch, eight
370
+ * probe corners, the sun's highlight on metal), `"lite"` = the blob the phones ship (bilinear fetch, one probe
371
+ * corner, no sun highlight; the movers' shadows on the statics stay) — on an integrated GPU some 30 % less per
372
+ * pixel on a baked level, the picture a phone shows. Read when a lightmap material is FIRST made, which
373
+ * `Model.load({ lightmap })` does once per world: set it BEFORE the level loads (a graphics preset applied at
374
+ * boot, like `Texture.maxSize`); a change afterwards takes the next level. A host without the lite material
375
+ * (a phone: its own blob is that shader already) takes the full one. */
376
+ static lightmapShading: "full" | "lite" = "full"
377
+ /** The lite tier's instance, or — said once — that this host has none and the full shader is used. */
378
+ private static _liteTier(lite: Handle | null): Handle | null {
379
+ if (!lite && !Material._liteWarned) { Material._liteWarned = true; console.warn("Material.lightmapShading = 'lite': this host carries no lite lightmap material — the full shader is used") }
380
+ return lite
381
+ }
382
+ private static _liteWarned = false
383
+
350
384
  /** The terrain splat material (docs/terrain-plan.md §1.5): four albedo (+ normal-map) layers blended by
351
385
  * a control map on the terrain's own grid, per-layer `tiling` (metres per repeat) / `roughness` /
352
386
  * `normalScale` / `triplanar`. `Terrain` builds and owns one per terrain; the uniforms are primed here so an
353
387
  * unset layer is white. Lit real-time; a BAKED terrain takes `Material.terrainLightmap()`. */
354
388
  static terrain(): Material {
355
- // Inline literal: the compiler's preload header captures the shader name from it.
356
- const m = new Material({ _id: _creatorUtils.fetchLocal("terrain.filamat") } as any)
389
+ const m = Material._builtin(_creator.builtinMaterial("terrain"), "terrain")
357
390
  m.set("tiling", [ 8, 8, 8, 8 ]).set("roughness", [ 1, 1, 1, 1 ]).set("normalScale", [ 0, 0, 0, 0 ]).set("triplanar", [ 0, 0, 0, 0 ])
358
391
  m.set("gridSize", [ 2, 2 ]).set("cellSize", 1).set("tint", "#ffffff")
359
392
  return m
@@ -365,8 +398,7 @@ export class Material {
365
398
  * bound bake it draws black, like any lightmapped static. No reflection probes (the sampler budget): it reflects the
366
399
  * sky through the baked sky visibility. */
367
400
  static terrainLightmap(): Material {
368
- // Inline literal: the compiler's preload header captures the shader name from it.
369
- const m = new Material({ _id: _creatorUtils.fetchLocal("terrain-lightmap.filamat") } as any)
401
+ const m = Material._builtin(_creator.builtinMaterial("terrain-lightmap"), "terrain-lightmap")
370
402
  m.set("tiling", [ 8, 8, 8, 8 ]).set("roughness", [ 1, 1, 1, 1 ]).set("normalScale", [ 0, 0, 0, 0 ]).set("triplanar", [ 0, 0, 0, 0 ])
371
403
  m.set("gridSize", [ 2, 2 ]).set("cellSize", 1).set("tint", "#ffffff")
372
404
  m.set("lightmapST", [ 1, 1, 0, 0 ]).set("lightScale", 1).set("debugMode", 0).set("debugParams", [ 0.001, 0, 5, 0 ])
@@ -386,7 +418,7 @@ export class Material {
386
418
  * and a per-copy tint. glTF parameters like `Material.lightmap()` plus `wind` / `sway` / `fade` / `benders`,
387
419
  * which the engine writes itself. Not baked by the rewritten bake yet. */
388
420
  static foliage(): Material {
389
- const m = new Material({ _id: _creatorUtils.fetchLocal("foliage.filamat") } as any)
421
+ const m = Material._builtin(_creator.builtinMaterial("foliage"), "foliage")
390
422
  Material._glbDefaults(m)
391
423
  return m
392
424
  }
@@ -394,7 +426,7 @@ export class Material {
394
426
  /** `Material.foliage()`'s masked twin (`blending: masked`): what the engine gives a foliage model's alpha-MASK
395
427
  * materials — the cards. */
396
428
  static foliageMasked(): Material {
397
- const m = new Material({ _id: _creatorUtils.fetchLocal("foliage-masked.filamat") } as any)
429
+ const m = Material._builtin(_creator.builtinMaterial("foliage-masked"), "foliage-masked")
398
430
  Material._glbDefaults(m)
399
431
  return m
400
432
  }
@@ -409,16 +441,28 @@ export class Material {
409
441
  m.set("lightmapST", [ 1, 1, 0, 0 ]).set("lightScale", 1).set("debugMode", 0).set("debugParams", [ 0.001, 0, 5, 0 ])
410
442
  }
411
443
 
412
- /** @internal the foliage template pair, like the lightmap one (a host that lacks the tier returns id 0 from
413
- * fetchLocal — `Model.load` then falls back to the lightmap tier). */
414
- static _foliageTemplate(): Material { return (Material._folTemplate ??= Material.foliage()) }
415
- private static _folTemplate: Material | undefined
416
- static _foliageMaskedTemplate(): Material { return (Material._folMaskedTemplate ??= Material.foliageMasked()) }
417
- private static _folMaskedTemplate: Material | undefined
444
+ /** @internal the foliage template pair, like the lightmap one; null on a host that carries no foliage
445
+ * tier — `Model.load` then falls back to the lightmap tier. */
446
+ static _foliageTemplate(): Material | null {
447
+ if (Material._folTemplate === undefined) Material._folTemplate = Material._glbTemplate(_creator.builtinMaterial("foliage"))
448
+ return Material._folTemplate
449
+ }
450
+ private static _folTemplate: Material | null | undefined
451
+ static _foliageMaskedTemplate(): Material | null {
452
+ if (Material._folMaskedTemplate === undefined) Material._folMaskedTemplate = Material._glbTemplate(_creator.builtinMaterial("foliage-masked"))
453
+ return Material._folMaskedTemplate
454
+ }
455
+ private static _folMaskedTemplate: Material | null | undefined
456
+ private static _glbTemplate(h: Handle | null): Material | null {
457
+ if (!h) return null
458
+ const m = new Material(h)
459
+ Material._glbDefaults(m)
460
+ return m
461
+ }
418
462
 
419
463
  /** Shadow-catcher material (transparent except where shadows fall). */
420
464
  static shadow(color: ColorInput = "#000000aa"): Material {
421
- const m = new Material({ _id: _creatorUtils.fetchLocal("shadow.filamat") } as any)
465
+ const m = Material._builtin(_creator.builtinMaterial("shadow"), "shadow")
422
466
  m._colorKey = "color"
423
467
  m.color = color
424
468
  return m
@@ -435,7 +479,7 @@ export class Material {
435
479
  if (!(id >= 0)) {
436
480
  return Promise.reject(new Error(`Shader not found next to the app (${url}). Compile it with \`lecodes shaders compile\`.`))
437
481
  }
438
- return Promise.resolve(new Material({ _id: id } as any))
482
+ return Promise.resolve(new Material({ _h: { id: id } } as any))
439
483
  }
440
484
  return fetch(url, { useOnce: true }).then((resp) => {
441
485
  if (resp.status >= 400) return Promise.reject(new Error(`Failed to fetch shader from ${url}. HTTP ${resp.status}`))
@@ -444,7 +488,7 @@ export class Material {
444
488
  }
445
489
 
446
490
  /** @internal */
447
- static idOf(material: Material): number { return material._id }
491
+ static idOf(material: Material): number { return material._h.id }
448
492
  }
449
493
 
450
494
  const applyDepth = (m: Material, o: MaterialStateOptions): void => {