@solidrt/3d 0.0.55 → 0.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +1083 -183
  2. package/README.md +15 -7
  3. package/examples/README.md +79 -5
  4. package/examples/aim.tsx +2 -3
  5. package/examples/cascades.tsx +1 -4
  6. package/examples/collision.tsx +250 -0
  7. package/examples/first-person.tsx +119 -0
  8. package/examples/fleet.tsx +194 -0
  9. package/examples/fog.tsx +2 -3
  10. package/examples/instanced.tsx +72 -32
  11. package/examples/lamps.tsx +92 -0
  12. package/examples/lit.tsx +3 -5
  13. package/examples/materials.tsx +150 -0
  14. package/examples/model-load.tsx +95 -0
  15. package/examples/model.glb +0 -0
  16. package/examples/model.tsx +32 -6
  17. package/examples/pick.tsx +2 -4
  18. package/examples/probe.tsx +101 -0
  19. package/examples/scene-background.tsx +2 -4
  20. package/examples/scene-basic.tsx +2 -4
  21. package/examples/scene-views.tsx +32 -3
  22. package/examples/shadows.tsx +2 -4
  23. package/examples/sky-lit.tsx +86 -0
  24. package/examples/skybox.tsx +128 -0
  25. package/examples/sprites.tsx +4 -6
  26. package/examples/standard.tsx +132 -0
  27. package/examples/sweep-paths.tsx +2 -4
  28. package/package.json +3 -3
  29. package/src/camera.ts +148 -0
  30. package/src/clip.ts +96 -0
  31. package/src/collision.ts +199 -0
  32. package/src/color.ts +32 -0
  33. package/src/components.tsx +763 -82
  34. package/src/environment-bake.ts +492 -0
  35. package/src/environment.ts +325 -0
  36. package/src/first-person.ts +306 -0
  37. package/src/geometry.ts +6 -2
  38. package/src/glsl.ts +1343 -82
  39. package/src/gltf.ts +432 -112
  40. package/src/index.ts +57 -9
  41. package/src/light.ts +383 -0
  42. package/src/material.ts +667 -232
  43. package/src/math.ts +110 -0
  44. package/src/mesh.ts +728 -0
  45. package/src/mixer.ts +476 -0
  46. package/src/model-file.ts +80 -15
  47. package/src/model.ts +204 -35
  48. package/src/node.ts +506 -0
  49. package/src/orbit.ts +97 -48
  50. package/src/scene-pointer.ts +166 -0
  51. package/src/scene-shadows.ts +471 -0
  52. package/src/scene.ts +1242 -1557
  53. package/tools/environment.ts +69 -0
  54. package/tools/model.ts +4 -3
@@ -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 />)
Binary file
@@ -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 { add, createModel, DirectionalLight, Group, HemisphereLight, parseGltf, PerspectiveCamera, Scene, setVisible } from "@solidrt/3d"
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
- const SIZE = 720
23
+ // Face edge of the gradient environment cube: a gradient needs no detail.
24
+ const ENV_FACE = 16
25
+ // The gradient, zenith to nadir, sRGB bytes: sky, horizon, ground, below.
26
+ const ENV_ROWS: [number, number, number][] = [
27
+ [96, 122, 168],
28
+ [176, 190, 210],
29
+ [120, 105, 90],
30
+ [60, 52, 45],
31
+ ]
32
+
33
+ // A tiny equirectangular panorama (2 columns, one row per band, linear
34
+ // filtering between) turned into the cube the scene lights by.
35
+ function gradientEnvironment(): TextureId {
36
+ let px = new Uint8Array(2 * ENV_ROWS.length * 4)
37
+ ENV_ROWS.forEach((c, y) => {
38
+ for (let x = 0; x < 2; x++) px.set([c[0], c[1], c[2], 255], (y * 2 + x) * 4)
39
+ })
40
+ let panorama = createTexture(px, 2, ENV_ROWS.length, { format: "rgba8-srgb", filter: "linear" })
41
+ return equirectToCube(panorama, ENV_FACE, { format: "rgba8-srgb", mipmap: true, label: "gradient-env" })
42
+ }
17
43
 
