lecodes-sdk 0.20.0 → 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 (64) hide show
  1. package/dist/global.d.ts +31 -0
  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/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/Material.d.ts +86 -2
  14. package/dist/types/gl/Mesh.d.ts +11 -0
  15. package/dist/types/gl/Scene.d.ts +23 -0
  16. package/dist/types/gl/SceneAudio.d.ts +11 -0
  17. package/dist/types/gl/Texture.d.ts +29 -1
  18. package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
  19. package/dist/types/gl/animation/core.d.ts +15 -10
  20. package/dist/types/gl/state.d.ts +0 -1
  21. package/dist/types/inject.d.ts +10 -0
  22. package/dist/types/runtime/input.d.ts +11 -0
  23. package/dist/types/ui/UIImage.d.ts +15 -5
  24. package/dist/types.json +1 -1
  25. package/package.json +1 -1
  26. package/prompts/dist/2d-game.md +408 -197
  27. package/prompts/dist/3d-app.md +491 -166
  28. package/prompts/dist/ar-app.md +373 -163
  29. package/prompts/dist/design.md +83 -87
  30. package/prompts/dist/ui-app.md +325 -136
  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 -1352
  37. package/src/compile/compileProject.ts +30 -15
  38. package/src/compile/index.ts +3 -0
  39. package/src/core/Aspect.ts +33 -8
  40. package/src/g2/Scene2D.ts +7 -0
  41. package/src/gl/AudioSource.ts +113 -0
  42. package/src/gl/AudioZone.ts +75 -0
  43. package/src/gl/CameraPlace.ts +52 -52
  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/Material.ts +152 -4
  49. package/src/gl/Mesh.ts +20 -1
  50. package/src/gl/Ragdoll.ts +270 -270
  51. package/src/gl/Scene.ts +41 -7
  52. package/src/gl/SceneAudio.ts +26 -0
  53. package/src/gl/Texture.ts +43 -3
  54. package/src/gl/Trigger.ts +45 -45
  55. package/src/gl/Vehicle.ts +5 -5
  56. package/src/gl/animation/AnimationClip.ts +43 -20
  57. package/src/gl/animation/Animator.ts +4 -3
  58. package/src/gl/animation/core.ts +20 -15
  59. package/src/gl/scenarios.ts +291 -291
  60. package/src/gl/state.ts +1 -1
  61. package/src/inject.ts +12 -0
  62. package/src/runtime/input.ts +6 -1
  63. package/src/scene/gizmos.ts +148 -148
  64. package/src/ui/UIImage.ts +21 -7
