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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// A reflection probe: a cube of the scene's light drawn around a point, live (creator-gl renderview.cpp: a render view of
|
|
2
|
+
// six faces into an HDR cube render texture; with PREFILTER, Filament's libs/iblprefilter filter after each completed
|
|
3
|
+
// cube). A Node, placed and parented like any; its view starts once the node is in a scene (Node.ts awaitScene) — or at
|
|
4
|
+
// `scene.environment = probe` while it is in none. RenderTexture.ts holds the half it shares with a secondary Camera.
|
|
5
|
+
|
|
6
|
+
import { Node, awaitScene } from "./Node"
|
|
7
|
+
import { CAMERA_DEFAULTS } from "./Camera"
|
|
8
|
+
import {
|
|
9
|
+
RenderTexture, RT_FLAG_CUBEMAP, RT_FLAG_HDR, RT_FLAG_PREFILTER, RT_MAX_SIDE, makeRenderTexture, requireRenderTextures, startRenderView,
|
|
10
|
+
} from "./RenderTexture"
|
|
11
|
+
|
|
12
|
+
/** `new ReflectionProbe(options)`: the cube, what its view draws, and how often. */
|
|
13
|
+
export type ReflectionProbeOptions = {
|
|
14
|
+
/** The cube's edge, texels per face side (default 128): a power of two, 16..4096, when prefiltered; 1..4096 otherwise.
|
|
15
|
+
* A probe is light, not a picture — 64..128 is plenty for a car's reflections. */
|
|
16
|
+
size?: number
|
|
17
|
+
/** The layers its view draws, a bit mask over `Node.layer` (default 0xff = everything): leave out the probe's own
|
|
18
|
+
* carrier, or the cube is the inside of the car it rides on. Fixed. */
|
|
19
|
+
layers?: number
|
|
20
|
+
/** Shadows in its view (default `false`): with them, a full cube renders its own shadow maps six times. Fixed. */
|
|
21
|
+
shadows?: boolean
|
|
22
|
+
/** Faces drawn per render, 1..6, round-robin (default 6 = the whole cube) — `probe.facesPerFrame`. */
|
|
23
|
+
facesPerFrame?: number
|
|
24
|
+
/** Render once every N frames drawn (default 1 = every frame) — `probe.everyNth`. */
|
|
25
|
+
everyNth?: number
|
|
26
|
+
/** Filter the cube for ROUGHNESS (default `true`; `size` a power of two then). Without it the cube has one sharp level:
|
|
27
|
+
* under `scene.environment` only mirror-like surfaces (roughness near 0) are right, and the diffuse ambient stays the
|
|
28
|
+
* scene's own probe's. With it the GPU filters every completed cube into five roughness levels — a brushed or painted
|
|
29
|
+
* body shows its surroundings blurred as much as its roughness says — and the roughest level becomes the diffuse
|
|
30
|
+
* ambient: what the probe sees lights matte surfaces too (a red wall tints them red).
|
|
31
|
+
*
|
|
32
|
+
* COST: one filtering per completed cube (every render at `facesPerFrame` 6, every third at 2), on the frame after:
|
|
33
|
+
* the cube's mip chain plus a pass per level whose taps are the level's texels × `samples` — for a 64 cube at the
|
|
34
|
+
* default 256 samples about 2 million cube taps — and ten small driver submissions on the CPU. `everyNth` /
|
|
35
|
+
* `facesPerFrame` space the filterings out. Fixed. */
|
|
36
|
+
prefilter?: boolean
|
|
37
|
+
/** The filter's samples per texel, 1..2048 (default 256 — a quarter of Filament's offline default: each sample reads
|
|
38
|
+
* the mip that matches its footprint, which keeps a low count smooth; raise it if a mid-roughness surface reads
|
|
39
|
+
* blotchy). Smoothness for time; read with `prefilter` only. */
|
|
40
|
+
samples?: number
|
|
41
|
+
/** Near clip plane in metres (default 0.01). Fixed. */
|
|
42
|
+
near?: number
|
|
43
|
+
/** Far clip plane in metres (default 1000). Fixed. */
|
|
44
|
+
far?: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A REFLECTION PROBE: a cube of the scene's light drawn around a point, live — chrome and glass show what is actually
|
|
49
|
+
* around them, moving objects included. A Node: place it, or parent it (`car.add(probe)` — it rides along); the six faces
|
|
50
|
+
* look along the world axes from its WORLD position (its rotation and scale are ignored). It draws once it is in a scene
|
|
51
|
+
* (added to one, or under a node that is), every frame that scene is on show — after the frame's update passes, before
|
|
52
|
+
* the scene's own picture — in linear HDR light (no tone mapping) at the scene camera's exposure. It sees the scene's
|
|
53
|
+
* renderables, lights, environment and sky, not its overlays.
|
|
54
|
+
*
|
|
55
|
+
* Two ways to use it:
|
|
56
|
+
* - `scene.environment = probe`: the scene's reflections come from the cube instead of the scene's own probe
|
|
57
|
+
* (`SceneOptions.ibl`) — and, prefiltered (the default), its diffuse ambient too. A probe in no scene yet starts in
|
|
58
|
+
* that one.
|
|
59
|
+
* - `probe.texture` in a material that samples a cube itself (a custom shader's samplerCubemap): the same cube as a
|
|
60
|
+
* `RenderTexture` — when prefiltered, the filtered one, five roughness levels.
|
|
61
|
+
*
|
|
62
|
+
* COST: a full render is six passes over the scene, plus (prefiltered) a GPU filtering per completed cube (`prefilter`
|
|
63
|
+
* says what one costs). Keep `size` small (128 is a lot), spread the cube over frames (`facesPerFrame: 2` = a whole cube
|
|
64
|
+
* every 3 frames, a third of the cost each), render less often (`everyNth`), leave out what it need not see (`layers` —
|
|
65
|
+
* its carrier first), keep `shadows` off, and turn it off once a static surrounding is drawn (`enabled = false` keeps the
|
|
66
|
+
* last cube).
|
|
67
|
+
*
|
|
68
|
+
* LIFETIME: the cube and its view live as long as the probe — keep a reference (`scene.environment` holds it too).
|
|
69
|
+
* Destroying the probe's node stops the view (the cube keeps its last light); moving it to another scene later does not
|
|
70
|
+
* move the view. Throws on a host without render textures (`createRenderTexture` — the web hosts, for now).
|
|
71
|
+
*/
|
|
72
|
+
export class ReflectionProbe extends Node {
|
|
73
|
+
/** The cube the probe draws (`cubemap`, linear HDR light) — the filtered one, five roughness levels, when `prefilter`. */
|
|
74
|
+
readonly texture: RenderTexture
|
|
75
|
+
/** Filtered for roughness (the `prefilter` option). */
|
|
76
|
+
readonly prefilter: boolean
|
|
77
|
+
/** @internal The render view's owned handle (`_view.id` = the engine's view id), null until the view starts: it lives
|
|
78
|
+
* exactly as long as this probe holds it. */
|
|
79
|
+
_view: Handle | null = null
|
|
80
|
+
private readonly _layers: number
|
|
81
|
+
private readonly _shadows: boolean
|
|
82
|
+
private readonly _near: number
|
|
83
|
+
private readonly _far: number
|
|
84
|
+
private _enabled = true
|
|
85
|
+
private _everyNth: number
|
|
86
|
+
private _facesPerFrame: number
|
|
87
|
+
|
|
88
|
+
constructor(options: ReflectionProbeOptions = {}) {
|
|
89
|
+
requireRenderTextures("new ReflectionProbe")
|
|
90
|
+
const size = Math.max(1, Math.floor(options.size ?? 128))
|
|
91
|
+
const prefilter = options.prefilter !== false
|
|
92
|
+
if (prefilter && (size < 16 || size > RT_MAX_SIDE || (size & (size - 1)) !== 0)) {
|
|
93
|
+
throw new Error(`new ReflectionProbe: a prefiltered probe's size is a power of two, 16..${RT_MAX_SIDE} (got ${size}) — or prefilter: false`)
|
|
94
|
+
}
|
|
95
|
+
super()
|
|
96
|
+
const samples = prefilter ? Math.max(0, Math.floor(options.samples ?? 0)) : 0
|
|
97
|
+
this.texture = makeRenderTexture("new ReflectionProbe", size, size, RT_FLAG_CUBEMAP | RT_FLAG_HDR | (prefilter ? RT_FLAG_PREFILTER : 0), samples)
|
|
98
|
+
this.prefilter = prefilter
|
|
99
|
+
this._layers = (options.layers ?? 0xff) & 0xff
|
|
100
|
+
this._shadows = options.shadows === true
|
|
101
|
+
this._near = options.near ?? CAMERA_DEFAULTS.near
|
|
102
|
+
this._far = options.far ?? CAMERA_DEFAULTS.far
|
|
103
|
+
this._everyNth = Math.max(1, Math.floor(options.everyNth ?? 1))
|
|
104
|
+
this._facesPerFrame = Math.min(6, Math.max(1, Math.floor(options.facesPerFrame ?? 6)))
|
|
105
|
+
awaitScene(this)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @internal Node.ts awaitScene / Scene.environment: the probe is in scene `sceneId` — start its view into its cube. */
|
|
109
|
+
_bindView(sceneId: number): void {
|
|
110
|
+
if (this._view) return
|
|
111
|
+
this._view = startRenderView("ReflectionProbe", sceneId, this.texture, this.id, this._layers, 90, this._near, this._far, this._shadows)
|
|
112
|
+
if (this._view && (!this._enabled || this._everyNth !== 1 || this._facesPerFrame !== 6)) this._sendSchedule()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether the probe draws (default `true`). Off keeps the last cube and costs nothing — a probe that has drawn its
|
|
116
|
+
* static surroundings once; back on, it renders on the next frame drawn. */
|
|
117
|
+
get enabled(): boolean { return this._enabled }
|
|
118
|
+
set enabled(on: boolean) {
|
|
119
|
+
this._enabled = on
|
|
120
|
+
this._sendSchedule()
|
|
121
|
+
}
|
|
122
|
+
/** Render once every N frames DRAWN (default 1 = every frame). The count restarts with each change: the next frame
|
|
123
|
+
* drawn renders, then one in every N. */
|
|
124
|
+
get everyNth(): number { return this._everyNth }
|
|
125
|
+
set everyNth(n: number) {
|
|
126
|
+
this._everyNth = Math.max(1, Math.floor(n))
|
|
127
|
+
this._sendSchedule()
|
|
128
|
+
}
|
|
129
|
+
/** The cube's faces drawn per render, 1..6 (default 6), round-robin from where the last render stopped: 2 spreads a
|
|
130
|
+
* whole cube over 3 renders at a third of the cost each. A prefiltered cube is filtered each time it completes. */
|
|
131
|
+
get facesPerFrame(): number { return this._facesPerFrame }
|
|
132
|
+
set facesPerFrame(n: number) {
|
|
133
|
+
this._facesPerFrame = Math.min(6, Math.max(1, Math.floor(n)))
|
|
134
|
+
this._sendSchedule()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @internal Send the whole schedule (`setRenderViewOptions` restarts the count, so the next frame drawn renders). */
|
|
138
|
+
private _sendSchedule(): void {
|
|
139
|
+
if (this._view) _creator.setRenderViewOptions?.(this._view.id, this._enabled, this._everyNth, this._facesPerFrame)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// A texture a render view draws into (creator-gl renderview.cpp; creator.h createRenderTexture / createRenderView /
|
|
2
|
+
// setRenderViewOptions / setRenderViewProjection / readRenderTexturePixels). Only the texture: the view drawing into it
|
|
3
|
+
// belongs to what made it — a secondary Camera (`camera.renderToTexture`, a 2D picture) or a ReflectionProbe (a cube) —
|
|
4
|
+
// which holds the view's owned handle, its schedule and its projection. The helpers below are those two makers' shared
|
|
5
|
+
// half: the host's texture, and the view started once their node is in a scene (Node.ts awaitScene).
|
|
6
|
+
|
|
7
|
+
import { Texture } from "./Texture"
|
|
8
|
+
|
|
9
|
+
/** @internal `_creator.createRenderTexture` flags (creator.h CREATOR_RENDER_TEXTURE_*). */
|
|
10
|
+
export const RT_FLAG_CUBEMAP = 1
|
|
11
|
+
/** @internal */
|
|
12
|
+
export const RT_FLAG_HDR = 2
|
|
13
|
+
/** @internal */
|
|
14
|
+
export const RT_FLAG_PREFILTER = 4
|
|
15
|
+
/** @internal `_creator.createRenderView` flags bit: no shadow maps in this view (creator.h CREATOR_RENDER_VIEW_SHADOWS_OFF). */
|
|
16
|
+
const VIEW_FLAG_SHADOWS_OFF = 1
|
|
17
|
+
/** @internal The longest side the host takes (creator.h createRenderTexture). */
|
|
18
|
+
export const RT_MAX_SIDE = 4096
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A texture a render view draws into — made by `camera.renderToTexture({ width, height })` (a 2D picture: the
|
|
22
|
+
* tone-mapped image as the screen would show it) or by a `ReflectionProbe` (`probe.texture`: a cube of linear HDR light).
|
|
23
|
+
* It IS a `Texture`: a 2D one goes wherever an image does (`material.map` — a monitor, a rear-view mirror), a cube
|
|
24
|
+
* wherever a cubemap does (`scene.environment` takes the probe itself).
|
|
25
|
+
*
|
|
26
|
+
* ROW ORDER: a 2D render texture's first texel row is the BOTTOM of the camera's picture (OpenGL's order, on every
|
|
27
|
+
* backend) — an image's first row is its top. A mesh whose v runs UP its face (`Mesh.plane`) shows it upright as is; one
|
|
28
|
+
* laid out like a glTF (v down) samples it with v flipped. A cube face is laid out as every GPU's cube lookup reads it.
|
|
29
|
+
*
|
|
30
|
+
* LIFETIME: the texture lives while something holds it — the camera or probe that made it (each keeps its texture), a
|
|
31
|
+
* material showing it — and keeps its last picture when its view stops (`enabled = false`, the camera's next
|
|
32
|
+
* `renderToTexture`, the camera gone).
|
|
33
|
+
*/
|
|
34
|
+
export class RenderTexture extends Texture {
|
|
35
|
+
/** A cube (a `ReflectionProbe`'s) rather than a 2D picture (a `Camera`'s). A cube's `width` and `height` are both its
|
|
36
|
+
* edge. */
|
|
37
|
+
readonly cubemap: boolean
|
|
38
|
+
|
|
39
|
+
/** @internal Made by `Camera.renderToTexture` and `ReflectionProbe` (through `makeRenderTexture`): the size, the
|
|
40
|
+
* kind and the texture's owned handle. */
|
|
41
|
+
constructor(width: number, height: number, cubemap: boolean, texture: Handle) {
|
|
42
|
+
super(width, height, texture)
|
|
43
|
+
this.cubemap = cubemap
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* RGBA8 bytes (`width × height × 4`) of a rectangle of the texture as the NEXT frame drawn leaves it — a GPU
|
|
48
|
+
* readback, for checks and tools, not a per-frame path. Rows come in the texture's own order: the first is texel row
|
|
49
|
+
* `y`, which for a 2D texture counts from the picture's BOTTOM. `face` 0..5 = +X −X +Y −Y +Z −Z of a cube (0 for a 2D
|
|
50
|
+
* picture). A camera's (LDR) texture gives its stored sRGB bytes, a probe's (HDR) its linear light clamped to 0..1 —
|
|
51
|
+
* a prefiltered probe its sharpest level. Rejects on a host without the call, or when the host refuses the read (the
|
|
52
|
+
* rectangle outside the texture, the engine gone first).
|
|
53
|
+
*/
|
|
54
|
+
readPixels(x: number, y: number, width: number, height: number, face = 0): Promise<Uint8Array> {
|
|
55
|
+
if (!_creator.readRenderTexturePixels) return Promise.reject(new Error("RenderTexture.readPixels: this host has no readRenderTexturePixels"))
|
|
56
|
+
return new Promise<Uint8Array>((resolve, reject) => {
|
|
57
|
+
_creator.readRenderTexturePixels!(this._h.id, face, x, y, width, height, (pixels) => {
|
|
58
|
+
if (pixels) resolve(pixels)
|
|
59
|
+
else reject(new Error(`RenderTexture.readPixels: the host refused (${x}, ${y}) ${width}×${height} of face ${face}`))
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @internal Throw for `who` on a host without render textures (createRenderTexture + createRenderView). */
|
|
66
|
+
export const requireRenderTextures = (who: string): void => {
|
|
67
|
+
if (!_creator.createRenderTexture || !_creator.createRenderView) {
|
|
68
|
+
throw new Error(`${who}: this host has no render textures (createRenderTexture) — the web hosts do not yet`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @internal The host's render texture (`flags` RT_FLAG_*; a cube's `height` is its `width`), or a throw naming `who`. */
|
|
73
|
+
export const makeRenderTexture = (who: string, width: number, height: number, flags: number, samples = 0): RenderTexture => {
|
|
74
|
+
requireRenderTextures(who)
|
|
75
|
+
const cube = (flags & RT_FLAG_CUBEMAP) !== 0
|
|
76
|
+
const texture = _creator.createRenderTexture!(width, cube ? 0 : height, flags, samples)
|
|
77
|
+
if (!texture || texture.id === 0xFFFFFFFF) {
|
|
78
|
+
throw new Error(`${who}: the host refused a ${cube ? `cube of ${width}` : `${width}×${height}`} render texture (sides 1..${RT_MAX_SIDE})`)
|
|
79
|
+
}
|
|
80
|
+
return new RenderTexture(width, cube ? width : height, cube, texture)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @internal Start a render view of scene `sceneId` into `texture` from entity `poseId` (createRenderView), or warn for
|
|
84
|
+
* `who` and give null — the host refuses an overlay scene. */
|
|
85
|
+
export const startRenderView = (
|
|
86
|
+
who: string, sceneId: number, texture: RenderTexture, poseId: number,
|
|
87
|
+
layers: number, fov: number, near: number, far: number, shadows: boolean,
|
|
88
|
+
): Handle | null => {
|
|
89
|
+
const view = _creator.createRenderView!(sceneId, texture._h.id, poseId, layers & 0xff, fov, near, far, shadows ? 0 : VIEW_FLAG_SHADOWS_OFF)
|
|
90
|
+
if (view && view.id !== 0) return view
|
|
91
|
+
console.warn(`[${who}] the host made no render view in scene ${sceneId} (an overlay scene has none) — its texture stays empty`)
|
|
92
|
+
return null
|
|
93
|
+
}
|
package/src/gl/Scene.ts
CHANGED
|
@@ -18,8 +18,9 @@ import { Camera } from "./Camera"
|
|
|
18
18
|
import { SceneAudio } from "./audio/SceneAudio"
|
|
19
19
|
import { Material } from "./Material"
|
|
20
20
|
import { PostProcessing, type PostProcessingOptions } from "./postProcessing"
|
|
21
|
+
import { ReflectionProbe } from "./ReflectionProbe"
|
|
21
22
|
import { Texture } from "./Texture"
|
|
22
|
-
import { Node, nodes } from "./Node"
|
|
23
|
+
import { Node, bindPendingViews, nodes, sceneIdOf } from "./Node"
|
|
23
24
|
import { glState } from "./state"
|
|
24
25
|
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
25
26
|
|
|
@@ -176,6 +177,12 @@ export class Scene implements Presentable {
|
|
|
176
177
|
/** @internal what `environmentIntensity` currently asks for — the accessor's backing field, so it
|
|
177
178
|
* answers on hosts whose engine has no live setter too. */
|
|
178
179
|
_environmentIntensity = 20000
|
|
180
|
+
/** @internal The probe the constructor named for `setDefaultIbl` (-1 = none: the project-wide
|
|
181
|
+
* `ibl.ktx`) — re-sent when `environment` goes back to null. */
|
|
182
|
+
_iblId = -1
|
|
183
|
+
/** @internal What `environment` lights the scene from (null = the scene's own probe). Holding it
|
|
184
|
+
* keeps the probe — its cube and the view drawing into it — alive while it lights. */
|
|
185
|
+
_environment: ReflectionProbe | null = null
|
|
179
186
|
readonly camera: Camera
|
|
180
187
|
/** The listener + global 3D audio knobs (docs/audio-plan.md). */
|
|
181
188
|
readonly audio: SceneAudio = new SceneAudio()
|
|
@@ -226,6 +233,7 @@ export class Scene implements Presentable {
|
|
|
226
233
|
// "no probe named", which leaves the host on the project-wide `ibl.ktx` (the old behaviour).
|
|
227
234
|
const src = typeof options.ibl === "string" ? options.ibl : undefined
|
|
228
235
|
const iblId = src === undefined ? -1 : src.startsWith("id:") ? Number(src.slice(3)) : _creatorFetch.local(src)
|
|
236
|
+
this._iblId = iblId
|
|
229
237
|
_creator.setDefaultIbl(this._id, options.environmentIntensity ?? 20000, iblId)
|
|
230
238
|
}
|
|
231
239
|
this._environmentIntensity = options.ibl === false ? 0 : (options.environmentIntensity ?? 20000)
|
|
@@ -341,6 +349,44 @@ export class Scene implements Presentable {
|
|
|
341
349
|
}
|
|
342
350
|
}
|
|
343
351
|
private static _warnedEnvIntensity = false
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Light the scene from a live REFLECTION PROBE (ReflectionProbe.ts) — a cube drawn around the probe's position, the
|
|
355
|
+
* probe riding the car / standing in the room: chrome and glass show what is actually around them, moving objects
|
|
356
|
+
* included. `null` gives the light back to the scene's own probe (`SceneOptions.ibl`, at `environmentIntensity`;
|
|
357
|
+
* re-installing it reloads the probe — a switch, not a per-frame call). A probe in no scene yet starts its view in this
|
|
358
|
+
* one; one already in another scene draws only while that scene is on show.
|
|
359
|
+
*
|
|
360
|
+
* Prefiltered (the default), the probe gives the reflections at every roughness — a car body, painted metal blur it as
|
|
361
|
+
* they should — and the diffuse ambient too (then `environmentIntensity` no longer changes it); it costs a GPU
|
|
362
|
+
* filtering per completed cube (`ReflectionProbeOptions.prefilter`). `prefilter: false` gives one sharp level:
|
|
363
|
+
* mirror-like surfaces (roughness near 0) are right, rougher ones read the sharp cube too, and only the reflections
|
|
364
|
+
* change — the diffuse ambient keeps the scene's own probe's shape at `environmentIntensity`.
|
|
365
|
+
*
|
|
366
|
+
* A probe that draws while it lights reads what it writes — fine on OpenGL; a Vulkan backend may want two probes in
|
|
367
|
+
* turn (a prefiltered probe never does: it lights from the filtered copy). Leave the probe's own carrier out of its view
|
|
368
|
+
* (`layers`), or the cube is the inside of the car. Warns once on a host without the call.
|
|
369
|
+
*/
|
|
370
|
+
get environment(): ReflectionProbe | null { return this._environment }
|
|
371
|
+
set environment(probe: ReflectionProbe | null) {
|
|
372
|
+
if (probe !== null && !(probe instanceof ReflectionProbe)) {
|
|
373
|
+
throw new Error("scene.environment: a ReflectionProbe (new ReflectionProbe({ size: 128 })) or null")
|
|
374
|
+
}
|
|
375
|
+
if (probe === this._environment) return
|
|
376
|
+
this._environment = probe
|
|
377
|
+
if (probe && sceneIdOf(probe) < 0) probe._bindView(this._id) // in no scene yet: its view starts in this one
|
|
378
|
+
if (!_creator.setEnvironmentTexture) {
|
|
379
|
+
if (!Scene._warnedEnvTexture) {
|
|
380
|
+
Scene._warnedEnvTexture = true
|
|
381
|
+
console.warn("[scene] this host has no setEnvironmentTexture — environment stays the scene's own probe")
|
|
382
|
+
}
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
if (probe) _creator.setEnvironmentTexture(this._id, probe.texture._h.id)
|
|
386
|
+
else _creator.setDefaultIbl(this._id, this._environmentIntensity, this._iblId) // installing a probe frees the cube's light
|
|
387
|
+
}
|
|
388
|
+
private static _warnedEnvTexture = false
|
|
389
|
+
|
|
344
390
|
/** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
|
|
345
391
|
setStencil(enabled: boolean): void {
|
|
346
392
|
if (_creator.setSceneStencil) _creator.setSceneStencil(this._id, enabled)
|
|
@@ -348,12 +394,18 @@ export class Scene implements Presentable {
|
|
|
348
394
|
}
|
|
349
395
|
|
|
350
396
|
add(...list: Node[]): this {
|
|
351
|
-
for (const n of list) {
|
|
397
|
+
for (const n of list) {
|
|
398
|
+
_creator.addEntityToScene(this._id, n.id)
|
|
399
|
+
nodes.pin(n) // the scene owns it now
|
|
400
|
+
n._inScene = this._id // what a render view under it starts in (Node.ts sceneIdOf)
|
|
401
|
+
}
|
|
402
|
+
bindPendingViews()
|
|
352
403
|
return this
|
|
353
404
|
}
|
|
354
405
|
remove(...list: Node[]): this {
|
|
355
406
|
for (const n of list) {
|
|
356
407
|
_creator.removeEntityFromScene(this._id, n.id)
|
|
408
|
+
if (n._inScene === this._id) n._inScene = -1
|
|
357
409
|
if (!_creator.entityIsOwned(n.id)) nodes.unpin(n.id) // loose again: its handle owns it
|
|
358
410
|
}
|
|
359
411
|
return this
|
package/src/host.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// JS-runtime globals provided by the host engine (web viewer / desktop / iOS), NOT by the SDK.
|
|
2
2
|
// Declared only so SDK and user code type-check — never bundled or injected.
|
|
3
3
|
|
|
4
|
-
// The
|
|
5
|
-
//
|
|
4
|
+
// The SDK types the macros return: an SVG image source, the same `SvgSource` a project has as a
|
|
5
|
+
// global, and the lightmap def `bake()` compiles to (build-types.ts points these imports into
|
|
6
|
+
// dist/types when it copies the file).
|
|
6
7
|
import type { SvgSource } from "./runtime/misc"
|
|
8
|
+
import type { LightmapDef, LightmapOptions } from "./gl/Lightmap"
|
|
7
9
|
|
|
8
10
|
declare global {
|
|
9
11
|
/** As in the browser, but the delay is REQUIRED (the native runtime throws without it) and no
|
|
@@ -50,6 +52,15 @@ declare global {
|
|
|
50
52
|
* comments and string literals are text, not macros. */
|
|
51
53
|
function asset(path: string): string
|
|
52
54
|
|
|
55
|
+
/** THE LEVEL'S BAKE: `lecodes lightmap bake` writes `<stem>.bake` (a JSON manifest) and the files it names next to
|
|
56
|
+
* it. Compile-time macro: the manifest is read, every file it names is staged, and the call becomes the literal
|
|
57
|
+
* `Lightmap.load` takes — `env: { lightmap: bake('./assets/lightmap/level.bake') }` in a scene file. The path is a
|
|
58
|
+
* string literal, relative to the file that calls it (`./`, `../`), else to the project root. No file yet →
|
|
59
|
+
* `{ data: null, path }`: the level runs unbaked and says so; under `lecodes lightmap bake` this is the target the
|
|
60
|
+
* bake writes. A file the manifest names that is missing fails the compile. `options` (an object literal, copied
|
|
61
|
+
* as written) are the level's bake / load options. */
|
|
62
|
+
function bake(path: `${string}.bake`, options?: LightmapOptions): LightmapDef
|
|
63
|
+
|
|
53
64
|
/** Compile-time macro: installs a font and returns its family name — using a font IS declaring
|
|
54
65
|
* it. A registry id (`font("manrope")`) resolves against the font registry; a project path
|
|
55
66
|
* (`font("./fonts/Brand.ttf")`) reads family/weight/italic from the file itself. `opts`
|
package/src/inject.ts
CHANGED
|
@@ -233,6 +233,7 @@ export type { DynamicBoneCurve, DynamicBoneFloor, DynamicBoneColliders } from ".
|
|
|
233
233
|
export type { IKTwoBone, IKLookAt } from "./gl/animation/IK"
|
|
234
234
|
export { Light } from "./gl/Light"
|
|
235
235
|
export { Lightmap } from "./gl/Lightmap"
|
|
236
|
+
export type { LightmapDef, LightmapFiles, LightmapOptions, LightmapTransmit } from "./gl/Lightmap"
|
|
236
237
|
export { Foliage } from "./gl/Foliage"
|
|
237
238
|
export type { FoliageOptions, FoliageWind } from "./gl/Foliage"
|
|
238
239
|
export { Terrain } from "./gl/terrain/Terrain"
|
|
@@ -251,7 +252,11 @@ export type { ScatterPlacement, ScatterOptions } from "./gl/Scatter"
|
|
|
251
252
|
export type { InstanceTransform } from "./gl/InstancedMesh"
|
|
252
253
|
export { Texture } from "./gl/Texture"
|
|
253
254
|
export type { TextureLoadOptions } from "./gl/Texture"
|
|
255
|
+
export { RenderTexture } from "./gl/RenderTexture"
|
|
254
256
|
export { Camera } from "./gl/Camera"
|
|
257
|
+
export type { CameraOptions, RenderToTextureOptions } from "./gl/Camera"
|
|
258
|
+
export { ReflectionProbe } from "./gl/ReflectionProbe"
|
|
259
|
+
export type { ReflectionProbeOptions } from "./gl/ReflectionProbe"
|
|
255
260
|
export { Ray } from "./gl/Ray"
|
|
256
261
|
export { Plane } from "./gl/Plane"
|
|
257
262
|
export { Noise } from "./gl/Noise"
|
package/src/scene/defineScene.ts
CHANGED
|
@@ -52,11 +52,11 @@ import { Mesh } from "../gl/Mesh"
|
|
|
52
52
|
import { Model } from "../gl/Model"
|
|
53
53
|
import { Physics } from "../gl/physics/Physics"
|
|
54
54
|
import { CharacterController } from "../gl/physics/CharacterController"
|
|
55
|
-
import { Lightmap, type
|
|
55
|
+
import { Lightmap, type LightmapDef } from "../gl/Lightmap"
|
|
56
56
|
import { Foliage, type FoliageOptions } from "../gl/Foliage"
|
|
57
57
|
import { NavMesh } from "../gl/nav/NavMesh"
|
|
58
58
|
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/terrain/Terrain"
|
|
59
|
-
import { Light, type SunOptions } from "../gl/Light"
|
|
59
|
+
import { Light, type PointOptions, type SpotOptions, type SunOptions } from "../gl/Light"
|
|
60
60
|
import { Material } from "../gl/Material"
|
|
61
61
|
import { assignMaterialDef, MaterialHandle, resolveMaterialDef, type MaterialDef } from "./material"
|
|
62
62
|
import type { CapsuleOptions, CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
@@ -71,7 +71,13 @@ export type MeshDef =
|
|
|
71
71
|
| ({ kind: "capsule" } & CapsuleOptions)
|
|
72
72
|
| ({ kind: "plane" } & PlaneOptions)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
/** The `light: {}` source block, by `kind` — `Light.sun` / `Light.point` / `Light.spot` with the node's transform: a
|
|
75
|
+
* point light's `bakeArea` panel lies in the node's local XZ plane and emits along its local -Y, so `eulerAngles`
|
|
76
|
+
* tilts it; a spot shines along the node's -Z (`direction` aims it, as a sun's does). */
|
|
77
|
+
export type LightDef =
|
|
78
|
+
| ({ kind: "sun" } & SunOptions)
|
|
79
|
+
| ({ kind: "point" } & PointOptions)
|
|
80
|
+
| ({ kind: "spot" } & SpotOptions)
|
|
75
81
|
|
|
76
82
|
/** A stamp: the terrain conforms to another node of this file (a road) at load — see Terrain.conform. */
|
|
77
83
|
export type TerrainStampDef = {
|
|
@@ -192,7 +198,7 @@ export type SceneNodeDef = {
|
|
|
192
198
|
* `'cube'` = a static whose every mesh bakes as AMBIENT CUBES instead of atlas texels — an occluder (its shadow and
|
|
193
199
|
* bounce stay in the bake) lit where it stands, the way the bake treats small and thin props on its own: a pipe run, a
|
|
194
200
|
* fence, a prop whose lightmap seams show. A prefab marked `'cube'` bakes its statics that way too. By name for a whole
|
|
195
|
-
* kit: `env.lightmap
|
|
201
|
+
* kit: the `cube` option of `env.lightmap`'s `bake()`. */
|
|
196
202
|
lightmap?: boolean | "cube"
|
|
197
203
|
/** `model` nodes: vegetation — load through the FOLIAGE tier (wind, touch bending, per-copy tint; see
|
|
198
204
|
* `Foliage`). `{ fade: [start, end] }` also thins the cards out over that distance range (m) and drops the
|
|
@@ -215,55 +221,6 @@ export type SceneCameraDef = CameraProjectionDef & {
|
|
|
215
221
|
target?: Vec3Like
|
|
216
222
|
}
|
|
217
223
|
|
|
218
|
-
/** `env.lightmap` — the level's baked lighting (engines/bake): the files `lecodes lightmap bake`
|
|
219
|
-
* writes. Absent = real-time only. */
|
|
220
|
-
export type SceneLightmapDef = {
|
|
221
|
-
/** `asset('./assets/lightmap/level.bake')` */
|
|
222
|
-
data: string
|
|
223
|
-
/** `asset('./assets/lightmap/level-light.ktx2')` — or, for a bake that took more than one atlas PAGE
|
|
224
|
-
* (`lecodes lightmap bake --pages`), every page in order: `[asset('x-light.ktx2'), asset('x-light_1.ktx2')]`. */
|
|
225
|
-
light: string | string[]
|
|
226
|
-
/** `asset('./assets/lightmap/level-aux.ktx2')` (pages like `light`) — sun / sky visibility + light direction. */
|
|
227
|
-
aux: string | string[]
|
|
228
|
-
/** DEBUG: the page sets a `lecodes lightmap bake --split` wrote (`<stem>-direct[_n].ktx2`, `<stem>-indirect[_n].ktx2`),
|
|
229
|
-
* pages like `light` — the `Lightmap.debug("direct" | "indirect")` views bind them in place of `light`. */
|
|
230
|
-
direct?: string | string[]
|
|
231
|
-
indirect?: string | string[]
|
|
232
|
-
/** `asset('./assets/lightmap/level-probes.ktx2')` — the bake's reflection probes (one prefiltered cubemap of the level's
|
|
233
|
-
* radiance per probe; every static reflects the nearest). Absent = statics reflect the sky's SH alone. */
|
|
234
|
-
probes?: string
|
|
235
|
-
/** FOR THE BAKE: the radiance in cd / m² of emission 1.0 on this level (emissive factor x strength x map) - every lit
|
|
236
|
-
* region of an emissive surface bakes as a rectangle lamp. An imported pack's emission is a LOOK (the school's panels
|
|
237
|
-
* say 10); this is where the level says what they give: a 1.3 m ceiling panel of ~8000 lm is ~1600 nits. Default: what
|
|
238
|
-
* the camera makes of 1.0 (`1.2 x 2^EV100`). 0 = emissive surfaces light nothing. `--emissive-nits` overrides. */
|
|
239
|
-
emissiveNits?: number
|
|
240
|
-
/** `false` = the emissive surfaces light nothing in the bake - the level is lit by its lamps (`Light.point` with a
|
|
241
|
-
* `bakeArea` next to every panel, the Unity packs' way) and the panels are decoration; `emissiveNits` then only says how
|
|
242
|
-
* bright their glow is DRAWN (scene-referred: nits through the camera's exposure). Default true. */
|
|
243
|
-
emissiveBake?: boolean
|
|
244
|
-
/** FOR THE BAKE: materials that let light THROUGH them, by glTF material name (`"name*"` = every name with that start) →
|
|
245
|
-
* the share of a shadow ray that passes, tinted by the material's base colour x map: `{ mat_awning: 0.3 }` puts the
|
|
246
|
-
* awning's warm, patterned light on the sand. The surface stays opaque - baked, drawn as before, a shadow caster.
|
|
247
|
-
* `{ through, diffuse }` splits it: `through` goes straight (the picture of the map), `diffuse` is scattered by the
|
|
248
|
-
* fibres (the underside glows, a soft fill with no picture) - dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
|
|
249
|
-
transmit?: Record<string, LightmapTransmit>
|
|
250
|
-
/** FOR THE BAKE: CUBES BY NAME — regular expressions (ECMAScript, case-sensitive) searched in every baked mesh's key
|
|
251
|
-
* (`<node path>@<ordinal>`): `['SM_pipe', 'SM_fence']` bakes every pipe and fence node as ambient cubes instead of atlas
|
|
252
|
-
* texels, like a node's `lightmap: 'cube'`. The CLI's `--cube` adds to these. */
|
|
253
|
-
cube?: string[]
|
|
254
|
-
/** `asset('./assets/lightmap/level.lgrid')` — THE LIGHT GRID for movers: every node a Physics aspect or a
|
|
255
|
-
* CharacterController moves (and what code marks with `Lightmap.track`) takes its ambient light from the bake at the
|
|
256
|
-
* place it is at, whatever its shader. Absent = movers keep the scene's IBL. */
|
|
257
|
-
volume?: string
|
|
258
|
-
/** `asset('./assets/lightmap/level-uv1.bin')` — THE PACKED LIGHTMAP UV1 the bake's rects map: it packs every mesh's
|
|
259
|
-
* lightmap charts into a square of their own instead of the unwrap's (once per mesh, whatever model holds it), and the
|
|
260
|
-
* engine swaps this into the models' TEXCOORD_1 before the rects go on. Required when the bake wrote one (the .bake's `uv1`): without it the statics
|
|
261
|
-
* read the wrong texels, so the bake is not applied. */
|
|
262
|
-
uv1?: string
|
|
263
|
-
/** A DEBUG multiplier on the atlas' lux (default 1). */
|
|
264
|
-
lightScale?: number
|
|
265
|
-
}
|
|
266
|
-
|
|
267
224
|
/** `env.navmesh` — the level's navigation mesh (see navmesh.md): the file `lecodes navmesh bake`
|
|
268
225
|
* writes, the agent size it is built for, and the named areas. Absent = no navigation. */
|
|
269
226
|
export type SceneNavmeshDef = {
|
|
@@ -276,7 +233,9 @@ export type SceneNavmeshDef = {
|
|
|
276
233
|
}
|
|
277
234
|
|
|
278
235
|
export type SceneDef = {
|
|
279
|
-
|
|
236
|
+
/** `lightmap` = the level's baked lighting (engines/bake): `bake('./assets/lightmap/level.bake', { …options })`, the
|
|
237
|
+
* macro that reads the bake's manifest at compile time (`LightmapDef`). Absent = real-time only. */
|
|
238
|
+
env?: SceneOptions & { lightmap?: LightmapDef, navmesh?: SceneNavmeshDef, foliage?: FoliageOptions }
|
|
280
239
|
camera?: SceneCameraDef
|
|
281
240
|
nodes?: Record<string, SceneNodeDef>
|
|
282
241
|
}
|
|
@@ -503,12 +462,23 @@ export const createSource = (path: string, def: SceneNodeDef, lightmap: boolean)
|
|
|
503
462
|
if (def.model !== undefined) return loadModel(def.model, lightmap, !!def.foliage)
|
|
504
463
|
if (def.mesh !== undefined) return createMesh(def.mesh, def.material)
|
|
505
464
|
if (def.terrain !== undefined) return createTerrain(def.terrain, lightmap)
|
|
506
|
-
if (def.light !== undefined)
|
|
465
|
+
if (def.light !== undefined) return createLight(path, def.light)
|
|
507
466
|
// make/prefab/camera/group nodes are plain wrappers — make/prefab subtrees mount under them
|
|
508
467
|
// during the build (attachPrefab) or in phase 2 (attachMake); a camera node drives scene.camera
|
|
509
468
|
return new Node()
|
|
510
469
|
}
|
|
511
470
|
|
|
471
|
+
/** A `light:` block by its kind. The node IS the light: its transform (applyNode) aims a sun / spot and turns a
|
|
472
|
+
* point light's `bakeArea` panel (local XZ, emitting along local -Y). */
|
|
473
|
+
const createLight = (path: string, def: LightDef): Light => {
|
|
474
|
+
switch (def.kind) {
|
|
475
|
+
case "sun": { const { kind: _k, ...opts } = def; return Light.sun(opts) }
|
|
476
|
+
case "point": { const { kind: _k, ...opts } = def; return Light.point(opts) }
|
|
477
|
+
case "spot": { const { kind: _k, ...opts } = def; return Light.spot(opts) }
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`Scene node "${path}": light kind "${String((def as { kind?: unknown }).kind)}" is not sun, point or spot`)
|
|
480
|
+
}
|
|
481
|
+
|
|
512
482
|
/** Edit mode: play the preview pose on a model node (`editor: { clip, time }`) — idempotent, the
|
|
513
483
|
* harness re-applies it on inspector edits. No clip = back to the rest pose. */
|
|
514
484
|
export const applyEditorPose = (node: Node, pose: { clip?: string, time?: number } | undefined): void => {
|
|
@@ -1034,10 +1004,7 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
1034
1004
|
}
|
|
1035
1005
|
|
|
1036
1006
|
// the level is complete: apply the bake — or, under `lecodes lightmap bake`, run it
|
|
1037
|
-
if (lm && lightmap)
|
|
1038
|
-
await Lightmap.load(scene, { data: lightmap.data, light: lightmap.light, aux: lightmap.aux, direct: lightmap.direct, indirect: lightmap.indirect, probes: lightmap.probes, volume: lightmap.volume, uv1: lightmap.uv1 },
|
|
1039
|
-
{ lightScale: lightmap.lightScale, emissiveNits: lightmap.emissiveNits, emissiveBake: lightmap.emissiveBake, transmit: lightmap.transmit, cube: lightmap.cube })
|
|
1040
|
-
}
|
|
1007
|
+
if (lm && lightmap) await Lightmap.load(scene, lightmap)
|
|
1041
1008
|
// navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
|
|
1042
1009
|
// geometry for it (the static bodies exist by now; edit mode never loads it — v1)
|
|
1043
1010
|
if (navmesh && !(EDITOR && isEditMode())) {
|
package/src/ui/tree.ts
CHANGED
|
@@ -93,6 +93,7 @@ export const OWNED_KIND_ENTITY_2D = 5
|
|
|
93
93
|
export const OWNED_KIND_MEDIA = 6
|
|
94
94
|
export const OWNED_KIND_TWEEN = 7
|
|
95
95
|
export const OWNED_KIND_CANVAS = 8
|
|
96
|
+
export const OWNED_KIND_GL_RENDER_VIEW = 9
|
|
96
97
|
|
|
97
98
|
// PanDirection (creator-pkg.h) — the low byte of a track answer.
|
|
98
99
|
import { DEAD_HANDLE } from "../core/pins"
|
package/src/version.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
|
|
5
5
|
// and a host's embedded version must agree on the major for the host to run the bundle; minor
|
|
6
6
|
// and patch never gate anything (features are detected, never versioned).
|
|
7
|
-
export const SDK_VERSION = "2.4.
|
|
7
|
+
export const SDK_VERSION = "2.4.9"
|
|
8
8
|
|
|
9
9
|
/** The major of a semver string, or null when it is not one. */
|
|
10
10
|
export const sdkMajor = (version: string | null | undefined): number | null => {
|