@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/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), geometry generators (box, plane, circle,
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), a per-vertex data channel
87
- (`withColors` adds an `aColor` vec4 - tint, baked AO, any four scalars -
88
- to any geometry, for materials that read it), one perspective camera
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, and scene backgrounds.
91
- Lights, transparency and model loading are
92
- staged next - see `okf/research/scene-graph-3d.md` for the roadmap. Full
93
- usage notes and traps: [AGENTS.md](AGENTS.md); runnable examples:
94
- [examples/](examples/).
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/).
@@ -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 via the exported
13
- GLSL so the creased-vs-smooth normals actually show.
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)} viewBox={[SIZE, SIZE]}>
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 />)
@@ -117,19 +117,19 @@ function App() {
117
117
 
118
118
  return (
119
119
  <window>
120
- <view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
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(1, 1, 1, "rock")}
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]}
@@ -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)} viewBox={[SIZE, SIZE]}>
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)} viewBox={[SIZE, SIZE]}>
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>
@@ -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)} viewBox={[SIZE, SIZE]}>
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)} viewBox={[SIZE, SIZE]}>
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 />)