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.
- package/dist/global.d.ts +31 -0
- package/dist/inject.js +361 -260
- package/dist/types/audio/Bus.d.ts +45 -0
- package/dist/types/audio/Sound.d.ts +28 -0
- package/dist/types/audio/Voice.d.ts +27 -0
- package/dist/types/audio/audio.d.ts +83 -0
- package/dist/types/audio/support.d.ts +1 -0
- package/dist/types/gl/AudioSource.d.ts +60 -0
- package/dist/types/gl/AudioZone.d.ts +32 -0
- package/dist/types/gl/DecalSet.d.ts +103 -0
- package/dist/types/gl/Geometry.d.ts +5 -0
- package/dist/types/gl/Light.d.ts +7 -0
- package/dist/types/gl/Material.d.ts +86 -2
- package/dist/types/gl/Mesh.d.ts +11 -0
- package/dist/types/gl/Scene.d.ts +23 -0
- package/dist/types/gl/SceneAudio.d.ts +11 -0
- package/dist/types/gl/Texture.d.ts +29 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
- package/dist/types/gl/animation/core.d.ts +15 -10
- package/dist/types/gl/state.d.ts +0 -1
- package/dist/types/inject.d.ts +10 -0
- package/dist/types/runtime/input.d.ts +11 -0
- package/dist/types/ui/UIImage.d.ts +15 -5
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/dist/2d-game.md +408 -197
- package/prompts/dist/3d-app.md +491 -166
- package/prompts/dist/ar-app.md +373 -163
- package/prompts/dist/design.md +83 -87
- package/prompts/dist/ui-app.md +325 -136
- package/src/audio/Bus.ts +102 -0
- package/src/audio/Sound.ts +96 -0
- package/src/audio/Voice.ts +102 -0
- package/src/audio/audio.ts +161 -0
- package/src/audio/support.ts +6 -0
- package/src/bridges.d.ts +1481 -1352
- package/src/compile/compileProject.ts +30 -15
- package/src/compile/index.ts +3 -0
- package/src/core/Aspect.ts +33 -8
- package/src/g2/Scene2D.ts +7 -0
- package/src/gl/AudioSource.ts +113 -0
- package/src/gl/AudioZone.ts +75 -0
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/DecalSet.ts +233 -0
- package/src/gl/Geometry.ts +5 -0
- package/src/gl/Light.ts +16 -0
- package/src/gl/Lightmap.ts +3 -2
- package/src/gl/Material.ts +152 -4
- package/src/gl/Mesh.ts +20 -1
- package/src/gl/Ragdoll.ts +270 -270
- package/src/gl/Scene.ts +41 -7
- package/src/gl/SceneAudio.ts +26 -0
- package/src/gl/Texture.ts +43 -3
- package/src/gl/Trigger.ts +45 -45
- package/src/gl/Vehicle.ts +5 -5
- package/src/gl/animation/AnimationClip.ts +43 -20
- package/src/gl/animation/Animator.ts +4 -3
- package/src/gl/animation/core.ts +20 -15
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +1 -1
- package/src/inject.ts +12 -0
- package/src/runtime/input.ts +6 -1
- package/src/scene/gizmos.ts +148 -148
- 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
|
+
}
|
package/src/gl/Geometry.ts
CHANGED
|
@@ -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
|
}
|
package/src/gl/Lightmap.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/gl/Material.ts
CHANGED
|
@@ -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
|
|
222
|
-
const
|
|
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
|
}
|