18
44
  function App() {
45
+ let cube = gradientEnvironment()
19
46
  let [t, setT] = createSignal(0)
20
47
  onFrame(tick => setT(tick / 1000))
21
48
 
@@ -36,10 +63,9 @@ function App() {
36
63
 
37
64
  return (
38
65
  <window>
39
- <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
40
- <Scene width={SIZE} height={SIZE} clearColor={[0.1, 0.11, 0.14, 1]} samples={4} label="model">
66
+ <view width={pct(100)} height={pct(100)}>
67
+ <Scene clearColor={[0.1, 0.11, 0.14, 1]} environment={{ cube }} samples={4} label="model">
41
68
  <PerspectiveCamera fov={40} position={[center[0] + radius * 1.4, center[1] + radius * 1.1, center[2] + radius * 2.2]} lookAt={center} />
42
- <HemisphereLight sky={[0.45, 0.5, 0.6]} ground={[0.2, 0.17, 0.14]} />
43
69
  <DirectionalLight direction={[0.5, -0.8, 0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
44
70
  <Group rotation={[0, t() / 3, 0]} ref={(g: SceneNode) => add(g, model)} />
45
71
  </Scene>
package/examples/pick.tsx CHANGED
@@ -14,8 +14,6 @@ import { createSignal, pct, render } from "@solidrt/core"
14
14
  import { box, cone, Group, Mesh, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit } from "@solidrt/3d"
15
15
  import type { Geometry, ScenePointerEvent, Vec3 } from "@solidrt/3d"
16
16
 
17
- const SIZE = 720
18
-
19
17
  type Color = [number, number, number]
20
18
 
21
19
  let [hovered, setHovered] = createSignal("nothing")
@@ -70,8 +68,8 @@ function App() {
70
68
 
71
69
  return (
72
70
  <window>
73
- <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
74
- <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="pick">
71
+ <view width={pct(100)} height={pct(100)}>
72
+ <Scene clearColor={[0.07, 0.07, 0.1, 1]} label="pick">
75
73
  <PerspectiveCamera fov={55} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
76
74
  <Mesh geometry={floor} material={unlit({ color: [0.15, 0.16, 0.2] })} rotation={[-Math.PI / 2, 0, 0]} />
77
75
  {/* The group hears every child click that is not stopped. */}
@@ -0,0 +1,101 @@
1
+ // A reflection probe: `scene.createReflectionProbe` renders the scene into
2
+ // a cube map from a point, and that cube is the environment a chrome ball
3
+ // at the same point mirrors - Three's CubeCamera, Unity's and Godot's
4
+ // realtime ReflectionProbe. Six colored walls make a room, four glowing
5
+ // spheres orbit the ball, and the probe re-renders every frame (six scene
6
+ // passes) so the orbit shows in the chrome. Layers keep the ball out of
7
+ // its own probe: the room and the orbiters live on layers 1 and 2, the
8
+ // ball on layer 1 only, and the probe looks at layer 2. Each update also
9
+ // prefilters the faces into the roughness chain (the default), so the
10
+ // ball's satin finish blurs the room the way a baked environment would;
11
+ // `prefilter: false` would keep a sharp mirror at a sixth of the passes
12
+ // (the chain is the frame's largest GPU item; see AGENTS.md). Drag to
13
+ // look around.
14
+
15
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
16
+ import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, plane, Scene, sphere, standard, unlit } from "@solidrt/3d"
17
+ import type { ReflectionProbe, SceneHandle } from "@solidrt/3d"
18
+ import type { Vec3 } from "@solidrt/3d/math"
19
+
20
+ // Half the room's edge.
21
+ const ROOM = 5
22
+ // The orbiters' radius, orbit radius and speed (radians per second).
23
+ const ORBITER_RADIUS = 0.35
24
+ const ORBIT = 2
25
+ const ORBIT_SPEED = 0.8
26
+ // The chrome ball's radius and its satin roughness (0 is a mirror).
27
+ const BALL = 1
28
+ const BALL_ROUGHNESS = 0.2
29
+ // The probe's face edge.
30
+ const PROBE_SIZE = 128
31
+ // Layer bits: the scene camera sees both, the probe only ROOM_LAYER.
32
+ const BALL_LAYER = 1
33
+ const ROOM_LAYER = 2
34
+
35
+ // Wall colors (sRGB), one per face: +X, -X, +Y, -Y, +Z, -Z.
36
+ const WALL_COLORS: [number, number, number][] = [
37
+ [0.75, 0.3, 0.25],
38
+ [0.25, 0.6, 0.35],
39
+ [0.85, 0.85, 0.9],
40
+ [0.3, 0.28, 0.26],
41
+ [0.25, 0.4, 0.75],
42
+ [0.8, 0.7, 0.3],
43
+ ]
44
+ // Each wall's position and the rotation that turns the plane's front inward.
45
+ const WALLS: { position: Vec3; rotation: Vec3 }[] = [
46
+ { position: [ROOM, 0, 0], rotation: [0, -Math.PI / 2, 0] },
47
+ { position: [-ROOM, 0, 0], rotation: [0, Math.PI / 2, 0] },
48
+ { position: [0, ROOM, 0], rotation: [Math.PI / 2, 0, 0] },
49
+ { position: [0, -ROOM, 0], rotation: [-Math.PI / 2, 0, 0] },
50
+ { position: [0, 0, ROOM], rotation: [0, Math.PI, 0] },
51
+ { position: [0, 0, -ROOM], rotation: [0, 0, 0] },
52
+ ]
53
+ const ORBITER_COLORS: [number, number, number][] = [
54
+ [1, 0.4, 0.2],
55
+ [0.3, 0.9, 1],
56
+ [1, 0.9, 0.3],
57
+ [0.7, 0.4, 1],
58
+ ]
59
+
60
+ function App() {
61
+ let [t, setT] = createSignal(0)
62
+ let scene: SceneHandle | undefined
63
+ let probe: ReflectionProbe | undefined
64
+ onFrame(tick => {
65
+ setT(tick / 1000)
66
+ if (scene !== undefined && probe === undefined) {
67
+ probe = scene.createReflectionProbe({ position: [0, 0, 0], size: PROBE_SIZE, layers: ROOM_LAYER, label: "ball-probe" })
68
+ scene.setEnvironment({ cube: probe.cube })
69
+ }
70
+ probe?.update()
71
+ })
72
+ let wall = plane({ width: ROOM * 2, height: ROOM * 2 })
73
+ let walls = WALL_COLORS.map(color => unlit({ color }))
74
+ let orbiter = sphere({ radius: ORBITER_RADIUS, widthSegments: 24, heightSegments: 16 })
75
+ let orbiters = ORBITER_COLORS.map(color => unlit({ color }))
76
+ let ball = standard({ color: [1, 1, 1], metalness: 1, roughness: BALL_ROUGHNESS })
77
+ let orbitPosition = (i: number): Vec3 => {
78
+ let a = t() * ORBIT_SPEED + (i * Math.PI) / 2
79
+ return [Math.cos(a) * ORBIT, Math.sin(a * 1.7) * 0.8, Math.sin(a) * ORBIT]
80
+ }
81
+ return (
82
+ <window>
83
+ <view width={pct(100)} height={pct(100)}>
84
+ <Scene clearColor={[0.05, 0.05, 0.06, 1]} samples={4} label="probe-demo" ref={s => (scene = s)}>
85
+ <OrbitCamera target={[0, 0, 0]} azimuth={0.5} elevation={0.2} distance={4} />
86
+ <HemisphereLight sky={[0.6, 0.65, 0.7]} ground={[0.3, 0.28, 0.26]} intensity={0.6} />
87
+ <DirectionalLight direction={[0.4, -0.8, 0.3]} intensity={0.8} />
88
+ {WALLS.map((w, i) => (
89
+ <Mesh geometry={wall} material={walls[i]!} position={w.position} rotation={w.rotation} layers={BALL_LAYER | ROOM_LAYER} />
90
+ ))}
91
+ {ORBITER_COLORS.map((_, i) => (
92
+ <Mesh geometry={orbiter} material={orbiters[i]!} position={orbitPosition(i)} layers={BALL_LAYER | ROOM_LAYER} />
93
+ ))}
94
+ <Mesh geometry={sphere({ radius: BALL, widthSegments: 64, heightSegments: 48 })} material={ball} layers={BALL_LAYER} />
95
+ </Scene>
96
+ </view>
97
+ </window>
98
+ )
99
+ }
100
+
101
+ render(() => <App />)
@@ -10,8 +10,6 @@ import { pct, render } from "@solidrt/core"
10
10
  import { glsl } from "@solidrt/core/gpu"
11
11
  import { box, Mesh, PerspectiveCamera, Scene, sphere, torusKnot, unlit } from "@solidrt/3d"
12
12
 
13
- const SIZE = 720
14
-
15
13
  // A radial night-sky gradient with hash grain so the ramp does not band.
16
14
  let BACKDROP = glsl`
17
15
  void main() {
@@ -28,8 +26,8 @@ let BACKDROP = glsl`
28
26
  function App() {
29
27
  return (
30
28
  <window>
31
- <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
32
- <Scene width={SIZE} height={SIZE} background={BACKDROP} label="backdrop-demo">
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} />
@@ -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)} designSize={[SIZE, SIZE]}>
30
- <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="scene-basic">
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}
@@ -6,10 +6,20 @@
6
6
  // at once, so the extra views cost the app nothing per frame (and the
7
7
  // light set fans out to them too - the map is lit like the main view).
8
8
  // Views die with the scene.
9
+ //
10
+ // Layers and per-view fog: the marker disc is on layer bit 2, so the
11
+ // main render (mask 1) and the silhouette never draw it - only the map,
12
+ // whose mask admits both bits. The scene is fogged; `fog: null` on the
13
+ // map keeps the top-down view clear (its fog names are view-owned, so
14
+ // scene-wide fog writes never clobber them).
9
15
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
10
- import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
16
+ import { box, circle, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
11
17
  import type { SceneHandle } from "@solidrt/3d"
12
18
 
19
+ // Layer bits: the world everything defaults to, and the map-only markers.
20
+ const WORLD_LAYER = 1
21
+ const MARKER_LAYER = 2
22
+
13
23
  const MAIN = 720
14
24
  const SIDE = 360
15
25
 
@@ -20,6 +30,7 @@ function App() {
20
30
  let cube = box()
21
31
  let floor = plane({ width: 6, height: 6, label: "floor" })
22
32
  let ball = sphere({ radius: 0.35 })
33
+ let marker = circle({ radius: 0.3, label: "marker" })
23
34
  // `ref` runs before `output`, so the handle is set when the views are
24
35
  // created there; both callbacks run once, untracked - no signals needed.
25
36
  let scene!: SceneHandle
@@ -31,12 +42,22 @@ function App() {
31
42
  width={MAIN}
32
43
  height={MAIN}
33
44
  clearColor={[0.07, 0.07, 0.1, 1]}
45
+ fog={{ color: [0.07, 0.07, 0.1], near: 2.5, far: 9 }}
34
46
  label="scene-views"
35
47
  ref={s => (scene = s)}
36
48
  output={tex => {
37
49
  // The map: straight down from y = 10, world -z toward the top of
38
- // the leaf, seven world units across at any depth.
39
- let map = scene.createView({ width: SIDE, height: SIDE, clearColor: [0.05, 0.08, 0.06, 1], label: "map" })
50
+ // the leaf, seven world units across at any depth. Its mask also
51
+ // admits the marker layer, and `fog: null` keeps it clear while
52
+ // the main view fades to the horizon.
53
+ let map = scene.createView({
54
+ width: SIDE,
55
+ height: SIDE,
56
+ clearColor: [0.05, 0.08, 0.06, 1],
57
+ layers: WORLD_LAYER | MARKER_LAYER,
58
+ fog: null,
59
+ label: "map",
60
+ })
40
61
  map.setCamera({
41
62
  position: [0, 10, 0],
42
63
  target: [0, 0, 0],
@@ -85,6 +106,14 @@ function App() {
85
106
  scale={[0.5, 1.4, 0.5]}
86
107
  />
87
108
  <Mesh geometry={ball} material={lit({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
109
+ {/* The ball's map marker: drawn by the map view alone. */}
110
+ <Mesh
111
+ geometry={marker}
112
+ material={unlit({ color: [0.95, 0.25, 0.2] })}
113
+ layers={MARKER_LAYER}
114
+ position={[1.1, 2, 0]}
115
+ rotation={[-Math.PI / 2, 0, 0]}
116
+ />
88
117
  </Group>
89
118
  </Scene>
90
119
  </view>
@@ -21,8 +21,6 @@ import { createSignal, onFrame, pct, render } from "@solidrt/core"
21
21
  import { box, cylinder, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, setTransform, sphere, torusKnot } from "@solidrt/3d"
22
22
  import type { DirectionalLightNode } from "@solidrt/3d"
23
23
 
24
- const SIZE = 720
25
-
26
24
  function App() {
27
25
  let [t, setT] = createSignal(0)
28
26
  let sun!: DirectionalLightNode
@@ -43,8 +41,8 @@ function App() {
43
41
 
44
42
  return (
45
43
  <window>
46
- <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
47
- <Scene width={SIZE} height={SIZE} clearColor={[0.55, 0.65, 0.8, 1]} samples={4} label="shadows">
44
+ <view width={pct(100)} height={pct(100)}>
45
+ <Scene clearColor={[0.55, 0.65, 0.8, 1]} samples={4} label="shadows">
48
46
  <PerspectiveCamera fov={45} position={[0, 4, 7]} lookAt={[0, 0.5, 0]} />
49
47
  <HemisphereLight sky={[0.45, 0.5, 0.6]} ground={[0.2, 0.18, 0.15]} />
50
48
  <DirectionalLight
@@ -0,0 +1,86 @@
1
+ // A sky-lit scene: the background is a procedural GLSL sky (a horizon
2
+ // gradient, a sun disc and its glow, written along the view ray), and
3
+ // `scene.bakeBackground()` renders that same fragment into a cube and
4
+ // prefilters it into the roughness chain the scene's environment samples -
5
+ // Godot's sky-to-radiance bake: the sky both draws behind the meshes and
6
+ // lights them, with no light nodes at all. A row of `standard` spheres,
7
+ // metal above, dielectric below, roughness 0 to 1 across: the sharp ones
8
+ // mirror the sun and horizon, the rough ones catch the sky's soft
9
+ // gradient. The sky is a scene param (uSunDir), so the bake sees the same
10
+ // sun the backdrop draws. Drag to look around.
11
+
12
+ import { pct, render } from "@solidrt/core"
13
+ import { glsl } from "@solidrt/core/gpu"
14
+ import { Mesh, OrbitCamera, Scene, sphere, standard } from "@solidrt/3d"
15
+ import type { SceneHandle } from "@solidrt/3d"
16
+ import { normalize } from "@solidrt/3d/math"
17
+ import type { Vec3 } from "@solidrt/3d/math"
18
+
19
+ // Spheres per row, their radius and spacing.
20
+ const COUNT = 6
21
+ const RADIUS = 0.45
22
+ const GAP = 1.15
23
+ // The sun's direction: high and on the camera's side, so its reflection
24
+ // lands on the spheres' upper right - a sharp disc on the mirror, a broad
25
+ // bright highlight on the rough metals (the HDR bake keeps the disc's
26
+ // energy, which an 8-bit probe would clamp to white).
27
+ const SUN: Vec3 = normalize([0.6, 0.5, 0.6])
28
+ // The bake's face edge: 128 is the environment default; the sun disc
29
+ // blurs into its glow at this size, which is what a rough surface wants.
30
+ const BAKE_SIZE = 128
31
+
32
+ // Horizon gradient (zenith blue to a pale horizon, a dark ground), a sun
33
+ // disc with a glow. Linear colors in, the output stage encodes for the
34
+ // screen and leaves them linear for the bake.
35
+ const SKY = glsl`
36
+ uniform vec3 uSunDir;
37
+ // Cosine of the disc's angular radius (about 2 degrees), the glow's
38
+ // falloff exponent and the disc's radiance (well above 1: a low-key
39
+ // sun, real ones are far brighter still).
40
+ const float SUN_DISC = 0.9994;
41
+ const float SUN_GLOW = 24.0;
42
+ const float SUN_RADIANCE = 40.0;
43
+ void main() {
44
+ vec3 d = normalize(vRay);
45
+ vec3 zenith = vec3(0.08, 0.22, 0.55);
46
+ vec3 horizon = vec3(0.65, 0.72, 0.85);
47
+ vec3 ground = vec3(0.05, 0.045, 0.04);
48
+ float h = clamp(d.y, -1.0, 1.0);
49
+ vec3 col = h >= 0.0 ? mix(horizon, zenith, pow(h, 0.6)) : mix(horizon, ground, pow(-h, 0.2));
50
+ float s = max(dot(d, uSunDir), 0.0);
51
+ col += vec3(1.0, 0.85, 0.6) * pow(s, SUN_GLOW) * 0.6;
52
+ if (s > SUN_DISC) col = vec3(1.0, 0.87, 0.67) * SUN_RADIANCE;
53
+ fragColor = outputColor(col, 1.0);
54
+ }
55
+ `
56
+
57
+ function App() {
58
+ let spheres: { position: Vec3; material: ReturnType<typeof standard> }[] = []
59
+ for (let i = 0; i < COUNT; i++) {
60
+ let roughness = i / (COUNT - 1)
61
+ let x = (i - (COUNT - 1) / 2) * GAP
62
+ spheres.push({ position: [x, GAP / 2, 0], material: standard({ color: [0.95, 0.93, 0.88], metalness: 1, roughness }) })
63
+ spheres.push({ position: [x, -GAP / 2, 0], material: standard({ color: [0.8, 0.15, 0.1], metalness: 0, roughness }) })
64
+ }
65
+ let geometry = sphere({ radius: RADIUS, widthSegments: 48, heightSegments: 32 })
66
+ // The ref hands out the scene with its props applied (the background
67
+ // is set), so the bake happens right here, before the first frame.
68
+ let lit = (scene: SceneHandle) => {
69
+ scene.setParams({ uSunDir: SUN })
70
+ scene.setEnvironment({ cube: scene.bakeBackground(BAKE_SIZE) })
71
+ }
72
+ return (
73
+ <window>
74
+ <view width={pct(100)} height={pct(100)}>
75
+ <Scene background={SKY} toneMapping="aces" samples={4} label="sky-lit" ref={lit}>
76
+ <OrbitCamera target={[0, 0, 0]} azimuth={0.3} elevation={0.15} distance={7} />
77
+ {spheres.map(s => (
78
+ <Mesh geometry={geometry} material={s.material} position={s.position} />
79
+ ))}
80
+ </Scene>
81
+ </view>
82
+ </window>
83
+ )
84
+ }
85
+
86
+ render(() => <App />)
@@ -0,0 +1,128 @@
1
+ // A skybox and its reflections: the scene's background sampled from a
2
+ // cube map along each pixel's view ray - `<Scene background={{ cube,
3
+ // intensity?, rotation? }}>`, Three's `scene.background = cubeTexture` -
4
+ // and the same cube as the scene's environment (`environment={{ cube,
5
+ // rotation }}`, Three's `scene.environment`), which every lit material
6
+ // with `reflectivity` mirrors: the chrome sphere sharply, the brass knot
7
+ // blurred by its shininess. The cube map is baked in JS at startup (a
8
+ // horizon gradient, a sun disc and its glow), so the example ships no
9
+ // image assets; a photographed sky is the same createCubeTexture call
10
+ // with six decoded faces in Three's px, nx, py, ny, pz, nz order, or
11
+ // equirectToCube on a panorama. The sky turns slowly and the sun light
12
+ // turns with it: `rotation` is a reactive prop the scene updates in
13
+ // place, no recompile. Drag to look around, wheel to zoom.
14
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
15
+ import { createCubeTexture } from "@solidrt/core/gpu"
16
+ import { box, DirectionalLight, HemisphereLight, lit, Mesh, OrbitCamera, plane, Scene, sphere, torusKnot } from "@solidrt/3d"
17
+ import { normalize } from "@solidrt/3d/math"
18
+ import type { Vec3 } from "@solidrt/3d/math"
19
+
20
+ // Face edge in texels; the sun disc is the finest feature, and mipmaps
21
+ // keep the horizon smooth when it shrinks toward a screen pixel.
22
+ const FACE = 128
23
+ // The sun's direction in the unrotated sky.
24
+ const SUN: Vec3 = normalize([0.6, 0.35, -0.7])
25
+ // Cosine of the disc's angular radius (1.5 degrees).
26
+ const SUN_DISC = Math.cos((1.5 * Math.PI) / 180)
27
+ // Glow falloff exponent around the disc (higher = tighter).
28
+ const SUN_GLOW = 40
29
+ // How fast the sky turns about y, radians per second.
30
+ const TURN_SPEED = 0.06
31
+ // How quickly the ground band darkens below the horizon (per unit of -y).
32
+ const GROUND_FALL = 4
33
+
34
+ type Rgb = [number, number, number]
35
+ const ZENITH: Rgb = [0.16, 0.34, 0.74]
36
+ const HORIZON: Rgb = [0.8, 0.87, 0.95]
37
+ const GROUND: Rgb = [0.3, 0.27, 0.24]
38
+ const SUN_COLOR: Rgb = [1, 0.95, 0.82]
39
+
40
+ let mix = (a: Rgb, b: Rgb, t: number): Rgb => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
41
+
42
+ // The sky's color along a world direction.
43
+ function sky(d: Vec3): Rgb {
44
+ let y = d[1]
45
+ let rgb = y >= 0 ? mix(HORIZON, ZENITH, Math.sqrt(y)) : mix(HORIZON, GROUND, Math.min(1, -y * GROUND_FALL))
46
+ let s = d[0] * SUN[0] + d[1] * SUN[1] + d[2] * SUN[2]
47
+ if (s > 0) {
48
+ // The disc is the glow at full strength: added to the sky it clamps
49
+ // to white, and the warm tint shows in the halo around it.
50
+ let g = s > SUN_DISC ? 1 : Math.pow(s, SUN_GLOW)
51
+ rgb = [rgb[0] + SUN_COLOR[0] * g, rgb[1] + SUN_COLOR[1] * g, rgb[2] + SUN_COLOR[2] * g]
52
+ }
53
+ return [Math.min(1, rgb[0]), Math.min(1, rgb[1]), Math.min(1, rgb[2])]
54
+ }
55
+
56
+ // The world direction whose lookup lands on face i's texel (s, t), t = 0
57
+ // the first row: the GL cube-map table, which is also what the library
58
+ // samples with (no flip) - so the texel holds the sky in that direction.
59
+ function texelDirection(face: number, s: number, t: number): Vec3 {
60
+ let a = 2 * s - 1
61
+ let b = 2 * t - 1
62
+ let d: Vec3 =
63
+ face === 0 ? [1, -b, -a] : face === 1 ? [-1, -b, a] : face === 2 ? [a, 1, b] : face === 3 ? [a, -1, -b] : face === 4 ? [a, -b, 1] : [-a, -b, -1]
64
+ return normalize(d)
65
+ }
66
+
67
+ function bakeSky(): Uint8Array[] {
68
+ let faces: Uint8Array[] = []
69
+ for (let face = 0; face < 6; face++) {
70
+ let px = new Uint8Array(FACE * FACE * 4)
71
+ for (let row = 0; row < FACE; row++) {
72
+ for (let col = 0; col < FACE; col++) {
73
+ let rgb = sky(texelDirection(face, (col + 0.5) / FACE, (row + 0.5) / FACE))
74
+ let i = (row * FACE + col) * 4
75
+ px[i] = Math.round(rgb[0] * 255)
76
+ px[i + 1] = Math.round(rgb[1] * 255)
77
+ px[i + 2] = Math.round(rgb[2] * 255)
78
+ px[i + 3] = 255
79
+ }
80
+ }
81
+ faces.push(px)
82
+ }
83
+ return faces
84
+ }
85
+
86
+ // The sun's direction once the sky has turned by `angle` about y (the same
87
+ // turn a node with rotation [0, angle, 0] applies).
88
+ function turnedSun(angle: number): Vec3 {
89
+ let c = Math.cos(angle)
90
+ let s = Math.sin(angle)
91
+ return [SUN[0] * c + SUN[2] * s, SUN[1], -SUN[0] * s + SUN[2] * c]
92
+ }
93
+
94
+ function App() {
95
+ let cube = createCubeTexture(bakeSky(), FACE, { format: "rgba8-srgb", mipmap: true, label: "sky" })
96
+ let [turn, setTurn] = createSignal(0)
97
+ let last = 0
98
+ onFrame(tick => {
99
+ if (last !== 0) setTurn(turn() + ((tick - last) / 1000) * TURN_SPEED)
100
+ last = tick
101
+ })
102
+ let sun = () => turnedSun(turn())
103
+
104
+ let ground = lit({ color: [0.36, 0.4, 0.3] })
105
+ // Glossy: a blurred reflection (shininess 48) at a dielectric's face-on
106
+ // weight, rising toward the silhouette.
107
+ let brass = lit({ color: [0.85, 0.6, 0.3], specular: 0.6, shininess: 48, reflectivity: 0.15 })
108
+ let stone = lit({ color: [0.6, 0.6, 0.62] })
109
+ // Chrome: the environment itself, sharp.
110
+ let chrome = lit({ color: [1, 1, 1], specular: 1, shininess: 400, reflectivity: 1 })
111
+ return (
112
+ <window>
113
+ <view width={pct(100)} height={pct(100)}>
114
+ <Scene background={{ cube, rotation: turn() }} environment={{ cube, rotation: turn() }} label="skybox-demo">
115
+ <OrbitCamera azimuth={0.4} elevation={0.12} distance={7} />
116
+ <HemisphereLight sky={[0.5, 0.6, 0.8]} ground={[0.3, 0.27, 0.24]} />
117
+ <DirectionalLight direction={[-sun()[0], -sun()[1], -sun()[2]]} color={SUN_COLOR} intensity={0.9} />
118
+ <Mesh geometry={plane({ width: 12, height: 12 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
119
+ <Mesh geometry={torusKnot({ radius: 0.7, tube: 0.22, tubularSegments: 128, radialSegments: 16 })} material={brass} position={[0, 1.2, 0]} />
120
+ <Mesh geometry={box()} material={stone} position={[-2, 0.5, -0.5]} />
121
+ <Mesh geometry={sphere({ radius: 0.6, widthSegments: 48, heightSegments: 32 })} material={chrome} position={[2, 0.6, -0.5]} />
122
+ </Scene>
123
+ </view>
124
+ </window>
125
+ )
126
+ }
127
+
128
+ render(() => <App />)
@@ -9,8 +9,6 @@ import { createSignal, onFrame, pct, render } from "@solidrt/core"
9
9
  import { createTexture } from "@solidrt/core/gpu"
10
10
  import { box, Group, Mesh, PerspectiveCamera, plane, Scene, sprite, Sprite, unlit } from "@solidrt/3d"
11
11
 
12
- const SIZE = 720
13
-
14
12
  // A soft disc, alpha falling off from the center; premultiplied like every
15
13
  // texture the engine samples.
16
14
  function glow(): ReturnType<typeof createTexture> {
@@ -25,7 +23,7 @@ function glow(): ReturnType<typeof createTexture> {
25
23
  data.set([255 * a, 240 * a, 200 * a, 255 * a], (y * n + x) * 4)
26
24
  }
27
25
  }
28
- return createTexture(data, n, n)
26
+ return createTexture(data, n, n, { format: "rgba8-srgb" })
29
27
  }
30
28
 
31
29
  // A cutout tree: green triangle over a brown trunk, transparent elsewhere.
@@ -43,7 +41,7 @@ function tree(): ReturnType<typeof createTexture> {
43
41
  data.set([rgb[0]!, rgb[1]!, rgb[2]!, a], (y * n + x) * 4)
44
42
  }
45
43
  }
46
- return createTexture(data, n, n)
44
+ return createTexture(data, n, n, { format: "rgba8-srgb" })
47
45
  }
48
46
 
49
47
  function App() {
@@ -68,8 +66,8 @@ function App() {
68
66
 
69
67
  return (
70
68
  <window>
71
- <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
72
- <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sprites">
69
+ <view width={pct(100)} height={pct(100)}>
70
+ <Scene clearColor={[0.07, 0.07, 0.1, 1]} label="sprites">
73
71
  <PerspectiveCamera fov={50} position={eye()} lookAt={[0, 0.6, 0]} />
74
72
  <Mesh
75
73
  geometry={plane({ width: 8, height: 8 })}