lecodes-sdk 0.19.2 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/global.d.ts +62 -0
  2. package/dist/host.d.ts +3 -0
  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/gl/AudioSource.d.ts +60 -0
  9. package/dist/types/gl/AudioZone.d.ts +32 -0
  10. package/dist/types/gl/DecalSet.d.ts +103 -0
  11. package/dist/types/gl/Geometry.d.ts +5 -0
  12. package/dist/types/gl/Light.d.ts +7 -0
  13. package/dist/types/gl/Locomotion.d.ts +3 -1
  14. package/dist/types/gl/Material.d.ts +86 -2
  15. package/dist/types/gl/Mesh.d.ts +11 -0
  16. package/dist/types/gl/Scene.d.ts +23 -0
  17. package/dist/types/gl/SceneAudio.d.ts +11 -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/Animator.d.ts +51 -183
  21. package/dist/types/gl/animation/Feet.d.ts +85 -0
  22. package/dist/types/gl/animation/Warp.d.ts +53 -0
  23. package/dist/types/gl/animation/core.d.ts +61 -17
  24. package/dist/types/gl/state.d.ts +0 -1
  25. package/dist/types/inject.d.ts +17 -2
  26. package/dist/types/plugins/map.d.ts +174 -0
  27. package/dist/types/runtime/input.d.ts +11 -0
  28. package/dist/types/ui/UIImage.d.ts +15 -5
  29. package/dist/types.json +1 -1
  30. package/package.json +1 -1
  31. package/src/audio/Bus.ts +102 -0
  32. package/src/audio/Sound.ts +96 -0
  33. package/src/audio/Voice.ts +102 -0
  34. package/src/audio/audio.ts +161 -0
  35. package/src/audio/support.ts +6 -0
  36. package/src/bridges.d.ts +1481 -1345
  37. package/src/compile/__tests__/compile.test.ts +12 -0
  38. package/src/compile/compileProject.ts +35 -15
  39. package/src/compile/index.ts +3 -0
  40. package/src/core/Aspect.ts +34 -9
  41. package/src/g2/Scene2D.ts +7 -0
  42. package/src/gl/AudioSource.ts +113 -0
  43. package/src/gl/AudioZone.ts +75 -0
  44. package/src/gl/DecalSet.ts +233 -0
  45. package/src/gl/Geometry.ts +5 -0
  46. package/src/gl/Light.ts +16 -0
  47. package/src/gl/Lightmap.ts +3 -2
  48. package/src/gl/Locomotion.ts +7 -5
  49. package/src/gl/Material.ts +152 -4
  50. package/src/gl/Mesh.ts +20 -1
  51. package/src/gl/Particles.ts +3 -3
  52. package/src/gl/Scene.ts +42 -8
  53. package/src/gl/SceneAudio.ts +26 -0
  54. package/src/gl/Texture.ts +43 -3
  55. package/src/gl/Vehicle.ts +5 -5
  56. package/src/gl/animation/AnimationClip.ts +43 -20
  57. package/src/gl/animation/Animator.ts +138 -329
  58. package/src/gl/animation/Feet.ts +134 -0
  59. package/src/gl/animation/Loop.ts +3 -1
  60. package/src/gl/animation/Warp.ts +96 -0
  61. package/src/gl/animation/core.ts +741 -670
  62. package/src/gl/state.ts +6 -6
  63. package/src/host.d.ts +3 -0
  64. package/src/inject.ts +23 -2
  65. package/src/plugins/map.ts +396 -0
  66. package/src/runtime/input.ts +6 -1
  67. package/src/ui/UIImage.ts +21 -7
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
  }
@@ -241,7 +241,8 @@ export class Lightmap {
241
241
  if (pageFiles.length < pages) {
242
242
  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
243
  }
244
- const textures = await Promise.all(pageFiles.slice(0, pages).map((f) => Texture.load(f)))
244
+ // baked lighting is exempt from Texture.maxSize: a capped atlas blurs every shadow edge
245
+ const textures = await Promise.all(pageFiles.slice(0, pages).map((f) => Texture.load(f, { fullSize: true })))
245
246
  // the light atlas: the bake carries the point lights only when the scene passes their atlas
246
247
  const lightFiles = list(files.light)
247
248
  let lightTextures: Texture[] = []
@@ -252,7 +253,7 @@ export class Lightmap {
252
253
  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
254
  } else {
254
255
  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)))
