@solidrt/3d 0.0.50 → 0.0.52

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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["ESNext"],
5
+ "jsx": "preserve",
6
+ "jsxImportSource": "@solidrt/core",
7
+ "moduleResolution": "bundler",
8
+ "allowImportingTsExtensions": true,
9
+ "noEmit": true,
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "types": ["@solidrt/flux-types"]
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -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,3 +29,29 @@ 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.
36
+ - `instanced.tsx` - instanced meshes: one material class declaring
37
+ `instanceAttributes`, two `<InstancedMesh>` fleets (400 scattered
38
+ rocks, a ring of pines) each ONE draw entry and ONE uModel, a spinning
39
+ group moving both with two matrix writes, and `setInstanceCount` from
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
+ - `model.tsx` - a model from a file: `model.glb` (a small rover with
54
+ nested node transforms, a mirrored node, a textured material, a
55
+ transparent dome and a mesh without normals) parsed with `parseGltf`
56
+ from a binary import and built by `createModel` into a Group of named
57
+ 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,158 @@
1
+ // Instanced meshes: one draw entry covering a whole population. A material
2
+ // class declares `instanceAttributes`, createInstancedMesh (here via the
3
+ // <InstancedMesh> component) supplies one interleaved record per instance,
4
+ // and the vertex stage reads each record through the matching `in`
5
+ // variables. Two meshes share the one class: 400 scattered "rocks" and a
6
+ // breathing ring of "pines" - each is ONE entry and ONE uModel however
7
+ // many instances it draws, so the spinning group below moves both fleets
8
+ // with two matrix writes per frame.
9
+ //
10
+ // setInstanceCount is the population dial (the pines breathe); records are
11
+ // data, not matrices - position/scale/tint here, whatever your shader
12
+ // wants in general. The explicit `bounds` cover the scatter so picking
13
+ // still works (one conservative box around the population; omit bounds and
14
+ // the mesh simply never picks).
15
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
16
+ import { glsl } from "@solidrt/core/gpu"
17
+ import {
18
+ box,
19
+ cone,
20
+ Group,
21
+ InstancedMesh,
22
+ Mesh,
23
+ PerspectiveCamera,
24
+ plane,
25
+ Scene,
26
+ setInstanceCount,
27
+ shaderMaterialClass,
28
+ unlit,
29
+ } from "@solidrt/3d"
30
+ import type { InstancedMeshNode } from "@solidrt/3d"
31
+ import { HEMISPHERE } from "@solidrt/3d/glsl"
32
+
33
+ const SIZE = 720
34
+
35
+ const INSTANCE_VERTEX = glsl`
36
+ in vec3 aPos;
37
+ in vec3 aNormal;
38
+ in vec3 iPos;
39
+ in float iScale;
40
+ in vec3 iTint;
41
+ out vec3 vNormal;
42
+ out vec3 vTint;
43
+ uniform mat4 uModel;
44
+ uniform mat4 uViewProj;
45
+
46
+ void main() {
47
+ vec3 p = aPos * iScale + iPos;
48
+ gl_Position = uViewProj * uModel * vec4(p, 1.0);
49
+ vNormal = mat3(uModel) * aNormal;
50
+ vTint = iTint;
51
+ }
52
+ `
53
+
54
+ const INSTANCE_FRAGMENT = glsl`
55
+ in vec3 vNormal;
56
+ in vec3 vTint;
57
+ ${HEMISPHERE}
58
+
59
+ void main() {
60
+ vec3 c = vTint * hemisphere(normalize(vNormal), vec3(1.05, 1.0, 0.95), vec3(0.35, 0.32, 0.3));
61
+ fragColor = vec4(c, 1.0);
62
+ }
63
+ `
64
+
65
+ // One record per instance, interleaved in attribute order: 7 floats.
66
+ const STRIDE = 7
67
+
68
+ // A deterministic scatter (no per-run surprises when eyeballing).
69
+ function rocks(count: number): Float32Array {
70
+ let records = new Float32Array(count * STRIDE)
71
+ let a = 0
72
+ for (let i = 0; i < count; i++) {
73
+ a += 2.399963 // golden angle: an even spiral scatter
74
+ let r = 0.35 + 3.4 * Math.sqrt((i + 0.5) / count)
75
+ let s = 0.05 + 0.11 * ((i * 7) % 10) / 10
76
+ let o = i * STRIDE
77
+ records[o] = Math.cos(a) * r
78
+ records[o + 1] = s / 2
79
+ records[o + 2] = Math.sin(a) * r
80
+ records[o + 3] = s
81
+ records[o + 4] = 0.55 + 0.3 * ((i * 3) % 5) / 5
82
+ records[o + 5] = 0.5 + 0.2 * ((i * 11) % 7) / 7
83
+ records[o + 6] = 0.45
84
+ }
85
+ return records
86
+ }
87
+
88
+ function pines(count: number): Float32Array {
89
+ let records = new Float32Array(count * STRIDE)
90
+ for (let i = 0; i < count; i++) {
91
+ let a = (i / count) * Math.PI * 2
92
+ let s = 0.5 + 0.25 * ((i * 5) % 8) / 8
93
+ let o = i * STRIDE
94
+ records[o] = Math.cos(a) * 2.4
95
+ records[o + 1] = s / 2
96
+ records[o + 2] = Math.sin(a) * 2.4
97
+ records[o + 3] = s
98
+ records[o + 4] = 0.15
99
+ records[o + 5] = 0.4 + 0.25 * ((i * 3) % 6) / 6
100
+ records[o + 6] = 0.2
101
+ }
102
+ return records
103
+ }
104
+
105
+ const PINE_COUNT = 48
106
+
107
+ function App() {
108
+ let [spin, setSpin] = createSignal(0)
109
+ let pinesMesh!: InstancedMeshNode
110
+ onFrame(tick => {
111
+ setSpin(tick / 6000)
112
+ // The population dial: draw the first N records. The buffer holds the
113
+ // full ring; only the draw range moves, one setDrawRange per change.
114
+ let n = Math.round(PINE_COUNT * (0.5 + 0.5 * Math.sin(tick / 900)))
115
+ if (pinesMesh) setInstanceCount(pinesMesh, n)
116
+ })
117
+
118
+ return (
119
+ <window>
120
+ <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
121
+ <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.08, 0.1, 1]} label="instanced">
122
+ <PerspectiveCamera fov={55} position={[0, 3.2, 5.4]} lookAt={[0, 0.2, 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
+ <Group rotation={[0, spin(), 0]}>
125
+ <InstancedMesh
126
+ geometry={box({ label: "rock" })}
127
+ material={instancedLook.instance()}
128
+ records={rocks(400)}
129
+ bounds={[-3.9, 0, -3.9, 3.9, 0.2, 3.9]}
130
+ />
131
+ <InstancedMesh
132
+ geometry={cone({ radius: 0.3, height: 1, radialSegments: 10, label: "pine" })}
133
+ material={instancedLook.instance()}
134
+ records={pines(PINE_COUNT)}
135
+ bounds={[-2.8, 0, -2.8, 2.8, 0.8, 2.8]}
136
+ ref={m => (pinesMesh = m)}
137
+ />
138
+ </Group>
139
+ </Scene>
140
+ </view>
141
+ </window>
142
+ )
143
+ }
144
+
145
+ // One class, one compiled pipeline; each mesh gets its own instance() so
146
+ // per-mesh uniforms stay independent (none are used here).
147
+ let instancedLook = shaderMaterialClass({
148
+ vertex: INSTANCE_VERTEX,
149
+ fragment: INSTANCE_FRAGMENT,
150
+ instanceAttributes: [
151
+ { name: "iPos", format: "vec3" },
152
+ { name: "iScale", format: "f32" },
153
+ { name: "iTint", format: "vec3" },
154
+ ],
155
+ label: "instanced-look",
156
+ })
157
+
158
+ render(() => <App />)
@@ -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 />)