@solidrt/3d 0.0.57 → 0.0.59

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 +1479 -678
  2. package/README.md +4 -2
  3. package/examples/README.md +56 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +129 -0
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +187 -22
  51. package/src/geometry.ts +1718 -203
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +457 -154
  54. package/src/index.ts +32 -21
  55. package/src/input.ts +11 -2
  56. package/src/light.ts +7 -8
  57. package/src/lod.ts +148 -0
  58. package/src/material.ts +322 -190
  59. package/src/mesh.ts +492 -259
  60. package/src/mixer.ts +27 -14
  61. package/src/model-file.ts +54 -15
  62. package/src/model.ts +151 -54
  63. package/src/motion.ts +17 -0
  64. package/src/node.ts +423 -23
  65. package/src/orbit.ts +378 -116
  66. package/src/profile.ts +4 -25
  67. package/src/resolve.ts +181 -0
  68. package/src/scene-shadows.ts +94 -93
  69. package/src/scene.ts +608 -185
  70. package/src/skeleton.ts +211 -0
  71. package/src/sweep.ts +27 -8
  72. package/tools/model.ts +3 -1
@@ -0,0 +1,118 @@
1
+ // Normals as data. Three cylinders from ONE triangle soup (a cylinder
2
+ // split by toNonIndexed, every face its own vertices - the shape an STL
3
+ // or a hand-written face list arrives in) shaded by withNormals at three
4
+ // crease angles: 0 is flat, the default 60 finds the rims and smooths the
5
+ // side, 180 blends the rims away. The result is indexed again -
6
+ // mergeVertices welds the corners back wherever a crease or a seam does
7
+ // not need them apart - so each costs what the generator's own cylinder
8
+ // costs. The sphere is the CPU deformation loop: fillAttribute moves its
9
+ // positions, computeVertexNormals recomputes its normals in place (the
10
+ // math runs in the core, one call), updateVertices re-uploads. Its normal
11
+ // lines are refreshed the same way, in place, from the sphere's data. The
12
+ // loop is here to be seen; a ripple like this one belongs in a vertex
13
+ // shader in a real app, where the CPU touches no vertex at all - the loop
14
+ // is for data that genuinely changes on the CPU.
15
+ // normalsHelper draws every vertex normal as a line: a crease is a fan of
16
+ // lines from one position, a smooth vertex one line. Space or the
17
+ // `helpers` debug command ({ on?: boolean }) toggles them.
18
+
19
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
20
+ import { registerDebug } from "srt:dev"
21
+ import { computeVertexNormals, cylinder, DirectionalLight, fillAttribute, geometryAttribute, geometryVertexCount, Group, HemisphereLight, phong, Mesh, normalsHelper, PerspectiveCamera, Scene, sphere, toNonIndexed, unlit, updateVertices, withNormals } from "@solidrt/3d"
22
+
23
+ // The crease angles in degrees: flat, the default, everything smoothed.
24
+ const ANGLES = [0, 60, 180] as const
25
+ // Spacing between the three cylinders along x.
26
+ const SPACING = 2.4
27
+ // Tilt of the cylinders toward the camera, so their top rims show.
28
+ const TILT = 0.5
29
+ // Normal line length, a fraction of the unit shapes.
30
+ const HELPER_SIZE = 0.2
31
+ // Facets around the cylinders: few enough that flat shading reads as facets.
32
+ const RADIAL_SEGMENTS = 16
33
+ // The sphere ripple: radial amplitude, waves along y, radians per second.
34
+ const RIPPLE_AMPLITUDE = 0.12
35
+ const RIPPLE_WAVES = 5
36
+ const RIPPLE_SPEED = 2
37
+ const SHADE_COLOR: [number, number, number] = [0.85, 0.55, 0.3]
38
+ const SPHERE_COLOR: [number, number, number] = [0.35, 0.6, 0.85]
39
+
40
+ function App() {
41
+ let [t, setT] = createSignal(0)
42
+ let [helpers, setHelpers] = createSignal(true)
43
+ let toggle = (): void => {
44
+ setHelpers(v => !v)
45
+ }
46
+ registerDebug("helpers", (args?: Record<string, unknown>) => {
47
+ if (typeof args?.on === "boolean") setHelpers(args.on)
48
+ else toggle()
49
+ })
50
+
51
+ let soup = toNonIndexed(cylinder({ radialSegments: RADIAL_SEGMENTS }), "soup")
52
+ let shaded = ANGLES.map(angle => withNormals(soup, angle, "cylinder-" + angle))
53
+ let shade = phong({ color: SHADE_COLOR, specular: 0.5, shininess: 40 })
54
+ let lines = unlit({ vertexColors: true })
55
+
56
+ // The rippling sphere. Base positions are kept aside: fillAttribute's
57
+ // callback sees the CURRENT position, and the ripple is a function of
58
+ // the rest shape.
59
+ let ball = sphere({ label: "ripple" })
60
+ let count = geometryVertexCount(ball, "normals example")
61
+ let pos = geometryAttribute(ball, "aPos")!
62
+ let nrm = geometryAttribute(ball, "aNormal")!
63
+ let base = new Float32Array(count * 3)
64
+ for (let i = 0; i < count; i++) {
65
+ base[i * 3] = pos.get(i, 0)
66
+ base[i * 3 + 1] = pos.get(i, 1)
67
+ base[i * 3 + 2] = pos.get(i, 2)
68
+ }
69
+ let ballLines = normalsHelper(ball, { size: HELPER_SIZE })
70
+ onFrame(tick => {
71
+ setT(tick / 1000)
72
+ let phase = (tick / 1000) * RIPPLE_SPEED
73
+ fillAttribute(ball, "aPos", i => {
74
+ let x = base[i * 3]!, y = base[i * 3 + 1]!, z = base[i * 3 + 2]!
75
+ let r = 1 + RIPPLE_AMPLITUDE * Math.sin(y * RIPPLE_WAVES * Math.PI + phase)
76
+ return [x * r, y * r, z * r]
77
+ })
78
+ computeVertexNormals(ball)
79
+ updateVertices(ball)
80
+ // The helper's line ends follow: vertex 2v is the sphere's vertex v,
81
+ // 2v + 1 its tip along the recomputed normal.
82
+ fillAttribute(ballLines, "aPos", i => {
83
+ let v = i >> 1
84
+ let x = pos.get(v, 0), y = pos.get(v, 1), z = pos.get(v, 2)
85
+ if ((i & 1) === 0) return [x, y, z]
86
+ return [x + nrm.get(v, 0) * HELPER_SIZE, y + nrm.get(v, 1) * HELPER_SIZE, z + nrm.get(v, 2) * HELPER_SIZE]
87
+ })
88
+ updateVertices(ballLines)
89
+ })
90
+
91
+ return (
92
+ <window
93
+ onKeyDown={e => {
94
+ if (e.key === " ") toggle()
95
+ }}
96
+ >
97
+ <view width={pct(100)} height={pct(100)}>
98
+ <Scene clearColor={[0.1, 0.11, 0.14, 1]} samples={4} label="normals">
99
+ <PerspectiveCamera fov={40} position={[0, 1.6, 8.5]} lookAt={[0, 0, 0]} />
100
+ <HemisphereLight sky={[0.35, 0.38, 0.45]} ground={[0.12, 0.1, 0.08]} />
101
+ <DirectionalLight direction={[0.4, -0.7, -0.6]} color={[1, 0.95, 0.85]} intensity={0.9} />
102
+ {shaded.map((geometry, i) => (
103
+ <Group position={[(i - 1) * SPACING, 1.1, 0]} rotation={[TILT, t() / 3, 0]}>
104
+ <Mesh geometry={geometry} material={shade} />
105
+ <Mesh geometry={normalsHelper(geometry, { size: HELPER_SIZE })} material={lines} visible={helpers()} />
106
+ </Group>
107
+ ))}
108
+ <Group position={[0, -1.3, 0]}>
109
+ <Mesh geometry={ball} material={phong({ color: SPHERE_COLOR, specular: 0.4, shininess: 30 })} />
110
+ <Mesh geometry={ballLines} material={lines} visible={helpers()} />
111
+ </Group>
112
+ </Scene>
113
+ </view>
114
+ </window>
115
+ )
116
+ }
117
+
118
+ render(() => <App />)
@@ -1,4 +1,4 @@
1
- // The lit material and the scene's lights. lit() is the standard look -
1
+ // The Blinn-Phong material and the scene's lights. phong() is the stock lit look -
2
2
  // hemisphere ambient, the scene's directional lights, Lambert diffuse,