256
+ lightTextures = await Promise.all(lightFiles.slice(0, pages).map((f) => Texture.load(f, { fullSize: true })))
256
257
  lightScale = (data.lightScale ?? 0) * (options.lightBoost ?? 1)
257
258
  volumeScale = (data.lightScale ?? 0) * (options.volumeBoost ?? options.lightBoost ?? 1)
258
259
  }
@@ -29,7 +29,9 @@
29
29
 
30
30
  import { Aspect } from "../core/Aspect"
31
31
  import { Vec3, cx, cz, type Vec3Like } from "../math/vec"
32
- import { Animator, type FeetOptions, type WarpOptions } from "./animation/Animator"
32
+ import { Animator } from "./animation/Animator"
33
+ import type { FeetOptions } from "./animation/Feet"
34
+ import type { WarpOptions } from "./animation/Warp"
33
35
  import { CharacterController } from "./CharacterController"
34
36
  import type { Node } from "./Node"
35
37
 
@@ -288,8 +290,8 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
288
290
  for (const c of this.set.stops ?? []) this.enter(c, KIND.STOP)
289
291
  for (const c of this.set.turns ?? []) this.enter(c, KIND.TURN)
290
292
  for (const c of this.set.spins ?? []) this.enter(c, KIND.SPIN)
291
- if (this.warp !== undefined) anim.warp = this.warp
292
- anim.feet = this.feet === undefined ? { lock: true } : this.feet === true ? { lock: true, ik: true } : this.feet === false ? { lock: false, ik: false } : this.feet
293
+ if (this.warp !== undefined) anim.warp.set(this.warp)
294
+ anim.feet.set(this.feet === undefined ? { lock: true } : this.feet === true ? { lock: true, ik: true } : this.feet === false ? { lock: false, ik: false } : this.feet)
293
295
  this.pushTuning()
294
296
  }
295
297
 
@@ -303,7 +305,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
303
305
  if (slot < 0) { console.warn(`Locomotion: no clip '${n}' on ${this.node.name || "the character"}`); return }
304
306
  this._names.set(slot, n)
305
307
  const o = typeof clip === "string" ? undefined : clip
306
- const measured = anim.curves(n)
308
+ const measured = anim.clipInfo(n)
307
309
  const angle = o?.angle ?? (measured ? measured.turn * RAD2DEG : 0)
308
310
  const gait = kind === KIND.GAIT ? -1 : o?.gait ? GAITS.indexOf(o.gait) : -1
309
311
  _creator.locoSetEntry(this._loco, slot, kind, angle, o?.speed ?? 0, gait)
@@ -422,7 +424,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
422
424
  const named = t ? t[this._gait] : undefined
423
425
  if (named !== undefined) return named * this._mag
424
426
  const clip = (this.set.gaits ?? [])[GAITS.indexOf(this._gait)]
425
- const measured = clip !== undefined ? this.anim?.curves(name(clip))?.speed : undefined
427
+ const measured = clip !== undefined ? this.anim?.clipInfo(name(clip))?.speed : undefined
426
428
  return (measured ?? 0) * this._mag
427
429
  }
428
430
 
