@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,150 @@
|
|
|
1
|
+
// The surface maps on lit(), all procedural so the example is
|
|
2
|
+
// self-contained. normalMap bends the lighting per texel with NO tangent
|
|
3
|
+
// channel (the frame comes from screen-space derivatives) - the flat
|
|
4
|
+
// plane and the sphere both show relief from one bump texture.
|
|
5
|
+
// emissive/emissiveMap add light the lights do not provide (the window
|
|
6
|
+
// grid stays lit on the dark side). specularMap masks the highlight
|
|
7
|
+
// (the striped sphere is chrome on the bright bands, rubber between).
|
|
8
|
+
// mapTransform scrolls the belt's uv per frame with one setMeshParams
|
|
9
|
+
// write - per MATERIAL, Godot's uv1_offset/scale, not Three's
|
|
10
|
+
// per-texture transform. lightMap adds a baked glow by the aUV2 channel
|
|
11
|
+
// (withAttribute) at zero light cost.
|
|
12
|
+
|
|
13
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
14
|
+
import { createTexture } from "@solidrt/core/gpu"
|
|
15
|
+
import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, setMeshParams, sphere, withAttribute } from "@solidrt/3d"
|
|
16
|
+
import type { MeshNode } from "@solidrt/3d"
|
|
17
|
+
|
|
18
|
+
// A tangent-space normal map from a procedural height field (a grid of
|
|
19
|
+
// round bumps): finite-difference the height, pack the normal as
|
|
20
|
+
// OpenGL-style rgb. What every baked normal map contains, minus the
|
|
21
|
+
// authoring tool.
|
|
22
|
+
function bumpNormalMap() {
|
|
23
|
+
let n = 128
|
|
24
|
+
const BUMPS = 4 // bump grid cells per texture edge
|
|
25
|
+
let height = (x: number, y: number): number => {
|
|
26
|
+
let u = ((x * BUMPS) / n) % 1
|
|
27
|
+
let v = ((y * BUMPS) / n) % 1
|
|
28
|
+
let dx = u - 0.5
|
|
29
|
+
let dy = v - 0.5
|
|
30
|
+
let d = Math.sqrt(dx * dx + dy * dy)
|
|
31
|
+
return d < 0.35 ? Math.cos((d / 0.35) * Math.PI * 0.5) : 0
|
|
32
|
+
}
|
|
33
|
+
const STRENGTH = 3 // height units per texel step: how steep the bumps read
|
|
34
|
+
let data = new Uint8Array(n * n * 4)
|
|
35
|
+
for (let y = 0; y < n; y++) {
|
|
36
|
+
for (let x = 0; x < n; x++) {
|
|
37
|
+
let sx = (height(x + 1, y) - height(x - 1, y)) * STRENGTH
|
|
38
|
+
let sy = (height(x, y + 1) - height(x, y - 1)) * STRENGTH
|
|
39
|
+
let inv = 1 / Math.sqrt(sx * sx + sy * sy + 1)
|
|
40
|
+
data.set([((-sx * inv) * 0.5 + 0.5) * 255, ((-sy * inv) * 0.5 + 0.5) * 255, (inv * 0.5 + 0.5) * 255, 255], (y * n + x) * 4)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return createTexture(data, n, n, { wrap: "repeat", mipmap: true })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A window grid: dark wall, warm lit windows - the emissive map.
|
|
47
|
+
function windowMap() {
|
|
48
|
+
let n = 64
|
|
49
|
+
let data = new Uint8Array(n * n * 4)
|
|
50
|
+
for (let y = 0; y < n; y++) {
|
|
51
|
+
for (let x = 0; x < n; x++) {
|
|
52
|
+
let on = (x >> 3) % 2 === 1 && (y >> 3) % 2 === 1 && (x & 7) > 1 && (x & 7) < 6 && (y & 7) > 1 && (y & 7) < 6
|
|
53
|
+
data.set(on ? [255, 190, 90, 255] : [0, 0, 0, 255], (y * n + x) * 4)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return createTexture(data, n, n, { format: "rgba8-srgb", mipmap: true })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Vertical stripes in the red channel: the specular mask.
|
|
60
|
+
function stripeMap() {
|
|
61
|
+
let n = 64
|
|
62
|
+
let data = new Uint8Array(n * n * 4)
|
|
63
|
+
for (let y = 0; y < n; y++) {
|
|
64
|
+
for (let x = 0; x < n; x++) {
|
|
65
|
+
let v = (x >> 3) & 1 ? 255 : 0
|
|
66
|
+
data.set([v, v, v, 255], (y * n + x) * 4)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return createTexture(data, n, n, { wrap: "repeat", mipmap: true })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Chevrons for the scrolling belt.
|
|
73
|
+
function chevronMap() {
|
|
74
|
+
let n = 64
|
|
75
|
+
let data = new Uint8Array(n * n * 4)
|
|
76
|
+
for (let y = 0; y < n; y++) {
|
|
77
|
+
for (let x = 0; x < n; x++) {
|
|
78
|
+
let on = ((x + Math.abs(y - n / 2)) >> 3) & 1
|
|
79
|
+
data.set(on ? [235, 220, 60, 255] : [40, 40, 45, 255], (y * n + x) * 4)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return createTexture(data, n, n, { format: "rgba8-srgb", wrap: "repeat", mipmap: true })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A radial warm glow, the stand-in for an offline lightmap bake.
|
|
86
|
+
function bakedGlowMap() {
|
|
87
|
+
let n = 64
|
|
88
|
+
let data = new Uint8Array(n * n * 4)
|
|
89
|
+
for (let y = 0; y < n; y++) {
|
|
90
|
+
for (let x = 0; x < n; x++) {
|
|
91
|
+
let dx = x / n - 0.5
|
|
92
|
+
let dy = y / n - 0.5
|
|
93
|
+
let g = Math.max(0, 1 - Math.sqrt(dx * dx + dy * dy) * 2.2)
|
|
94
|
+
data.set([255 * g, 180 * g, 90 * g, 255], (y * n + x) * 4)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return createTexture(data, n, n, { mipmap: true })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function App() {
|
|
101
|
+
let [t, setT] = createSignal(0)
|
|
102
|
+
let beltMesh: MeshNode | undefined
|
|
103
|
+
onFrame(tick => {
|
|
104
|
+
setT(tick / 1000)
|
|
105
|
+
// The scrolling belt: one per-frame uniform write, the setTransform
|
|
106
|
+
// split - structure through signals, frame-rate values through refs.
|
|
107
|
+
if (beltMesh !== undefined) setMeshParams(beltMesh, { uMapTransform: [3, 1, tick / 1500, 0] })
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
let bumps = bumpNormalMap()
|
|
111
|
+
let bumpy = lit({ color: [0.75, 0.5, 0.3], normalMap: bumps, specular: 0.5, shininess: 40 })
|
|
112
|
+
let flat = lit({ color: [0.75, 0.5, 0.3], specular: 0.5, shininess: 40 })
|
|
113
|
+
let city = lit({ color: [0.25, 0.28, 0.34], emissiveMap: windowMap() })
|
|
114
|
+
let striped = lit({ color: [0.2, 0.22, 0.26], specularMap: stripeMap(), shininess: 90 })
|
|
115
|
+
let belt = lit({ map: chevronMap(), mapTransform: { repeat: [3, 1] } })
|
|
116
|
+
// The ground carries its own uv2 island (here just the 0..1 plane UVs
|
|
117
|
+
// recomputed from position) and adds the "baked" glow with no light.
|
|
118
|
+
let groundGeometry = withAttribute(plane({ width: 8, height: 8 }), { name: "aUV2", format: "vec2" }, (_i, pos) => [
|
|
119
|
+
pos[0] / 8 + 0.5,
|
|
120
|
+
pos[1] / 8 + 0.5,
|
|
121
|
+
])
|
|
122
|
+
let ground = lit({ color: [0.45, 0.45, 0.5], lightMap: bakedGlowMap(), lightMapIntensity: 1.5 })
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<window>
|
|
126
|
+
<view width={pct(100)} height={pct(100)}>
|
|
127
|
+
<Scene clearColor={[0.06, 0.06, 0.09, 1]} samples={4} label="materials">
|
|
128
|
+
<PerspectiveCamera fov={50} position={[0, 2.8, 5.2]} lookAt={[0, 0.5, 0]} />
|
|
129
|
+
<HemisphereLight sky={[0.3, 0.33, 0.4]} ground={[0.1, 0.09, 0.08]} />
|
|
130
|
+
<Group rotation={[0, t() / 2, 0]}>
|
|
131
|
+
<DirectionalLight direction={[0.7, -0.8, 0.2]} color={[1, 0.95, 0.85]} intensity={1} />
|
|
132
|
+
</Group>
|
|
133
|
+
<Mesh geometry={groundGeometry} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
|
|
134
|
+
<Mesh geometry={sphere({ radius: 0.55 })} material={bumpy} position={[-1.7, 0.55, 0.6]} />
|
|
135
|
+
<Mesh geometry={sphere({ radius: 0.55 })} material={flat} position={[-1.7, 0.55, -0.9]} />
|
|
136
|
+
<Mesh geometry={box({ width: 1, height: 1.6, depth: 1 })} material={city} position={[0, 0.8, -0.6]} rotation={[0, t() / 3, 0]} />
|
|
137
|
+
<Mesh geometry={sphere({ radius: 0.55 })} material={striped} position={[1.7, 0.55, -0.6]} rotation={[0, t() / 2, 0]} />
|
|
138
|
+
<Mesh
|
|
139
|
+
geometry={box({ width: 2.2, height: 0.12, depth: 0.7 })}
|
|
140
|
+
material={belt}
|
|
141
|
+
position={[0.6, 0.06, 1.6]}
|
|
142
|
+
ref={mesh => (beltMesh = mesh)}
|
|
143
|
+
/>
|
|
144
|
+
</Scene>
|
|
145
|
+
</view>
|
|
146
|
+
</window>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
render(() => <App />)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Async model loading: loadModel + <Loading>. The same rover as
|
|
2
|
+
// model.tsx, but read from assets/ at runtime instead of riding in the
|
|
3
|
+
// bundle - the shape every app with real content ends up needing. Bake
|
|
4
|
+
// the asset once and drop it in your app's assets tree:
|
|
5
|
+
//
|
|
6
|
+
// bunx srt tool 3d/model examples/model.glb -o assets/model.srtm
|
|
7
|
+
//
|
|
8
|
+
// (loadGltf("assets/model.glb") has the exact same async shape; the bake
|
|
9
|
+
// just removes the runtime parse.)
|
|
10
|
+
//
|
|
11
|
+
// TWO rules make the async read work, both enforced by the runtime:
|
|
12
|
+
//
|
|
13
|
+
// 1. Read the async value only inside a TRACKING scope whose result JSX
|
|
14
|
+
// reads back - the memo pattern below. Reading it straight in the
|
|
15
|
+
// component body (`let model = loaded()`) throws
|
|
16
|
+
// PENDING_ASYNC_UNTRACKED_READ: outside tracking there is nothing to
|
|
17
|
+
// suspend and retry.
|
|
18
|
+
// 2. The component under <Loading> creates NO elements of its own before
|
|
19
|
+
// the suspending read. An element built ahead of a suspend is orphaned
|
|
20
|
+
// when the boundary discards and retries the subtree, and orphaned
|
|
21
|
+
// subtrees are never freed (the dev leak sentinel reports them). So
|
|
22
|
+
// the window/view shell lives in App ABOVE the boundary, and Rover
|
|
23
|
+
// suspends at `loaded()` before building any JSX.
|
|
24
|
+
|
|
25
|
+
import { createMemo, createSignal, Loading, onFrame, pct, render } from "@solidrt/core"
|
|
26
|
+
import { createTexture } from "@solidrt/core/gpu"
|
|
27
|
+
import type { TextureId } from "@solidrt/core/gpu"
|
|
28
|
+
import { add, DirectionalLight, equirectToCube, Group, loadModel, PerspectiveCamera, Scene } from "@solidrt/3d"
|
|
29
|
+
import type { Model, SceneNode } from "@solidrt/3d"
|
|
30
|
+
|
|
31
|
+
// The same gradient environment as model.tsx: the default `standard`
|
|
32
|
+
// materials want one (a baked HDRI in a real app, see environment.tsx).
|
|
33
|
+
const ENV_FACE = 16
|
|
34
|
+
const ENV_ROWS: [number, number, number][] = [
|
|
35
|
+
[96, 122, 168],
|
|
36
|
+
[176, 190, 210],
|
|
37
|
+
[120, 105, 90],
|
|
38
|
+
[60, 52, 45],
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
function gradientEnvironment(): TextureId {
|
|
42
|
+
let px = new Uint8Array(2 * ENV_ROWS.length * 4)
|
|
43
|
+
ENV_ROWS.forEach((c, y) => {
|
|
44
|
+
for (let x = 0; x < 2; x++) px.set([c[0], c[1], c[2], 255], (y * 2 + x) * 4)
|
|
45
|
+
})
|
|
46
|
+
let panorama = createTexture(px, 2, ENV_ROWS.length, { format: "rgba8-srgb", filter: "linear" })
|
|
47
|
+
return equirectToCube(panorama, ENV_FACE, { format: "rgba8-srgb", mipmap: true, label: "gradient-env" })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Rover(props: { turn: () => number }) {
|
|
51
|
+
// The async value: a memo returning a Promise. Its read below suspends
|
|
52
|
+
// until the file is read and the model built, then resumes under the
|
|
53
|
+
// <Loading> boundary in App.
|
|
54
|
+
let loaded = createMemo(() => loadModel("assets/model.srtm", { label: "rover" }))
|
|
55
|
+
|
|
56
|
+
// Everything derived from the model - framing, mounting - happens in
|
|
57
|
+
// this memo, AFTER the suspending read, so nothing here runs or builds
|
|
58
|
+
// until the model exists. The memo returns the scene JSX; the component
|
|
59
|
+
// returns the memo read and nothing else.
|
|
60
|
+
let scene = createMemo(() => {
|
|
61
|
+
let model: Model = loaded()
|
|
62
|
+
let cube = gradientEnvironment()
|
|
63
|
+
let b = model.bounds
|
|
64
|
+
let center: [number, number, number] = [(b[0]! + b[3]!) / 2, (b[1]! + b[4]!) / 2, (b[2]! + b[5]!) / 2]
|
|
65
|
+
let radius = Math.hypot(b[3]! - b[0]!, b[4]! - b[1]!, b[5]! - b[2]!) / 2
|
|
66
|
+
return (
|
|
67
|
+
<Scene clearColor={[0.1, 0.11, 0.14, 1]} environment={{ cube }} samples={4} label="model-load">
|
|
68
|
+
<PerspectiveCamera fov={40} position={[center[0] + radius * 1.4, center[1] + radius * 1.1, center[2] + radius * 2.2]} lookAt={center} />
|
|
69
|
+
<DirectionalLight direction={[0.5, -0.8, 0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
|
|
70
|
+
<Group rotation={[0, props.turn(), 0]} ref={(g: SceneNode) => add(g, model)} />
|
|
71
|
+
</Scene>
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return <>{scene()}</>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function App() {
|
|
79
|
+
let [t, setT] = createSignal(0)
|
|
80
|
+
onFrame(tick => setT(tick / 1000))
|
|
81
|
+
|
|
82
|
+
// The shell above the boundary; the fallback is plain UI and may be
|
|
83
|
+
// anything - it is created and torn down by <Loading> itself.
|
|
84
|
+
return (
|
|
85
|
+
<window>
|
|
86
|
+
<view width={pct(100)} height={pct(100)}>
|
|
87
|
+
<Loading fallback={<text fontSize={24} color="#8899aa">Loading rover...</text>}>
|
|
88
|
+
<Rover turn={() => t() / 3} />
|
|
89
|
+
</Loading>
|
|
90
|
+
</view>
|
|
91
|
+
</window>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
render(() => <App />)
|
package/examples/model.glb
CHANGED
|
Binary file
|
package/examples/model.tsx
CHANGED
|
@@ -6,16 +6,43 @@
|
|
|
6
6
|
// through loadGltf (same parser, read with flux:fs) or, baked once with
|
|
7
7
|
// `srt tool 3d/model`, through loadModel (no parsing at all). Every part
|
|
8
8
|
// keeps its node name and is an ordinary mesh: pointer events, transforms
|
|
9
|
-
// and materials work per part.
|
|
9
|
+
// and materials work per part. The materials are createModel's default
|
|
10
|
+
// `standard` from the file's PBR factors, so the scene carries an
|
|
11
|
+
// environment: the steel reflects it and every part takes its diffuse
|
|
12
|
+
// ambient from it (a metal with nothing to reflect renders near black) -
|
|
13
|
+
// here a sky-to-ground gradient built at startup, in an app usually a
|
|
14
|
+
// baked HDRI (see environment.tsx).
|
|
10
15
|
|
|
11
16
|
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
12
|
-
import {
|
|
17
|
+
import { createTexture } from "@solidrt/core/gpu"
|
|
18
|
+
import type { TextureId } from "@solidrt/core/gpu"
|
|
19
|
+
import { add, createModel, DirectionalLight, equirectToCube, Group, parseGltf, PerspectiveCamera, Scene, setVisible } from "@solidrt/3d"
|
|
13
20
|
import type { SceneNode } from "@solidrt/3d"
|
|
14
21
|
import modelBytes from "./model.glb" with { type: "binary" }
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
// Face edge of the gradient environment cube: a gradient needs no detail.
|
|
24
|
+
const ENV_FACE = 16
|
|
25
|
+
// The gradient, zenith to nadir, sRGB bytes: sky, horizon, ground, below.
|
|
26
|
+
const ENV_ROWS: [number, number, number][] = [
|
|
27
|
+
[96, 122, 168],
|
|
28
|
+
[176, 190, 210],
|
|
29
|
+
[120, 105, 90],
|
|
30
|
+
[60, 52, 45],
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
// A tiny equirectangular panorama (2 columns, one row per band, linear
|
|
34
|
+
// filtering between) turned into the cube the scene lights by.
|
|
35
|
+
function gradientEnvironment(): TextureId {
|
|
36
|
+
let px = new Uint8Array(2 * ENV_ROWS.length * 4)
|
|
37
|
+
ENV_ROWS.forEach((c, y) => {
|
|
38
|
+
for (let x = 0; x < 2; x++) px.set([c[0], c[1], c[2], 255], (y * 2 + x) * 4)
|
|
39
|
+
})
|
|
40
|
+
let panorama = createTexture(px, 2, ENV_ROWS.length, { format: "rgba8-srgb", filter: "linear" })
|
|
41
|
+
return equirectToCube(panorama, ENV_FACE, { format: "rgba8-srgb", mipmap: true, label: "gradient-env" })
|
|
42
|
+
}
|
|
17
43
|
|
|
18
44
|
function App() {
|
|
45
|
+
let cube = gradientEnvironment()
|
|
19
46
|
let [t, setT] = createSignal(0)
|
|
20
47
|
onFrame(tick => setT(tick / 1000))
|
|
21
48
|
|
|
@@ -36,10 +63,9 @@ function App() {
|
|
|
36
63
|
|
|
37
64
|
return (
|
|
38
65
|
<window>
|
|
39
|
-
<view width={pct(100)} height={pct(100)}
|
|
40
|
-
<Scene
|
|
66
|
+
<view width={pct(100)} height={pct(100)}>
|
|
67
|
+
<Scene clearColor={[0.1, 0.11, 0.14, 1]} environment={{ cube }} samples={4} label="model">
|
|
41
68
|
<PerspectiveCamera fov={40} position={[center[0] + radius * 1.4, center[1] + radius * 1.1, center[2] + radius * 2.2]} lookAt={center} />
|
|
42
|
-
<HemisphereLight sky={[0.45, 0.5, 0.6]} ground={[0.2, 0.17, 0.14]} />
|
|
43
69
|
<DirectionalLight direction={[0.5, -0.8, 0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
|
|
44
70
|
<Group rotation={[0, t() / 3, 0]} ref={(g: SceneNode) => add(g, model)} />
|
|
45
71
|
</Scene>
|
package/examples/pick.tsx
CHANGED
|
@@ -14,8 +14,6 @@ import { createSignal, pct, render } from "@solidrt/core"
|
|
|
14
14
|
import { box, cone, Group, Mesh, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit } from "@solidrt/3d"
|
|
15
15
|
import type { Geometry, ScenePointerEvent, Vec3 } from "@solidrt/3d"
|
|
16
16
|
|
|
17
|
-
const SIZE = 720
|
|
18
|
-
|
|
19
17
|
type Color = [number, number, number]
|
|
20
18
|
|
|
21
19
|
let [hovered, setHovered] = createSignal("nothing")
|
|
@@ -70,8 +68,8 @@ function App() {
|
|
|
70
68
|
|
|
71
69
|
return (
|
|
72
70
|
<window>
|
|
73
|
-
<view width={pct(100)} height={pct(100)}
|
|
74
|
-
<Scene
|
|
71
|
+
<view width={pct(100)} height={pct(100)}>
|
|
72
|
+
<Scene clearColor={[0.07, 0.07, 0.1, 1]} label="pick">
|
|
75
73
|
<PerspectiveCamera fov={55} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
|
|
76
74
|
<Mesh geometry={floor} material={unlit({ color: [0.15, 0.16, 0.2] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
77
75
|
{/* The group hears every child click that is not stopped. */}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// A reflection probe: `scene.createReflectionProbe` renders the scene into
|
|
2
|
+
// a cube map from a point, and that cube is the environment a chrome ball
|
|
3
|
+
// at the same point mirrors - Three's CubeCamera, Unity's and Godot's
|
|
4
|
+
// realtime ReflectionProbe. Six colored walls make a room, four glowing
|
|
5
|
+
// spheres orbit the ball, and the probe re-renders every frame (six scene
|
|
6
|
+
// passes) so the orbit shows in the chrome. Layers keep the ball out of
|
|
7
|
+
// its own probe: the room and the orbiters live on layers 1 and 2, the
|
|
8
|
+
// ball on layer 1 only, and the probe looks at layer 2. Each update also
|
|
9
|
+
// prefilters the faces into the roughness chain (the default), so the
|
|
10
|
+
// ball's satin finish blurs the room the way a baked environment would;
|
|
11
|
+
// `prefilter: false` would keep a sharp mirror at a sixth of the passes
|
|
12
|
+
// (the chain is the frame's largest GPU item; see AGENTS.md). Drag to
|
|
13
|
+
// look around.
|
|
14
|
+
|
|
15
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
16
|
+
import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, plane, Scene, sphere, standard, unlit } from "@solidrt/3d"
|
|
17
|
+
import type { ReflectionProbe, SceneHandle } from "@solidrt/3d"
|
|
18
|
+
import type { Vec3 } from "@solidrt/3d/math"
|
|
19
|
+
|
|
20
|
+
// Half the room's edge.
|
|
21
|
+
const ROOM = 5
|
|
22
|
+
// The orbiters' radius, orbit radius and speed (radians per second).
|
|
23
|
+
const ORBITER_RADIUS = 0.35
|
|
24
|
+
const ORBIT = 2
|
|
25
|
+
const ORBIT_SPEED = 0.8
|
|
26
|
+
// The chrome ball's radius and its satin roughness (0 is a mirror).
|
|
27
|
+
const BALL = 1
|
|
28
|
+
const BALL_ROUGHNESS = 0.2
|
|
29
|
+
// The probe's face edge.
|
|
30
|
+
const PROBE_SIZE = 128
|
|
31
|
+
// Layer bits: the scene camera sees both, the probe only ROOM_LAYER.
|
|
32
|
+
const BALL_LAYER = 1
|
|
33
|
+
const ROOM_LAYER = 2
|
|
34
|
+
|
|
35
|
+
// Wall colors (sRGB), one per face: +X, -X, +Y, -Y, +Z, -Z.
|
|
36
|
+
const WALL_COLORS: [number, number, number][] = [
|
|
37
|
+
[0.75, 0.3, 0.25],
|
|
38
|
+
[0.25, 0.6, 0.35],
|
|
39
|
+
[0.85, 0.85, 0.9],
|
|
40
|
+
[0.3, 0.28, 0.26],
|
|
41
|
+
[0.25, 0.4, 0.75],
|
|
42
|
+
[0.8, 0.7, 0.3],
|
|
43
|
+
]
|
|
44
|
+
// Each wall's position and the rotation that turns the plane's front inward.
|
|
45
|
+
const WALLS: { position: Vec3; rotation: Vec3 }[] = [
|
|
46
|
+
{ position: [ROOM, 0, 0], rotation: [0, -Math.PI / 2, 0] },
|
|
47
|
+
{ position: [-ROOM, 0, 0], rotation: [0, Math.PI / 2, 0] },
|
|
48
|
+
{ position: [0, ROOM, 0], rotation: [Math.PI / 2, 0, 0] },
|
|
49
|
+
{ position: [0, -ROOM, 0], rotation: [-Math.PI / 2, 0, 0] },
|
|
50
|
+
{ position: [0, 0, ROOM], rotation: [0, Math.PI, 0] },
|
|
51
|
+
{ position: [0, 0, -ROOM], rotation: [0, 0, 0] },
|
|
52
|
+
]
|
|
53
|
+
const ORBITER_COLORS: [number, number, number][] = [
|
|
54
|
+
[1, 0.4, 0.2],
|
|
55
|
+
[0.3, 0.9, 1],
|
|
56
|
+
[1, 0.9, 0.3],
|
|
57
|
+
[0.7, 0.4, 1],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
function App() {
|
|
61
|
+
let [t, setT] = createSignal(0)
|
|
62
|
+
let scene: SceneHandle | undefined
|
|
63
|
+
let probe: ReflectionProbe | undefined
|
|
64
|
+
onFrame(tick => {
|
|
65
|
+
setT(tick / 1000)
|
|
66
|
+
if (scene !== undefined && probe === undefined) {
|
|
67
|
+
probe = scene.createReflectionProbe({ position: [0, 0, 0], size: PROBE_SIZE, layers: ROOM_LAYER, label: "ball-probe" })
|
|
68
|
+
scene.setEnvironment({ cube: probe.cube })
|
|
69
|
+
}
|
|
70
|
+
probe?.update()
|
|
71
|
+
})
|
|
72
|
+
let wall = plane({ width: ROOM * 2, height: ROOM * 2 })
|
|
73
|
+
let walls = WALL_COLORS.map(color => unlit({ color }))
|
|
74
|
+
let orbiter = sphere({ radius: ORBITER_RADIUS, widthSegments: 24, heightSegments: 16 })
|
|
75
|
+
let orbiters = ORBITER_COLORS.map(color => unlit({ color }))
|
|
76
|
+
let ball = standard({ color: [1, 1, 1], metalness: 1, roughness: BALL_ROUGHNESS })
|
|
77
|
+
let orbitPosition = (i: number): Vec3 => {
|
|
78
|
+
let a = t() * ORBIT_SPEED + (i * Math.PI) / 2
|
|
79
|
+
return [Math.cos(a) * ORBIT, Math.sin(a * 1.7) * 0.8, Math.sin(a) * ORBIT]
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<window>
|
|
83
|
+
<view width={pct(100)} height={pct(100)}>
|
|
84
|
+
<Scene clearColor={[0.05, 0.05, 0.06, 1]} samples={4} label="probe-demo" ref={s => (scene = s)}>
|
|
85
|
+
<OrbitCamera target={[0, 0, 0]} azimuth={0.5} elevation={0.2} distance={4} />
|
|
86
|
+
<HemisphereLight sky={[0.6, 0.65, 0.7]} ground={[0.3, 0.28, 0.26]} intensity={0.6} />
|
|
87
|
+
<DirectionalLight direction={[0.4, -0.8, 0.3]} intensity={0.8} />
|
|
88
|
+
{WALLS.map((w, i) => (
|
|
89
|
+
<Mesh geometry={wall} material={walls[i]!} position={w.position} rotation={w.rotation} layers={BALL_LAYER | ROOM_LAYER} />
|
|
90
|
+
))}
|
|
91
|
+
{ORBITER_COLORS.map((_, i) => (
|
|
92
|
+
<Mesh geometry={orbiter} material={orbiters[i]!} position={orbitPosition(i)} layers={BALL_LAYER | ROOM_LAYER} />
|
|
93
|
+
))}
|
|
94
|
+
<Mesh geometry={sphere({ radius: BALL, widthSegments: 64, heightSegments: 48 })} material={ball} layers={BALL_LAYER} />
|
|
95
|
+
</Scene>
|
|
96
|
+
</view>
|
|
97
|
+
</window>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
render(() => <App />)
|
|
@@ -10,8 +10,6 @@ import { pct, render } from "@solidrt/core"
|
|
|
10
10
|
import { glsl } from "@solidrt/core/gpu"
|
|
11
11
|
import { box, Mesh, PerspectiveCamera, Scene, sphere, torusKnot, unlit } from "@solidrt/3d"
|
|
12
12
|
|
|
13
|
-
const SIZE = 720
|
|
14
|
-
|
|
15
13
|
// A radial night-sky gradient with hash grain so the ramp does not band.
|
|
16
14
|
let BACKDROP = glsl`
|
|
17
15
|
void main() {
|
|
@@ -28,8 +26,8 @@ let BACKDROP = glsl`
|
|
|
28
26
|
function App() {
|
|
29
27
|
return (
|
|
30
28
|
<window>
|
|
31
|
-
<view width={pct(100)} height={pct(100)}
|
|
32
|
-
<Scene
|
|
29
|
+
<view width={pct(100)} height={pct(100)}>
|
|
30
|
+
<Scene background={BACKDROP} label="backdrop-demo">
|
|
33
31
|
<PerspectiveCamera fov={55} position={[0, 1.8, 4.4]} lookAt={[0, 0.4, 0]} />
|
|
34
32
|
<Mesh geometry={torusKnot({ radius: 0.7, tube: 0.2, tubularSegments: 128, radialSegments: 16 })} material={unlit({ color: [0.85, 0.55, 0.25] })} position={[0, 0.9, 0]} />
|
|
35
33
|
<Mesh geometry={box()} material={unlit({ color: [0.3, 0.5, 0.8] })} position={[-1.5, 0.4, -0.5]} scale={0.8} />
|
package/examples/scene-basic.tsx
CHANGED
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
12
12
|
import { box, Group, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
13
13
|
|
|
14
|
-
const SIZE = 720
|
|
15
|
-
|
|
16
14
|
function App() {
|
|
17
15
|
let [spin, setSpin] = createSignal(0)
|
|
18
16
|
onFrame(tick => setSpin(tick / 2000))
|
|
@@ -26,8 +24,8 @@ function App() {
|
|
|
26
24
|
|
|
27
25
|
return (
|
|
28
26
|
<window>
|
|
29
|
-
<view width={pct(100)} height={pct(100)}
|
|
30
|
-
<Scene
|
|
27
|
+
<view width={pct(100)} height={pct(100)}>
|
|
28
|
+
<Scene clearColor={[0.07, 0.07, 0.1, 1]} label="scene-basic">
|
|
31
29
|
<PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
|
|
32
30
|
<Mesh
|
|
33
31
|
geometry={floor}
|
package/examples/scene-views.tsx
CHANGED
|
@@ -6,10 +6,20 @@
|
|
|
6
6
|
// at once, so the extra views cost the app nothing per frame (and the
|
|
7
7
|
// light set fans out to them too - the map is lit like the main view).
|
|
8
8
|
// Views die with the scene.
|
|
9
|
+
//
|
|
10
|
+
// Layers and per-view fog: the marker disc is on layer bit 2, so the
|
|
11
|
+
// main render (mask 1) and the silhouette never draw it - only the map,
|
|
12
|
+
// whose mask admits both bits. The scene is fogged; `fog: null` on the
|
|
13
|
+
// map keeps the top-down view clear (its fog names are view-owned, so
|
|
14
|
+
// scene-wide fog writes never clobber them).
|
|
9
15
|
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
10
|
-
import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
16
|
+
import { box, circle, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
11
17
|
import type { SceneHandle } from "@solidrt/3d"
|
|
12
18
|
|
|
19
|
+
// Layer bits: the world everything defaults to, and the map-only markers.
|
|
20
|
+
const WORLD_LAYER = 1
|
|
21
|
+
const MARKER_LAYER = 2
|
|
22
|
+
|
|
13
23
|
const MAIN = 720
|
|
14
24
|
const SIDE = 360
|
|
15
25
|
|
|
@@ -20,6 +30,7 @@ function App() {
|
|
|
20
30
|
let cube = box()
|
|
21
31
|
let floor = plane({ width: 6, height: 6, label: "floor" })
|
|
22
32
|
let ball = sphere({ radius: 0.35 })
|
|
33
|
+
let marker = circle({ radius: 0.3, label: "marker" })
|
|
23
34
|
// `ref` runs before `output`, so the handle is set when the views are
|
|
24
35
|
// created there; both callbacks run once, untracked - no signals needed.
|
|
25
36
|
let scene!: SceneHandle
|
|
@@ -31,12 +42,22 @@ function App() {
|
|
|
31
42
|
width={MAIN}
|
|
32
43
|
height={MAIN}
|
|
33
44
|
clearColor={[0.07, 0.07, 0.1, 1]}
|
|
45
|
+
fog={{ color: [0.07, 0.07, 0.1], near: 2.5, far: 9 }}
|
|
34
46
|
label="scene-views"
|
|
35
47
|
ref={s => (scene = s)}
|
|
36
48
|
output={tex => {
|
|
37
49
|
// The map: straight down from y = 10, world -z toward the top of
|
|
38
|
-
// the leaf, seven world units across at any depth.
|
|
39
|
-
|
|
50
|
+
// the leaf, seven world units across at any depth. Its mask also
|
|
51
|
+
// admits the marker layer, and `fog: null` keeps it clear while
|
|
52
|
+
// the main view fades to the horizon.
|
|
53
|
+
let map = scene.createView({
|
|
54
|
+
width: SIDE,
|
|
55
|
+
height: SIDE,
|
|
56
|
+
clearColor: [0.05, 0.08, 0.06, 1],
|
|
57
|
+
layers: WORLD_LAYER | MARKER_LAYER,
|
|
58
|
+
fog: null,
|
|
59
|
+
label: "map",
|
|
60
|
+
})
|
|
40
61
|
map.setCamera({
|
|
41
62
|
position: [0, 10, 0],
|
|
42
63
|
target: [0, 0, 0],
|
|
@@ -85,6 +106,14 @@ function App() {
|
|
|
85
106
|
scale={[0.5, 1.4, 0.5]}
|
|
86
107
|
/>
|
|
87
108
|
<Mesh geometry={ball} material={lit({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
|
|
109
|
+
{/* The ball's map marker: drawn by the map view alone. */}
|
|
110
|
+
<Mesh
|
|
111
|
+
geometry={marker}
|
|
112
|
+
material={unlit({ color: [0.95, 0.25, 0.2] })}
|
|
113
|
+
layers={MARKER_LAYER}
|
|
114
|
+
position={[1.1, 2, 0]}
|
|
115
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
116
|
+
/>
|
|
88
117
|
</Group>
|
|
89
118
|
</Scene>
|
|
90
119
|
</view>
|
package/examples/shadows.tsx
CHANGED
|
@@ -21,8 +21,6 @@ import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
|
21
21
|
import { box, cylinder, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, setTransform, sphere, torusKnot } from "@solidrt/3d"
|
|
22
22
|
import type { DirectionalLightNode } from "@solidrt/3d"
|
|
23
23
|
|
|
24
|
-
const SIZE = 720
|
|
25
|
-
|
|
26
24
|
function App() {
|
|
27
25
|
let [t, setT] = createSignal(0)
|
|
28
26
|
let sun!: DirectionalLightNode
|
|
@@ -43,8 +41,8 @@ function App() {
|
|
|
43
41
|
|
|
44
42
|
return (
|
|
45
43
|
<window>
|
|
46
|
-
<view width={pct(100)} height={pct(100)}
|
|
47
|
-
<Scene
|
|
44
|
+
<view width={pct(100)} height={pct(100)}>
|
|
45
|
+
<Scene clearColor={[0.55, 0.65, 0.8, 1]} samples={4} label="shadows">
|
|
48
46
|
<PerspectiveCamera fov={45} position={[0, 4, 7]} lookAt={[0, 0.5, 0]} />
|
|
49
47
|
<HemisphereLight sky={[0.45, 0.5, 0.6]} ground={[0.2, 0.18, 0.15]} />
|
|
50
48
|
<DirectionalLight
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// A sky-lit scene: the background is a procedural GLSL sky (a horizon
|
|
2
|
+
// gradient, a sun disc and its glow, written along the view ray), and
|
|
3
|
+
// `scene.bakeBackground()` renders that same fragment into a cube and
|
|
4
|
+
// prefilters it into the roughness chain the scene's environment samples -
|
|
5
|
+
// Godot's sky-to-radiance bake: the sky both draws behind the meshes and
|
|
6
|
+
// lights them, with no light nodes at all. A row of `standard` spheres,
|
|
7
|
+
// metal above, dielectric below, roughness 0 to 1 across: the sharp ones
|
|
8
|
+
// mirror the sun and horizon, the rough ones catch the sky's soft
|
|
9
|
+
// gradient. The sky is a scene param (uSunDir), so the bake sees the same
|
|
10
|
+
// sun the backdrop draws. Drag to look around.
|
|
11
|
+
|
|
12
|
+
import { pct, render } from "@solidrt/core"
|
|
13
|
+
import { glsl } from "@solidrt/core/gpu"
|
|
14
|
+
import { Mesh, OrbitCamera, Scene, sphere, standard } from "@solidrt/3d"
|
|
15
|
+
import type { SceneHandle } from "@solidrt/3d"
|
|
16
|
+
import { normalize } from "@solidrt/3d/math"
|
|
17
|
+
import type { Vec3 } from "@solidrt/3d/math"
|
|
18
|
+
|
|
19
|
+
// Spheres per row, their radius and spacing.
|
|
20
|
+
const COUNT = 6
|
|
21
|
+
const RADIUS = 0.45
|
|
22
|
+
const GAP = 1.15
|
|
23
|
+
// The sun's direction: high and on the camera's side, so its reflection
|
|
24
|
+
// lands on the spheres' upper right - a sharp disc on the mirror, a broad
|
|
25
|
+
// bright highlight on the rough metals (the HDR bake keeps the disc's
|
|
26
|
+
// energy, which an 8-bit probe would clamp to white).
|
|
27
|
+
const SUN: Vec3 = normalize([0.6, 0.5, 0.6])
|
|
28
|
+
// The bake's face edge: 128 is the environment default; the sun disc
|
|
29
|
+
// blurs into its glow at this size, which is what a rough surface wants.
|
|
30
|
+
const BAKE_SIZE = 128
|
|
31
|
+
|
|
32
|
+
// Horizon gradient (zenith blue to a pale horizon, a dark ground), a sun
|
|
33
|
+
// disc with a glow. Linear colors in, the output stage encodes for the
|
|
34
|
+
// screen and leaves them linear for the bake.
|
|
35
|
+
const SKY = glsl`
|
|
36
|
+
uniform vec3 uSunDir;
|
|
37
|
+
// Cosine of the disc's angular radius (about 2 degrees), the glow's
|
|
38
|
+
// falloff exponent and the disc's radiance (well above 1: a low-key
|
|
39
|
+
// sun, real ones are far brighter still).
|
|
40
|
+
const float SUN_DISC = 0.9994;
|
|
41
|
+
const float SUN_GLOW = 24.0;
|
|
42
|
+
const float SUN_RADIANCE = 40.0;
|
|
43
|
+
void main() {
|
|
44
|
+
vec3 d = normalize(vRay);
|
|
45
|
+
vec3 zenith = vec3(0.08, 0.22, 0.55);
|
|
46
|
+
vec3 horizon = vec3(0.65, 0.72, 0.85);
|
|
47
|
+
vec3 ground = vec3(0.05, 0.045, 0.04);
|
|
48
|
+
float h = clamp(d.y, -1.0, 1.0);
|
|
49
|
+
vec3 col = h >= 0.0 ? mix(horizon, zenith, pow(h, 0.6)) : mix(horizon, ground, pow(-h, 0.2));
|
|
50
|
+
float s = max(dot(d, uSunDir), 0.0);
|
|
51
|
+
col += vec3(1.0, 0.85, 0.6) * pow(s, SUN_GLOW) * 0.6;
|
|
52
|
+
if (s > SUN_DISC) col = vec3(1.0, 0.87, 0.67) * SUN_RADIANCE;
|
|
53
|
+
fragColor = outputColor(col, 1.0);
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
function App() {
|
|
58
|
+
let spheres: { position: Vec3; material: ReturnType<typeof standard> }[] = []
|
|
59
|
+
for (let i = 0; i < COUNT; i++) {
|
|
60
|
+
let roughness = i / (COUNT - 1)
|
|
61
|
+
let x = (i - (COUNT - 1) / 2) * GAP
|
|
62
|
+
spheres.push({ position: [x, GAP / 2, 0], material: standard({ color: [0.95, 0.93, 0.88], metalness: 1, roughness }) })
|
|
63
|
+
spheres.push({ position: [x, -GAP / 2, 0], material: standard({ color: [0.8, 0.15, 0.1], metalness: 0, roughness }) })
|
|
64
|
+
}
|
|
65
|
+
let geometry = sphere({ radius: RADIUS, widthSegments: 48, heightSegments: 32 })
|
|
66
|
+
// The ref hands out the scene with its props applied (the background
|
|
67
|
+
// is set), so the bake happens right here, before the first frame.
|
|
68
|
+
let lit = (scene: SceneHandle) => {
|
|
69
|
+
scene.setParams({ uSunDir: SUN })
|
|
70
|
+
scene.setEnvironment({ cube: scene.bakeBackground(BAKE_SIZE) })
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
<window>
|
|
74
|
+
<view width={pct(100)} height={pct(100)}>
|
|
75
|
+
<Scene background={SKY} toneMapping="aces" samples={4} label="sky-lit" ref={lit}>
|
|
76
|
+
<OrbitCamera target={[0, 0, 0]} azimuth={0.3} elevation={0.15} distance={7} />
|
|
77
|
+
{spheres.map(s => (
|
|
78
|
+
<Mesh geometry={geometry} material={s.material} position={s.position} />
|
|
79
|
+
))}
|
|
80
|
+
</Scene>
|
|
81
|
+
</view>
|
|
82
|
+
</window>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render(() => <App />)
|