@solidrt/3d 0.0.51 → 0.0.53
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 +354 -91
- package/README.md +13 -9
- package/examples/README.md +37 -2
- package/examples/aim.tsx +5 -5
- package/examples/cascades.tsx +121 -0
- package/examples/instanced.tsx +4 -4
- package/examples/lit.tsx +66 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +51 -0
- package/examples/pick.tsx +5 -5
- package/examples/scene-background.tsx +3 -3
- package/examples/scene-basic.tsx +3 -3
- package/examples/scene-post-effect.tsx +3 -3
- package/examples/scene-views.tsx +95 -0
- package/examples/shadows.tsx +86 -0
- package/examples/sprites.tsx +95 -0
- package/examples/sweep-paths.tsx +11 -27
- package/package.json +4 -3
- package/src/components.tsx +110 -14
- package/src/geometry-gpu.ts +13 -2
- package/src/geometry.ts +331 -127
- package/src/glsl.ts +138 -3
- package/src/gltf.ts +437 -0
- package/src/index.ts +17 -12
- package/src/material.ts +353 -47
- package/src/math.ts +145 -0
- package/src/model-file.ts +122 -0
- package/src/model.ts +105 -0
- package/src/orbit.ts +13 -9
- package/src/order.ts +12 -5
- package/src/profile.ts +4 -8
- package/src/scene.ts +1294 -283
- package/src/sweep.ts +21 -36
- package/tools/model.ts +52 -0
- package/src/bvh.ts +0 -258
package/README.md
CHANGED
|
@@ -77,18 +77,22 @@ blinn, fresnel, a standard vertex stage) to compose your own lit looks
|
|
|
77
77
|
from plain template literals.
|
|
78
78
|
|
|
79
79
|
v1 scope: unlit color/textured materials plus `shaderMaterial` (your own
|
|
80
|
-
GLSL as a first-class material),
|
|
80
|
+
GLSL as a first-class material), sprites (`<Sprite>` with a `sprite()`
|
|
81
|
+
material: a quad facing the camera in the vertex stage, full or fixed-y
|
|
82
|
+
billboarding), geometry generators (box, plane, circle,
|
|
81
83
|
ring, sphere, cylinder, cone, torus, torus knot), a profile kit for custom
|
|
82
84
|
solids (`extrude` with bevels, `lathe`, polyline `sweep`/`tube` with
|
|
83
85
|
mitred joints, flat `shape`, with `fillet`/`roundRect`/`triangulate`
|
|
84
86
|
helpers), geometry as data (`transformGeometry` bakes a placement into
|
|
85
87
|
vertices and `mergeGeometries` concatenates parts, so a static scene is
|
|
86
|
-
one mesh per material),
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
one mesh per material), open vertex layouts (`withAttribute` appends any
|
|
89
|
+
named channel to a geometry's interleave, `withColors` is the `aColor`
|
|
90
|
+
vec4 case - tint, baked AO, any four scalars - and materials read channels
|
|
91
|
+
by name), one perspective camera
|
|
89
92
|
with an orbit control (`createOrbitCamera`: drag, pinch/wheel zoom, auto-orbit),
|
|
90
|
-
mesh picking with pointer events,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
[
|
|
93
|
+
mesh picking with pointer events, scene backgrounds, transparency, and
|
|
94
|
+
lights (`lit` material with hemisphere ambient, up to four directional
|
|
95
|
+
light nodes, Blinn-Phong highlight and triplanar mapping).
|
|
96
|
+
Model loading and shadows are staged next - see `okf/notes/3d-roadmap.md`
|
|
97
|
+
for the ranked list. Full usage notes and traps: [AGENTS.md](AGENTS.md);
|
|
98
|
+
runnable examples: [examples/](examples/).
|
package/examples/README.md
CHANGED
|
@@ -9,8 +9,14 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
9
9
|
and pipeline sharing, and the one-signal onFrame drive.
|
|
10
10
|
- `sweep-paths.tsx` - swept solids along polylines: a flat strap folding
|
|
11
11
|
over a crate (bare path points crease on the mitred bends) and a coiled
|
|
12
|
-
tube (smooth-tagged helix, one continuous mesh), lit
|
|
13
|
-
|
|
12
|
+
tube (smooth-tagged helix, one continuous mesh), lit so the
|
|
13
|
+
creased-vs-smooth normals actually show.
|
|
14
|
+
- `lit.tsx` - the lit material and light nodes: `<HemisphereLight>` plus
|
|
15
|
+
a warm key `<DirectionalLight>` turning inside a spinning `<Group>` and
|
|
16
|
+
a fixed cool fill,
|
|
17
|
+
a glossy sphere (specular/shininess), a transparent glass sphere, and
|
|
18
|
+
one checker map shared by a UV-mapped cube (stretched per face) and
|
|
19
|
+
two triplanar meshes tiling it at one world density.
|
|
14
20
|
- `aim.tsx` - the rotation verbs, one pointer each tracking an orbiting
|
|
15
21
|
target: `lookAt` for a +z solid, `quatFromTo` for aiming a y-axis cone,
|
|
16
22
|
and a `quatSlerp` damped follow that visibly lags; all driven from
|
|
@@ -23,8 +29,37 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
23
29
|
scene's own pass (`<Scene background>`): one target, no stacked
|
|
24
30
|
backdrop texture, no resize plumbing; the source is shader-target
|
|
25
31
|
compatible verbatim.
|
|
32
|
+
- `sprites.tsx` - sprites: a ring of `"full"` billboard glows that stay
|
|
33
|
+
flat to the screen and `"fixed-y"` cutout trees that only yaw toward
|
|
34
|
+
the camera and stay upright as it climbs, both turned in the vertex
|
|
35
|
+
stage while the camera circles - no per-sprite JS per frame.
|
|
26
36
|
- `instanced.tsx` - instanced meshes: one material class declaring
|
|
27
37
|
`instanceAttributes`, two `<InstancedMesh>` fleets (400 scattered
|
|
28
38
|
rocks, a ring of pines) each ONE draw entry and ONE uModel, a spinning
|
|
29
39
|
group moving both with two matrix writes, and `setInstanceCount` from
|
|
30
40
|
onFrame breathing the pine population.
|
|
41
|
+
- `scene-views.tsx` - scene views: one scene rendered three times, the
|
|
42
|
+
built-in perspective leaf plus two `scene.createView` targets - a
|
|
43
|
+
top-down ORTHOGRAPHIC map (`ortho` on setCamera) and a side silhouette
|
|
44
|
+
drawn with an `overrideMaterial`; one spinning group, one signal, every
|
|
45
|
+
target fed by the core's one flush.
|
|
46
|
+
- `shadows.tsx` - directional shadow maps from three casting lights: a
|
|
47
|
+
`castShadow` sun swinging through its arc (one setTransform per frame
|
|
48
|
+
on the light node), a fixed cool fill and a low rim light, `castShadow`
|
|
49
|
+
meshes turning in a group throwing three crossing shadows each, the
|
|
50
|
+
ground and the casters receiving through plain `lit` (the default);
|
|
51
|
+
each shadow camera follows its light's world matrix, each map is
|
|
52
|
+
rendered by an internal view.
|
|
53
|
+
- `cascades.tsx` - cascaded shadow maps: a sun with `shadow: { cascades:
|
|
54
|
+
3 }` over a field of pillars to the horizon under a slowly flying
|
|
55
|
+
camera; three maps fitted to slices of the camera frustum, sampled
|
|
56
|
+
tightest-first, so the shadows are sharp at the camera's feet and still
|
|
57
|
+
there at the far edge of the ground. A click cycles 1..4 cascades (1 is
|
|
58
|
+
the plain box widened to cover the field: one map's texels spread over
|
|
59
|
+
it, blocky everywhere) and the `cascades`/`fly` debug commands set the
|
|
60
|
+
count and the shadow distance and park the flight.
|
|
61
|
+
- `model.tsx` - a model from a file: `model.glb` (a small rover with
|
|
62
|
+
nested node transforms, a mirrored node, a textured material, a
|
|
63
|
+
transparent dome and a mesh without normals) parsed with `parseGltf`
|
|
64
|
+
from a binary import and built by `createModel` into a Group of named
|
|
65
|
+
parts; clicking a part hides it, clicking the body restores all.
|
package/examples/aim.tsx
CHANGED
|
@@ -74,16 +74,16 @@ function App() {
|
|
|
74
74
|
setTransform(lazy, { quaternion: aimQ })
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
let ball = sphere(0.22)
|
|
78
|
-
let pointer = cone(0.3, 0.9)
|
|
77
|
+
let ball = sphere({ radius: 0.22 })
|
|
78
|
+
let pointer = cone({ radius: 0.3, height: 0.9 })
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
81
|
<window>
|
|
82
|
-
<view width={pct(100)} height={pct(100)}
|
|
82
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
83
83
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="aim">
|
|
84
84
|
<PerspectiveCamera fov={55} position={[0, 2.6, 4.6]} lookAt={[0, 0.7, 0]} />
|
|
85
85
|
<Mesh
|
|
86
|
-
geometry={plane(7, 7, "floor")}
|
|
86
|
+
geometry={plane({ width: 7, height: 7, label: "floor" })}
|
|
87
87
|
material={unlit({ color: [0.16, 0.17, 0.22] })}
|
|
88
88
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
89
89
|
/>
|
|
@@ -93,7 +93,7 @@ function App() {
|
|
|
93
93
|
ref={n => (target = n)}
|
|
94
94
|
/>
|
|
95
95
|
<Mesh
|
|
96
|
-
geometry={tube([[0, 0, 0], [0, 0, 1.1]], 0.09, 10, "rod")}
|
|
96
|
+
geometry={tube([[0, 0, 0], [0, 0, 1.1]], { radius: 0.09, radialSegments: 10, label: "rod" })}
|
|
97
97
|
material={unlit({ color: [0.85, 0.3, 0.3] })}
|
|
98
98
|
position={[-1.4, 0.5, 0]}
|
|
99
99
|
ref={n => (rod = n)}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Cascaded shadow maps: a field of pillars to the horizon under a slowly
|
|
2
|
+
// flying camera, lit by one casting sun with `shadow: { cascades: 3 }`.
|
|
3
|
+
// A box shadow (`shadow.camera`) is one map over a fixed area: widened to
|
|
4
|
+
// cover this field its 1024 texels spread over 260 world units and every
|
|
5
|
+
// shadow is blocky, kept tight the far field is unshadowed. With cascades
|
|
6
|
+
// the light renders one map per slice of the SCENE camera's frustum
|
|
7
|
+
// (near..far, tightest first), re-fitted every time the camera or the
|
|
8
|
+
// light moves, and a receiver samples the tightest map that has the
|
|
9
|
+
// point: sharp contact shadows at the camera's feet, coarser ones toward
|
|
10
|
+
// the horizon, at the cost of N times the shadow fill. Every map is a
|
|
11
|
+
// tile of the scene's one atlas, so the pass count is unchanged.
|
|
12
|
+
//
|
|
13
|
+
// A click cycles 1..4 cascades (1 = the box, widened over the field).
|
|
14
|
+
// The `cascades` debug command sets the count and the shadow distance
|
|
15
|
+
// (`{ count, distance }`; the range the cascades split, the camera's far
|
|
16
|
+
// by default - pulling it in sharpens every cascade) and `fly` parks the
|
|
17
|
+
// flight (`{ t: seconds }`), so a capture repeats.
|
|
18
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
19
|
+
import { registerDebug } from "srt:dev"
|
|
20
|
+
import { box, DirectionalLight, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere } from "@solidrt/3d"
|
|
21
|
+
import type { Geometry, Vec3 } from "@solidrt/3d"
|
|
22
|
+
|
|
23
|
+
const SIZE = 720
|
|
24
|
+
const FIELD = 260
|
|
25
|
+
const FAR = 200
|
|
26
|
+
// The flight: a circle over the field, looking ahead along it.
|
|
27
|
+
const RADIUS = 50
|
|
28
|
+
const HEIGHT = 5
|
|
29
|
+
const PERIOD = 90
|
|
30
|
+
|
|
31
|
+
let [cascades, setCascades] = createSignal(3)
|
|
32
|
+
let [distance, setDistance] = createSignal<number | null>(null)
|
|
33
|
+
// The flight clock, in seconds; `parked` holds it.
|
|
34
|
+
let [time, setTime] = createSignal(0)
|
|
35
|
+
let parked: number | null = null
|
|
36
|
+
|
|
37
|
+
registerDebug("cascades", (args?: Record<string, unknown>) => {
|
|
38
|
+
if (typeof args?.count === "number") setCascades(args.count)
|
|
39
|
+
if (typeof args?.distance === "number" || args?.distance === null) setDistance(args.distance)
|
|
40
|
+
return { cascades: cascades(), distance: distance() }
|
|
41
|
+
})
|
|
42
|
+
registerDebug("fly", (args?: Record<string, unknown>) => {
|
|
43
|
+
if (typeof args?.t === "number") {
|
|
44
|
+
parked = args.t
|
|
45
|
+
setTime(args.t)
|
|
46
|
+
} else if (args?.t === null) {
|
|
47
|
+
parked = null
|
|
48
|
+
}
|
|
49
|
+
return { t: time(), parked: parked !== null }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// A grid of pillars with a sphere on every third one, heights varying so
|
|
53
|
+
// the shadows differ in length; one geometry per height, shared.
|
|
54
|
+
let pillars: { position: Vec3; height: number; ball: boolean }[] = []
|
|
55
|
+
let step = 16
|
|
56
|
+
for (let i = -7; i <= 7; i++) {
|
|
57
|
+
for (let j = -7; j <= 7; j++) {
|
|
58
|
+
let h = 2 + ((i * 7 + j * 3 + 100) % 5)
|
|
59
|
+
pillars.push({ position: [i * step + (j % 2) * 4, h / 2, j * step + (i % 2) * 5], height: h, ball: (i + j) % 3 === 0 })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let boxes = new Map<number, Geometry>()
|
|
63
|
+
let pillar = (height: number): Geometry => {
|
|
64
|
+
let g = boxes.get(height)
|
|
65
|
+
if (g === undefined) boxes.set(height, (g = box({ width: 1.2, height, depth: 1.2 })))
|
|
66
|
+
return g
|
|
67
|
+
}
|
|
68
|
+
let orb = sphere({ radius: 0.9 })
|
|
69
|
+
|
|
70
|
+
function App() {
|
|
71
|
+
onFrame(tick => {
|
|
72
|
+
if (parked === null) setTime(tick / 1000)
|
|
73
|
+
})
|
|
74
|
+
let eye = (): Vec3 => {
|
|
75
|
+
let a = (time() / PERIOD) * Math.PI * 2
|
|
76
|
+
return [Math.sin(a) * RADIUS, HEIGHT, Math.cos(a) * RADIUS]
|
|
77
|
+
}
|
|
78
|
+
let ahead = (): Vec3 => {
|
|
79
|
+
let a = (time() / PERIOD) * Math.PI * 2 + 0.5
|
|
80
|
+
return [Math.sin(a) * RADIUS, 1.5, Math.cos(a) * RADIUS]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let ground = lit({ color: [0.5, 0.55, 0.45] })
|
|
84
|
+
let stone = lit({ color: [0.75, 0.7, 0.62] })
|
|
85
|
+
let ball = lit({ color: [0.85, 0.35, 0.3], specular: 0.4, shininess: 30 })
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<window>
|
|
89
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]} onPointerDown={() => setCascades(c => (c % 4) + 1)}>
|
|
90
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.6, 0.72, 0.88, 1]} samples={4} label="cascades">
|
|
91
|
+
<PerspectiveCamera fov={50} near={0.5} far={FAR} position={eye()} lookAt={ahead()} />
|
|
92
|
+
<HemisphereLight sky={[0.5, 0.58, 0.7]} ground={[0.25, 0.22, 0.18]} />
|
|
93
|
+
<DirectionalLight
|
|
94
|
+
color={[1, 0.95, 0.85]}
|
|
95
|
+
intensity={0.9}
|
|
96
|
+
position={[40, 90, 30]}
|
|
97
|
+
direction={[-1, -0.55, -0.4]}
|
|
98
|
+
castShadow
|
|
99
|
+
shadow={{
|
|
100
|
+
mapSize: 1024,
|
|
101
|
+
normalBias: 0.08,
|
|
102
|
+
cascades: cascades(),
|
|
103
|
+
distance: distance(),
|
|
104
|
+
// The box tier's frustum, when cascades is 1: the whole field.
|
|
105
|
+
camera: { left: -FIELD / 2, right: FIELD / 2, top: FIELD / 2, bottom: -FIELD / 2, near: 1, far: 400 },
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
<Mesh geometry={plane({ width: FIELD, height: FIELD })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
|
|
109
|
+
{pillars.map(p => (
|
|
110
|
+
<>
|
|
111
|
+
<Mesh geometry={pillar(p.height)} material={stone} position={p.position} castShadow />
|
|
112
|
+
{p.ball ? <Mesh geometry={orb} material={ball} position={[p.position[0], p.height + 0.9, p.position[2]]} castShadow /> : null}
|
|
113
|
+
</>
|
|
114
|
+
))}
|
|
115
|
+
</Scene>
|
|
116
|
+
</view>
|
|
117
|
+
</window>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
render(() => <App />)
|
package/examples/instanced.tsx
CHANGED
|
@@ -117,19 +117,19 @@ function App() {
|
|
|
117
117
|
|
|
118
118
|
return (
|
|
119
119
|
<window>
|
|
120
|
-
<view width={pct(100)} height={pct(100)}
|
|
120
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
121
121
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.08, 0.1, 1]} label="instanced">
|
|
122
122
|
<PerspectiveCamera fov={55} position={[0, 3.2, 5.4]} lookAt={[0, 0.2, 0]} />
|
|
123
|
-
<Mesh geometry={plane(9, 9, "meadow")} material={unlit({ color: [0.16, 0.18, 0.16] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
123
|
+
<Mesh geometry={plane({ width: 9, height: 9, label: "meadow" })} material={unlit({ color: [0.16, 0.18, 0.16] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
124
124
|
<Group rotation={[0, spin(), 0]}>
|
|
125
125
|
<InstancedMesh
|
|
126
|
-
geometry={box(
|
|
126
|
+
geometry={box({ label: "rock" })}
|
|
127
127
|
material={instancedLook.instance()}
|
|
128
128
|
records={rocks(400)}
|
|
129
129
|
bounds={[-3.9, 0, -3.9, 3.9, 0.2, 3.9]}
|
|
130
130
|
/>
|
|
131
131
|
<InstancedMesh
|
|
132
|
-
geometry={cone(0.3, 1, 10, "pine")}
|
|
132
|
+
geometry={cone({ radius: 0.3, height: 1, radialSegments: 10, label: "pine" })}
|
|
133
133
|
material={instancedLook.instance()}
|
|
134
134
|
records={pines(PINE_COUNT)}
|
|
135
135
|
bounds={[-2.8, 0, -2.8, 2.8, 0.8, 2.8]}
|
package/examples/lit.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The lit material and the scene's lights. lit() is the standard look -
|
|
2
|
+
// hemisphere ambient, the scene's directional lights, Lambert diffuse,
|
|
3
|
+
// optional Blinn-Phong highlight - with the same color/map/transparent
|
|
4
|
+
// options as unlit. Lights are NODES: <HemisphereLight> plus up to four
|
|
5
|
+
// <DirectionalLight>s, here a warm key inside a spinning <Group> (a
|
|
6
|
+
// parent's rotation turns the light, no per-frame direction math) and a
|
|
7
|
+
// fixed cool fill; however many meshes, a light change is one shared
|
|
8
|
+
// uniform write. The three boxes share one checker map: the
|
|
9
|
+
// UV-mapped one stretches it per face (every generator emits 0..1 UVs),
|
|
10
|
+
// the two triplanar ones tile it at one world density whatever their
|
|
11
|
+
// size - the reason triplanar is an option on lit.
|
|
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, Scene, sphere, torusKnot, plane } from "@solidrt/3d"
|
|
16
|
+
|
|
17
|
+
const SIZE = 720
|
|
18
|
+
|
|
19
|
+
function checker(): ReturnType<typeof createTexture> {
|
|
20
|
+
let n = 64
|
|
21
|
+
let data = new Uint8Array(n * n * 4)
|
|
22
|
+
for (let y = 0; y < n; y++) {
|
|
23
|
+
for (let x = 0; x < n; x++) {
|
|
24
|
+
let v = ((x >> 3) + (y >> 3)) & 1 ? 235 : 120
|
|
25
|
+
data.set([v, v, v, 255], (y * n + x) * 4)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return createTexture(data, n, n, { wrap: "repeat", mipmap: true })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
let [t, setT] = createSignal(0)
|
|
33
|
+
onFrame(tick => setT(tick / 1000))
|
|
34
|
+
|
|
35
|
+
let map = checker()
|
|
36
|
+
let uvMapped = lit({ map })
|
|
37
|
+
let tiled = lit({ map, triplanar: 2 })
|
|
38
|
+
let glossy = lit({ color: [0.85, 0.3, 0.25], specular: 0.6, shininess: 60 })
|
|
39
|
+
let matte = lit({ color: [0.3, 0.55, 0.85] })
|
|
40
|
+
let glass = lit({ color: [0.9, 0.95, 1, 0.35], specular: 1, shininess: 120, transparent: true })
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<window>
|
|
44
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
45
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} samples={4} label="lit">
|
|
46
|
+
<PerspectiveCamera fov={50} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
|
|
47
|
+
<HemisphereLight sky={[0.35, 0.38, 0.45]} ground={[0.12, 0.1, 0.08]} />
|
|
48
|
+
<Group rotation={[0, t(), 0]}>
|
|
49
|
+
<DirectionalLight direction={[0.7, -0.8, 0]} color={[1, 0.92, 0.8]} intensity={0.9} />
|
|
50
|
+
</Group>
|
|
51
|
+
<DirectionalLight direction={[0.6, -0.3, 0.5]} color={[0.4, 0.5, 0.8]} intensity={0.35} />
|
|
52
|
+
<Mesh geometry={plane({ width: 8, height: 8 })} material={tiled} rotation={[-Math.PI / 2, 0, 0]} />
|
|
53
|
+
<Group rotation={[0, t() / 4, 0]}>
|
|
54
|
+
<Mesh geometry={box({ width: 0.8, height: 0.8, depth: 0.8 })} material={uvMapped} position={[-1.6, 0.4, 0]} />
|
|
55
|
+
<Mesh geometry={box({ width: 1.6, height: 0.5, depth: 0.8 })} material={tiled} position={[0, 0.25, 1.2]} />
|
|
56
|
+
<Mesh geometry={sphere({ radius: 0.5 })} material={glossy} position={[1.5, 0.5, 0]} />
|
|
57
|
+
<Mesh geometry={torusKnot({ radius: 0.35, tube: 0.12 })} material={matte} position={[0, 0.9, -1]} />
|
|
58
|
+
<Mesh geometry={sphere({ radius: 0.45 })} material={glass} position={[0, 0.45, 0]} />
|
|
59
|
+
</Group>
|
|
60
|
+
</Scene>
|
|
61
|
+
</view>
|
|
62
|
+
</window>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render(() => <App />)
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// A model from a file: model.glb (a small authored scene - a rover with a
|
|
2
|
+
// node tree of nested transforms, a mirrored node, a textured material, a
|
|
3
|
+
// transparent dome and a mesh shipped without normals) parsed at runtime
|
|
4
|
+
// with parseGltf and turned into a Group of meshes by createModel. The
|
|
5
|
+
// glb rides in the bundle as a binary import; a model under assets/ goes
|
|
6
|
+
// through loadGltf (same parser, read with flux:fs) or, baked once with
|
|
7
|
+
// `srt tool 3d/model`, through loadModel (no parsing at all). Every part
|
|
8
|
+
// keeps its node name and is an ordinary mesh: pointer events, transforms
|
|
9
|
+
// and materials work per part.
|
|
10
|
+
|
|
11
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
12
|
+
import { add, createModel, DirectionalLight, Group, HemisphereLight, parseGltf, PerspectiveCamera, Scene, setVisible } from "@solidrt/3d"
|
|
13
|
+
import type { SceneNode } from "@solidrt/3d"
|
|
14
|
+
import modelBytes from "./model.glb" with { type: "binary" }
|
|
15
|
+
|
|
16
|
+
const SIZE = 720
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
let [t, setT] = createSignal(0)
|
|
20
|
+
onFrame(tick => setT(tick / 1000))
|
|
21
|
+
|
|
22
|
+
// Parse + upload once; the model is plain scene data from here on.
|
|
23
|
+
let model = createModel(parseGltf(modelBytes), { label: "rover" })
|
|
24
|
+
let b = model.bounds
|
|
25
|
+
let center: [number, number, number] = [(b[0]! + b[3]!) / 2, (b[1]! + b[4]!) / 2, (b[2]! + b[5]!) / 2]
|
|
26
|
+
let radius = Math.hypot(b[3]! - b[0]!, b[4]! - b[1]!, b[5]! - b[2]!) / 2
|
|
27
|
+
|
|
28
|
+
// Parts are named after the glTF nodes: a click hides that part, a
|
|
29
|
+
// click on the body brings everything back.
|
|
30
|
+
for (let part of model.parts) {
|
|
31
|
+
part.mesh.onPointerDown = () => {
|
|
32
|
+
if (part.name === "body") for (let p of model.parts) setVisible(p.mesh, true)
|
|
33
|
+
else setVisible(part.mesh, false)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<window>
|
|
39
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
40
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.1, 0.11, 0.14, 1]} samples={4} label="model">
|
|
41
|
+
<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
|
+
<DirectionalLight direction={[0.5, -0.8, 0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
|
|
44
|
+
<Group rotation={[0, t() / 3, 0]} ref={(g: SceneNode) => add(g, model)} />
|
|
45
|
+
</Scene>
|
|
46
|
+
</view>
|
|
47
|
+
</window>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(() => <App />)
|
package/examples/pick.tsx
CHANGED
|
@@ -62,15 +62,15 @@ function Pickable(p: { name: string; color: Color; geometry: Geometry; position:
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function App() {
|
|
65
|
-
let floor = plane(8, 8, "floor")
|
|
65
|
+
let floor = plane({ width: 8, height: 8, label: "floor" })
|
|
66
66
|
let crate = box()
|
|
67
|
-
let ball = sphere(0.45)
|
|
68
|
-
let spike = cone(0.45, 1)
|
|
69
|
-
let ring = torus(0.4, 0.14)
|
|
67
|
+
let ball = sphere({ radius: 0.45 })
|
|
68
|
+
let spike = cone({ radius: 0.45, height: 1 })
|
|
69
|
+
let ring = torus({ radius: 0.4, tube: 0.14 })
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
72
|
<window>
|
|
73
|
-
<view width={pct(100)} height={pct(100)}
|
|
73
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
74
74
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="pick">
|
|
75
75
|
<PerspectiveCamera fov={55} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
|
|
76
76
|
<Mesh geometry={floor} material={unlit({ color: [0.15, 0.16, 0.2] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
@@ -28,12 +28,12 @@ let BACKDROP = glsl`
|
|
|
28
28
|
function App() {
|
|
29
29
|
return (
|
|
30
30
|
<window>
|
|
31
|
-
<view width={pct(100)} height={pct(100)}
|
|
31
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
32
32
|
<Scene width={SIZE} height={SIZE} background={BACKDROP} label="backdrop-demo">
|
|
33
33
|
<PerspectiveCamera fov={55} position={[0, 1.8, 4.4]} lookAt={[0, 0.4, 0]} />
|
|
34
|
-
<Mesh geometry={torusKnot(0.7, 0.2, 128, 16)} material={unlit({ color: [0.85, 0.55, 0.25] })} position={[0, 0.9, 0]} />
|
|
34
|
+
<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
35
|
<Mesh geometry={box()} material={unlit({ color: [0.3, 0.5, 0.8] })} position={[-1.5, 0.4, -0.5]} scale={0.8} />
|
|
36
|
-
<Mesh geometry={sphere(0.4)} material={unlit({ color: [0.4, 0.75, 0.45] })} position={[1.5, 0.4, -0.5]} />
|
|
36
|
+
<Mesh geometry={sphere({ radius: 0.4 })} material={unlit({ color: [0.4, 0.75, 0.45] })} position={[1.5, 0.4, -0.5]} />
|
|
37
37
|
</Scene>
|
|
38
38
|
</view>
|
|
39
39
|
</window>
|
package/examples/scene-basic.tsx
CHANGED
|
@@ -21,12 +21,12 @@ function App() {
|
|
|
21
21
|
// use one cube geometry (one vertex/index buffer pair on the GPU), and
|
|
22
22
|
// every mesh here shares the one unlit-color pipeline.
|
|
23
23
|
let cube = box()
|
|
24
|
-
let floor = plane(6, 6, "floor")
|
|
25
|
-
let ball = sphere(0.35)
|
|
24
|
+
let floor = plane({ width: 6, height: 6, label: "floor" })
|
|
25
|
+
let ball = sphere({ radius: 0.35 })
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
28
|
<window>
|
|
29
|
-
<view width={pct(100)} height={pct(100)}
|
|
29
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
30
30
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="scene-basic">
|
|
31
31
|
<PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
|
|
32
32
|
<Mesh
|
|
@@ -39,12 +39,12 @@ function App() {
|
|
|
39
39
|
onFrame(tick => setSpin(tick / 2000))
|
|
40
40
|
|
|
41
41
|
let cube = box()
|
|
42
|
-
let floor = plane(6, 6, "floor")
|
|
43
|
-
let ball = sphere(0.35)
|
|
42
|
+
let floor = plane({ width: 6, height: 6, label: "floor" })
|
|
43
|
+
let ball = sphere({ radius: 0.35 })
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<window>
|
|
47
|
-
<view width={pct(100)} height={pct(100)}
|
|
47
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
48
48
|
<Scene
|
|
49
49
|
width={RENDER}
|
|
50
50
|
height={RENDER}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// One scene, three renderings: the built-in leaf from a perspective
|
|
2
|
+
// camera, plus two `scene.createView` targets - a top-down ORTHOGRAPHIC
|
|
3
|
+
// map (`ortho` on setCamera) and a side silhouette drawn with an
|
|
4
|
+
// `overrideMaterial`. The group spins from ONE signal: each mesh is one
|
|
5
|
+
// core node whose flush writes its world matrix into every target's entry
|
|
6
|
+
// at once, so the extra views cost the app nothing per frame (and the
|
|
7
|
+
// light set fans out to them too - the map is lit like the main view).
|
|
8
|
+
// Views die with the scene.
|
|
9
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
10
|
+
import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
11
|
+
import type { SceneHandle } from "@solidrt/3d"
|
|
12
|
+
|
|
13
|
+
const MAIN = 720
|
|
14
|
+
const SIDE = 360
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
let [spin, setSpin] = createSignal(0)
|
|
18
|
+
onFrame(tick => setSpin(tick / 2000))
|
|
19
|
+
|
|
20
|
+
let cube = box()
|
|
21
|
+
let floor = plane({ width: 6, height: 6, label: "floor" })
|
|
22
|
+
let ball = sphere({ radius: 0.35 })
|
|
23
|
+
// `ref` runs before `output`, so the handle is set when the views are
|
|
24
|
+
// created there; both callbacks run once, untracked - no signals needed.
|
|
25
|
+
let scene!: SceneHandle
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<window>
|
|
29
|
+
<view width={pct(100)} height={pct(100)} designSize={[MAIN + SIDE, MAIN]}>
|
|
30
|
+
<Scene
|
|
31
|
+
width={MAIN}
|
|
32
|
+
height={MAIN}
|
|
33
|
+
clearColor={[0.07, 0.07, 0.1, 1]}
|
|
34
|
+
label="scene-views"
|
|
35
|
+
ref={s => (scene = s)}
|
|
36
|
+
output={tex => {
|
|
37
|
+
// The map: straight down from y = 10, world -z toward the top of
|
|
38
|
+
// the leaf, seven world units across at any depth.
|
|
39
|
+
let map = scene.createView({ width: SIDE, height: SIDE, clearColor: [0.05, 0.08, 0.06, 1], label: "map" })
|
|
40
|
+
map.setCamera({
|
|
41
|
+
position: [0, 10, 0],
|
|
42
|
+
target: [0, 0, 0],
|
|
43
|
+
up: [0, 0, -1],
|
|
44
|
+
ortho: { left: -3.5, right: 3.5, top: 3.5, bottom: -3.5 },
|
|
45
|
+
near: 0.1,
|
|
46
|
+
far: 20,
|
|
47
|
+
})
|
|
48
|
+
// The silhouette: every mesh drawn flat amber, from the side. The
|
|
49
|
+
// floor is edge-on here, a single line at the bottom.
|
|
50
|
+
let side = scene.createView({
|
|
51
|
+
width: SIDE,
|
|
52
|
+
height: SIDE,
|
|
53
|
+
clearColor: [0.1, 0.06, 0.05, 1],
|
|
54
|
+
overrideMaterial: unlit({ color: [0.95, 0.75, 0.3] }),
|
|
55
|
+
label: "silhouette",
|
|
56
|
+
})
|
|
57
|
+
side.setCamera({
|
|
58
|
+
position: [6, 1, 0],
|
|
59
|
+
target: [0, 0.6, 0],
|
|
60
|
+
ortho: { left: -2.4, right: 2.4, top: 2.4, bottom: -2.4 },
|
|
61
|
+
near: 0.1,
|
|
62
|
+
far: 20,
|
|
63
|
+
})
|
|
64
|
+
return (
|
|
65
|
+
<view flexDirection="row" width={MAIN + SIDE} height={MAIN}>
|
|
66
|
+
<texture src={tex} width={MAIN} height={MAIN} />
|
|
67
|
+
<view flexDirection="column" width={SIDE} height={MAIN}>
|
|
68
|
+
<texture src={map.texture} width={SIDE} height={SIDE} />
|
|
69
|
+
<texture src={side.texture} width={SIDE} height={SIDE} />
|
|
70
|
+
</view>
|
|
71
|
+
</view>
|
|
72
|
+
)
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
|
|
76
|
+
<HemisphereLight sky={[0.6, 0.7, 0.9]} ground={[0.25, 0.2, 0.15]} intensity={0.6} />
|
|
77
|
+
<DirectionalLight direction={[-0.5, -1, -0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
|
|
78
|
+
<Mesh geometry={floor} material={lit({ color: [0.3, 0.32, 0.38] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
79
|
+
<Group rotation={[0, spin(), 0]}>
|
|
80
|
+
<Mesh geometry={cube} material={lit({ color: [0.85, 0.3, 0.3] })} position={[0, 0.5, 0]} />
|
|
81
|
+
<Mesh
|
|
82
|
+
geometry={cube}
|
|
83
|
+
material={lit({ color: [0.9, 0.8, 0.35] })}
|
|
84
|
+
position={[-1.1, 0.7, 0]}
|
|
85
|
+
scale={[0.5, 1.4, 0.5]}
|
|
86
|
+
/>
|
|
87
|
+
<Mesh geometry={ball} material={lit({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
|
|
88
|
+
</Group>
|
|
89
|
+
</Scene>
|
|
90
|
+
</view>
|
|
91
|
+
</window>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
render(() => <App />)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Directional shadow maps: three `castShadow` lights, `castShadow` meshes
|
|
2
|
+
// and `lit` materials, which receive shadows by default. The warm sun
|
|
3
|
+
// swings through its arc from onFrame - ONE setTransform on the light
|
|
4
|
+
// node per frame; the scene re-places its shadow camera from the light's
|
|
5
|
+
// world matrix, so its shadows sweep across the ground while the casters
|
|
6
|
+
// turn inside their group. A cool fill from the opposite side and a low
|
|
7
|
+
// rim light stand still, so every caster throws three shadows that cross
|
|
8
|
+
// as the sun moves (each casting light is its own map and pass, capped
|
|
9
|
+
// by MAX_LIGHTS). Three's vocabulary throughout: `castShadow` on the
|
|
10
|
+
// light and on meshes, `shadow.mapSize/bias/normalBias/camera` on the
|
|
11
|
+
// light; the one divergence is that opting OUT of receiving is a material
|
|
12
|
+
// option (`lit({ receiveShadow: false })`, Godot's split), because the
|
|
13
|
+
// material picks the program (like vertexColors and triplanar).
|
|
14
|
+
//
|
|
15
|
+
// Placement matters for a casting light: its shadow camera sits at the
|
|
16
|
+
// light's WORLD position looking along its direction, so the sun is
|
|
17
|
+
// positioned above the scene (a light at the origin would shadow nothing
|
|
18
|
+
// above it). The light frustum is `shadow.camera`, +-5 world units by
|
|
19
|
+
// default - anything outside it is lit.
|
|
20
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
21
|
+
import { box, cylinder, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, setTransform, sphere, torusKnot } from "@solidrt/3d"
|
|
22
|
+
import type { DirectionalLightNode } from "@solidrt/3d"
|
|
23
|
+
|
|
24
|
+
const SIZE = 720
|
|
25
|
+
|
|
26
|
+
function App() {
|
|
27
|
+
let [t, setT] = createSignal(0)
|
|
28
|
+
let sun!: DirectionalLightNode
|
|
29
|
+
onFrame(tick => {
|
|
30
|
+
setT(tick / 1000)
|
|
31
|
+
// The sun's arc: east to west over the scene, always pointing at the
|
|
32
|
+
// origin. The direction is the node's local -y, so aiming is a
|
|
33
|
+
// rotation about z; the position rides the same arc so the shadow
|
|
34
|
+
// camera stays above the casters.
|
|
35
|
+
let a = Math.sin(tick / 4000) * 1.0
|
|
36
|
+
setTransform(sun, { position: [Math.sin(a) * 6, Math.cos(a) * 6, 2], rotation: [0.32, 0, -a] })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
let ground = lit({ color: [0.55, 0.58, 0.6] })
|
|
40
|
+
let red = lit({ color: [0.85, 0.3, 0.25] })
|
|
41
|
+
let blue = lit({ color: [0.3, 0.55, 0.85] })
|
|
42
|
+
let gold = lit({ color: [0.9, 0.75, 0.3], specular: 0.5, shininess: 40 })
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<window>
|
|
46
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
47
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.55, 0.65, 0.8, 1]} samples={4} label="shadows">
|
|
48
|
+
<PerspectiveCamera fov={45} position={[0, 4, 7]} lookAt={[0, 0.5, 0]} />
|
|
49
|
+
<HemisphereLight sky={[0.45, 0.5, 0.6]} ground={[0.2, 0.18, 0.15]} />
|
|
50
|
+
<DirectionalLight
|
|
51
|
+
ref={l => (sun = l)}
|
|
52
|
+
color={[1, 0.95, 0.85]}
|
|
53
|
+
intensity={0.8}
|
|
54
|
+
castShadow
|
|
55
|
+
shadow={{ mapSize: 1024, normalBias: 0.02, camera: { near: 1, far: 20 } }}
|
|
56
|
+
/>
|
|
57
|
+
<DirectionalLight
|
|
58
|
+
color={[0.45, 0.6, 0.9]}
|
|
59
|
+
intensity={0.5}
|
|
60
|
+
position={[-5, 5, -4]}
|
|
61
|
+
direction={[5, -5, 4]}
|
|
62
|
+
castShadow
|
|
63
|
+
shadow={{ normalBias: 0.02, camera: { near: 1, far: 20 } }}
|
|
64
|
+
/>
|
|
65
|
+
<DirectionalLight
|
|
66
|
+
color={[0.9, 0.5, 0.35]}
|
|
67
|
+
intensity={0.45}
|
|
68
|
+
position={[5, 2, -5]}
|
|
69
|
+
direction={[-5, -2, 5]}
|
|
70
|
+
castShadow
|
|
71
|
+
shadow={{ mapSize: 512, normalBias: 0.03, camera: { near: 1, far: 20 } }}
|
|
72
|
+
/>
|
|
73
|
+
<Mesh geometry={plane({ width: 10, height: 10 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
|
|
74
|
+
<Group rotation={[0, t() / 3, 0]}>
|
|
75
|
+
<Mesh geometry={box({ width: 1, height: 1, depth: 1 })} material={red} position={[-1.6, 0.5, 0]} castShadow />
|
|
76
|
+
<Mesh geometry={sphere({ radius: 0.6 })} material={blue} position={[1.5, 0.6, 0]} castShadow />
|
|
77
|
+
<Mesh geometry={torusKnot({ radius: 0.4, tube: 0.14 })} material={gold} position={[0, 1.3, 0]} castShadow />
|
|
78
|
+
<Mesh geometry={cylinder({ radiusTop: 0.12, radiusBottom: 0.12, height: 1.6 })} material={gold} position={[0, 0.8, 0]} castShadow />
|
|
79
|
+
</Group>
|
|
80
|
+
</Scene>
|
|
81
|
+
</view>
|
|
82
|
+
</window>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render(() => <App />)
|