@@ -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
@@ -247,6 +368,21 @@ export class Material {
247
368
  static _lightmapTemplate(): Material { return (Material._lmTemplate ??= Material.lightmap()) }
248
369
  private static _lmTemplate: Material | undefined
249
370
 
371
+ static _lightmapShading: LightmapShading = "full"
372
+ /** Which shader lightmapped models take — a graphics-quality tier, engine-wide. `"full"` is filament's
373
+ * lit path over the baked atlas (IBL specular, real-time point lights such as a muzzle flash, sun
374
+ * shadows on dynamic objects). `"baked"` keeps the baked light and an approximated ambient but drops
375
+ * the lit path: ~40 % cheaper per pixel on a fill-bound GPU. `"baked-lite"` is that minus the normal,
376
+ * metallic/roughness and occlusion map reads (the factors stand in, the atlas keeps the baked AO;
377
+ * emissive still glows): +20 % more at 1080p on the same GPU. Read when a lightmapped model LOADS, so set
378
+ * it before the level (a settings menu applies it on the next level load), like `Texture.maxSize`. */
379
+ static get lightmapShading(): LightmapShading { return Material._lightmapShading }
380
+ static set lightmapShading(mode: LightmapShading) {
381
+ if (mode === Material._lightmapShading) return
382
+ Material._lightmapShading = mode
383
+ Material._lmTemplate = undefined // the next load builds a template on the new shader
384
+ }
385
+
250
386
  /** Shadow-catcher material (transparent except where shadows fall). */
251
387
  static shadow(color: ColorInput = "#000000aa"): Material {
252
388
  const m = new Material({ _id: _creatorUtils.fetchLocal("shadow.filamat") } as any)
@@ -277,3 +413,15 @@ export class Material {
277
413
  /** @internal */
278
414
  static idOf(material: Material): number { return material._id }
279
415
  }
416
+
417
+ const applyDepth = (m: Material, o: MaterialStateOptions): void => {
418
+ if (o.depthTest !== undefined) m.depthTest = o.depthTest
419
+ if (o.depthWrite !== undefined) m.depthWrite = o.depthWrite
420
+ if (o.doubleSided !== undefined) m.doubleSided = o.doubleSided
421
+ if (o.stencil !== undefined) m.stencil = o.stencil
422
+ }
423
+ /** the wire order of the stencil enums (creator-gl material.cpp switches on the same numbers) */
424
+ const STENCIL_TEST: StencilTest[] = [ "always", "never", "less", "lessEqual", "greater", "greaterEqual", "equal", "notEqual" ]
425
+ const STENCIL_OP: StencilOp[] = [ "keep", "zero", "replace", "increment", "decrement", "invert" ]
426
+ const warned = new Set<string>()
427
+ const warnOnce = (key: string, msg: string): void => { if (!warned.has(key)) { warned.add(key); console.warn(msg) } }
package/src/gl/Mesh.ts CHANGED
@@ -20,6 +20,8 @@ export type MeshOptions = {
20
20
  name?: string
21
21
  castShadows?: boolean
22
22
  receiveShadows?: boolean
23
+ /** Coarse draw order, 0 (first) … 7 (last); default 4. See `Mesh.renderPriority`. */
24
+ renderPriority?: number
23
25
  }
24
26
 
25
27
  export class Mesh extends Node {
@@ -31,17 +33,33 @@ export class Mesh extends Node {
31
33
  const mat = material ?? Material.unlit()
32
34
  this._geometry = geometry
33
35
  this._materials = [ mat ]
34
- _creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1)
36
+ _creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
35
37
  }
36
38
  }
37
39
 
38
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
+ }
39
48
 
40
49
  /** A Mesh always carries a material (slot 0) — see Node.setMaterial for the slot API. */
41
50
  override get material(): Material { return this._materials![0]! }
42
51
  override set material(m: Material) { this.setMaterial(m, 0) }
43
52
 
44
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
45
63
  set castShadows(v: boolean) { _creator.setCastShadows(this.id, v) }
46
64
  set receiveShadows(v: boolean) { _creator.setReceiveShadows(this.id, v) }
47
65
 
@@ -79,5 +97,6 @@ const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
79
97
  if (o.name !== undefined) mesh.name = o.name
80
98
  if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
81
99
  if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
100
+ if (o.renderPriority !== undefined) mesh.renderPriority = o.renderPriority
82
101
  return mesh
83
102
  }
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // All emitter/curve config crosses the bridge as ONE Float32Array of [tag, payloadLen, ...payload]
5
5
  // records (_creator.setParticleSystemConfig) — the constructor batches every option into a single
