lecodes-sdk 0.20.0 → 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 (114) hide show
  1. package/dist/global.d.ts +48 -5
  2. package/dist/inject.js +361 -260
  3. package/dist/types/audio/Bus.d.ts +45 -0
  4. package/dist/types/audio/Sound.d.ts +28 -0
  5. package/dist/types/audio/Voice.d.ts +27 -0
  6. package/dist/types/audio/audio.d.ts +83 -0
  7. package/dist/types/audio/support.d.ts +1 -0
  8. package/dist/types/canvas/Canvas.d.ts +2 -0
  9. package/dist/types/gl/DecalSet.d.ts +148 -0
  10. package/dist/types/gl/Geometry.d.ts +17 -0
  11. package/dist/types/gl/Light.d.ts +7 -0
  12. package/dist/types/gl/Lightmap.d.ts +9 -0
  13. package/dist/types/gl/Material.d.ts +90 -2
  14. package/dist/types/gl/Mesh.d.ts +18 -1
  15. package/dist/types/gl/Model.d.ts +34 -0
  16. package/dist/types/gl/Particles.d.ts +13 -0
  17. package/dist/types/gl/Scene.d.ts +23 -0
  18. package/dist/types/gl/Texture.d.ts +29 -1
  19. package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
  20. package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
  21. package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
  22. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  23. package/dist/types/gl/animation/core.d.ts +15 -10
  24. package/dist/types/gl/audio/AudioSource.d.ts +60 -0
  25. package/dist/types/gl/audio/AudioZone.d.ts +32 -0
  26. package/dist/types/gl/audio/SceneAudio.d.ts +11 -0
  27. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  28. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  29. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  30. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  31. package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
  32. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  33. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  34. package/dist/types/gl/state.d.ts +0 -1
  35. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
  36. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  37. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  38. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  39. package/dist/types/inject.d.ts +33 -21
  40. package/dist/types/runtime/files.d.ts +24 -1
  41. package/dist/types/runtime/input.d.ts +11 -0
  42. package/dist/types/scene/defineScene.d.ts +10 -3
  43. package/dist/types/ui/UIImage.d.ts +15 -5
  44. package/dist/types.json +1 -1
  45. package/package.json +1 -1
  46. package/prompts/README.md +142 -142
  47. package/prompts/dist/2d-game.md +408 -197
  48. package/prompts/dist/3d-app.md +491 -166
  49. package/prompts/dist/ar-app.md +373 -163
  50. package/prompts/dist/design.md +83 -87
  51. package/prompts/dist/ui-app.md +325 -136
  52. package/src/audio/Bus.ts +102 -0
  53. package/src/audio/Sound.ts +96 -0
  54. package/src/audio/Voice.ts +102 -0
  55. package/src/audio/audio.ts +161 -0
  56. package/src/audio/support.ts +6 -0
  57. package/src/bridges.d.ts +279 -32
  58. package/src/canvas/Canvas.ts +21 -0
  59. package/src/compile/__tests__/compile.test.ts +11 -0
  60. package/src/compile/compileProject.ts +30 -15
  61. package/src/compile/header.ts +6 -3
  62. package/src/compile/index.ts +4 -0
  63. package/src/compile/sceneEditor.ts +42 -1
  64. package/src/core/Aspect.ts +33 -8
  65. package/src/g2/Scene2D.ts +7 -0
  66. package/src/gl/CameraPlace.ts +52 -52
  67. package/src/gl/DecalSet.ts +360 -0
  68. package/src/gl/Geometry.ts +348 -279
  69. package/src/gl/Light.ts +16 -0
  70. package/src/gl/Lightmap.ts +35 -7
  71. package/src/gl/Material.ts +173 -4
  72. package/src/gl/Mesh.ts +120 -83
  73. package/src/gl/Model.ts +33 -1
  74. package/src/gl/Node.ts +1 -1
  75. package/src/gl/Particles.ts +21 -3
  76. package/src/gl/Scene.ts +41 -7
  77. package/src/gl/Texture.ts +43 -3
  78. package/src/gl/animation/AnimationClip.ts +43 -20
  79. package/src/gl/animation/Animator.ts +4 -3
  80. package/src/gl/animation/DynamicBone.ts +459 -0
  81. package/src/gl/{IK.ts → animation/IK.ts} +4 -4
  82. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  83. package/src/gl/animation/core.ts +20 -15
  84. package/src/gl/audio/AudioSource.ts +113 -0
  85. package/src/gl/audio/AudioZone.ts +75 -0
  86. package/src/gl/audio/SceneAudio.ts +26 -0
  87. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  88. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  89. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  90. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  91. package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +272 -270
  92. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  93. package/src/gl/{Trigger.ts → physics/Trigger.ts} +2 -2
  94. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  95. package/src/gl/scenarios.ts +291 -291
  96. package/src/gl/state.ts +1 -1
  97. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
  98. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  99. package/src/gl/vehicle/Vehicle.ts +666 -0
  100. package/src/gl/vehicle/Wheel.ts +290 -0
  101. package/src/inject.ts +226 -212
  102. package/src/runtime/files.ts +32 -2
  103. package/src/runtime/input.ts +6 -1
  104. package/src/scene/defineScene.ts +26 -10
  105. package/src/scene/gizmos.ts +148 -148
  106. package/src/scene/level.ts +2 -2
  107. package/src/ui/UIImage.ts +21 -7
  108. package/dist/types/gl/Gearbox.d.ts +0 -86
  109. package/dist/types/gl/Vehicle.d.ts +0 -191
  110. package/dist/types/gl/Wheel.d.ts +0 -95
  111. package/src/gl/Gearbox.ts +0 -212
  112. package/src/gl/Vehicle.ts +0 -473
  113. package/src/gl/Wheel.ts +0 -240
  114. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/gl/Light.ts CHANGED
