@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,95 @@
|
|
|
1
|
+
// Sprites: camera-facing quads turned in the vertex stage, so the camera
|
|
2
|
+
// can circle and climb while no per-frame JS touches a single sprite. Two
|
|
3
|
+
// billboard modes side by side: "full" glows that stay flat to the screen
|
|
4
|
+
// whatever the camera does, and "fixed-y" trees that only yaw to follow
|
|
5
|
+
// the camera and stay upright as it climbs - the classic upright sprite.
|
|
6
|
+
// The spinning cube is the reference solid; the sprites' `scale` is their
|
|
7
|
+
// world size, and their (ignored) rotation is never set.
|
|
8
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
9
|
+
import { createTexture } from "@solidrt/core/gpu"
|
|
10
|
+
import { box, Group, Mesh, PerspectiveCamera, plane, Scene, sprite, Sprite, unlit } from "@solidrt/3d"
|
|
11
|
+
|
|
12
|
+
const SIZE = 720
|
|
13
|
+
|
|
14
|
+
// A soft disc, alpha falling off from the center; premultiplied like every
|
|
15
|
+
// texture the engine samples.
|
|
16
|
+
function glow(): ReturnType<typeof createTexture> {
|
|
17
|
+
let n = 64
|
|
18
|
+
let data = new Uint8Array(n * n * 4)
|
|
19
|
+
for (let y = 0; y < n; y++) {
|
|
20
|
+
for (let x = 0; x < n; x++) {
|
|
21
|
+
let dx = (x + 0.5) / n - 0.5
|
|
22
|
+
let dy = (y + 0.5) / n - 0.5
|
|
23
|
+
let a = Math.max(0, 1 - Math.sqrt(dx * dx + dy * dy) * 2)
|
|
24
|
+
a = a * a
|
|
25
|
+
data.set([255 * a, 240 * a, 200 * a, 255 * a], (y * n + x) * 4)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return createTexture(data, n, n)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A cutout tree: green triangle over a brown trunk, transparent elsewhere.
|
|
32
|
+
function tree(): ReturnType<typeof createTexture> {
|
|
33
|
+
let n = 64
|
|
34
|
+
let data = new Uint8Array(n * n * 4)
|
|
35
|
+
for (let y = 0; y < n; y++) {
|
|
36
|
+
for (let x = 0; x < n; x++) {
|
|
37
|
+
let u = (x + 0.5) / n
|
|
38
|
+
let v = (y + 0.5) / n
|
|
39
|
+
let inCrown = v < 0.8 && Math.abs(u - 0.5) < v * 0.55
|
|
40
|
+
let inTrunk = v >= 0.8 && Math.abs(u - 0.5) < 0.08
|
|
41
|
+
let rgb = inCrown ? [40, 140, 60] : inTrunk ? [110, 70, 40] : [0, 0, 0]
|
|
42
|
+
let a = inCrown || inTrunk ? 255 : 0
|
|
43
|
+
data.set([rgb[0]!, rgb[1]!, rgb[2]!, a], (y * n + x) * 4)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return createTexture(data, n, n)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function App() {
|
|
50
|
+
let [t, setT] = createSignal(0)
|
|
51
|
+
onFrame(tick => setT(tick / 1000))
|
|
52
|
+
|
|
53
|
+
// The camera circles the scene and climbs from eye level to well above
|
|
54
|
+
// it: the glows never change shape, the trees tilt away only as the
|
|
55
|
+
// view looks down on them.
|
|
56
|
+
let eye = () => {
|
|
57
|
+
let a = t() * 0.4
|
|
58
|
+
let elevation = 1.2 + 2.5 * (0.5 - 0.5 * Math.cos(t() * 0.5))
|
|
59
|
+
return [Math.sin(a) * 5, elevation, Math.cos(a) * 5] as [number, number, number]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let glows = sprite({ map: glow(), color: [1, 0.85, 0.5] })
|
|
63
|
+
let trees = sprite({ map: tree(), billboard: "fixed-y" })
|
|
64
|
+
let ringPositions = Array.from({ length: 8 }, (_, i) => {
|
|
65
|
+
let a = (i / 8) * Math.PI * 2
|
|
66
|
+
return [Math.cos(a) * 1.4, 0.9 + Math.sin(a * 2) * 0.3, Math.sin(a) * 1.4] as [number, number, number]
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<window>
|
|
71
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
72
|
+
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sprites">
|
|
73
|
+
<PerspectiveCamera fov={50} position={eye()} lookAt={[0, 0.6, 0]} />
|
|
74
|
+
<Mesh
|
|
75
|
+
geometry={plane({ width: 8, height: 8 })}
|
|
76
|
+
material={unlit({ color: [0.16, 0.17, 0.22] })}
|
|
77
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
78
|
+
/>
|
|
79
|
+
<Mesh geometry={box({ width: 0.7, height: 0.7, depth: 0.7 })} material={unlit({ color: [0.85, 0.3, 0.3] })} position={[0, 0.35, 0]} rotation={[0, t(), 0]} />
|
|
80
|
+
<Group rotation={[0, -t() * 0.3, 0]}>
|
|
81
|
+
{ringPositions.map(p => (
|
|
82
|
+
<Sprite material={glows} position={p} scale={0.5} />
|
|
83
|
+
))}
|
|
84
|
+
</Group>
|
|
85
|
+
<Sprite material={trees} position={[-2.2, 0.8, -1]} scale={[1.2, 1.6, 1]} />
|
|
86
|
+
<Sprite material={trees} position={[2.4, 0.9, 0.5]} scale={[1.4, 1.8, 1]} />
|
|
87
|
+
<Sprite material={trees} position={[0.8, 0.7, -2.6]} scale={[1, 1.4, 1]} />
|
|
88
|
+
<Sprite material={trees} position={[-1.5, 0.6, 2.2]} scale={[0.9, 1.2, 1]} />
|
|
89
|
+
</Scene>
|
|
90
|
+
</view>
|
|
91
|
+
</window>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
render(() => <App />)
|
package/examples/sweep-paths.tsx
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
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.
|
|
6
|
+
// tube() is the round-profile shorthand. lit() 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, Group, Mesh, PerspectiveCamera, plane, roundRect, Scene,
|
|
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
|
-
import { HEMISPHERE, LAMBERT, LIT_VERTEX } from "@solidrt/3d/glsl"
|
|
13
12
|
|
|
14
13
|
const SIZE = 720
|
|
15
14
|
|
|
@@ -17,23 +16,6 @@ function App() {
|
|
|
17
16
|
let [spin, setSpin] = createSignal(0)
|
|
18
17
|
onFrame(tick => setSpin(tick / 4000))
|
|
19
18
|
|
|
20
|
-
// One hemisphere + lambert look per color (a material instance is the
|
|
21
|
-
// pipeline handle, so each look is created once and shared).
|
|
22
|
-
let lit = (r: number, g: number, b: number) =>
|
|
23
|
-
shaderMaterial({
|
|
24
|
-
vertex: LIT_VERTEX,
|
|
25
|
-
fragment: `
|
|
26
|
-
in vec3 vNormal;
|
|
27
|
-
${HEMISPHERE}
|
|
28
|
-
${LAMBERT}
|
|
29
|
-
void main() {
|
|
30
|
-
vec3 n = normalize(vNormal);
|
|
31
|
-
vec3 l = normalize(vec3(0.5, 0.8, 0.4));
|
|
32
|
-
vec3 base = vec3(${r}, ${g}, ${b});
|
|
33
|
-
fragColor = vec4(base * (hemisphere(n, vec3(0.45), vec3(0.22)) + 0.8 * lambert(n, l)), 1.0);
|
|
34
|
-
}`,
|
|
35
|
-
})
|
|
36
|
-
|
|
37
19
|
// The strap hugs the crate at half its thickness; every bend is a bare
|
|
38
20
|
// (sharp) point, so each fold creases exactly on a crate edge.
|
|
39
21
|
let o = 0.013
|
|
@@ -45,7 +27,7 @@ function App() {
|
|
|
45
27
|
[-0.3 + o, o, 0],
|
|
46
28
|
[0.15, o, 0],
|
|
47
29
|
]
|
|
48
|
-
let strap = sweep(roundRect(0.3, 0.026, 0.008), strapPath, "strap")
|
|
30
|
+
let strap = sweep(roundRect(0.3, 0.026, 0.008), strapPath, { label: "strap" })
|
|
49
31
|
|
|
50
32
|
// A smooth-tagged helix: one continuous tube, not a stack of segments.
|
|
51
33
|
let coilPath: SweepPath = []
|
|
@@ -53,22 +35,24 @@ function App() {
|
|
|
53
35
|
let a = (i / 60) * Math.PI * 5
|
|
54
36
|
coilPath.push({ p: [0.85 + Math.cos(a) * 0.35, 0.055 + i * 0.0095, Math.sin(a) * 0.35], smooth: true })
|
|
55
37
|
}
|
|
56
|
-
let coil = tube(coilPath, 0.05, 12, "coil")
|
|
38
|
+
let coil = tube(coilPath, { radius: 0.05, radialSegments: 12, label: "coil" })
|
|
57
39
|
|
|
58
40
|
return (
|
|
59
41
|
<window>
|
|
60
|
-
<view width={pct(100)} height={pct(100)}
|
|
42
|
+
<view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
|
|
61
43
|
<Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sweep-paths">
|
|
62
44
|
<PerspectiveCamera fov={55} position={[0, 1.9, 3.9]} lookAt={[0, 0.35, 0]} />
|
|
45
|
+
<HemisphereLight sky={[0.45, 0.45, 0.45]} ground={[0.22, 0.22, 0.22]} />
|
|
46
|
+
<DirectionalLight direction={[-0.5, -0.8, -0.4]} intensity={0.8} />
|
|
63
47
|
<Mesh
|
|
64
|
-
geometry={plane(6, 6, "floor")}
|
|
48
|
+
geometry={plane({ width: 6, height: 6, label: "floor" })}
|
|
65
49
|
material={unlit({ color: [0.16, 0.17, 0.22] })}
|
|
66
50
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
67
51
|
/>
|
|
68
52
|
<Group rotation={[0, spin(), 0]}>
|
|
69
|
-
<Mesh geometry={box(1, 0.6, 0.8)} material={lit(0.55, 0.42, 0.28)} position={[-0.8, 0.3, 0]} />
|
|
70
|
-
<Mesh geometry={strap} material={lit(0.9, 0.55, 0.2)} />
|
|
71
|
-
<Mesh geometry={coil} material={lit(0.45, 0.6, 0.8)} />
|
|
53
|
+
<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]} />
|
|
54
|
+
<Mesh geometry={strap} material={lit({ color: [0.9, 0.55, 0.2] })} />
|
|
55
|
+
<Mesh geometry={coil} material={lit({ color: [0.45, 0.6, 0.8] })} />
|
|
72
56
|
</Group>
|
|
73
57
|
</Scene>
|
|
74
58
|
</view>
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
4
4
|
"license": "MIT",
|
|
5
|
+
"funding": "https://github.com/sponsors/wellawaretech",
|
|
5
6
|
"author": "Antoine van Wel",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "src/index.ts",
|
|
@@ -13,10 +14,11 @@
|
|
|
13
14
|
"files": [
|
|
14
15
|
"src/",
|
|
15
16
|
"examples/",
|
|
17
|
+
"demos/",
|
|
16
18
|
"AGENTS.md"
|
|
17
19
|
],
|
|
18
20
|
"peerDependencies": {
|
|
19
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
20
|
-
"@solidrt/core": "0.0.
|
|
21
|
+
"@solidjs/signals": "2.0.0-rc.1",
|
|
22
|
+
"@solidrt/core": "0.0.52"
|
|
21
23
|
}
|
|
22
24
|
}
|
package/src/components.tsx
CHANGED
|
@@ -10,11 +10,20 @@ import { createContext, createEffect, onCleanup, untrack, useContext } from "@so
|
|
|
10
10
|
import type { Element, ParentComponent, TextureId, VoidComponent } from "@solidrt/core"
|
|
11
11
|
import {
|
|
12
12
|
add,
|
|
13
|
+
createDirectionalLight,
|
|
13
14
|
createGroup,
|
|
15
|
+
createHemisphereLight,
|
|
16
|
+
createInstancedMesh,
|
|
14
17
|
createMesh,
|
|
15
18
|
createScene,
|
|
19
|
+
createSprite,
|
|
20
|
+
disposeInstances,
|
|
16
21
|
remove,
|
|
17
22
|
setGeometry,
|
|
23
|
+
setInstanceCount,
|
|
24
|
+
setInstances,
|
|
25
|
+
setLight,
|
|
26
|
+
setCastShadow,
|
|
18
27
|
setMaterial,
|
|
19
28
|
setMeshParams,
|
|
20
29
|
setRenderOrder,
|
|
@@ -22,7 +31,7 @@ import {
|
|
|
22
31
|
setVisible,
|
|
23
32
|
} from "./scene.ts"
|
|
24
33
|
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
25
|
-
import type { Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent } from "./scene.ts"
|
|
34
|
+
import type { DirectionalLight as DirectionalLightNode, HemisphereLight as HemisphereLightNode, InstancedMesh as InstancedMeshNode, Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent, ShadowOptions } from "./scene.ts"
|
|
26
35
|
import type { Geometry } from "./geometry.ts"
|
|
27
36
|
import type { Material } from "./material.ts"
|
|
28
37
|
import type { Quat, Vec3 } from "./math.ts"
|
|
@@ -99,6 +108,9 @@ export type SceneProps = {
|
|
|
99
108
|
* `scene.background = color` is `clearColor` here. */
|
|
100
109
|
background?: string
|
|
101
110
|
label?: string
|
|
111
|
+
/** Multisample count (1, 2, 4 or 8; default 1): anti-aliased mesh edges.
|
|
112
|
+
* Fixed at creation. */
|
|
113
|
+
samples?: 1 | 2 | 4 | 8
|
|
102
114
|
ref?: (scene: SceneHandle) => void
|
|
103
115
|
/**
|
|
104
116
|
* Compose the output yourself: called once (untracked) with the scene's
|
|
@@ -127,7 +139,7 @@ export type SceneProps = {
|
|
|
127
139
|
*/
|
|
128
140
|
export let Scene: ParentComponent<SceneProps> = props => {
|
|
129
141
|
let scene = untrack(() =>
|
|
130
|
-
createScene(props.width, props.height, { clearColor: props.clearColor, label: props.label }),
|
|
142
|
+
createScene(props.width, props.height, { clearColor: props.clearColor, label: props.label, samples: props.samples }),
|
|
131
143
|
)
|
|
132
144
|
createEffect(
|
|
133
145
|
() => [props.width, props.height] as const,
|
|
@@ -182,9 +194,34 @@ export type MeshProps = TransformProps & PointerEventProps & {
|
|
|
182
194
|
params?: ShaderParams
|
|
183
195
|
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
184
196
|
renderOrder?: number
|
|
197
|
+
/** Draw into the scene's shadow map (setCastShadow as a prop); default
|
|
198
|
+
* false. Needs a `castShadow` DirectionalLight to show. */
|
|
199
|
+
castShadow?: boolean
|
|
185
200
|
ref?: (mesh: MeshNode) => void
|
|
186
201
|
}
|
|
187
202
|
|
|
203
|
+
// The mesh-side props Mesh and Sprite share (Sprite has no geometry).
|
|
204
|
+
function syncMesh(mesh: MeshNode, props: SpriteProps): void {
|
|
205
|
+
createEffect(
|
|
206
|
+
() => props.material,
|
|
207
|
+
m => setMaterial(mesh, m),
|
|
208
|
+
{ defer: true },
|
|
209
|
+
)
|
|
210
|
+
createEffect(
|
|
211
|
+
() => props.params,
|
|
212
|
+
p => {
|
|
213
|
+
if (p !== undefined) setMeshParams(mesh, p)
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
createEffect(
|
|
217
|
+
() => props.renderOrder,
|
|
218
|
+
o => setRenderOrder(mesh, o ?? 0),
|
|
219
|
+
)
|
|
220
|
+
syncNode(mesh, props)
|
|
221
|
+
untrack(() => props.ref)?.(mesh)
|
|
222
|
+
onCleanup(() => remove(mesh))
|
|
223
|
+
}
|
|
224
|
+
|
|
188
225
|
/** One draw entry: geometry drawn with a material at a transform. */
|
|
189
226
|
export let Mesh: VoidComponent<MeshProps> = props => {
|
|
190
227
|
let ctx = useContext(SceneContext)
|
|
@@ -195,6 +232,81 @@ export let Mesh: VoidComponent<MeshProps> = props => {
|
|
|
195
232
|
g => setGeometry(mesh, g),
|
|
196
233
|
{ defer: true },
|
|
197
234
|
)
|
|
235
|
+
createEffect(
|
|
236
|
+
() => props.castShadow,
|
|
237
|
+
c => setCastShadow(mesh, c === true),
|
|
238
|
+
)
|
|
239
|
+
syncMesh(mesh, props)
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export type SpriteProps = TransformProps & PointerEventProps & {
|
|
244
|
+
/** A `sprite()` material (any material draws, only a sprite one turns). */
|
|
245
|
+
material: Material
|
|
246
|
+
/** Per-mesh uniforms, merge semantics - as on Mesh. */
|
|
247
|
+
params?: ShaderParams
|
|
248
|
+
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
249
|
+
renderOrder?: number
|
|
250
|
+
ref?: (mesh: MeshNode) => void
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** A camera-facing unit quad (createSprite as a component): no geometry
|
|
254
|
+
* prop, `scale` is its world size, rotation is ignored. */
|
|
255
|
+
export let Sprite: VoidComponent<SpriteProps> = props => {
|
|
256
|
+
let ctx = useContext(SceneContext)
|
|
257
|
+
let mesh = untrack(() => createSprite(props.material))
|
|
258
|
+
add(ctx.parent, mesh)
|
|
259
|
+
syncMesh(mesh, props)
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type InstancedMeshProps = TransformProps & PointerEventProps & {
|
|
264
|
+
geometry: Geometry
|
|
265
|
+
/** Must declare instanceAttributes (shaderMaterialClass). */
|
|
266
|
+
material: Material
|
|
267
|
+
/** Interleaved per-instance records (stride = the material's instance
|
|
268
|
+
* attributes summed). Reactive; a later array larger than the buffer
|
|
269
|
+
* grows it (capacity doubles into a replacement buffer). */
|
|
270
|
+
records: Float32Array
|
|
271
|
+
/** How many records draw; default all of the latest `records`. */
|
|
272
|
+
count?: number
|
|
273
|
+
/** LOCAL bounds covering every instance ([minX..maxZ]), fixed at
|
|
274
|
+
* creation. Without them the mesh has no picking leaf, so pointer events
|
|
275
|
+
* never target it. */
|
|
276
|
+
bounds?: ArrayLike<number>
|
|
277
|
+
/** Per-mesh uniforms, merge semantics - as on Mesh. */
|
|
278
|
+
params?: ShaderParams
|
|
279
|
+
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
280
|
+
renderOrder?: number
|
|
281
|
+
ref?: (mesh: InstancedMeshNode) => void
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** One draw entry covering N instances: geometry repeated per record of
|
|
285
|
+
* `records` (createInstancedMesh as a component). The record buffer is
|
|
286
|
+
* component-owned and freed on unmount. */
|
|
287
|
+
export let InstancedMesh: VoidComponent<InstancedMeshProps> = props => {
|
|
288
|
+
let ctx = useContext(SceneContext)
|
|
289
|
+
let mesh = untrack(() =>
|
|
290
|
+
createInstancedMesh(props.geometry, props.material, props.records, props.count, { bounds: props.bounds }),
|
|
291
|
+
)
|
|
292
|
+
add(ctx.parent, mesh)
|
|
293
|
+
createEffect(
|
|
294
|
+
() => props.records,
|
|
295
|
+
r => setInstances(mesh, r, untrack(() => props.count)),
|
|
296
|
+
{ defer: true },
|
|
297
|
+
)
|
|
298
|
+
createEffect(
|
|
299
|
+
() => props.count,
|
|
300
|
+
c => {
|
|
301
|
+
if (c !== undefined) setInstanceCount(mesh, c)
|
|
302
|
+
},
|
|
303
|
+
{ defer: true },
|
|
304
|
+
)
|
|
305
|
+
createEffect(
|
|
306
|
+
() => props.geometry,
|
|
307
|
+
g => setGeometry(mesh, g),
|
|
308
|
+
{ defer: true },
|
|
309
|
+
)
|
|
198
310
|
createEffect(
|
|
199
311
|
() => props.material,
|
|
200
312
|
m => setMaterial(mesh, m),
|
|
@@ -212,7 +324,7 @@ export let Mesh: VoidComponent<MeshProps> = props => {
|
|
|
212
324
|
)
|
|
213
325
|
syncNode(mesh, props)
|
|
214
326
|
untrack(() => props.ref)?.(mesh)
|
|
215
|
-
onCleanup(() =>
|
|
327
|
+
onCleanup(() => disposeInstances(mesh))
|
|
216
328
|
return null
|
|
217
329
|
}
|
|
218
330
|
|
|
@@ -240,3 +352,59 @@ export let PerspectiveCamera: VoidComponent<PerspectiveCameraProps> = props => {
|
|
|
240
352
|
)
|
|
241
353
|
return null
|
|
242
354
|
}
|
|
355
|
+
|
|
356
|
+
export type HemisphereLightProps = { sky?: Vec3; ground?: Vec3; intensity?: number; ref?: (light: HemisphereLightNode) => void }
|
|
357
|
+
|
|
358
|
+
/** The scene's ambient term as a node (createHemisphereLight); one per
|
|
359
|
+
* scene, the last mounted wins. */
|
|
360
|
+
export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
|
|
361
|
+
let ctx = useContext(SceneContext)
|
|
362
|
+
let light = untrack(() => createHemisphereLight({ sky: props.sky, ground: props.ground, intensity: props.intensity }))
|
|
363
|
+
add(ctx.parent, light)
|
|
364
|
+
createEffect(
|
|
365
|
+
() => [props.sky, props.ground, props.intensity] as const,
|
|
366
|
+
([sky, ground, intensity]) => setLight(light, { sky, ground, intensity }),
|
|
367
|
+
)
|
|
368
|
+
untrack(() => props.ref)?.(light)
|
|
369
|
+
onCleanup(() => remove(light))
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export type DirectionalLightProps = TransformProps & {
|
|
374
|
+
/** Travel direction in the node's local space; default [0, -1, 0]. */
|
|
375
|
+
direction?: Vec3
|
|
376
|
+
color?: Vec3
|
|
377
|
+
intensity?: number
|
|
378
|
+
/** Render a shadow map from this light (any directional light may;
|
|
379
|
+
* each is a pass). Its shadow camera sits at the light's WORLD
|
|
380
|
+
* position, so give a casting light a `position` above the scene. */
|
|
381
|
+
castShadow?: boolean
|
|
382
|
+
/** Shadow-map options (mapSize, bias, normalBias, camera frustum),
|
|
383
|
+
* merged key by key. */
|
|
384
|
+
shadow?: ShadowOptions
|
|
385
|
+
ref?: (light: DirectionalLightNode) => void
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** A directional light node (createDirectionalLight): a parent Group's
|
|
389
|
+
* rotation turns it; up to MAX_LIGHTS per scene, in mount order. */
|
|
390
|
+
export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
|
|
391
|
+
let ctx = useContext(SceneContext)
|
|
392
|
+
let light = untrack(() =>
|
|
393
|
+
createDirectionalLight({
|
|
394
|
+
direction: props.direction,
|
|
395
|
+
color: props.color,
|
|
396
|
+
intensity: props.intensity,
|
|
397
|
+
castShadow: props.castShadow,
|
|
398
|
+
shadow: props.shadow,
|
|
399
|
+
}),
|
|
400
|
+
)
|
|
401
|
+
add(ctx.parent, light)
|
|
402
|
+
syncNode(light, props)
|
|
403
|
+
createEffect(
|
|
404
|
+
() => [props.direction, props.color, props.intensity, props.castShadow, props.shadow] as const,
|
|
405
|
+
([direction, color, intensity, castShadow, shadow]) => setLight(light, { direction, color, intensity, castShadow, shadow }),
|
|
406
|
+
)
|
|
407
|
+
untrack(() => props.ref)?.(light)
|
|
408
|
+
onCleanup(() => remove(light))
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Geometry on the GPU: the lazy buffer step for geometry.ts's data. Buffers
|
|
2
|
+
// (and the picking shape - the spatial core's own copy of positions, UVs
|
|
3
|
+
// and indices for the triangle narrowphase, one per geometry however many
|
|
4
|
+
// meshes share it) are created on first acquire and shared by every mesh
|
|
5
|
+
// and scene drawing the geometry; each draw entry holds one reference, and
|
|
6
|
+
// the buffers are
|
|
7
|
+
// freed when the last reference is released - deferred to a microtask, so
|
|
8
|
+
// a same-tick entry rebuild (a material swap, a geometry that comes right
|
|
9
|
+
// back) keeps its upload. The handles and the reference count live in a
|
|
10
|
+
// map private to this module, keeping Geometry itself plain data.
|
|
11
|
+
// disposeGeometry frees immediately, the explicit override; either way the
|
|
12
|
+
// geometry stays usable - fresh buffers are created on next acquire.
|
|
13
|
+
|
|
14
|
+
import { createBuffer, destroyBuffer } from "@solidrt/core/gpu"
|
|
15
|
+
import type { BufferId, IndexFormat } from "@solidrt/core/gpu"
|
|
16
|
+
import { createShape, destroyShape } from "flux:spatial"
|
|
17
|
+
import type { ShapeId } from "flux:spatial"
|
|
18
|
+
import { layoutStride } from "./geometry.ts"
|
|
19
|
+
import type { Geometry } from "./geometry.ts"
|
|
20
|
+
|
|
21
|
+
/** An acquired reference to a geometry's GPU buffers: what a draw entry
|
|
22
|
+
* binds, and the token releaseGeometryBuffers takes - releasing the exact
|
|
23
|
+
* acquisition keeps the pairing correct however the caller's geometry
|
|
24
|
+
* fields have moved since. */
|
|
25
|
+
export type GeometryBuffers = {
|
|
26
|
+
buffer: BufferId
|
|
27
|
+
index: BufferId
|
|
28
|
+
indexFormat: IndexFormat
|
|
29
|
+
/** The picking shape (positions at 0, uv at 6 of every layout). */
|
|
30
|
+
shape: ShapeId
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type GpuEntry = GeometryBuffers & { geometry: Geometry; refs: number }
|
|
34
|
+
|
|
35
|
+
let entries = new WeakMap<Geometry, GpuEntry>()
|
|
36
|
+
|
|
37
|
+
/** The geometry's GPU buffers, created on first use, plus the index format
|
|
38
|
+
* the draw entry must bind them with. Takes a reference - pair every
|
|
39
|
+
* acquire with a releaseGeometryBuffers of the returned token when the
|
|
40
|
+
* entry built from it goes. */
|
|
41
|
+
export function acquireGeometryBuffers(geometry: Geometry): GeometryBuffers {
|
|
42
|
+
let entry = entries.get(geometry)
|
|
43
|
+
if (entry === undefined) {
|
|
44
|
+
entry = {
|
|
45
|
+
geometry,
|
|
46
|
+
buffer: createBuffer(geometry.vertices, {
|
|
47
|
+
autoFree: false,
|
|
48
|
+
label: geometry.label ? geometry.label + "-verts" : undefined,
|
|
49
|
+
}),
|
|
50
|
+
index: createBuffer(geometry.indices, {
|
|
51
|
+
autoFree: false,
|
|
52
|
+
label: geometry.label ? geometry.label + "-indices" : undefined,
|
|
53
|
+
}),
|
|
54
|
+
indexFormat: geometry.indices instanceof Uint32Array ? "uint32" : "uint16",
|
|
55
|
+
shape: createShape(geometry.vertices, layoutStride(geometry.layout), 0, 6, geometry.indices),
|
|
56
|
+
refs: 0,
|
|
57
|
+
}
|
|
58
|
+
entries.set(geometry, entry)
|
|
59
|
+
}
|
|
60
|
+
entry.refs++
|
|
61
|
+
return entry
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Release one acquire. At zero references the buffers are freed at the
|
|
65
|
+
* end of the microtask; an acquire before then keeps them, so a detach and
|
|
66
|
+
* re-attach in one tick never re-uploads. A token orphaned by an explicit
|
|
67
|
+
* disposeGeometry releases against the orphan, never against a successor's
|
|
68
|
+
* fresh buffers. */
|
|
69
|
+
export function releaseGeometryBuffers(acquired: GeometryBuffers): void {
|
|
70
|
+
let entry = acquired as GpuEntry
|
|
71
|
+
if (entry.refs === 0) return
|
|
72
|
+
entry.refs--
|
|
73
|
+
if (entry.refs > 0) return
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
if (entries.get(entry.geometry) !== entry || entry.refs > 0) return
|
|
76
|
+
entries.delete(entry.geometry)
|
|
77
|
+
destroyBuffer(entry.buffer)
|
|
78
|
+
destroyBuffer(entry.index)
|
|
79
|
+
destroyShape(entry.shape)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Free the geometry's GPU buffers now, held references or not - the
|
|
85
|
+
* explicit override for geometry an app is done with for good. Draw
|
|
86
|
+
* entries created from them hold their own reference, so destruction order
|
|
87
|
+
* is safe; the geometry can be used again afterwards (fresh buffers are
|
|
88
|
+
* created on next use).
|
|
89
|
+
*/
|
|
90
|
+
export function disposeGeometry(geometry: Geometry): void {
|
|
91
|
+
let entry = entries.get(geometry)
|
|
92
|
+
if (entry === undefined) return
|
|
93
|
+
entries.delete(geometry)
|
|
94
|
+
destroyBuffer(entry.buffer)
|
|
95
|
+
destroyBuffer(entry.index)
|
|
96
|
+
destroyShape(entry.shape)
|
|
97
|
+
}
|