@solidrt/3d 0.0.55 → 0.0.57

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.
Files changed (72) hide show
  1. package/AGENTS.md +1206 -224
  2. package/README.md +16 -7
  3. package/examples/README.md +88 -9
  4. package/examples/aim.tsx +2 -3
  5. package/examples/cascades.tsx +1 -4
  6. package/examples/collision.tsx +263 -0
  7. package/examples/first-person.tsx +129 -0
  8. package/examples/fleet.tsx +200 -0
  9. package/examples/fog.tsx +2 -3
  10. package/examples/instanced.tsx +72 -32
  11. package/examples/lamps.tsx +92 -0
  12. package/examples/lit.tsx +3 -5
  13. package/examples/materials.tsx +150 -0
  14. package/examples/model-load.tsx +95 -0
  15. package/examples/model.glb +0 -0
  16. package/examples/model.tsx +35 -9
  17. package/examples/pick.tsx +145 -37
  18. package/examples/probe.tsx +107 -0
  19. package/examples/scene-background.tsx +2 -4
  20. package/examples/scene-basic.tsx +2 -4
  21. package/examples/scene-views.tsx +68 -48
  22. package/examples/shadows.tsx +2 -4
  23. package/examples/sky-lit.tsx +92 -0
  24. package/examples/skybox.tsx +134 -0
  25. package/examples/sprites.tsx +4 -6
  26. package/examples/standard.tsx +138 -0
  27. package/examples/sweep-paths.tsx +2 -4
  28. package/package.json +3 -3
  29. package/src/camera.ts +148 -0
  30. package/src/clip.ts +96 -0
  31. package/src/collision.ts +20 -0
  32. package/src/color.ts +32 -0
  33. package/src/components/context.tsx +35 -0
  34. package/src/components/directional-light.tsx +48 -0
  35. package/src/components/first-person-camera.tsx +72 -0
  36. package/src/components/group.tsx +18 -0
  37. package/src/components/hemisphere-light.tsx +24 -0
  38. package/src/components/index.ts +46 -0
  39. package/src/components/instanced-mesh.tsx +105 -0
  40. package/src/components/mesh.tsx +92 -0
  41. package/src/components/node-props.ts +90 -0
  42. package/src/components/orbit-camera.tsx +75 -0
  43. package/src/components/perspective-camera.tsx +32 -0
  44. package/src/components/point-light.tsx +52 -0
  45. package/src/components/record-mesh.tsx +68 -0
  46. package/src/components/scene.tsx +281 -0
  47. package/src/components/spot-light.tsx +65 -0
  48. package/src/components/sprite.tsx +38 -0
  49. package/src/components/view3d.tsx +152 -0
  50. package/src/environment-bake.ts +492 -0
  51. package/src/environment.ts +325 -0
  52. package/src/first-person.ts +258 -0
  53. package/src/geometry.ts +6 -2
  54. package/src/glsl.ts +1343 -82
  55. package/src/gltf.ts +432 -112
  56. package/src/index.ts +61 -9
  57. package/src/input.ts +70 -0
  58. package/src/light.ts +383 -0
  59. package/src/material.ts +667 -232
  60. package/src/math.ts +110 -0
  61. package/src/mesh.ts +728 -0
  62. package/src/mixer.ts +476 -0
  63. package/src/model-file.ts +80 -15
  64. package/src/model.ts +204 -35
  65. package/src/node.ts +579 -0
  66. package/src/orbit.ts +292 -212
  67. package/src/scene-pointer.ts +294 -0
  68. package/src/scene-shadows.ts +471 -0
  69. package/src/scene.ts +1435 -1576
  70. package/tools/environment.ts +69 -0
  71. package/tools/model.ts +4 -3
  72. package/src/components.tsx +0 -420
package/src/index.ts CHANGED
@@ -5,8 +5,34 @@
5
5
  // without Solid components) and the component face (Scene/Mesh/Group/
6
6
  // PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
7
7
 