3
3
  // optional Blinn-Phong highlight - with the same color/map/transparent
4
4
  // options as unlit. Lights are NODES: <HemisphereLight> plus up to four
@@ -8,11 +8,11 @@
8
8
  // uniform write. The three boxes share one checker map: the
9
9
  // UV-mapped one stretches it per face (every generator emits 0..1 UVs),
10
10
  // the two triplanar ones tile it at one world density whatever their
11
- // size - the reason triplanar is an option on lit.
11
+ // size - the reason triplanar is an option on phong.
12
12
 
13
13
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
14
14
  import { createTexture } from "@solidrt/core/gpu"
15
- import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, Scene, sphere, torusKnot, plane } from "@solidrt/3d"
15
+ import { box, DirectionalLight, Group, HemisphereLight, phong, Mesh, PerspectiveCamera, Scene, sphere, torusKnot, plane } from "@solidrt/3d"
16
16
 
17
17
  function checker(): ReturnType<typeof createTexture> {
18
18
  let n = 64
@@ -31,16 +31,16 @@ function App() {
31
31
  onFrame(tick => setT(tick / 1000))
32
32
 
33
33
  let map = checker()
34
- let uvMapped = lit({ map })
35
- let tiled = lit({ map, triplanar: 2 })
36
- let glossy = lit({ color: [0.85, 0.3, 0.25], specular: 0.6, shininess: 60 })
37
- let matte = lit({ color: [0.3, 0.55, 0.85] })
38
- let glass = lit({ color: [0.9, 0.95, 1, 0.35], specular: 1, shininess: 120, transparent: true })
34
+ let uvMapped = phong({ map })
35
+ let tiled = phong({ map, triplanar: 2 })
36
+ let glossy = phong({ color: [0.85, 0.3, 0.25], specular: 0.6, shininess: 60 })
37
+ let matte = phong({ color: [0.3, 0.55, 0.85] })
38
+ let glass = phong({ color: [0.9, 0.95, 1, 0.35], specular: 1, shininess: 120, transparent: true })
39
39
 
40
40
  return (
41
41
  <window>
42
42
  <view width={pct(100)} height={pct(100)}>
43
- <Scene clearColor={[0.07, 0.07, 0.1, 1]} samples={4} label="lit">
43
+ <Scene clearColor={[0.07, 0.07, 0.1, 1]} samples={4} label="phong">
44
44
  <PerspectiveCamera fov={50} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
45
45
  <HemisphereLight sky={[0.35, 0.38, 0.45]} ground={[0.12, 0.1, 0.08]} />
46
46
  <Group rotation={[0, t(), 0]}>
@@ -0,0 +1,61 @@
1
+ // The polyhedron family beside the lat/long sphere. Back row, flat-shaded
2
+ // (detail 0 keeps face normals): tetrahedron, octahedron, dodecahedron and
3
+ // icosahedron turning under the lights, so the facets catch the key one
4
+ // after another. Middle row, the icosahedron at detail 1, 2 and 3: the
5
+ // same builder, now a sphere of uniform triangles with radial normals -
6
+ // the icosphere. Front row, the two triangulations as wireframes at a
7
+ // matching triangle count: sphere({ widthSegments: 12, heightSegments: 8 })
8
+ // bunches slivers at its poles where icosahedron({ detail: 2 }) spreads
9
+ // its triangles evenly, which is what makes the icosphere the better
10
+ // sphere for flat shading and displacement. The textured sphere stays
11
+ // sphere(): the polyhedra's spherical UVs stretch toward the poles.
12
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
13
+ import { DirectionalLight, dodecahedron, Group, HemisphereLight, icosahedron, phong, Mesh, octahedron, PerspectiveCamera, plane, Scene, sphere, tetrahedron, unlit, wireframeGeometry } from "@solidrt/3d"
14
+
15
+ // One circumsphere radius for every solid, so the rows read at one scale.
16
+ const RADIUS = 0.45
17
+ // Row depths, back to front, and the column pitch.
18
+ const BACK = -2.1
19
+ const MIDDLE = 0
20
+ const FRONT = 2
21
+ const PITCH = 1.6
22
+
23
+ function App() {
24
+ let [t, setT] = createSignal(0)
25
+ onFrame(tick => setT(tick / 1000))
26
+
27
+ let red = phong({ color: [0.85, 0.3, 0.25] })
28
+ let amber = phong({ color: [0.9, 0.7, 0.3] })
29
+ let green = phong({ color: [0.4, 0.75, 0.4] })
30
+ let blue = phong({ color: [0.35, 0.55, 0.9] })
31
+ let ivory = phong({ color: [0.85, 0.82, 0.75], specular: 0.4, shininess: 40 })
32
+ let wire = unlit({ color: [0.9, 0.9, 0.95] })
33
+ let floor = phong({ color: [0.16, 0.17, 0.22] })
34
+
35
+ return (
36
+ <window>
37
+ <view width={pct(100)} height={pct(100)}>
38
+ <Scene clearColor={[0.07, 0.07, 0.1, 1]} samples={4} label="polyhedra">
39
+ <PerspectiveCamera fov={45} position={[0, 4.6, 6.8]} lookAt={[0, 0.1, 0]} />
40
+ <HemisphereLight sky={[0.35, 0.38, 0.45]} ground={[0.12, 0.1, 0.08]} />
41
+ <Group rotation={[0, t() / 3, 0]}>
42
+ <DirectionalLight direction={[0.7, -0.8, 0.2]} color={[1, 0.92, 0.8]} intensity={0.9} />
43
+ </Group>
44
+ <DirectionalLight direction={[-0.6, -0.3, -0.5]} color={[0.4, 0.5, 0.8]} intensity={0.35} />
45
+ <Mesh geometry={plane({ width: 10, height: 8 })} material={floor} rotation={[-Math.PI / 2, 0, 0]} />
46
+ <Mesh geometry={tetrahedron({ radius: RADIUS })} material={red} position={[-1.5 * PITCH, RADIUS, BACK]} rotation={[0, t(), 0]} />
47
+ <Mesh geometry={octahedron({ radius: RADIUS })} material={amber} position={[-0.5 * PITCH, RADIUS, BACK]} rotation={[0, t(), 0]} />
48
+ <Mesh geometry={dodecahedron({ radius: RADIUS })} material={green} position={[0.5 * PITCH, RADIUS, BACK]} rotation={[0, t(), 0]} />
49
+ <Mesh geometry={icosahedron({ radius: RADIUS })} material={blue} position={[1.5 * PITCH, RADIUS, BACK]} rotation={[0, t(), 0]} />
50
+ <Mesh geometry={icosahedron({ radius: RADIUS, detail: 1 })} material={ivory} position={[-PITCH, RADIUS, MIDDLE]} />
51
+ <Mesh geometry={icosahedron({ radius: RADIUS, detail: 2 })} material={ivory} position={[0, RADIUS, MIDDLE]} />
52
+ <Mesh geometry={icosahedron({ radius: RADIUS, detail: 3 })} material={ivory} position={[PITCH, RADIUS, MIDDLE]} />
53
+ <Mesh geometry={wireframeGeometry(sphere({ radius: RADIUS, widthSegments: 12, heightSegments: 8 }))} material={wire} position={[-0.5 * PITCH, RADIUS, FRONT]} rotation={[0, t() / 2, 0]} />
54
+ <Mesh geometry={wireframeGeometry(icosahedron({ radius: RADIUS, detail: 2 }))} material={wire} position={[0.5 * PITCH, RADIUS, FRONT]} rotation={[0, t() / 2, 0]} />
55
+ </Scene>
56
+ </view>
57
+ </window>
58
+ )
59
+ }
60
+
61
+ render(() => <App />)
@@ -0,0 +1,70 @@
1
+ // Tiled views into one atlas, resolved once by the app. Four `<View3d>`
2
+ // render `into` an app-owned draw target (one pass for all four, the
3
+ // atlas carrying depth and the buffer format), each from its own camera.
4
+ // A tile holds LINEAR light like every buffer, so the atlas is displayed
5
+ // through a resolve pass of the app's own: `resolveFragment()` (the
6
+ // stock resolve source with `uScene` and the RESOLVE set declared) over a
7
+ // `createShaderTexture` sampling the atlas, `resolveParams` writing its
8
+ // exposure and tone mapping. The scene's own target draws nothing here
9
+ // (layer mask 0, no leaf): the tiles are the picture. Godot's SubViewport
10
+ // atlas and Unity's camera stacking into one render texture, resolved
11
+ // where the runtime displays it.
12
+
13
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
14
+ import { createDrawTarget, createShaderTexture } from "@solidrt/core/gpu"
15
+ import { box, bufferFormat, cone, DirectionalLight, Group, HemisphereLight, phong, Mesh, plane, resolveParams, Scene, sphere, View3d } from "@solidrt/3d"
16
+ import { resolveFragment } from "@solidrt/3d/glsl"
17
+
18
+ // The atlas: two by two tiles.
19
+ const TILE_W = 480
20
+ const TILE_H = 270
21
+ const ATLAS_W = TILE_W * 2
22
+ const ATLAS_H = TILE_H * 2
23
+ // Each tile's backdrop: a view clears its own rectangle, so the color
24
+ // goes on the views (an sRGB color like a scene's clearColor, decoded by
25
+ // the library), not on the atlas.
26
+ const CLEAR: [number, number, number, number] = [0.12, 0.14, 0.2, 1]
27
+ // Where the four cameras stand: a quarter turn apart around the group.
28
+ const ORBIT_RADIUS = 6
29
+ const ORBIT_HEIGHT = 2.5
30
+
31
+ function App() {
32
+ let [spin, setSpin] = createSignal(0)
33
+ onFrame(tick => setSpin(tick / 2000))
34
+ let atlas = createDrawTarget(ATLAS_W, ATLAS_H, null, { depth: true, format: bufferFormat(), label: "atlas" })
35
+ // The app's resolve: the whole atlas in one pass, the display pixels.
36
+ let resolved = createShaderTexture(resolveFragment(), ATLAS_W, ATLAS_H, resolveParams({ toneMapping: "aces" }), {
37
+ textures: { uScene: atlas },
38
+ label: "atlas-resolve",
39
+ })
40
+ let floor = plane({ width: 8, height: 8 })
41
+ let tiles = [0, 1, 2, 3].map(i => {
42
+ let angle = (i * Math.PI) / 2
43
+ return {
44
+ x: (i % 2) * TILE_W,
45
+ y: Math.floor(i / 2) * TILE_H,
46
+ camera: { position: [Math.sin(angle) * ORBIT_RADIUS, ORBIT_HEIGHT, Math.cos(angle) * ORBIT_RADIUS] as [number, number, number], target: [0, 0.5, 0] as [number, number, number] },
47
+ }
48
+ })
49
+ return (
50
+ <window>
51
+ <view width={pct(100)} height={pct(100)} designSize={[ATLAS_W, ATLAS_H]}>
52
+ <Scene width={TILE_W} height={TILE_H} layers={0} label="atlas-scene" output={() => <texture src={resolved} width={ATLAS_W} height={ATLAS_H} />}>
53
+ <HemisphereLight sky={[0.6, 0.7, 0.9]} ground={[0.25, 0.2, 0.15]} intensity={0.7} />
54
+ <DirectionalLight direction={[-1, -2, -1]} intensity={1.2} />
55
+ <Mesh geometry={floor} material={phong({ color: [0.55, 0.55, 0.5] })} rotation={[-Math.PI / 2, 0, 0]} />
56
+ <Group rotation={[0, spin(), 0]}>
57
+ <Mesh geometry={box()} material={phong({ color: [0.85, 0.3, 0.3] })} position={[1.2, 0.5, 0]} />
58
+ <Mesh geometry={sphere({ radius: 0.5 })} material={phong({ color: [0.3, 0.6, 0.9], specular: 0.5, shininess: 40 })} position={[-1.2, 0.5, 0]} />
59
+ <Mesh geometry={cone({ radius: 0.5, height: 1.2 })} material={phong({ color: [0.9, 0.8, 0.3] })} position={[0, 0.6, 1.4]} />
60
+ </Group>
61
+ {tiles.map(t => (
62
+ <View3d width={TILE_W} height={TILE_H} into={atlas} x={t.x} y={t.y} clearColor={CLEAR} camera={t.camera} output={() => null} />
63
+ ))}
64
+ </Scene>
65
+ </view>
66
+ </window>
67
+ )
68
+ }
69
+
70
+ render(() => <App />)
@@ -15,7 +15,7 @@
15
15
  // map keeps the top-down view clear (its fog names are view-owned, so
16
16
  // scene-wide fog writes never clobber them).
17
17
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
18
- import { box, circle, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit, View3d } from "@solidrt/3d"
18
+ import { box, circle, DirectionalLight, Group, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit, View3d } from "@solidrt/3d"
19
19
 
20
20
  // Layer bits: the world everything defaults to, and the map-only markers.
21
21
  const WORLD_LAYER = 1
@@ -46,16 +46,16 @@ function App() {
46
46
  <PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
47
47
  <HemisphereLight sky={[0.6, 0.7, 0.9]} ground={[0.25, 0.2, 0.15]} intensity={0.6} />
48
48
  <DirectionalLight direction={[-0.5, -1, -0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
49
- <Mesh geometry={floor} material={lit({ color: [0.3, 0.32, 0.38] })} rotation={[-Math.PI / 2, 0, 0]} />
49
+ <Mesh geometry={floor} material={phong({ color: [0.3, 0.32, 0.38] })} rotation={[-Math.PI / 2, 0, 0]} />
50
50
  <Group rotation={[0, spin(), 0]}>
51
- <Mesh geometry={cube} material={lit({ color: [0.85, 0.3, 0.3] })} position={[0, 0.5, 0]} />
51
+ <Mesh geometry={cube} material={phong({ color: [0.85, 0.3, 0.3] })} position={[0, 0.5, 0]} />
52
52
  <Mesh
53
53
  geometry={cube}
54
- material={lit({ color: [0.9, 0.8, 0.35] })}
54
+ material={phong({ color: [0.9, 0.8, 0.35] })}
55
55
  position={[-1.1, 0.7, 0]}
56
56
  scale={[0.5, 1.4, 0.5]}
57
57
  />
58
- <Mesh geometry={ball} material={lit({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
58
+ <Mesh geometry={ball} material={phong({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
59
59
  {/* The ball's map marker: drawn by the map view alone. */}
60
60
  <Mesh
61
61
  geometry={marker}
@@ -1,15 +1,15 @@
1
1
  // Directional shadow maps: three `castShadow` lights, `castShadow` meshes
2
- // and `lit` materials, which receive shadows by default. The warm sun
2
+ // and `phong` materials, which receive shadows by default. The warm sun
3
3
  // swings through its arc from onFrame - ONE setTransform on the light
4
4
  // node per frame; the scene re-places its shadow camera from the light's
5
5
  // world matrix, so its shadows sweep across the ground while the casters
6
6
  // turn inside their group. A cool fill from the opposite side and a low
7
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
8
+ // as the sun moves (each casting light claims its tiles of the scene's
9
+ // atlas from the MAX_SHADOW_MAPS budget). Three's vocabulary throughout: `castShadow` on the
10
10
  // light and on meshes, `shadow.mapSize/bias/normalBias/camera` on the
11
11
  // light; the one divergence is that opting OUT of receiving is a material
12
- // option (`lit({ receiveShadow: false })`, Godot's split), because the
12
+ // option (`phong({ receiveShadow: false })`, Godot's split), because the
13
13
  // material picks the program (like vertexColors and triplanar).
14
14
  //
15
15
  // Placement matters for a casting light: its shadow camera sits at the
@@ -18,7 +18,7 @@
18
18
  // above it). The light frustum is `shadow.camera`, +-5 world units by
19
19
  // default - anything outside it is lit.
20
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"
21
+ import { box, cylinder, DirectionalLight, Group, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene, setTransform, sphere, torusKnot } from "@solidrt/3d"
22
22
  import type { DirectionalLightNode } from "@solidrt/3d"
23
23
 
24
24
  function App() {
@@ -34,10 +34,10 @@ function App() {
34
34
  setTransform(sun, { position: [Math.sin(a) * 6, Math.cos(a) * 6, 2], rotation: [0.32, 0, -a] })
35
35
  })
36
36
 
37
- let ground = lit({ color: [0.55, 0.58, 0.6] })
38
- let red = lit({ color: [0.85, 0.3, 0.25] })
39
- let blue = lit({ color: [0.3, 0.55, 0.85] })
40
- let gold = lit({ color: [0.9, 0.75, 0.3], specular: 0.5, shininess: 40 })
37
+ let ground = phong({ color: [0.55, 0.58, 0.6] })
38
+ let red = phong({ color: [0.85, 0.3, 0.25] })
39
+ let blue = phong({ color: [0.3, 0.55, 0.85] })
40
+ let gold = phong({ color: [0.9, 0.75, 0.3], specular: 0.5, shininess: 40 })
41
41
 
42
42
  return (
43
43
  <window>
@@ -30,8 +30,8 @@ const SUN: Vec3 = normalize([0.6, 0.5, 0.6])
30
30
  const BAKE_SIZE = 128
31
31
 
32
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.
33
+ // disc with a glow, written as linear light: the scene's resolve exposes,
34
+ // tone maps and encodes it for the screen, and the bake stores it as is.
35
35
  const SKY = glsl`
36
36
  uniform vec3 uSunDir;
37
37
  // Cosine of the disc's angular radius (about 2 degrees), the glow's
@@ -50,7 +50,7 @@ const SKY = glsl`
50
50
  float s = max(dot(d, uSunDir), 0.0);
51
51
  col += vec3(1.0, 0.85, 0.6) * pow(s, SUN_GLOW) * 0.6;
52
52
  if (s > SUN_DISC) col = vec3(1.0, 0.87, 0.67) * SUN_RADIANCE;
53
- fragColor = outputColor(col, 1.0);
53
+ fragColor = vec4(col, 1.0);
54
54
  }
55
55
  `
56
56
 
@@ -13,7 +13,7 @@
13
13
  // place, no recompile. Drag to look around, wheel to zoom.
14
14
  import { createInputMap, createPointerFeed, createSignal, gamepad, onFrame, pct, render } from "@solidrt/core"
15
15
  import { createCubeTexture } from "@solidrt/core/gpu"
16
- import { box, DirectionalLight, HemisphereLight, lit, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, sphere, torusKnot } from "@solidrt/3d"
16
+ import { box, DirectionalLight, HemisphereLight, phong, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, sphere, torusKnot } from "@solidrt/3d"
17
17
  import { normalize } from "@solidrt/3d/math"
18
18
  import type { Vec3 } from "@solidrt/3d/math"
19
19
 
@@ -107,13 +107,13 @@ function App() {
107
107
  })
108
108
  let sun = () => turnedSun(turn())
109
109
 
110
- let ground = lit({ color: [0.36, 0.4, 0.3] })
110
+ let ground = phong({ color: [0.36, 0.4, 0.3] })
111
111
  // Glossy: a blurred reflection (shininess 48) at a dielectric's face-on
112
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] })
113
+ let brass = phong({ color: [0.85, 0.6, 0.3], specular: 0.6, shininess: 48, reflectivity: 0.15 })
114
+ let stone = phong({ color: [0.6, 0.6, 0.62] })
115
115
  // Chrome: the environment itself, sharp.
116
- let chrome = lit({ color: [1, 1, 1], specular: 1, shininess: 400, reflectivity: 1 })
116
+ let chrome = phong({ color: [1, 1, 1], specular: 1, shininess: 400, reflectivity: 1 })
117
117
  return (
118
118
  <window>
119
119
  <view width={pct(100)} height={pct(100)}>
@@ -6,10 +6,10 @@
6
6
  // the base color and dark wherever the sky is; the dielectric row keeps
7
7
  // its diffuse color and shows the sky as a faint gloss, a mirror highlight
8
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,
9
+ // silhouette; the rows between blend the two. standard takes phong's base,
10
10
  // maps, cutout, shadow, emissive and fog options - only the Blinn-Phong
11
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.
12
+ // Light intensities read as phong's: 1 lights a white matte surface to 1.
13
13
  // Drag to look around.
14
14
 
15
15
  import { createInputMap, createPointerFeed, gamepad, pct, render } from "@solidrt/core"
@@ -0,0 +1,122 @@
1
+ // Vertex streams and the draw range: a sphere whose surface heaves every
2
+ // frame without its positions, normals or uvs ever re-uploading. The
3
+ // displacement rides in a stream of its own - `withAttribute(..., {
4
+ // stream: 1 })` opens a second vertex buffer holding one float per vertex
5
+ // - and each frame writes that stream through the attribute accessor and
6
+ // re-uploads it alone with updateVertices, Three's `needsUpdate` for one
7
+ // BufferAttribute. The static channels stay in the buffer they were
8
+ // uploaded to once; the pipeline reads both buffers.
9
+ //
10
+ // The wireframe over the same geometry shares every stream (and every GPU
11
+ // upload) with the surface, so the one update moves both meshes.
12
+ //
13
+ // setDrawRange reveals the sphere: the mesh draws a growing slice of its
14
+ // index list over the first seconds, then the whole of it - the same
15
+ // buffers, only the range moving.
16
+ import { onFrame, pct, render } from "@solidrt/core"
17
+ import { glsl } from "@solidrt/core/gpu"
18
+ import {
19
+ DirectionalLight,
20
+ geometryAttribute,
21
+ geometryVertexCount,
22
+ HemisphereLight,
23
+ Mesh,
24
+ PerspectiveCamera,
25
+ Scene,
26
+ setDrawRange,
27
+ shaderMaterialClass,
28
+ sphere,
29
+ updateVertices,
30
+ wireframeGeometry,
31
+ withAttribute,
32
+ } from "@solidrt/3d"
33
+ import type { MeshNode } from "@solidrt/3d"
34
+ import { phongFragment } from "@solidrt/3d/glsl"
35
+
36
+ // The lit varyings from a position pushed along its normal by the
37
+ // per-vertex aWave, which the pipeline fetches from the second stream.
38
+ const WAVE_VERTEX = glsl`
39
+ in vec3 aPos;
40
+ in vec3 aNormal;
41
+ in vec2 aUV;
42
+ in float aWave;
43
+ out vec3 vWorldPos;
44
+ out vec3 vNormal;
45
+ out vec2 vUv;
46
+ uniform mat4 uModel;
47
+ uniform mat4 uViewProj;
48
+ uniform mat4 uNormal;
49
+
50
+ void main() {
51
+ vec4 world = uModel * vec4(aPos + aNormal * aWave, 1.0);
52
+ gl_Position = uViewProj * world;
53
+ vWorldPos = world.xyz;
54
+ vNormal = mat3(uNormal) * aNormal;
55
+ vUv = aUV;
56
+ }
57
+ `
58
+
59
+ // The same displacement for the wireframe, drawn flat.
60
+ const WAVE_LINE_VERTEX = glsl`
61
+ in vec3 aPos;
62
+ in vec3 aNormal;
63
+ in float aWave;
64
+ uniform mat4 uModel;
65
+ uniform mat4 uViewProj;
66
+
67
+ void main() {
68
+ gl_Position = uViewProj * uModel * vec4(aPos + aNormal * (aWave + 0.004), 1.0);
69
+ }
70
+ `
71
+
72
+ const LINE_FRAGMENT = glsl`
73
+ void main() {
74
+ fragColor = vec4(0.12, 0.16, 0.2, 1.0);
75
+ }
76
+ `
77
+
78
+ // Displacement amplitude in model units, and the seconds the reveal takes.
79
+ const WAVE_HEIGHT = 0.06
80
+ const REVEAL_SECONDS = 3
81
+
82
+ let surfaceLook = shaderMaterialClass({ vertex: WAVE_VERTEX, fragment: phongFragment(), label: "wave-surface" })
83
+ let lineLook = shaderMaterialClass({ vertex: WAVE_LINE_VERTEX, fragment: LINE_FRAGMENT, label: "wave-lines" })
84
+
85
+ function App() {
86
+ // The sphere with its wave channel in stream 1: stream 0 keeps the
87
+ // generator's positions, normals and uvs untouched.
88
+ let ball = withAttribute(sphere({ radius: 1, widthSegments: 64, heightSegments: 40, label: "ball" }), { name: "aWave", format: "float32" }, () => [0], { stream: 1 })
89
+ let wave = geometryAttribute(ball, "aWave")!
90
+ let pos = geometryAttribute(ball, "aPos")!
91
+ let count = geometryVertexCount(ball, "streams")
92
+ let triangles = ball.indices.length / 3
93
+ let surface!: MeshNode
94
+ onFrame(tick => {
95
+ let t = tick / 1000
96
+ for (let i = 0; i < count; i++) {
97
+ let y = pos.get(i, 1)
98
+ let x = pos.get(i, 0)
99
+ wave.set(i, 0, WAVE_HEIGHT * Math.sin(y * 6 + t * 3) * Math.cos(x * 5 - t * 2))
100
+ }
101
+ // One write of the wave stream; the other buffers never move.
102
+ updateVertices(ball, { stream: 1 })
103
+ let shown = Math.min(triangles, Math.floor(triangles * Math.min(1, t / REVEAL_SECONDS)))
104
+ if (surface) setDrawRange(surface, 0, shown * 3)
105
+ })
106
+
107
+ return (
108
+ <window>
109
+ <view width={pct(100)} height={pct(100)}>
110
+ <Scene clearColor={[0.85, 0.87, 0.9, 1]} label="streams">
111
+ <PerspectiveCamera fov={45} position={[0, 1.4, 3.4]} lookAt={[0, 0, 0]} />
112
+ <HemisphereLight sky={[0.55, 0.58, 0.62]} ground={[0.25, 0.22, 0.2]} />
113
+ <DirectionalLight color={[1, 0.96, 0.9]} intensity={0.8} position={[3, 4, 2]} direction={[-3, -4, -2]} />
114
+ <Mesh geometry={ball} material={surfaceLook.instance({ params: { uColor: [0.9, 0.55, 0.3, 1], uSpecular: 0.3, uShininess: 40 } })} ref={m => (surface = m)} />
115
+ <Mesh geometry={wireframeGeometry(ball)} material={lineLook.instance()} />
116
+ </Scene>
117
+ </view>
118
+ </window>
119
+ )
120
+ }
121
+
122
+ render(() => <App />)
@@ -3,11 +3,11 @@
3
3
  // over the crate's edges like real webbing - while smooth-tagged points
4
4
  // share averaged normals, so the helix sweeps into ONE continuous coil
5
5
  // (per-segment boxes with unmitred gaps are exactly what this replaces).
6
- // tube() is the round-profile shorthand. lit() materials on purpose:
6
+ // tube() is the round-profile shorthand. phong() materials on purpose:
7
7
  // creased vs smooth joints differ only in normals, which unlit color
8
8
  // would hide.
9
9
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
10
- import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, roundRect, Scene, sweep, tube, unlit } from "@solidrt/3d"
10
+ import { box, DirectionalLight, Group, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, roundRect, Scene, sweep, tube, unlit } from "@solidrt/3d"
11
11
  import type { SweepPath } from "@solidrt/3d"
12
12
 
13
13
  function App() {
@@ -48,9 +48,9 @@ function App() {
48
48
  rotation={[-Math.PI / 2, 0, 0]}
49
49
  />
50
50
  <Group rotation={[0, spin(), 0]}>
51
- <Mesh geometry={box({ width: 1, height: 0.6, depth: 0.8 })} material={lit({ color: [0.55, 0.42, 0.28] })} position={[-0.8, 0.3, 0]} />
52
- <Mesh geometry={strap} material={lit({ color: [0.9, 0.55, 0.2] })} />
53
- <Mesh geometry={coil} material={lit({ color: [0.45, 0.6, 0.8] })} />
51
+ <Mesh geometry={box({ width: 1, height: 0.6, depth: 0.8 })} material={phong({ color: [0.55, 0.42, 0.28] })} position={[-0.8, 0.3, 0]} />
52
+ <Mesh geometry={strap} material={phong({ color: [0.9, 0.55, 0.2] })} />
53
+ <Mesh geometry={coil} material={phong({ color: [0.45, 0.6, 0.8] })} />
54
54
  </Group>
55
55
  </Scene>
56
56
  </view>