@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.
- package/AGENTS.md +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
package/examples/scene-views.tsx
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
// One scene, three renderings: the built-in leaf from a perspective
|
|
2
|
-
// camera, plus two
|
|
3
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
package/examples/shadows.tsx
CHANGED
|
@@ -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)}
|
|
47
|
-
<Scene
|
|
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 />)
|
package/examples/sprites.tsx
CHANGED
|
@@ -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)}
|
|
72
|
-
<Scene
|
|
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 />)
|
package/examples/sweep-paths.tsx
CHANGED
|
@@ -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)}
|
|
43
|
-
<Scene
|
|
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.
|
|
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.
|
|
22
|
-
"@solidrt/core": "0.0.
|
|
21
|
+
"@solidjs/signals": "2.0.0-rc.6",
|
|
22
|
+
"@solidrt/core": "0.0.57"
|
|
23
23
|
}
|
|
24
24
|
}
|