@solidrt/3d 0.0.55 → 0.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
|
@@ -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
|
|
|
@@ -25,10 +52,10 @@ function App() {
|
|
|
25
52
|
let center: [number, number, number] = [(b[0]! + b[3]!) / 2, (b[1]! + b[4]!) / 2, (b[2]! + b[5]!) / 2]
|
|
26
53
|
let radius = Math.hypot(b[3]! - b[0]!, b[4]! - b[1]!, b[5]! - b[2]!) / 2
|
|
27
54
|
|
|
28
|
-
// Parts are named after the glTF nodes: a
|
|
29
|
-
//
|
|
55
|
+
// Parts are named after the glTF nodes: a tap hides that part, a tap
|
|
56
|
+
// on the body brings everything back.
|
|
30
57
|
for (let part of model.parts) {
|
|
31
|
-
part.mesh.
|
|
58
|
+
part.mesh.onTap = () => {
|
|
32
59
|
if (part.name === "body") for (let p of model.parts) setVisible(p.mesh, true)
|
|
33
60
|
else setVisible(part.mesh, false)
|
|
34
61
|
}
|
|
@@ -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
|
@@ -1,62 +1,136 @@
|
|
|
1
|
-
// Mesh pointer events
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// Mesh pointer events under an orbit control, the scene as the root of
|
|
2
|
+
// the walk: hover tints (enter/leave), a TAP pops (a down may be the start
|
|
3
|
+
// of an orbit drag, so the pop waits for the release), a drag on the
|
|
4
|
+
// crate slides it over the floor and never orbits - its down claims the
|
|
5
|
+
// press, so the orbit control's feed, listening at the root, sees none
|
|
6
|
+
// of it - while a drag anywhere else orbits and a wheel zooms; the ring
|
|
7
|
+
// claims the wheel to spin itself; a tap on empty space un-pops
|
|
8
|
+
// everything (the scene's own onTap with `mesh` null, the deselect
|
|
9
|
+
// idiom). The Group hears the cone's downs through bubbling (the crate's
|
|
10
|
+
// claim stops the walk at the crate). The scene is STATIC - no onFrame - so it renders only when an event changes
|
|
11
|
+
// something, and the orbit runs frames only while it glides; hit testing
|
|
12
|
+
// runs over the scene's BVH, so a pointer move costs O(log meshes).
|
|
8
13
|
//
|
|
9
14
|
// The volume tier is honest about its shape: hits test each mesh's
|
|
10
15
|
// bounding box, so hovering just outside the ball's silhouette (inside its
|
|
11
16
|
// box corners) still counts. Triangle-accurate hits are a later tier.
|
|
17
|
+
//
|
|
18
|
+
// Debug commands: `state` (the orbit pose, the crate's position, the
|
|
19
|
+
// ring's spin, the popped and hovered names) and `pixel` ({ name }: the
|
|
20
|
+
// WINDOW pixel a mesh's position projects to, for a synthetic tap).
|
|
12
21
|
|
|
13
|
-
import { createSignal, pct, render } from "@solidrt/core"
|
|
14
|
-
import { box, cone, Group, Mesh, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit } from "@solidrt/3d"
|
|
15
|
-
import type { Geometry,
|
|
16
|
-
|
|
17
|
-
const SIZE = 720
|
|
22
|
+
import { createInputMap, createPointerFeed, createSignal, displayScale, pct, render } from "@solidrt/core"
|
|
23
|
+
import { box, cone, getRotation, Group, Mesh, OrbitCamera, orbitActions, orbitBindings, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit, useScene } from "@solidrt/3d"
|
|
24
|
+
import type { Geometry, MeshNode, NodePointerEvent, NodeTapEvent, NodeWheelEvent, OrbitCameraHandle, SceneHandle, Vec3 } from "@solidrt/3d"
|
|
25
|
+
import { registerDebug } from "srt:dev"
|
|
18
26
|
|
|
19
27
|
type Color = [number, number, number]
|
|
20
28
|
|
|
29
|
+
// How much a popped mesh grows.
|
|
30
|
+
const POP_SCALE = 1.25
|
|
31
|
+
// Radians of ring spin per wheel pixel.
|
|
32
|
+
const WHEEL_SPIN = 0.01
|
|
33
|
+
// The orbit's starting pose: the old fixed camera at [0, 2.6, 5] looking
|
|
34
|
+
// at the scene's middle.
|
|
35
|
+
const ORBIT_TARGET: Vec3 = [0, 0.4, 0]
|
|
36
|
+
const ORBIT_DISTANCE = 5.5
|
|
37
|
+
const ORBIT_ELEVATION = 0.42
|
|
38
|
+
|
|
21
39
|
let [hovered, setHovered] = createSignal("nothing")
|
|
22
|
-
|
|
40
|
+
// Popped meshes and the scale they pop back to.
|
|
41
|
+
let popped = new Map<MeshNode, number>()
|
|
42
|
+
let meshes: Record<string, MeshNode> = {}
|
|
43
|
+
let orbit!: OrbitCameraHandle
|
|
44
|
+
let scene!: SceneHandle
|
|
45
|
+
|
|
46
|
+
let unpopAll = () => {
|
|
47
|
+
for (let [mesh, base] of popped) setTransform(mesh, { scale: base })
|
|
48
|
+
popped.clear()
|
|
49
|
+
}
|
|
23
50
|
|
|
24
51
|
/** A pickable mesh: unlit color that brightens on hover, scale-pop on
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
52
|
+
* tap; `drag` slides it over the floor (claiming the press), `spin`
|
|
53
|
+
* turns it on the wheel (claiming the wheel). setMeshParams writes raw
|
|
54
|
+
* uniform values, so the tint premultiplies here (alpha 1: the rgb passes
|
|
55
|
+
* through). */
|
|
56
|
+
function Pickable(p: { name: string; color: Color; geometry: Geometry; position: Vec3; scale?: number; drag?: boolean; spin?: boolean }) {
|
|
57
|
+
let scene = useScene().scene
|
|
28
58
|
let lift = (up: number): [number, number, number, number] => [
|
|
29
59
|
Math.min(p.color[0] + up, 1),
|
|
30
60
|
Math.min(p.color[1] + up, 1),
|
|
31
61
|
Math.min(p.color[2] + up, 1),
|
|
32
62
|
1,
|
|
33
63
|
]
|
|
64
|
+
// The floor point under the pointer at the mesh's height: the camera
|
|
65
|
+
// ray through the event's scene pixel meets the plane y = mesh y.
|
|
66
|
+
let floorPoint = (e: NodePointerEvent): [number, number] | null => {
|
|
67
|
+
let ray = scene.screenRay(e.x, e.y)
|
|
68
|
+
if (Math.abs(ray.direction[1]) < 1e-6) return null
|
|
69
|
+
let t = (e.mesh.position[1] - ray.origin[1]) / ray.direction[1]
|
|
70
|
+
if (t < 0) return null
|
|
71
|
+
return [ray.origin[0] + t * ray.direction[0], ray.origin[2] + t * ray.direction[2]]
|
|
72
|
+
}
|
|
73
|
+
// The mesh's offset from the grabbed floor point while a drag is on.
|
|
74
|
+
let grab: [number, number] | null = null
|
|
75
|
+
let spin = 0
|
|
34
76
|
return (
|
|
35
77
|
<Mesh
|
|
36
78
|
geometry={p.geometry}
|
|
37
79
|
material={unlit({ color: p.color })}
|
|
38
80
|
position={p.position}
|
|
39
81
|
scale={p.scale}
|
|
40
|
-
|
|
82
|
+
ref={m => (meshes[p.name] = m)}
|
|
83
|
+
onPointerEnter={(e: NodePointerEvent) => {
|
|
41
84
|
setHovered(p.name)
|
|
42
85
|
setMeshParams(e.mesh, { uColor: lift(0.25) })
|
|
43
|
-
console.log(`enter ${p.name}`)
|
|
44
86
|
}}
|
|
45
|
-
onPointerLeave={(e:
|
|
87
|
+
onPointerLeave={(e: NodePointerEvent) => {
|
|
46
88
|
setHovered("nothing")
|
|
47
89
|
setMeshParams(e.mesh, { uColor: lift(0) })
|
|
48
|
-
console.log(`leave ${p.name}`)
|
|
49
90
|
}}
|
|
50
|
-
|
|
51
|
-
let
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
91
|
+
onTap={(e: NodeTapEvent) => {
|
|
92
|
+
let base = popped.get(e.mesh)
|
|
93
|
+
if (base === undefined) {
|
|
94
|
+
popped.set(e.mesh, p.scale ?? 1)
|
|
95
|
+
setTransform(e.mesh, { scale: (p.scale ?? 1) * POP_SCALE })
|
|
96
|
+
} else {
|
|
97
|
+
popped.delete(e.mesh)
|
|
98
|
+
setTransform(e.mesh, { scale: base })
|
|
99
|
+
}
|
|
55
100
|
let pt = e.point!
|
|
56
|
-
console.log(`
|
|
57
|
-
// The cone demonstrates stopping the bubble: its group never hears it.
|
|
58
|
-
if (p.stop) e.stopPropagation()
|
|
101
|
+
console.log(`tap ${p.name} x${e.tapCount} at ${pt[0].toFixed(2)},${pt[1].toFixed(2)},${pt[2].toFixed(2)} d=${e.distance!.toFixed(2)}`)
|
|
59
102
|
}}
|
|
103
|
+
onPointerDown={
|
|
104
|
+
p.drag
|
|
105
|
+
? (e: NodePointerEvent) => {
|
|
106
|
+
let f = floorPoint(e)
|
|
107
|
+
if (f === null) return
|
|
108
|
+
grab = [e.mesh.position[0] - f[0], e.mesh.position[2] - f[1]]
|
|
109
|
+
// The claim: this press is the crate's, the orbit never sees it.
|
|
110
|
+
e.stopPropagation()
|
|
111
|
+
}
|
|
112
|
+
: undefined
|
|
113
|
+
}
|
|
114
|
+
onPointerMove={
|
|
115
|
+
p.drag
|
|
116
|
+
? (e: NodePointerEvent) => {
|
|
117
|
+
if (grab === null) return
|
|
118
|
+
let f = floorPoint(e)
|
|
119
|
+
if (f === null) return
|
|
120
|
+
setTransform(e.mesh, { position: [f[0] + grab[0], e.mesh.position[1], f[1] + grab[1]] })
|
|
121
|
+
}
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
124
|
+
onPointerUp={p.drag ? () => (grab = null) : undefined}
|
|
125
|
+
onWheel={
|
|
126
|
+
p.spin
|
|
127
|
+
? (e: NodeWheelEvent) => {
|
|
128
|
+
spin += e.deltaY * WHEEL_SPIN
|
|
129
|
+
setTransform(e.mesh, { rotation: [0, spin, 0] })
|
|
130
|
+
e.stopPropagation()
|
|
131
|
+
}
|
|
132
|
+
: undefined
|
|
133
|
+
}
|
|
60
134
|
/>
|
|
61
135
|
)
|
|
62
136
|
}
|
|
@@ -67,27 +141,61 @@ function App() {
|
|
|
67
141
|
let ball = sphere({ radius: 0.45 })
|
|
68
142
|
let spike = cone({ radius: 0.45, height: 1 })
|
|
69
143
|
let ring = torus({ radius: 0.4, tube: 0.14 })
|
|
144
|
+
// The orbit control's input: the scene leaf's pointer feed, bound to
|
|
145
|
+
// the standard orbit actions (drag rotates, wheel and pinch zoom).
|
|
146
|
+
let pointer = createPointerFeed()
|
|
147
|
+
let input = createInputMap(orbitActions)
|
|
148
|
+
input.bind(orbitBindings({ pointer }))
|
|
149
|
+
|
|
150
|
+
registerDebug("state", () => ({
|
|
151
|
+
pose: orbit.pose(),
|
|
152
|
+
crate: meshes.crate?.position,
|
|
153
|
+
ringSpin: meshes.ring ? getRotation(meshes.ring)[1] : null,
|
|
154
|
+
popped: Object.keys(meshes).filter(n => popped.has(meshes[n]!)),
|
|
155
|
+
hovered: hovered(),
|
|
156
|
+
}))
|
|
157
|
+
// The scene fills the window at device density, so a scene pixel over
|
|
158
|
+
// the display scale is a window pixel.
|
|
159
|
+
registerDebug("pixel", (args?: { name?: string }) => {
|
|
160
|
+
let mesh = meshes[args?.name ?? ""]
|
|
161
|
+
if (!mesh) return null
|
|
162
|
+
let p = scene.project(mesh.position)
|
|
163
|
+
if (p === null) return null
|
|
164
|
+
return { x: p.x / displayScale(), y: p.y / displayScale() }
|
|
165
|
+
})
|
|
70
166
|
|
|
71
167
|
return (
|
|
72
168
|
<window>
|
|
73
|
-
<view width={pct(100)} height={pct(100)}
|
|
74
|
-
<Scene
|
|
75
|
-
|
|
169
|
+
<view width={pct(100)} height={pct(100)}>
|
|
170
|
+
<Scene
|
|
171
|
+
clearColor={[0.07, 0.07, 0.1, 1]}
|
|
172
|
+
label="pick"
|
|
173
|
+
pointer={pointer}
|
|
174
|
+
ref={s => (scene = s)}
|
|
175
|
+
onTap={e => {
|
|
176
|
+
if (e.mesh !== null) return
|
|
177
|
+
console.log(`tap on nothing at ${e.x.toFixed(0)},${e.y.toFixed(0)}: un-pop`)
|
|
178
|
+
unpopAll()
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<PerspectiveCamera fov={55} />
|
|
182
|
+
<OrbitCamera input={input} target={ORBIT_TARGET} distance={ORBIT_DISTANCE} elevation={ORBIT_ELEVATION} ref={o => (orbit = o)} />
|
|
76
183
|
<Mesh geometry={floor} material={unlit({ color: [0.15, 0.16, 0.2] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
77
|
-
{/* The group hears
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<Pickable name="
|
|
184
|
+
{/* The group hears the cone's downs through bubbling; the crate's
|
|
185
|
+
claim stops the walk at the crate, so the group never sees those. */}
|
|
186
|
+
<Group onPointerDown={e => console.log(`group saw down on ${e.mesh === meshes.crate ? "crate" : "cone"}`)}>
|
|
187
|
+
<Pickable name="crate" color={[0.85, 0.3, 0.3]} geometry={crate} position={[-1.6, 0.4, 0]} scale={0.8} drag />
|
|
188
|
+
<Pickable name="cone" color={[0.35, 0.65, 0.9]} geometry={spike} position={[1.6, 0.5, 0]} />
|
|
81
189
|
</Group>
|
|
82
190
|
<Pickable name="ball" color={[0.9, 0.8, 0.35]} geometry={ball} position={[0, 0.45, 0]} />
|
|
83
|
-
<Pickable name="ring" color={[0.5, 0.85, 0.5]} geometry={ring} position={[0.9, 0.35, -1.6]} />
|
|
191
|
+
<Pickable name="ring" color={[0.5, 0.85, 0.5]} geometry={ring} position={[0.9, 0.35, -1.6]} spin />
|
|
84
192
|
</Scene>
|
|
85
193
|
<view position="absolute" x={0} y={0} padding={16} gap={4}>
|
|
86
194
|
<text color="#eef4ff" fontSize={22} fontWeight={700}>
|
|
87
195
|
{`hover: ${hovered()}`}
|
|
88
196
|
</text>
|
|
89
197
|
<text color="#8fa6c8" fontSize={13}>
|
|
90
|
-
hover tints,
|
|
198
|
+
hover tints, tap pops, drag the crate; drag elsewhere orbits, wheel zooms (over the ring: spins it), tap nothing to un-pop
|
|
91
199
|
</text>
|
|
92
200
|
</view>
|
|
93
201
|
</view>
|
|
@@ -0,0 +1,107 @@
|
|
|
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 { createInputMap, createPointerFeed, createSignal, gamepad, onFrame, pct, render } from "@solidrt/core"
|
|
16
|
+
import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, orbitActions, orbitBindings, 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
|
+
// The camera's input: the scene leaf's gestures and any pad, bound to the
|
|
62
|
+
// orbit control's actions - the app wires devices, the component takes
|
|
63
|
+
// the map (ARCHITECTURE.md).
|
|
64
|
+
let pointer = createPointerFeed()
|
|
65
|
+
let input = createInputMap(orbitActions)
|
|
66
|
+
input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
|
|
67
|
+
let [t, setT] = createSignal(0)
|
|
68
|
+
let scene: SceneHandle | undefined
|
|
69
|
+
let probe: ReflectionProbe | undefined
|
|
70
|
+
onFrame(tick => {
|
|
71
|
+
setT(tick / 1000)
|
|
72
|
+
if (scene !== undefined && probe === undefined) {
|
|
73
|
+
probe = scene.createReflectionProbe({ position: [0, 0, 0], size: PROBE_SIZE, layers: ROOM_LAYER, label: "ball-probe" })
|
|
74
|
+
scene.setEnvironment({ cube: probe.cube })
|
|
75
|
+
}
|
|
76
|
+
probe?.update()
|
|
77
|
+
})
|
|
78
|
+
let wall = plane({ width: ROOM * 2, height: ROOM * 2 })
|
|
79
|
+
let walls = WALL_COLORS.map(color => unlit({ color }))
|
|
80
|
+
let orbiter = sphere({ radius: ORBITER_RADIUS, widthSegments: 24, heightSegments: 16 })
|
|
81
|
+
let orbiters = ORBITER_COLORS.map(color => unlit({ color }))
|
|
82
|
+
let ball = standard({ color: [1, 1, 1], metalness: 1, roughness: BALL_ROUGHNESS })
|
|
83
|
+
let orbitPosition = (i: number): Vec3 => {
|
|
84
|
+
let a = t() * ORBIT_SPEED + (i * Math.PI) / 2
|
|
85
|
+
return [Math.cos(a) * ORBIT, Math.sin(a * 1.7) * 0.8, Math.sin(a) * ORBIT]
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
<window>
|
|
89
|
+
<view width={pct(100)} height={pct(100)}>
|
|
90
|
+
<Scene clearColor={[0.05, 0.05, 0.06, 1]} samples={4} label="probe-demo" ref={s => (scene = s)} pointer={pointer}>
|
|
91
|
+
<OrbitCamera input={input} target={[0, 0, 0]} azimuth={0.5} elevation={0.2} distance={4} />
|
|
92
|
+
<HemisphereLight sky={[0.6, 0.65, 0.7]} ground={[0.3, 0.28, 0.26]} intensity={0.6} />
|
|
93
|
+
<DirectionalLight direction={[0.4, -0.8, 0.3]} intensity={0.8} />
|
|
94
|
+
{WALLS.map((w, i) => (
|
|
95
|
+
<Mesh geometry={wall} material={walls[i]!} position={w.position} rotation={w.rotation} layers={BALL_LAYER | ROOM_LAYER} />
|
|
96
|
+
))}
|
|
97
|
+
{ORBITER_COLORS.map((_, i) => (
|
|
98
|
+
<Mesh geometry={orbiter} material={orbiters[i]!} position={orbitPosition(i)} layers={BALL_LAYER | ROOM_LAYER} />
|
|
99
|
+
))}
|
|
100
|
+
<Mesh geometry={sphere({ radius: BALL, widthSegments: 64, heightSegments: 48 })} material={ball} layers={BALL_LAYER} />
|
|
101
|
+
</Scene>
|
|
102
|
+
</view>
|
|
103
|
+
</window>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
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}
|