@solidrt/3d 0.0.50 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +393 -87
- package/README.md +20 -10
- package/demos/README.md +15 -0
- package/demos/assets/icon.svg +23 -0
- package/demos/package.json +9 -0
- package/demos/src/the-third-dimension.tsx +866 -0
- package/demos/tsconfig.json +15 -0
- package/examples/README.md +34 -2
- package/examples/aim.tsx +5 -5
- package/examples/instanced.tsx +158 -0
- package/examples/lit.tsx +66 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +51 -0
- package/examples/pick.tsx +5 -5
- package/examples/scene-background.tsx +3 -3
- package/examples/scene-basic.tsx +3 -3
- package/examples/scene-post-effect.tsx +3 -3
- package/examples/scene-views.tsx +95 -0
- package/examples/shadows.tsx +86 -0
- package/examples/sprites.tsx +95 -0
- package/examples/sweep-paths.tsx +11 -27
- package/package.json +5 -3
- package/src/components.tsx +171 -3
- package/src/geometry-gpu.ts +97 -0
- package/src/geometry.ts +413 -162
- package/src/glsl.ts +83 -3
- package/src/gltf.ts +437 -0
- package/src/index.ts +18 -11
- package/src/material.ts +373 -47
- package/src/math.ts +114 -0
- package/src/model-file.ts +122 -0
- package/src/model.ts +105 -0
- package/src/orbit.ts +13 -9
- package/src/order.ts +12 -5
- package/src/profile.ts +4 -8
- package/src/scene.ts +1270 -281
- package/src/sweep.ts +21 -36
- package/src/bvh.ts +0 -258
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"jsx": "preserve",
|
|
6
|
+
"jsxImportSource": "@solidrt/core",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["@solidrt/flux-types"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
package/examples/README.md
CHANGED
|
@@ -9,8 +9,14 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
9
9
|
and pipeline sharing, and the one-signal onFrame drive.
|
|
10
10
|
- `sweep-paths.tsx` - swept solids along polylines: a flat strap folding
|
|
11
11
|
over a crate (bare path points crease on the mitred bends) and a coiled
|
|
12
|
-
tube (smooth-tagged helix, one continuous mesh), lit
|
|
13
|
-
|
|
12
|
+
tube (smooth-tagged helix, one continuous mesh), lit so the
|
|
13
|
+
creased-vs-smooth normals actually show.
|
|
14
|
+
- `lit.tsx` - the lit material and light nodes: `<HemisphereLight>` plus
|
|
15
|
+
a warm key `<DirectionalLight>` turning inside a spinning `<Group>` and
|
|
16
|
+
a fixed cool fill,
|
|
17
|
+
a glossy sphere (specular/shininess), a transparent glass sphere, and
|
|
18
|
+
one checker map shared by a UV-mapped cube (stretched per face) and
|
|
19
|
+
two triplanar meshes tiling it at one world density.
|
|
14
20
|
- `aim.tsx` - the rotation verbs, one pointer each tracking an orbiting
|
|
15
21
|
target: `lookAt` for a +z solid, `quatFromTo` for aiming a y-axis cone,
|
|
16
22
|
and a `quatSlerp` damped follow that visibly lags; all driven from
|
|
@@ -23,3 +29,29 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
23
29
|
scene's own pass (`<Scene background>`): one target, no stacked
|
|
24
30
|
backdrop texture, no resize plumbing; the source is shader-target
|
|
25
31
|
compatible verbatim.
|
|
32
|
+
- `sprites.tsx` - sprites: a ring of `"full"` billboard glows that stay
|
|
33
|
+
flat to the screen and `"fixed-y"` cutout trees that only yaw toward
|
|
34
|
+
the camera and stay upright as it climbs, both turned in the vertex
|
|
35
|
+
stage while the camera circles - no per-sprite JS per frame.
|
|
36
|
+
- `instanced.tsx` - instanced meshes: one material class declaring
|
|
37
|
+
`instanceAttributes`, two `<InstancedMesh>` fleets (400 scattered
|
|
38
|
+
rocks, a ring of pines) each ONE draw entry and ONE uModel, a spinning
|
|
39
|
+
group moving both with two matrix writes, and `setInstanceCount` from
|
|
40
|
+
onFrame breathing the pine population.
|
|
41
|
+
- `scene-views.tsx` - scene views: one scene rendered three times, the
|
|
42
|
+
built-in perspective leaf plus two `scene.createView` targets - a
|
|
43
|
+
top-down ORTHOGRAPHIC map (`ortho` on setCamera) and a side silhouette
|
|
44
|
+
drawn with an `overrideMaterial`; one spinning group, one signal, every
|
|
45
|
+
target fed by the core's one flush.
|
|
46
|
+
- `shadows.tsx` - directional shadow maps from three casting lights: a
|
|
47
|
+
`castShadow` sun swinging through its arc (one setTransform per frame
|
|
48
|
+
on the light node), a fixed cool fill and a low rim light, `castShadow`
|
|
49
|
+
meshes turning in a group throwing three crossing shadows each, the
|
|
50
|
+
ground and the casters receiving through plain `lit` (the default);
|
|
51
|
+
each shadow camera follows its light's world matrix, each map is
|
|
52
|
+
rendered by an internal view.
|
|
53
|
+
- `model.tsx` - a model from a file: `model.glb` (a small rover with
|
|
54
|
+
nested node transforms, a mirrored node, a textured material, a
|
|
55
|
+
transparent dome and a mesh without normals) parsed with `parseGltf`
|
|
56
|
+
from a binary import and built by `createModel` into a Group of named
|
|
57
|
+
parts; clicking a part hides it, clicking the body restores all.
|
package/examples/aim.tsx
CHANGED
|
@@ -74,16 +74,16 @@ function App() {
|
|
|
74
74
|
setTransform(lazy, { quaternion: aimQ })
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
let ball = sphere(0.22)
|
|
78
|
-
let pointer = cone(0.3, 0.9)
|
|
77
|
+
let ball = sphere({ radius: 0.22 })
|
|
78
|
+
let pointer = cone({ radius: 0.3, height: 0.9 })
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
81
|
<window>
|
|
82
|
-
<view width={pct(100)} height={pct(100)}
|
|
82
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
83
83
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="aim">
|
|
84
84
|
<PerspectiveCamera fov={55} position={[0, 2.6, 4.6]} lookAt={[0, 0.7, 0]} />
|
|
85
85
|
<Mesh
|
|
86
|
-
geometry={plane(7, 7, "floor")}
|
|
86
|
+
geometry={plane({ width: 7, height: 7, label: "floor" })}
|
|
87
87
|
material={unlit({ color: [0.16, 0.17, 0.22] })}
|
|
88
88
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
89
89
|
/>
|
|
@@ -93,7 +93,7 @@ function App() {
|
|
|
93
93
|
ref={n => (target = n)}
|
|
94
94
|
/>
|
|
95
95
|
<Mesh
|
|
96
|
-
geometry={tube([[0, 0, 0], [0, 0, 1.1]], 0.09, 10, "rod")}
|
|
96
|
+
geometry={tube([[0, 0, 0], [0, 0, 1.1]], { radius: 0.09, radialSegments: 10, label: "rod" })}
|
|
97
97
|
material={unlit({ color: [0.85, 0.3, 0.3] })}
|
|
98
98
|
position={[-1.4, 0.5, 0]}
|
|
99
99
|
ref={n => (rod = n)}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Instanced meshes: one draw entry covering a whole population. A material
|
|
2
|
+
// class declares `instanceAttributes`, createInstancedMesh (here via the
|
|
3
|
+
// <InstancedMesh> component) supplies one interleaved record per instance,
|
|
4
|
+
// and the vertex stage reads each record through the matching `in`
|
|
5
|
+
// variables. Two meshes share the one class: 400 scattered "rocks" and a
|
|
6
|
+
// breathing ring of "pines" - each is ONE entry and ONE uModel however
|
|
7
|
+
// many instances it draws, so the spinning group below moves both fleets
|
|
8
|
+
// with two matrix writes per frame.
|
|
9
|
+
//
|
|
10
|
+
// setInstanceCount is the population dial (the pines breathe); records are
|
|
11
|
+
// data, not matrices - position/scale/tint here, whatever your shader
|
|
12
|
+
// wants in general. The explicit `bounds` cover the scatter so picking
|
|
13
|
+
// still works (one conservative box around the population; omit bounds and
|
|
14
|
+
// the mesh simply never picks).
|
|
15
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
16
|
+
import { glsl } from "@solidrt/core/gpu"
|
|
17
|
+
import {
|
|
18
|
+
box,
|
|
19
|
+
cone,
|
|
20
|
+
Group,
|
|
21
|
+
InstancedMesh,
|
|
22
|
+
Mesh,
|
|
23
|
+
PerspectiveCamera,
|
|
24
|
+
plane,
|
|
25
|
+
Scene,
|
|
26
|
+
setInstanceCount,
|
|
27
|
+
shaderMaterialClass,
|
|
28
|
+
unlit,
|
|
29
|
+
} from "@solidrt/3d"
|
|
30
|
+
import type { InstancedMeshNode } from "@solidrt/3d"
|
|
31
|
+
import { HEMISPHERE } from "@solidrt/3d/glsl"
|
|
32
|
+
|
|
33
|
+
const SIZE = 720
|
|
34
|
+
|
|
35
|
+
const INSTANCE_VERTEX = glsl`
|
|
36
|
+
in vec3 aPos;
|
|
37
|
+
in vec3 aNormal;
|
|
38
|
+
in vec3 iPos;
|
|
39
|
+
in float iScale;
|
|
40
|
+
in vec3 iTint;
|
|
41
|
+
out vec3 vNormal;
|
|
42
|
+
out vec3 vTint;
|
|
43
|
+
uniform mat4 uModel;
|
|
44
|
+
uniform mat4 uViewProj;
|
|
45
|
+
|
|
46
|
+
void main() {
|
|
47
|
+
vec3 p = aPos * iScale + iPos;
|
|
48
|
+
gl_Position = uViewProj * uModel * vec4(p, 1.0);
|
|
49
|
+
vNormal = mat3(uModel) * aNormal;
|
|
50
|
+
vTint = iTint;
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const INSTANCE_FRAGMENT = glsl`
|
|
55
|
+
in vec3 vNormal;
|
|
56
|
+
in vec3 vTint;
|
|
57
|
+
${HEMISPHERE}
|
|
58
|
+
|
|
59
|
+
void main() {
|
|
60
|
+
vec3 c = vTint * hemisphere(normalize(vNormal), vec3(1.05, 1.0, 0.95), vec3(0.35, 0.32, 0.3));
|
|
61
|
+
fragColor = vec4(c, 1.0);
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
// One record per instance, interleaved in attribute order: 7 floats.
|
|
66
|
+
const STRIDE = 7
|
|
67
|
+
|
|
68
|
+
// A deterministic scatter (no per-run surprises when eyeballing).
|
|
69
|
+
function rocks(count: number): Float32Array {
|
|
70
|
+
let records = new Float32Array(count * STRIDE)
|
|
71
|
+
let a = 0
|
|
72
|
+
for (let i = 0; i < count; i++) {
|
|
73
|
+
a += 2.399963 // golden angle: an even spiral scatter
|
|
74
|
+
let r = 0.35 + 3.4 * Math.sqrt((i + 0.5) / count)
|
|
75
|
+
let s = 0.05 + 0.11 * ((i * 7) % 10) / 10
|
|
76
|
+
let o = i * STRIDE
|
|
77
|
+
records[o] = Math.cos(a) * r
|
|
78
|
+
records[o + 1] = s / 2
|
|
79
|
+
records[o + 2] = Math.sin(a) * r
|
|
80
|
+
records[o + 3] = s
|
|
81
|
+
records[o + 4] = 0.55 + 0.3 * ((i * 3) % 5) / 5
|
|
82
|
+
records[o + 5] = 0.5 + 0.2 * ((i * 11) % 7) / 7
|
|
83
|
+
records[o + 6] = 0.45
|
|
84
|
+
}
|
|
85
|
+
return records
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pines(count: number): Float32Array {
|
|
89
|
+
let records = new Float32Array(count * STRIDE)
|
|
90
|
+
for (let i = 0; i < count; i++) {
|
|
91
|
+
let a = (i / count) * Math.PI * 2
|
|
92
|
+
let s = 0.5 + 0.25 * ((i * 5) % 8) / 8
|
|
93
|
+
let o = i * STRIDE
|
|
94
|
+
records[o] = Math.cos(a) * 2.4
|
|
95
|
+
records[o + 1] = s / 2
|
|
96
|
+
records[o + 2] = Math.sin(a) * 2.4
|
|
97
|
+
records[o + 3] = s
|
|
98
|
+
records[o + 4] = 0.15
|
|
99
|
+
records[o + 5] = 0.4 + 0.25 * ((i * 3) % 6) / 6
|
|
100
|
+
records[o + 6] = 0.2
|
|
101
|
+
}
|
|
102
|
+
return records
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const PINE_COUNT = 48
|
|
106
|
+
|
|
107
|
+
function App() {
|
|
108
|
+
let [spin, setSpin] = createSignal(0)
|
|
109
|
+
let pinesMesh!: InstancedMeshNode
|
|
110
|
+
onFrame(tick => {
|
|
111
|
+
setSpin(tick / 6000)
|
|
112
|
+
// The population dial: draw the first N records. The buffer holds the
|
|
113
|
+
// full ring; only the draw range moves, one setDrawRange per change.
|
|
114
|
+
let n = Math.round(PINE_COUNT * (0.5 + 0.5 * Math.sin(tick / 900)))
|
|
115
|
+
if (pinesMesh) setInstanceCount(pinesMesh, n)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<window>
|
|
120
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
121
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.08, 0.1, 1]} label="instanced">
|
|
122
|
+
<PerspectiveCamera fov={55} position={[0, 3.2, 5.4]} lookAt={[0, 0.2, 0]} />
|
|
123
|
+
<Mesh geometry={plane({ width: 9, height: 9, label: "meadow" })} material={unlit({ color: [0.16, 0.18, 0.16] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
124
|
+
<Group rotation={[0, spin(), 0]}>
|
|
125
|
+
<InstancedMesh
|
|
126
|
+
geometry={box({ label: "rock" })}
|
|
127
|
+
material={instancedLook.instance()}
|
|
128
|
+
records={rocks(400)}
|
|
129
|
+
bounds={[-3.9, 0, -3.9, 3.9, 0.2, 3.9]}
|
|
130
|
+
/>
|
|
131
|
+
<InstancedMesh
|
|
132
|
+
geometry={cone({ radius: 0.3, height: 1, radialSegments: 10, label: "pine" })}
|
|
133
|
+
material={instancedLook.instance()}
|
|
134
|
+
records={pines(PINE_COUNT)}
|
|
135
|
+
bounds={[-2.8, 0, -2.8, 2.8, 0.8, 2.8]}
|
|
136
|
+
ref={m => (pinesMesh = m)}
|
|
137
|
+
/>
|
|
138
|
+
</Group>
|
|
139
|
+
</Scene>
|
|
140
|
+
</view>
|
|
141
|
+
</window>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// One class, one compiled pipeline; each mesh gets its own instance() so
|
|
146
|
+
// per-mesh uniforms stay independent (none are used here).
|
|
147
|
+
let instancedLook = shaderMaterialClass({
|
|
148
|
+
vertex: INSTANCE_VERTEX,
|
|
149
|
+
fragment: INSTANCE_FRAGMENT,
|
|
150
|
+
instanceAttributes: [
|
|
151
|
+
{ name: "iPos", format: "vec3" },
|
|
152
|
+
{ name: "iScale", format: "f32" },
|
|
153
|
+
{ name: "iTint", format: "vec3" },
|
|
154
|
+
],
|
|
155
|
+
label: "instanced-look",
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
render(() => <App />)
|
package/examples/lit.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The lit material and the scene's lights. lit() is the standard look -
|
|
2
|
+
// hemisphere ambient, the scene's directional lights, Lambert diffuse,
|
|
3
|
+
// optional Blinn-Phong highlight - with the same color/map/transparent
|
|
4
|
+
// options as unlit. Lights are NODES: <HemisphereLight> plus up to four
|
|
5
|
+
// <DirectionalLight>s, here a warm key inside a spinning <Group> (a
|
|
6
|
+
// parent's rotation turns the light, no per-frame direction math) and a
|
|
7
|
+
// fixed cool fill; however many meshes, a light change is one shared
|
|
8
|
+
// uniform write. The three boxes share one checker map: the
|
|
9
|
+
// UV-mapped one stretches it per face (every generator emits 0..1 UVs),
|
|
10
|
+
// the two triplanar ones tile it at one world density whatever their
|
|
11
|
+
// size - the reason triplanar is an option on lit.
|
|
12
|
+
|
|
13
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
14
|
+
import { createTexture } from "@solidrt/core/gpu"
|
|
15
|
+
import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, Scene, sphere, torusKnot, plane } from "@solidrt/3d"
|
|
16
|
+
|
|
17
|
+
const SIZE = 720
|
|
18
|
+
|
|
19
|
+
function checker(): ReturnType<typeof createTexture> {
|
|
20
|
+
let n = 64
|
|
21
|
+
let data = new Uint8Array(n * n * 4)
|
|
22
|
+
for (let y = 0; y < n; y++) {
|
|
23
|
+
for (let x = 0; x < n; x++) {
|
|
24
|
+
let v = ((x >> 3) + (y >> 3)) & 1 ? 235 : 120
|
|
25
|
+
data.set([v, v, v, 255], (y * n + x) * 4)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return createTexture(data, n, n, { wrap: "repeat", mipmap: true })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
let [t, setT] = createSignal(0)
|
|
33
|
+
onFrame(tick => setT(tick / 1000))
|
|
34
|
+
|
|
35
|
+
let map = checker()
|
|
36
|
+
let uvMapped = lit({ map })
|
|
37
|
+
let tiled = lit({ map, triplanar: 2 })
|
|
38
|
+
let glossy = lit({ color: [0.85, 0.3, 0.25], specular: 0.6, shininess: 60 })
|
|
39
|
+
let matte = lit({ color: [0.3, 0.55, 0.85] })
|
|
40
|
+
let glass = lit({ color: [0.9, 0.95, 1, 0.35], specular: 1, shininess: 120, transparent: true })
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<window>
|
|
44
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
45
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} samples={4} label="lit">
|
|
46
|
+
<PerspectiveCamera fov={50} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
|
|
47
|
+
<HemisphereLight sky={[0.35, 0.38, 0.45]} ground={[0.12, 0.1, 0.08]} />
|
|
48
|
+
<Group rotation={[0, t(), 0]}>
|
|
49
|
+
<DirectionalLight direction={[0.7, -0.8, 0]} color={[1, 0.92, 0.8]} intensity={0.9} />
|
|
50
|
+
</Group>
|
|
51
|
+
<DirectionalLight direction={[0.6, -0.3, 0.5]} color={[0.4, 0.5, 0.8]} intensity={0.35} />
|
|
52
|
+
<Mesh geometry={plane({ width: 8, height: 8 })} material={tiled} rotation={[-Math.PI / 2, 0, 0]} />
|
|
53
|
+
<Group rotation={[0, t() / 4, 0]}>
|
|
54
|
+
<Mesh geometry={box({ width: 0.8, height: 0.8, depth: 0.8 })} material={uvMapped} position={[-1.6, 0.4, 0]} />
|
|
55
|
+
<Mesh geometry={box({ width: 1.6, height: 0.5, depth: 0.8 })} material={tiled} position={[0, 0.25, 1.2]} />
|
|
56
|
+
<Mesh geometry={sphere({ radius: 0.5 })} material={glossy} position={[1.5, 0.5, 0]} />
|
|
57
|
+
<Mesh geometry={torusKnot({ radius: 0.35, tube: 0.12 })} material={matte} position={[0, 0.9, -1]} />
|
|
58
|
+
<Mesh geometry={sphere({ radius: 0.45 })} material={glass} position={[0, 0.45, 0]} />
|
|
59
|
+
</Group>
|
|
60
|
+
</Scene>
|
|
61
|
+
</view>
|
|
62
|
+
</window>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render(() => <App />)
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// A model from a file: model.glb (a small authored scene - a rover with a
|
|
2
|
+
// node tree of nested transforms, a mirrored node, a textured material, a
|
|
3
|
+
// transparent dome and a mesh shipped without normals) parsed at runtime
|
|
4
|
+
// with parseGltf and turned into a Group of meshes by createModel. The
|
|
5
|
+
// glb rides in the bundle as a binary import; a model under assets/ goes
|
|
6
|
+
// through loadGltf (same parser, read with flux:fs) or, baked once with
|
|
7
|
+
// `srt tool 3d/model`, through loadModel (no parsing at all). Every part
|
|
8
|
+
// keeps its node name and is an ordinary mesh: pointer events, transforms
|
|
9
|
+
// and materials work per part.
|
|
10
|
+
|
|
11
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
12
|
+
import { add, createModel, DirectionalLight, Group, HemisphereLight, parseGltf, PerspectiveCamera, Scene, setVisible } from "@solidrt/3d"
|
|
13
|
+
import type { SceneNode } from "@solidrt/3d"
|
|
14
|
+
import modelBytes from "./model.glb" with { type: "binary" }
|
|
15
|
+
|
|
16
|
+
const SIZE = 720
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
let [t, setT] = createSignal(0)
|
|
20
|
+
onFrame(tick => setT(tick / 1000))
|
|
21
|
+
|
|
22
|
+
// Parse + upload once; the model is plain scene data from here on.
|
|
23
|
+
let model = createModel(parseGltf(modelBytes), { label: "rover" })
|
|
24
|
+
let b = model.bounds
|
|
25
|
+
let center: [number, number, number] = [(b[0]! + b[3]!) / 2, (b[1]! + b[4]!) / 2, (b[2]! + b[5]!) / 2]
|
|
26
|
+
let radius = Math.hypot(b[3]! - b[0]!, b[4]! - b[1]!, b[5]! - b[2]!) / 2
|
|
27
|
+
|
|
28
|
+
// Parts are named after the glTF nodes: a click hides that part, a
|
|
29
|
+
// click on the body brings everything back.
|
|
30
|
+
for (let part of model.parts) {
|
|
31
|
+
part.mesh.onPointerDown = () => {
|
|
32
|
+
if (part.name === "body") for (let p of model.parts) setVisible(p.mesh, true)
|
|
33
|
+
else setVisible(part.mesh, false)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<window>
|
|
39
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
40
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.1, 0.11, 0.14, 1]} samples={4} label="model">
|
|
41
|
+
<PerspectiveCamera fov={40} position={[center[0] + radius * 1.4, center[1] + radius * 1.1, center[2] + radius * 2.2]} lookAt={center} />
|
|
42
|
+
<HemisphereLight sky={[0.45, 0.5, 0.6]} ground={[0.2, 0.17, 0.14]} />
|
|
43
|
+
<DirectionalLight direction={[0.5, -0.8, 0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
|
|
44
|
+
<Group rotation={[0, t() / 3, 0]} ref={(g: SceneNode) => add(g, model)} />
|
|
45
|
+
</Scene>
|
|
46
|
+
</view>
|
|
47
|
+
</window>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(() => <App />)
|
package/examples/pick.tsx
CHANGED
|
@@ -62,15 +62,15 @@ function Pickable(p: { name: string; color: Color; geometry: Geometry; position:
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function App() {
|
|
65
|
-
let floor = plane(8, 8, "floor")
|
|
65
|
+
let floor = plane({ width: 8, height: 8, label: "floor" })
|
|
66
66
|
let crate = box()
|
|
67
|
-
let ball = sphere(0.45)
|
|
68
|
-
let spike = cone(0.45, 1)
|
|
69
|
-
let ring = torus(0.4, 0.14)
|
|
67
|
+
let ball = sphere({ radius: 0.45 })
|
|
68
|
+
let spike = cone({ radius: 0.45, height: 1 })
|
|
69
|
+
let ring = torus({ radius: 0.4, tube: 0.14 })
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
72
|
<window>
|
|
73
|
-
<view width={pct(100)} height={pct(100)}
|
|
73
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
74
74
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="pick">
|
|
75
75
|
<PerspectiveCamera fov={55} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
|
|
76
76
|
<Mesh geometry={floor} material={unlit({ color: [0.15, 0.16, 0.2] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
@@ -28,12 +28,12 @@ let BACKDROP = glsl`
|
|
|
28
28
|
function App() {
|
|
29
29
|
return (
|
|
30
30
|
<window>
|
|
31
|
-
<view width={pct(100)} height={pct(100)}
|
|
31
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
32
32
|
<Scene width={SIZE} height={SIZE} background={BACKDROP} label="backdrop-demo">
|
|
33
33
|
<PerspectiveCamera fov={55} position={[0, 1.8, 4.4]} lookAt={[0, 0.4, 0]} />
|
|
34
|
-
<Mesh geometry={torusKnot(0.7, 0.2, 128, 16)} material={unlit({ color: [0.85, 0.55, 0.25] })} position={[0, 0.9, 0]} />
|
|
34
|
+
<Mesh geometry={torusKnot({ radius: 0.7, tube: 0.2, tubularSegments: 128, radialSegments: 16 })} material={unlit({ color: [0.85, 0.55, 0.25] })} position={[0, 0.9, 0]} />
|
|
35
35
|
<Mesh geometry={box()} material={unlit({ color: [0.3, 0.5, 0.8] })} position={[-1.5, 0.4, -0.5]} scale={0.8} />
|
|
36
|
-
<Mesh geometry={sphere(0.4)} material={unlit({ color: [0.4, 0.75, 0.45] })} position={[1.5, 0.4, -0.5]} />
|
|
36
|
+
<Mesh geometry={sphere({ radius: 0.4 })} material={unlit({ color: [0.4, 0.75, 0.45] })} position={[1.5, 0.4, -0.5]} />
|
|
37
37
|
</Scene>
|
|
38
38
|
</view>
|
|
39
39
|
</window>
|
package/examples/scene-basic.tsx
CHANGED
|
@@ -21,12 +21,12 @@ function App() {
|
|
|
21
21
|
// use one cube geometry (one vertex/index buffer pair on the GPU), and
|
|
22
22
|
// every mesh here shares the one unlit-color pipeline.
|
|
23
23
|
let cube = box()
|
|
24
|
-
let floor = plane(6, 6, "floor")
|
|
25
|
-
let ball = sphere(0.35)
|
|
24
|
+
let floor = plane({ width: 6, height: 6, label: "floor" })
|
|
25
|
+
let ball = sphere({ radius: 0.35 })
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
28
|
<window>
|
|
29
|
-
<view width={pct(100)} height={pct(100)}
|
|
29
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
30
30
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="scene-basic">
|
|
31
31
|
<PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
|
|
32
32
|
<Mesh
|
|
@@ -39,12 +39,12 @@ function App() {
|
|
|
39
39
|
onFrame(tick => setSpin(tick / 2000))
|
|
40
40
|
|
|
41
41
|
let cube = box()
|
|
42
|
-
let floor = plane(6, 6, "floor")
|
|
43
|
-
let ball = sphere(0.35)
|
|
42
|
+
let floor = plane({ width: 6, height: 6, label: "floor" })
|
|
43
|
+
let ball = sphere({ radius: 0.35 })
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<window>
|
|
47
|
-
<view width={pct(100)} height={pct(100)}
|
|
47
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
48
48
|
<Scene
|
|
49
49
|
width={RENDER}
|
|
50
50
|
height={RENDER}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// One scene, three renderings: the built-in leaf from a perspective
|
|
2
|
+
// camera, plus two `scene.createView` targets - a top-down ORTHOGRAPHIC
|
|
3
|
+
// map (`ortho` on setCamera) and a side silhouette drawn with an
|
|
4
|
+
// `overrideMaterial`. The group spins from ONE signal: each mesh is one
|
|
5
|
+
// core node whose flush writes its world matrix into every target's entry
|
|
6
|
+
// at once, so the extra views cost the app nothing per frame (and the
|
|
7
|
+
// light set fans out to them too - the map is lit like the main view).
|
|
8
|
+
// Views die with the scene.
|
|
9
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
10
|
+
import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
11
|
+
import type { SceneHandle } from "@solidrt/3d"
|
|
12
|
+
|
|
13
|
+
const MAIN = 720
|
|
14
|
+
const SIDE = 360
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
let [spin, setSpin] = createSignal(0)
|
|
18
|
+
onFrame(tick => setSpin(tick / 2000))
|
|
19
|
+
|
|
20
|
+
let cube = box()
|
|
21
|
+
let floor = plane({ width: 6, height: 6, label: "floor" })
|
|
22
|
+
let ball = sphere({ radius: 0.35 })
|
|
23
|
+
// `ref` runs before `output`, so the handle is set when the views are
|
|
24
|
+
// created there; both callbacks run once, untracked - no signals needed.
|
|
25
|
+
let scene!: SceneHandle
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<window>
|
|
29
|
+
<view width={pct(100)} height={pct(100)} designSize={[MAIN + SIDE, MAIN]}>
|
|
30
|
+
<Scene
|
|
31
|
+
width={MAIN}
|
|
32
|
+
height={MAIN}
|
|
33
|
+
clearColor={[0.07, 0.07, 0.1, 1]}
|
|
34
|
+
label="scene-views"
|
|
35
|
+
ref={s => (scene = s)}
|
|
36
|
+
output={tex => {
|
|
37
|
+
// The map: straight down from y = 10, world -z toward the top of
|
|
38
|
+
// the leaf, seven world units across at any depth.
|
|
39
|
+
let map = scene.createView({ width: SIDE, height: SIDE, clearColor: [0.05, 0.08, 0.06, 1], label: "map" })
|
|
40
|
+
map.setCamera({
|
|
41
|
+
position: [0, 10, 0],
|
|
42
|
+
target: [0, 0, 0],
|
|
43
|
+
up: [0, 0, -1],
|
|
44
|
+
ortho: { left: -3.5, right: 3.5, top: 3.5, bottom: -3.5 },
|
|
45
|
+
near: 0.1,
|
|
46
|
+
far: 20,
|
|
47
|
+
})
|
|
48
|
+
// The silhouette: every mesh drawn flat amber, from the side. The
|
|
49
|
+
// floor is edge-on here, a single line at the bottom.
|
|
50
|
+
let side = scene.createView({
|
|
51
|
+
width: SIDE,
|
|
52
|
+
height: SIDE,
|
|
53
|
+
clearColor: [0.1, 0.06, 0.05, 1],
|
|
54
|
+
overrideMaterial: unlit({ color: [0.95, 0.75, 0.3] }),
|
|
55
|
+
label: "silhouette",
|
|
56
|
+
})
|
|
57
|
+
side.setCamera({
|
|
58
|
+
position: [6, 1, 0],
|
|
59
|
+
target: [0, 0.6, 0],
|
|
60
|
+
ortho: { left: -2.4, right: 2.4, top: 2.4, bottom: -2.4 },
|
|
61
|
+
near: 0.1,
|
|
62
|
+
far: 20,
|
|
63
|
+
})
|
|
64
|
+
return (
|
|
65
|
+
<view flexDirection="row" width={MAIN + SIDE} height={MAIN}>
|
|
66
|
+
<texture src={tex} width={MAIN} height={MAIN} />
|
|
67
|
+
<view flexDirection="column" width={SIDE} height={MAIN}>
|
|
68
|
+
<texture src={map.texture} width={SIDE} height={SIDE} />
|
|
69
|
+
<texture src={side.texture} width={SIDE} height={SIDE} />
|
|
70
|
+
</view>
|
|
71
|
+
</view>
|
|
72
|
+
)
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
|
|
76
|
+
<HemisphereLight sky={[0.6, 0.7, 0.9]} ground={[0.25, 0.2, 0.15]} intensity={0.6} />
|
|
77
|
+
<DirectionalLight direction={[-0.5, -1, -0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
|
|
78
|
+
<Mesh geometry={floor} material={lit({ color: [0.3, 0.32, 0.38] })} rotation={[-Math.PI / 2, 0, 0]} />
|
|
79
|
+
<Group rotation={[0, spin(), 0]}>
|
|
80
|
+
<Mesh geometry={cube} material={lit({ color: [0.85, 0.3, 0.3] })} position={[0, 0.5, 0]} />
|
|
81
|
+
<Mesh
|
|
82
|
+
geometry={cube}
|
|
83
|
+
material={lit({ color: [0.9, 0.8, 0.35] })}
|
|
84
|
+
position={[-1.1, 0.7, 0]}
|
|
85
|
+
scale={[0.5, 1.4, 0.5]}
|
|
86
|
+
/>
|
|
87
|
+
<Mesh geometry={ball} material={lit({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
|
|
88
|
+
</Group>
|
|
89
|
+
</Scene>
|
|
90
|
+
</view>
|
|
91
|
+
</window>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
render(() => <App />)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Directional shadow maps: three `castShadow` lights, `castShadow` meshes
|
|
2
|
+
// and `lit` materials, which receive shadows by default. The warm sun
|
|
3
|
+
// swings through its arc from onFrame - ONE setTransform on the light
|
|
4
|
+
// node per frame; the scene re-places its shadow camera from the light's
|
|
5
|
+
// world matrix, so its shadows sweep across the ground while the casters
|
|
6
|
+
// turn inside their group. A cool fill from the opposite side and a low
|
|
7
|
+
// rim light stand still, so every caster throws three shadows that cross
|
|
8
|
+
// as the sun moves (each casting light is its own map and pass, capped
|
|
9
|
+
// by MAX_LIGHTS). Three's vocabulary throughout: `castShadow` on the
|
|
10
|
+
// light and on meshes, `shadow.mapSize/bias/normalBias/camera` on the
|
|
11
|
+
// light; the one divergence is that opting OUT of receiving is a material
|
|
12
|
+
// option (`lit({ receiveShadow: false })`, Godot's split), because the
|
|
13
|
+
// material picks the program (like vertexColors and triplanar).
|
|
14
|
+
//
|
|
15
|
+
// Placement matters for a casting light: its shadow camera sits at the
|
|
16
|
+
// light's WORLD position looking along its direction, so the sun is
|
|
17
|
+
// positioned above the scene (a light at the origin would shadow nothing
|
|
18
|
+
// above it). The light frustum is `shadow.camera`, +-5 world units by
|
|
19
|
+
// default - anything outside it is lit.
|
|
20
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
21
|
+
import { box, cylinder, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, Scene, setTransform, sphere, torusKnot } from "@solidrt/3d"
|
|
22
|
+
import type { DirectionalLightNode } from "@solidrt/3d"
|
|
23
|
+
|
|
24
|
+
const SIZE = 720
|
|
25
|
+
|
|
26
|
+
function App() {
|
|
27
|
+
let [t, setT] = createSignal(0)
|
|
28
|
+
let sun!: DirectionalLightNode
|
|
29
|
+
onFrame(tick => {
|
|
30
|
+
setT(tick / 1000)
|
|
31
|
+
// The sun's arc: east to west over the scene, always pointing at the
|
|
32
|
+
// origin. The direction is the node's local -y, so aiming is a
|
|
33
|
+
// rotation about z; the position rides the same arc so the shadow
|
|
34
|
+
// camera stays above the casters.
|
|
35
|
+
let a = Math.sin(tick / 4000) * 1.0
|
|
36
|
+
setTransform(sun, { position: [Math.sin(a) * 6, Math.cos(a) * 6, 2], rotation: [0.32, 0, -a] })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
let ground = lit({ color: [0.55, 0.58, 0.6] })
|
|
40
|
+
let red = lit({ color: [0.85, 0.3, 0.25] })
|
|
41
|
+
let blue = lit({ color: [0.3, 0.55, 0.85] })
|
|
42
|
+
let gold = lit({ color: [0.9, 0.75, 0.3], specular: 0.5, shininess: 40 })
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<window>
|
|
46
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
47
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.55, 0.65, 0.8, 1]} samples={4} label="shadows">
|
|
48
|
+
<PerspectiveCamera fov={45} position={[0, 4, 7]} lookAt={[0, 0.5, 0]} />
|
|
49
|
+
<HemisphereLight sky={[0.45, 0.5, 0.6]} ground={[0.2, 0.18, 0.15]} />
|
|
50
|
+
<DirectionalLight
|
|
51
|
+
ref={l => (sun = l)}
|
|
52
|
+
color={[1, 0.95, 0.85]}
|
|
53
|
+
intensity={0.8}
|
|
54
|
+
castShadow
|
|
55
|
+
shadow={{ mapSize: 1024, normalBias: 0.02, camera: { near: 1, far: 20 } }}
|
|
56
|
+
/>
|
|
57
|
+
<DirectionalLight
|
|
58
|
+
color={[0.45, 0.6, 0.9]}
|
|
59
|
+
intensity={0.5}
|
|
60
|
+
position={[-5, 5, -4]}
|
|
61
|
+
direction={[5, -5, 4]}
|
|
62
|
+
castShadow
|
|
63
|
+
shadow={{ normalBias: 0.02, camera: { near: 1, far: 20 } }}
|
|
64
|
+
/>
|
|
65
|
+
<DirectionalLight
|
|
66
|
+
color={[0.9, 0.5, 0.35]}
|
|
67
|
+
intensity={0.45}
|
|
68
|
+
position={[5, 2, -5]}
|
|
69
|
+
direction={[-5, -2, 5]}
|
|
70
|
+
castShadow
|
|
71
|
+
shadow={{ mapSize: 512, normalBias: 0.03, camera: { near: 1, far: 20 } }}
|
|
72
|
+
/>
|
|
73
|
+
<Mesh geometry={plane({ width: 10, height: 10 })} material={ground} rotation={[-Math.PI / 2, 0, 0]} />
|
|
74
|
+
<Group rotation={[0, t() / 3, 0]}>
|
|
75
|
+
<Mesh geometry={box({ width: 1, height: 1, depth: 1 })} material={red} position={[-1.6, 0.5, 0]} castShadow />
|
|
76
|
+
<Mesh geometry={sphere({ radius: 0.6 })} material={blue} position={[1.5, 0.6, 0]} castShadow />
|
|
77
|
+
<Mesh geometry={torusKnot({ radius: 0.4, tube: 0.14 })} material={gold} position={[0, 1.3, 0]} castShadow />
|
|
78
|
+
<Mesh geometry={cylinder({ radiusTop: 0.12, radiusBottom: 0.12, height: 1.6 })} material={gold} position={[0, 0.8, 0]} castShadow />
|
|
79
|
+
</Group>
|
|
80
|
+
</Scene>
|
|
81
|
+
</view>
|
|
82
|
+
</window>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render(() => <App />)
|