6
- // call, live setters send a one-record buffer. The tag values mirror PsTag in
7
- // creator-gl/src/particles.h (guarded by tests/particles-tags.test.ts).
6
+ // call, live setters send a one-record buffer. The tag values mirror CPART_TAG_* in
7
+ // creator-particles/include/creator-particles/creator-particles.h (guarded by tests/particles-tags.test.ts).
8
8
  //
9
9
  // Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
10
10
  // below. A builder's whole state is the flat `_data` array (the exact record payload), so the
@@ -18,7 +18,7 @@ import { Node } from "./Node"
18
18
 
19
19
  type Range<T> = T | { min: T, max: T }
20
20
 
21
- // --- config record tags (keep in sync with PsTag in creator-gl/src/particles.h) -----------------
21
+ // --- config record tags (keep in sync with CPART_TAG_* in creator-particles.h) ------------------
22
22
 
23
23
  const TAG_SHAPE = 1
24
24
  const TAG_LIFETIME = 2
package/src/gl/Scene.ts CHANGED
@@ -13,8 +13,10 @@ 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
17
  import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
17
18
  import { Material } from "./Material"
19
+ import { Texture } from "./Texture"
18
20
  import { Node, nodeRegistry } from "./Node"
19
21
  import { glState } from "./state"
20
22
  import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
@@ -107,6 +109,9 @@ export type SceneOptions = {
107
109
  * takes over). Unset keeps the host default (desktop 4×, mobile/web off). The biggest single
108
110
  * fill-rate cost after resolution — turn it down on big screens before anything else. */
109
111
  antialias?: boolean | 2 | 4
112
+ /** Keep a stencil buffer for this scene (off by default: it costs memory and a clear per frame).
113
+ * Needed before any material's `stencil` test or write does anything. */
114
+ stencil?: boolean
110
115
  /** Render the 3D at this fraction of the viewport (0.25–1) and upscale; the UI stays at native
111
116
  * resolution. A fixed, predictable cut of per-pixel GPU work — `0.75` is ~45 % cheaper and
112
117
  * barely visible in motion, `0.5` quarters it. Headless renders ignore it. */
@@ -128,6 +133,10 @@ export type SceneOptions = {
128
133
  * scene file — `env` is applied before the nodes build) and leaves already-loaded ones alone.
129
134
  * On the desktop host `CREATOR_TEXTURE_ANISOTROPY` overrides it, for tuning without a rebuild. */
130
135
  anisotropy?: number
136
+ /** Engine-wide cap on texture size, 0 / unset = none (see `Texture.maxSize`): a KTX2 above it
137
+ * loses its top mip levels on load, a glTF image is downsampled. Applied before this scene's
138
+ * assets load; like `anisotropy` it does not touch textures already loaded. */
139
+ maxTextureSize?: number
131
140
  /** The look on an HDR display (a screen with headroom above SDR white — Apple XDR panels, the
132
141
  * macOS host today); ignored on SDR. `strength` 0..1 is how much of the picture reaches for the
133
142
  * display's headroom (0 only what SDR clipped, 1 nearly everything; default 0.35). `paperWhite`
@@ -148,17 +157,16 @@ const installRenderSyncedFrames = (): void => {
148
157
  framesInstalled = true
149
158
  _installAspectFrames((early, late) => {
150
159
  if (typeof _creator.setEarlyUpdate !== "function") return false
151
- // the frame stamp invalidates every Node's local-matrix cache once per frame (physics / character
152
- // controllers / animation rewrite transforms natively between frames)
153
- _creator.setEarlyUpdate((dt: number) => { glState.frame++; early(dt) })
160
+ // (The per-phase read-cache stamp — `_phaseStamp` in core/Aspect.ts — advances inside the phase
161
+ // runners themselves, so it is right on this path and on the setLoop fallback alike.)
162
+ _creator.setEarlyUpdate(early)
154
163
  _creator.setLateUpdate(late)
155
164
  return true
156
165
  }, (fixed) => {
157
- // The fixed phase runs INSIDE the engine's substep loop (physics/world.cpp) when the host has the
158
- // hook; each substep moves bodies natively, so the frame stamp bumps per step too. Feature-detected:
159
- // an older host returns false and the dispatcher steps its own 1/60 accumulator.
166
+ // The fixed phase runs INSIDE the engine's substep loop (creator-physics' frame protocol) when the host has the
167
+ // hook. Feature-detected: an older host returns false and the dispatcher steps its own 1/60 accumulator.
160
168
  if (typeof _creator.setFixedUpdate !== "function") return false
161
- _creator.setFixedUpdate((dt: number) => { glState.frame++; fixed(dt) })
169
+ _creator.setFixedUpdate(fixed)
162
170
  return true
163
171
  })
164
172
  // Time.scale / Time.paused reach the engine's own clocks (physics, animators, particles) through
@@ -172,6 +180,8 @@ export class Scene implements Presentable {
172
180
  /** @internal the IBL intensity this scene was built with (Lightmap.load's ambient floor). */