8
- export { add, createDirectionalLight, createGroup, createHemisphereLight, createInstancedMesh, createMesh, createScene, createSprite, setLight, disposeInstances, getRotation, lookAt, remove, setCastShadow, setGeometry, setInstanceCount, setInstances, setMaterial, setMeshParams, setRenderOrder, setTransform, setTransition, setVisible, worldPosition, MAX_SHADOWS } from "./scene.ts"
9
- export type { CameraUpdate, DirectionalLight as DirectionalLightNode, DirectionalLightOptions, FogOptions, HemisphereLight as HemisphereLightNode, HemisphereLightOptions, Hit, Light, InstancedMesh as InstancedMeshNode, InstancedMeshOptions, Mesh as MeshNode, MeshInstances, OrthoExtent, Scene as SceneHandle, SceneHandlers, SceneNode, SceneOptions, ScenePointerEvent, ShadowCamera, ShadowOptions, TransformUpdate, TransitionEndEvent, View, ViewOptions } from "./scene.ts"
8
+ export { add, createGroup, getRotation, getTransform, lookAt, remove, setTransform, setTransition, setVisible, worldPosition } from "./node.ts"
9
+ export type { NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneEventBase, SceneNode, ScenePointerEvent, ScenePointerListener, SceneTapEvent, SceneWheelEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
10
+ export { addInstance, createInstancedMesh, createMesh, createRecordMesh, createSprite, disposeInstances, removeInstance, setCastShadow, setCulling, setGeometry, setInstanceStyle, setLayers, setMaterial, setMeshParams, setRecordCount, setRecords, setRenderOrder, INSTANCE_FLOATS } from "./mesh.ts"
11
+ export type { InstancedMesh as InstancedMeshNode, InstancedMeshOptions, InstanceNode, InstanceSlots, InstanceStyle, Mesh as MeshNode, MeshInstances, RecordMesh as RecordMeshNode, RecordMeshOptions } from "./mesh.ts"
12
+ export { createDirectionalLight, createHemisphereLight, createPointLight, createSpotLight, setLight, MAX_SHADOWS } from "./light.ts"
13
+ export type {
14
+ DirectionalLight as DirectionalLightNode,
15
+ DirectionalLightOptions,
16
+ HemisphereLight as HemisphereLightNode,
17
+ HemisphereLightOptions,
18
+ Light,
19
+ PointLight as PointLightNode,
20
+ PointLightOptions,
21
+ CastingLight,
22
+ ShadowCamera,
23
+ ShadowOptions,
24
+ SpotLight as SpotLightNode,
25
+ SpotLightOptions,
26
+ SpotShadowOptions,
27
+ } from "./light.ts"
28
+ export { createScene } from "./scene.ts"
29
+ export { feedPointer } from "./scene-pointer.ts"
30
+ export type { Capsule, EnvironmentOptions, FogOptions, Hit, Impact, MoveOptions, MoveResult, OrientedBox, Overlap, QueryOptions, ReflectionProbe, ReflectionProbeOptions, Scene as SceneHandle, SceneHandlers, SceneOptions, ScreenRay, SkyboxOptions, Sphere, ToneMapping, ViewHandle, ViewOptions, Volume } from "./scene.ts"
31
+ export { moveAndSlide } from "./collision.ts"
32
+ export type { MoveScene } from "./collision.ts"
33
+ export { equirectToCube, loadEnvironment } from "./environment.ts"
34
+ export { linearColor, linearToSrgb, srgbToLinear } from "./color.ts"
35
+ export type { CameraState, CameraUpdate, OrthoExtent } from "./camera.ts"
10
36
  export type { NodeTransition, NodeTransitionSpec } from "flux:spatial"
11
37
  export { disposeGeometry } from "./geometry-gpu.ts"
12
38
  export { box, circle, cone, cylinder, fillAttribute, fillColors, geometryBounds, layoutAttributes, layoutKey, layoutSlot, layoutStride, mergeGeometries, packGeometry, plane, ring, sphere, torus, torusKnot, transformGeometry, validateGeometry, withAttribute, withColors, STANDARD_FLOATS, VERTEX_LAYOUTS } from "./geometry.ts"
@@ -15,17 +41,43 @@ export { fillet, roundRect, shape, triangulate } from "./profile.ts"
15
41
  export type { Profile, ProfilePoint } from "./profile.ts"
16
42
  export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
17
43
  export type { ExtrudeOptions, LatheOptions, PathFrames, PathPoint, SweepPath, TubeOptions } from "./sweep.ts"
18
- export { lit, shaderMaterial, shaderMaterialClass, sprite, unlit } from "./material.ts"
19
- export type { LitOptions, Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, UnlitOptions } from "./material.ts"
20
- export { DirectionalLight, Group, HemisphereLight, InstancedMesh, Mesh, PerspectiveCamera, Scene, Sprite, useScene } from "./components.tsx"
21
- export type { DirectionalLightProps, HemisphereLightProps, InstancedMeshProps, MeshProps, PerspectiveCameraProps, PointerEventProps, SceneProps, SpriteProps, TransformProps } from "./components.tsx"
44
+ export { lit, shaderMaterial, shaderMaterialClass, sprite, standard, unlit } from "./material.ts"
45
+ export type { LitOptions, Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, StandardOptions, UnlitOptions } from "./material.ts"
46
+ export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedMesh, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, View3d, useScene } from "./components/index.ts"
47
+ export type {
48
+ CameraTarget,
49
+ DirectionalLightProps,
50
+ FirstPersonCameraProps,
51
+ HemisphereLightProps,
52
+ InstancedMeshProps,
53
+ InstanceProps,
54
+ MeshProps,
55
+ OrbitCameraProps,
56
+ PerspectiveCameraProps,
57
+ PointerEventProps,
58
+ PointLightProps,
59
+ RecordMeshProps,
60
+ ScenePointerProps,
61
+ SceneProps,
62
+ SpotLightProps,
63
+ SpriteProps,
64
+ TransformProps,
65
+ View3dProps,
66
+ } from "./components/index.ts"
22
67
  export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
