@solidrt/3d 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +1206 -224
  2. package/README.md +16 -7
  3. package/examples/README.md +88 -9
  4. package/examples/aim.tsx +2 -3
  5. package/examples/cascades.tsx +1 -4
  6. package/examples/collision.tsx +263 -0
  7. package/examples/first-person.tsx +129 -0
  8. package/examples/fleet.tsx +200 -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 +35 -9
  17. package/examples/pick.tsx +145 -37
  18. package/examples/probe.tsx +107 -0
  19. package/examples/scene-background.tsx +2 -4
  20. package/examples/scene-basic.tsx +2 -4
  21. package/examples/scene-views.tsx +68 -48
  22. package/examples/shadows.tsx +2 -4
  23. package/examples/sky-lit.tsx +92 -0
  24. package/examples/skybox.tsx +134 -0
  25. package/examples/sprites.tsx +4 -6
  26. package/examples/standard.tsx +138 -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 +20 -0
  32. package/src/color.ts +32 -0
  33. package/src/components/context.tsx +35 -0
  34. package/src/components/directional-light.tsx +48 -0
  35. package/src/components/first-person-camera.tsx +72 -0
  36. package/src/components/group.tsx +18 -0
  37. package/src/components/hemisphere-light.tsx +24 -0
  38. package/src/components/index.ts +46 -0
  39. package/src/components/instanced-mesh.tsx +105 -0
  40. package/src/components/mesh.tsx +92 -0
  41. package/src/components/node-props.ts +90 -0
  42. package/src/components/orbit-camera.tsx +75 -0
  43. package/src/components/perspective-camera.tsx +32 -0
  44. package/src/components/point-light.tsx +52 -0
  45. package/src/components/record-mesh.tsx +68 -0
  46. package/src/components/scene.tsx +281 -0
  47. package/src/components/spot-light.tsx +65 -0
  48. package/src/components/sprite.tsx +38 -0
  49. package/src/components/view3d.tsx +152 -0
  50. package/src/environment-bake.ts +492 -0
  51. package/src/environment.ts +325 -0
  52. package/src/first-person.ts +258 -0
  53. package/src/geometry.ts +6 -2
  54. package/src/glsl.ts +1343 -82
  55. package/src/gltf.ts +432 -112
  56. package/src/index.ts +61 -9
  57. package/src/input.ts +70 -0
  58. package/src/light.ts +383 -0
  59. package/src/material.ts +667 -232
  60. package/src/math.ts +110 -0
  61. package/src/mesh.ts +728 -0
  62. package/src/mixer.ts +476 -0
  63. package/src/model-file.ts +80 -15
  64. package/src/model.ts +204 -35
  65. package/src/node.ts +579 -0
  66. package/src/orbit.ts +292 -212
  67. package/src/scene-pointer.ts +294 -0
  68. package/src/scene-shadows.ts +471 -0
  69. package/src/scene.ts +1435 -1576
  70. package/tools/environment.ts +69 -0
  71. package/tools/model.ts +4 -3
  72. package/src/components.tsx +0 -420
@@ -1,14 +1,25 @@
1
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
2
+ // camera, plus two `<View3d>`s - a top-down ORTHOGRAPHIC map (`ortho` in
3
+ // the view's `camera`) and a side silhouette drawn with an
4
4
  // `overrideMaterial`. The group spins from ONE signal: each mesh is one
5
5
  // core node whose flush writes its world matrix into every target's entry
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
- // Views die with the scene.
8
+ // A view is a Scene child: it renders its own `<texture>` leaf where it
9
+ // sits, so the two stack in the column beside the scene's leaf, and it
10
+ // dies with the scene.
11
+ //
12
+ // Layers and per-view fog: the marker disc is on layer bit 2, so the
13
+ // main render (mask 1) and the silhouette never draw it - only the map,
14
+ // whose mask admits both bits. The scene is fogged; `fog={null}` on the
15
+ // map keeps the top-down view clear (its fog names are view-owned, so
16
+ // scene-wide fog writes never clobber them).
9
17
  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"
18
+ import { box, circle, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit, View3d } from "@solidrt/3d"
19
+
20
+ // Layer bits: the world everything defaults to, and the map-only markers.
21
+ const WORLD_LAYER = 1
22
+ const MARKER_LAYER = 2
12
23
 
13
24
  const MAIN = 720
14
25
  const SIDE = 360
