lecodes-sdk 2.4.5 → 2.4.7
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/types/gl/Lightmap.d.ts +25 -4
- package/dist/types/gl/Model.d.ts +4 -4
- package/dist/types/runtime/app.d.ts +14 -0
- package/dist/types/scene/defineScene.d.ts +15 -2
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/app.d.ts +8 -1
- package/src/bridges/gl.d.ts +38 -17
- package/src/gl/Lightmap.ts +127 -48
- package/src/gl/Model.ts +4 -4
- package/src/math/vec.ts +1 -1
- package/src/runtime/app.ts +14 -0
- package/src/scene/defineScene.ts +28 -13
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/bridges/app.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// `?` on a method = an optional slot: a host that leaves it null gets no such JS method, so the SDK's
|
|
12
12
|
// `typeof` feature-detects stay honest. The `HostApp` interface below adds the slots the runtime
|
|
13
13
|
// calls that JS never sees (notifications to the host).
|
|
14
|
-
import type { bytes, i32 } from "./types"
|
|
14
|
+
import type { bytes, f64, i32 } from "./types"
|
|
15
15
|
|
|
16
16
|
declare global {
|
|
17
17
|
// ---- World control (run/restart/quit — the platform's `location` analog) ----------------------
|
|
@@ -58,6 +58,13 @@ declare global {
|
|
|
58
58
|
/** Collect garbage, drain the release queue and report the live owned records per kind
|
|
59
59
|
* (`{"live":{"tree":n,…},"pending":n}`) — the read behind checks/owned. @custom */
|
|
60
60
|
ownedStats(): string
|
|
61
|
+
/** Collect garbage and drain the release queue now, logging `[gc] app.gc: <ms>, heap …, next at
|
|
62
|
+
* …, <n> owned resources freed`. `headroomMB` (> 0) puts the next automatic pass at least that
|
|
63
|
+
* many megabytes above the heap the pass left (QuickJS; until that pass). On JavaScriptCore a
|
|
64
|
+
* request (the engine collects on its own schedule, the drain follows: on iOS the line says 0;
|
|
65
|
+
* no headroom); the wasm arm collects only under Bun, in a browser page it is the drain alone.
|
|
66
|
+
* Web Lite has no such member. Backs app.gc. @custom */
|
|
67
|
+
gc?(headroomMB?: f64): void
|
|
61
68
|
/** Current lifecycle state: "active" | "background". Surfaced as app.state. */
|
|
62
69
|
state?(): string
|
|
63
70
|
/** Current connectivity (navigator.onLine semantics — best-effort: false only when the platform
|
package/src/bridges/gl.d.ts
CHANGED
|
@@ -14,9 +14,11 @@ export type PickBone = { name: string, weight: f32 }
|
|
|
14
14
|
* `bones` = its JOINTS_0 / WEIGHTS_0 pairs with weight > 0 (empty = unweighted). */
|
|
15
15
|
export type PickVertex = { index: u32, bind: [f32, f32, f32], world: [f32, f32, f32], bones: PickBone[] }
|
|
16
16
|
/** The closest triangle under a screen point (`pickTriangle`). `node` = the mesh node's own entity (0
|
|
17
|
-
* when the host has none), `
|
|
17
|
+
* when the host has none), `lightmapOrdinal` = the primitive's index over the file's meshes (the bake's record
|
|
18
|
+
* `<key>@<ordinal>`), `uv1` (the lightmap uv the GPU reads: the bake's packed one when loaded) only when the primitive
|
|
19
|
+
* carries one. */
|
|
18
20
|
export type TrianglePick = {
|
|
19
|
-
entity: u32, node: u32, nodeName: string,
|
|
21
|
+
entity: u32, node: u32, nodeName: string, lightmapOrdinal: u32,
|
|
20
22
|
mesh: string, primitive: u32, material: string,
|
|
21
23
|
triangle: u32, backface: boolean, distance: f32,
|
|
22
24
|
point: [f32, f32, f32], bary: [f32, f32, f32], uv1?: [f32, f32],
|
|
@@ -671,23 +673,31 @@ declare global {
|
|
|
671
673
|
// JoltPhysics. motionType: 0 static / 1 kinematic / 2 dynamic.
|
|
672
674
|
/** @c creatorHasPhysics */
|
|
673
675
|
physicsHasSupport(): boolean
|
|
674
|
-
/** Lightmap bake (engines/bake) — a dev-time tool compiled into the
|
|
676
|
+
/** Lightmap bake (engines/bake) — a dev-time tool compiled into the bake tool lecodes-bake only; `lightmapBake` exists
|
|
675
677
|
* only where `lightmapHasSupport()` is true. Describes the given static instances (GLB roots / Mesh entities;
|
|
676
678
|
* `keysJoined` = one key per id, '
|
|
677
679
|
'-joined), the scene's brightest sun, its point lights (spherical emitters of
|
|
678
|
-
* `lightRadius` metres) and its IBL to the bake, which traces them on the
|
|
680
|
+
* `lightRadius` metres) and its IBL to the bake, which traces them on the GPU (`tracer`: 0 auto = OptiX where an NVIDIA driver
|
|
681
|
+
* with OptiX opens, else Vulkan ray query on any card with hardware ray tracing; 1 OptiX, 2 Vulkan by name) — direct light plus
|
|
679
682
|
* `bounceRays` Lambertian paths of `bounces` vertices per texel (0 / 0 = direct only) with `patchSamples` next-event
|
|
680
683
|
* samples over the bright patches of a first direct pass — and writes
|
|
681
684
|
* `<outDir>/<stem>.bake` + `<stem>-light[_n].ktx2` (irradiance in lux / lightScale, BC6H) + `<stem>-aux[_n].ktx2`
|
|
682
685
|
* (sun / sky visibility + light direction); `split` adds `<stem>-direct[_n]` / `-indirect[_n]` page sets for the
|
|
683
|
-
* SDK's two views; `denoise` runs
|
|
686
|
+
* SDK's two views; `denoise` runs the AI denoiser (`denoiser`: 0 auto = OptiX's on an NVIDIA card, else Intel Open Image Denoise; 1 OptiX, 2 OIDN by name —
|
|
687
|
+
* independent of the tracer) over 0 nothing, 1 the indirect part, 2 + the sky and the lamps, 3 + the sun; `filter` = the texel's reconstruction filter radius in texels (1 = ±1 texel tent, 0.5 = within the texel, 0 = the centre alone);
|
|
684
688
|
* `probeSpacing` > 0 places REFLECTION PROBES on a grid of that many metres and writes `<stem>-probes.ktx2` (a BC6H atlas of octahedral
|
|
685
689
|
* maps: one column of roughness-level tiles per probe, captured with `probeSize`² cube faces and `probeRays` paths per texel; the scene's
|
|
686
690
|
* IBL cubemap is what an escaping ray sees; `probeIndoor` keeps only the probes under a roof (an open share of the sphere of at most 15 %) — the
|
|
687
691
|
* outdoors reflects the sky through the texel's baked sky visibility instead; `volumeSpacing` (metres, 0 = none) bakes THE LIGHT GRID for movers — `<name>.lgrid`, named in the .bake's `volume`: per cell
|
|
688
692
|
* in free space an ambient cube without the direct sun, links between neighbours a ray connects (what `setAmbientCube`
|
|
689
|
-
* is fed from); `minSize` (metres, 0 = off) leaves every instance smaller than that along every axis out of the atlas — an occluder
|
|
690
|
-
* only, `"small": true` + its `light` (an ambient cube baked where it stands) in the .bake, applied by lightmapObjectApply; `
|
|
693
|
+
* is fed from); `minSize` (metres, 0 = off) leaves every record (a mesh of an instance) smaller than that along every axis out of the atlas — an occluder
|
|
694
|
+
* only, `"small": true` + its `light` (an ambient cube baked where it stands) in the .bake, applied by lightmapObjectApply; `thinTexels` (0 = off) does the same to every
|
|
695
|
+
* record none of whose lightmap charts is wider than that many texels across its short side — a railing's tubes, a cable, a pipe run: a strip of 1-3 texels around a tube
|
|
696
|
+
* cannot hold the light's change around it and its uv seam shows as a step; `objectSlab` (metres, 0 = one cube an object) gives a long one of
|
|
697
|
+
* those round(length / objectSlab) cubes along the longest axis of its bounds (16 at most), blended per pixel along it; BY HAND, the same way out of
|
|
698
|
+
* the atlas: every record whose key (`<node key>@<ordinal>`) `cubeKeys` matches — an ECMAScript regular expression, searched, case-sensitive ("" = off; one
|
|
699
|
+
* that does not compile fails the bake: "error: bad --cube pattern") — and every record of the entities in `cubeIds` (of `entityIds`: a node's
|
|
700
|
+
* `lightmap: 'cube'`), whatever their size or charts; `probeLayout` 0 = ROOMS (indoor points slide to their
|
|
691
701
|
* room's middle and merge: one probe per room), 1 = GRID; `probeBox` = 6 floats, min xyz + max xyz, keeps the grid inside that box — null = the
|
|
692
702
|
* receivers' bounds) and the grid into the .bake; `debugDir` (optional) gets the float layers per page. Synchronous; progress on stdout as
|
|
693
703
|
* "[lightmap] …" lines, "[lightmap] done" on success. @c creatorHasLightmapBake */
|
|
@@ -697,7 +707,7 @@ declare global {
|
|
|
697
707
|
sunRays: u32, skyRays: u32, lightRays: u32, lightRadius: f32, sunAngleDeg: f32, bias: f32, seed: u32,
|
|
698
708
|
bounceRays: u32, bounces: u32, patchSamples: u32, split: boolean, denoise: u32, filter: f32,
|
|
699
709
|
probeSpacing: f32, probeSize: u32, probeRays: u32, probeIndoor: boolean, probeLayout: u32, probeBox: Float32Array | null, minSize: f32, volumeSpacing: f32, emissiveNits: f32, detailTexels: u32, debugDir?: string,
|
|
700
|
-
transmit?: string): boolean
|
|
710
|
+
transmit?: string, tracer?: u32, denoiser?: u32, thinTexels?: f32, objectSlab?: f32, cubeKeys?: string, cubeIds?: Uint32Array): boolean
|
|
701
711
|
/** Lightmap consumption: the next createGlb takes lightmap.filamat (the material behind that instance id) for its
|
|
702
712
|
* OPAQUE materials and the masked twin for its MASK materials instead of the ubershader, and keeps TEXCOORD_1;
|
|
703
713
|
* BLEND (glass) materials always keep the ubershader. 0xFFFFFFFF clears. The same call arms the foliage tier. @custom */
|
|
@@ -724,17 +734,22 @@ declare global {
|
|
|
724
734
|
* Returns the probe count; an invalid texture id clears. */
|
|
725
735
|
lightmapProbesSet?(atlasTextureId: u32, oct: u32, levels: u32, perRow: u32, count: u32,
|
|
726
736
|
lox: f32, loy: f32, loz: f32, sx: f32, sy: f32, sz: f32): u32
|
|
727
|
-
/** Bind the light page + the aux page +
|
|
728
|
-
*
|
|
729
|
-
*
|
|
737
|
+
/** Bind the light page + the aux page + a RECORD's rect (uv1 * [sx, sy] + [ox, oy]) and move the renderables it went on
|
|
738
|
+
* to light channel 1 alone — a baked static takes nothing from the real-time sun and lamps (channel 0). The bake's
|
|
739
|
+
* unit is the mesh: a GLB instance is one record per primitive (`<key>@<ordinal>` in the .bake); `ordinal` >= 0 binds
|
|
740
|
+
* that primitive (its index over the file's meshes) of every node that draws it, on a material instance of its own;
|
|
741
|
+
* -1 (the default) = every lightmap-material primitive of the entity (a Mesh entity, a terrain). Returns how many
|
|
730
742
|
* material instances took it (0 = not loaded lightmapped). */
|
|
731
|
-
lightmapApply?(entityId: u32, lightTextureId: u32, auxTextureId: u32, sx: f32, sy: f32, ox: f32, oy: f32,
|
|
743
|
+
lightmapApply?(entityId: u32, lightTextureId: u32, auxTextureId: u32, sx: f32, sy: f32, ox: f32, oy: f32, ordinal?: i32): u32
|
|
732
744
|
/** A SMALL STATIC the atlas left out (the bake's `minSize`: `small` + `light` in the .bake): no rect — the light
|
|
733
|
-
* baked where it stands goes into its material parameters. `data` =
|
|
734
|
-
* bounds
|
|
735
|
-
* (
|
|
736
|
-
*
|
|
737
|
-
|
|
745
|
+
* baked where it stands goes into its material parameters. `data` = 7 + 24 n floats: axis (0 x, 1 y, 2 z), from, to
|
|
746
|
+
* (its bounds along the axis), n (slabs, 1..16); then n AMBIENT CUBES, slab by slab from `from` — per side of the slab's
|
|
747
|
+
* box (+x -x +y -y +z -z) the irradiance rgb in lux and the sky visibility; then probe a, probe b, a's weight (65535 =
|
|
748
|
+
* no probe, 65534 = the sky). Any other count is refused. The shader blends the two slabs around a pixel along the
|
|
749
|
+
* axis. The textures = any page of the level (the probes' records sit under every aux page). `ordinal` as
|
|
750
|
+
* `lightmapApply` (a small record of a GLB is one primitive). Returns the material instances that took it (0 = not
|
|
751
|
+
* loaded lightmapped). @count count=data */
|
|
752
|
+
lightmapObjectApply?(entityId: u32, lightTextureId: u32, auxTextureId: u32, data: Float32Array, ordinal?: i32): u32
|
|
738
753
|
/** BAKED AMBIENT LIGHT for a mover, or anything on a standard or custom LIT shader: every renderable of the GLB
|
|
739
754
|
* instance (or the Mesh entity) takes its diffuse indirect light from `data`'s ambient cube instead of the scene's
|
|
740
755
|
* IBL, and the IBL's reflections through the sky visibility. `data` = 25 or 26 floats: six sides (+x -x +y -y +z -z) of
|
|
@@ -751,6 +766,12 @@ declare global {
|
|
|
751
766
|
* `lightVolumeSample(x, y, z)` = that cube at a point (26 floats, null = no grid or no valid cell near), for tools;
|
|
752
767
|
* `lightVolumeClear()` drops the grid and every mover's cube. */
|
|
753
768
|
lightVolumeLoad?(fetchId: bytes): boolean
|
|
769
|
+
/** THE PACKED LIGHTMAP UV1 (`<name>-uv1.bin`, the .bake's `uv1`): the bake packs every file's lightmap charts into a
|
|
770
|
+
* square of their own and its rects map THAT uv1. `lightmapUv1Load(fetchId)` hands the engine the fetched sidecar
|
|
771
|
+
* (false = not one); every GLB a rect is applied to then takes its file's packed uv1 into TEXCOORD_1, once per asset
|
|
772
|
+
* (assets bound before the call are applied again). `lightmapUv1Clear()` drops it: the models get their own uv1 back. */
|
|
773
|
+
lightmapUv1Load?(fetchId: bytes): boolean
|
|
774
|
+
lightmapUv1Clear?(): void
|
|
754
775
|
lightVolumeTrack?(entityId: u32, on: boolean, fixed?: boolean): void
|
|
755
776
|
lightVolumeClear?(): void
|
|
756
777
|
/** @out 26 @out 26 */
|
package/src/gl/Lightmap.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
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
|
|
6
|
-
// `lecodes lightmap bake` (the
|
|
7
|
-
// on the
|
|
6
|
+
// `lecodes lightmap bake` (the bake tool lecodes-bake — the windowless host with the bake built in —
|
|
7
|
+
// describes the live scene to creator-bake, which traces it on the GPU and writes the atlas), and
|
|
8
|
+
// `Lightmap.load` applies it at runtime:
|
|
8
9
|
//
|
|
9
10
|
// const crate = await Model.load(asset('./assets/props/crate.glb'), { lightmap: true })
|
|
10
11
|
// Lightmap.add(holder) // a Model, a Mesh, or a node holding them
|
|
@@ -67,6 +68,11 @@ export type LightmapLoadOptions = {
|
|
|
67
68
|
* `{ through, diffuse }` adds the share the fibres SCATTER - the cloth's underside glows like a matte panel and lights
|
|
68
69
|
* what is under it by the solid angle it fills, with no picture. Dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
|
|
69
70
|
transmit?: Record<string, LightmapTransmit>
|
|
71
|
+
/** FOR THE BAKE: CUBES BY NAME — regular expressions (ECMAScript, case-sensitive) searched in every record's key
|
|
72
|
+
* (`<node key>@<ordinal>`: `"SM_pipe"` takes every pipe node, `"@1$"` the second mesh of each): a record that matches
|
|
73
|
+
* leaves the atlas like a small or thin one — an occluder lit by ambient cubes baked where it stands — whatever its size
|
|
74
|
+
* or charts. Added to the CLI's `--cube` patterns (both apply). */
|
|
75
|
+
cube?: string[]
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
/** one `transmit` entry (LightmapLoadOptions.transmit): the straight share, or both shares */
|
|
@@ -98,11 +104,17 @@ export type LightmapFiles = {
|
|
|
98
104
|
/** THE LIGHT GRID for movers (`<stem>.lgrid`, `lecodes lightmap bake --volume`): the baked ambient light of every
|
|
99
105
|
* place a mover can be. Absent = movers keep the scene's IBL. */
|
|
100
106
|
volume?: string
|
|
107
|
+
/** THE PACKED LIGHTMAP UV1 (`<stem>-uv1.bin`, the .bake's `uv1`): the bake packs every mesh's lightmap charts into a
|
|
108
|
+
* square of their own and its rects map that uv1 — the engine swaps it into the models' TEXCOORD_1 first. Required
|
|
109
|
+
* when the bake names one. */
|
|
110
|
+
uv1?: string
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
/** The lightmap material's data views (`Lightmap.debug`); "direct" / "indirect" bind a `--split` bake's pages instead. */
|
|
104
114
|
export type LightmapDebugMode = "off" | "irradiance" | "albedo" | "normal" | "shadingNormal" | "share" | "skyVis" | "atlas" | "direction" | "lux" | "relief" | "direct" | "indirect" | "probe" | "probeMap" | "skyPath" | "probePath"
|
|
105
115
|
const DENOISE_LEVELS = { none: 0, indirect: 1, "no-sun": 2, all: 3 } as const
|
|
116
|
+
const TRACERS = { auto: 0, optix: 1, vulkan: 2 } as const // CBAKE_TRACER_*
|
|
117
|
+
const DENOISERS = { auto: 0, optix: 1, oidn: 2 } as const // CBAKE_DENOISER_*
|
|
106
118
|
const DEBUG_MODES: LightmapDebugMode[] = [ "off", "irradiance", "albedo", "normal", "shadingNormal", "share", "skyVis", "atlas", "direction", "lux", "relief", "direct", "indirect", "probe", "probeMap", "skyPath", "probePath" ]
|
|
107
119
|
const PAGE_VIEWS: Record<number, "direct" | "indirect"> = { 11: "direct", 12: "indirect" }
|
|
108
120
|
|
|
@@ -125,15 +137,32 @@ export type LightmapProbe = {
|
|
|
125
137
|
distance: number
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
/** `ground` = a terrain node (built with `Material.terrainLightmap()`): one rect, bound to its chunks by the host
|
|
129
|
-
|
|
140
|
+
/** `ground` = a terrain node (built with `Material.terrainLightmap()`): one rect, bound to its chunks by the host;
|
|
141
|
+
* `cube` = every record of it bakes as ambient cubes (`LightmapAddOptions.cube`, a scene node's `lightmap: 'cube'`) */
|
|
142
|
+
type Entry = { key: string, model?: Model, mesh?: Mesh, ground?: Node, cube?: boolean }
|
|
143
|
+
|
|
144
|
+
/** `Lightmap.add`'s options */
|
|
145
|
+
export type LightmapAddOptions = {
|
|
146
|
+
/** Bake it as AMBIENT CUBES instead of atlas texels: an occluder (its shadow and bounce stay in the atlas) whose own
|
|
147
|
+
* light is baked where it stands — the way the bake treats small and thin objects (`--min-texels`, `--thin-texels`),
|
|
148
|
+
* here by hand: a pipe run, a fence, a prop whose charts show their seams. A long one takes a slab of cubes per metre
|
|
149
|
+
* (`--object-slab`). Default false. */
|
|
150
|
+
cube?: boolean
|
|
151
|
+
}
|
|
130
152
|
|
|
131
153
|
type BakeConfig = {
|
|
132
154
|
outDir: string, stem: string, size?: number, texel?: number, pages?: number,
|
|
133
155
|
sunRays?: number, skyRays?: number, lightRays?: number, lightRadius?: number, sunAngle?: number, bias?: number, seed?: number,
|
|
134
156
|
bounceRays?: number, bounces?: number, patchSamples?: number, split?: boolean,
|
|
135
|
-
/**
|
|
157
|
+
/** 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"`. */
|
|
136
158
|
denoise?: "none" | "indirect" | "no-sun" | "all",
|
|
159
|
+
/** Which GPU API traces: `"auto"` (the default) = OptiX where an NVIDIA driver with OptiX opens, else Vulkan ray query — any card
|
|
160
|
+
* with hardware ray tracing (RTX, Radeon RX 6000+, Arc) with its driver alone; `"optix"` / `"vulkan"` by name (the bake fails
|
|
161
|
+
* when it cannot open). */
|
|
162
|
+
tracer?: "auto" | "optix" | "vulkan",
|
|
163
|
+
/** Which AI denoiser `denoise` runs: `"auto"` (the default) = OptiX's on an NVIDIA card, else Intel Open Image Denoise (its CUDA /
|
|
164
|
+
* HIP / CPU device); `"optix"` / `"oidn"` by name. Independent of the tracer. One that cannot open is a warning: the pages stay as traced. */
|
|
165
|
+
denoiser?: "auto" | "optix" | "oidn",
|
|
137
166
|
/** The texel's reconstruction filter radius in texels: 1 (the default) = a ±1 texel tent, 0.5 = within the texel, 0 = the centre alone. */
|
|
138
167
|
filter?: number,
|
|
139
168
|
/** Reflection probes: the grid spacing in metres (4; 0 = none), a probe face's edge in texels (32) and the paths per probe texel (128). */
|
|
@@ -153,6 +182,19 @@ type BakeConfig = {
|
|
|
153
182
|
* how many texels it gets (a fire extinguisher of 0.55 m: 300 charts, the largest 29 texels - every chart edge a step).
|
|
154
183
|
* 0 = everything bakes. Default 12. */
|
|
155
184
|
minTexels?: number,
|
|
185
|
+
/** ROUND AND NARROW objects leave the atlas the same way: an instance none of whose lightmap charts is wider than this
|
|
186
|
+
* many texels across its short side (at `texel`) — a railing's tubes, a cable, a pipe run, blinds. A strip of 1-3 texels
|
|
187
|
+
* around a tube cannot hold the light's change around it and its uv seam shows as a step; the ambient cube baked where
|
|
188
|
+
* it stands is smooth. A flat object has a wide chart somewhere and stays. 0 = off. Default 4 (20 cm at 5 cm texels). */
|
|
189
|
+
thinTexels?: number,
|
|
190
|
+
/** A LONG object the atlas left out takes its light in SLABS of this many metres along the longest axis of its bounds
|
|
191
|
+
* (round(length / objectSlab), 16 at most) — a 6 m pipe run, a railing along a wall: one end under a lamp, the other in
|
|
192
|
+
* the dark, where one cube gave all of it the mean. The shader blends the two slabs around each pixel along that axis.
|
|
193
|
+
* 0 = one cube an object. Default 1. */
|
|
194
|
+
objectSlab?: number,
|
|
195
|
+
/** CUBES BY NAME (the CLI's `--cube`): regular expressions over the records' keys, joined with the level's own
|
|
196
|
+
* (`LightmapLoadOptions.cube`) into one — each wrapped in `(?: )`, `|` between. */
|
|
197
|
+
cube?: string[],
|
|
156
198
|
/** THE LIGHT GRID for movers: the cell size in metres (0.75 of it tall); 0 = none. Default 1. */
|
|
157
199
|
volumeSpacing?: number,
|
|
158
200
|
/** A SMALL SURFACE GETS A FINER TEXEL: an instance whose surface is under this many atlas texels (area / texel²) is laid
|
|
@@ -170,9 +212,12 @@ type BakeConfig = {
|
|
|
170
212
|
debugDir?: string,
|
|
171
213
|
}
|
|
172
214
|
|
|
173
|
-
/** a small static's light where it stands (creator-bake objects.h):
|
|
174
|
-
*
|
|
175
|
-
|
|
215
|
+
/** a small static's light where it stands (creator-bake objects.h): its SLABS along `axis` (0 x, 1 y, 2 z; its bounds run
|
|
216
|
+
* from `from` to `to` along it) — one for a small one, round(length / objectSlab) for a long one — and per slab per side
|
|
217
|
+
* of its box (+x -x +y -y +z -z) the irradiance rgb in lux + the sky visibility; probes = a, b, a's weight (-1 = none,
|
|
218
|
+
* -2 = the sky) */
|
|
219
|
+
type BakeObjectLight = { at: number[], axis: number, from: number, to: number, cubes: number[][][], probes: number[] }
|
|
220
|
+
/** a RECORD of the .bake: a registered node's key, or `<key>@<ordinal>` for one mesh of a model (creator-bake atlas.h) */
|
|
176
221
|
type BakeInstance = { key: string, receiver: boolean, small?: boolean, light?: BakeObjectLight, page?: number, rect?: number[], st?: number[] }
|
|
177
222
|
type BakeProbes = {
|
|
178
223
|
file: string, size: number, levels: number, count: number, oct: number, tile: number[], perRow: number, atlas: number[],
|
|
@@ -189,6 +234,8 @@ type BakeData = {
|
|
|
189
234
|
probes?: BakeProbes | null,
|
|
190
235
|
/** the light grid (creator-bake volume.h): its file and lattice */
|
|
191
236
|
volume?: { file: string, n: number[], origin: number[], cell: number[], valid: number } | null,
|
|
237
|
+
/** the packed lightmap uv1 the rects map (creator-bake atlas.h), null = the models' own */
|
|
238
|
+
uv1?: string | null,
|
|
192
239
|
sun?: { direction: number[], color: number[], lux: number } | null, sunMap?: boolean, sunMapLevels?: number, mips?: number, emissiveNits?: number,
|
|
193
240
|
instances: BakeInstance[],
|
|
194
241
|
}
|
|
@@ -225,16 +272,17 @@ export class Lightmap {
|
|
|
225
272
|
* both receivers and occluders in the bake (a model without lightmap UVs, or with materials the lightmap shader
|
|
226
273
|
* declines — alpha cards — is an occluder alone; a terrain is ONE rect, built with `Material.terrainLightmap()`).
|
|
227
274
|
* `key` names the entry in the .bake (default: the node's name + a running number, `container#3`); pass one when
|
|
228
|
-
* names are not stable. */
|
|
229
|
-
static add(node: Node, key?: string): Node {
|
|
275
|
+
* names are not stable. `{ cube: true }` bakes it as ambient cubes instead of atlas texels (`LightmapAddOptions`). */
|
|
276
|
+
static add(node: Node, key?: string, options: LightmapAddOptions = {}): Node {
|
|
230
277
|
const base = key ?? `${node.name || "node"}#${Lightmap.next(node.name || "node")}`
|
|
278
|
+
const cube = options.cube === true
|
|
231
279
|
const found: Entry[] = []
|
|
232
280
|
const walk = (n: Node): void => {
|
|
233
281
|
// the node `Terrain.create` returned carries its terrain (Terrain.of reads the same field): the host keeps the
|
|
234
282
|
// terrain's triangles under this node's id for the bake and binds the rect to its chunks
|
|
235
|
-
if ((n as unknown as { _terrain?: unknown })._terrain) { found.push({ key: base, ground: n }); return }
|
|
236
|
-
if (n instanceof Model) { found.push({ key: base, model: n }); return }
|
|
237
|
-
if (n instanceof Mesh) { found.push({ key: base, mesh: n }); return }
|
|
283
|
+
if ((n as unknown as { _terrain?: unknown })._terrain) { found.push({ key: base, ground: n, cube }); return }
|
|
284
|
+
if (n instanceof Model) { found.push({ key: base, model: n, cube }); return }
|
|
285
|
+
if (n instanceof Mesh) { found.push({ key: base, mesh: n, cube }); return }
|
|
238
286
|
for (const c of n.children) walk(c)
|
|
239
287
|
}
|
|
240
288
|
walk(node)
|
|
@@ -244,13 +292,13 @@ export class Lightmap {
|
|
|
244
292
|
return node
|
|
245
293
|
}
|
|
246
294
|
|
|
247
|
-
/** @internal Scene-file runtime: one model/mesh node under its path key (no subtree walk)
|
|
248
|
-
static _register(node: Model | Mesh, key: string): void {
|
|
249
|
-
Lightmap.entries.push(node instanceof Model ? { key, model: node } : { key, mesh: node })
|
|
295
|
+
/** @internal Scene-file runtime: one model/mesh node under its path key (no subtree walk); `cube` = `lightmap: 'cube'`. */
|
|
296
|
+
static _register(node: Model | Mesh, key: string, cube = false): void {
|
|
297
|
+
Lightmap.entries.push(node instanceof Model ? { key, model: node, cube } : { key, mesh: node, cube })
|
|
250
298
|
}
|
|
251
299
|
/** @internal Scene-file runtime: a terrain node - a receiver with one rect (the host keeps its triangles for the bake). */
|
|
252
|
-
static _registerGround(node: Node, key: string): void {
|
|
253
|
-
Lightmap.entries.push({ key, ground: node })
|
|
300
|
+
static _registerGround(node: Node, key: string, cube = false): void {
|
|
301
|
+
Lightmap.entries.push({ key, ground: node, cube })
|
|
254
302
|
}
|
|
255
303
|
|
|
256
304
|
private static next(name: string): number {
|
|
@@ -308,12 +356,31 @@ export class Lightmap {
|
|
|
308
356
|
console.log(`[lightmap] light grid ${data.volume.n.join(" x ")} cells of ${data.volume.cell.map((v) => v.toFixed(2)).join(" x ")} m, ${Lightmap.movers.size} mover(s)`)
|
|
309
357
|
}
|
|
310
358
|
|
|
359
|
+
/** THE PACKED UV1 the bake's rects map (the .bake's `uv1`), into the host BEFORE any rect is applied: the host swaps it
|
|
360
|
+
* into the models' TEXCOORD_1 as their rects go on. A bake without one clears the last (the models' own uv1 back).
|
|
361
|
+
* False = the bake needs it and it cannot be had - its rects would read the wrong texels. */
|
|
362
|
+
private static async loadUv1(data: BakeData, files: LightmapFiles): Promise<boolean> {
|
|
363
|
+
if (!data.uv1) { _creator.lightmapUv1Clear?.(); return true }
|
|
364
|
+
if (!files.uv1) { console.warn(`Lightmap: the bake packed the models' lightmap charts (${data.uv1}) but the scene does not list it — add uv1: asset('…/${data.uv1}') to env.lightmap; the bake is not applied until then`); return false }
|
|
365
|
+
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
|
+
try {
|
|
367
|
+
const resp = await fetch(files.uv1, { useOnce: true })
|
|
368
|
+
if (resp.status >= 400) { console.warn(`Lightmap: no packed uv1 at ${files.uv1} (HTTP ${resp.status}) — the bake is not applied`); return false }
|
|
369
|
+
if (!_creator.lightmapUv1Load((resp as unknown as { _h: Handle })._h.id)) { console.warn(`Lightmap: ${files.uv1} is not a packed uv1 — rebake`); return false }
|
|
370
|
+
} catch (e) {
|
|
371
|
+
console.warn(`Lightmap: could not read ${files.uv1}: ${String(e)}`)
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
return true
|
|
375
|
+
}
|
|
376
|
+
|
|
311
377
|
/** Forget every registration (a scene rebuild). */
|
|
312
378
|
static clear(): void {
|
|
313
379
|
for (const l of Light._points) l._holdDark(false)
|
|
314
380
|
Lightmap.movers.clear()
|
|
315
381
|
Lightmap.volumeOn = false
|
|
316
382
|
_creator.lightVolumeClear?.()
|
|
383
|
+
_creator.lightmapUv1Clear?.()
|
|
317
384
|
Lightmap.entries = []
|
|
318
385
|
Lightmap.ordinals.clear()
|
|
319
386
|
Lightmap.bound = null
|
|
@@ -350,7 +417,7 @@ export class Lightmap {
|
|
|
350
417
|
static async load(scene: Scene, files: LightmapFiles, options: LightmapLoadOptions = {}): Promise<LightmapInfo | null> {
|
|
351
418
|
Lightmap.scene = scene
|
|
352
419
|
const bake = bakeConfig()
|
|
353
|
-
if (bake) { Lightmap.bake(bake, options.emissiveNits, options.emissiveBake ?? true, options.transmit); return null }
|
|
420
|
+
if (bake) { Lightmap.bake(bake, options.emissiveNits, options.emissiveBake ?? true, options.transmit, options.cube); return null }
|
|
354
421
|
if (!_creator.lightmapApply) {
|
|
355
422
|
if (!Lightmap.warned) { Lightmap.warned = true; console.warn("Lightmap: this host has no lightmap support — rendering without the bake") }
|
|
356
423
|
return null
|
|
@@ -369,6 +436,7 @@ export class Lightmap {
|
|
|
369
436
|
return null
|
|
370
437
|
}
|
|
371
438
|
if (!data.pages || !(data.lightScale > 0)) return null // the CLI's placeholder: nothing baked yet
|
|
439
|
+
if (!(await Lightmap.loadUv1(data, files))) return null // the rects map the packed uv1: it goes in first
|
|
372
440
|
const pages = Math.max(1, data.pages)
|
|
373
441
|
const lightFiles = list(files.light), auxFiles = list(files.aux)
|
|
374
442
|
if (lightFiles.length === 0 || auxFiles.length === 0) { console.warn("Lightmap: the scene passes no `light` / `aux` atlas"); return null }
|
|
@@ -410,9 +478,15 @@ export class Lightmap {
|
|
|
410
478
|
const declined = new Set<string>()
|
|
411
479
|
for (const inst of data.instances) if (!inst.receiver && !inst.small) declined.add(inst.key)
|
|
412
480
|
const probeId = (k: number) => k === -2 ? 65534 : k < 0 ? 65535 : k
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
481
|
+
// lightmapObjectApply's numbers: axis, from, to, n, then n slabs x six sides x (rgb, sky), then probe a, probe b, a's weight
|
|
482
|
+
const objectData = (l: BakeObjectLight) =>
|
|
483
|
+
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
|
+
// a bake from before the slabs (`light.cube`, one cube): not read — its small statics stay unlit until it is rebaked
|
|
485
|
+
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: ${files.data} has small statics without \`light.cubes\` (baked before the slabbed object light) — they stay unlit; rebake with \`lecodes lightmap bake\``)
|
|
487
|
+
// THE BAKE'S UNIT IS THE MESH: a model's primitives are records of their own, `<key>@<ordinal>` (ordinal = the primitive's
|
|
488
|
+
// index over the file's meshes) - a rect, or a small static's light, each; the host binds record k to that primitive
|
|
489
|
+
const parts = Lightmap.recordsOf(data.instances)
|
|
416
490
|
let smallLit = 0
|
|
417
491
|
const byEntity = new Map<number, Entry>()
|
|
418
492
|
let applied = 0
|
|
@@ -420,19 +494,20 @@ export class Lightmap {
|
|
|
420
494
|
for (const e of Lightmap.entries) {
|
|
421
495
|
const node = e.model ?? e.mesh ?? e.ground!
|
|
422
496
|
byEntity.set(node.id, e)
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
let took = 0
|
|
426
|
-
for (const [ k, inst ] of
|
|
427
|
-
if (inst.small
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
497
|
+
const records = parts.get(e.key)
|
|
498
|
+
if (records) {
|
|
499
|
+
let took = 0, receivers = 0
|
|
500
|
+
for (const [ k, inst ] of records) {
|
|
501
|
+
if (inst.small) {
|
|
502
|
+
if (lit(inst.light) && _creator.lightmapObjectApply && n > 0 && _creator.lightmapObjectApply(node.id, light[0]._h.id, aux[0]._h.id, objectData(inst.light), k) > 0) { took++; smallLit++ }
|
|
503
|
+
} else if (inst.receiver) {
|
|
504
|
+
receivers++
|
|
505
|
+
if (!inst.st || inst.page === undefined || inst.page >= n) { missing.push(inst.key); continue }
|
|
506
|
+
if (_creator.lightmapApply(node.id, light[inst.page]._h.id, aux[inst.page]._h.id, inst.st[0], inst.st[1], inst.st[2], inst.st[3], k) > 0) took++
|
|
432
507
|
}
|
|
433
508
|
}
|
|
434
|
-
if (took
|
|
435
|
-
else
|
|
509
|
+
if (took > 0) applied++
|
|
510
|
+
else if (receivers > 0) console.warn(`Lightmap: ${e.key} (${records.size} mesh record(s)) took no rect — not loaded lightmapped (Model.load { lightmap: true }), or the host binds no records (rebuild it)`)
|
|
436
511
|
continue
|
|
437
512
|
}
|
|
438
513
|
const r = rects.get(e.key)
|
|
@@ -440,10 +515,9 @@ export class Lightmap {
|
|
|
440
515
|
if (tiny) {
|
|
441
516
|
// no rect: the light baked where it stands (any page's samplers — the probes' records sit under every aux page)
|
|
442
517
|
const l = tiny.light
|
|
443
|
-
if (!l || !_creator.lightmapObjectApply || n === 0) continue
|
|
518
|
+
if (!lit(l) || !_creator.lightmapObjectApply || n === 0) continue
|
|
444
519
|
if (e.mesh) e.mesh.material = Lightmap.meshMaterial(e.mesh)
|
|
445
|
-
|
|
446
|
-
if (_creator.lightmapObjectApply(node.id, light[0]._h.id, aux[0]._h.id, d) > 0) smallLit++
|
|
520
|
+
if (_creator.lightmapObjectApply(node.id, light[0]._h.id, aux[0]._h.id, objectData(l)) > 0) smallLit++
|
|
447
521
|
continue
|
|
448
522
|
}
|
|
449
523
|
if (!r && declined.has(e.key)) continue // glass / a declined material: the bake knows it as an occluder - the light grid lights it (loadVolume)
|
|
@@ -455,19 +529,19 @@ export class Lightmap {
|
|
|
455
529
|
if (took === 0) { console.warn(`Lightmap: ${e.key} was not loaded lightmapped (Model.load { lightmap: true } / Material.lightmap())`); continue }
|
|
456
530
|
applied++
|
|
457
531
|
}
|
|
458
|
-
if (small.size) console.log(`[lightmap] ${small.size} small
|
|
459
|
-
if (missing.length) console.warn(`Lightmap: ${missing.length} static(s) have no rect in the bake (${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "…" : ""}) — rebake`)
|
|
532
|
+
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
|
+
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`)
|
|
460
534
|
Lightmap.bound = { data, rects, byEntity, files, pages: n, aux, light, probes, probeAtlas, showing: "light" }
|
|
461
535
|
await Lightmap.loadVolume(data, files)
|
|
462
536
|
return { size: data.size, texel: data.texel, pages: n, applied, total: rects.size, missing }
|
|
463
537
|
}
|
|
464
538
|
|
|
465
|
-
/** The bake's `<key
|
|
466
|
-
private static
|
|
539
|
+
/** The bake's `<key>@k` records by their node key: a model's meshes (k = the primitive's ordinal in its file). */
|
|
540
|
+
private static recordsOf(instances: BakeInstance[]): Map<string, Map<number, BakeInstance>> {
|
|
467
541
|
const out = new Map<string, Map<number, BakeInstance>>()
|
|
468
542
|
const registered = new Set(Lightmap.entries.map((e) => e.key))
|
|
469
543
|
for (const inst of instances) {
|
|
470
|
-
const m = /^(.*)
|
|
544
|
+
const m = /^(.*)@(\d+)$/.exec(inst.key)
|
|
471
545
|
if (!m || !registered.has(m[1])) continue
|
|
472
546
|
let g = out.get(m[1])
|
|
473
547
|
if (!g) { g = new Map(); out.set(m[1], g) }
|
|
@@ -502,7 +576,7 @@ export class Lightmap {
|
|
|
502
576
|
b[kind] = tex
|
|
503
577
|
}
|
|
504
578
|
b.showing = kind
|
|
505
|
-
const parts = Lightmap.
|
|
579
|
+
const parts = Lightmap.recordsOf(b.data.instances)
|
|
506
580
|
for (const [ id, e ] of b.byEntity) {
|
|
507
581
|
for (const [ k, inst ] of parts.get(e.key) ?? []) {
|
|
508
582
|
if (inst.receiver && inst.st && inst.page !== undefined && inst.page < b.pages) _creator.lightmapApply(id, tex[inst.page]._h.id, b.aux[inst.page]._h.id, inst.st[0], inst.st[1], inst.st[2], inst.st[3], k)
|
|
@@ -553,7 +627,7 @@ export class Lightmap {
|
|
|
553
627
|
static probe(screenX: number, screenY: number): LightmapProbe | null {
|
|
554
628
|
const b = Lightmap.bound
|
|
555
629
|
if (!b || !_creator.pickTriangle) return null
|
|
556
|
-
type Hit = { entity: number, nodeName: string, material: string, point: number[], distance: number, uv1?: [number, number],
|
|
630
|
+
type Hit = { entity: number, nodeName: string, material: string, point: number[], distance: number, uv1?: [number, number], lightmapOrdinal?: number }
|
|
557
631
|
// the nearest hit over the registered statics: one pick per entry (the all-instances pick, entity 0, walks
|
|
558
632
|
// every GLB instance including the off-scene templates and is what a scene file's clones confuse)
|
|
559
633
|
let hit: Hit | null = null
|
|
@@ -564,7 +638,7 @@ export class Lightmap {
|
|
|
564
638
|
if (!hit) { console.log(`[probe] nothing baked under ${screenX.toFixed(0)},${screenY.toFixed(0)}`); return null }
|
|
565
639
|
const entry = b.byEntity.get(hit.entity)
|
|
566
640
|
if (!entry) { console.log(`[probe] hit entity ${hit.entity} node "${hit.nodeName}" at ${hit.point.map((c) => c.toFixed(2)).join(",")} is not a registered static`); return null }
|
|
567
|
-
const hitKey = hit.
|
|
641
|
+
const hitKey = hit.lightmapOrdinal !== undefined && b.rects.has(`${entry.key}@${hit.lightmapOrdinal}`) ? `${entry.key}@${hit.lightmapOrdinal}` : entry.key
|
|
568
642
|
const inst = b.rects.get(hitKey)
|
|
569
643
|
if (!inst || !inst.st) { console.log(`[probe] ${hitKey}: no rect in the bake`); return null }
|
|
570
644
|
if (!hit.uv1) { console.log(`[probe] ${entry.key}: the hit primitive carries no TEXCOORD_1`); return null }
|
|
@@ -593,10 +667,14 @@ export class Lightmap {
|
|
|
593
667
|
return m
|
|
594
668
|
}
|
|
595
669
|
|
|
596
|
-
private static bake(cfg: BakeConfig, levelNits?: number, emissiveBake = true, transmit?: Record<string, LightmapTransmit
|
|
597
|
-
if (!_creator.lightmapBake) { console.error("[lightmap] error: this host has no bake module (
|
|
598
|
-
const
|
|
670
|
+
private static bake(cfg: BakeConfig, levelNits?: number, emissiveBake = true, transmit?: Record<string, LightmapTransmit>, levelCube?: string[]): void {
|
|
671
|
+
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
|
+
const idOf = (e: Entry): number => (e.model ?? e.mesh ?? e.ground!).id
|
|
673
|
+
const ids = Uint32Array.from(Lightmap.entries.map(idOf))
|
|
599
674
|
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 env.lightmap.cube, one regex
|
|
676
|
+
const cubeIds = Uint32Array.from(Lightmap.entries.filter((e) => e.cube).map(idOf))
|
|
677
|
+
const cubeKeys = [ ...(cfg.cube ?? []), ...(levelCube ?? []) ].filter((p) => p !== "").map((p) => `(?:${p})`).join("|")
|
|
600
678
|
if (ids.length === 0) { console.error("[lightmap] error: nothing registered — call Lightmap.add on the static props before Lightmap.load"); return }
|
|
601
679
|
// a `baked: false` light is held dark for the bake — the engine bakes every lit point light it finds
|
|
602
680
|
for (const l of Light._points) l._holdDark(false) // a bake started over a loaded level
|
|
@@ -609,7 +687,8 @@ export class Lightmap {
|
|
|
609
687
|
cfg.sunRays ?? 64, cfg.skyRays ?? 2048, cfg.lightRays ?? 64, cfg.lightRadius ?? 0.05, cfg.sunAngle ?? 0.53, cfg.bias ?? 0.01, cfg.seed ?? 1,
|
|
610
688
|
cfg.bounceRays ?? 128, cfg.bounces ?? 3, cfg.patchSamples ?? 64, cfg.split ?? false, DENOISE_LEVELS[cfg.denoise ?? "no-sun"], cfg.filter ?? 1,
|
|
611
689
|
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 ?? "",
|
|
612
|
-
Object.entries(transmit ?? {}).map(([name, share]) => typeof share === "number" ? `${name}=${share}` : `${name}=${share.through ?? 0},${share.diffuse ?? 0}`).join(";")
|
|
690
|
+
Object.entries(transmit ?? {}).map(([name, share]) => typeof share === "number" ? `${name}=${share}` : `${name}=${share.through ?? 0},${share.diffuse ?? 0}`).join(";"),
|
|
691
|
+
TRACERS[cfg.tracer ?? "auto"] ?? 0, DENOISERS[cfg.denoiser ?? "auto"] ?? 0, cfg.thinTexels ?? 4, cfg.objectSlab ?? 1, cubeKeys, cubeIds)
|
|
613
692
|
for (const l of live) _creator.setLightIntensity?.(l.id, l.intensity)
|
|
614
693
|
}
|
|
615
694
|
}
|
package/src/gl/Model.ts
CHANGED
|
@@ -20,8 +20,8 @@ export interface TrianglePick {
|
|
|
20
20
|
mesh: string
|
|
21
21
|
primitive: number
|
|
22
22
|
material: string
|
|
23
|
-
/** the
|
|
24
|
-
|
|
23
|
+
/** the primitive's index over its file's meshes: the lightmap bake's record `<key>@<ordinal>` */
|
|
24
|
+
lightmapOrdinal: number
|
|
25
25
|
/** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
|
|
26
26
|
triangle: number
|
|
27
27
|
backface: boolean
|
|
@@ -130,8 +130,8 @@ export class Model extends Node {
|
|
|
130
130
|
culling?: boolean
|
|
131
131
|
onProgress?: (p: { loaded: number, total?: number }) => void
|
|
132
132
|
/** Baked lighting (engines/bake). `true` = a STATIC: loads through the lightmap material so
|
|
133
|
-
* `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 —
|
|
134
|
-
*
|
|
133
|
+
* `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 —
|
|
134
|
+
* `lecodes assets unwrap`, which `lightmap bake` runs itself on raw GLBs) and takes nothing from the real-time lights once the bake applies. Omitted / `false` =
|
|
135
135
|
* the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
|
|
136
136
|
lightmap?: boolean
|
|
137
137
|
/** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
|
package/src/math/vec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Fluent 2D / 3D vectors — the SDK's user-facing vector math (no gl-matrix, no out-params, no imports).
|
|
2
2
|
//
|
|
3
|
-
// Design
|
|
3
|
+
// Design: a vector is a *mutable little struct* — `v.x = 3`, `v.set(x, y, z)` mutate in
|
|
4
4
|
// place — but every *method* is PURE: `a.add(b)` returns a NEW vector and never touches `a`. So chains
|
|
5
5
|
// (`dir.normalize().scale(s)`) can't clobber their source, and the immutable-method style is the most
|
|
6
6
|
// amenable to a future escape-analysis / scalar-replacement compiler pass. Backing is plain number
|
package/src/runtime/app.ts
CHANGED
|
@@ -113,6 +113,20 @@ export const app = {
|
|
|
113
113
|
quit(): void {
|
|
114
114
|
_creatorApp.quit?.()
|
|
115
115
|
},
|
|
116
|
+
/** Collect garbage now: what the app no longer references — a finished level's nodes, its
|
|
117
|
+
* textures and materials — is freed before this returns, not whenever the collector next runs.
|
|
118
|
+
* Call it where a pause is invisible (a loading screen, between levels), after dropping the
|
|
119
|
+
* references; never during play — a collection walks the whole heap. It also puts the next
|
|
120
|
+
* automatic collection at half again the heap it leaves: the room the next stretch of play has.
|
|
121
|
+
* `headroomMB` asks for more room when the play ahead allocates more than that — a drive that
|
|
122
|
+
* streams in hundreds of megabytes of terrain buffers: `app.gc({ headroomMB: 512 })` keeps the
|
|
123
|
+
* next collection at least 512 MB away (until it comes; then the engine's rule again). The log's
|
|
124
|
+
* `[gc]` line says what it took, what it freed and where the next one is. On iOS a request the
|
|
125
|
+
* engine serves on its own schedule, without the headroom; a no-op in the browser, where a page
|
|
126
|
+
* cannot ask for a collection. */
|
|
127
|
+
gc(options?: { headroomMB?: number }): void {
|
|
128
|
+
_creatorApp.gc?.(options?.headroomMB)
|
|
129
|
+
},
|
|
116
130
|
/** Global back-press fallback (Android back button/gesture — iOS has no system back). Fires
|
|
117
131
|
* only when nothing else consumed the press: no open widget/modal claimed it, the current
|
|
118
132
|
* screen has no `onBack`, every pager is at its tab root and the router is at its
|