173
181
  _environmentIntensity = 20000
174
182
  readonly camera: Camera
183
+ /** The listener + global 3D audio knobs (docs/audio-plan.md). */
184
+ readonly audio: SceneAudio = new SceneAudio()
175
185
 
176
186
  /** @internal touch listeners, read by the dispatch system. */
177
187
  readonly _clickListeners: Array<(ev: ClickEvent<Node | null>) => void> = []
@@ -220,9 +230,11 @@ export class Scene implements Presentable {
220
230
  // `false` must reach the host: desktop defaults to 4× MSAA, so an explicit off is a real change.
221
231
  _creator.setSceneMultiSampleAntiAliasing(this._id, a !== false, typeof a === "number" ? a : 4)
222
232
  }
233
+ if (options.stencil !== undefined) this.setStencil(options.stencil)
223
234
  // Before anything else in this constructor that could load a texture: the value has to be in
224
235
  // place by the time assets bind their samplers.
225
- if (options.anisotropy !== undefined) _creator.setTextureAnisotropy?.(options.anisotropy)
236
+ if (options.anisotropy !== undefined) Texture.anisotropy = options.anisotropy
237
+ if (options.maxTextureSize !== undefined) Texture.maxSize = options.maxTextureSize
226
238
  if (options.hdr) {
227
239
  if (options.hdr.strength !== undefined) device.hdr.strength = options.hdr.strength
228
240
  if (options.hdr.paperWhite !== undefined) device.hdr.paperWhite = options.hdr.paperWhite
@@ -280,6 +292,28 @@ export class Scene implements Presentable {
280
292
  setAntialias(enabled: boolean, scale = 4): void {
281
293
  _creator.setSceneMultiSampleAntiAliasing(this._id, enabled, scale)
282
294
  }
295
+ /** Depth-reading effects on / off (a graphics-settings menu): soft particles and projected decals
296
+ * read the scene depth, which costs a depth pre-pass of every opaque draw (~12 % of a fill-bound
297
+ * frame). Off = hard-edged particles, no decals, no pre-pass. Engine-wide, live. */
298
+ setDepthEffects(enabled: boolean): void {
299
+ _creator.setDepthEffects?.(enabled)
300
+ }
301
+ /** LOD distance (a graphics-settings menu): the engine's LOD thresholds × `bias`. 2 = every level
302
+ * switches at half the distance (a model must look twice as big on screen to keep its detail),
303
+ * 0.5 = full detail twice as far, 1 = the defaults. Engine-wide, live. Only GLBs that carry
304
+ * `_LOD<n>` meshes (`lecodes assets doctor --lod`) have levels to switch. */
305
+ setLodBias(bias: number): void {
306
+ _creator.setLodBias?.(bias)
307
+ }
308
+ /** Runtime form of `bloom` / `bloomIntensity` (a graphics-settings menu). */
309
+ setBloom(enabled: boolean, intensity = 0.2): void {
310
+ _creator.setBloomOptions(this._id, enabled, intensity, 1)
311
+ }
312
+ /** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
313
+ setStencil(enabled: boolean): void {
314
+ if (_creator.setSceneStencil) _creator.setSceneStencil(this._id, enabled)
315
+ else if (enabled) console.warn("[scene] this host has no setSceneStencil — stencil ignored")
316
+ }
283
317
 
284
318
  add(...nodes: Node[]): this {
285
319
  for (const n of nodes) _creator.addEntityToScene(this._id, n.id)
@@ -0,0 +1,26 @@
1
+ // `scene.audio` (docs/audio-plan.md §2): the listener and the global 3D knobs. The listener is the
2
+ // scene's active camera by default — a `CameraPlace` switch or a follow rig changes nothing, the
3
+ // engine reads whatever camera it renders with. Set a node to listen from somewhere else (the
4
+ // player's head in a third-person game, so sounds pan around the character, not the camera).
5
+
6
+ import { audioSupported } from "../audio/support"
7
+ import type { Node } from "./Node"
8
+
9
+ export class SceneAudio {
10
+ private _listener: Node | null = null
11
+ private _doppler = 1
12
+
13
+ /** The node the engine listens from; null (default) = the active camera. */
14
+ get listener(): Node | null { return this._listener }
15
+ set listener(node: Node | null) {
16
+ this._listener = node
17
+ if (audioSupported) _creatorAudio.setListener(node ? node.id : 0)
18
+ }
19
+
20
+ /** Multiplies every source's doppler amount (0 = off everywhere). Default 1. */
21
+ get dopplerFactor(): number { return this._doppler }
22
+ set dopplerFactor(v: number) {
23
+ this._doppler = v
24
+ if (audioSupported) _creatorAudio.setListenerOptions(v)
25
+ }
26
+ }
package/src/gl/Texture.ts CHANGED
@@ -2,7 +2,41 @@
2
2
 
3
3
  import { fetch, type FetchResponse, type File } from "../runtime/fetch"
4
4
 
5
+ /** `Texture.load` options. */
6
+ export type TextureLoadOptions = {
7
+ /** `true` (default): colour, stored sRGB. `false`: data (normal map, mask, heightmap) — kept linear. */
8
+ srgb?: boolean
9
+ /** Ignore `Texture.maxSize` for this texture (a lightmap page, a lookup table). */
10
+ fullSize?: boolean
11
+ }
12
+ /** `_creator.createTexture` flags bit: linear data (creator-gl CREATOR_TEXTURE_LINEAR). */
13
+ const TEXTURE_FLAG_LINEAR = 1
14
+ /** `_creator.createTexture` flags bit: exempt from `Texture.maxSize` (creator-gl CREATOR_TEXTURE_FULL_SIZE). */
15
+ const TEXTURE_FLAG_FULL_SIZE = 2
16
+
5
17
  export class Texture {
18
+ static _maxSize = 0
19
+ /** Engine-wide cap on texture size (a "texture quality" setting), 0 = none. A KTX2 wider or
20
+ * taller than this loses its top mip levels on load (nothing resampled, less memory and
21
+ * bandwidth), a glTF PNG/JPEG is downsampled. Reaches textures loaded AFTER it is set — a loaded
22
+ * level keeps its textures — so set it up front (`SceneOptions.maxTextureSize`, or before the
23
+ * level loads) and apply a menu change on the next level load. Lightmap pages are exempt
24
+ * (`TextureLoadOptions.fullSize`). A host may pin it (desktop `CREATOR_TEXTURE_MAX_SIZE`). */
25
+ static _anisotropy = 2
26
+ /** Engine-wide anisotropic filtering, 1 (off) … 16 (default 2; `SceneOptions.anisotropy` sets it up
27
+ * front). A sampler is baked when its texture is bound, so like `maxSize` this reaches textures
28
+ * loaded AFTER it — set it before the level loads. Measured on a lightmapped interior at 720p:
29
+ * 4× costs ~20 % of the frame over 1× on an integrated GPU. A host may pin it. */
30
+ static get anisotropy(): number { return Texture._anisotropy }
31
+ static set anisotropy(level: number) {
32
+ Texture._anisotropy = level >= 1 ? Math.min(16, level) : 1
33
+ _creator.setTextureAnisotropy?.(Texture._anisotropy)
34
+ }
35
+ static get maxSize(): number { return Texture._maxSize }
36
+ static set maxSize(size: number) {
37
+ Texture._maxSize = size > 0 ? Math.floor(size) : 0
38
+ _creator.setTextureMaxSize?.(Texture._maxSize)
39
+ }
6
40
  /** @internal native texture handle. */
7
41
  readonly _id: number
8
42
  readonly width: number
@@ -47,17 +81,23 @@ export class Texture {
47
81
  _creator.updateTexturePixels?.(this._id, x, y, width, height, data)
48
82
  }
49
83
 
50
- static load(source: string | FetchResponse | File): Promise<Texture> {
84
+ /** Decode an image (PNG / JPG, or a KTX2 the core transcodes) into a texture. `srgb` (default
85
+ * true) says the bytes are COLOUR, stored sRGB so the GPU linearises them on sample; pass
86
+ * `false` for DATA — a normal map, a mask, a heightmap — which must come back as stored (a
87
+ * flat normal read through sRGB bends by ~35°). A KTX2 decides by its own header. Image
88
+ * textures get a mip chain (trilinear) on hosts that build one. */
89
+ static load(source: string | FetchResponse | File, options: TextureLoadOptions = {}): Promise<Texture> {
51
90
  if (typeof source === "string") {
52
91
  return fetch(source, { useOnce: true }).then((resp) => {
53
92
  if (resp.status >= 400) {
54
93
  return Promise.reject(new Error(`Failed to fetch texture from ${source}. HTTP ${resp.status}`))
55
94
  }
56
- return Texture.load(resp)
95
+ return Texture.load(resp, options)
57
96
  })
58
97
  }
98
+ const flags = (options.srgb === false ? TEXTURE_FLAG_LINEAR : 0) | (options.fullSize ? TEXTURE_FLAG_FULL_SIZE : 0)
59
99
  return new Promise<Texture>((resolve, reject) => {
60
- _creator.createTexture((source as any)._id, (id, w, h) => resolve(new Texture(w, h, id)), reject)
100
+ _creator.createTexture((source as any)._id, (id, w, h) => resolve(new Texture(w, h, id)), reject, flags)
61
101
  })
62
102
  }
63
103
  }
package/src/gl/Vehicle.ts CHANGED
@@ -25,13 +25,12 @@
25
25
  //
26
26
  // Layering, and why it is where it is: docs/vehicle-rework-plan.md.
27
27
 
28
- import { Aspect } from "../core/Aspect"
28
+ import { Aspect, _phaseStamp } from "../core/Aspect"
29
29
  import type { FieldMeta } from "../core/fields"
30
30
  import { Mat4 } from "../math/mat4"
31
31
  import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
32
32
  import { cw, type QuatLike } from "../math/quat"
33
33
  import { Gizmos } from "../scene/gizmos"
34
- import { glState } from "./state"
35
34
  import { Gearbox } from "./Gearbox"
36
35
  import { Node } from "./Node"
37
36
  import { DEFAULT_FRICTION, Physics } from "./Physics"
@@ -314,12 +313,13 @@ export class Vehicle extends Aspect<"vehicle", Node> {
314
313
  if (this._id) _creator.vehicleSetTransmission(this._id, ratio, clamp(clutch, 0, 1))
315
314
  }
316
315
 
317
- // --- readouts (one native read per frame, shared by the car and every wheel) -------------------
316
+ // --- readouts (one native read per PHASE — early sees the pre-step state, late the settled one —
317
+ // shared by the car and every wheel; keyed on core/Aspect's _phaseStamp) -------------------
318
318
 
319
319
  private _read(): Float32Array {
320
- if (this._id && this._stateFrame !== glState.frame) {
320
+ if (this._id && this._stateFrame !== _phaseStamp.value) {
321
321
  _creator.vehicleGetState(this._id, this._state)
322
- this._stateFrame = glState.frame
322
+ this._stateFrame = _phaseStamp.value
323
323
  }
324
324
  return this._state
325
325
  }