@solidrt/3d 0.0.54 → 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.
- package/AGENTS.md +1121 -177
- package/README.md +15 -7
- package/examples/README.md +86 -5
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +20 -5
- package/examples/collision.tsx +250 -0
- package/examples/first-person.tsx +119 -0
- package/examples/fleet.tsx +194 -0
- package/examples/fog.tsx +173 -0
- 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 +32 -6
- package/examples/pick.tsx +2 -4
- package/examples/probe.tsx +101 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +32 -3
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +86 -0
- package/examples/skybox.tsx +128 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +132 -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 +199 -0
- package/src/color.ts +32 -0
- package/src/components.tsx +773 -82
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +306 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1390 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +57 -9
- package/src/light.ts +383 -0
- package/src/material.ts +682 -216
- 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 +211 -34
- package/src/node.ts +506 -0
- package/src/orbit.ts +97 -48
- package/src/scene-pointer.ts +166 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1304 -1532
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// A skybox and its reflections: the scene's background sampled from a
|
|
2
|
+
// cube map along each pixel's view ray - `<Scene background={{ cube,
|
|
3
|
+
// intensity?, rotation? }}>`, Three's `scene.background = cubeTexture` -
|
|
4
|
+
// and the same cube as the scene's environment (`environment={{ cube,
|
|
5
|
+
// rotation }}`, Three's `scene.environment`), which every lit material
|
|
6
|
+
// with `reflectivity` mirrors: the chrome sphere sharply, the brass knot
|
|
7
|
+
// blurred by its shininess. The cube map is baked in JS at startup (a
|
|
8
|
+
// horizon gradient, a sun disc and its glow), so the example ships no
|
|
9
|
+
// image assets; a photographed sky is the same createCubeTexture call
|
|
10
|
+
// with six decoded faces in Three's px, nx, py, ny, pz, nz order, or
|
|
11
|
+
// equirectToCube on a panorama. The sky turns slowly and the sun light
|
|
12
|
+
// turns with it: `rotation` is a reactive prop the scene updates in
|
|
13
|
+
// place, no recompile. Drag to look around, wheel to zoom.
|
|
14
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
15
|
+
import { createCubeTexture } from "@solidrt/core/gpu"
|
|
16
|
+
import { box, DirectionalLight, HemisphereLight, lit, Mesh, OrbitCamera, plane, Scene, sphere, torusKnot } from "@solidrt/3d"
|
|
17
|
+
import { normalize } from "@solidrt/3d/math"
|
|
18
|
+
import type { Vec3 } from "@solidrt/3d/math"
|
|
19
|
+
|
|
20
|
+
// Face edge in texels; the sun disc is the finest feature, and mipmaps
|
|
21
|
+
// keep the horizon smooth when it shrinks toward a screen pixel.
|
|
22
|
+
const FACE = 128
|
|
23
|
+
// The sun's direction in the unrotated sky.
|
|
24
|
+
const SUN: Vec3 = normalize([0.6, 0.35, -0.7])
|
|
25
|
+
// Cosine of the disc's angular radius (1.5 degrees).
|
|
26
|
+
const SUN_DISC = Math.cos((1.5 * Math.PI) / 180)
|
|
27
|
+
// Glow falloff exponent around the disc (higher = tighter).
|
|
28
|
+
const SUN_GLOW = 40
|
|
29
|
+
// How fast the sky turns about y, radians per second.
|
|
30
|
+
const TURN_SPEED = 0.06
|
|
31
|
+
// How quickly the ground band darkens below the horizon (per unit of -y).
|
|
32
|
+
const GROUND_FALL = 4
|
|
33
|
+
|
|
34
|
+
type Rgb = [number, number, number]
|
|
35
|
+
const ZENITH: Rgb = [0.16, 0.34, 0.74]
|
|
36
|
+
const HORIZON: Rgb = [0.8, 0.87, 0.95]
|
|
37
|
+
const GROUND: Rgb = [0.3, 0.27, 0.24]
|
|
38
|
+
const SUN_COLOR: Rgb = [1, 0.95, 0.82]
|
|
39
|
+
|
|
40
|
+
let mix = (a: Rgb, b: Rgb, t: number): Rgb => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
|
41
|
+
|
|
42
|
+
// The sky's color along a world direction.
|
|
43
|
+
function sky(d: Vec3): Rgb {
|
|
44
|
+
let y = d[1]
|
|
45
|
+
let rgb = y >= 0 ? mix(HORIZON, ZENITH, Math.sqrt(y)) : mix(HORIZON, GROUND, Math.min(1, -y * GROUND_FALL))
|
|
46
|
+
let s = d[0] * SUN[0] + d[1] * SUN[1] + d[2] * SUN[2]
|
|
47
|
+
if (s > 0) {
|
|
48
|
+
// The disc is the glow at full strength: added to the sky it clamps
|
|
49
|
+
// to white, and the warm tint shows in the halo around it.
|
|
50
|
+
let g = s > SUN_DISC ? 1 : Math.pow(s, SUN_GLOW)
|
|
51
|
+
rgb = [rgb[0] + SUN_COLOR[0] * g, rgb[1] + SUN_COLOR[1] * g, rgb[2] + SUN_COLOR[2] * g]
|
|
52
|
+
}
|
|
53
|
+
return [Math.min(1, rgb[0]), Math.min(1, rgb[1]), Math.min(1, rgb[2])]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The world direction whose lookup lands on face i's texel (s, t), t = 0
|
|
57
|
+
// the first row: the GL cube-map table, which is also what the library
|
|
58
|
+
// samples with (no flip) - so the texel holds the sky in that direction.
|
|
59
|
+
function texelDirection(face: number, s: number, t: number): Vec3 {
|
|
60
|
+
let a = 2 * s - 1
|
|
61
|
+
let b = 2 * t - 1
|
|
62
|
+
let d: Vec3 =
|
|
63
|
+
face === 0 ? [1, -b, -a] : face === 1 ? [-1, -b, a] : face === 2 ? [a, 1, b] : face === 3 ? [a, -1, -b] : face === 4 ? [a, -b, 1] : [-a, -b, -1]
|
|
64
|
+
return normalize(d)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function bakeSky(): Uint8Array[] {
|
|
68
|
+
let faces: Uint8Array[] = []
|
|
69
|
+
for (let face = 0; face < 6; face++) {
|
|
70
|
+
let px = new Uint8Array(FACE * FACE * 4)
|
|
71
|
+
for (let row = 0; row < FACE; row++) {
|
|
72
|
+
for (let col = 0; col < FACE; col++) {
|
|
73
|
+
let rgb = sky(texelDirection(face, (col + 0.5) / FACE, (row + 0.5) / FACE))
|
|
74
|
+
let i = (row * FACE + col) * 4
|
|
75
|
+
px[i] = Math.round(rgb[0] * 255)
|
|
76
|
+
px[i + 1] = Math.round(rgb[1] * 255)
|
|
77
|
+
px[i + 2] = Math.round(rgb[2] * 255)
|
|
78
|
+
px[i + 3] = 255
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
faces.push(px)
|
|
82
|
+
}
|
|
83
|
+
return faces
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The sun's direction once the sky has turned by `angle` about y (the same
|
|
87
|
+
// turn a node with rotation [0, angle, 0] applies).
|
|
88
|
+
function turnedSun(angle: number): Vec3 {
|
|
89
|
+
let c = Math.cos(angle)
|
|
90
|
+
let s = Math.sin(angle)
|
|
91
|
+
return [SUN[0] * c + SUN[2] * s, SUN[1], -SUN[0] * s + SUN[2] * c]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function App() {
|
|
95
|
+
let cube = createCubeTexture(bakeSky(), FACE, { format: "rgba8-srgb", mipmap: true, label: "sky" })
|
|
96
|
+
let [turn, setTurn] = createSignal(0)
|
|
97
|
+
let last = 0
|
|
98
|
+
onFrame(tick => {
|
|
99
|
+
if (last !== 0) setTurn(turn() + ((tick - last) / 1000) * TURN_SPEED)
|
|
100
|
+
last = tick
|
|
101
|
+
})
|
|
102
|
+
let sun = () => turnedSun(turn())
|
|
103
|
+
|
|
104
|
+
let ground = lit({ color: [0.36, 0.4, 0.3] })
|
|
105
|
+
// Glossy: a blurred reflection (shininess 48) at a dielectric's face-on
|
|
106
|
+
// weight, rising toward the silhouette.
|
|
107
|
+
let brass = lit({ color: [0.85, 0.6, 0.3], specular: 0.6, shininess: 48, reflectivity: 0.15 })
|
|
108
|
+
let stone = lit({ color: [0.6, 0.6, 0.62] })
|
|
109
|
+
// Chrome: the environment itself, sharp.
|
|
110
|
+
let chrome = lit({ color: [1, 1, 1], specular: 1, shininess: 400, reflectivity: 1 })
|
|
111
|
+
return (
|
|
112
|
+
<window>
|
|
113
|
+
<view width={pct(100)} height={pct(100)}>
|
|
114
|
+
<Scene background={{ cube, rotation: turn() }} environment={{ cube, rotation: turn() }} label="skybox-demo">
|
|
115
|
+
<OrbitCamera azimuth={0.4} elevation={0.12} distance={7} />
|
|
116
|
+
<HemisphereLight sky={[0.5, 0.6, 0.8]} ground={[0.3, 0.27, 0.24]} />
|
|
117
|
+
<DirectionalLight direction={[-sun()[0], -sun()[1], -sun()[2]]} color={SUN_COLOR} intensity={0.9} />
|
|
118
|
+
<Mesh geometry={plane({ width: 12, height: 12 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
|
|
119
|
+
<Mesh geometry={torusKnot({ radius: 0.7, tube: 0.22, tubularSegments: 128, radialSegments: 16 })} material={brass} position={[0, 1.2, 0]} />
|
|
120
|
+
<Mesh geometry={box()} material={stone} position={[-2, 0.5, -0.5]} />
|
|
121
|
+
<Mesh geometry={sphere({ radius: 0.6, widthSegments: 48, heightSegments: 32 })} material={chrome} position={[2, 0.6, -0.5]} />
|
|
122
|
+
</Scene>
|
|
123
|
+
</view>
|
|
124
|
+
</window>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
render(() => <App />)
|
package/examples/sprites.tsx
CHANGED
|
@@ -9,8 +9,6 @@ import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
|
9
9
|
import { createTexture } from "@solidrt/core/gpu"
|
|
10
10
|
import { box, Group, Mesh, PerspectiveCamera, plane, Scene, sprite, Sprite, unlit } from "@solidrt/3d"
|
|
11
11
|
|
|
12
|
-
const SIZE = 720
|
|
13
|
-
|
|
14
12
|
// A soft disc, alpha falling off from the center; premultiplied like every
|
|
15
13
|
// texture the engine samples.
|
|
16
14
|
function glow(): ReturnType<typeof createTexture> {
|
|
@@ -25,7 +23,7 @@ function glow(): ReturnType<typeof createTexture> {
|
|
|
25
23
|
data.set([255 * a, 240 * a, 200 * a, 255 * a], (y * n + x) * 4)
|
|
26
24
|
}
|
|
27
25
|
}
|
|
28
|
-
return createTexture(data, n, n)
|
|
26
|
+
return createTexture(data, n, n, { format: "rgba8-srgb" })
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
// A cutout tree: green triangle over a brown trunk, transparent elsewhere.
|
|
@@ -43,7 +41,7 @@ function tree(): ReturnType<typeof createTexture> {
|
|
|
43
41
|
data.set([rgb[0]!, rgb[1]!, rgb[2]!, a], (y * n + x) * 4)
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
|
-
return createTexture(data, n, n)
|
|
44
|
+
return createTexture(data, n, n, { format: "rgba8-srgb" })
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
function App() {
|
|
@@ -68,8 +66,8 @@ function App() {
|
|
|
68
66
|
|
|
69
67
|
return (
|
|
70
68
|
<window>
|
|
71
|
-
<view width={pct(100)} height={pct(100)}
|
|
72
|
-
<Scene
|
|
69
|
+
<view width={pct(100)} height={pct(100)}>
|
|
70
|
+
<Scene clearColor={[0.07, 0.07, 0.1, 1]} label="sprites">
|
|
73
71
|
<PerspectiveCamera fov={50} position={eye()} lookAt={[0, 0.6, 0]} />
|
|
74
72
|
<Mesh
|
|
75
73
|
geometry={plane({ width: 8, height: 8 })}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// The standard material: the sphere grid every PBR engine's docs open
|
|
2
|
+
// with - metalness down the rows, roughness across the columns - under a
|
|
3
|
+
// baked sky as background and environment (the skybox example's bake) and
|
|
4
|
+
// a warm sun casting shadows. What to look for: the bare-metal row IS the
|
|
5
|
+
// environment, sharp at the left and blurred toward the right, tinted by
|
|
6
|
+
// the base color and dark wherever the sky is; the dielectric row keeps
|
|
7
|
+
// its diffuse color and shows the sky as a faint gloss, a mirror highlight
|
|
8
|
+
// at the left that widens to the right and brightens toward every
|
|
9
|
+
// silhouette; the rows between blend the two. standard takes lit's base,
|
|
10
|
+
// maps, cutout, shadow, emissive and fog options - only the Blinn-Phong
|
|
11
|
+
// knobs are gone, replaced by metalness/roughness and their packed maps.
|
|
12
|
+
// Light intensities read as lit's: 1 lights a white matte surface to 1.
|
|
13
|
+
// Drag to look around.
|
|
14
|
+
|
|
15
|
+
import { pct, render } from "@solidrt/core"
|
|
16
|
+
import { createCubeTexture } from "@solidrt/core/gpu"
|
|
17
|
+
import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, plane, Scene, sphere, standard } from "@solidrt/3d"
|
|
18
|
+
import { normalize } from "@solidrt/3d/math"
|
|
19
|
+
import type { Vec3 } from "@solidrt/3d/math"
|
|
20
|
+
|
|
21
|
+
// Metalness steps down the rows, roughness across the columns.
|
|
22
|
+
const ROWS = 4
|
|
23
|
+
const COLS = 6
|
|
24
|
+
// Sphere radius and grid pitch, world units.
|
|
25
|
+
const RADIUS = 0.42
|
|
26
|
+
const PITCH = 1.05
|
|
27
|
+
// The spheres' base color, sRGB: a warm red that reads as paint and as metal.
|
|
28
|
+
const COLOR: [number, number, number] = [0.8, 0.25, 0.2]
|
|
29
|
+
// Height of the bottom row's centers above the ground.
|
|
30
|
+
const LIFT = 0.6
|
|
31
|
+
// Face edge of the baked sky in texels.
|
|
32
|
+
const FACE = 128
|
|
33
|
+
// The sun's direction (toward it).
|
|
34
|
+
const SUN: Vec3 = normalize([0.6, 0.45, -0.7])
|
|
35
|
+
// How far out the sun light sits, so its shadow camera covers the grid.
|
|
36
|
+
const SUN_DISTANCE = 12
|
|
37
|
+
// Cosine of the sun disc's angular radius (1.5 degrees).
|
|
38
|
+
const SUN_DISC = Math.cos((1.5 * Math.PI) / 180)
|
|
39
|
+
// Glow falloff exponent around the disc (higher = tighter).
|
|
40
|
+
const SUN_GLOW = 40
|
|
41
|
+
// How quickly the ground band darkens below the horizon (per unit of -y).
|
|
42
|
+
const GROUND_FALL = 4
|
|
43
|
+
|
|
44
|
+
type Rgb = [number, number, number]
|
|
45
|
+
const ZENITH: Rgb = [0.16, 0.34, 0.74]
|
|
46
|
+
const HORIZON: Rgb = [0.8, 0.87, 0.95]
|
|
47
|
+
const GROUND: Rgb = [0.3, 0.27, 0.24]
|
|
48
|
+
const SUN_COLOR: Rgb = [1, 0.95, 0.82]
|
|
49
|
+
|
|
50
|
+
let mix = (a: Rgb, b: Rgb, t: number): Rgb => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
|
51
|
+
|
|
52
|
+
// The sky's color along a world direction.
|
|
53
|
+
function sky(d: Vec3): Rgb {
|
|
54
|
+
let y = d[1]
|
|
55
|
+
let rgb = y >= 0 ? mix(HORIZON, ZENITH, Math.sqrt(y)) : mix(HORIZON, GROUND, Math.min(1, -y * GROUND_FALL))
|
|
56
|
+
let s = d[0] * SUN[0] + d[1] * SUN[1] + d[2] * SUN[2]
|
|
57
|
+
if (s > 0) {
|
|
58
|
+
let g = s > SUN_DISC ? 1 : Math.pow(s, SUN_GLOW)
|
|
59
|
+
rgb = [rgb[0] + SUN_COLOR[0] * g, rgb[1] + SUN_COLOR[1] * g, rgb[2] + SUN_COLOR[2] * g]
|
|
60
|
+
}
|
|
61
|
+
return [Math.min(1, rgb[0]), Math.min(1, rgb[1]), Math.min(1, rgb[2])]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The world direction whose lookup lands on face i's texel (s, t) - the
|
|
65
|
+
// GL cube-map table, no flip (see skybox.tsx).
|
|
66
|
+
function texelDirection(face: number, s: number, t: number): Vec3 {
|
|
67
|
+
let a = 2 * s - 1
|
|
68
|
+
let b = 2 * t - 1
|
|
69
|
+
let d: Vec3 =
|
|
70
|
+
face === 0 ? [1, -b, -a] : face === 1 ? [-1, -b, a] : face === 2 ? [a, 1, b] : face === 3 ? [a, -1, -b] : face === 4 ? [a, -b, 1] : [-a, -b, -1]
|
|
71
|
+
return normalize(d)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bakeSky(): Uint8Array[] {
|
|
75
|
+
let faces: Uint8Array[] = []
|
|
76
|
+
for (let face = 0; face < 6; face++) {
|
|
77
|
+
let px = new Uint8Array(FACE * FACE * 4)
|
|
78
|
+
for (let row = 0; row < FACE; row++) {
|
|
79
|
+
for (let col = 0; col < FACE; col++) {
|
|
80
|
+
let rgb = sky(texelDirection(face, (col + 0.5) / FACE, (row + 0.5) / FACE))
|
|
81
|
+
let i = (row * FACE + col) * 4
|
|
82
|
+
px[i] = Math.round(rgb[0] * 255)
|
|
83
|
+
px[i + 1] = Math.round(rgb[1] * 255)
|
|
84
|
+
px[i + 2] = Math.round(rgb[2] * 255)
|
|
85
|
+
px[i + 3] = 255
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
faces.push(px)
|
|
89
|
+
}
|
|
90
|
+
return faces
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function App() {
|
|
94
|
+
let cube = createCubeTexture(bakeSky(), FACE, { format: "rgba8-srgb", mipmap: true, label: "sky" })
|
|
95
|
+
let ball = sphere({ radius: RADIUS, widthSegments: 48, heightSegments: 32 })
|
|
96
|
+
let ground = standard({ color: [0.32, 0.33, 0.3], roughness: 0.9 })
|
|
97
|
+
// One material per cell: metalness by row (top row bare metal, bottom
|
|
98
|
+
// row dielectric), roughness by column (mirror at the left).
|
|
99
|
+
let cells: { material: ReturnType<typeof standard>; position: Vec3 }[] = []
|
|
100
|
+
for (let r = 0; r < ROWS; r++) {
|
|
101
|
+
for (let c = 0; c < COLS; c++) {
|
|
102
|
+
cells.push({
|
|
103
|
+
material: standard({ color: COLOR, metalness: 1 - r / (ROWS - 1), roughness: c / (COLS - 1) }),
|
|
104
|
+
position: [(c - (COLS - 1) / 2) * PITCH, LIFT + (ROWS - 1 - r) * PITCH, 0],
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let center: Vec3 = [0, LIFT + ((ROWS - 1) * PITCH) / 2, 0]
|
|
109
|
+
return (
|
|
110
|
+
<window>
|
|
111
|
+
<view width={pct(100)} height={pct(100)}>
|
|
112
|
+
<Scene background={{ cube }} environment={{ cube }} samples={4} label="standard-demo">
|
|
113
|
+
<OrbitCamera target={center} azimuth={0.25} elevation={0.15} distance={9} />
|
|
114
|
+
<HemisphereLight sky={[0.5, 0.6, 0.8]} ground={[0.3, 0.27, 0.24]} intensity={0.5} />
|
|
115
|
+
<DirectionalLight
|
|
116
|
+
position={[SUN[0] * SUN_DISTANCE, SUN[1] * SUN_DISTANCE, SUN[2] * SUN_DISTANCE]}
|
|
117
|
+
direction={[-SUN[0], -SUN[1], -SUN[2]]}
|
|
118
|
+
color={SUN_COLOR}
|
|
119
|
+
intensity={1}
|
|
120
|
+
castShadow
|
|
121
|
+
/>
|
|
122
|
+
<Mesh geometry={plane({ width: 16, height: 16 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
|
|
123
|
+
{cells.map(cell => (
|
|
124
|
+
<Mesh geometry={ball} material={cell.material} position={cell.position} castShadow />
|
|
125
|
+
))}
|
|
126
|
+
</Scene>
|
|
127
|
+
</view>
|
|
128
|
+
</window>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
render(() => <App />)
|
package/examples/sweep-paths.tsx
CHANGED
|
@@ -10,8 +10,6 @@ import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
|
10
10
|
import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, roundRect, Scene, sweep, tube, unlit } from "@solidrt/3d"
|
|
11
11
|
import type { SweepPath } from "@solidrt/3d"
|
|
12
12
|
|
|
13
|
-
const SIZE = 720
|
|
14
|
-
|
|
15
13
|
function App() {
|
|
16
14
|
let [spin, setSpin] = createSignal(0)
|
|
17
15
|
onFrame(tick => setSpin(tick / 4000))
|
|
@@ -39,8 +37,8 @@ function App() {
|
|
|
39
37
|
|
|
40
38
|
return (
|
|
41
39
|
<window>
|
|
42
|
-
<view width={pct(100)} height={pct(100)}
|
|
43
|
-
<Scene
|
|
40
|
+
<view width={pct(100)} height={pct(100)}>
|
|
41
|
+
<Scene clearColor={[0.07, 0.07, 0.1, 1]} label="sweep-paths">
|
|
44
42
|
<PerspectiveCamera fov={55} position={[0, 1.9, 3.9]} lookAt={[0, 0.35, 0]} />
|
|
45
43
|
<HemisphereLight sky={[0.45, 0.45, 0.45]} ground={[0.22, 0.22, 0.22]} />
|
|
46
44
|
<DirectionalLight direction={[-0.5, -0.8, -0.4]} intensity={0.8} />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.56",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"AGENTS.md"
|
|
19
19
|
],
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
22
|
-
"@solidrt/core": "0.0.
|
|
21
|
+
"@solidjs/signals": "2.0.0-rc.6",
|
|
22
|
+
"@solidrt/core": "0.0.56"
|
|
23
23
|
}
|
|
24
24
|
}
|
package/src/camera.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The camera record: one per scene target and one per view (the shadow
|
|
2
|
+
// views included), the same state and the same one-shared-write contract
|
|
3
|
+
// everywhere. Split out of scene.ts so the shadow system can place its
|
|
4
|
+
// map cameras through the exact code the scene uses for its own.
|
|
5
|
+
|
|
6
|
+
import { invert, lookAt as lookAtMatrix, mat4, multiply, orthographic, perspective } from "./math.ts"
|
|
7
|
+
import type { Mat4, Vec3 } from "./math.ts"
|
|
8
|
+
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
9
|
+
|
|
10
|
+
/** An orthographic projection's view-space extents, in world units (the
|
|
11
|
+
* same box at every depth). */
|
|
12
|
+
export type OrthoExtent = { left: number; right: number; top: number; bottom: number }
|
|
13
|
+
|
|
14
|
+
/** A camera snapshot (Scene/View `camera()`): CameraUpdate's fields, all
|
|
15
|
+
* present. Arrays are copies of the internal state. */
|
|
16
|
+
export type CameraState = {
|
|
17
|
+
fov: number
|
|
18
|
+
near: number
|
|
19
|
+
far: number
|
|
20
|
+
position: Vec3
|
|
21
|
+
target: Vec3
|
|
22
|
+
up: Vec3
|
|
23
|
+
ortho: OrthoExtent | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CameraUpdate = {
|
|
27
|
+
/** Vertical field of view in DEGREES (default 60). */
|
|
28
|
+
fov?: number
|
|
29
|
+
near?: number
|
|
30
|
+
far?: number
|
|
31
|
+
position?: Vec3
|
|
32
|
+
target?: Vec3
|
|
33
|
+
up?: Vec3
|
|
34
|
+
/** An orthographic projection with these extents (`fov` is then
|
|
35
|
+
* ignored); null returns to perspective. Three's OrthographicCamera as
|
|
36
|
+
* a camera option: a top-down map, an isometric view, a shadow-map
|
|
37
|
+
* light. */
|
|
38
|
+
ortho?: OrthoExtent | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// `dirty` = the matrices need recomputing (a setCamera or a resize),
|
|
42
|
+
// `pending` = the GPU write is owed to the next sync. The recompute is
|
|
43
|
+
// split from the sync so project()/viewProj() see a fresh matrix right
|
|
44
|
+
// after setCamera, before the microtask runs.
|
|
45
|
+
export type Camera = {
|
|
46
|
+
fov: number
|
|
47
|
+
near: number
|
|
48
|
+
far: number
|
|
49
|
+
eye: Vec3
|
|
50
|
+
target: Vec3
|
|
51
|
+
up: Vec3
|
|
52
|
+
ortho: OrthoExtent | null
|
|
53
|
+
/** Mirror the projection's x: the image comes out flipped left to
|
|
54
|
+
* right. A reflection probe's face cameras set it - a GL cube face is
|
|
55
|
+
* seen from outside, the x mirror of what a camera sees - and the cube
|
|
56
|
+
* draw target's pass inverts the winding to match. */
|
|
57
|
+
mirror: boolean
|
|
58
|
+
dirty: boolean
|
|
59
|
+
pending: boolean
|
|
60
|
+
proj: Mat4
|
|
61
|
+
view: Mat4
|
|
62
|
+
viewProj: Mat4
|
|
63
|
+
invViewProj: Mat4
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function makeCamera(): Camera {
|
|
67
|
+
return {
|
|
68
|
+
fov: 60,
|
|
69
|
+
near: 0.1,
|
|
70
|
+
far: 100,
|
|
71
|
+
eye: [0, 0, 3],
|
|
72
|
+
target: [0, 0, 0],
|
|
73
|
+
up: [0, 1, 0],
|
|
74
|
+
ortho: null,
|
|
75
|
+
mirror: false,
|
|
76
|
+
dirty: true,
|
|
77
|
+
pending: false,
|
|
78
|
+
proj: mat4(),
|
|
79
|
+
view: mat4(),
|
|
80
|
+
viewProj: mat4(),
|
|
81
|
+
invViewProj: mat4(),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function cameraState(cam: Camera): CameraState {
|
|
86
|
+
return {
|
|
87
|
+
fov: cam.fov,
|
|
88
|
+
near: cam.near,
|
|
89
|
+
far: cam.far,
|
|
90
|
+
position: [cam.eye[0], cam.eye[1], cam.eye[2]],
|
|
91
|
+
target: [cam.target[0], cam.target[1], cam.target[2]],
|
|
92
|
+
up: [cam.up[0], cam.up[1], cam.up[2]],
|
|
93
|
+
ortho: cam.ortho === null ? null : { left: cam.ortho.left, right: cam.ortho.right, top: cam.ortho.top, bottom: cam.ortho.bottom },
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function updateCamera(cam: Camera, update: CameraUpdate): void {
|
|
98
|
+
if (update.fov !== undefined) cam.fov = update.fov
|
|
99
|
+
if (update.near !== undefined) cam.near = update.near
|
|
100
|
+
if (update.far !== undefined) cam.far = update.far
|
|
101
|
+
if (update.position) cam.eye = [update.position[0], update.position[1], update.position[2]]
|
|
102
|
+
if (update.target) cam.target = [update.target[0], update.target[1], update.target[2]]
|
|
103
|
+
if (update.up) cam.up = [update.up[0], update.up[1], update.up[2]]
|
|
104
|
+
if (update.ortho !== undefined) {
|
|
105
|
+
let o = update.ortho
|
|
106
|
+
cam.ortho = o === null ? null : { left: o.left, right: o.right, top: o.top, bottom: o.bottom }
|
|
107
|
+
}
|
|
108
|
+
cam.dirty = true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function ensureCamera(cam: Camera, width: number, height: number): void {
|
|
112
|
+
if (!cam.dirty) return
|
|
113
|
+
cam.dirty = false
|
|
114
|
+
cam.pending = true
|
|
115
|
+
let o = cam.ortho
|
|
116
|
+
if (o === null) perspective(cam.proj, (cam.fov * Math.PI) / 180, width / height, cam.near, cam.far)
|
|
117
|
+
else orthographic(cam.proj, o.left, o.right, o.top, o.bottom, cam.near, cam.far)
|
|
118
|
+
if (cam.mirror) {
|
|
119
|
+
// Negate the clip x row (column-major: elements 0, 4, 8, 12).
|
|
120
|
+
cam.proj[0] = -cam.proj[0]!
|
|
121
|
+
cam.proj[4] = -cam.proj[4]!
|
|
122
|
+
cam.proj[8] = -cam.proj[8]!
|
|
123
|
+
cam.proj[12] = -cam.proj[12]!
|
|
124
|
+
}
|
|
125
|
+
lookAtMatrix(cam.view, cam.eye, cam.target, cam.up)
|
|
126
|
+
multiply(cam.viewProj, cam.proj, cam.view)
|
|
127
|
+
invert(cam.invViewProj, cam.viewProj)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The camera is target state: one shared write, whatever the target holds.
|
|
131
|
+
// Entries are untouched - uModel is camera-independent, and uCamPos is
|
|
132
|
+
// stored even when no current material declares it. The camera basis rides
|
|
133
|
+
// along: the view matrix's first two rows are the camera's world-space
|
|
134
|
+
// right and up (no clip flip - that lives in the projection), so a
|
|
135
|
+
// billboard needs no reconstruction from uViewProj. The inverse rides too:
|
|
136
|
+
// uInvViewProj carries a clip position back to world, which is how the
|
|
137
|
+
// background slot (and any shader declaring it) gets a world-space ray
|
|
138
|
+
// per pixel without knowing the projection.
|
|
139
|
+
export function cameraParams(cam: Camera): ShaderParams {
|
|
140
|
+
let v = cam.view
|
|
141
|
+
return {
|
|
142
|
+
uViewProj: cam.viewProj,
|
|
143
|
+
uInvViewProj: cam.invViewProj,
|
|
144
|
+
uCamPos: cam.eye,
|
|
145
|
+
uCamRight: [v[0], v[4], v[8]],
|
|
146
|
+
uCamUp: [v[1], v[5], v[9]],
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/clip.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Pure clip sampling - the mixer's core, split out so it runs with no
|
|
2
|
+
// scene or engine (checks under flux, custom drivers, a future bake-side
|
|
3
|
+
// resample). Everything here is plain array math over ModelChannel data.
|
|
4
|
+
|
|
5
|
+
import { quatNormalize, quatSlerp } from "./math.ts"
|
|
6
|
+
import type { Quat } from "./math.ts"
|
|
7
|
+
import type { ModelChannel } from "./gltf.ts"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sample one channel at `time` (seconds, clamped to the key range) into
|
|
11
|
+
* `out` - 3 floats for position/scale, 4 for rotation. Pure array math:
|
|
12
|
+
* step holds the earlier key, linear lerps (rotation slerps the shortest
|
|
13
|
+
* path), cubic evaluates the glTF CUBICSPLINE Hermite (rotation
|
|
14
|
+
* renormalized, per spec).
|
|
15
|
+
*/
|
|
16
|
+
export function sampleChannel(channel: ModelChannel, time: number, out: number[]): void {
|
|
17
|
+
let times = channel.times
|
|
18
|
+
let values = channel.values
|
|
19
|
+
let keys = times.length
|
|
20
|
+
let elements = channel.path === "rotation" ? 4 : 3
|
|
21
|
+
let stride = channel.interpolation === "cubic" ? elements * 3 : elements
|
|
22
|
+
// The value element offset within a key: cubic keys are [in, value, out].
|
|
23
|
+
let mid = channel.interpolation === "cubic" ? elements : 0
|
|
24
|
+
if (keys === 0) return
|
|
25
|
+
if (time <= times[0]! || keys === 1) {
|
|
26
|
+
for (let e = 0; e < elements; e++) out[e] = values[mid + e]!
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
if (time >= times[keys - 1]!) {
|
|
30
|
+
let at = (keys - 1) * stride + mid
|
|
31
|
+
for (let e = 0; e < elements; e++) out[e] = values[at + e]!
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
// The key pair around `time`: binary search for the last key at or
|
|
35
|
+
// before it.
|
|
36
|
+
let lo = 0
|
|
37
|
+
let hi = keys - 1
|
|
38
|
+
while (hi - lo > 1) {
|
|
39
|
+
let m = (lo + hi) >> 1
|
|
40
|
+
if (times[m]! <= time) lo = m
|
|
41
|
+
else hi = m
|
|
42
|
+
}
|
|
43
|
+
let t0 = times[lo]!
|
|
44
|
+
let t1 = times[hi]!
|
|
45
|
+
let span = t1 - t0
|
|
46
|
+
let s = span > 0 ? (time - t0) / span : 0
|
|
47
|
+
let a = lo * stride + mid
|
|
48
|
+
let b = hi * stride + mid
|
|
49
|
+
switch (channel.interpolation) {
|
|
50
|
+
case "step":
|
|
51
|
+
for (let e = 0; e < elements; e++) out[e] = values[a + e]!
|
|
52
|
+
return
|
|
53
|
+
case "linear":
|
|
54
|
+
if (channel.path === "rotation") {
|
|
55
|
+
readQuat(SAMPLE_A, values, a)
|
|
56
|
+
readQuat(SAMPLE_B, values, b)
|
|
57
|
+
quatSlerp(SAMPLE_A, SAMPLE_A, SAMPLE_B, s)
|
|
58
|
+
for (let e = 0; e < 4; e++) out[e] = SAMPLE_A[e]!
|
|
59
|
+
} else {
|
|
60
|
+
for (let e = 0; e < elements; e++) out[e] = values[a + e]! + (values[b + e]! - values[a + e]!) * s
|
|
61
|
+
}
|
|
62
|
+
return
|
|
63
|
+
case "cubic": {
|
|
64
|
+
// glTF's Hermite: p(s) = h00 v0 + h10 d b0 + h01 v1 + h11 d a1,
|
|
65
|
+
// where b0 is key lo's OUT-tangent and a1 key hi's IN-tangent
|
|
66
|
+
// (per-second, so they scale by the span d).
|
|
67
|
+
let s2 = s * s
|
|
68
|
+
let s3 = s2 * s
|
|
69
|
+
let h00 = 2 * s3 - 3 * s2 + 1
|
|
70
|
+
let h10 = s3 - 2 * s2 + s
|
|
71
|
+
let h01 = -2 * s3 + 3 * s2
|
|
72
|
+
let h11 = s3 - s2
|
|
73
|
+
let outTan = lo * stride + elements * 2
|
|
74
|
+
let inTan = hi * stride
|
|
75
|
+
for (let e = 0; e < elements; e++) {
|
|
76
|
+
out[e] = h00 * values[a + e]! + h10 * span * values[outTan + e]! + h01 * values[b + e]! + h11 * span * values[inTan + e]!
|
|
77
|
+
}
|
|
78
|
+
if (channel.path === "rotation") {
|
|
79
|
+
readQuat(SAMPLE_A, out, 0)
|
|
80
|
+
quatNormalize(SAMPLE_A, SAMPLE_A)
|
|
81
|
+
for (let e = 0; e < 4; e++) out[e] = SAMPLE_A[e]!
|
|
82
|
+
}
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const SAMPLE_A: Quat = [0, 0, 0, 1]
|
|
89
|
+
const SAMPLE_B: Quat = [0, 0, 0, 1]
|
|
90
|
+
|
|
91
|
+
function readQuat(out: Quat, values: ArrayLike<number>, at: number): void {
|
|
92
|
+
out[0] = values[at]!
|
|
93
|
+
out[1] = values[at + 1]!
|
|
94
|
+
out[2] = values[at + 2]!
|
|
95
|
+
out[3] = values[at + 3]!
|
|
96
|
+
}
|