@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.
- package/AGENTS.md +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- 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,
|
|
9
|
-
export type {
|
|
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.
|
|
21
|
-
export type {
|
|
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
|
+
}
|