@@ -67,6 +67,11 @@ export class Light extends Node {
67
67
  _direction: [number, number, number] = [ 0, -1, 0 ]
68
68
  /** The most recently created sun — what `Lightmap.load` uses unless told otherwise. */
69
69
  static lastSun: Light | null = null
70
+ /** @internal the colour as created / last set (a settings menu recreates a sun from these). */
71
+ _color: ColorInput = 0xffffff
72
+ /** @internal sun shadow options as created (creation-time in the engine). */
73
+ _shadowsQuality = 1
74
+ _shadowDistance = 100
70
75
 
71
76
  /** A directional sun light. */
72
77
  static sun(options: SunOptions = {}): Light {
@@ -74,6 +79,9 @@ export class Light extends Node {
74
79
  const [ dx, dy, dz ] = options.direction ?? [ 0.548267, -0.473983, -0.689016 ]
75
80
  light._direction = [ dx, dy, dz ]
76
81
  light._intensity = options.intensity ?? 100000
82
+ light._color = options.color ?? 0xffffff
83
+ light._shadowsQuality = options.shadowsQuality ?? 1
84
+ light._shadowDistance = options.shadowDistance ?? 100
77
85
  Light.lastSun = light
78
86
  _creator.createSunLight(
79
87
  light.id, dx, dy, dz,
@@ -95,6 +103,7 @@ export class Light extends Node {
95
103
  static point(options: PointOptions = {}): Light {
96
104
  const light = new Light()
97
105
  light._intensity = options.intensity ?? 1000
106
+ light._color = options.color ?? 0xffffff
98
107
  light._baked = options.baked ?? true
99
108
  Light._points.add(light)
100
109
  _creator.createPointLight?.(
@@ -130,6 +139,13 @@ export class Light extends Node {
130
139
  }
131
140
 
132
141
  set color(value: ColorInput) {
142
+ this._color = value
133
143
  _creator.setLightColor?.(this.id, Color.toPackedRgb(value))
134
144
  }
145
+ get color(): ColorInput { return this._color }
146
+ /** Sun direction as created (the engine keeps it; a settings menu rebuilds a sun from it). */
147
+ get direction(): [number, number, number] { return this._direction }
148
+ /** Sun shadow options as created — creation-time in the engine, so a change means a new sun. */
149
+ get shadowsQuality(): number { return this._shadowsQuality }
150
+ get shadowDistance(): number { return this._shadowDistance }
135
151
  }
@@ -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
 
@@ -241,7 +250,8 @@ export class Lightmap {
241
250
  if (pageFiles.length < pages) {
242
251
  console.warn(`Lightmap: the bake took ${pages} atlas pages but the scene lists ${pageFiles.length} — add the other page files (${(data.textures ?? []).slice(pageFiles.length).join(", ")}) to env.lightmap.texture`)
243
252
  }
244
- const textures = await Promise.all(pageFiles.slice(0, pages).map((f) => Texture.load(f)))
253
+ // baked lighting is exempt from Texture.maxSize: a capped atlas blurs every shadow edge
254
+ const textures = await Promise.all(pageFiles.slice(0, pages).map((f) => Texture.load(f, { fullSize: true })))
245
255
  // the light atlas: the bake carries the point lights only when the scene passes their atlas
246
256
  const lightFiles = list(files.light)
247
257
  let lightTextures: Texture[] = []
@@ -252,7 +262,7 @@ export class Lightmap {
252
262
  console.warn(`Lightmap: the bake carries ${data.lights?.length ?? 0} baked point light(s) but the scene passes no \`light\` atlas (${data.lightTextures[0]}) — the lamps stay real-time`)
253
263
  } else {
254
264
  if (lightFiles.length < pages) console.warn(`Lightmap: the light atlas has ${pages} pages but the scene lists ${lightFiles.length}`)
255
- lightTextures = await Promise.all(lightFiles.slice(0, pages).map((f) => Texture.load(f)))
265
+ lightTextures = await Promise.all(lightFiles.slice(0, pages).map((f) => Texture.load(f, { fullSize: true })))
256
266
  lightScale = (data.lightScale ?? 0) * (options.lightBoost ?? 1)
257
267
  volumeScale = (data.lightScale ?? 0) * (options.volumeBoost ?? options.lightBoost ?? 1)
258
268
  }
@@ -273,12 +283,29 @@ export class Lightmap {
273
283
  // the light volume next: it claims every lightmap-material instance, the atlas rects below win back the statics
274
284
  const volume = files.volume ? await Lightmap.loadVolume(files.volume, volumeScale, lightGamma, ambientScale, strength, aoStrength) : null
275
285
  const rects = new Map<string, { st: number[], page: number }>()
276
- for (const inst of data.instances) if (inst.receiver && inst.st) rects.set(inst.key, { st: inst.st, page: inst.page ?? 0 })
277
- 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
278
296
  const missing: string[] = []
279
297
  for (const e of Lightmap.entries) {
280
298
  const r = rects.get(e.key)
281
- 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
+ }
282
309
  const st = r.st
283
310
  const texture = textures[r.page]
284
311
  const light = r.page < lightTextures.length ? lightTextures[r.page] : null
@@ -303,7 +330,8 @@ export class Lightmap {
303
330
  // the lights the bake carries go dark: the atlas holds them for the statics, the volume for the movers
304
331
  let lights = 0
305
332
  if (lightTextures.length > 0 && data.lights?.length) lights = Lightmap.switchOffBaked(data.lights)
306
- 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 }
307
335
  if (volume) info.volume = { dims: volume.dims, cell: volume.cell }
308
336
  return info
309
337
  }
@@ -36,8 +36,49 @@ const applyUniform = (id: number, key: string, value: UniformValue): void => {
36
36
 
37
37
  export type MaterialColorOptions = { color?: ColorInput, map?: Texture | Canvas | null }
38
38
 
39
+ /** How a stencil test / write on a material compares and what it writes. `test` runs against the
40
+ * scene's stencil buffer (`Scene.stencil` must be on); the ops say what the buffer gets when the
41
+ * fragment passes / fails the stencil test / fails the depth test (`replace` writes `ref`). */
42
+ export type StencilTest = "always" | "never" | "less" | "lessEqual" | "greater" | "greaterEqual" | "equal" | "notEqual"
43
+ export type StencilOp = "keep" | "zero" | "replace" | "increment" | "decrement" | "invert"
44
+ export type MaterialStencil = {
45
+ /** Write the stencil buffer at all. Default `false`. */
46
+ write?: boolean
47
+ /** The reference value, 0..255. Default 0. */
48
+ ref?: number
49
+ /** Default `"always"`. */
50
+ test?: StencilTest
51
+ onPass?: StencilOp
52
+ onFail?: StencilOp
53
+ onDepthFail?: StencilOp
54
+ readMask?: number
55
+ writeMask?: number
56
+ }
57
+
58
+ /** Render state a material instance can override at run time (Filament keeps the defaults in the
59
+ * shader package; these are per-instance overrides on top). */
60
+ export type MaterialStateOptions = {
61
+ /** Test against the scene's depth buffer. `false` draws over everything already drawn — an
62
+ * editor gizmo, a marker that must never hide behind a wall. Pair it with a high
63
+ * `Mesh.renderPriority` so nothing drawn later covers it. Default `true`. */
64
+ depthTest?: boolean
65
+ /** Write to the depth buffer. Leave it on for something drawn over the scene whose own parts must
66
+ * still occlude each other (a gizmo's cone in front of its shaft). Unset = the shader's default
67
+ * (on for opaque, off for a blended one). */
68
+ depthWrite?: boolean
69
+ /** Draw both faces (no back-face culling): a plane seen from behind, a ribbon, cloth, a flat
70
+ * marker. Default `false` = the shader's culling (back faces dropped). */
71
+ doubleSided?: boolean
72
+ /** A stencil test / write for this material — see `MaterialStencil`. The selection-outline
73
+ * recipe: the object writes `{ write: true, ref: 1, onPass: "replace" }`, and a slightly larger
74
+ * copy of it draws with `{ test: "notEqual", ref: 1 }` + `depthTest: false` in `renderPriority` 7. */
75
+ stencil?: MaterialStencil
76
+ }
77
+ /** @deprecated the name before doubleSided / stencil joined it — the same type */
78
+ export type MaterialDepthOptions = MaterialStateOptions
79
+
39
80
  /** `Material.unlit` options. */
40
- export type UnlitMaterialOptions = MaterialColorOptions & {
81
+ export type UnlitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
41
82
  /** Alpha-blend this material instead of drawing it opaque. Opaque is the default: a blended draw
42
83
  * writes no depth, is sorted back-to-front and casts no shadow, which is rarely what a flat
43
84
  * colour wants. Turn it on for anything that must show what is behind it — a glass pane, a
@@ -46,6 +87,27 @@ export type UnlitMaterialOptions = MaterialColorOptions & {
46
87
  transparent?: boolean
47
88
  }
48
89
 
90
+ /** `Material.decal` options — the projected-decal material a `DecalSet` draws with. */
91
+ export type DecalMaterialOptions = {
92
+ /** The atlas (a `DecalSet`'s `sheet` cuts it into cells). */
93
+ map?: Texture
94
+ /** A tangent-space normal atlas (same cell grid; +X = the image's right, +Y = its top; LINEAR —
95
+ * `Texture.fromPixels(…, { srgb: false })`). Turns the decal into a RELIEF decal: instead of
96
+ * painting a colour it bends the surface's lighting, so a footprint or a dent shows on any
97
+ * surface without a colour of its own. With `map` too, the colour multiplies in (a crater
98
+ * darkens by the map's alpha). The sun term applies in shadow as well (no shadow read). */
99
+ normalMap?: Texture
100
+ /** Relief strength — the normal map's xy scale. Default 1. */
101
+ bump?: number
102
+ /** Soft fraction (0..1) of the box's half depth at both ends. Default 0.3. */
103
+ edge?: number
104
+ /** Cosine of the surface angle past which the decal fades (0.3 ≈ 72°); 0 = project onto
105
+ * anything. Default 0.3. */
106
+ angleFade?: number
107
+ /** HDR boost of the image (0 = none). */
108
+ emissive?: number
109
+ }
110
+
49
111
  /** `Material.particles` options — the default point-sprite material for particle systems. */
50
112
  export type ParticlesMaterialOptions = {
51
113
  /** Sprite texture — a single image or a flipbook sheet of frames. Unset = soft round dot. */
@@ -92,13 +154,16 @@ export const _sheetColsRows = (sheet: number | [number, number] | undefined): [n
92
154
  }
93
155
 
94
156
  /** `Material.lit` options — PBR scalars on top of the color/map pair. */
95
- export type LitMaterialOptions = MaterialColorOptions & {
157
+ export type LitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
96
158
  /** Perceptual roughness, 0 (mirror) … 1 (matte). Unset = the shader's default. */
97
159
  roughness?: number
98
160
  /** Metallic factor, 0 (dielectric) … 1 (metal). Unset = the shader's default. */
99
161
  metallic?: number
100
162
  }
101
163
 
164
+ /** `Material.lightmapShading` tiers — see the setter. */
165
+ export type LightmapShading = "full" | "baked" | "baked-lite"
166
+
102
167
  export class Material {
103
168
  /** @internal native material-instance handle. */
104
169
  _id: number
@@ -133,6 +198,33 @@ export class Material {
133
198
  if (this._colorKey) this.uniforms[this._colorKey] = Color.toHexString(c)
134
199
  }
135
200
 
201
+ /** Depth test against the scene (write-only; see `MaterialDepthOptions`). A host that predates
202
+ * the call leaves the material as the shader has it. */
203
+ set depthTest(on: boolean) {
204
+ if (_creator.setMaterialDepthTest) _creator.setMaterialDepthTest(this._id, on)
205
+ else warnOnce("depthTest", "[material] this host has no setMaterialDepthTest — depthTest ignored")
206
+ }
207
+ /** Depth write (write-only; see `MaterialStateOptions`). */
208
+ set depthWrite(on: boolean) {
209
+ if (_creator.setMaterialDepthWrite) _creator.setMaterialDepthWrite(this._id, on)
210
+ else warnOnce("depthWrite", "[material] this host has no setMaterialDepthWrite — depthWrite ignored")
211
+ }
212
+ /** Both faces drawn (write-only; see `MaterialStateOptions`). */
213
+ set doubleSided(on: boolean) {
214
+ // 0 = no culling, 2 = back faces culled (the shader's usual default)
215
+ if (_creator.setMaterialCulling) _creator.setMaterialCulling(this._id, on ? 0 : 2)
216
+ else warnOnce("doubleSided", "[material] this host has no setMaterialCulling — doubleSided ignored")
217
+ }
218
+ /** The stencil test / write (write-only; `null` = back to none). See `MaterialStencil`. */
219
+ set stencil(s: MaterialStencil | null) {
220
+ if (!_creator.setMaterialStencil) { warnOnce("stencil", "[material] this host has no setMaterialStencil — stencil ignored"); return }
221
+ const o = s ?? {}
222
+ const clamp = (v: number | undefined, d: number): number => Math.max(0, Math.min(255, Math.round(v ?? d)))
223
+ _creator.setMaterialStencil(this._id, !!o.write, STENCIL_TEST.indexOf(o.test ?? "always"), clamp(o.ref, 0),
224
+ STENCIL_OP.indexOf(o.onPass ?? "keep"), STENCIL_OP.indexOf(o.onFail ?? "keep"), STENCIL_OP.indexOf(o.onDepthFail ?? "keep"),
225
+ clamp(o.readMask, 255), clamp(o.writeMask, 255))
226
+ }
227
+
136
228
  // lit/unlit shade baseColor × baseColorMap (factor×map; engines default both to white), so
137
229
  // removing the map means resetting baseColorMap to the white 1×1 — no flag to clear. (Materials
138
230
  // used to carry a `useBaseColorMap` bool for shaders that branched instead of multiplying; the
@@ -158,6 +250,7 @@ export class Material {
158
250
  if (options.map) m.map = options.map
159
251
  if (options.roughness !== undefined) m.uniforms.roughness = options.roughness
160
252
  if (options.metallic !== undefined) m.uniforms.metallic = options.metallic
253
+ applyDepth(m, options)
161
254
  return m
162
255
  }
163
256
 
@@ -177,6 +270,7 @@ export class Material {
177
270
  m._colorKey = "baseColor"
178
271
  if (options.color !== undefined) m.color = options.color
179
272
  if (options.map) m.map = options.map
273
+ applyDepth(m, options)
180
274
  return m
181
275
  }
182
276
 
@@ -205,6 +299,28 @@ export class Material {
205
299
  return m
206
300
  }
207
301
 
302
+ /** Projected-decal material (`DecalSet`): samples the atlas where the decal's box meets the opaque
303
+ * scene behind it. Blended, no depth write, no shadows — the engine keeps the scene depth bound
304
+ * while a set with live decals is on screen. */
305
+ static decal(options: DecalMaterialOptions = {}): Material {
306
+ // Both fetchLocal literals inline so the compiler's preload header captures the shader names.
307
+ const m = options.normalMap
308
+ ? new Material({ _id: _creatorUtils.fetchLocal("decal-relief.filamat") } as any)
309
+ : new Material({ _id: _creatorUtils.fetchLocal("decal.filamat") } as any)
310
+ m.uniforms.time = 0
311
+ m.uniforms.edge = options.edge ?? 0.3
312
+ m.uniforms.angleFade = options.angleFade ?? 0.3
313
+ if (options.normalMap) {
314
+ m.uniforms.normalMap = options.normalMap
315
+ m.uniforms.bump = options.bump ?? 1
316
+ m.uniforms.useColor = options.map ? 1 : 0
317
+ } else {
318
+ m.uniforms.emissive = options.emissive ?? 0
319
+ }
320
+ if (options.map) m.map = options.map
321
+ return m
322
+ }
323
+
208
324
  /** Material that samples a VideoPlayer's texture. */
209
325
  static video(map?: Texture): Material {
210
326
  const m = new Material({ _id: _creatorUtils.fetchLocal("video.filamat") } as any)
@@ -218,8 +334,13 @@ export class Material {
218
334
  * `metallicFactor`) plus `lightmap`, `lightmapST`, `ambientScale`, `sunStrength` (the sun / IBL terms come from
219
335
  * filament's per-frame uniforms inside the shader). */
220
336
  static lightmap(): Material {
221
- // Inline literal: the compiler's preload header captures the shader name from it.
222
- const m = new Material({ _id: _creatorUtils.fetchLocal("lightmap.filamat") } as any)
337
+ // Inline literals: the compiler's preload header captures the shader names from them.
338
+ const id = Material.lightmapShading === "baked"
339
+ ? _creatorUtils.fetchLocal("lightmap-baked.filamat")
340
+ : Material.lightmapShading === "baked-lite"
341
+ ? _creatorUtils.fetchLocal("lightmap-baked-lite.filamat")
342
+ : _creatorUtils.fetchLocal("lightmap.filamat")
343
+ const m = new Material({ _id: id } as any)
223
344
  m.set("baseColorFactor", "#ffffffff").set("roughnessFactor", 1).set("metallicFactor", 0)
224
345
  m.set("lightmapST", [ 1, 1, 0, 0 ]).set("ambientScale", 1).set("sunStrength", 1).set("hasNormalMap", 0)
225
346
  // lightScale 0 = no baked point lights until Lightmap.load binds a light atlas / volume
@@ -228,6 +349,22 @@ export class Material {
228
349
  return m
229
350
  }
230
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
+
231
368
  /** The terrain splat material (docs/terrain-plan.md §1.5): four albedo (+ normal-map) layers blended by
232
369
  * a control map on the terrain's own grid, per-layer `tiling` (metres per repeat) / `roughness` /
233
370
  * `normalScale` / `triplanar`, lightmap-aware (the same `lightmap` / `lightmapST` block as
@@ -246,6 +383,26 @@ export class Material {
246
383
  * the GLB provider instantiates; the template itself never draws). */
247
384
  static _lightmapTemplate(): Material { return (Material._lmTemplate ??= Material.lightmap()) }
248
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
390
+
391
+ static _lightmapShading: LightmapShading = "full"
392
+ /** Which shader lightmapped models take — a graphics-quality tier, engine-wide. `"full"` is filament's
393
+ * lit path over the baked atlas (IBL specular, real-time point lights such as a muzzle flash, sun
394
+ * shadows on dynamic objects). `"baked"` keeps the baked light and an approximated ambient but drops
395
+ * the lit path: ~40 % cheaper per pixel on a fill-bound GPU. `"baked-lite"` is that minus the normal,
396
+ * metallic/roughness and occlusion map reads (the factors stand in, the atlas keeps the baked AO;
397
+ * emissive still glows): +20 % more at 1080p on the same GPU. Read when a lightmapped model LOADS, so set
398
+ * it before the level (a settings menu applies it on the next level load), like `Texture.maxSize`. */
399
+ static get lightmapShading(): LightmapShading { return Material._lightmapShading }
400
+ static set lightmapShading(mode: LightmapShading) {
401
+ if (mode === Material._lightmapShading) return
402
+ Material._lightmapShading = mode
403
+ Material._lmTemplate = undefined // the next load builds a template on the new shader
404
+ Material._lmMaskedTemplate = undefined
405
+ }
249
406
 
250
407
  /** Shadow-catcher material (transparent except where shadows fall). */
251
408
  static shadow(color: ColorInput = "#000000aa"): Material {
@@ -277,3 +434,15 @@ export class Material {
277
434
  /** @internal */
278
435
  static idOf(material: Material): number { return material._id }
279
436
  }
437
+
438
+ const applyDepth = (m: Material, o: MaterialStateOptions): void => {
439
+ if (o.depthTest !== undefined) m.depthTest = o.depthTest
440
+ if (o.depthWrite !== undefined) m.depthWrite = o.depthWrite
441
+ if (o.doubleSided !== undefined) m.doubleSided = o.doubleSided
442
+ if (o.stencil !== undefined) m.stencil = o.stencil
443
+ }
444
+ /** the wire order of the stencil enums (creator-gl material.cpp switches on the same numbers) */
445
+ const STENCIL_TEST: StencilTest[] = [ "always", "never", "less", "lessEqual", "greater", "greaterEqual", "equal", "notEqual" ]
446
+ const STENCIL_OP: StencilOp[] = [ "keep", "zero", "replace", "increment", "decrement", "invert" ]
447
+ const warned = new Set<string>()
448
+ const warnOnce = (key: string, msg: string): void => { if (!warned.has(key)) { warned.add(key); console.warn(msg) } }
package/src/gl/Mesh.ts CHANGED
@@ -1,83 +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
- }
24
-
25
- export class Mesh extends Node {
26
- private _geometry?: Geometry
27
-
28
- constructor(geometry?: Geometry, material?: Material) {
29
- super()
30
- if (geometry) {
31
- const mat = material ?? Material.unlit()
32
- this._geometry = geometry
33
- this._materials = [ mat ]
34
- _creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1)
35
- }
36
- }
37
-
38
- override get geometry(): Geometry | undefined { return this._geometry }
39
-
40
- /** A Mesh always carries a material (slot 0) — see Node.setMaterial for the slot API. */
41
- override get material(): Material { return this._materials![0]! }
42
- override set material(m: Material) { this.setMaterial(m, 0) }
43
-
44
- set culling(v: boolean) { _creator.setCulling(this.id, v) }
45
- set castShadows(v: boolean) { _creator.setCastShadows(this.id, v) }
46
- set receiveShadows(v: boolean) { _creator.setReceiveShadows(this.id, v) }
47
-
48
- // --- factories ---
49
-
50
- static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
51
- // the size goes into the geometry (not a scale after): the lightmap chart is laid out per face area
52
- return apply(new Mesh(Geometry.box(options.size ?? 1), options.material), options)
53
- }
54
-
55
- static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
56
- return apply(new Mesh(Geometry.sphere(options), options.material), options)
57
- }
58
-
59
- static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
60
- return apply(new Mesh(Geometry.cylinder(options), options.material), options)
61
- }
62
-
63
- static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
64
- return apply(new Mesh(Geometry.plane(options), options.material), options)
65
- }
66
-
67
- /** Wrap a custom Geometry. */
68
- static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
69
- return apply(new Mesh(geometry, options.material), options)
70
- }
71
- }
72
-
73
- const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])
74
-
75
- const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
76
- if (o.position) mesh.position = o.position
77
- if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
78
- if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
79
- if (o.name !== undefined) mesh.name = o.name
80
- if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
81
- if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
82
- return mesh
83
- }
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
+ }