23
- export type { ModelData, ModelMaterial, ModelPart, UriResolver } from "./gltf.ts"
68
+ export type { ModelChannel, ModelClip, ModelData, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
24
69
  export { decodeModel, encodeModel } from "./model-file.ts"
25
70
  export { createModel, loadGltf, loadModel } from "./model.ts"
26
- export type { Model, ModelOptions } from "./model.ts"
71
+ export type { Model, ModelMaps, ModelOptions } from "./model.ts"
72
+ export { createMixer } from "./mixer.ts"
73
+ export type { Mixer, MixerOptions, MixerPlayOptions } from "./mixer.ts"
74
+ export { sampleChannel } from "./clip.ts"
27
75
  export { createOrbitCamera } from "./orbit.ts"
28
- export type { OrbitCamera, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
76
+ export type { OrbitAxes, OrbitCamera as OrbitCameraHandle, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
77
+ export { createFirstPersonCamera } from "./first-person.ts"
78
+ export type { FirstPersonAxes, FirstPersonCamera as FirstPersonCameraHandle, FirstPersonCameraOptions, FirstPersonPose, FirstPersonTarget } from "./first-person.ts"
79
+ export { firstPersonActions, firstPersonBindings, orbitActions, orbitBindings } from "./input.ts"
80
+ export type { CameraDevices } from "./input.ts"
29
81
  // math's lookAt (the camera view matrix) stays on the /math subpath: the
30
82
  // root's lookAt is the scene verb, the same split as `add`.
31
83
  export { rayBoxDistance, compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, orthographic, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
package/src/input.ts ADDED
@@ -0,0 +1,70 @@
1
+ // The standard input wiring for the 3d camera controls: the action
2
+ // declarations each control consumes (its axes, by the shared vocabulary)
3
+ // and a preset per control that binds the devices an app hands in. A
4
+ // preset is plain data - the bindings the app could write by hand -
5
+ // applied with `input.bind(orbitBindings({ pointer, gamepad }))` and
6
+ // adjusted afterwards with unbind; nothing binds unless the app says so
7
+ // (ARCHITECTURE.md).
8
+ //
9
+ // Devices are the nouns: `pointer` is the feed of the element showing the
10
+ // scene (`createPointerFeed()`, handed to `<Scene pointer>`), `gamepad` a
11
+ // core gamepad device (`gamepad(0)`, or `gamepad()` for any pad), and
12
+ // `keyboard` the core keyboard device, whose key events reach the map
13
+ // through `input.handlers` spread on the window.
14
+ //
15
+ // Keys and sticks move the CAMERA where a drag moves the content (the
16
+ // stick right orbits the eye to the right; a drag right spins the scene
17
+ // to the right), so the rotate and pan bindings of those devices go
18
+ // through invert() and each axis keeps its one meaning. `look` is the
19
+ // exception by nature: a drag and a stick both turn the eye.
20
+
21
+ import { invert } from "@solidrt/core/input"
22
+ import type { Binding, GamepadDevice, KeyboardDevice, PointerFeed } from "@solidrt/core"
23
+
24
+ export type CameraDevices = {
25
+ pointer?: PointerFeed
26
+ gamepad?: GamepadDevice
27
+ keyboard?: KeyboardDevice
28
+ }
29
+
30
+ /** The orbit control's actions: `rotate`, `zoom`, `pan`. */
31
+ export let orbitActions = { rotate: "vec2", zoom: "axis", pan: "vec2" } as const
32
+
33
+ /**
34
+ * The orbit camera's standard bindings: a drag rotates, a pinch and the
35
+ * wheel zoom, two fingers pan; the right stick rotates, the triggers
36
+ * zoom, the left stick pans; the arrow keys rotate and minus/equals zoom.
37
+ */
38
+ export function orbitBindings(devices: CameraDevices): Binding[] {
39
+ let out: Binding[] = []
40
+ let { pointer, gamepad, keyboard } = devices
41
+ if (pointer) {
42
+ out.push({ action: "rotate", source: pointer.drag }, { action: "zoom", source: pointer.pinch }, { action: "zoom", source: pointer.wheel }, { action: "pan", source: pointer.pan })
43
+ }
44
+ if (gamepad) {
45
+ out.push({ action: "rotate", source: invert(gamepad.rightStick) }, { action: "zoom", source: gamepad.triggers }, { action: "pan", source: invert(gamepad.leftStick) })
46
+ }
47
+ if (keyboard) {
48
+ out.push({ action: "rotate", source: invert(keyboard.arrows) }, { action: "zoom", source: keyboard.axis("Minus", "Equal") })
49
+ }
50
+ return out
51
+ }
52
+
53
+ /** The first-person control's actions: `look`, `move`, `rise`. */
54
+ export let firstPersonActions = { look: "vec2", move: "vec2", rise: "axis" } as const
55
+
56
+ /**
57
+ * The first-person camera's standard bindings: a drag looks around and so
58
+ * does mouse motion while the pointer is locked (the app locks it: see
59
+ * examples/first-person.tsx); the right stick looks, the left stick
60
+ * walks, the shoulders rise and sink in fly mode; WASD and the arrows
61
+ * walk, Q/E rise and sink.
62
+ */
63
+ export function firstPersonBindings(devices: CameraDevices): Binding[] {
64
+ let out: Binding[] = []
65
+ let { pointer, gamepad, keyboard } = devices
66
+ if (pointer) out.push({ action: "look", source: pointer.drag }, { action: "look", source: pointer.mouseDelta })
67
+ if (gamepad) out.push({ action: "look", source: gamepad.rightStick }, { action: "move", source: gamepad.leftStick }, { action: "rise", source: gamepad.shoulders })
68
+ if (keyboard) out.push({ action: "move", source: keyboard.wasd }, { action: "move", source: keyboard.arrows }, { action: "rise", source: keyboard.axis("KeyQ", "KeyE") })
69
+ return out
70
+ }
package/src/light.ts ADDED
@@ -0,0 +1,383 @@
1
+ // Lights: the directional light (with its shadow options) and the
2
+ // hemisphere ambient, their constructors and the setLight write path.
3
+ // Scene reactions go through the node's SceneHooks (node.ts).
4
+
5
+ import { MAX_CASCADES, MAX_LIGHTS } from "./glsl.ts"
6
+ import type { Vec3 } from "./math.ts"
7
+ import { makeNode } from "./node.ts"
8
+ import type { SceneNode } from "./node.ts"
9
+
10
+ /** The orthographic frustum a casting light renders its shadow map from,
11
+ * in the light's own space (x right, y up, looking along its direction),
12
+ * Three's DirectionalLightShadow camera. Everything outside it is lit. */
13
+ export type ShadowCamera = { left: number; right: number; top: number; bottom: number; near: number; far: number }
14
+
15
+ export type ShadowOptions = {
16
+ /** Shadow map resolution in texels, square (default 1024). */
17
+ mapSize?: number
18
+ /** Depth bias against acne, in the map's 0..1 depth (default 0). */
19
+ bias?: number
20
+ /** Offset a receiving point along its normal before the lookup, in
21
+ * world units (default 0) - the acne fix that keeps contact shadows. */
22
+ normalBias?: number
23
+ /** Filter radius in map texels (default 1): 1 is the single hardware
24
+ * compare tap (a 2x2 bilinear PCF); above 1 a 3x3 grid of those taps
25
+ * spread `radius` texels apart, Three's `shadow.radius` - softer edges
26
+ * for the texel stairs a close receiver shows, at nine taps. */
27
+ radius?: number
28
+ /** The light frustum; absent keys keep the defaults +-5, 0.5..500.
29
+ * Ignored by a cascaded light (its frustums follow the scene camera). */
30
+ camera?: Partial<ShadowCamera>
31
+ /** Split the shadow into this many cascades (1..MAX_CASCADES, default
32
+ * 1 = the `camera` box). With more, the light renders one map per slice
33
+ * of the SCENE camera's frustum (near .. far, tightest first), each
34
+ * `mapSize` texels wide and fitted every time the camera or the light
35
+ * moves, and a receiver samples the tightest one that has the point:
36
+ * sharp contact shadows near the camera and coarser ones toward the
37
+ * horizon, for a scene the box cannot cover at one map's resolution.
38
+ * Views (`scene.createView`) sample the same maps, fitted to the scene
39
+ * camera, not their own. Each cascade is a tile of the atlas. */
40
+ cascades?: number
41
+ /** How far from the scene camera a cascaded light shadows, in world
42
+ * units (default null = the camera's far). The cascades span
43
+ * near..distance and a point past it is lit, so pulling it in sharpens
44
+ * every cascade; it also bounds the maps' depth range, which is what
45
+ * `bias` is measured against. A box light ignores it. */
46
+ distance?: number | null
47
+ }
48
+
49
+ /** A directional light node: parallel rays travelling along `direction`
50
+ * in the node's LOCAL space, so a parent's rotation turns the light with
51
+ * it (the default `[0, -1, 0]` is a sun straight overhead; the length is
52
+ * ignored). Scale does not affect it, and neither does position UNLESS
53
+ * it casts: a casting light's shadow camera sits at its WORLD position
54
+ * looking along its world direction (Three's rule), so place a casting
55
+ * sun above the scene. Write through setLight. */
56
+ export type DirectionalLight = SceneNode & {
57
+ kind: "light"
58
+ type: "directional"
59
+ direction: Vec3
60
+ /** Linear [r, g, b] 0..1. */
61
+ color: Vec3
62
+ intensity: number
63
+ /** Render a shadow map from this light (any directional light may;
64
+ * each map is a full extra pass over the casting meshes); meshes with
65
+ * `castShadow` draw into it, `lit` materials read it (unless
66
+ * `receiveShadow: false`). */
67
+ castShadow: boolean
68
+ /** The resolved shadow options (read; write through setLight). */
69
+ shadow: { mapSize: number; bias: number; normalBias: number; radius: number; camera: ShadowCamera; cascades: number; distance: number | null }
70
+ }
71
+
72
+ /** A spot light node: a cone of light from the node's WORLD position
73
+ * along `direction` in its LOCAL space (default [0, -1, 0], a lamp
74
+ * aimed straight down), so a parent's rotation turns it and setTransform
75
+ * places it - deliberately a direction, not Three's target object, the
76
+ * DirectionalLight rule. The cone fades across `penumbra` of `angle`
77
+ * and the strength falls off as `1 / d^decay`, windowed to zero at
78
+ * `distance` (Three's punctual-light falloff). Write through setLight. */
79
+ export type SpotLight = SceneNode & {
80
+ kind: "light"
81
+ type: "spot"
82
+ direction: Vec3
83
+ /** Linear [r, g, b] 0..1. */
84
+ color: Vec3
85
+ intensity: number
86
+ /** Falloff cutoff in world units; the light is zero past it (0 = no
87
+ * cutoff, Three's rule). */
88
+ distance: number
89
+ /** The cone's half-angle in DEGREES from `direction` (default 60, at
90
+ * most 90). Degrees like camera fov and like Unity/Godot; Three's
91
+ * radians `SpotLight.angle` converts as `angle * 180 / PI`. */
92
+ angle: number
93
+ /** 0..1: the outer fraction of the cone that fades to the rim
94
+ * (default 0, a hard edge). */
95
+ penumbra: number
96
+ /** Falloff exponent (default 2, physical inverse-square; 0 = no
97
+ * distance falloff). */
98
+ decay: number
99
+ /** Render a shadow map from this light: the directional machinery
100
+ * with a perspective camera at the light's world position along its
101
+ * world direction, fov = the cone (2 * angle), far = `distance` (or
102
+ * the default when 0). One map, one shadow slot. */
103
+ castShadow: boolean
104
+ /** The resolved shadow options (read; write through setLight). */
105
+ shadow: { mapSize: number; bias: number; normalBias: number; radius: number; near: number }
106
+ }
107
+
108
+ /** The perspective-map shadow options a spot or point light takes. */
109
+ export type SpotShadowOptions = {
110
+ /** Shadow map resolution in texels, square (default 1024). A point
111
+ * light renders SIX maps at this size, one per face. */
112
+ mapSize?: number
113
+ /** Depth bias against acne, in the map's 0..1 depth (default 0). Note
114
+ * a perspective map's depth is nonlinear: `normalBias` is the knob to
115
+ * reach for first. */
116
+ bias?: number
117
+ /** Offset a receiving point along its normal before the lookup, in
118
+ * world units (default 0). */
119
+ normalBias?: number
120
+ /** Filter radius in map texels (default 1): 1 is the single hardware
121
+ * compare tap (a 2x2 bilinear PCF); above 1 a 3x3 grid of those taps
122
+ * spread `radius` texels apart, Three's `shadow.radius` - softer edges
123
+ * for the texel stairs a close receiver shows, at nine taps. */
124
+ radius?: number
125
+ /** The shadow camera's near plane in world units (default 0.5); its
126
+ * far is the light's `distance` (or the directional default, 500,
127
+ * when 0). */
128
+ near?: number
129
+ }
130
+
131
+ /** A point light node: light in every direction from the node's WORLD
132
+ * position (setTransform places it; rotation and scale do not matter),
133
+ * with the same `distance`/`decay` falloff as a spot. Write through
134
+ * setLight. */
135
+ export type PointLight = SceneNode & {
136
+ kind: "light"
137
+ type: "point"
138
+ /** Linear [r, g, b] 0..1. */
139
+ color: Vec3
140
+ intensity: number
141
+ /** Falloff cutoff in world units (0 = no cutoff). */
142
+ distance: number
143
+ /** Falloff exponent (default 2). */
144
+ decay: number
145
+ /** Render shadow maps from this light: six 90-degree face maps
146
+ * (perspective, world-axis aligned) as tiles of the scene's shadow
147
+ * atlas, claiming six shadow slots; a receiver picks the face by the
148
+ * light-to-point direction. Far = `distance` (or the directional
149
+ * default when 0), so give a casting bulb a distance. */
150
+ castShadow: boolean
151
+ /** The resolved shadow options (read; write through setLight). */
152
+ shadow: { mapSize: number; bias: number; normalBias: number; radius: number; near: number }
153
+ }
154
+
155
+ /** The ambient term: a sky/ground gradient by the WORLD normal's
156
+ * vertical tilt (fixed to world up, not the node's). One per scene - the
157
+ * last attached wins. Write through setLight. */
158
+ export type HemisphereLight = SceneNode & {
159
+ kind: "light"
160
+ type: "hemisphere"
161
+ sky: Vec3
162
+ ground: Vec3
163
+ intensity: number
164
+ }
165
+
166
+ export type Light = DirectionalLight | SpotLight | PointLight | HemisphereLight
167
+
168
+ /** The light types that can render a shadow map - all of them: a
169
+ * directional light's box or cascades, a spot's one cone map, a point
170
+ * light's six face maps. */
171
+ export type CastingLight = DirectionalLight | SpotLight | PointLight
172
+
173
+ // Every light `color` (and the hemisphere's sky/ground) is sRGB 0..1
174
+ // like a material color, decoded when the scene writes the light list;
175
+ // `intensity` scales it in linear light.
176
+ export type DirectionalLightOptions = {
177
+ direction?: Vec3
178
+ color?: Vec3
179
+ intensity?: number
180
+ castShadow?: boolean
181
+ /** Shadow-map options, merged key by key (setLight keeps unmentioned ones). */
182
+ shadow?: ShadowOptions
183
+ }
184
+ export type SpotLightOptions = {
185
+ direction?: Vec3
186
+ color?: Vec3
187
+ intensity?: number
188
+ distance?: number
189
+ angle?: number
190
+ penumbra?: number
191
+ decay?: number
192
+ castShadow?: boolean
193
+ /** Shadow-map options, merged key by key (setLight keeps unmentioned ones). */
194
+ shadow?: SpotShadowOptions
195
+ }
196
+ export type PointLightOptions = {
197
+ color?: Vec3
198
+ intensity?: number
199
+ distance?: number
200
+ decay?: number
201
+ castShadow?: boolean
202
+ /** Shadow-map options, merged key by key (setLight keeps unmentioned ones). */
203
+ shadow?: SpotShadowOptions
204
+ }
205
+ export type HemisphereLightOptions = { sky?: Vec3; ground?: Vec3; intensity?: number }
206
+
207
+ /** Every light may cast; the real bound is the shadow-slot budget
208
+ * (MAX_SHADOW_MAPS: a directional light claims `shadow.cascades` slots,
209
+ * a point light six, a spot one, and a caster past the budget throws at
210
+ * attach). Every map is a tile of the scene's one shadow atlas, so the
211
+ * pass count never follows the caster count, the fill does. */
212
+ export const MAX_SHADOWS = MAX_LIGHTS
213
+ export { MAX_CASCADES }
214
+
215
+ // The spot cone bounds: a half-angle in (0, 90] degrees, penumbra a
216
+ // 0..1 fraction of it.
217
+ const SPOT_ANGLE_MAX = 90
218
+
219
+ function checkSpot(update: SpotLightOptions): void {
220
+ if (update.angle !== undefined && !(update.angle > 0 && update.angle <= SPOT_ANGLE_MAX)) {
221
+ throw new Error("Spot angle must be in (0, 90] degrees")
222
+ }
223
+ if (update.penumbra !== undefined && !(update.penumbra >= 0 && update.penumbra <= 1)) {
224
+ throw new Error("Spot penumbra must be in 0..1")
225
+ }
226
+ checkFalloff(update)
227
+ }
228
+
229
+ function checkFalloff(update: { distance?: number; decay?: number }): void {
230
+ if (update.distance !== undefined && !(update.distance >= 0)) throw new Error("Light distance must be >= 0 (0 = no cutoff)")
231
+ if (update.decay !== undefined && !(update.decay >= 0)) throw new Error("Light decay must be >= 0")
232
+ }
233
+
234
+ function checkRadius(radius: number): number {
235
+ if (!(radius >= 1)) throw new Error("shadow.radius must be >= 1 (1 = the single hardware tap)")
236
+ return radius
237
+ }
238
+
239
+ function mergeShadow(into: DirectionalLight["shadow"], update: ShadowOptions): void {
240
+ if (update.mapSize !== undefined) into.mapSize = update.mapSize
241
+ if (update.bias !== undefined) into.bias = update.bias
242
+ if (update.normalBias !== undefined) into.normalBias = update.normalBias
243
+ if (update.radius !== undefined) into.radius = checkRadius(update.radius)
244
+ if (update.camera !== undefined) Object.assign(into.camera, update.camera)
245
+ if (update.cascades !== undefined) {
246
+ let n = update.cascades
247
+ if (!Number.isInteger(n) || n < 1 || n > MAX_CASCADES) throw new Error("shadow.cascades must be an integer from 1 to " + MAX_CASCADES)
248
+ into.cascades = n
249
+ }
250
+ if (update.distance !== undefined) {
251
+ let d = update.distance
252
+ if (d !== null && !(d > 0)) throw new Error("shadow.distance must be a positive number or null")
253
+ into.distance = d
254
+ }
255
+ }
256
+
257
+ export function createDirectionalLight(opts: DirectionalLightOptions = {}): DirectionalLight {
258
+ let light = makeNode("light") as DirectionalLight
259
+ light.type = "directional"
260
+ light.direction = [...(opts.direction ?? [0, -1, 0])] as Vec3
261
+ light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
262
+ light.intensity = opts.intensity ?? 1
263
+ light.castShadow = opts.castShadow === true
264
+ light.shadow = { mapSize: 1024, bias: 0, normalBias: 0, radius: 1, camera: { left: -5, right: 5, top: 5, bottom: -5, near: 0.5, far: 500 }, cascades: 1, distance: null }
265
+ if (opts.shadow !== undefined) mergeShadow(light.shadow, opts.shadow)
266
+ return light
267
+ }
268
+
269
+ export function createSpotLight(opts: SpotLightOptions = {}): SpotLight {
270
+ checkSpot(opts)
271
+ let light = makeNode("light") as SpotLight
272
+ light.type = "spot"
273
+ light.direction = [...(opts.direction ?? [0, -1, 0])] as Vec3
274
+ light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
275
+ light.intensity = opts.intensity ?? 1
276
+ light.distance = opts.distance ?? 0
277
+ light.angle = opts.angle ?? 60
278
+ light.penumbra = opts.penumbra ?? 0
279
+ light.decay = opts.decay ?? 2
280
+ light.castShadow = opts.castShadow === true
281
+ light.shadow = { mapSize: 1024, bias: 0, normalBias: 0, radius: 1, near: 0.5 }
282
+ if (opts.shadow !== undefined) mergeSpotShadow(light.shadow, opts.shadow)
283
+ return light
284
+ }
285
+
286
+ function mergeSpotShadow(into: SpotLight["shadow"], update: SpotShadowOptions): void {
287
+ if (update.mapSize !== undefined) into.mapSize = update.mapSize
288
+ if (update.bias !== undefined) into.bias = update.bias
289
+ if (update.normalBias !== undefined) into.normalBias = update.normalBias
290
+ if (update.near !== undefined) {
291
+ if (!(update.near > 0)) throw new Error("shadow.near must be a positive number")
292
+ into.near = update.near
293
+ }
294
+ }
295
+
296
+ export function createPointLight(opts: PointLightOptions = {}): PointLight {
297
+ checkFalloff(opts)
298
+ let light = makeNode("light") as PointLight
299
+ light.type = "point"
300
+ light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
301
+ light.intensity = opts.intensity ?? 1
302
+ light.distance = opts.distance ?? 0
303
+ light.decay = opts.decay ?? 2
304
+ light.castShadow = opts.castShadow === true
305
+ light.shadow = { mapSize: 1024, bias: 0, normalBias: 0, radius: 1, near: 0.5 }
306
+ if (opts.shadow !== undefined) mergeSpotShadow(light.shadow, opts.shadow)
307
+ return light
308
+ }
309
+
310
+ export function createHemisphereLight(opts: HemisphereLightOptions = {}): HemisphereLight {
311
+ let light = makeNode("light") as HemisphereLight
312
+ light.type = "hemisphere"
313
+ light.sky = [...(opts.sky ?? [1, 1, 1])] as Vec3
314
+ light.ground = [...(opts.ground ?? [0.2, 0.2, 0.2])] as Vec3
315
+ light.intensity = opts.intensity ?? 1
316
+ return light
317
+ }
318
+
319
+ /** The write path for a light's own fields (color, intensity, direction,
320
+ * the cone and falloff, or sky/ground); absent keys keep their value.
321
+ * Its placement goes through setTransform like any node.
322
+ * Frame-rate-safe. */
323
+ export function setLight(light: DirectionalLight, update: DirectionalLightOptions): void
324
+ export function setLight(light: SpotLight, update: SpotLightOptions): void
325
+ export function setLight(light: PointLight, update: PointLightOptions): void
326
+ export function setLight(light: HemisphereLight, update: HemisphereLightOptions): void
327
+ export function setLight(
328
+ light: Light,
329
+ update: DirectionalLightOptions & SpotLightOptions & PointLightOptions & HemisphereLightOptions,
330
+ ): void {
331
+ if (update.intensity !== undefined) light.intensity = update.intensity
332
+ if (light.type === "directional") {
333
+ if (update.direction !== undefined) light.direction = [...update.direction] as Vec3
334
+ if (update.color !== undefined) light.color = [...update.color] as Vec3
335
+ let shadowChanged = false
336
+ if (update.castShadow !== undefined && update.castShadow !== light.castShadow) {
337
+ light.castShadow = update.castShadow
338
+ shadowChanged = true
339
+ }
340
+ if (update.shadow !== undefined) {
341
+ mergeShadow(light.shadow, update.shadow)
342
+ shadowChanged = true
343
+ }
344
+ if (shadowChanged) light._scene?._shadowChanged(light)
345
+ } else if (light.type === "spot") {
346
+ checkSpot(update)
347
+ if (update.direction !== undefined) light.direction = [...update.direction] as Vec3
348
+ if (update.color !== undefined) light.color = [...update.color] as Vec3
349
+ if (update.distance !== undefined) light.distance = update.distance
350
+ if (update.angle !== undefined) light.angle = update.angle
351
+ if (update.penumbra !== undefined) light.penumbra = update.penumbra
352
+ if (update.decay !== undefined) light.decay = update.decay
353
+ let shadowChanged = false
354
+ if (update.castShadow !== undefined && update.castShadow !== light.castShadow) {
355
+ light.castShadow = update.castShadow
356
+ shadowChanged = true
357
+ }
358
+ if (update.shadow !== undefined) {
359
+ mergeSpotShadow(light.shadow, update.shadow)
360
+ shadowChanged = true
361
+ }
362
+ if (shadowChanged) light._scene?._shadowChanged(light)
363
+ } else if (light.type === "point") {
364
+ checkFalloff(update)
365
+ if (update.color !== undefined) light.color = [...update.color] as Vec3
366
+ if (update.distance !== undefined) light.distance = update.distance
367
+ if (update.decay !== undefined) light.decay = update.decay
368
+ let shadowChanged = false
369
+ if (update.castShadow !== undefined && update.castShadow !== light.castShadow) {
370
+ light.castShadow = update.castShadow
371
+ shadowChanged = true
372
+ }
373
+ if (update.shadow !== undefined) {
374
+ mergeSpotShadow(light.shadow, update.shadow)
375
+ shadowChanged = true
376
+ }
377
+ if (shadowChanged) light._scene?._shadowChanged(light)
378
+ } else {
379
+ if (update.sky !== undefined) light.sky = [...update.sky] as Vec3
380
+ if (update.ground !== undefined) light.ground = [...update.ground] as Vec3
381
+ }
382
+ light._scene?._lightChanged()
383
+ }