@@ -20,57 +31,17 @@ function App() {
20
31
  let cube = box()
21
32
  let floor = plane({ width: 6, height: 6, label: "floor" })
22
33
  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
34
+ let marker = circle({ radius: 0.3, label: "marker" })
26
35
 
27
36
  return (
28
37
  <window>
29
- <view width={pct(100)} height={pct(100)} designSize={[MAIN + SIDE, MAIN]}>
38
+ <view width={pct(100)} height={pct(100)} designSize={[MAIN + SIDE, MAIN]} flexDirection="row">
30
39
  <Scene
31
40
  width={MAIN}
32
41
  height={MAIN}
33
42
  clearColor={[0.07, 0.07, 0.1, 1]}
43
+ fog={{ color: [0.07, 0.07, 0.1], near: 2.5, far: 9 }}
34
44
  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
45
  >
75
46
  <PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
76
47
  <HemisphereLight sky={[0.6, 0.7, 0.9]} ground={[0.25, 0.2, 0.15]} intensity={0.6} />
@@ -85,7 +56,56 @@ function App() {
85
56
  scale={[0.5, 1.4, 0.5]}
86
57
  />
87
58
  <Mesh geometry={ball} material={lit({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
59
+ {/* The ball's map marker: drawn by the map view alone. */}
60
+ <Mesh
61
+ geometry={marker}
62
+ material={unlit({ color: [0.95, 0.25, 0.2] })}
63
+ layers={MARKER_LAYER}
64
+ position={[1.1, 2, 0]}
65
+ rotation={[-Math.PI / 2, 0, 0]}
66
+ />
88
67
  </Group>
68
+ {/* The two views' leaves stack beside the scene's: a laid-out
69
+ element among Scene children renders where it sits, and the
70
+ views inside it stay Scene children through context. */}
71
+ <view flexDirection="column" width={SIDE} height={MAIN}>
72
+ {/* The map: straight down from y = 10, world -z toward the top
73
+ of the leaf, seven world units across at any depth. Its mask
74
+ also admits the marker layer, and `fog={null}` keeps it clear
75
+ while the main view fades to the horizon. */}
76
+ <View3d
77
+ width={SIDE}
78
+ height={SIDE}
79
+ clearColor={[0.05, 0.08, 0.06, 1]}
80
+ layers={WORLD_LAYER | MARKER_LAYER}
81
+ fog={null}
82
+ label="map"
83
+ camera={{
84
+ position: [0, 10, 0],
85
+ target: [0, 0, 0],
86
+ up: [0, 0, -1],
87
+ ortho: { left: -3.5, right: 3.5, top: 3.5, bottom: -3.5 },
88
+ near: 0.1,
89
+ far: 20,
90
+ }}
91
+ />
92
+ {/* The silhouette: every mesh drawn flat amber, from the side.
93
+ The floor is edge-on here, a single line at the bottom. */}
94
+ <View3d
95
+ width={SIDE}
96
+ height={SIDE}
97
+ clearColor={[0.1, 0.06, 0.05, 1]}
98
+ overrideMaterial={unlit({ color: [0.95, 0.75, 0.3] })}
99
+ label="silhouette"
100
+ camera={{
101
+ position: [6, 1, 0],
102
+ target: [0, 0.6, 0],
103
+ ortho: { left: -2.4, right: 2.4, top: 2.4, bottom: -2.4 },
104
+ near: 0.1,
105
+ far: 20,
106
+ }}
107
+ />
108
+ </view>
89
109
  </Scene>
90
110
  </view>
91
111
  </window>
@@ -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,92 @@
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 { createInputMap, createPointerFeed, gamepad, pct, render } from "@solidrt/core"
13
+ import { glsl } from "@solidrt/core/gpu"
14
+ import { Mesh, OrbitCamera, orbitActions, orbitBindings, 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
+ // The camera's input: the scene leaf's gestures and any pad, bound to the
59
+ // orbit control's actions - the app wires devices, the component takes
60
+ // the map (ARCHITECTURE.md).
61
+ let pointer = createPointerFeed()
62
+ let input = createInputMap(orbitActions)
63
+ input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
64
+ let spheres: { position: Vec3; material: ReturnType<typeof standard> }[] = []
65
+ for (let i = 0; i < COUNT; i++) {
66
+ let roughness = i / (COUNT - 1)
67
+ let x = (i - (COUNT - 1) / 2) * GAP
68
+ spheres.push({ position: [x, GAP / 2, 0], material: standard({ color: [0.95, 0.93, 0.88], metalness: 1, roughness }) })
69
+ spheres.push({ position: [x, -GAP / 2, 0], material: standard({ color: [0.8, 0.15, 0.1], metalness: 0, roughness }) })
70
+ }
71
+ let geometry = sphere({ radius: RADIUS, widthSegments: 48, heightSegments: 32 })
72
+ // The ref hands out the scene with its props applied (the background
73
+ // is set), so the bake happens right here, before the first frame.
74
+ let lit = (scene: SceneHandle) => {
75
+ scene.setParams({ uSunDir: SUN })
76
+ scene.setEnvironment({ cube: scene.bakeBackground(BAKE_SIZE) })
77
+ }
78
+ return (
79
+ <window>
80
+ <view width={pct(100)} height={pct(100)}>
81
+ <Scene background={SKY} toneMapping="aces" samples={4} label="sky-lit" ref={lit} pointer={pointer}>
82
+ <OrbitCamera input={input} target={[0, 0, 0]} azimuth={0.3} elevation={0.15} distance={7} />
83
+ {spheres.map(s => (
84
+ <Mesh geometry={geometry} material={s.material} position={s.position} />
85
+ ))}
86
+ </Scene>
87
+ </view>
88
+ </window>
89
+ )
90
+ }
91
+
92
+ render(() => <App />)
@@ -0,0 +1,134 @@
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 { createInputMap, createPointerFeed, createSignal, gamepad, onFrame, pct, render } from "@solidrt/core"
15
+ import { createCubeTexture } from "@solidrt/core/gpu"
16
+ import { box, DirectionalLight, HemisphereLight, lit, Mesh, OrbitCamera, orbitActions, orbitBindings, 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
+ // The camera's input: the scene leaf's gestures and any pad, bound to the
96
+ // orbit control's actions - the app wires devices, the component takes
97
+ // the map (ARCHITECTURE.md).
98
+ let pointer = createPointerFeed()
99
+ let input = createInputMap(orbitActions)
100
+ input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
101
+ let cube = createCubeTexture(bakeSky(), FACE, { format: "rgba8-srgb", mipmap: true, label: "sky" })
102
+ let [turn, setTurn] = createSignal(0)
103
+ let last = 0
104
+ onFrame(tick => {
105
+ if (last !== 0) setTurn(turn() + ((tick - last) / 1000) * TURN_SPEED)
106
+ last = tick
107
+ })
108
+ let sun = () => turnedSun(turn())
109
+
110
+ let ground = lit({ color: [0.36, 0.4, 0.3] })
111
+ // Glossy: a blurred reflection (shininess 48) at a dielectric's face-on
112
+ // weight, rising toward the silhouette.
113
+ let brass = lit({ color: [0.85, 0.6, 0.3], specular: 0.6, shininess: 48, reflectivity: 0.15 })
114
+ let stone = lit({ color: [0.6, 0.6, 0.62] })
115
+ // Chrome: the environment itself, sharp.
116
+ let chrome = lit({ color: [1, 1, 1], specular: 1, shininess: 400, reflectivity: 1 })
117
+ return (
118
+ <window>
119
+ <view width={pct(100)} height={pct(100)}>
120
+ <Scene background={{ cube, rotation: turn() }} environment={{ cube, rotation: turn() }} label="skybox-demo" pointer={pointer}>
121
+ <OrbitCamera input={input} azimuth={0.4} elevation={0.12} distance={7} />
122
+ <HemisphereLight sky={[0.5, 0.6, 0.8]} ground={[0.3, 0.27, 0.24]} />
123
+ <DirectionalLight direction={[-sun()[0], -sun()[1], -sun()[2]]} color={SUN_COLOR} intensity={0.9} />
124
+ <Mesh geometry={plane({ width: 12, height: 12 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
125
+ <Mesh geometry={torusKnot({ radius: 0.7, tube: 0.22, tubularSegments: 128, radialSegments: 16 })} material={brass} position={[0, 1.2, 0]} />
126
+ <Mesh geometry={box()} material={stone} position={[-2, 0.5, -0.5]} />
127
+ <Mesh geometry={sphere({ radius: 0.6, widthSegments: 48, heightSegments: 32 })} material={chrome} position={[2, 0.6, -0.5]} />
128
+ </Scene>
129
+ </view>
130
+ </window>
131
+ )
132
+ }
133
+
134
+ 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 })}
@@ -0,0 +1,138 @@
1
+ // The standard material: the sphere grid every PBR engine's docs open
2
+ // with - metalness down the rows, roughness across the columns - under a
3
+ // baked sky as background and environment (the skybox example's bake) and
4
+ // a warm sun casting shadows. What to look for: the bare-metal row IS the
5
+ // environment, sharp at the left and blurred toward the right, tinted by
6
+ // the base color and dark wherever the sky is; the dielectric row keeps
7
+ // its diffuse color and shows the sky as a faint gloss, a mirror highlight
8
+ // at the left that widens to the right and brightens toward every
9
+ // silhouette; the rows between blend the two. standard takes lit's base,
10
+ // maps, cutout, shadow, emissive and fog options - only the Blinn-Phong
11
+ // knobs are gone, replaced by metalness/roughness and their packed maps.
12
+ // Light intensities read as lit's: 1 lights a white matte surface to 1.
13
+ // Drag to look around.
14
+
15
+ import { createInputMap, createPointerFeed, gamepad, pct, render } from "@solidrt/core"
16
+ import { createCubeTexture } from "@solidrt/core/gpu"
17
+ import { DirectionalLight, HemisphereLight, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, sphere, standard } from "@solidrt/3d"
18
+ import { normalize } from "@solidrt/3d/math"
19
+ import type { Vec3 } from "@solidrt/3d/math"
20
+
21
+ // Metalness steps down the rows, roughness across the columns.
22
+ const ROWS = 4
23
+ const COLS = 6
24
+ // Sphere radius and grid pitch, world units.
25
+ const RADIUS = 0.42
26
+ const PITCH = 1.05
27
+ // The spheres' base color, sRGB: a warm red that reads as paint and as metal.
28
+ const COLOR: [number, number, number] = [0.8, 0.25, 0.2]
29
+ // Height of the bottom row's centers above the ground.
30
+ const LIFT = 0.6
31
+ // Face edge of the baked sky in texels.
32
+ const FACE = 128
33
+ // The sun's direction (toward it).
34
+ const SUN: Vec3 = normalize([0.6, 0.45, -0.7])
35
+ // How far out the sun light sits, so its shadow camera covers the grid.
36
+ const SUN_DISTANCE = 12
37
+ // Cosine of the sun disc's angular radius (1.5 degrees).
38
+ const SUN_DISC = Math.cos((1.5 * Math.PI) / 180)
39
+ // Glow falloff exponent around the disc (higher = tighter).
40
+ const SUN_GLOW = 40
41
+ // How quickly the ground band darkens below the horizon (per unit of -y).
42
+ const GROUND_FALL = 4
43
+
44
+ type Rgb = [number, number, number]
45
+ const ZENITH: Rgb = [0.16, 0.34, 0.74]
46
+ const HORIZON: Rgb = [0.8, 0.87, 0.95]
47
+ const GROUND: Rgb = [0.3, 0.27, 0.24]
48
+ const SUN_COLOR: Rgb = [1, 0.95, 0.82]
49
+
50
+ 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]
51
+
52
+ // The sky's color along a world direction.
53
+ function sky(d: Vec3): Rgb {
54
+ let y = d[1]
55
+ let rgb = y >= 0 ? mix(HORIZON, ZENITH, Math.sqrt(y)) : mix(HORIZON, GROUND, Math.min(1, -y * GROUND_FALL))
56
+ let s = d[0] * SUN[0] + d[1] * SUN[1] + d[2] * SUN[2]
57
+ if (s > 0) {
58
+ let g = s > SUN_DISC ? 1 : Math.pow(s, SUN_GLOW)
59
+ rgb = [rgb[0] + SUN_COLOR[0] * g, rgb[1] + SUN_COLOR[1] * g, rgb[2] + SUN_COLOR[2] * g]
60
+ }
61
+ return [Math.min(1, rgb[0]), Math.min(1, rgb[1]), Math.min(1, rgb[2])]
62
+ }
63
+
64
+ // The world direction whose lookup lands on face i's texel (s, t) - the
65
+ // GL cube-map table, no flip (see skybox.tsx).
66
+ function texelDirection(face: number, s: number, t: number): Vec3 {
67
+ let a = 2 * s - 1
68
+ let b = 2 * t - 1
69
+ let d: Vec3 =
70
+ 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]
71
+ return normalize(d)
72
+ }
73
+
74
+ function bakeSky(): Uint8Array[] {
75
+ let faces: Uint8Array[] = []
76
+ for (let face = 0; face < 6; face++) {
77
+ let px = new Uint8Array(FACE * FACE * 4)
78
+ for (let row = 0; row < FACE; row++) {
79
+ for (let col = 0; col < FACE; col++) {
80
+ let rgb = sky(texelDirection(face, (col + 0.5) / FACE, (row + 0.5) / FACE))
81
+ let i = (row * FACE + col) * 4
82
+ px[i] = Math.round(rgb[0] * 255)
83
+ px[i + 1] = Math.round(rgb[1] * 255)
84
+ px[i + 2] = Math.round(rgb[2] * 255)
85
+ px[i + 3] = 255
86
+ }
87
+ }
88
+ faces.push(px)
89
+ }
90
+ return faces
91
+ }
92
+
93
+ function App() {
94
+ // The camera's input: the scene leaf's gestures and any pad, bound to the
95
+ // orbit control's actions - the app wires devices, the component takes
96
+ // the map (ARCHITECTURE.md).
97
+ let pointer = createPointerFeed()
98
+ let input = createInputMap(orbitActions)
99
+ input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
100
+ let cube = createCubeTexture(bakeSky(), FACE, { format: "rgba8-srgb", mipmap: true, label: "sky" })
101
+ let ball = sphere({ radius: RADIUS, widthSegments: 48, heightSegments: 32 })
102
+ let ground = standard({ color: [0.32, 0.33, 0.3], roughness: 0.9 })
103
+ // One material per cell: metalness by row (top row bare metal, bottom
104
+ // row dielectric), roughness by column (mirror at the left).
105
+ let cells: { material: ReturnType<typeof standard>; position: Vec3 }[] = []
106
+ for (let r = 0; r < ROWS; r++) {
107
+ for (let c = 0; c < COLS; c++) {
108
+ cells.push({
109
+ material: standard({ color: COLOR, metalness: 1 - r / (ROWS - 1), roughness: c / (COLS - 1) }),
110
+ position: [(c - (COLS - 1) / 2) * PITCH, LIFT + (ROWS - 1 - r) * PITCH, 0],
111
+ })
112
+ }
113
+ }
114
+ let center: Vec3 = [0, LIFT + ((ROWS - 1) * PITCH) / 2, 0]
115
+ return (
116
+ <window>
117
+ <view width={pct(100)} height={pct(100)}>
118
+ <Scene background={{ cube }} environment={{ cube }} samples={4} label="standard-demo" pointer={pointer}>
119
+ <OrbitCamera input={input} target={center} azimuth={0.25} elevation={0.15} distance={9} />
120
+ <HemisphereLight sky={[0.5, 0.6, 0.8]} ground={[0.3, 0.27, 0.24]} intensity={0.5} />
121
+ <DirectionalLight
122
+ position={[SUN[0] * SUN_DISTANCE, SUN[1] * SUN_DISTANCE, SUN[2] * SUN_DISTANCE]}
123
+ direction={[-SUN[0], -SUN[1], -SUN[2]]}
124
+ color={SUN_COLOR}
125
+ intensity={1}
126
+ castShadow
127
+ />
128
+ <Mesh geometry={plane({ width: 16, height: 16 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
129
+ {cells.map(cell => (
130
+ <Mesh geometry={ball} material={cell.material} position={cell.position} castShadow />
131
+ ))}
132
+ </Scene>
133
+ </view>
134
+ </window>
135
+ )
136
+ }
137
+
138
+ render(() => <App />)
@@ -10,8 +10,6 @@ import { createSignal, onFrame, pct, render } from "@solidrt/core"
10
10
  import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, roundRect, Scene, sweep, tube, unlit } from "@solidrt/3d"
11
11
  import type { SweepPath } from "@solidrt/3d"
12
12
 
13
- const SIZE = 720
14
-
15
13
  function App() {
16
14
  let [spin, setSpin] = createSignal(0)
17
15
  onFrame(tick => setSpin(tick / 4000))
@@ -39,8 +37,8 @@ function App() {
39
37
 
40
38
  return (
41
39
  <window>
42
- <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
43
- <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sweep-paths">
40
+ <view width={pct(100)} height={pct(100)}>
41
+ <Scene clearColor={[0.07, 0.07, 0.1, 1]} label="sweep-paths">
44
42
  <PerspectiveCamera fov={55} position={[0, 1.9, 3.9]} lookAt={[0, 0.35, 0]} />
45
43
  <HemisphereLight sky={[0.45, 0.45, 0.45]} ground={[0.22, 0.22, 0.22]} />
46
44
  <DirectionalLight direction={[-0.5, -0.8, -0.4]} intensity={0.8} />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -18,7 +18,7 @@
18
18
  "AGENTS.md"
19
19
  ],
20
20
  "peerDependencies": {
21
- "@solidjs/signals": "2.0.0-rc.4",
22
- "@solidrt/core": "0.0.55"
21
+ "@solidjs/signals": "2.0.0-rc.6",
22
+ "@solidrt/core": "0.0.57"
23
23
  }
24
24
  }