@@ -0,0 +1,233 @@
1
+ // Projected decals — bullet holes, footprints, dirt, blood. A DecalSet is one node that owns up to
2
+ // `max` decals drawn as ONE native renderable: each decal is a box that projects its image onto the
3
+ // opaque scene inside it (the engine reads the scene depth — creator-gl src/decals.cpp +
4
+ // materials/src/decal.mat), so it wraps floor-wall corners, stairs and curved surfaces without any
5
+ // geometry access. It lands on opaque surfaces only: never on particles, glass or other decals.
6
+ //
7
+ // Coordinates are in the SET's space: a set added at the scene root with no transform takes world
8
+ // coordinates (the usual case); a set parented to a door rides with it and takes door-local ones.
9
+ // Slots are a ring — a full set recycles its oldest decal, so `max` is the on-screen budget.
10
+
11
+ import { Node } from "./Node"
12
+ import { Material } from "./Material"
13
+ import type { Texture } from "./Texture"
14
+ import { Vec3, type Vec3Like } from "../math/vec"
15
+ import { Quat, type QuatLike } from "../math/quat"
16
+ import { Color, type ColorInput } from "../core/color"
17
+
18
+ /** Per-decal look and life — defaults come from the set's options. */
19
+ export type DecalOptions = {
20
+ /** Image width and height on the surface (world units); a number = square. Default 0.2. */
21
+ size?: number | [number, number]
22
+ /** Projection depth (world units): how far in front of and behind the hit point the decal still
23
+ * lands. Default = the smaller of width / height. */
24
+ depth?: number
25
+ /** Atlas cell (row-major from the top-left) for a set with a `sheet`; `"random"` picks one.
26
+ * Default 0. */
27
+ frame?: number | "random"
28
+ /** Tint multiplied into the image. Default white. */
29
+ tint?: ColorInput
30
+ /** 0..1 on top of the tint's alpha. Default 1. */
31
+ opacity?: number
32
+ /** Seconds until the decal is gone; 0 = stays until recycled. Default 0. */
33
+ life?: number
34
+ /** Seconds to fade in after spawning. Default 0. */
35
+ fadeIn?: number
36
+ /** Seconds of fade at the end of `life` (ignored with life 0). Default 0. */
37
+ fadeOut?: number
38
+ }
39
+
40
+ export type DecalSetOptions = DecalOptions & {
41
+ /** The material — `Material.decal({ map })` by default (`map` below is its shortcut). A custom
42
+ * material must keep decal.mat's vertex contract. */
43
+ material?: Material
44
+ /** Atlas texture for the default material. */
45
+ map?: Texture
46
+ /** Tangent-space normal atlas (same cell grid) → a RELIEF decal that bends the surface's
47
+ * lighting instead of painting a colour (`Material.decal` `normalMap`). Footprints and dents
48
+ * need only this; a bullet hole gives `map` too and its colour multiplies in. */
49
+ normalMap?: Texture
50
+ /** Relief strength for `normalMap`. Default 1. Default material only. */
51
+ bump?: number
52
+ /** Atlas grid of the map: columns, or [columns, rows]. Default 1 (the whole texture). */
53
+ sheet?: number | [number, number]
54
+ /** Slot budget — the most decals alive at once. Default 256. */
55
+ max?: number
56
+ /** Soft fraction (0..1) of the box's half depth at both ends, so an oblique surface leaves the
57
+ * box gently. Default 0.3. Default material only. */
58
+ edge?: number
59
+ /** Surfaces turned more than this away from the projection axis fade out — the cosine of the
60
+ * angle (0.3 ≈ 72°) keeps a floor hit off the wall it meets; 0 = project onto anything.
61
+ * Default 0.3. Default material only. */
62
+ angleFade?: number
63
+ /** HDR boost of the image (0 = none). Default material only. */
64
+ emissive?: number
65
+ name?: string
66
+ /** Coarse draw order, 0 (first) … 7 (last); default 4 — see `Mesh.renderPriority`. Decals are
67
+ * blended and sort with the other blended draws of their priority. */
68
+ renderPriority?: number
69
+ }
70
+
71
+ /** `spawn` placement: where the image sits on the surface. */
72
+ export type DecalSpawnOptions = DecalOptions & {
73
+ /** World direction the image's top points along the surface (projected onto it) — a footprint's
74
+ * travel direction. Default: a random spin. */
75
+ up?: Vec3Like
76
+ /** Extra spin around the normal, radians. Default: random when `up` is not given, else 0. */
77
+ rotation?: number
78
+ }
79
+
80
+ /** `place` / `update` placement: a full frame, like a node looking INTO the surface (its −Z is the
81
+ * projection direction, +Y the image's top). */
82
+ export type DecalPlacement = DecalOptions & {
83
+ position: Vec3Like
84
+ /** A quaternion, or Euler degrees (YXZ) like `Node.eulerAngles`. Default identity = projecting
85
+ * down −Z. */
86
+ rotation?: QuatLike | Vec3Like
87
+ }
88
+
89
+ const RECORD = 23
90
+ const NO_SLOT = 0xFFFFFFFF
91
+
92
+ export class DecalSet extends Node {
93
+ private _material: Material
94
+ private readonly _max: number
95
+ private readonly _cols: number
96
+ private readonly _rows: number
97
+ private readonly _defaults: DecalOptions
98
+ private readonly _rec = new Float32Array(RECORD)
99
+ private readonly _slots = new Map<number, DecalPlacement>()
100
+ private static _warned = false
101
+
102
+ constructor(options: DecalSetOptions = {}) {
103
+ super()
104
+ if (options.name) this.name = options.name
105
+ this._material = options.material ?? Material.decal({
106
+ map: options.map, normalMap: options.normalMap, bump: options.bump,
107
+ edge: options.edge, angleFade: options.angleFade, emissive: options.emissive,
108
+ })
109
+ this._max = Math.max(1, Math.round(options.max ?? 256))
110
+ const sheet = options.sheet ?? 1
111
+ this._cols = Math.max(1, Array.isArray(sheet) ? sheet[0] : sheet)
112
+ this._rows = Math.max(1, Array.isArray(sheet) ? sheet[1] : 1)
113
+ this._defaults = {
114
+ size: options.size ?? 0.2, depth: options.depth, frame: options.frame ?? 0, tint: options.tint,
115
+ opacity: options.opacity ?? 1, life: options.life ?? 0, fadeIn: options.fadeIn ?? 0, fadeOut: options.fadeOut ?? 0,
116
+ }
117
+ if (_creator.createDecalSet) {
118
+ _creator.createDecalSet(this.id, Material.idOf(this._material), this._max)
119
+ if (options.renderPriority !== undefined) this.renderPriority = options.renderPriority
120
+ } else if (!DecalSet._warned) {
121
+ DecalSet._warned = true
122
+ console.warn("[decals] this host has no createDecalSet — decals are not drawn")
123
+ }
124
+ }
125
+
126
+ get material(): Material { return this._material }
127
+ /** Decals alive right now. */
128
+ get count(): number { return _creator.decalCount ? _creator.decalCount(this.id) : 0 }
129
+ /** The slot budget the set was created with. */
130
+ get max(): number { return this._max }
131
+
132
+ /** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
133
+ set renderPriority(v: number) {
134
+ if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
135
+ }
136
+
137
+ /** Stamp a decal on a surface: `point` on it, `normal` out of it (a raycast hit, a foot plant
138
+ * with `Vec3.up`). Returns the slot for `update` / `remove` (−1 when the host draws none). */
139
+ spawn(point: Vec3Like, normal: Vec3Like, options: DecalSpawnOptions = {}): number {
140
+ const n = new Vec3(normal).normalize()
141
+ // Image up on the surface: the hint projected onto the plane, or any perpendicular.
142
+ let up = options.up ? new Vec3(options.up) : null
143
+ if (up) { up = up.sub(n.scale(up.dot(n))); if (up.length() < 1e-4) up = null }
144
+ if (!up) {
145
+ const helper = Math.abs(n.y) < 0.99 ? Vec3.up : Vec3.right
146
+ up = helper.sub(n.scale(helper.dot(n)))
147
+ }
148
+ up = up.normalize()
149
+ // right = up × normal keeps the frame right-handed (right × up = normal), so the image reads
150
+ // unmirrored from the normal's side; then the spin around the normal.
151
+ let right = up.cross(n)
152
+ const spin = options.rotation ?? (options.up ? 0 : Math.random() * Math.PI * 2)
153
+ if (spin !== 0) {
154
+ const c = Math.cos(spin), s = Math.sin(spin)
155
+ const r2 = right.scale(c).add(up.scale(s))
156
+ up = up.scale(c).sub(right.scale(s))
157
+ right = r2
158
+ }
159
+ return this._write(NO_SLOT, right, up, n, new Vec3(point), options)
160
+ }
161
+
162
+ /** Place a decal by a full frame (an editor-placed stain, a moving marker). Returns the slot. */
163
+ place(placement: DecalPlacement): number {
164
+ const slot = this._writePlacement(NO_SLOT, placement)
165
+ if (slot >= 0) this._slots.set(slot, placement)
166
+ return slot
167
+ }
168
+
169
+ /** Move / restyle a placed decal; fields left out keep the values it was placed with. Its birth
170
+ * time (the life clock) is kept. */
171
+ update(slot: number, placement: Partial<DecalPlacement>): this {
172
+ const prev = this._slots.get(slot)
173
+ if (!prev) return this
174
+ const next = { ...prev, ...placement }
175
+ this._slots.set(slot, next)
176
+ this._writePlacement(slot, next)
177
+ return this
178
+ }
179
+
180
+ remove(slot: number): this {
181
+ this._slots.delete(slot)
182
+ if (_creator.removeDecal && slot >= 0) _creator.removeDecal(this.id, slot)
183
+ return this
184
+ }
185
+
186
+ clear(): this {
187
+ this._slots.clear()
188
+ if (_creator.clearDecals) _creator.clearDecals(this.id)
189
+ return this
190
+ }
191
+
192
+ private _writePlacement(slot: number, p: DecalPlacement): number {
193
+ let q: Quat
194
+ if (p.rotation === undefined) q = Quat.identity
195
+ else if (p.rotation instanceof Quat || (p.rotation as any).w !== undefined || (p.rotation as any).length === 4) q = new Quat(p.rotation as QuatLike)
196
+ else { const e = new Vec3(p.rotation as Vec3Like); q = Quat.fromEuler(e.x, e.y, e.z) }
197
+ const right = q.rotateVec3(Vec3.right)
198
+ const up = q.rotateVec3(Vec3.up)
199
+ const normal = right.cross(up) // +Z of the frame: the node's −Z looks into the surface
200
+ return this._write(slot, right, up, normal, new Vec3(p.position), p)
201
+ }
202
+
203
+ private _write(slot: number, right: Vec3, up: Vec3, normal: Vec3, centre: Vec3, o: DecalOptions): number {
204
+ const d = this._defaults
205
+ const size = o.size ?? d.size!
206
+ const w = typeof size === "number" ? size : size[0]
207
+ const h = typeof size === "number" ? size : size[1]
208
+ const depth = o.depth ?? d.depth ?? Math.min(w, h)
209
+ const r = this._rec
210
+ r[0] = right.x * w; r[1] = right.y * w; r[2] = right.z * w
211
+ r[3] = up.x * h; r[4] = up.y * h; r[5] = up.z * h
212
+ r[6] = normal.x * depth; r[7] = normal.y * depth; r[8] = normal.z * depth
213
+ r[9] = centre.x; r[10] = centre.y; r[11] = centre.z
214
+ // atlas cell → rect (v grows downward: v0 is the cell's top)
215
+ const cells = this._cols * this._rows
216
+ const frameOpt = o.frame ?? d.frame!
217
+ const frame = frameOpt === "random" ? Math.floor(Math.random() * cells) : ((Math.floor(frameOpt) % cells) + cells) % cells
218
+ const col = frame % this._cols, row = Math.floor(frame / this._cols)
219
+ r[12] = col / this._cols; r[13] = row / this._rows; r[14] = (col + 1) / this._cols; r[15] = (row + 1) / this._rows
220
+ const tint = o.tint ?? d.tint
221
+ const [tr, tg, tb, ta] = tint === undefined ? [1, 1, 1, 1] : Color.toRgba01(tint)
222
+ const opacity = o.opacity ?? d.opacity!
223
+ r[16] = tr; r[17] = tg; r[18] = tb; r[19] = ta * opacity
224
+ r[20] = o.life ?? d.life!; r[21] = o.fadeIn ?? d.fadeIn!; r[22] = o.fadeOut ?? d.fadeOut!
225
+ if (slot === NO_SLOT) {
226
+ if (!_creator.addDecal) return -1
227
+ const s = _creator.addDecal(this.id, r)
228
+ return s === NO_SLOT ? -1 : s
229
+ }
230
+ if (_creator.updateDecal) _creator.updateDecal(this.id, slot, r)
231
+ return slot
232
+ }
233
+ }
@@ -25,6 +25,11 @@ export class Geometry {
25
25
  * tiles per face, which would fold every face onto the same texels); absent = the host reuses
26
26
  * `uv`, which is right for a plane. */
27
27
  uv1?: Float32Array
28
+ /** Per-vertex COLOURS: 4 bytes (r, g, b, a) per vertex, 0..255. A material that declares
29
+ * `requires: [color]` reads them through `getColor()` — one mesh, many colours, no material per
30
+ * shade (a debug drawer, a gradient along a curve). Absent = every vertex white, which is what
31
+ * the built-in materials expect. */
32
+ colors?: Uint8Array
28
33
  /** @internal native mesh-type enum (0 triangles / 1 edges / 2 vertices). */
29
34
  _meshKind = 0
30
35
 
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
  }
@@ -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
  }