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.
@@ -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) { _creator.addEntityToScene(this._id, n.id); nodes.pin(n) } // the scene owns it now
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 one SDK type the macros return: an SVG image source, the same `SvgSource` a project has as
5
- // a global (build-types.ts points this import into dist/types when it copies the file).
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"
@@ -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 LightmapTransmit } from "../gl/Lightmap"
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
- export type LightDef = { kind: "sun" } & SunOptions
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.cube`. */
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
- env?: SceneOptions & { lightmap?: SceneLightmapDef, navmesh?: SceneNavmeshDef, foliage?: FoliageOptions }
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) { const { kind: _k, ...opts } = def.light; return Light.sun(opts) }
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"
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 => {