lecodes-sdk 2.4.7 → 2.4.9
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 +11 -0
- package/dist/host.d.ts +13 -2
- package/dist/types/gl/Camera.d.ts +100 -13
- package/dist/types/gl/Light.d.ts +7 -2
- package/dist/types/gl/Lightmap.d.ts +45 -25
- package/dist/types/gl/Material.d.ts +16 -2
- package/dist/types/gl/Node.d.ts +13 -0
- package/dist/types/gl/Particles.d.ts +1 -1
- package/dist/types/gl/ReflectionProbe.d.ts +87 -0
- package/dist/types/gl/RenderTexture.d.ts +29 -0
- package/dist/types/gl/Scene.d.ts +21 -0
- package/dist/types/inject.d.ts +5 -0
- package/dist/types/scene/defineScene.d.ts +15 -54
- package/dist/types/ui/tree.d.ts +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/gl.d.ts +34 -0
- package/src/compile/assetMacro.ts +2 -2
- package/src/compile/bakeMacro.ts +243 -0
- package/src/compile/bundler.ts +38 -6
- package/src/compile/compileProject.ts +13 -7
- package/src/compile/index.ts +2 -1
- package/src/compile/resourceSlots.ts +4 -3
- package/src/gl/Camera.ts +198 -25
- package/src/gl/Light.ts +280 -274
- package/src/gl/Lightmap.ts +114 -81
- package/src/gl/Material.ts +36 -2
- package/src/gl/Node.ts +61 -0
- package/src/gl/Particles.ts +1 -1
- package/src/gl/ReflectionProbe.ts +141 -0
- package/src/gl/RenderTexture.ts +93 -0
- package/src/gl/Scene.ts +54 -2
- package/src/host.d.ts +13 -2
- package/src/inject.ts +5 -0
- package/src/scene/defineScene.ts +26 -59
- package/src/ui/tree.ts +1 -0
- package/src/version.ts +1 -1
package/src/gl/Lightmap.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Baked lighting for static 3D geometry (engines/bake). A scene FILE does all of this by
|
|
2
|
-
// itself: `env: { lightmap:
|
|
2
|
+
// itself: `env: { lightmap: bake('./assets/lightmap/level.bake') }` marks the level as baked, every model/mesh node
|
|
3
3
|
// that no Physics aspect moves is a static (keys = node paths, `lightmap: true|false` overrides),
|
|
4
4
|
// and the runtime applies the bake when the level finishes loading (scene/defineScene.ts).
|
|
5
5
|
// Hand-built scenes register what never moves with `Lightmap.add`, bake once with
|
|
@@ -10,20 +10,22 @@
|
|
|
10
10
|
// const crate = await Model.load(asset('./assets/props/crate.glb'), { lightmap: true })
|
|
11
11
|
// Lightmap.add(holder) // a Model, a Mesh, or a node holding them
|
|
12
12
|
// ...
|
|
13
|
-
// await Lightmap.load(scene,
|
|
14
|
-
// light: asset('./assets/lightmap/level-light.ktx2'),
|
|
15
|
-
// aux: asset('./assets/lightmap/level-aux.ktx2'),
|
|
16
|
-
// probes: asset('./assets/lightmap/level-probes.ktx2') })
|
|
13
|
+
// await Lightmap.load(scene, bake('./assets/lightmap/level.bake', { emissiveBake: false }))
|
|
17
14
|
//
|
|
18
|
-
// `
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// `bake()` is a COMPILE-TIME macro (compile/bakeMacro.ts), the one reference to a bake: the `.bake` is a JSON
|
|
16
|
+
// MANIFEST that names every file the bake wrote next to it (the atlas pages, the aux pages, the reflection probes, the
|
|
17
|
+
// light grid, the packed uv1, a `--split` bake's page sets). The compiler reads it, stages each file the way asset()
|
|
18
|
+
// does and writes the list in place (`LightmapDef`), the options (its second argument) beside it. Before the first bake
|
|
19
|
+
// there is no manifest: the def is `{ data: null, path }` — the level runs unbaked and says so, and under `lecodes
|
|
20
|
+
// lightmap bake` that path is where the bake writes. (The data file is JSON with a .bake extension: a .json would be
|
|
21
|
+
// scanned as a code module.)
|
|
22
22
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// the
|
|
26
|
-
//
|
|
23
|
+
// `probes` (when the bake placed some) is the bake's REFLECTION PROBES — the level's radiance traced by the same bake from
|
|
24
|
+
// a grid of points, prefiltered per roughness, one octahedral tile column per probe in one atlas; every pixel of a static
|
|
25
|
+
// blends the probes around it (the .bake carries the grid). Without them a static's specular reflects the sky's SH alone.
|
|
26
|
+
//
|
|
27
|
+
// `load` is a no-op when the bake is missing or the host predates the feature, so the project stays runnable — and in
|
|
28
|
+
// BAKE mode (`lecodes lightmap bake`) it is where the bake fires: the same call marks "the scene is complete".
|
|
27
29
|
//
|
|
28
30
|
// What a static gets: its whole DIFFUSE light from the atlas — the sun, the sky and the lamps that the
|
|
29
31
|
// bake traced onto it, in lux — and nothing from the real-time sun or lamps (the engine keeps those
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
// albedo, normals, visibilities, the atlas checker, the light direction, false-colour lux) and
|
|
36
38
|
// `Lightmap.probe(x, y)` names the texel under a screen point. View 1 ("irradiance") is the lighting alone:
|
|
37
39
|
// a white diffuse material under the camera's exposure. Views "direct" / "indirect" show the bake's two
|
|
38
|
-
// halves: a bake made with `--split` writes them as page sets of their own,
|
|
40
|
+
// halves: a bake made with `--split` writes them as page sets of their own, its manifest names them as
|
|
39
41
|
// `direct` / `indirect` next to `light`, and the view binds those pages in place of `light` (the shading
|
|
40
42
|
// is untouched — it is the same material reading other pages). The bake's `--debug-dir` keeps the
|
|
41
43
|
// float layers `lecodes lightmap inspect` reads.
|
|
@@ -50,16 +52,22 @@ import { Node, nodes } from "./Node"
|
|
|
50
52
|
import { Light } from "./Light"
|
|
51
53
|
import type { Scene } from "./Scene"
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
/** The level's bake / load options: `bake()`'s second argument (an object literal, copied into the def as written). */
|
|
56
|
+
export type LightmapOptions = {
|
|
54
57
|
/** A DEBUG multiplier on the atlas' lux (default 1): the atlas is physical, so this is a knob for looking,
|
|
55
58
|
* not a look. */
|
|
56
59
|
lightScale?: number
|
|
57
|
-
/**
|
|
58
|
-
* emissive
|
|
59
|
-
*
|
|
60
|
+
/** FOR THE BAKE: the radiance in cd / m² of emission 1.0 on this level (emissive factor x strength x map) - every lit
|
|
61
|
+
* region of an emissive surface bakes as a rectangle lamp. An imported pack's emission is a LOOK, tuned for the eye;
|
|
62
|
+
* this is where the level says what its panels really give: a 1.3 m ceiling panel of ~8000 lm is ~1600 nits. Default:
|
|
63
|
+
* what the camera makes of 1.0 (`1.2 x 2^EV100`). 0 = emissive surfaces light nothing. The CLI's `--emissive-nits`
|
|
64
|
+
* wins over it. A `Material.lit` glow with `emissiveNits` of its own emits those instead. */
|
|
60
65
|
emissiveNits?: number
|
|
61
|
-
/** `false` = the emissive surfaces light NOTHING in the bake
|
|
62
|
-
*
|
|
66
|
+
/** `false` = the emissive surfaces light NOTHING in the bake - the level is lit by its lamps (a `kind: 'point'` light
|
|
67
|
+
* with a `bakeArea` next to every panel, the Unity packs' way) and the panels are decoration; `emissiveNits` then only
|
|
68
|
+
* says how bright their glow is DRAWN (scene-referred: nits through the camera's exposure). Default true. The
|
|
69
|
+
* OVERRIDE over every surface's own switch (`Material.lit`'s `emissiveBake`, a GLB material's extras
|
|
70
|
+
* `{"lecodes":{"emissiveBake":false}}`), which vetoes one material while the rest still light the level. */
|
|
63
71
|
emissiveBake?: boolean
|
|
64
72
|
/** FOR THE BAKE: materials that let light THROUGH them, by their glTF name (`"name*"` = every name with that start):
|
|
65
73
|
* the share of a shadow ray that passes, tinted by the material's base colour x map - an awning's warm, patterned light
|
|
@@ -75,7 +83,7 @@ export type LightmapLoadOptions = {
|
|
|
75
83
|
cube?: string[]
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
/** one `transmit` entry (
|
|
86
|
+
/** one `transmit` entry (LightmapOptions.transmit): the straight share, or both shares */
|
|
79
87
|
export type LightmapTransmit = number | { through?: number, diffuse?: number }
|
|
80
88
|
|
|
81
89
|
export type LightmapInfo = {
|
|
@@ -89,27 +97,36 @@ export type LightmapInfo = {
|
|
|
89
97
|
missing: string[]
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
/** The bake's files as `bake()` reads them off the `.bake` manifest at compile time — each one staged like an asset()
|
|
101
|
+
* (every name in the manifest is relative to the `.bake`'s folder). Written by the macro, never by hand. */
|
|
92
102
|
export type LightmapFiles = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
/** The `.bake` (the manifest) — null until `lecodes lightmap bake` has written it: the level then runs unbaked. */
|
|
104
|
+
data: string | null
|
|
105
|
+
/** Project-relative path of the `.bake`, always set: what the messages name, and where `lecodes lightmap bake` writes. */
|
|
106
|
+
path: string
|
|
107
|
+
/** The irradiance atlas (`<stem>-light[_n].ktx2`), every page in order. */
|
|
108
|
+
light: string[]
|
|
109
|
+
/** The aux atlas (`<stem>-aux[_n].ktx2`: sun / sky visibility + light direction), pages like `light`. */
|
|
110
|
+
aux: string[]
|
|
98
111
|
/** DEBUG: the direct-light-only pages a `--split` bake wrote (`<stem>-direct[_n].ktx2`), for the "direct" view. */
|
|
99
|
-
direct?: string
|
|
112
|
+
direct?: string[]
|
|
100
113
|
/** DEBUG: the bounce-only pages of a `--split` bake (`<stem>-indirect[_n].ktx2`), for the "indirect" view. */
|
|
101
|
-
indirect?: string
|
|
114
|
+
indirect?: string[]
|
|
102
115
|
/** The reflection probes (`<stem>-probes.ktx2`), when the bake placed some (`lecodes lightmap bake --probe-spacing`). */
|
|
103
116
|
probes?: string
|
|
104
|
-
/** THE LIGHT GRID for movers (`<stem>.lgrid`, `lecodes lightmap bake --volume`):
|
|
105
|
-
*
|
|
117
|
+
/** THE LIGHT GRID for movers (`<stem>.lgrid`, `lecodes lightmap bake --volume`): every node a Physics aspect or a
|
|
118
|
+
* CharacterController moves (and what code marks with `Lightmap.track`) takes its ambient light from the bake at the
|
|
119
|
+
* place it is at, whatever its shader. Absent = movers keep the scene's IBL. */
|
|
106
120
|
volume?: string
|
|
107
|
-
/** THE PACKED LIGHTMAP UV1 (`<stem>-uv1.bin
|
|
108
|
-
*
|
|
109
|
-
* when the bake names one. */
|
|
121
|
+
/** THE PACKED LIGHTMAP UV1 (`<stem>-uv1.bin`): the bake packs every mesh's lightmap charts into a square of their own
|
|
122
|
+
* and its rects map that uv1 — the engine swaps it into the models' TEXCOORD_1 before the rects go on. */
|
|
110
123
|
uv1?: string
|
|
111
124
|
}
|
|
112
125
|
|
|
126
|
+
/** What `bake('<path>.bake', options)` compiles to — the files of the bake and the level's options in one literal:
|
|
127
|
+
* `env.lightmap` in a scene file, `Lightmap.load`'s argument in a hand-built scene. */
|
|
128
|
+
export type LightmapDef = LightmapFiles & LightmapOptions
|
|
129
|
+
|
|
113
130
|
/** The lightmap material's data views (`Lightmap.debug`); "direct" / "indirect" bind a `--split` bake's pages instead. */
|
|
114
131
|
export type LightmapDebugMode = "off" | "irradiance" | "albedo" | "normal" | "shadingNormal" | "share" | "skyVis" | "atlas" | "direction" | "lux" | "relief" | "direct" | "indirect" | "probe" | "probeMap" | "skyPath" | "probePath"
|
|
115
132
|
const DENOISE_LEVELS = { none: 0, indirect: 1, "no-sun": 2, all: 3 } as const
|
|
@@ -151,7 +168,13 @@ export type LightmapAddOptions = {
|
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
type BakeConfig = {
|
|
154
|
-
|
|
171
|
+
/** The project root (forward slashes): the bake writes `<root>/<the def's path>` — where the level's `bake()` points. */
|
|
172
|
+
root: string,
|
|
173
|
+
/** `--out`: write the files into this folder instead (absolute) — a second seed for `lecodes lightmap diff`. */
|
|
174
|
+
outDir?: string,
|
|
175
|
+
/** `--name`: only the level whose `.bake` has this stem bakes (a bundle that reaches several bake() calls). */
|
|
176
|
+
stem?: string,
|
|
177
|
+
size?: number, texel?: number, pages?: number,
|
|
155
178
|
sunRays?: number, skyRays?: number, lightRays?: number, lightRadius?: number, sunAngle?: number, bias?: number, seed?: number,
|
|
156
179
|
bounceRays?: number, bounces?: number, patchSamples?: number, split?: boolean,
|
|
157
180
|
/** The AI denoiser (see `denoiser`) over: `"no-sun"` (the default) the bounce, the sky and the lamps, `"indirect"` the bounce alone, `"all"` + the sun (its shadow edges too), `"none"`. */
|
|
@@ -193,7 +216,7 @@ type BakeConfig = {
|
|
|
193
216
|
* 0 = one cube an object. Default 1. */
|
|
194
217
|
objectSlab?: number,
|
|
195
218
|
/** CUBES BY NAME (the CLI's `--cube`): regular expressions over the records' keys, joined with the level's own
|
|
196
|
-
* (`
|
|
219
|
+
* (`LightmapOptions.cube`) into one — each wrapped in `(?: )`, `|` between. */
|
|
197
220
|
cube?: string[],
|
|
198
221
|
/** THE LIGHT GRID for movers: the cell size in metres (0.75 of it tall); 0 = none. Default 1. */
|
|
199
222
|
volumeSpacing?: number,
|
|
@@ -251,11 +274,9 @@ type Bound = {
|
|
|
251
274
|
|
|
252
275
|
const bakeConfig = (): BakeConfig | null => {
|
|
253
276
|
const g = (globalThis as unknown as { __lecodesLightmap?: BakeConfig }).__lecodesLightmap
|
|
254
|
-
return g && typeof g.
|
|
277
|
+
return g && typeof g.root === "string" ? g : null
|
|
255
278
|
}
|
|
256
279
|
|
|
257
|
-
const list = (v: string | string[] | undefined): string[] => v === undefined ? [] : Array.isArray(v) ? v : [ v ]
|
|
258
|
-
|
|
259
280
|
const NONE = 0xFFFFFFFF
|
|
260
281
|
|
|
261
282
|
export class Lightmap {
|
|
@@ -311,7 +332,7 @@ export class Lightmap {
|
|
|
311
332
|
private static movers = new Set<Model | Mesh>()
|
|
312
333
|
private static volumeOn = false
|
|
313
334
|
|
|
314
|
-
/** A MOVER takes its ambient light from the level's baked LIGHT GRID (`
|
|
335
|
+
/** A MOVER takes its ambient light from the level's baked LIGHT GRID (the bake's `.lgrid`) at the place it is at,
|
|
315
336
|
* every frame — whatever its shader: the standard glTF one or a custom lit material. Every Model / Mesh under `node`
|
|
316
337
|
* is marked (a character, a weapon with its parts). A scene file does this itself for every node a Physics aspect or
|
|
317
338
|
* a CharacterController moves; call it for what CODE spawns — an enemy, a pickup, a projectile. `on = false` hands
|
|
@@ -333,10 +354,7 @@ export class Lightmap {
|
|
|
333
354
|
|
|
334
355
|
private static async loadVolume(data: BakeData, files: LightmapFiles): Promise<void> {
|
|
335
356
|
Lightmap.volumeOn = false
|
|
336
|
-
if (!files.volume || !data.volume || !_creator.lightVolumeLoad)
|
|
337
|
-
if (data.volume && !files.volume) console.warn(`Lightmap: the bake wrote a light grid (${data.volume.file}) but the scene does not list it — add volume: asset('…/${data.volume.file}') to env.lightmap, movers keep the IBL until then`)
|
|
338
|
-
return
|
|
339
|
-
}
|
|
357
|
+
if (!files.volume || !data.volume || !_creator.lightVolumeLoad) return
|
|
340
358
|
try {
|
|
341
359
|
const resp = await fetch(files.volume, { useOnce: true })
|
|
342
360
|
if (resp.status >= 400) { console.warn(`Lightmap: no light grid at ${files.volume} (HTTP ${resp.status})`); return }
|
|
@@ -361,7 +379,8 @@ export class Lightmap {
|
|
|
361
379
|
* False = the bake needs it and it cannot be had - its rects would read the wrong texels. */
|
|
362
380
|
private static async loadUv1(data: BakeData, files: LightmapFiles): Promise<boolean> {
|
|
363
381
|
if (!data.uv1) { _creator.lightmapUv1Clear?.(); return true }
|
|
364
|
-
|
|
382
|
+
// bake() lists every file the manifest names: only a def written by hand can miss it
|
|
383
|
+
if (!files.uv1) { console.warn(`Lightmap: ${files.path} maps a packed uv1 (${data.uv1}) the def does not carry — the bake is not applied`); return false }
|
|
365
384
|
if (!_creator.lightmapUv1Load) { console.warn("Lightmap: this host cannot take the bake's packed lightmap uv1 (rebuild it) — the bake is not applied"); return false }
|
|
366
385
|
try {
|
|
367
386
|
const resp = await fetch(files.uv1, { useOnce: true })
|
|
@@ -411,43 +430,40 @@ export class Lightmap {
|
|
|
411
430
|
else _creator.lightmapSunSet?.(0, 1, 0, 0, 0, 0, false)
|
|
412
431
|
}
|
|
413
432
|
|
|
414
|
-
/** Apply a bake — or, under `lecodes lightmap bake`, run it.
|
|
415
|
-
* applied (no bake yet, a host without the
|
|
433
|
+
/** Apply a bake — or, under `lecodes lightmap bake`, run it. `def` is what `bake('<path>.bake', options)` compiled
|
|
434
|
+
* to (a scene file's `env.lightmap`). Resolves to null when nothing was applied (no bake yet, a host without the
|
|
435
|
+
* feature, bake mode). */
|
|
416
436
|
private static scene: Scene | null = null
|
|
417
|
-
static async load(scene: Scene,
|
|
437
|
+
static async load(scene: Scene, def: LightmapDef): Promise<LightmapInfo | null> {
|
|
418
438
|
Lightmap.scene = scene
|
|
419
439
|
const bake = bakeConfig()
|
|
420
|
-
if (bake) { Lightmap.bake(bake,
|
|
440
|
+
if (bake) { Lightmap.bake(bake, def); return null }
|
|
421
441
|
if (!_creator.lightmapApply) {
|
|
422
442
|
if (!Lightmap.warned) { Lightmap.warned = true; console.warn("Lightmap: this host has no lightmap support — rendering without the bake") }
|
|
423
443
|
return null
|
|
424
444
|
}
|
|
445
|
+
if (def.data === null) { console.warn(`Lightmap: no bake at ${def.path} yet — run \`lecodes lightmap bake\``); return null }
|
|
425
446
|
let data: BakeData
|
|
426
447
|
try {
|
|
427
|
-
const resp = await fetch(
|
|
428
|
-
if (resp.status >= 400) { console.warn(`Lightmap: no bake at ${
|
|
448
|
+
const resp = await fetch(def.data, { useOnce: true })
|
|
449
|
+
if (resp.status >= 400) { console.warn(`Lightmap: no bake at ${def.path} (HTTP ${resp.status}) — run \`lecodes lightmap bake\``); return null }
|
|
429
450
|
data = resp.json()
|
|
430
451
|
} catch (e) {
|
|
431
|
-
console.warn(`Lightmap: could not read ${
|
|
452
|
+
console.warn(`Lightmap: could not read ${def.path}: ${String(e)}`)
|
|
432
453
|
return null
|
|
433
454
|
}
|
|
434
455
|
if (data.version !== 4 || !Array.isArray(data.instances)) {
|
|
435
|
-
console.warn(`Lightmap: ${
|
|
456
|
+
console.warn(`Lightmap: ${def.path} is not a creator-bake file (version ${String(data.version)}) — rebake with \`lecodes lightmap bake\``)
|
|
436
457
|
return null
|
|
437
458
|
}
|
|
438
|
-
if (!
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
// both atlases are DATA and exempt from Texture.maxSize (a capped atlas blurs every shadow edge)
|
|
447
|
-
const n = Math.min(pages, lightFiles.length, auxFiles.length)
|
|
448
|
-
const light = await Promise.all(lightFiles.slice(0, n).map((f) => Texture.load(f, { fullSize: true, srgb: false })))
|
|
449
|
-
const aux = await Promise.all(auxFiles.slice(0, n).map((f) => Texture.load(f, { fullSize: true, srgb: false })))
|
|
450
|
-
const lightScale = data.lightScale * (options.lightScale ?? 1)
|
|
459
|
+
if (!(await Lightmap.loadUv1(data, def))) return null // the rects map the packed uv1: it goes in first
|
|
460
|
+
// the pages the manifest names, in order (bake() listed them off it); both atlases are DATA and exempt from
|
|
461
|
+
// Texture.maxSize (a capped atlas blurs every shadow edge)
|
|
462
|
+
const n = Math.min(data.pages, def.light.length, def.aux.length)
|
|
463
|
+
if (!(n > 0)) { console.warn(`Lightmap: ${def.path} names no atlas page — rebake with \`lecodes lightmap bake\``); return null }
|
|
464
|
+
const light = await Promise.all(def.light.slice(0, n).map((f) => Texture.load(f, { fullSize: true, srgb: false })))
|
|
465
|
+
const aux = await Promise.all(def.aux.slice(0, n).map((f) => Texture.load(f, { fullSize: true, srgb: false })))
|
|
466
|
+
const lightScale = data.lightScale * (def.lightScale ?? 1)
|
|
451
467
|
_creator.lightmapSetOptions?.(lightScale, data.emissiveNits ?? 0)
|
|
452
468
|
// a mover's sun shadow on the statics: only a bake that wrote the sun map under its aux pages
|
|
453
469
|
const sun = data.sunMap ? data.sun : null
|
|
@@ -457,19 +473,18 @@ export class Lightmap {
|
|
|
457
473
|
let probes = 0
|
|
458
474
|
let probeAtlas: Texture | undefined
|
|
459
475
|
const pb = data.probes
|
|
460
|
-
if (pb && pb.count > 0) {
|
|
461
|
-
if (!
|
|
462
|
-
else if (!_creator.lightmapProbesSet) console.warn("Lightmap: this host has no reflection probes — statics reflect the sky alone")
|
|
476
|
+
if (pb && pb.count > 0 && def.probes) {
|
|
477
|
+
if (!_creator.lightmapProbesSet) console.warn("Lightmap: this host has no reflection probes — statics reflect the sky alone")
|
|
463
478
|
else if (!Array.isArray(pb.encode) || pb.encode.length < 6) console.warn("Lightmap: the bake's probes are in an older format — rebake (statics reflect the sky alone until then)")
|
|
464
479
|
else {
|
|
465
|
-
probeAtlas = await Texture.load(
|
|
480
|
+
probeAtlas = await Texture.load(def.probes, { fullSize: true, srgb: false })
|
|
466
481
|
// which probes a pixel reflects is in the aux pages (their probe maps); the host needs the atlas and the records' range
|
|
467
482
|
probes = _creator.lightmapProbesSet(probeAtlas._h.id, pb.oct, pb.levels, pb.perRow, pb.count,
|
|
468
483
|
pb.encode[0], pb.encode[1], pb.encode[2], pb.encode[3], pb.encode[4], pb.encode[5])
|
|
469
484
|
if (probes !== pb.count) console.warn(`Lightmap: the host took ${probes} of ${pb.count} probe(s) — rebake`)
|
|
470
485
|
}
|
|
471
486
|
}
|
|
472
|
-
console.log(`[lightmap] bound ${n} page(s) of ${data.size} at ${(data.texel * 100).toFixed(2)} cm (${data.mips ?? 1} level(s), sun map ${data.sunMapLevels ?? (data.sunMap ? 1 : 0)}), lightScale ${lightScale}${(
|
|
487
|
+
console.log(`[lightmap] bound ${n} page(s) of ${data.size} at ${(data.texel * 100).toFixed(2)} cm (${data.mips ?? 1} level(s), sun map ${data.sunMapLevels ?? (data.sunMap ? 1 : 0)}), lightScale ${lightScale}${(def.lightScale ?? 1) !== 1 ? ` (bake ${data.lightScale} x ${def.lightScale})` : ""}${probes > 0 ? `, ${probes} reflection probe(s)` : ""}`)
|
|
473
488
|
const rects = new Map<string, BakeInstance>()
|
|
474
489
|
for (const inst of data.instances) if (inst.receiver && inst.st && inst.page !== undefined) rects.set(inst.key, inst)
|
|
475
490
|
// the ones the bake left out for their size: occluders there, movers here — not a rect gone missing
|
|
@@ -483,7 +498,7 @@ export class Lightmap {
|
|
|
483
498
|
Float32Array.from([ l.axis, l.from, l.to, l.cubes.length, ...l.cubes.flat(2), probeId(l.probes[0]), probeId(l.probes[1]), l.probes[2] ])
|
|
484
499
|
// a bake from before the slabs (`light.cube`, one cube): not read — its small statics stay unlit until it is rebaked
|
|
485
500
|
const lit = (l?: BakeObjectLight): l is BakeObjectLight => !!l && Array.isArray(l.cubes)
|
|
486
|
-
if (data.instances.some((i) => i.small && i.light && !lit(i.light))) console.warn(`Lightmap: ${
|
|
501
|
+
if (data.instances.some((i) => i.small && i.light && !lit(i.light))) console.warn(`Lightmap: ${def.path} has small statics without \`light.cubes\` (baked before the slabbed object light) — they stay unlit; rebake with \`lecodes lightmap bake\``)
|
|
487
502
|
// THE BAKE'S UNIT IS THE MESH: a model's primitives are records of their own, `<key>@<ordinal>` (ordinal = the primitive's
|
|
488
503
|
// index over the file's meshes) - a rect, or a small static's light, each; the host binds record k to that primitive
|
|
489
504
|
const parts = Lightmap.recordsOf(data.instances)
|
|
@@ -531,8 +546,8 @@ export class Lightmap {
|
|
|
531
546
|
}
|
|
532
547
|
if (small.size) console.log(`[lightmap] ${small.size} small, thin or cube-marked record(s) are not in the atlas (the bake's minTexels / thinTexels / cube): ${smallLit} lit by the light baked where they stand`)
|
|
533
548
|
if (missing.length) console.warn(`Lightmap: ${missing.length} static(s) or mesh record(s) have no rect in the bake (${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "…" : ""}) — rebake`)
|
|
534
|
-
Lightmap.bound = { data, rects, byEntity, files, pages: n, aux, light, probes, probeAtlas, showing: "light" }
|
|
535
|
-
await Lightmap.loadVolume(data,
|
|
549
|
+
Lightmap.bound = { data, rects, byEntity, files: def, pages: n, aux, light, probes, probeAtlas, showing: "light" }
|
|
550
|
+
await Lightmap.loadVolume(data, def)
|
|
536
551
|
return { size: data.size, texel: data.texel, pages: n, applied, total: rects.size, missing }
|
|
537
552
|
}
|
|
538
553
|
|
|
@@ -551,7 +566,7 @@ export class Lightmap {
|
|
|
551
566
|
}
|
|
552
567
|
|
|
553
568
|
/** Switch every lightmapped surface to a DATA view (or back with `"off"` / 0). Level-wide, instant —
|
|
554
|
-
* except "direct" / "indirect", which load that page set the first time (a
|
|
569
|
+
* except "direct" / "indirect", which load that page set the first time (a bake made with `--split`). */
|
|
555
570
|
static debug(mode: LightmapDebugMode | number, params: LightmapDebugParams = {}): void {
|
|
556
571
|
const m = typeof mode === "number" ? mode : Math.max(0, DEBUG_MODES.indexOf(mode))
|
|
557
572
|
const [ lo, hi ] = params.luxRange ?? [ 0, 5 ]
|
|
@@ -566,9 +581,9 @@ export class Lightmap {
|
|
|
566
581
|
if (!b || b.showing === kind || !_creator.lightmapApply) return
|
|
567
582
|
let tex = b[kind]
|
|
568
583
|
if (!tex) {
|
|
569
|
-
const files =
|
|
584
|
+
const files = b.files[kind] ?? []
|
|
570
585
|
if (files.length < b.pages) {
|
|
571
|
-
console.warn(`Lightmap:
|
|
586
|
+
console.warn(`Lightmap: ${b.files.path} has no "${kind}" pages — rebake with \`lecodes lightmap bake --split\``)
|
|
572
587
|
return
|
|
573
588
|
}
|
|
574
589
|
tex = await Promise.all(files.slice(0, b.pages).map((f) => Texture.load(f, { fullSize: true, srgb: false })))
|
|
@@ -656,7 +671,12 @@ export class Lightmap {
|
|
|
656
671
|
return typeof c === "string" ? Color.toHexString(c) : "#ffffffff"
|
|
657
672
|
}
|
|
658
673
|
|
|
659
|
-
/** The lightmap material for a static Mesh, from whatever lit material it carried (colour, map, roughness, metallic)
|
|
674
|
+
/** The lightmap material for a static Mesh, from whatever lit material it carried (colour, map, roughness, metallic)
|
|
675
|
+
* — and its GLOW, so a glowing Mesh looks the same before and after the bake applies: `emissive` (linear) becomes the
|
|
676
|
+
* lightmap material's `emissiveFactor` at strength 1; its own `emissiveNits` go over with `emissiveOwn` 1, which the
|
|
677
|
+
* level-wide write (`lightmapSetOptions`) leaves alone. A glow without nits takes the level's, like a GLB panel:
|
|
678
|
+
* screen-referred under a level of 0 nits, else emission x the level's nits through the exposure — the same as
|
|
679
|
+
* before the bake when the level's nits are the camera's (the default). */
|
|
660
680
|
private static meshMaterial(mesh: Mesh): Material {
|
|
661
681
|
const u = mesh.material?.uniforms ?? {}
|
|
662
682
|
const m = Material.lightmap()
|
|
@@ -664,15 +684,28 @@ export class Lightmap {
|
|
|
664
684
|
if (u.baseColorMap instanceof Texture) m.set("baseColorMap", u.baseColorMap)
|
|
665
685
|
if (typeof u.roughness === "number") m.set("roughnessFactor", u.roughness)
|
|
666
686
|
if (typeof u.metallic === "number") m.set("metallicFactor", u.metallic)
|
|
687
|
+
// linear [r, g, b] as Material.lit writes it, or a colour string a later `.set('emissive', '#…')` left (the engine
|
|
688
|
+
// converts that one like any colour uniform)
|
|
689
|
+
const e = u.emissive
|
|
690
|
+
if ((Array.isArray(e) && e.length === 3 && e[0] + e[1] + e[2] > 0) || typeof e === "string") {
|
|
691
|
+
m.set("emissiveFactor", Array.isArray(e) ? [ e[0], e[1], e[2] ] : e as string).set("emissiveStrength", 1)
|
|
692
|
+
if (typeof u.emissiveNits === "number" && u.emissiveNits > 0) m.set("emissiveNits", u.emissiveNits).set("emissiveOwn", 1)
|
|
693
|
+
}
|
|
667
694
|
return m
|
|
668
695
|
}
|
|
669
696
|
|
|
670
|
-
private static bake(cfg: BakeConfig,
|
|
697
|
+
private static bake(cfg: BakeConfig, def: LightmapDef): void {
|
|
698
|
+
// the target is where the level's bake() points (`<root>/<path>`): the files land next to the `.bake` it names
|
|
699
|
+
const slash = def.path.lastIndexOf("/")
|
|
700
|
+
const stem = def.path.slice(slash + 1).replace(/\.bake$/i, "")
|
|
701
|
+
if (cfg.stem !== undefined && cfg.stem !== stem) { console.log(`[lightmap] ${def.path} is not the level being baked (--name ${cfg.stem}): skipped`); return }
|
|
702
|
+
const outDir = cfg.outDir ?? (slash > 0 ? `${cfg.root}/${def.path.slice(0, slash)}` : cfg.root)
|
|
703
|
+
const levelNits = def.emissiveNits, emissiveBake = def.emissiveBake ?? true, transmit = def.transmit, levelCube = def.cube
|
|
671
704
|
if (!_creator.lightmapBake) { console.error("[lightmap] error: this host has no bake module (the bake runs in lecodes-bake: LECODES_BAKE_EXE names a host without it)"); return }
|
|
672
705
|
const idOf = (e: Entry): number => (e.model ?? e.mesh ?? e.ground!).id
|
|
673
706
|
const ids = Uint32Array.from(Lightmap.entries.map(idOf))
|
|
674
707
|
const keys = Lightmap.entries.map((e) => e.key).join("\n")
|
|
675
|
-
// the statics baked as cubes: by hand per node (`cube`), and by key - the CLI's --cube and the level's
|
|
708
|
+
// the statics baked as cubes: by hand per node (`cube`), and by key - the CLI's --cube and the level's `cube` option, one regex
|
|
676
709
|
const cubeIds = Uint32Array.from(Lightmap.entries.filter((e) => e.cube).map(idOf))
|
|
677
710
|
const cubeKeys = [ ...(cfg.cube ?? []), ...(levelCube ?? []) ].filter((p) => p !== "").map((p) => `(?:${p})`).join("|")
|
|
678
711
|
if (ids.length === 0) { console.error("[lightmap] error: nothing registered — call Lightmap.add on the static props before Lightmap.load"); return }
|
|
@@ -682,8 +715,8 @@ export class Lightmap {
|
|
|
682
715
|
for (const l of live) _creator.setLightIntensity?.(l.id, 0)
|
|
683
716
|
const ev100 = Lightmap.scene?.camera.ev100 ?? 15
|
|
684
717
|
const emissiveNits = cfg.emissiveNits ?? levelNits ?? 1.2 * 2 ** ev100
|
|
685
|
-
if (emissiveNits > 0) console.log(`[lightmap] emission 1.0 = ${Math.round(emissiveNits)} nits${emissiveBake ? "" : ", DRAWN only (
|
|
686
|
-
_creator.lightmapBake(
|
|
718
|
+
if (emissiveNits > 0) console.log(`[lightmap] emission 1.0 = ${Math.round(emissiveNits)} nits${emissiveBake ? "" : ", DRAWN only (the level's emissiveBake: false - the emissive surfaces light nothing)"} (${cfg.emissiveNits !== undefined ? "--emissive-nits" : levelNits !== undefined ? "the level's emissiveNits" : `the camera's EV100 ${ev100.toFixed(1)}`})`)
|
|
719
|
+
_creator.lightmapBake(outDir, stem, ids, keys, cfg.size ?? 4096, cfg.texel ?? 0.05, cfg.pages ?? 4,
|
|
687
720
|
cfg.sunRays ?? 64, cfg.skyRays ?? 2048, cfg.lightRays ?? 64, cfg.lightRadius ?? 0.05, cfg.sunAngle ?? 0.53, cfg.bias ?? 0.01, cfg.seed ?? 1,
|
|
688
721
|
cfg.bounceRays ?? 128, cfg.bounces ?? 3, cfg.patchSamples ?? 64, cfg.split ?? false, DENOISE_LEVELS[cfg.denoise ?? "no-sun"], cfg.filter ?? 1,
|
|
689
722
|
cfg.probeSpacing ?? 4, cfg.probeSize ?? 32, cfg.probeRays ?? 128, cfg.probeIndoor ?? false, cfg.probeLayout === "grid" ? 1 : 0, cfg.probeBox ? Float32Array.from(cfg.probeBox) : null, (cfg.minTexels ?? 12) * (cfg.texel ?? 0.05), cfg.volumeSpacing ?? 1, emissiveBake ? emissiveNits : -emissiveNits, cfg.detailTexels ?? 2000, cfg.debugDir ?? "",
|
package/src/gl/Material.ts
CHANGED
|
@@ -162,12 +162,36 @@ export const _sheetColsRows = (sheet: number | [number, number] | undefined): [n
|
|
|
162
162
|
return [ Math.max(1, Math.floor(c) || 1), Math.max(1, Math.floor(r) || 1) ]
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
/** `Material.lit` options — PBR scalars on top of the color/map pair. */
|
|
165
|
+
/** `Material.lit` options — PBR scalars on top of the color/map pair, and the surface's own glow. */
|
|
166
166
|
export type LitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
|
|
167
167
|
/** Perceptual roughness, 0 (mirror) … 1 (matte). Unset = the shader's default. */
|
|
168
168
|
roughness?: number
|
|
169
169
|
/** Metallic factor, 0 (dielectric) … 1 (metal). Unset = the shader's default. */
|
|
170
170
|
metallic?: number
|
|
171
|
+
/** The surface's GLOW, added on top of its lit colour: a ceiling panel, a screen, a sign. Any colour (sRGB, like
|
|
172
|
+
* `color`); an `[r, g, b]` array may go past 1 — an HDR colour, its hue in sRGB and the excess a linear multiplier
|
|
173
|
+
* (`[4, 3.6, 3]` = `[1, 0.9, 0.75]` four times as bright). Unset = no glow. */
|
|
174
|
+
emissive?: ColorInput
|
|
175
|
+
/** The glow's luminance in cd / m² (nits) for `emissive` 1.0 — `#ffffff` at 1600 is a typical ceiling panel. Set,
|
|
176
|
+
* the glow is drawn SCENE-referred: through the camera's exposure, as bright as the light it gives the room in a
|
|
177
|
+
* bake. Unset (or 0) = SCREEN-referred: the colour is drawn as it is whatever the exposure, and a bake counts it at
|
|
178
|
+
* the level's `emissiveNits` (what the camera makes of 1.0 unless the level says otherwise). */
|
|
179
|
+
emissiveNits?: number
|
|
180
|
+
/** `false` = decoration: the glow is drawn, but a lightmap bake does not light the level with it (Unity's "Global
|
|
181
|
+
* Illumination: None") — the level's lamps do. Default true: a glowing static lights the level like a lamp. The
|
|
182
|
+
* level-wide `bake('…', { emissiveBake: false })` overrides it for every surface. */
|
|
183
|
+
emissiveBake?: boolean
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// sRGB → linear, one channel (the exact curve: IEC 61966-2-1)
|
|
187
|
+
const srgbToLinear = (v: number): number => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)
|
|
188
|
+
|
|
189
|
+
/** @internal `emissive` as lit.mat takes it: LINEAR rgb. An HDR array keeps its excess over 1 as a linear multiplier
|
|
190
|
+
* on the sRGB hue (`peak` = its brightest channel). */
|
|
191
|
+
export const _emissiveLinear = (c: ColorInput): [number, number, number] => {
|
|
192
|
+
const [ r, g, b ] = Color.toRgb01(c)
|
|
193
|
+
const peak = Math.max(r, g, b, 1)
|
|
194
|
+
return [ srgbToLinear(Math.max(0, r) / peak) * peak, srgbToLinear(Math.max(0, g) / peak) * peak, srgbToLinear(Math.max(0, b) / peak) * peak ]
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
|
|
@@ -179,6 +203,9 @@ export class Material {
|
|
|
179
203
|
readonly uniforms: Record<string, UniformValue>
|
|
180
204
|
|
|
181
205
|
private _colorKey: string | null = null
|
|
206
|
+
/** @internal lit: whether the glow lights the level in a bake (`LitMaterialOptions.emissiveBake`); lit.mat carries
|
|
207
|
+
* the same as its `emissiveBake` parameter, which is what the bake reads off a Mesh's instance */
|
|
208
|
+
_emissiveBake = true
|
|
182
209
|
|
|
183
210
|
constructor(shader: FetchResponse | Handle | number) {
|
|
184
211
|
if (typeof shader === "number") {
|
|
@@ -265,7 +292,8 @@ export class Material {
|
|
|
265
292
|
return new Material(h)
|
|
266
293
|
}
|
|
267
294
|
|
|
268
|
-
/** PBR lit material.
|
|
295
|
+
/** PBR lit material. `emissive` / `emissiveNits` / `emissiveBake` are its glow (`LitMaterialOptions`); the uniforms
|
|
296
|
+
* are written only for a material that asks for a glow, so a plain lit material never names them. */
|
|
269
297
|
static lit(options: LitMaterialOptions = {}): Material {
|
|
270
298
|
const m = Material._builtin(_creator.builtinMaterial("lit"), "lit")
|
|
271
299
|
m._colorKey = "baseColor"
|
|
@@ -273,6 +301,12 @@ export class Material {
|
|
|
273
301
|
if (options.map) m.map = options.map
|
|
274
302
|
if (options.roughness !== undefined) m.uniforms.roughness = options.roughness
|
|
275
303
|
if (options.metallic !== undefined) m.uniforms.metallic = options.metallic
|
|
304
|
+
if (options.emissiveBake === false) m._emissiveBake = false
|
|
305
|
+
if (options.emissive !== undefined) {
|
|
306
|
+
m.uniforms.emissive = _emissiveLinear(options.emissive)
|
|
307
|
+
if (options.emissiveNits !== undefined) m.uniforms.emissiveNits = Math.max(0, options.emissiveNits)
|
|
308
|
+
m.uniforms.emissiveBake = m._emissiveBake ? 1 : 0
|
|
309
|
+
}
|
|
276
310
|
applyDepth(m, options)
|
|
277
311
|
return m
|
|
278
312
|
}
|
package/src/gl/Node.ts
CHANGED
|
@@ -94,6 +94,43 @@ const findNodeByWalk = (rootId: number, name: string): number => {
|
|
|
94
94
|
// Scratch for the world-position read-back in _setOwnedPosition (one per module, never handed out).
|
|
95
95
|
const worldScratch = new Float32Array(3)
|
|
96
96
|
|
|
97
|
+
/** @internal The scene a node is in: the one `scene.add` put it or its nearest such ancestor in (`_inScene`), -1 = none
|
|
98
|
+
* yet. A scene's nodes are pinned, so the walk meets the very wrappers `scene.add` stamped. */
|
|
99
|
+
export const sceneIdOf = (node: Node): number => {
|
|
100
|
+
if (node._inScene >= 0) return node._inScene
|
|
101
|
+
for (let id = _creator.getParent(node.id); id !== 0; id = _creator.getParent(id)) {
|
|
102
|
+
const n = nodes.get(id)
|
|
103
|
+
if (n && n._inScene >= 0) return n._inScene
|
|
104
|
+
}
|
|
105
|
+
return -1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @internal A node whose render view starts once the node is in a scene (a secondary Camera's `renderToTexture`, a
|
|
109
|
+
* ReflectionProbe): `_bindView(sceneId)` starts it if it has none. */
|
|
110
|
+
export type ViewBinder = Node & { _bindView(sceneId: number): void }
|
|
111
|
+
// The binders waiting for a scene, WEAKLY: a view never placed must not pin its node. Tried whenever the scene graph
|
|
112
|
+
// grows (bindPendingViews) — an empty set costs that call one size check.
|
|
113
|
+
const pendingViews = new Set<WeakRef<ViewBinder>>()
|
|
114
|
+
/** @internal Start `binder`'s view now if its node is in a scene, else at the scene.add / node.add that puts it in one.
|
|
115
|
+
* One try per placement: a view the host refuses (an overlay scene) warns and is not retried. */
|
|
116
|
+
export const awaitScene = (binder: ViewBinder): void => {
|
|
117
|
+
const sceneId = sceneIdOf(binder)
|
|
118
|
+
if (sceneId >= 0) binder._bindView(sceneId)
|
|
119
|
+
else pendingViews.add(new WeakRef(binder))
|
|
120
|
+
}
|
|
121
|
+
/** @internal The scene graph grew (scene.add, node.add, setParent): start the waiting views whose node is in a scene now. */
|
|
122
|
+
export const bindPendingViews = (): void => {
|
|
123
|
+
if (pendingViews.size === 0) return
|
|
124
|
+
for (const ref of pendingViews) {
|
|
125
|
+
const binder = ref.deref()
|
|
126
|
+
if (!binder) { pendingViews.delete(ref); continue }
|
|
127
|
+
const sceneId = sceneIdOf(binder)
|
|
128
|
+
if (sceneId < 0) continue
|
|
129
|
+
pendingViews.delete(ref)
|
|
130
|
+
binder._bindView(sceneId)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
97
134
|
export class Node extends AspectHost<NodeEvents> implements CompWriter {
|
|
98
135
|
/** @internal The entity's owned handle (`_h.id` = the entity id; DEAD_HANDLE once destroyed). An
|
|
99
136
|
* engine-created entity (a glTF part, an anchor) is wrapped from its number and pinned as the
|
|
@@ -101,6 +138,9 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
|
|
|
101
138
|
_h: Handle
|
|
102
139
|
/** Native entity id. */
|
|
103
140
|
get id(): number { return this._h.id }
|
|
141
|
+
/** @internal The id of the scene `scene.add` put this node in (-1 = none; `scene.remove` clears it) — how a render
|
|
142
|
+
* view finds its scene (sceneIdOf). */
|
|
143
|
+
_inScene = -1
|
|
104
144
|
|
|
105
145
|
private _matrix?: Float32Array
|
|
106
146
|
private _worldMatrix?: Float32Array
|
|
@@ -132,6 +172,25 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
|
|
|
132
172
|
set name(value: string) { _creator.setName(this.id, value) }
|
|
133
173
|
get visible(): boolean { return _creator.isVisible(this.id) }
|
|
134
174
|
set visible(value: boolean) { _creator.setVisible(this.id, value) }
|
|
175
|
+
/**
|
|
176
|
+
* The node's visibility LAYERS, a bit mask 1..255. The scene's own view draws every layer; a render
|
|
177
|
+
* view (a secondary `Camera`'s or a `ReflectionProbe`'s, their `layers` option) draws only the
|
|
178
|
+
* renderables whose layers share a bit with its mask — which is what layers are for: leaving a subtree
|
|
179
|
+
* out of a second camera (the car out of the reflection probe riding on it, the player's own body out
|
|
180
|
+
* of a mirror). A node without a layer
|
|
181
|
+
* of its own takes its nearest ancestor's, else 1, so a car's layer covers everything parented under
|
|
182
|
+
* it; `0` clears the node's own (inherit again). `visible = false` wins over any layer. Reads the
|
|
183
|
+
* effective bits. A host without layers draws everything everywhere (reads 1, a write warns once).
|
|
184
|
+
*/
|
|
185
|
+
get layer(): number { return _creator.getLayer ? _creator.getLayer(this.id) : 1 }
|
|
186
|
+
set layer(mask: number) {
|
|
187
|
+
if (_creator.setLayer) _creator.setLayer(this.id, mask & 0xff)
|
|
188
|
+
else if (!Node._warnedLayer) {
|
|
189
|
+
Node._warnedLayer = true
|
|
190
|
+
console.warn("[node] this host has no setLayer — node layers are ignored, every view draws everything")
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
private static _warnedLayer = false
|
|
135
194
|
|
|
136
195
|
// --- local transform ---
|
|
137
196
|
// Native owns the authoritative matrix. `_sync()` reads it into a per-node scratch buffer on EVERY
|
|
@@ -317,6 +376,7 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
|
|
|
317
376
|
/** Parent the given nodes under this one. */
|
|
318
377
|
add(...children: Node[]): this {
|
|
319
378
|
for (const c of children) { _creator.setParent(c.id, this.id, false); nodes.pin(c) }
|
|
379
|
+
bindPendingViews()
|
|
320
380
|
return this
|
|
321
381
|
}
|
|
322
382
|
setParent(parent: Node | null, worldPositionStays = false): this {
|
|
@@ -326,6 +386,7 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
|
|
|
326
386
|
} else {
|
|
327
387
|
_creator.setParent(this.id, parent.id, worldPositionStays)
|
|
328
388
|
nodes.pin(this)
|
|
389
|
+
bindPendingViews()
|
|
329
390
|
}
|
|
330
391
|
return this
|
|
331
392
|
}
|
package/src/gl/Particles.ts
CHANGED
|
@@ -306,7 +306,7 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
|
306
306
|
* without this the engine picks who covers whom per frame (smoke popping over a fireball).
|
|
307
307
|
* Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
|
|
308
308
|
order?: number
|
|
309
|
-
/** Sprites take the light of the PLACE each one is in — the level's baked light grid (`
|
|
309
|
+
/** Sprites take the light of the PLACE each one is in — the level's baked light grid (the bake's `.lgrid`).
|
|
310
310
|
* A sprite is unlit: its colour is the picture, which is right for fire and wrong for dust — a puff kicked up
|
|
311
311
|
* under an awning glows as if it stood in the sun. With `lit` every particle's colour is multiplied by the light
|
|
312
312
|
* where it is, relative to the level's OPEN ground: exactly the authored colour out in the sun, the ambient's
|