lecodes-sdk 0.19.0
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/README.md +114 -0
- package/dist/global.d.ts +503 -0
- package/dist/host.d.ts +61 -0
- package/dist/inject.js +4730 -0
- package/dist/types/animate/animate.d.ts +20 -0
- package/dist/types/animate/bezier.d.ts +2 -0
- package/dist/types/animate/easings.d.ts +21 -0
- package/dist/types/canvas/Canvas.d.ts +168 -0
- package/dist/types/core/Aspect.d.ts +171 -0
- package/dist/types/core/InspectorUI.d.ts +88 -0
- package/dist/types/core/StateMachine.d.ts +81 -0
- package/dist/types/core/color.d.ts +13 -0
- package/dist/types/core/compWrite.d.ts +11 -0
- package/dist/types/core/events.d.ts +11 -0
- package/dist/types/core/fields.d.ts +58 -0
- package/dist/types/core/registry.d.ts +7 -0
- package/dist/types/core/signals.d.ts +29 -0
- package/dist/types/core/time.d.ts +31 -0
- package/dist/types/g2/Camera2D.d.ts +19 -0
- package/dist/types/g2/CharacterController2D.d.ts +134 -0
- package/dist/types/g2/Node2D.d.ts +91 -0
- package/dist/types/g2/OneWay2D.d.ts +24 -0
- package/dist/types/g2/Physics2D.d.ts +147 -0
- package/dist/types/g2/Scene2D.d.ts +72 -0
- package/dist/types/g2/Shape2D.d.ts +58 -0
- package/dist/types/g2/Sprite.d.ts +51 -0
- package/dist/types/g2/SpriteAnimation.d.ts +50 -0
- package/dist/types/g2/SpriteSheet.d.ts +69 -0
- package/dist/types/g2/Texture2D.d.ts +16 -0
- package/dist/types/g2/Tilemap.d.ts +20 -0
- package/dist/types/g2/Tileset.d.ts +32 -0
- package/dist/types/g2/Trigger2D.d.ts +20 -0
- package/dist/types/g2/autotile.d.ts +133 -0
- package/dist/types/g2/cells.d.ts +14 -0
- package/dist/types/g2/defineScene2d.d.ts +141 -0
- package/dist/types/g2/groups2d.d.ts +16 -0
- package/dist/types/g2/loop.d.ts +4 -0
- package/dist/types/g2/scenarios2d.d.ts +30 -0
- package/dist/types/g2/touch.d.ts +2 -0
- package/dist/types/gl/Camera.d.ts +79 -0
- package/dist/types/gl/CameraPlace.d.ts +26 -0
- package/dist/types/gl/CharacterController.d.ts +98 -0
- package/dist/types/gl/Gearbox.d.ts +86 -0
- package/dist/types/gl/Geometry.d.ts +39 -0
- package/dist/types/gl/IK.d.ts +53 -0
- package/dist/types/gl/InstancedMesh.d.ts +46 -0
- package/dist/types/gl/Light.d.ts +73 -0
- package/dist/types/gl/Lightmap.d.ts +85 -0
- package/dist/types/gl/Locomotion.d.ts +279 -0
- package/dist/types/gl/Material.d.ts +100 -0
- package/dist/types/gl/Mesh.d.ts +33 -0
- package/dist/types/gl/Model.d.ts +53 -0
- package/dist/types/gl/NavAgent.d.ts +99 -0
- package/dist/types/gl/NavMesh.d.ts +169 -0
- package/dist/types/gl/Node.d.ts +99 -0
- package/dist/types/gl/Noise.d.ts +20 -0
- package/dist/types/gl/Particles.d.ts +277 -0
- package/dist/types/gl/Physics.d.ts +91 -0
- package/dist/types/gl/Plane.d.ts +19 -0
- package/dist/types/gl/Ragdoll.d.ts +86 -0
- package/dist/types/gl/Ray.d.ts +8 -0
- package/dist/types/gl/Scene.d.ts +221 -0
- package/dist/types/gl/Shape.d.ts +122 -0
- package/dist/types/gl/Terrain.d.ts +280 -0
- package/dist/types/gl/Texture.d.ts +27 -0
- package/dist/types/gl/Trigger.d.ts +10 -0
- package/dist/types/gl/Vehicle.d.ts +191 -0
- package/dist/types/gl/Wheel.d.ts +95 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
- package/dist/types/gl/animation/Animator.d.ts +219 -0
- package/dist/types/gl/animation/Layer.d.ts +31 -0
- package/dist/types/gl/animation/Loop.d.ts +17 -0
- package/dist/types/gl/animation/Playback.d.ts +36 -0
- package/dist/types/gl/animation/core.d.ts +367 -0
- package/dist/types/gl/controls.d.ts +19 -0
- package/dist/types/gl/physicsEvents.d.ts +1 -0
- package/dist/types/gl/scenarios.d.ts +98 -0
- package/dist/types/gl/state.d.ts +5 -0
- package/dist/types/gl/terrainMesh.d.ts +46 -0
- package/dist/types/gl/touch.d.ts +2 -0
- package/dist/types/inject.d.ts +123 -0
- package/dist/types/math/Mathf.d.ts +39 -0
- package/dist/types/math/mat4.d.ts +66 -0
- package/dist/types/math/quat.d.ts +53 -0
- package/dist/types/math/vec.d.ts +129 -0
- package/dist/types/net/codec.d.ts +25 -0
- package/dist/types/net/core.d.ts +106 -0
- package/dist/types/net/index.d.ts +80 -0
- package/dist/types/net/replication.d.ts +101 -0
- package/dist/types/plugins/camera.d.ts +25 -0
- package/dist/types/plugins/geolocation.d.ts +39 -0
- package/dist/types/plugins/oauth.d.ts +25 -0
- package/dist/types/plugins/permission.d.ts +1 -0
- package/dist/types/plugins/push.d.ts +46 -0
- package/dist/types/plugins/qr.d.ts +21 -0
- package/dist/types/plugins/service.d.ts +33 -0
- package/dist/types/runtime/app.d.ts +67 -0
- package/dist/types/runtime/appEvents.d.ts +7 -0
- package/dist/types/runtime/channel.d.ts +1 -0
- package/dist/types/runtime/clipboard.d.ts +7 -0
- package/dist/types/runtime/datetime.d.ts +80 -0
- package/dist/types/runtime/device.d.ts +126 -0
- package/dist/types/runtime/fetch.d.ts +31 -0
- package/dist/types/runtime/files.d.ts +33 -0
- package/dist/types/runtime/input.d.ts +121 -0
- package/dist/types/runtime/media.d.ts +46 -0
- package/dist/types/runtime/misc.d.ts +11 -0
- package/dist/types/runtime/net.d.ts +15 -0
- package/dist/types/runtime/rpc.d.ts +44 -0
- package/dist/types/runtime/service.d.ts +1 -0
- package/dist/types/runtime/share.d.ts +2 -0
- package/dist/types/runtime/storage.d.ts +5 -0
- package/dist/types/runtime/touch.d.ts +56 -0
- package/dist/types/scene/defineScene.d.ts +296 -0
- package/dist/types/scene/editorPlugins.d.ts +82 -0
- package/dist/types/scene/gizmos.d.ts +56 -0
- package/dist/types/scene/grammar.d.ts +56 -0
- package/dist/types/scene/level.d.ts +98 -0
- package/dist/types/scene/material.d.ts +76 -0
- package/dist/types/server/auth/appConfig.d.ts +16 -0
- package/dist/types/server/auth/global.d.ts +56 -0
- package/dist/types/server/auth/models.d.ts +164 -0
- package/dist/types/server/auth/types.d.ts +56 -0
- package/dist/types/server/channel.d.ts +28 -0
- package/dist/types/server/context.d.ts +27 -0
- package/dist/types/server/db/defineDb.d.ts +36 -0
- package/dist/types/server/db/fields.d.ts +146 -0
- package/dist/types/server/db/index.d.ts +6 -0
- package/dist/types/server/db/marci/query.d.ts +326 -0
- package/dist/types/server/db/types.d.ts +163 -0
- package/dist/types/server/errors.d.ts +8 -0
- package/dist/types/server/inject.d.ts +5 -0
- package/dist/types/ui/NativeView.d.ts +38 -0
- package/dist/types/ui/UI.d.ts +27 -0
- package/dist/types/ui/UIBottomSheet.d.ts +71 -0
- package/dist/types/ui/UIButton.d.ts +30 -0
- package/dist/types/ui/UIContainer.d.ts +33 -0
- package/dist/types/ui/UIImage.d.ts +35 -0
- package/dist/types/ui/UIInput.d.ts +110 -0
- package/dist/types/ui/UIModal.d.ts +70 -0
- package/dist/types/ui/UINode.d.ts +363 -0
- package/dist/types/ui/UIPager.d.ts +123 -0
- package/dist/types/ui/UIPopover.d.ts +39 -0
- package/dist/types/ui/UIScreen.d.ts +57 -0
- package/dist/types/ui/UIScrollable.d.ts +40 -0
- package/dist/types/ui/UISpacer.d.ts +9 -0
- package/dist/types/ui/UITabs.d.ts +96 -0
- package/dist/types/ui/UIText.d.ts +19 -0
- package/dist/types/ui/UIVideo.d.ts +28 -0
- package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
- package/dist/types/ui/UIWidget.d.ts +65 -0
- package/dist/types/ui/fonts.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +68 -0
- package/dist/types/ui/router.d.ts +40 -0
- package/dist/types/ui/theme.d.ts +26 -0
- package/dist/types.json +1 -0
- package/package.json +46 -0
- package/prompts/2d.md +395 -0
- package/prompts/3d-scene-files.md +113 -0
- package/prompts/3d-scene.md +119 -0
- package/prompts/3d.md +303 -0
- package/prompts/README.md +142 -0
- package/prompts/ar.md +109 -0
- package/prompts/canvas.md +27 -0
- package/prompts/compose.ts +46 -0
- package/prompts/concept.md +49 -0
- package/prompts/core-design.md +75 -0
- package/prompts/core.md +264 -0
- package/prompts/design.md +218 -0
- package/prompts/dist/2d-game.md +1530 -0
- package/prompts/dist/3d-app.md +1672 -0
- package/prompts/dist/ar-app.md +1548 -0
- package/prompts/dist/concept.md +49 -0
- package/prompts/dist/design.md +498 -0
- package/prompts/dist/ui-app.md +1134 -0
- package/prompts/index.md +37 -0
- package/prompts/intake-prompt.md +33 -0
- package/prompts/namer-prompt.md +12 -0
- package/prompts/router-prompt.md +50 -0
- package/prompts/select.ts +111 -0
- package/prompts/ui-design.md +203 -0
- package/prompts/ui.md +803 -0
- package/src/animate/animate.ts +238 -0
- package/src/animate/bezier.ts +138 -0
- package/src/animate/easings.ts +126 -0
- package/src/bridges.d.ts +1345 -0
- package/src/canvas/Canvas.ts +305 -0
- package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
- package/src/compile/__tests__/assetMacro.test.ts +74 -0
- package/src/compile/__tests__/assetName.test.ts +55 -0
- package/src/compile/__tests__/compile.test.ts +287 -0
- package/src/compile/__tests__/detectEntry.test.ts +132 -0
- package/src/compile/__tests__/fontMacro.test.ts +199 -0
- package/src/compile/aspectMacro.ts +86 -0
- package/src/compile/assetIconMacro.ts +384 -0
- package/src/compile/assetMacro.ts +146 -0
- package/src/compile/assetName.ts +50 -0
- package/src/compile/bundler.ts +287 -0
- package/src/compile/compileProject.ts +127 -0
- package/src/compile/detectEntry.ts +128 -0
- package/src/compile/fontMacro.ts +459 -0
- package/src/compile/fontRegistry.ts +78 -0
- package/src/compile/header.ts +67 -0
- package/src/compile/index.ts +100 -0
- package/src/compile/libraryImports.ts +52 -0
- package/src/compile/liteMaterial.ts +247 -0
- package/src/compile/sceneEditor.ts +88 -0
- package/src/compile/serverSplit.ts +233 -0
- package/src/compile/serverTypes.ts +227 -0
- package/src/compile/sfnt.ts +98 -0
- package/src/compile/shaderSchema.ts +202 -0
- package/src/compile/shaderTargets.ts +77 -0
- package/src/compile/sourcemap.ts +25 -0
- package/src/core/Aspect.ts +568 -0
- package/src/core/InspectorUI.ts +212 -0
- package/src/core/StateMachine.ts +308 -0
- package/src/core/__tests__/stateMachine.test.ts +132 -0
- package/src/core/color.ts +66 -0
- package/src/core/compWrite.ts +42 -0
- package/src/core/events.ts +38 -0
- package/src/core/fields.ts +120 -0
- package/src/core/registry.ts +23 -0
- package/src/core/signals.ts +277 -0
- package/src/core/time.ts +84 -0
- package/src/g2/Camera2D.ts +40 -0
- package/src/g2/CharacterController2D.ts +276 -0
- package/src/g2/Node2D.ts +267 -0
- package/src/g2/OneWay2D.ts +66 -0
- package/src/g2/Physics2D.ts +346 -0
- package/src/g2/Scene2D.ts +209 -0
- package/src/g2/Shape2D.ts +259 -0
- package/src/g2/Sprite.ts +89 -0
- package/src/g2/SpriteAnimation.ts +171 -0
- package/src/g2/SpriteSheet.ts +166 -0
- package/src/g2/Texture2D.ts +47 -0
- package/src/g2/Tilemap.ts +41 -0
- package/src/g2/Tileset.ts +71 -0
- package/src/g2/Trigger2D.ts +77 -0
- package/src/g2/autotile.ts +433 -0
- package/src/g2/cells.ts +91 -0
- package/src/g2/defineScene2d.ts +381 -0
- package/src/g2/groups2d.ts +106 -0
- package/src/g2/loop.ts +50 -0
- package/src/g2/scenarios2d.ts +69 -0
- package/src/g2/touch.ts +83 -0
- package/src/gl/Camera.ts +160 -0
- package/src/gl/CameraPlace.ts +52 -0
- package/src/gl/CharacterController.ts +238 -0
- package/src/gl/Gearbox.ts +212 -0
- package/src/gl/Geometry.ts +279 -0
- package/src/gl/IK.ts +193 -0
- package/src/gl/InstancedMesh.ts +132 -0
- package/src/gl/Light.ts +135 -0
- package/src/gl/Lightmap.ts +402 -0
- package/src/gl/Locomotion.ts +498 -0
- package/src/gl/Material.ts +279 -0
- package/src/gl/Mesh.ts +83 -0
- package/src/gl/Model.ts +124 -0
- package/src/gl/NavAgent.ts +337 -0
- package/src/gl/NavMesh.ts +397 -0
- package/src/gl/Node.ts +350 -0
- package/src/gl/Noise.ts +30 -0
- package/src/gl/Particles.ts +676 -0
- package/src/gl/Physics.ts +222 -0
- package/src/gl/Plane.ts +53 -0
- package/src/gl/Ragdoll.ts +270 -0
- package/src/gl/Ray.ts +16 -0
- package/src/gl/Scene.ts +505 -0
- package/src/gl/Shape.ts +409 -0
- package/src/gl/Terrain.ts +1092 -0
- package/src/gl/Texture.ts +63 -0
- package/src/gl/Trigger.ts +45 -0
- package/src/gl/Vehicle.ts +473 -0
- package/src/gl/Wheel.ts +240 -0
- package/src/gl/animation/AnimationClip.ts +204 -0
- package/src/gl/animation/Animator.ts +329 -0
- package/src/gl/animation/Layer.ts +39 -0
- package/src/gl/animation/Loop.ts +31 -0
- package/src/gl/animation/Playback.ts +52 -0
- package/src/gl/animation/core.ts +670 -0
- package/src/gl/controls.ts +95 -0
- package/src/gl/physicsEvents.ts +20 -0
- package/src/gl/scenarios.ts +291 -0
- package/src/gl/state.ts +6 -0
- package/src/gl/terrainMesh.ts +219 -0
- package/src/gl/touch.ts +68 -0
- package/src/host.d.ts +61 -0
- package/src/inject.ts +203 -0
- package/src/math/Mathf.ts +118 -0
- package/src/math/mat4.ts +278 -0
- package/src/math/quat.ts +232 -0
- package/src/math/vec.ts +255 -0
- package/src/net/codec.ts +119 -0
- package/src/net/core.ts +384 -0
- package/src/net/index.ts +181 -0
- package/src/net/replication.ts +622 -0
- package/src/plugins/camera.ts +81 -0
- package/src/plugins/geolocation.ts +123 -0
- package/src/plugins/oauth.ts +61 -0
- package/src/plugins/permission.ts +7 -0
- package/src/plugins/push.ts +132 -0
- package/src/plugins/qr.ts +73 -0
- package/src/plugins/service.ts +47 -0
- package/src/runtime/app.ts +101 -0
- package/src/runtime/appEvents.ts +54 -0
- package/src/runtime/channel.ts +61 -0
- package/src/runtime/clipboard.ts +20 -0
- package/src/runtime/datetime.ts +329 -0
- package/src/runtime/device.ts +293 -0
- package/src/runtime/fetch.ts +77 -0
- package/src/runtime/files.ts +108 -0
- package/src/runtime/input.ts +175 -0
- package/src/runtime/media.ts +111 -0
- package/src/runtime/misc.ts +16 -0
- package/src/runtime/net.ts +36 -0
- package/src/runtime/rpc.ts +218 -0
- package/src/runtime/service.ts +83 -0
- package/src/runtime/share.ts +9 -0
- package/src/runtime/storage.ts +13 -0
- package/src/runtime/touch.ts +76 -0
- package/src/scene/defineScene.ts +1396 -0
- package/src/scene/editorPlugins.ts +110 -0
- package/src/scene/gizmos.ts +148 -0
- package/src/scene/grammar.ts +120 -0
- package/src/scene/level.ts +296 -0
- package/src/scene/material.ts +188 -0
- package/src/server/auth/appConfig.ts +12 -0
- package/src/server/auth/global.ts +80 -0
- package/src/server/auth/host.ts +318 -0
- package/src/server/auth/models.ts +83 -0
- package/src/server/auth/types.ts +50 -0
- package/src/server/channel.ts +56 -0
- package/src/server/context.ts +36 -0
- package/src/server/db/defineDb.ts +237 -0
- package/src/server/db/fields.ts +132 -0
- package/src/server/db/httpTransport.ts +93 -0
- package/src/server/db/index.ts +7 -0
- package/src/server/db/marci/query.ts +412 -0
- package/src/server/db/types.ts +202 -0
- package/src/server/errors.ts +12 -0
- package/src/server/host.ts +74 -0
- package/src/server/inject.ts +13 -0
- package/src/server/runtime.ts +133 -0
- package/src/server/validate.ts +87 -0
- package/src/ui/NativeView.ts +142 -0
- package/src/ui/UI.ts +39 -0
- package/src/ui/UIBottomSheet.ts +139 -0
- package/src/ui/UIButton.ts +101 -0
- package/src/ui/UIContainer.ts +60 -0
- package/src/ui/UIImage.ts +83 -0
- package/src/ui/UIInput.ts +185 -0
- package/src/ui/UIModal.ts +139 -0
- package/src/ui/UINode.ts +830 -0
- package/src/ui/UIPager.ts +362 -0
- package/src/ui/UIPopover.ts +100 -0
- package/src/ui/UIScreen.ts +123 -0
- package/src/ui/UIScrollable.ts +87 -0
- package/src/ui/UISpacer.ts +14 -0
- package/src/ui/UITabs.ts +236 -0
- package/src/ui/UIText.ts +51 -0
- package/src/ui/UIVideo.ts +88 -0
- package/src/ui/UIVirtualizedList.ts +241 -0
- package/src/ui/UIWidget.ts +127 -0
- package/src/ui/fonts.ts +13 -0
- package/src/ui/presentable.ts +117 -0
- package/src/ui/router.ts +132 -0
- package/src/ui/theme.ts +84 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// One geometry drawn many times — grass tufts, crates, debris, coins, a crowd of identical props —
|
|
2
|
+
// with a transform per instance and a handful of draw calls. Hardware instancing (Filament's
|
|
3
|
+
// InstanceBuffer): the engine applies each instance's local matrix, so any regular material works.
|
|
4
|
+
//
|
|
5
|
+
// Transforms live in a CPU-side Float32Array (16 floats per instance, column-major, local to this
|
|
6
|
+
// node). `setAt` / `hideAt` / `setMatrixAt` edit it and track the dirty range; `commit()` uploads
|
|
7
|
+
// that range in one bridge call — call it once per frame after your edits, not per instance.
|
|
8
|
+
// The native side splits the instances into chunks of the platform's per-draw cap (64 native,
|
|
9
|
+
// 8 on WebGL) on child entities, and keeps a bounding box per chunk so frustum culling stays on.
|
|
10
|
+
|
|
11
|
+
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
12
|
+
import { Quat, type QuatLike } from "../math/quat"
|
|
13
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
14
|
+
import type { Geometry } from "./Geometry"
|
|
15
|
+
import { Material } from "./Material"
|
|
16
|
+
import type { MeshOptions } from "./Mesh"
|
|
17
|
+
import { Node } from "./Node"
|
|
18
|
+
|
|
19
|
+
/** One instance's placement, local to the InstancedMesh node. Omitted fields mean identity. */
|
|
20
|
+
export type InstanceTransform = {
|
|
21
|
+
position?: Vec3Like
|
|
22
|
+
/** Quaternion; wins over `eulerAngles`. */
|
|
23
|
+
rotation?: QuatLike
|
|
24
|
+
/** Degrees (YXZ), like `Node.eulerAngles`. */
|
|
25
|
+
eulerAngles?: Vec3Like
|
|
26
|
+
scale?: Vec3Like | number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEG = Math.PI / 180
|
|
30
|
+
|
|
31
|
+
export class InstancedMesh extends Node {
|
|
32
|
+
/** Instance capacity, fixed at construction. Unset instances are hidden. */
|
|
33
|
+
readonly count: number
|
|
34
|
+
private _geometry: Geometry
|
|
35
|
+
private _material: Material
|
|
36
|
+
private _data: Float32Array
|
|
37
|
+
private _lo = Infinity
|
|
38
|
+
private _hi = -1
|
|
39
|
+
|
|
40
|
+
constructor(geometry: Geometry, count: number, options: MeshOptions = {}) {
|
|
41
|
+
super()
|
|
42
|
+
if (!(count >= 1)) throw new Error("InstancedMesh: count must be ≥ 1")
|
|
43
|
+
this.count = Math.floor(count)
|
|
44
|
+
this._geometry = geometry
|
|
45
|
+
this._material = options.material ?? Material.unlit()
|
|
46
|
+
this._data = new Float32Array(16 * this.count)
|
|
47
|
+
_creator.createInstancedMesh(this.id, Material.idOf(this._material), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, this.count)
|
|
48
|
+
if (options.position) this.position = options.position
|
|
49
|
+
if (options.eulerAngles) this.eulerAngles = options.eulerAngles
|
|
50
|
+
if (options.scale !== undefined) this.scale = typeof options.scale === "number" ? [ options.scale, options.scale, options.scale ] : options.scale
|
|
51
|
+
if (options.name !== undefined) this.name = options.name
|
|
52
|
+
if (options.castShadows !== undefined || options.receiveShadows !== undefined) {
|
|
53
|
+
_creator.setInstancedMeshShadows(this.id, options.castShadows ?? true, options.receiveShadows ?? true)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override get geometry(): Geometry { return this._geometry }
|
|
58
|
+
|
|
59
|
+
get material(): Material { return this._material }
|
|
60
|
+
set material(m: Material) {
|
|
61
|
+
this._material = m
|
|
62
|
+
_creator.setInstancedMeshMaterial(this.id, Material.idOf(m))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Shadow flags for all instances (both at once — the engine sets them per draw). */
|
|
66
|
+
setShadows(cast: boolean, receive = cast): this {
|
|
67
|
+
_creator.setInstancedMeshShadows(this.id, cast, receive)
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private _touch(i: number): void {
|
|
72
|
+
if (i < this._lo) this._lo = i
|
|
73
|
+
if (i > this._hi) this._hi = i
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Place instance `i`. Pending until `commit()`. */
|
|
77
|
+
setAt(i: number, t: InstanceTransform): this {
|
|
78
|
+
if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
|
|
79
|
+
const rot = t.rotation ?? (t.eulerAngles
|
|
80
|
+
? Quat.fromEuler(cx(t.eulerAngles) * DEG, cy(t.eulerAngles) * DEG, cz(t.eulerAngles) * DEG)
|
|
81
|
+
: Quat.identity)
|
|
82
|
+
const m = Mat4.compose(t.position ?? [ 0, 0, 0 ], rot, t.scale ?? 1).toArray()
|
|
83
|
+
this._data.set(m, i * 16)
|
|
84
|
+
this._touch(i)
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Place instance `i` from a 4×4 column-major matrix (local to this node). */
|
|
89
|
+
setMatrixAt(i: number, m: Mat4Like): this {
|
|
90
|
+
if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
|
|
91
|
+
const a = m instanceof Mat4 ? m.toArray() : m
|
|
92
|
+
for (let k = 0; k < 16; k++) this._data[i * 16 + k] = a[k]
|
|
93
|
+
this._touch(i)
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Hide instance `i` (a zero matrix — no geometry is emitted for it). */
|
|
98
|
+
hideAt(i: number): this {
|
|
99
|
+
if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
|
|
100
|
+
this._data.fill(0, i * 16, i * 16 + 16)
|
|
101
|
+
this._touch(i)
|
|
102
|
+
return this
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Bulk write: `matrices` holds 16 floats per instance, starting at instance `offset`. */
|
|
106
|
+
setMatrices(matrices: ArrayLike<number>, offset = 0): this {
|
|
107
|
+
const n = Math.min(Math.floor(matrices.length / 16), this.count - offset)
|
|
108
|
+
if (n <= 0) return this
|
|
109
|
+
this._data.set(Array.prototype.slice.call(matrices, 0, n * 16), offset * 16)
|
|
110
|
+
this._touch(offset)
|
|
111
|
+
this._touch(offset + n - 1)
|
|
112
|
+
return this
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The 4×4 of instance `i` as stored (a copy). */
|
|
116
|
+
matrixAt(i: number): Mat4 {
|
|
117
|
+
return Mat4.from(this._data.subarray(i * 16, i * 16 + 16))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Upload every change since the last commit (one bridge call covering the dirty range). */
|
|
121
|
+
commit(): this {
|
|
122
|
+
if (this._hi < this._lo) return this
|
|
123
|
+
const lo = this._lo, hi = this._hi
|
|
124
|
+
this._lo = Infinity
|
|
125
|
+
this._hi = -1
|
|
126
|
+
_creator.setInstanceTransforms(this.id, this._data.subarray(lo * 16, (hi + 1) * 16), lo)
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Whether edits are waiting for `commit()`. */
|
|
131
|
+
get dirty(): boolean { return this._hi >= this._lo }
|
|
132
|
+
}
|
package/src/gl/Light.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Lights. A directional sun (the engine's primary light) and punctual point lights. Add one to a
|
|
2
|
+
// scene like any node; `intensity` and `color` stay writable so a light can be animated.
|
|
3
|
+
|
|
4
|
+
import { Color, type ColorInput } from "../core/color"
|
|
5
|
+
import type { Vec3Like } from "../math/vec"
|
|
6
|
+
import { Node } from "./Node"
|
|
7
|
+
|
|
8
|
+
export type SunOptions = {
|
|
9
|
+
/** Light direction (points where the light travels). Defaults to a typical key-light angle. */
|
|
10
|
+
direction?: Vec3Like
|
|
11
|
+
/** Luminous intensity. */
|
|
12
|
+
intensity?: number
|
|
13
|
+
color?: ColorInput
|
|
14
|
+
/**
|
|
15
|
+
* Shadow quality (default 1):
|
|
16
|
+
* 0 - no shadows
|
|
17
|
+
* 1 - 1024 map, hard edges (one filtered tap; the cheap level, ~3 % of a frame on an iGPU)
|
|
18
|
+
* 2 - 1024 map, soft edges (variance shadows + blur; ~+15 % of a frame on an iGPU)
|
|
19
|
+
* 3 - 2048 map, contact-hardening soft edges (PCSS: sharp where the caster touches, softer
|
|
20
|
+
* away; discrete-GPU territory, ~+60 % of a frame on an iGPU)
|
|
21
|
+
* A lightmapped level already carries every static shadow, so 0 is a legitimate choice there.
|
|
22
|
+
*/
|
|
23
|
+
shadowsQuality?: 0 | 1 | 2 | 3
|
|
24
|
+
/**
|
|
25
|
+
* Metres from the camera within which the sun casts shadows (default 100). Shadows fade out by
|
|
26
|
+
* this distance and the shadow map covers only this range, so a shorter distance is crisper for
|
|
27
|
+
* the same quality and puts fewer casters in the shadow pass. 30-40 is plenty in first person.
|
|
28
|
+
*/
|
|
29
|
+
shadowDistance?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type PointOptions = {
|
|
33
|
+
/**
|
|
34
|
+
* Luminous POWER in lumens — the same physical scale as the sun's lux and `environmentIntensity`,
|
|
35
|
+
* so a light keeps its look when the scene's exposure changes. A candle is ~12 lm, a bare 60 W
|
|
36
|
+
* bulb ~800, a car headlight ~1500, a fireball millions. Default 1000.
|
|
37
|
+
*/
|
|
38
|
+
intensity?: number
|
|
39
|
+
color?: ColorInput
|
|
40
|
+
/**
|
|
41
|
+
* Metres of influence — past it the light contributes nothing. This is the performance knob:
|
|
42
|
+
* overlapping point lights are the expensive case, so keep it as small as the look allows.
|
|
43
|
+
* Default 10.
|
|
44
|
+
*/
|
|
45
|
+
range?: number
|
|
46
|
+
/** Point-light shadows are a cubemap render per light; off by default. */
|
|
47
|
+
castShadows?: boolean
|
|
48
|
+
/**
|
|
49
|
+
* Whether `lecodes lightmap bake` bakes this light into the level's lightmap (docs/lightmap-plan.md).
|
|
50
|
+
* A baked light is switched OFF at runtime once the bake applies — statics read it from the atlas,
|
|
51
|
+
* movers from the light volume — so it costs nothing per frame; a light that must stay live (a
|
|
52
|
+
* flicker, a lamp the player can shoot out) says `baked: false` and is kept out of the bake.
|
|
53
|
+
* Lights created after the level loads (a muzzle flash) are never in a bake. Default true.
|
|
54
|
+
*/
|
|
55
|
+
baked?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class Light extends Node {
|
|
59
|
+
private _intensity = 0
|
|
60
|
+
/** @internal PointOptions.baked — a candidate for the lightmap bake (default true). */
|
|
61
|
+
_baked = true
|
|
62
|
+
/** @internal the bake carries this light: the real-time one is held dark (Lightmap.load). */
|
|
63
|
+
_bakedOff = false
|
|
64
|
+
/** @internal every live point light, for the bake / the switch-off; a destroyed one leaves. */
|
|
65
|
+
static _points = new Set<Light>()
|
|
66
|
+
/** @internal sun direction as created (the engine keeps it; Lightmap.load reads it back). */
|
|
67
|
+
_direction: [number, number, number] = [ 0, -1, 0 ]
|
|
68
|
+
/** The most recently created sun — what `Lightmap.load` uses unless told otherwise. */
|
|
69
|
+
static lastSun: Light | null = null
|
|
70
|
+
|
|
71
|
+
/** A directional sun light. */
|
|
72
|
+
static sun(options: SunOptions = {}): Light {
|
|
73
|
+
const light = new Light()
|
|
74
|
+
const [ dx, dy, dz ] = options.direction ?? [ 0.548267, -0.473983, -0.689016 ]
|
|
75
|
+
light._direction = [ dx, dy, dz ]
|
|
76
|
+
light._intensity = options.intensity ?? 100000
|
|
77
|
+
Light.lastSun = light
|
|
78
|
+
_creator.createSunLight(
|
|
79
|
+
light.id, dx, dy, dz,
|
|
80
|
+
light._intensity,
|
|
81
|
+
Color.toPackedRgb(options.color ?? 0xffffff),
|
|
82
|
+
options.shadowsQuality ?? 1,
|
|
83
|
+
options.shadowDistance ?? 100,
|
|
84
|
+
)
|
|
85
|
+
return light
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A point light — a lamp, a muzzle flash, a fireball. Position it like any node.
|
|
90
|
+
*
|
|
91
|
+
* Feature-detected: hosts that predate it create the node and light nothing, which keeps a scene
|
|
92
|
+
* that adds atmosphere on top of its sun renderable everywhere. Check `Light.supportsPoint`
|
|
93
|
+
* before making one carry the scene.
|
|
94
|
+
*/
|
|
95
|
+
static point(options: PointOptions = {}): Light {
|
|
96
|
+
const light = new Light()
|
|
97
|
+
light._intensity = options.intensity ?? 1000
|
|
98
|
+
light._baked = options.baked ?? true
|
|
99
|
+
Light._points.add(light)
|
|
100
|
+
_creator.createPointLight?.(
|
|
101
|
+
light.id,
|
|
102
|
+
light._intensity,
|
|
103
|
+
Color.toPackedRgb(options.color ?? 0xffffff),
|
|
104
|
+
options.range ?? 10,
|
|
105
|
+
options.castShadows ?? false,
|
|
106
|
+
)
|
|
107
|
+
return light
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Whether this host can create point lights at all. */
|
|
111
|
+
static get supportsPoint(): boolean { return _creator.createPointLight !== undefined }
|
|
112
|
+
|
|
113
|
+
/** Live intensity (sun: lux, point: lumens) — animate a flash without rebuilding the light. */
|
|
114
|
+
get intensity(): number { return this._intensity }
|
|
115
|
+
set intensity(value: number) {
|
|
116
|
+
this._intensity = value
|
|
117
|
+
_creator.setLightIntensity?.(this.id, this._bakedOff ? 0 : value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @internal Hold a baked point light dark (the atlas / volume carries it) or hand it back to the real-time path. */
|
|
121
|
+
_setBakedOff(off: boolean): void {
|
|
122
|
+
if (this._bakedOff === off) return
|
|
123
|
+
this._bakedOff = off
|
|
124
|
+
_creator.setLightIntensity?.(this.id, off ? 0 : this._intensity)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
destroy(): void {
|
|
128
|
+
Light._points.delete(this)
|
|
129
|
+
super.destroy()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
set color(value: ColorInput) {
|
|
133
|
+
_creator.setLightColor?.(this.id, Color.toPackedRgb(value))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// Baked lighting for static 3D geometry (docs/lightmap-plan.md). A scene FILE does all of this by
|
|
2
|
+
// itself: `env: { lightmap: { data, texture } }` marks the level as baked, every model/mesh node
|
|
3
|
+
// that no Physics aspect moves is a static (keys = node paths, `lightmap: true|false` overrides),
|
|
4
|
+
// and the runtime applies the bake when the level finishes loading (scene/defineScene.ts).
|
|
5
|
+
// Hand-built scenes register what never moves with `Lightmap.add`, bake once with
|
|
6
|
+
// `lecodes lightmap bake` (the desktop host traces the live scene and writes lightmap.ktx2 +
|
|
7
|
+
// lightmap.bake), and `Lightmap.load` applies the atlas at runtime:
|
|
8
|
+
//
|
|
9
|
+
// const crate = await Model.load(asset('./assets/props/crate.glb'), { lightmap: true })
|
|
10
|
+
// Lightmap.add(holder) // a Model, a Mesh, or a node holding them
|
|
11
|
+
// Lightmap.add(ground) // Mesh.plane works too
|
|
12
|
+
// ...
|
|
13
|
+
// await Lightmap.load(scene, { data: asset('./assets/lightmap/lightmap.bake'),
|
|
14
|
+
// texture: asset('./assets/lightmap/lightmap.ktx2') })
|
|
15
|
+
//
|
|
16
|
+
// Both files are asset() handles because the compiler stages exactly the files it sees (the data
|
|
17
|
+
// file is JSON with a .bake extension — a .json would be scanned as a code module). `load` is a
|
|
18
|
+
// no-op when the bake is missing or the host predates the feature, so the project stays runnable —
|
|
19
|
+
// and in BAKE mode (`lecodes lightmap bake`) it is where the bake fires: the same call marks "the
|
|
20
|
+
// scene is complete". Atlas channels: R = sun visibility, G = ambient occlusion; the shader keeps
|
|
21
|
+
// the real-time sun for dynamic casters and multiplies the baked mask in with an ambient floor.
|
|
22
|
+
//
|
|
23
|
+
// PAGES: a large level takes more than one atlas (`--pages`); `texture` is then every page in order.
|
|
24
|
+
//
|
|
25
|
+
// POINT LIGHTS: the bake traces the scene's point lights too (filament's own candela / falloff
|
|
26
|
+
// math, shadow rays) into a second RGB atlas — pass it as `light` — and lists them in the .bake;
|
|
27
|
+
// once that atlas applies, the real-time lights it carries are switched off (`Light.point({ baked:
|
|
28
|
+
// false })` keeps one live and out of the bake). Statics read the lamps from the atlas, movers from
|
|
29
|
+
// the volume's light block — for the price of nothing per frame.
|
|
30
|
+
//
|
|
31
|
+
// DYNAMIC objects (docs/lightmap-plan.md §9): the bake also writes lightmap.volume — the same
|
|
32
|
+
// channels on a 3D grid over the level. Pass it as `volume` and every model loaded with
|
|
33
|
+
// `{ lightmap: 'dynamic' }` (the default while a scene file with `env.lightmap.volume` runs) plus
|
|
34
|
+
// every dynamic Mesh a scene file registers samples it per pixel: a crate rolled under the roof
|
|
35
|
+
// loses the sun like the floor does, a character near a wall picks up its ambient occlusion.
|
|
36
|
+
|
|
37
|
+
import { fetch } from "../runtime/fetch"
|
|
38
|
+
import { Texture } from "./Texture"
|
|
39
|
+
import { Material } from "./Material"
|
|
40
|
+
import { Mesh } from "./Mesh"
|
|
41
|
+
import { Model } from "./Model"
|
|
42
|
+
import { Node } from "./Node"
|
|
43
|
+
import { Light } from "./Light"
|
|
44
|
+
import { cx, cy, cz } from "../math/vec"
|
|
45
|
+
import type { Scene } from "./Scene"
|
|
46
|
+
|
|
47
|
+
export type LightmapLoadOptions = {
|
|
48
|
+
/** 1 = baked sun shadows at full strength, 0 = ambient occlusion only. Default 1. */
|
|
49
|
+
sunStrength?: number
|
|
50
|
+
/** Multiplier on the ambient (IBL) share in the shadow math — how bright a fully shadowed texel
|
|
51
|
+
* stays. 1 reproduces filament's own real-time shadow darkness (the shader reads the scene's sun
|
|
52
|
+
* and IBL from filament's per-frame uniforms); raise it for lighter shadows, lower for deeper. Default 1. */
|
|
53
|
+
ambientScale?: number
|
|
54
|
+
/** How much of the baked ambient occlusion applies: 1 = all of it, 0 = none. An interior lit by
|
|
55
|
+
* its ambient probe alone can want less than the geometric truth. Default 1. */
|
|
56
|
+
aoStrength?: number
|
|
57
|
+
/** Multiplier on the baked point lights' irradiance (default 1). Purely ARTISTIC: the bake is
|
|
58
|
+
* physical (verified against the analytic 1/d^2 sum over the level's lamps - where line of sight
|
|
59
|
+
* is clear the two agree within ~17 %), and a room lit to its real illuminance simply reads dim
|
|
60
|
+
* against an ambient fill. This dials the lamps up without a re-bake, since their irradiance is
|
|
61
|
+
* IN the atlas and `--lamp-gain` would cost a full one.
|
|
62
|
+
* It scales the atlas AND the light volume together, so statics and movers stay consistent - but
|
|
63
|
+
* note they are NOT equally forgiving: a static surface carries the bake's own cosine and
|
|
64
|
+
* shadowing, while a mover reads the volume through a flat half-cosine, so a large boost blows
|
|
65
|
+
* movers out long before it blows out the level. Past ~x100 expect to light the movers
|
|
66
|
+
* separately. */
|
|
67
|
+
lightBoost?: number
|
|
68
|
+
/** Multiplier on the light VOLUME's irradiance alone — the movers' own `lightBoost`. Default =
|
|
69
|
+
* `lightBoost`, and since 2026-08-31 that default is the right one: the volume is an ambient cube
|
|
70
|
+
* (irradiance per axis face, blended by the pixel's normal), so a mover's top takes a lamp above it
|
|
71
|
+
* and its underside does not — the same shading the atlas gives the statics. Before that the volume
|
|
72
|
+
* was one flat 0.5·E on every face and this knob was the workaround. Kept as a trim. */
|
|
73
|
+
volumeBoost?: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type LightmapInfo = {
|
|
77
|
+
size: number
|
|
78
|
+
texel: number
|
|
79
|
+
/** Atlas pages the bake took (`texture` lists them in order). */
|
|
80
|
+
pages: number
|
|
81
|
+
/** The light volume, when one was loaded: grid dims + cell size in metres. */
|
|
82
|
+
volume?: { dims: number[], cell: number }
|
|
83
|
+
/** Point lights the bake carries and this load switched off (statics read them from the atlas,
|
|
84
|
+
* movers from the volume); 0 without a `light` atlas. */
|
|
85
|
+
lights: number
|
|
86
|
+
/** keys applied / keys in the file / registered statics without a rect */
|
|
87
|
+
applied: number
|
|
88
|
+
total: number
|
|
89
|
+
missing: string[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type LightmapFiles = {
|
|
93
|
+
data: string
|
|
94
|
+
/** The atlas — one page, or every page in order for a bake that took several. */
|
|
95
|
+
texture: string | string[]
|
|
96
|
+
/** The point lights' baked irradiance (pages like `texture`). */
|
|
97
|
+
light?: string | string[]
|
|
98
|
+
volume?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
import type { Terrain } from "./Terrain"
|
|
102
|
+
|
|
103
|
+
type Entry = { key: string, model?: Model, mesh?: Mesh, terrain?: Terrain }
|
|
104
|
+
|
|
105
|
+
const terrainOf = (n: Node): Terrain | null => (n as unknown as { _terrain?: Terrain })._terrain ?? null
|
|
106
|
+
|
|
107
|
+
type BakeConfig = {
|
|
108
|
+
outDir: string, size?: number, texel?: number, sunRays?: number, aoRays?: number,
|
|
109
|
+
aoDistance?: number, bias?: number, sunAngle?: number, volumeCell?: number,
|
|
110
|
+
pages?: number, lightRays?: number, lightRadius?: number,
|
|
111
|
+
/** One bounce of indirect light from the point lights, gathered on the AO rays. The value is the
|
|
112
|
+
* ALBEDO the bounce assumes for every surface it hits (0 = off; ~0.35 is concrete). A bake with
|
|
113
|
+
* no bounce lights only what a lamp sees directly - in an interior that leaves the walls between
|
|
114
|
+
* the fixtures black, and a cast shadow invisible for want of light to be missing from. */
|
|
115
|
+
bounce?: number, bounceDistance?: number, bounceRays?: number,
|
|
116
|
+
/** Metres within which an occluder PARALLEL to the surface being lit is ignored (default 0.05).
|
|
117
|
+
* A modular kit stacks two wall pieces in the same place; to the bake they are coincident and
|
|
118
|
+
* the AO goes black in hard-edged patches. Real contact shadows are unaffected - they come off
|
|
119
|
+
* surfaces at an angle. 0 turns it off. */
|
|
120
|
+
coplanarSkip?: number,
|
|
121
|
+
/** Radius (px) of the geometry-aware denoise over the traced texels (default 2, 0 = off): the
|
|
122
|
+
* Monte-Carlo grain goes, edges of geometry stay. */
|
|
123
|
+
denoise?: number,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type VolumeHeader = { texture: number, dims: number[], min: number[], cell: number, light?: number, faces?: number }
|
|
127
|
+
|
|
128
|
+
type BakeData = {
|
|
129
|
+
version?: number, size: number, texel: number, pages?: number, textures?: string[],
|
|
130
|
+
lightTextures?: string[], lightScale?: number, lightGamma?: number,
|
|
131
|
+
lights?: { p: number[], color: number[], cd: number, range: number }[],
|
|
132
|
+
sun?: number[] | null,
|
|
133
|
+
instances: { key: string, receiver: boolean, st?: number[], page?: number }[],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const bakeConfig = (): BakeConfig | null => {
|
|
137
|
+
const g = (globalThis as unknown as { __lecodesLightmap?: BakeConfig }).__lecodesLightmap
|
|
138
|
+
return g && typeof g.outDir === "string" ? g : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A hex colour as the 9-char form the float4 uniform path expects ("#rrggbbaa"). */
|
|
142
|
+
const hex8 = (c: unknown): string => {
|
|
143
|
+
if (typeof c !== "string") return "#ffffffff"
|
|
144
|
+
if (c.length === 7) return c + "ff"
|
|
145
|
+
if (c.length === 4) return "#" + c[1] + c[1] + c[2] + c[2] + c[3] + c[3] + "ff"
|
|
146
|
+
return c
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const list = (v: string | string[] | undefined): string[] => v === undefined ? [] : Array.isArray(v) ? v : [ v ]
|
|
150
|
+
|
|
151
|
+
const NONE = 0xFFFFFFFF
|
|
152
|
+
|
|
153
|
+
export class Lightmap {
|
|
154
|
+
/** True while `lecodes lightmap bake` runs the app — skip menus and build the scene straight away. */
|
|
155
|
+
static get baking(): boolean { return bakeConfig() !== null }
|
|
156
|
+
|
|
157
|
+
private static entries: Entry[] = []
|
|
158
|
+
private static dynamics: Mesh[] = []
|
|
159
|
+
private static ordinals = new Map<string, number>()
|
|
160
|
+
private static warned = false
|
|
161
|
+
private static switchedOff: Light[] = []
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
/** Register static geometry — a Model, a Mesh, or any node whose subtree holds them. Statics are
|
|
165
|
+
* both receivers and occluders in the bake. `key` names the entry in lightmap.bake (default:
|
|
166
|
+
* the node's name + a running number, `container#3`); pass one when names are not stable. */
|
|
167
|
+
static add(node: Node, key?: string): Node {
|
|
168
|
+
const base = key ?? `${node.name || "node"}#${Lightmap.next(node.name || "node")}`
|
|
169
|
+
const found: Entry[] = []
|
|
170
|
+
const walk = (n: Node): void => {
|
|
171
|
+
const t = terrainOf(n)
|
|
172
|
+
if (t) { found.push({ key: base, terrain: t }); return } // one receiver, its chunks are internal
|
|
173
|
+
if (n instanceof Model) { found.push({ key: base, model: n }); return }
|
|
174
|
+
if (n instanceof Mesh) { found.push({ key: base, mesh: n }); return }
|
|
175
|
+
for (const c of n.children) walk(c)
|
|
176
|
+
}
|
|
177
|
+
walk(node)
|
|
178
|
+
if (found.length === 0) { console.warn(`Lightmap.add: ${base} holds no Model or Mesh`); return node }
|
|
179
|
+
if (found.length > 1) found.forEach((e, i) => { e.key = `${base}/${i}` })
|
|
180
|
+
Lightmap.entries.push(...found)
|
|
181
|
+
return node
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @internal Scene-file runtime: one model/mesh node under its path key (no subtree walk). */
|
|
185
|
+
static _register(node: Model | Mesh, key: string): void {
|
|
186
|
+
Lightmap.entries.push(node instanceof Model ? { key, model: node } : { key, mesh: node })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @internal Scene-file runtime: a terrain node (its own material carries the atlas, no swap). */
|
|
190
|
+
static _registerTerrain(terrain: Terrain, key: string): void {
|
|
191
|
+
Lightmap.entries.push({ key, terrain })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @internal Scene-file runtime: a dynamic Mesh — swapped to the lightmap material on the volume
|
|
195
|
+
* when `load` runs (dynamic Models need nothing here: the engine binds the volume itself). */
|
|
196
|
+
static _registerDynamic(mesh: Mesh): void {
|
|
197
|
+
Lightmap.dynamics.push(mesh)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static next(name: string): number {
|
|
201
|
+
const n = Lightmap.ordinals.get(name) ?? 0
|
|
202
|
+
Lightmap.ordinals.set(name, n + 1)
|
|
203
|
+
return n
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Forget every registration (a scene rebuild); baked-off lights come back to the real-time path. */
|
|
207
|
+
static clear(): void {
|
|
208
|
+
Lightmap.entries = []
|
|
209
|
+
Lightmap.dynamics = []
|
|
210
|
+
Lightmap.ordinals.clear()
|
|
211
|
+
Model._lightmapDefault = false
|
|
212
|
+
for (const l of Lightmap.switchedOff) l._setBakedOff(false)
|
|
213
|
+
Lightmap.switchedOff = []
|
|
214
|
+
_creator.lightmapSetOptions?.(1, 1, 1, 0)
|
|
215
|
+
_creator.lightmapVolumeSet?.(NONE, NONE, 0, 0, 0, 0, 0, 0, 0)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Apply a bake — or, under `lecodes lightmap bake`, run it. Resolves to null when nothing was
|
|
219
|
+
* applied (no bake yet, a host without the feature, bake mode). */
|
|
220
|
+
static async load(scene: Scene, files: LightmapFiles, options: LightmapLoadOptions = {}): Promise<LightmapInfo | null> {
|
|
221
|
+
const bake = bakeConfig()
|
|
222
|
+
if (bake) { Lightmap.bake(bake); return null }
|
|
223
|
+
if (!_creator.lightmapApply) {
|
|
224
|
+
if (!Lightmap.warned) { Lightmap.warned = true; console.warn("Lightmap: this host has no lightmap support — rendering without the bake") }
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
let data: BakeData
|
|
228
|
+
try {
|
|
229
|
+
const resp = await fetch(files.data, { useOnce: true })
|
|
230
|
+
if (resp.status >= 400) { console.warn(`Lightmap: no bake at ${files.data} (HTTP ${resp.status}) — run \`lecodes lightmap bake\``); return null }
|
|
231
|
+
data = resp.json()
|
|
232
|
+
resp.dispose()
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.warn(`Lightmap: could not read ${files.data}: ${String(e)}`)
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
// the atlas pages: a bake that took more pages than the scene lists applies what it can and says so
|
|
238
|
+
const pages = Math.max(1, data.pages ?? 1)
|
|
239
|
+
const pageFiles = list(files.texture)
|
|
240
|
+
if (pageFiles.length === 0) { console.warn("Lightmap: no atlas texture given"); return null }
|
|
241
|
+
if (pageFiles.length < pages) {
|
|
242
|
+
console.warn(`Lightmap: the bake took ${pages} atlas pages but the scene lists ${pageFiles.length} — add the other page files (${(data.textures ?? []).slice(pageFiles.length).join(", ")}) to env.lightmap.texture`)
|
|
243
|
+
}
|
|
244
|
+
const textures = await Promise.all(pageFiles.slice(0, pages).map((f) => Texture.load(f)))
|
|
245
|
+
// the light atlas: the bake carries the point lights only when the scene passes their atlas
|
|
246
|
+
const lightFiles = list(files.light)
|
|
247
|
+
let lightTextures: Texture[] = []
|
|
248
|
+
let lightScale = 0
|
|
249
|
+
let volumeScale = 0
|
|
250
|
+
if (data.lightTextures?.length && (data.lightScale ?? 0) > 0) {
|
|
251
|
+
if (lightFiles.length === 0) {
|
|
252
|
+
console.warn(`Lightmap: the bake carries ${data.lights?.length ?? 0} baked point light(s) but the scene passes no \`light\` atlas (${data.lightTextures[0]}) — the lamps stay real-time`)
|
|
253
|
+
} else {
|
|
254
|
+
if (lightFiles.length < pages) console.warn(`Lightmap: the light atlas has ${pages} pages but the scene lists ${lightFiles.length}`)
|
|
255
|
+
lightTextures = await Promise.all(lightFiles.slice(0, pages).map((f) => Texture.load(f)))
|
|
256
|
+
lightScale = (data.lightScale ?? 0) * (options.lightBoost ?? 1)
|
|
257
|
+
volumeScale = (data.lightScale ?? 0) * (options.volumeBoost ?? options.lightBoost ?? 1)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const ambientScale = options.ambientScale ?? 1
|
|
261
|
+
const strength = options.sunStrength ?? 1
|
|
262
|
+
const aoStrength = options.aoStrength ?? 1
|
|
263
|
+
// the light store's curve: sRGB(pow(E / lightScale, 1 / lightGamma)); bakes before 2026-08-31 are linear (1)
|
|
264
|
+
const lightGamma = data.lightGamma ?? 1
|
|
265
|
+
// level-wide knobs first: the volume and every rect below inherit them
|
|
266
|
+
_creator.lightmapSetOptions?.(ambientScale, strength, aoStrength, lightScale, volumeScale, lightGamma)
|
|
267
|
+
// what the bake actually BOUND, so a level can be debugged without guessing (FEEDBACK 95): the
|
|
268
|
+
// scale is the one number standing between the atlas' physical irradiance and the pixel
|
|
269
|
+
console.log(`[lightmap] bound ${textures.length} page(s)${lightTextures.length ? ` + ${lightTextures.length} light page(s)` : ""}`
|
|
270
|
+
+ ` lightScale ${lightScale.toFixed(2)}${(options.lightBoost ?? 1) !== 1 ? ` (bake ${(data.lightScale ?? 0).toFixed(2)} x boost ${options.lightBoost})` : ""}`
|
|
271
|
+
+ `${volumeScale !== lightScale ? ` volumeScale ${volumeScale.toFixed(2)}` : ""}`
|
|
272
|
+
+ ` gamma ${lightGamma} ao ${aoStrength} ambient ${ambientScale} sun ${strength}`)
|
|
273
|
+
// the light volume next: it claims every lightmap-material instance, the atlas rects below win back the statics
|
|
274
|
+
const volume = files.volume ? await Lightmap.loadVolume(files.volume, volumeScale, lightGamma, ambientScale, strength, aoStrength) : null
|
|
275
|
+
const rects = new Map<string, { st: number[], page: number }>()
|
|
276
|
+
for (const inst of data.instances) if (inst.receiver && inst.st) rects.set(inst.key, { st: inst.st, page: inst.page ?? 0 })
|
|
277
|
+
let applied = 0
|
|
278
|
+
const missing: string[] = []
|
|
279
|
+
for (const e of Lightmap.entries) {
|
|
280
|
+
const r = rects.get(e.key)
|
|
281
|
+
if (!r || r.page >= textures.length) { missing.push(e.key); continue }
|
|
282
|
+
const st = r.st
|
|
283
|
+
const texture = textures[r.page]
|
|
284
|
+
const light = r.page < lightTextures.length ? lightTextures[r.page] : null
|
|
285
|
+
if (e.model) {
|
|
286
|
+
const n = _creator.lightmapApply(e.model.id, texture._id, st[0], st[1], st[2], st[3], light ? light._id : NONE)
|
|
287
|
+
if (n === 0) { console.warn(`Lightmap: ${e.key} was not loaded with { lightmap: true } — skipped`); continue }
|
|
288
|
+
e.model.castShadows = false
|
|
289
|
+
} else if (e.mesh) {
|
|
290
|
+
Lightmap.swapMeshMaterial(e.mesh, texture, light, light ? lightScale : 0, lightGamma, st, ambientScale, strength, aoStrength)
|
|
291
|
+
e.mesh.castShadows = false
|
|
292
|
+
} else if (e.terrain) {
|
|
293
|
+
// terrain.mat carries lightmap.mat's atlas block: bind the rect on the terrain's own material
|
|
294
|
+
const m = e.terrain.material.set("lightmap", texture).set("lightmapST", [ st[0], st[1], st[2], st[3] ])
|
|
295
|
+
.set("ambientScale", ambientScale).set("sunStrength", strength).set("bakedAo", aoStrength).set("lightGamma", lightGamma)
|
|
296
|
+
if (light) m.set("lightmapLight", light).set("lightScale", lightScale)
|
|
297
|
+
else m.set("lightScale", 0)
|
|
298
|
+
e.terrain.castShadows = false
|
|
299
|
+
}
|
|
300
|
+
applied++
|
|
301
|
+
}
|
|
302
|
+
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`)
|
|
303
|
+
// the lights the bake carries go dark: the atlas holds them for the statics, the volume for the movers
|
|
304
|
+
let lights = 0
|
|
305
|
+
if (lightTextures.length > 0 && data.lights?.length) lights = Lightmap.switchOffBaked(data.lights)
|
|
306
|
+
const info: LightmapInfo = { size: data.size, texel: data.texel, pages, applied, total: rects.size, missing, lights }
|
|
307
|
+
if (volume) info.volume = { dims: volume.dims, cell: volume.cell }
|
|
308
|
+
return info
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Every live point light within 5 cm of a baked one is held dark (a moved lamp keeps lighting live
|
|
312
|
+
* — and double, until the next bake, which is the honest state of a stale bake). */
|
|
313
|
+
private static switchOffBaked(baked: { p: number[] }[]): number {
|
|
314
|
+
let n = 0
|
|
315
|
+
for (const light of Light._points) {
|
|
316
|
+
if (!light._baked) continue
|
|
317
|
+
const wp = light.worldPosition
|
|
318
|
+
const x = cx(wp), y = cy(wp), z = cz(wp)
|
|
319
|
+
const hit = baked.some((b) => Math.abs(b.p[0] - x) < 0.05 && Math.abs(b.p[1] - y) < 0.05 && Math.abs(b.p[2] - z) < 0.05)
|
|
320
|
+
if (!hit) continue
|
|
321
|
+
light._setBakedOff(true)
|
|
322
|
+
Lightmap.switchedOff.push(light)
|
|
323
|
+
n++
|
|
324
|
+
}
|
|
325
|
+
return n
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** lightmap.volume → the engine's 3D textures → THE volume for every dynamic lightmap-material instance,
|
|
329
|
+
* plus the registered dynamic Meshes. Null when the file is a placeholder or the host lacks the feature. */
|
|
330
|
+
private static async loadVolume(url: string, lightScale: number, lightGamma: number, ambientScale: number, strength: number, aoStrength: number): Promise<VolumeHeader | null> {
|
|
331
|
+
if (!_creator.lightmapVolumeLoad || !_creator.lightmapVolumeSet) return null
|
|
332
|
+
let header: VolumeHeader
|
|
333
|
+
try {
|
|
334
|
+
const resp = await fetch(url, { useOnce: true })
|
|
335
|
+
if (resp.status >= 400) { console.warn(`Lightmap: no volume at ${url} (HTTP ${resp.status}) — run \`lecodes lightmap bake\``); return null }
|
|
336
|
+
const json = _creator.lightmapVolumeLoad((resp as unknown as { _id: number })._id)
|
|
337
|
+
resp.dispose()
|
|
338
|
+
if (!json) { console.warn(`Lightmap: ${url} is not a light volume — rebake`); return null }
|
|
339
|
+
header = JSON.parse(json) as VolumeHeader
|
|
340
|
+
} catch (e) {
|
|
341
|
+
console.warn(`Lightmap: could not read ${url}: ${String(e)}`)
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
const [ nx, ny, nz ] = header.dims
|
|
345
|
+
if (nx * ny * nz <= 1) return null // the CLI's placeholder: nothing baked yet
|
|
346
|
+
const size = [ nx * header.cell, ny * header.cell, nz * header.cell ]
|
|
347
|
+
const texture = new Texture(nx, ny, header.texture)
|
|
348
|
+
const hasLight = header.light !== undefined && lightScale > 0
|
|
349
|
+
const light = hasLight ? new Texture(nx, ny, header.light!) : null
|
|
350
|
+
if (hasLight && (header.faces ?? 1) < 6) console.warn("Lightmap: the light volume predates the ambient cube (one directionless value per cell) — rebake")
|
|
351
|
+
_creator.lightmapVolumeSet(header.texture, hasLight ? header.light! : NONE, header.min[0], header.min[1], header.min[2], size[0], size[1], size[2], header.cell)
|
|
352
|
+
for (const mesh of Lightmap.dynamics) {
|
|
353
|
+
const m = Lightmap.meshMaterial(mesh, ambientScale, strength, aoStrength, lightGamma)
|
|
354
|
+
m.set("probeVolume", texture)
|
|
355
|
+
if (light) m.set("probeLight", light).set("lightScale", lightScale)
|
|
356
|
+
m.set("probeMin", [ header.min[0], header.min[1], header.min[2], header.cell * 0.5 ])
|
|
357
|
+
// .w = the grid's depth: the shader keeps its fetch inside one face slab of the light texture
|
|
358
|
+
m.set("probeInvSize", [ 1 / size[0], 1 / size[1], 1 / size[2], nz ])
|
|
359
|
+
mesh.setMaterial(m)
|
|
360
|
+
}
|
|
361
|
+
return header
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** A Mesh keeps its look (colour / map / roughness / metallic) but moves to the lightmap material. */
|
|
365
|
+
private static meshMaterial(mesh: Mesh, ambientScale: number, strength: number, aoStrength: number, lightGamma = 1): Material {
|
|
366
|
+
const src = mesh.material
|
|
367
|
+
const u = src?.uniforms ?? {}
|
|
368
|
+
const m = Material.lightmap()
|
|
369
|
+
m.set("baseColorFactor", hex8(u.baseColor))
|
|
370
|
+
if (u.baseColorMap instanceof Texture) m.set("baseColorMap", u.baseColorMap)
|
|
371
|
+
if (typeof u.roughness === "number") m.set("roughnessFactor", u.roughness)
|
|
372
|
+
if (typeof u.metallic === "number") m.set("metallicFactor", u.metallic)
|
|
373
|
+
m.set("ambientScale", ambientScale)
|
|
374
|
+
m.set("sunStrength", strength)
|
|
375
|
+
m.set("bakedAo", aoStrength)
|
|
376
|
+
m.set("lightGamma", lightGamma)
|
|
377
|
+
return m
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private static swapMeshMaterial(mesh: Mesh, texture: Texture, light: Texture | null, lightScale: number, lightGamma: number, st: number[],
|
|
381
|
+
ambientScale: number, strength: number, aoStrength: number): void {
|
|
382
|
+
const m = Lightmap.meshMaterial(mesh, ambientScale, strength, aoStrength, lightGamma)
|
|
383
|
+
m.set("lightmap", texture)
|
|
384
|
+
m.set("lightmapST", [ st[0], st[1], st[2], st[3] ])
|
|
385
|
+
if (light) m.set("lightmapLight", light).set("lightScale", lightScale)
|
|
386
|
+
mesh.setMaterial(m)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private static bake(cfg: BakeConfig): void {
|
|
390
|
+
if (!_creator.lightmapBake) { console.error("[lightmap] error: this host has no bake module (build the desktop host with CREATOR_GL_LIGHTMAP, or update lecodes desktop)"); return }
|
|
391
|
+
const ids = Uint32Array.from(Lightmap.entries.map((e) => (e.model ?? e.mesh ?? e.terrain!.node).id))
|
|
392
|
+
const keys = Lightmap.entries.map((e) => e.key).join("\n")
|
|
393
|
+
if (ids.length === 0) { console.error("[lightmap] error: nothing registered — call Lightmap.add on the static props before Lightmap.load"); return }
|
|
394
|
+
// a `baked: false` light is held dark for the bake — the engine bakes every lit point light it finds
|
|
395
|
+
const live = [ ...Light._points ].filter((l) => !l._baked)
|
|
396
|
+
for (const l of live) _creator.setLightIntensity?.(l.id, 0)
|
|
397
|
+
_creator.lightmapBake(cfg.outDir, ids, keys, cfg.size ?? 4096, cfg.texel ?? 0.02, cfg.sunRays ?? 32, cfg.aoRays ?? 64,
|
|
398
|
+
cfg.aoDistance ?? 2, cfg.bias ?? 0.02, cfg.sunAngle ?? 0.5, cfg.volumeCell ?? 0.5, cfg.pages ?? 1, cfg.lightRays ?? 8, cfg.lightRadius ?? 0.1,
|
|
399
|
+
cfg.bounce ?? 0, cfg.bounceDistance ?? 8, cfg.bounceRays ?? 1, cfg.coplanarSkip ?? 0.05, cfg.denoise ?? 2)
|
|
400
|
+
for (const l of live) _creator.setLightIntensity?.(l.id, l.intensity)
|
|
401
|
+
}
|
|
402
|
+
}
|