@solidrt/3d 0.0.55 → 0.0.56

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 (54) hide show
  1. package/AGENTS.md +1083 -183
  2. package/README.md +15 -7
  3. package/examples/README.md +79 -5
  4. package/examples/aim.tsx +2 -3
  5. package/examples/cascades.tsx +1 -4
  6. package/examples/collision.tsx +250 -0
  7. package/examples/first-person.tsx +119 -0
  8. package/examples/fleet.tsx +194 -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 +32 -6
  17. package/examples/pick.tsx +2 -4
  18. package/examples/probe.tsx +101 -0
  19. package/examples/scene-background.tsx +2 -4
  20. package/examples/scene-basic.tsx +2 -4
  21. package/examples/scene-views.tsx +32 -3
  22. package/examples/shadows.tsx +2 -4
  23. package/examples/sky-lit.tsx +86 -0
  24. package/examples/skybox.tsx +128 -0
  25. package/examples/sprites.tsx +4 -6
  26. package/examples/standard.tsx +132 -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 +199 -0
  32. package/src/color.ts +32 -0
  33. package/src/components.tsx +763 -82
  34. package/src/environment-bake.ts +492 -0
  35. package/src/environment.ts +325 -0
  36. package/src/first-person.ts +306 -0
  37. package/src/geometry.ts +6 -2
  38. package/src/glsl.ts +1343 -82
  39. package/src/gltf.ts +432 -112
  40. package/src/index.ts +57 -9
  41. package/src/light.ts +383 -0
  42. package/src/material.ts +667 -232
  43. package/src/math.ts +110 -0
  44. package/src/mesh.ts +728 -0
  45. package/src/mixer.ts +476 -0
  46. package/src/model-file.ts +80 -15
  47. package/src/model.ts +204 -35
  48. package/src/node.ts +506 -0
  49. package/src/orbit.ts +97 -48
  50. package/src/scene-pointer.ts +166 -0
  51. package/src/scene-shadows.ts +471 -0
  52. package/src/scene.ts +1242 -1557
  53. package/tools/environment.ts +69 -0
  54. package/tools/model.ts +4 -3
package/src/index.ts CHANGED
@@ -5,8 +5,33 @@
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 { SceneNode, ScenePointerEvent, 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 type { Capsule, EnvironmentOptions, FogOptions, Hit, Impact, OrientedBox, Overlap, QueryOptions, ReflectionProbe, ReflectionProbeOptions, Scene as SceneHandle, SceneHandlers, SceneOptions, ScreenRay, SkyboxOptions, Sphere, ToneMapping, View, ViewOptions, Volume } from "./scene.ts"
30
+ export { moveAndSlide } from "./collision.ts"
31
+ export type { MoveOptions, MoveResult, MoveScene } from "./collision.ts"
32
+ export { equirectToCube, loadEnvironment } from "./environment.ts"
33
+ export { linearColor, linearToSrgb, srgbToLinear } from "./color.ts"
34
+ export type { CameraState, CameraUpdate, OrthoExtent } from "./camera.ts"
10
35
  export type { NodeTransition, NodeTransitionSpec } from "flux:spatial"
11
36
  export { disposeGeometry } from "./geometry-gpu.ts"
12
37
  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 +40,40 @@ export { fillet, roundRect, shape, triangulate } from "./profile.ts"
15
40
  export type { Profile, ProfilePoint } from "./profile.ts"
16
41
  export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
17
42
  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"
43
+ export { lit, shaderMaterial, shaderMaterialClass, sprite, standard, unlit } from "./material.ts"
44
+ export type { LitOptions, Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, StandardOptions, UnlitOptions } from "./material.ts"
45
+ export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedMesh, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, useScene } from "./components.tsx"
46
+ export type {
47
+ DirectionalLightProps,
48
+ FirstPersonCameraProps,
49
+ HemisphereLightProps,
50
+ InstancedMeshProps,
51
+ InstanceProps,
52
+ MeshProps,
53
+ OrbitCameraProps,
54
+ PerspectiveCameraProps,
55
+ PointerEventProps,
56
+ PointLightProps,
57
+ RecordMeshProps,
58
+ SceneInput,
59
+ SceneInputListener,
60
+ SceneProps,
61
+ SpotLightProps,
62
+ SpriteProps,
63
+ TransformProps,
64
+ } from "./components.tsx"
22
65
  export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
23
- export type { ModelData, ModelMaterial, ModelPart, UriResolver } from "./gltf.ts"
66
+ export type { ModelChannel, ModelClip, ModelData, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
24
67
  export { decodeModel, encodeModel } from "./model-file.ts"
25
68
  export { createModel, loadGltf, loadModel } from "./model.ts"
26
- export type { Model, ModelOptions } from "./model.ts"
69
+ export type { Model, ModelMaps, ModelOptions } from "./model.ts"
70
+ export { createMixer } from "./mixer.ts"
71
+ export type { Mixer, MixerOptions, MixerPlayOptions } from "./mixer.ts"
72
+ export { sampleChannel } from "./clip.ts"
27
73
  export { createOrbitCamera } from "./orbit.ts"
28
- export type { OrbitCamera, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
74
+ export type { OrbitCamera as OrbitCameraHandle, OrbitCameraOptions, OrbitPose, OrbitTarget } from "./orbit.ts"
75
+ export { createFirstPersonCamera } from "./first-person.ts"
76
+ export type { FirstPersonCamera as FirstPersonCameraHandle, FirstPersonCameraOptions, FirstPersonPose, FirstPersonTarget } from "./first-person.ts"
29
77
  // math's lookAt (the camera view matrix) stays on the /math subpath: the
30
78
  // root's lookAt is the scene verb, the same split as `add`.
31
79
  export { rayBoxDistance, compose, copy, eulerFromQuat, identity, mat4, multiply, normalMatrix, orthographic, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
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
+ }