@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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { createEffect, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack } from "@solidrt/core"
|
|
2
|
+
import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
|
|
3
|
+
import { SceneContext } from "./context.tsx"
|
|
4
|
+
import { createScene } from "../scene.ts"
|
|
5
|
+
import { feedPointer } from "../scene-pointer.ts"
|
|
6
|
+
import type { EnvironmentOptions, FogOptions, Scene as SceneHandle, SkyboxOptions, ToneMapping } from "../scene.ts"
|
|
7
|
+
import type { ScenePointerEvent, SceneTapEvent, SceneWheelEvent } from "../node.ts"
|
|
8
|
+
import type { CameraUpdate } from "../camera.ts"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The scene's own pointer events, the root of the walk: every event the
|
|
12
|
+
* nodes let through arrives here last - `event.mesh` is the hit it
|
|
13
|
+
* bubbled from, or null over empty space (a tap there with `mesh` null is
|
|
14
|
+
* "deselect"), `x`/`y` scene pixels. scene.listen as props; an
|
|
15
|
+
* `<OrbitCamera>` child's feed listens at the same root.
|
|
16
|
+
*/
|
|
17
|
+
export type ScenePointerProps = {
|
|
18
|
+
onPointerDown?: (event: ScenePointerEvent) => void
|
|
19
|
+
onPointerMove?: (event: ScenePointerEvent) => void
|
|
20
|
+
onPointerUp?: (event: ScenePointerEvent) => void
|
|
21
|
+
onWheel?: (event: SceneWheelEvent) => void
|
|
22
|
+
onTap?: (event: SceneTapEvent) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SceneProps = ScenePointerProps & {
|
|
26
|
+
/**
|
|
27
|
+
* Target pixels - give both, or neither. Omitted, the scene FILLS: the
|
|
28
|
+
* built-in leaf is laid out at 100% of its parent's box (give it a sized
|
|
29
|
+
* parent, as on the web) and the target tracks the leaf's on-screen size
|
|
30
|
+
* in device pixels - display scale, designSize fits and ancestor
|
|
31
|
+
* transforms included - so a bare `<Scene>` renders at native density on
|
|
32
|
+
* any display, and viewport-relative camera controls scale with the box.
|
|
33
|
+
* Fill or fixed is decided at mount. `output` needs explicit sizes (the
|
|
34
|
+
* target cannot follow a leaf it does not own); the leaf's own
|
|
35
|
+
* width/height are then layout, so render size and display size separate
|
|
36
|
+
* (supersampling).
|
|
37
|
+
*/
|
|
38
|
+
width?: number
|
|
39
|
+
height?: number
|
|
40
|
+
clearColor?: [number, number, number, number]
|
|
41
|
+
/**
|
|
42
|
+
* Drive the scene camera declaratively (scene.setCamera as a prop): a
|
|
43
|
+
* partial CameraUpdate, absent keys keep their values - `ortho` included,
|
|
44
|
+
* which `<PerspectiveCamera>` by its name never sets. The prop and the
|
|
45
|
+
* `<PerspectiveCamera>` child write the same scene state, so use one
|
|
46
|
+
* form, not both (last write wins). The 2d layers' `camera` prop, one
|
|
47
|
+
* dimension up.
|
|
48
|
+
*/
|
|
49
|
+
camera?: CameraUpdate
|
|
50
|
+
/** The background drawn behind the meshes, inside the scene's own pass
|
|
51
|
+
* (scene.setBackground): fragment GLSL (vUV/iResolution/fragColor plus
|
|
52
|
+
* the vRay view ray, so a createShaderTexture backdrop ports verbatim
|
|
53
|
+
* and a directional sky is a few lines) or a skybox `{ cube, intensity?,
|
|
54
|
+
* rotation? }`. Reactive - swapping the source replaces the background,
|
|
55
|
+
* a skybox's knobs update in place; undefined removes it. Three's
|
|
56
|
+
* `scene.background = color` is `clearColor` here. */
|
|
57
|
+
background?: string | SkyboxOptions
|
|
58
|
+
/** The cube map reflective `lit` materials mirror (scene.setEnvironment):
|
|
59
|
+
* `{ cube, intensity?, rotation? }`, typically the skybox's own cube
|
|
60
|
+
* turned with it. Reactive; undefined removes it. */
|
|
61
|
+
environment?: EnvironmentOptions
|
|
62
|
+
/** Scene-wide fog (scene.setFog): linear `{ color, near, far }` or exp2
|
|
63
|
+
* `{ color, density }`, optionally thinning above `height` by
|
|
64
|
+
* `heightFalloff`; every standard material fades toward `color` by
|
|
65
|
+
* distance from the camera. Reactive; undefined removes it. Match
|
|
66
|
+
* `color` to clearColor or the background, which is not fogged. */
|
|
67
|
+
fog?: FogOptions
|
|
68
|
+
/** Output tone mapping (scene.setToneMapping): "none" (default) or
|
|
69
|
+
* "aces". Reactive. */
|
|
70
|
+
toneMapping?: ToneMapping
|
|
71
|
+
/** Output exposure (scene.setExposure), default 1. Reactive. */
|
|
72
|
+
exposure?: number
|
|
73
|
+
/** The scene target's layer mask (scene.setLayers as a prop; default 1):
|
|
74
|
+
* the scene draws the meshes whose `layers` intersect it. Reactive. */
|
|
75
|
+
layers?: number
|
|
76
|
+
/** `"texture"` exposes the target's depth as `scene.depthTexture` (a
|
|
77
|
+
* depth-reading post effect's input). Fixed at creation; not with
|
|
78
|
+
* `samples`. */
|
|
79
|
+
depth?: true | "texture"
|
|
80
|
+
label?: string
|
|
81
|
+
/** Multisample count (1, 2, 4 or 8; default 1): anti-aliased mesh edges.
|
|
82
|
+
* Fixed at creation. */
|
|
83
|
+
samples?: 1 | 2 | 4 | 8
|
|
84
|
+
ref?: (scene: SceneHandle) => void
|
|
85
|
+
/**
|
|
86
|
+
* Compose the output yourself: called once (untracked) with the scene's
|
|
87
|
+
* texture id, and its return renders in place of the built-in `<texture>`
|
|
88
|
+
* leaf - a `<d-texture>`, a leaf carrying paint/pointer/layout props, or
|
|
89
|
+
* a post-effect chain (a shader target sampling the id; created in the
|
|
90
|
+
* callback it disposes with the Scene). Return null to render no leaf.
|
|
91
|
+
* Pointer events then need the scene's handlers on your leaf:
|
|
92
|
+
* `<texture src={texture} {...useScene().scene.handlers} />` - one
|
|
93
|
+
* spread carries the node events, the scene's own and the pointer
|
|
94
|
+
* feed, which listens at the scene's root.
|
|
95
|
+
*/
|
|
96
|
+
output?: (texture: TextureId) => Element
|
|
97
|
+
/**
|
|
98
|
+
* Pointer events (default on): the built-in leaf carries scene.handlers,
|
|
99
|
+
* so nodes, the scene's own handlers and a camera control's feed
|
|
100
|
+
* receive input. `false` detaches them - the leaf then costs no pointer
|
|
101
|
+
* routing at all.
|
|
102
|
+
*/
|
|
103
|
+
events?: boolean
|
|
104
|
+
/**
|
|
105
|
+
* The pointer feed of the scene's leaf (createPointerFeed), fed from
|
|
106
|
+
* the scene's root - the events the nodes let through: a mesh that
|
|
107
|
+
* claims its press (stopPropagation on its down) keeps a camera control
|
|
108
|
+
* bound to the feed out of that drag. The feed's gestures - drag,
|
|
109
|
+
* pinch, two-finger pan, wheel, mouse motion under lock - come from
|
|
110
|
+
* this scene, and an input map binds them to the camera controls
|
|
111
|
+
* inside; `useScene().pointer` is this feed. Fixed at creation. Without
|
|
112
|
+
* one the leaf feeds no gestures and the controls move only through
|
|
113
|
+
* their handles.
|
|
114
|
+
*/
|
|
115
|
+
pointer?: PointerFeed
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Owns a draw target and composites it as an ordinary `<texture>` leaf, so
|
|
120
|
+
* the output takes layout, transforms, blendMode, and pointer events like
|
|
121
|
+
* any element - or hand `output` the texture id and compose it yourself.
|
|
122
|
+
* Children (Mesh/Group/PerspectiveCamera) render nothing themselves - they
|
|
123
|
+
* populate the retained scene through context.
|
|
124
|
+
*/
|
|
125
|
+
// Creation size of a fill-mode target: the first onLayout replaces it
|
|
126
|
+
// before the first paint, so it only has to be a valid texture size.
|
|
127
|
+
const FILL_INITIAL_SIZE = 1
|
|
128
|
+
|
|
129
|
+
export let Scene: ParentComponent<SceneProps> = props => {
|
|
130
|
+
// Fill vs fixed target is decided at mount, like `output`: both
|
|
131
|
+
// width and height (fixed pixels) or neither (fill).
|
|
132
|
+
let fill = untrack(() => {
|
|
133
|
+
if ((props.width === undefined) !== (props.height === undefined)) {
|
|
134
|
+
throw new Error("Scene: width and height come together - give both (fixed target) or neither (fill)")
|
|
135
|
+
}
|
|
136
|
+
if (props.width === undefined && props.output) {
|
|
137
|
+
throw new Error("Scene: fill needs the built-in leaf - with output, give width and height")
|
|
138
|
+
}
|
|
139
|
+
return props.width === undefined
|
|
140
|
+
})
|
|
141
|
+
// The initial props seed createScene, so `ref` (below) hands out a
|
|
142
|
+
// configured scene and the first frame draws it whole; the effects
|
|
143
|
+
// after this follow changes only (`defer`).
|
|
144
|
+
let scene = untrack(() => {
|
|
145
|
+
let s = createScene(props.width ?? FILL_INITIAL_SIZE, props.height ?? FILL_INITIAL_SIZE, {
|
|
146
|
+
clearColor: props.clearColor,
|
|
147
|
+
label: props.label,
|
|
148
|
+
samples: props.samples,
|
|
149
|
+
depth: props.depth,
|
|
150
|
+
background: props.background,
|
|
151
|
+
environment: props.environment,
|
|
152
|
+
fog: props.fog,
|
|
153
|
+
toneMapping: props.toneMapping,
|
|
154
|
+
exposure: props.exposure,
|
|
155
|
+
layers: props.layers,
|
|
156
|
+
})
|
|
157
|
+
if (props.camera) s.setCamera(props.camera)
|
|
158
|
+
return s
|
|
159
|
+
})
|
|
160
|
+
createEffect(
|
|
161
|
+
() => [props.width, props.height] as const,
|
|
162
|
+
([w, h]) => {
|
|
163
|
+
if ((w === undefined) !== (h === undefined) || (w === undefined) !== fill) {
|
|
164
|
+
throw new Error("Scene: fill/fixed is mount-fixed - width/height cannot appear or disappear")
|
|
165
|
+
}
|
|
166
|
+
if (!fill) scene.setSize(w!, h!)
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
createEffect(
|
|
170
|
+
() => props.camera,
|
|
171
|
+
camera => {
|
|
172
|
+
if (camera) scene.setCamera(camera)
|
|
173
|
+
},
|
|
174
|
+
{ defer: true },
|
|
175
|
+
)
|
|
176
|
+
createEffect(
|
|
177
|
+
() => props.background,
|
|
178
|
+
b => scene.setBackground(b ?? null),
|
|
179
|
+
{ defer: true },
|
|
180
|
+
)
|
|
181
|
+
createEffect(
|
|
182
|
+
() => props.environment,
|
|
183
|
+
e => scene.setEnvironment(e ?? null),
|
|
184
|
+
{ defer: true },
|
|
185
|
+
)
|
|
186
|
+
createEffect(
|
|
187
|
+
() => props.fog,
|
|
188
|
+
f => scene.setFog(f ?? null),
|
|
189
|
+
{ defer: true },
|
|
190
|
+
)
|
|
191
|
+
createEffect(
|
|
192
|
+
() => props.toneMapping,
|
|
193
|
+
t => scene.setToneMapping(t ?? "none"),
|
|
194
|
+
{ defer: true },
|
|
195
|
+
)
|
|
196
|
+
createEffect(
|
|
197
|
+
() => props.exposure,
|
|
198
|
+
e => scene.setExposure(e ?? 1),
|
|
199
|
+
{ defer: true },
|
|
200
|
+
)
|
|
201
|
+
createEffect(
|
|
202
|
+
() => props.layers,
|
|
203
|
+
l => scene.setLayers(l ?? 1),
|
|
204
|
+
{ defer: true },
|
|
205
|
+
)
|
|
206
|
+
untrack(() => props.ref)?.(scene)
|
|
207
|
+
// The pointer feed the app handed in listens at the scene's root, and
|
|
208
|
+
// children reach it through useScene().pointer. Fixed at creation, like
|
|
209
|
+
// output.
|
|
210
|
+
let pointer = untrack(() => props.pointer) ?? null
|
|
211
|
+
if (pointer) onCleanup(feedPointer(scene, pointer))
|
|
212
|
+
// The scene's own handlers at the root of the walk; the props are read
|
|
213
|
+
// per event, so a handler prop may change without re-registering.
|
|
214
|
+
onCleanup(
|
|
215
|
+
scene.listen({
|
|
216
|
+
onPointerDown: e => props.onPointerDown?.(e),
|
|
217
|
+
onPointerMove: e => props.onPointerMove?.(e),
|
|
218
|
+
onPointerUp: e => props.onPointerUp?.(e),
|
|
219
|
+
onWheel: e => props.onWheel?.(e),
|
|
220
|
+
onTap: e => props.onTap?.(e),
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
223
|
+
let output = untrack(() => props.output)
|
|
224
|
+
let events = untrack(() => props.events) !== false
|
|
225
|
+
let leafNode: { id: number } | undefined
|
|
226
|
+
// The built-in leaf's laid-out box in ITS units - what pointer
|
|
227
|
+
// localX/localY report in and every handlersFor scales against. Fixed
|
|
228
|
+
// mode lays the leaf out at the target size; fill mode reads the box
|
|
229
|
+
// back (getLayoutBox, the untransformed read, so a designSize fit or
|
|
230
|
+
// ancestor transform never skews events). Zero before the first layout:
|
|
231
|
+
// no event can arrive before one, and a degenerate layout passes
|
|
232
|
+
// coordinates through unscaled. A custom output leaf registers its own
|
|
233
|
+
// layout via handlersFor.
|
|
234
|
+
let builtinLayout = fill
|
|
235
|
+
? () => (leafNode && getLayoutBox(leafNode)) || { width: 0, height: 0 }
|
|
236
|
+
: () => ({ width: props.width!, height: props.height! })
|
|
237
|
+
if (fill) {
|
|
238
|
+
// Fill sizing: after every layout (and on display-scale changes, which
|
|
239
|
+
// lay nothing out) the target follows the leaf's on-screen box in
|
|
240
|
+
// device pixels - getBoundingBoxViewport composes designSize fits and
|
|
241
|
+
// ancestor transforms, so the scene renders at true density wherever
|
|
242
|
+
// it sits. onLayout runs before paint (no frame draws at a stale
|
|
243
|
+
// size); setSize no-ops when nothing changed. apply runs as an
|
|
244
|
+
// onLayout handler and as an effect apply, both untracked scopes, so
|
|
245
|
+
// its scale read is wrapped in an explicit untrack (the effect's
|
|
246
|
+
// compute is what tracks it).
|
|
247
|
+
let apply = () =>
|
|
248
|
+
untrack(() => {
|
|
249
|
+
if (!leafNode) return
|
|
250
|
+
let box = getBoundingBoxViewport(leafNode)
|
|
251
|
+
if (!box) return
|
|
252
|
+
let scale = displayScale()
|
|
253
|
+
scene.setSize(Math.max(1, Math.round(box.width * scale)), Math.max(1, Math.round(box.height * scale)))
|
|
254
|
+
})
|
|
255
|
+
onLayout(apply)
|
|
256
|
+
createEffect(() => displayScale(), apply)
|
|
257
|
+
}
|
|
258
|
+
// Pointer events on the built-in leaf: at target size the plain
|
|
259
|
+
// handlers, in fill mode scaled from the laid-out box.
|
|
260
|
+
let sceneHandlers = fill ? scene.handlersFor(builtinLayout) : scene.handlers
|
|
261
|
+
return (
|
|
262
|
+
<SceneContext value={{ scene, parent: scene.root, viewport: scene, pointer }}>
|
|
263
|
+
{output ? (
|
|
264
|
+
untrack(() => output(scene.texture))
|
|
265
|
+
) : (
|
|
266
|
+
<texture
|
|
267
|
+
ref={(n: { id: number }) => (leafNode = n)}
|
|
268
|
+
src={scene.texture}
|
|
269
|
+
width={fill ? "100%" : props.width}
|
|
270
|
+
height={fill ? "100%" : props.height}
|
|
271
|
+
onPointerDown={events ? sceneHandlers.onPointerDown : undefined}
|
|
272
|
+
onPointerMove={events ? sceneHandlers.onPointerMove : undefined}
|
|
273
|
+
onPointerUp={events ? sceneHandlers.onPointerUp : undefined}
|
|
274
|
+
onPointerLeave={events ? sceneHandlers.onPointerLeave : undefined}
|
|
275
|
+
onWheel={events ? sceneHandlers.onWheel : undefined}
|
|
276
|
+
/>
|
|
277
|
+
)}
|
|
278
|
+
{props.children}
|
|
279
|
+
</SceneContext>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
|
+
import type { VoidComponent } from "@solidrt/core"
|
|
3
|
+
import { SceneContext } from "./context.tsx"
|
|
4
|
+
import { syncNode } from "./node-props.ts"
|
|
5
|
+
import type { TransformProps } from "./node-props.ts"
|
|
6
|
+
import { add, remove } from "../node.ts"
|
|
7
|
+
import { createSpotLight, setLight } from "../light.ts"
|
|
8
|
+
import type { SpotLight as SpotLightNode, SpotShadowOptions } from "../light.ts"
|
|
9
|
+
import type { Vec3 } from "../math.ts"
|
|
10
|
+
|
|
11
|
+
export type SpotLightProps = TransformProps & {
|
|
12
|
+
/** Aim direction in the node's local space; default [0, -1, 0], a
|
|
13
|
+
* lamp pointing straight down. */
|
|
14
|
+
direction?: Vec3
|
|
15
|
+
color?: Vec3
|
|
16
|
+
intensity?: number
|
|
17
|
+
/** Falloff cutoff in world units (0 = no cutoff). */
|
|
18
|
+
distance?: number
|
|
19
|
+
/** Cone half-angle in DEGREES, (0, 90]; default 60 (degrees like
|
|
20
|
+
* camera fov; Three's radians convert as `angle * 180 / PI`). */
|
|
21
|
+
angle?: number
|
|
22
|
+
/** 0..1 fraction of the cone fading to the rim; default 0. */
|
|
23
|
+
penumbra?: number
|
|
24
|
+
/** Falloff exponent; default 2 (inverse square). */
|
|
25
|
+
decay?: number
|
|
26
|
+
/** Render a shadow map from this light (a perspective map of its
|
|
27
|
+
* cone; one shadow slot). */
|
|
28
|
+
castShadow?: boolean
|
|
29
|
+
/** Shadow-map options (mapSize, bias, normalBias, near), merged key
|
|
30
|
+
* by key. */
|
|
31
|
+
shadow?: SpotShadowOptions
|
|
32
|
+
ref?: (light: SpotLightNode) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A spot light node (createSpotLight): a cone from the node's world
|
|
36
|
+
* position along its local `direction` - give it a `position`, and aim
|
|
37
|
+
* it with `direction` or a parent's rotation. Counts against MAX_LIGHTS
|
|
38
|
+
* with the other non-ambient lights. */
|
|
39
|
+
export let SpotLight: VoidComponent<SpotLightProps> = props => {
|
|
40
|
+
let ctx = useContext(SceneContext)
|
|
41
|
+
let light = untrack(() =>
|
|
42
|
+
createSpotLight({
|
|
43
|
+
direction: props.direction,
|
|
44
|
+
color: props.color,
|
|
45
|
+
intensity: props.intensity,
|
|
46
|
+
distance: props.distance,
|
|
47
|
+
angle: props.angle,
|
|
48
|
+
penumbra: props.penumbra,
|
|
49
|
+
decay: props.decay,
|
|
50
|
+
castShadow: props.castShadow,
|
|
51
|
+
shadow: props.shadow,
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
add(ctx.parent, light)
|
|
55
|
+
syncNode(light, props)
|
|
56
|
+
createEffect(
|
|
57
|
+
() =>
|
|
58
|
+
[props.direction, props.color, props.intensity, props.distance, props.angle, props.penumbra, props.decay, props.castShadow, props.shadow] as const,
|
|
59
|
+
([direction, color, intensity, distance, angle, penumbra, decay, castShadow, shadow]) =>
|
|
60
|
+
setLight(light, { direction, color, intensity, distance, angle, penumbra, decay, castShadow, shadow }),
|
|
61
|
+
)
|
|
62
|
+
untrack(() => props.ref)?.(light)
|
|
63
|
+
onCleanup(() => remove(light))
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
|
+
import type { VoidComponent } from "@solidrt/core"
|
|
3
|
+
import { SceneContext } from "./context.tsx"
|
|
4
|
+
import type { TransformProps, PointerEventProps } from "./node-props.ts"
|
|
5
|
+
import { syncMesh } from "./mesh.tsx"
|
|
6
|
+
import { add, remove } from "../node.ts"
|
|
7
|
+
import { createSprite } from "../mesh.ts"
|
|
8
|
+
import type { Mesh as MeshNode } from "../mesh.ts"
|
|
9
|
+
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
10
|
+
import type { Material } from "../material.ts"
|
|
11
|
+
|
|
12
|
+
export type SpriteProps = TransformProps & PointerEventProps & {
|
|
13
|
+
/** A `sprite()` material (any material draws, only a sprite one turns). */
|
|
14
|
+
material: Material
|
|
15
|
+
/** Per-mesh uniforms, merge semantics - as on Mesh. */
|
|
16
|
+
params?: ShaderParams
|
|
17
|
+
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
18
|
+
renderOrder?: number
|
|
19
|
+
/** Layer membership bitmask (setLayers as a prop; default 1). */
|
|
20
|
+
layers?: number
|
|
21
|
+
/** Frustum culling switch (setCulling as a prop; default true). */
|
|
22
|
+
frustumCulled?: boolean
|
|
23
|
+
/** World units the culled box grows by (setCulling as a prop; default 0). */
|
|
24
|
+
cullMargin?: number
|
|
25
|
+
ref?: (mesh: MeshNode) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A camera-facing unit quad (createSprite as a component): no geometry
|
|
29
|
+
* prop, `scale` is its world size, rotation is ignored. */
|
|
30
|
+
export let Sprite: VoidComponent<SpriteProps> = props => {
|
|
31
|
+
let ctx = useContext(SceneContext)
|
|
32
|
+
let mesh = untrack(() => createSprite(props.material))
|
|
33
|
+
add(ctx.parent, mesh)
|
|
34
|
+
syncMesh(mesh, props)
|
|
35
|
+
untrack(() => props.ref)?.(mesh)
|
|
36
|
+
onCleanup(() => remove(mesh))
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
|
+
import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
|
|
3
|
+
import { SceneContext } from "./context.tsx"
|
|
4
|
+
import { feedPointer } from "../scene-pointer.ts"
|
|
5
|
+
import type { ViewHandle, ViewOptions } from "../scene.ts"
|
|
6
|
+
import type { CameraUpdate } from "../camera.ts"
|
|
7
|
+
import type { ScenePointerProps } from "./scene.tsx"
|
|
8
|
+
|
|
9
|
+
export type View3dProps = Pick<ViewOptions, "clearColor" | "label" | "overrideMaterial" | "fog" | "depth" | "samples" | "filter" | "wrap" | "into"> &
|
|
10
|
+
ScenePointerProps & {
|
|
11
|
+
/** Target pixels, live: the view resizes, or its tile moves. Fixed
|
|
12
|
+
* only, for now: a fill mode like Scene's is a later, additive step. */
|
|
13
|
+
width: number
|
|
14
|
+
height: number
|
|
15
|
+
/** The tile's top-left in `into` (view.setRect as props), live; without
|
|
16
|
+
* `into` they mean nothing. */
|
|
17
|
+
x?: number
|
|
18
|
+
y?: number
|
|
19
|
+
/** The view's layer mask (view.setLayers as a prop; default 1). Live. */
|
|
20
|
+
layers?: number
|
|
21
|
+
/** Partial camera update on the view's own camera (view.setCamera):
|
|
22
|
+
* absent keys keep their values. Live. The `<PerspectiveCamera>` child
|
|
23
|
+
* writes the same state - use one form, not both. */
|
|
24
|
+
camera?: CameraUpdate
|
|
25
|
+
/**
|
|
26
|
+
* Compose the leaf yourself: called once (untracked) with view.texture,
|
|
27
|
+
* and its return renders in place of the built-in `<texture>` leaf.
|
|
28
|
+
* Pointer events through the view then need its handlers on your leaf:
|
|
29
|
+
* `<texture src={texture} {...view.handlers} />` (the view from `ref`);
|
|
30
|
+
* the view's pointer feed listens at its root. A tiled view's id is a
|
|
31
|
+
* draw target, not a texture: show `into` with srcX/srcY, as the
|
|
32
|
+
* built-in leaf does.
|
|
33
|
+
*/
|
|
34
|
+
output?: (texture: TextureId) => Element
|
|
35
|
+
/** Pointer events (default on): the built-in leaf carries the view's
|
|
36
|
+
* handlers. `false` detaches them. */
|
|
37
|
+
events?: boolean
|
|
38
|
+
/** The pointer feed of this view's leaf (see Scene's `pointer`), fed
|
|
39
|
+
* from the view's root - the events the nodes let through - so a map
|
|
40
|
+
* bound to it drives the camera controls inside this view and no other;
|
|
41
|
+
* inside, `useScene().pointer` is this feed. Fixed at creation. */
|
|
42
|
+
pointer?: PointerFeed
|
|
43
|
+
ref?: (view: ViewHandle) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A second rendering of the enclosing scene from a camera of its own
|
|
48
|
+
* (scene.createView as a component): the same meshes, materials, lights
|
|
49
|
+
* and shadow maps, another target and one entry per mesh, so it costs no
|
|
50
|
+
* per-frame JS. Composites as an ordinary `<texture>` leaf at the target
|
|
51
|
+
* size (a tile of `into` shown through srcX/srcY), or through `output`.
|
|
52
|
+
* Camera-control children (`<OrbitCamera>`, `<FirstPersonCamera>`,
|
|
53
|
+
* `<PerspectiveCamera>`) drive the VIEW's camera: inside, `useScene()`
|
|
54
|
+
* reports the view as `viewport` and the view leaf's feed as `pointer`,
|
|
55
|
+
* so a map over that feed moves this view alone. Node children (`<Mesh>`) mount
|
|
56
|
+
* to the scene as they would outside - a view mirrors the scene's meshes,
|
|
57
|
+
* it has none of its own. Nodes under the view's leaf get their ordinary
|
|
58
|
+
* pointer handlers, picked through the view's camera; the view's own
|
|
59
|
+
* `onPointer*`/`onWheel`/`onTap` are the last stop of that walk
|
|
60
|
+
* (`event.mesh` null over empty space).
|
|
61
|
+
*/
|
|
62
|
+
export let View3d: ParentComponent<View3dProps> = props => {
|
|
63
|
+
let ctx = useContext(SceneContext)
|
|
64
|
+
// A tile of `into` or a target of its own is decided at mount, like
|
|
65
|
+
// Scene's fill/fixed: `into` cannot appear or disappear.
|
|
66
|
+
let tiled = untrack(() => props.into !== undefined)
|
|
67
|
+
// The initial props seed createView, so `ref` hands out a configured
|
|
68
|
+
// view and the first frame draws it whole; the effects follow changes
|
|
69
|
+
// only (`defer`).
|
|
70
|
+
let view = untrack(() => {
|
|
71
|
+
let v = ctx.scene.createView({
|
|
72
|
+
width: props.width,
|
|
73
|
+
height: props.height,
|
|
74
|
+
x: props.x,
|
|
75
|
+
y: props.y,
|
|
76
|
+
layers: props.layers,
|
|
77
|
+
clearColor: props.clearColor,
|
|
78
|
+
label: props.label,
|
|
79
|
+
overrideMaterial: props.overrideMaterial,
|
|
80
|
+
fog: props.fog,
|
|
81
|
+
depth: props.depth,
|
|
82
|
+
samples: props.samples,
|
|
83
|
+
filter: props.filter,
|
|
84
|
+
wrap: props.wrap,
|
|
85
|
+
into: props.into,
|
|
86
|
+
})
|
|
87
|
+
if (props.camera) v.setCamera(props.camera)
|
|
88
|
+
return v
|
|
89
|
+
})
|
|
90
|
+
createEffect(
|
|
91
|
+
() => props.camera,
|
|
92
|
+
camera => {
|
|
93
|
+
if (camera) view.setCamera(camera)
|
|
94
|
+
},
|
|
95
|
+
{ defer: true },
|
|
96
|
+
)
|
|
97
|
+
createEffect(
|
|
98
|
+
() => [props.x, props.y, props.width, props.height] as const,
|
|
99
|
+
([x, y, width, height]) => {
|
|
100
|
+
if (tiled) view.setRect({ x: x ?? 0, y: y ?? 0, width, height })
|
|
101
|
+
else view.setSize(width, height)
|
|
102
|
+
},
|
|
103
|
+
{ defer: true },
|
|
104
|
+
)
|
|
105
|
+
createEffect(
|
|
106
|
+
() => props.layers,
|
|
107
|
+
l => view.setLayers(l ?? 1),
|
|
108
|
+
{ defer: true },
|
|
109
|
+
)
|
|
110
|
+
untrack(() => props.ref)?.(view)
|
|
111
|
+
onCleanup(() => view.dispose())
|
|
112
|
+
// The pointer feed the app handed in (see Scene) listens at the view's
|
|
113
|
+
// root; children reach it through useScene().pointer.
|
|
114
|
+
let pointer = untrack(() => props.pointer) ?? null
|
|
115
|
+
if (pointer) onCleanup(feedPointer(view, pointer))
|
|
116
|
+
// The view's own handlers at the root of the walk; the props are read
|
|
117
|
+
// per event, so a handler prop may change without re-registering.
|
|
118
|
+
onCleanup(
|
|
119
|
+
view.listen({
|
|
120
|
+
onPointerDown: e => props.onPointerDown?.(e),
|
|
121
|
+
onPointerMove: e => props.onPointerMove?.(e),
|
|
122
|
+
onPointerUp: e => props.onPointerUp?.(e),
|
|
123
|
+
onWheel: e => props.onWheel?.(e),
|
|
124
|
+
onTap: e => props.onTap?.(e),
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
let output = untrack(() => props.output)
|
|
128
|
+
let events = untrack(() => props.events) !== false
|
|
129
|
+
return (
|
|
130
|
+
<SceneContext value={{ scene: ctx.scene, parent: ctx.parent, viewport: view, pointer }}>
|
|
131
|
+
{output ? (
|
|
132
|
+
untrack(() => output(view.texture))
|
|
133
|
+
) : (
|
|
134
|
+
<texture
|
|
135
|
+
src={tiled ? props.into : view.texture}
|
|
136
|
+
width={props.width}
|
|
137
|
+
height={props.height}
|
|
138
|
+
srcX={tiled ? (props.x ?? 0) : undefined}
|
|
139
|
+
srcY={tiled ? (props.y ?? 0) : undefined}
|
|
140
|
+
srcW={tiled ? props.width : undefined}
|
|
141
|
+
srcH={tiled ? props.height : undefined}
|
|
142
|
+
onPointerDown={events ? view.handlers.onPointerDown : undefined}
|
|
143
|
+
onPointerMove={events ? view.handlers.onPointerMove : undefined}
|
|
144
|
+
onPointerUp={events ? view.handlers.onPointerUp : undefined}
|
|
145
|
+
onPointerLeave={events ? view.handlers.onPointerLeave : undefined}
|
|
146
|
+
onWheel={events ? view.handlers.onWheel : undefined}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
{props.children}
|
|
150
|
+
</SceneContext>
|
|
151
|
+
)
|
|
152
|
+
}
|