@opsydyn/effect-three 0.0.0-stage → 0.2.0
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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +189 -2
- package/dist/index-01kwfekm.js +5118 -0
- package/dist/index-01kwfekm.js.map +40 -0
- package/dist/index-4045wme0.js +129 -0
- package/dist/index-4045wme0.js.map +10 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1361 -0
- package/dist/index.js.map +14 -0
- package/dist/kernel/animation.d.ts +14 -0
- package/dist/kernel/animation.d.ts.map +1 -0
- package/dist/kernel/behaviours.d.ts +16 -0
- package/dist/kernel/behaviours.d.ts.map +1 -0
- package/dist/kernel/bodies.d.ts +60 -0
- package/dist/kernel/bodies.d.ts.map +1 -0
- package/dist/kernel/bodySources.d.ts +47 -0
- package/dist/kernel/bodySources.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +564 -0
- package/dist/kernel/build.d.ts.map +1 -0
- package/dist/kernel/cameraMotion.d.ts +39 -0
- package/dist/kernel/cameraMotion.d.ts.map +1 -0
- package/dist/kernel/clock.d.ts +17 -0
- package/dist/kernel/clock.d.ts.map +1 -0
- package/dist/kernel/contactShadowPasses.d.ts +47 -0
- package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +1120 -0
- package/dist/kernel/description.d.ts.map +1 -0
- package/dist/kernel/diff.d.ts +90 -0
- package/dist/kernel/diff.d.ts.map +1 -0
- package/dist/kernel/environmentPresets.d.ts +30 -0
- package/dist/kernel/environmentPresets.d.ts.map +1 -0
- package/dist/kernel/equality.d.ts +64 -0
- package/dist/kernel/equality.d.ts.map +1 -0
- package/dist/kernel/frames.d.ts +55 -0
- package/dist/kernel/frames.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +37 -0
- package/dist/kernel/index.d.ts.map +1 -0
- package/dist/kernel/index.js +335 -0
- package/dist/kernel/index.js.map +9 -0
- package/dist/kernel/instances.d.ts +26 -0
- package/dist/kernel/instances.d.ts.map +1 -0
- package/dist/kernel/labels.d.ts +51 -0
- package/dist/kernel/labels.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts +49 -0
- package/dist/kernel/loop.d.ts.map +1 -0
- package/dist/kernel/materials.d.ts +24 -0
- package/dist/kernel/materials.d.ts.map +1 -0
- package/dist/kernel/motion.d.ts +36 -0
- package/dist/kernel/motion.d.ts.map +1 -0
- package/dist/kernel/orbit.d.ts +127 -0
- package/dist/kernel/orbit.d.ts.map +1 -0
- package/dist/kernel/overlays.d.ts +16 -0
- package/dist/kernel/overlays.d.ts.map +1 -0
- package/dist/kernel/performance.d.ts +41 -0
- package/dist/kernel/performance.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +166 -0
- package/dist/kernel/physics.d.ts.map +1 -0
- package/dist/kernel/physicsDebug.d.ts +18 -0
- package/dist/kernel/physicsDebug.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +357 -0
- package/dist/kernel/pointer.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +49 -0
- package/dist/kernel/renderKit.d.ts.map +1 -0
- package/dist/kernel/renderer.d.ts +83 -0
- package/dist/kernel/renderer.d.ts.map +1 -0
- package/dist/kernel/resources.d.ts +112 -0
- package/dist/kernel/resources.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +155 -0
- package/dist/kernel/sceneGraph.d.ts.map +1 -0
- package/dist/kernel/sceneIndex.d.ts +90 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -0
- package/dist/kernel/stage.d.ts +87 -0
- package/dist/kernel/stage.d.ts.map +1 -0
- package/dist/kernel/textLayout.d.ts +29 -0
- package/dist/kernel/textLayout.d.ts.map +1 -0
- package/dist/kernel/tween.d.ts +35 -0
- package/dist/kernel/tween.d.ts.map +1 -0
- package/dist/kernel/xrInput.d.ts +107 -0
- package/dist/kernel/xrInput.d.ts.map +1 -0
- package/dist/kernel/xrLocomotion.d.ts +38 -0
- package/dist/kernel/xrLocomotion.d.ts.map +1 -0
- package/dist/runtime/FrameSource.d.ts +29 -0
- package/dist/runtime/FrameSource.d.ts.map +1 -0
- package/dist/runtime/ResourceRegistry.d.ts +18 -0
- package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
- package/dist/runtime/SceneRuntime.d.ts +68 -0
- package/dist/runtime/SceneRuntime.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +2536 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -0
- package/dist/runtime/errors.d.ts +19 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/webgl/contactShadows.d.ts +5 -0
- package/dist/webgl/contactShadows.d.ts.map +1 -0
- package/dist/webgl/index.d.ts +11 -0
- package/dist/webgl/index.d.ts.map +1 -0
- package/dist/webgl/index.js +239 -0
- package/dist/webgl/index.js.map +13 -0
- package/dist/webgl/shaderGrid.d.ts +27 -0
- package/dist/webgl/shaderGrid.d.ts.map +1 -0
- package/dist/webgl/sky.d.ts +4 -0
- package/dist/webgl/sky.d.ts.map +1 -0
- package/dist/webgpu/contactShadows.d.ts +5 -0
- package/dist/webgpu/contactShadows.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts +10 -0
- package/dist/webgpu/index.d.ts.map +1 -0
- package/dist/webgpu/index.js +216 -0
- package/dist/webgpu/index.js.map +13 -0
- package/dist/webgpu/shaderGrid.d.ts +5 -0
- package/dist/webgpu/shaderGrid.d.ts.map +1 -0
- package/dist/webgpu/sky.d.ts +4 -0
- package/dist/webgpu/sky.d.ts.map +1 -0
- package/package.json +82 -4
- package/src/index.ts +11 -0
- package/src/kernel/animation.ts +24 -0
- package/src/kernel/behaviours.ts +36 -0
- package/src/kernel/bodies.ts +338 -0
- package/src/kernel/bodySources.ts +203 -0
- package/src/kernel/build.ts +1325 -0
- package/src/kernel/cameraMotion.ts +202 -0
- package/src/kernel/clock.ts +35 -0
- package/src/kernel/contactShadowPasses.ts +207 -0
- package/src/kernel/description.ts +1197 -0
- package/src/kernel/diff.ts +383 -0
- package/src/kernel/environmentPresets.ts +38 -0
- package/src/kernel/equality.ts +604 -0
- package/src/kernel/frames.ts +124 -0
- package/src/kernel/index.ts +274 -0
- package/src/kernel/instances.ts +97 -0
- package/src/kernel/labels.ts +54 -0
- package/src/kernel/loop.ts +248 -0
- package/src/kernel/materials.ts +258 -0
- package/src/kernel/motion.ts +75 -0
- package/src/kernel/orbit.ts +554 -0
- package/src/kernel/overlays.ts +130 -0
- package/src/kernel/performance.ts +101 -0
- package/src/kernel/physics.ts +361 -0
- package/src/kernel/physicsDebug.ts +68 -0
- package/src/kernel/pointer.ts +785 -0
- package/src/kernel/renderKit.ts +54 -0
- package/src/kernel/renderer.ts +136 -0
- package/src/kernel/resources.ts +504 -0
- package/src/kernel/sceneGraph.ts +1442 -0
- package/src/kernel/sceneIndex.ts +219 -0
- package/src/kernel/stage.ts +157 -0
- package/src/kernel/textLayout.ts +102 -0
- package/src/kernel/tween.ts +80 -0
- package/src/kernel/xrInput.ts +230 -0
- package/src/kernel/xrLocomotion.ts +72 -0
- package/src/runtime/FrameSource.ts +41 -0
- package/src/runtime/ResourceRegistry.ts +24 -0
- package/src/runtime/SceneRuntime.ts +229 -0
- package/src/runtime/SceneSchema.ts +983 -0
- package/src/runtime/errors.ts +18 -0
- package/src/runtime/index.ts +10 -0
- package/src/webgl/contactShadows.ts +67 -0
- package/src/webgl/index.ts +19 -0
- package/src/webgl/shaderGrid.ts +196 -0
- package/src/webgl/sky.ts +42 -0
- package/src/webgpu/contactShadows.ts +83 -0
- package/src/webgpu/index.ts +19 -0
- package/src/webgpu/shaderGrid.ts +158 -0
- package/src/webgpu/sky.ts +35 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the rendered camera reaches its description: at once, or damped round its target;
|
|
3
|
+
* and how it turns round its target by itself (`autoRotate`).
|
|
4
|
+
*
|
|
5
|
+
* Behavioural reference: three.js `examples/jsm/controls/OrbitControls.js` at 0.186.1,
|
|
6
|
+
* `update` with `enableDamping`: each update closes `dampingFactor` of the remaining
|
|
7
|
+
* rotation and pan. Here the remaining way to the described pose closes by that fraction
|
|
8
|
+
* every sixtieth of a second of scene time, so the motion is the same at any frame rate,
|
|
9
|
+
* and the camera travels round the target (azimuth the short way) rather than through it.
|
|
10
|
+
* `autoRotate` is `update` with `autoRotate`: `rotateLeft` by `2π / 60 * autoRotateSpeed`
|
|
11
|
+
* a second, only while no gesture is in progress (`state === NONE`).
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { OrthographicCamera, type PerspectiveCamera, Spherical, Vector3 } from 'three';
|
|
16
|
+
|
|
17
|
+
import type { Camera, Vec3 } from './description.ts';
|
|
18
|
+
|
|
19
|
+
type Pose = { readonly target: Vector3; readonly spherical: Spherical; zoom: number };
|
|
20
|
+
|
|
21
|
+
const createPose = (): Pose => ({ target: new Vector3(), spherical: new Spherical(), zoom: 1 });
|
|
22
|
+
|
|
23
|
+
const offset = new Vector3();
|
|
24
|
+
const position = new Vector3();
|
|
25
|
+
|
|
26
|
+
const readPose = (pose: Pose, description: Camera): void => {
|
|
27
|
+
pose.target.fromArray(description.lookAt);
|
|
28
|
+
offset.fromArray(description.position).sub(pose.target);
|
|
29
|
+
pose.spherical.setFromVector3(offset);
|
|
30
|
+
pose.zoom = description._tag === 'Orthographic' ? description.zoom : 1;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const copyPose = (to: Pose, from: Pose): void => {
|
|
34
|
+
to.target.copy(from.target);
|
|
35
|
+
to.spherical.copy(from.spherical);
|
|
36
|
+
to.zoom = from.zoom;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The signed angle from `from` to `to`, the short way round: within ±π. */
|
|
40
|
+
const turn = (from: number, to: number): number => {
|
|
41
|
+
const difference = (to - from) % (2 * Math.PI);
|
|
42
|
+
if (difference > Math.PI) return difference - 2 * Math.PI;
|
|
43
|
+
if (difference < -Math.PI) return difference + 2 * Math.PI;
|
|
44
|
+
return difference;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Close enough to the aim that the rest is invisible: snap and stop. */
|
|
48
|
+
const settled = (current: Pose, goal: Pose): boolean => {
|
|
49
|
+
const scale = Math.max(1, goal.spherical.radius);
|
|
50
|
+
return (
|
|
51
|
+
current.target.distanceTo(goal.target) < 1e-5 * scale &&
|
|
52
|
+
Math.abs(current.spherical.radius - goal.spherical.radius) < 1e-5 * scale &&
|
|
53
|
+
Math.abs(turn(current.spherical.theta, goal.spherical.theta)) < 1e-6 &&
|
|
54
|
+
Math.abs(current.spherical.phi - goal.spherical.phi) < 1e-6 &&
|
|
55
|
+
Math.abs(current.zoom - goal.zoom) < 1e-6 * goal.zoom
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function writeZoom(camera: PerspectiveCamera | OrthographicCamera, zoom: number): void {
|
|
60
|
+
if (!(camera instanceof OrthographicCamera) || camera.zoom === zoom) return;
|
|
61
|
+
camera.zoom = zoom;
|
|
62
|
+
camera.updateProjectionMatrix();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Writes a pose to a host camera. */
|
|
66
|
+
const write = (camera: PerspectiveCamera | OrthographicCamera, pose: Pose): void => {
|
|
67
|
+
position.setFromSpherical(pose.spherical).add(pose.target);
|
|
68
|
+
camera.position.copy(position);
|
|
69
|
+
camera.lookAt(pose.target);
|
|
70
|
+
writeZoom(camera, pose.zoom);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type CameraMotion = {
|
|
74
|
+
/**
|
|
75
|
+
* Aims at a described pose at scene time `now`. It is shown at once when `jump` is
|
|
76
|
+
* true, the camera has no damping, or nothing was shown before; otherwise `step`
|
|
77
|
+
* travels there. A new position, `lookAt` or zoom ends any turning (`autoRotate`).
|
|
78
|
+
*/
|
|
79
|
+
readonly aim: (
|
|
80
|
+
camera: PerspectiveCamera | OrthographicCamera,
|
|
81
|
+
description: Camera,
|
|
82
|
+
now: number,
|
|
83
|
+
jump: boolean,
|
|
84
|
+
) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Turns (`autoRotate`) and travels toward the aim up to scene time `now`. Returns
|
|
87
|
+
* whether it is still moving.
|
|
88
|
+
*/
|
|
89
|
+
readonly step: (camera: PerspectiveCamera | OrthographicCamera, now: number) => boolean;
|
|
90
|
+
/** Lands on the aim at once. */
|
|
91
|
+
readonly settle: (camera: PerspectiveCamera | OrthographicCamera) => void;
|
|
92
|
+
readonly moving: () => boolean;
|
|
93
|
+
/** Stops turning while `held` (a gesture is pressed), as `OrbitControls` turns only idle. */
|
|
94
|
+
readonly hold: (held: boolean) => void;
|
|
95
|
+
/** Where turning has taken the camera, when it has turned since its description. */
|
|
96
|
+
readonly turned: () => Vec3 | undefined;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Radians a turning camera turns per second per unit of `autoRotate` (three's `2π / 60`). */
|
|
100
|
+
const turnRate = (2 * Math.PI) / 60;
|
|
101
|
+
|
|
102
|
+
/** Creates the motion of one camera. Allocation-free once created. */
|
|
103
|
+
export const createCameraMotion = (): CameraMotion => {
|
|
104
|
+
const current = createPose();
|
|
105
|
+
// Where the description puts the camera, and where it is going: that, turned by `spun`.
|
|
106
|
+
const described = createPose();
|
|
107
|
+
const goal = createPose();
|
|
108
|
+
// The described position, written exactly on landing rather than through angles.
|
|
109
|
+
const goalPosition = new Vector3();
|
|
110
|
+
const lookAt = new Vector3();
|
|
111
|
+
let shown = false;
|
|
112
|
+
let easing = false;
|
|
113
|
+
let damping = 0;
|
|
114
|
+
let spin = 0;
|
|
115
|
+
let spun = 0;
|
|
116
|
+
let held = false;
|
|
117
|
+
let last = 0;
|
|
118
|
+
|
|
119
|
+
const turning = (): boolean => spin !== 0 && !held;
|
|
120
|
+
|
|
121
|
+
const aimGoal = (): void => {
|
|
122
|
+
copyPose(goal, described);
|
|
123
|
+
goal.spherical.theta += spun;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const land = (camera: PerspectiveCamera | OrthographicCamera): void => {
|
|
127
|
+
copyPose(current, goal);
|
|
128
|
+
easing = false;
|
|
129
|
+
if (spun !== 0) {
|
|
130
|
+
write(camera, goal);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
camera.position.copy(goalPosition);
|
|
134
|
+
camera.lookAt(goal.target);
|
|
135
|
+
writeZoom(camera, goal.zoom);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const settle: CameraMotion['settle'] = (camera) => {
|
|
139
|
+
if (shown) land(camera);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** Whether `description` puts the camera somewhere other than the last description did. */
|
|
143
|
+
const moved = (description: Camera): boolean =>
|
|
144
|
+
!shown ||
|
|
145
|
+
!goalPosition.equals(position.fromArray(description.position)) ||
|
|
146
|
+
!described.target.equals(lookAt.fromArray(description.lookAt)) ||
|
|
147
|
+
described.zoom !== (description._tag === 'Orthographic' ? description.zoom : 1);
|
|
148
|
+
|
|
149
|
+
/** One step of easing toward the goal, `seconds` after the last. */
|
|
150
|
+
const ease = (camera: PerspectiveCamera | OrthographicCamera, seconds: number): void => {
|
|
151
|
+
const remaining = (1 - damping) ** (seconds * 60);
|
|
152
|
+
const k = 1 - remaining;
|
|
153
|
+
current.target.lerp(goal.target, k);
|
|
154
|
+
current.spherical.radius += (goal.spherical.radius - current.spherical.radius) * k;
|
|
155
|
+
current.spherical.phi += (goal.spherical.phi - current.spherical.phi) * k;
|
|
156
|
+
current.spherical.theta += turn(current.spherical.theta, goal.spherical.theta) * k;
|
|
157
|
+
current.zoom += (goal.zoom - current.zoom) * k;
|
|
158
|
+
if (remaining < 1e-4 || settled(current, goal)) land(camera);
|
|
159
|
+
else write(camera, current);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
aim: (camera, description, now, jump) => {
|
|
164
|
+
if (moved(description)) spun = 0;
|
|
165
|
+
readPose(described, description);
|
|
166
|
+
goalPosition.fromArray(description.position);
|
|
167
|
+
damping = description.damping;
|
|
168
|
+
spin = description.autoRotate;
|
|
169
|
+
last = now;
|
|
170
|
+
aimGoal();
|
|
171
|
+
if (jump || damping === 0 || !shown) {
|
|
172
|
+
shown = true;
|
|
173
|
+
land(camera);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
easing = !settled(current, goal);
|
|
177
|
+
},
|
|
178
|
+
step: (camera, now) => {
|
|
179
|
+
const seconds = Math.max(0, now - last);
|
|
180
|
+
last = now;
|
|
181
|
+
if (turning() && seconds > 0) {
|
|
182
|
+
// Clockwise seen from above for a positive speed, as `rotateLeft` turns.
|
|
183
|
+
spun -= turnRate * spin * seconds;
|
|
184
|
+
aimGoal();
|
|
185
|
+
easing = damping !== 0;
|
|
186
|
+
if (!easing) land(camera);
|
|
187
|
+
}
|
|
188
|
+
if (easing) ease(camera, seconds);
|
|
189
|
+
return easing || turning();
|
|
190
|
+
},
|
|
191
|
+
settle,
|
|
192
|
+
moving: () => easing || turning(),
|
|
193
|
+
hold: (next) => {
|
|
194
|
+
held = next;
|
|
195
|
+
},
|
|
196
|
+
turned: () => {
|
|
197
|
+
if (spun === 0) return undefined;
|
|
198
|
+
position.setFromSpherical(goal.spherical).add(goal.target);
|
|
199
|
+
return [position.x, position.y, position.z];
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene time. Behaviours read elapsed seconds from here; pausing animation (for example
|
|
3
|
+
* under `prefers-reduced-motion`) freezes scene time while frames keep rendering, and
|
|
4
|
+
* resuming continues from the frozen value rather than jumping ahead.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type SceneClock = {
|
|
10
|
+
/** Scene seconds elapsed while animating. */
|
|
11
|
+
readonly elapsedSeconds: () => number;
|
|
12
|
+
/** Freezes (`false`) or resumes (`true`) scene time. Idempotent. */
|
|
13
|
+
readonly setAnimating: (animating: boolean) => void;
|
|
14
|
+
readonly animating: () => boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Creates a scene clock over a monotonic source of seconds. */
|
|
18
|
+
export const createSceneClock = (nowSeconds: () => number): SceneClock => {
|
|
19
|
+
const start = nowSeconds();
|
|
20
|
+
let frozenAt: number | undefined;
|
|
21
|
+
let paused = 0;
|
|
22
|
+
const elapsedSeconds = (): number => (frozenAt ?? nowSeconds()) - start - paused;
|
|
23
|
+
return {
|
|
24
|
+
elapsedSeconds,
|
|
25
|
+
setAnimating: (animating) => {
|
|
26
|
+
if (animating && frozenAt !== undefined) {
|
|
27
|
+
paused += nowSeconds() - frozenAt;
|
|
28
|
+
frozenAt = undefined;
|
|
29
|
+
} else if (!animating && frozenAt === undefined) {
|
|
30
|
+
frozenAt = nowSeconds();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
animating: () => frozenAt === undefined,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact shadows, drawn by the frame loop with the scene's renderer: the passes, shared
|
|
3
|
+
* by every render kit, which supplies the materials (`ShadowMaterials`).
|
|
4
|
+
*
|
|
5
|
+
* Behavioural reference: Drei `src/core/ContactShadows.tsx` at `bf6f4ad`, itself after
|
|
6
|
+
* three.js's `webgl_shadow_contact` example by mrdoob (both MIT). As there, every drawn
|
|
7
|
+
* frame:
|
|
8
|
+
*
|
|
9
|
+
* 1. renders the scene from below with an orthographic camera into a texture, through a
|
|
10
|
+
* depth material that turns nearness to the ground into shadow (alpha `1 − depth`,
|
|
11
|
+
* colour `color · depth · 2`), with the background off and the shadow plane hidden;
|
|
12
|
+
* 2. blurs it horizontally and vertically (`blur / 256`), twice with `smooth` (the second
|
|
13
|
+
* at 0.4 of the blur);
|
|
14
|
+
* 3. shows it on a transparent plane facing up.
|
|
15
|
+
*
|
|
16
|
+
* One difference: the blur passes draw a quad with their own camera, so they fill the
|
|
17
|
+
* texture wherever the shadows are placed. (Drei draws its quad at the world origin with
|
|
18
|
+
* the shadow camera, which leaves the blur empty when the shadows sit above `y = 0`.)
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import {
|
|
23
|
+
Color,
|
|
24
|
+
type Material,
|
|
25
|
+
Mesh,
|
|
26
|
+
MeshBasicMaterial,
|
|
27
|
+
OrthographicCamera,
|
|
28
|
+
PlaneGeometry,
|
|
29
|
+
Scene,
|
|
30
|
+
type Texture,
|
|
31
|
+
Vector3,
|
|
32
|
+
} from 'three';
|
|
33
|
+
|
|
34
|
+
import type { ContactShadows } from './description.ts';
|
|
35
|
+
import type { RenderTarget, SceneRenderer } from './renderer.ts';
|
|
36
|
+
import type { ContactShadowPasses, ShadowedScene } from './renderKit.ts';
|
|
37
|
+
|
|
38
|
+
/** One blur direction's material, and how to point it at a texture by an amount. */
|
|
39
|
+
export type BlurPass = {
|
|
40
|
+
readonly material: Material;
|
|
41
|
+
/** Reads `source`, blurring by `amount` of the texture's width (or height). */
|
|
42
|
+
readonly set: (source: Texture, amount: number) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** What a render kit draws contact shadows with. */
|
|
46
|
+
export type ShadowMaterials = {
|
|
47
|
+
/**
|
|
48
|
+
* The material the scene is drawn through from below: colour `color · depth · 2`, alpha
|
|
49
|
+
* `1 − depth`, no depth test or write.
|
|
50
|
+
*/
|
|
51
|
+
readonly depth: (color: Color) => Material;
|
|
52
|
+
readonly blur: (direction: 'horizontal' | 'vertical') => BlurPass;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the renderer reads a target's texture with v running the other way from
|
|
55
|
+
* WebGL's: three's WebGPU renderer does, on either backend.
|
|
56
|
+
*/
|
|
57
|
+
readonly flippedTargets: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Everything one set of contact shadows holds while shown. */
|
|
61
|
+
type Shown = {
|
|
62
|
+
readonly description: ContactShadows;
|
|
63
|
+
readonly plane: Mesh<PlaneGeometry, MeshBasicMaterial>;
|
|
64
|
+
readonly camera: OrthographicCamera;
|
|
65
|
+
readonly target: RenderTarget;
|
|
66
|
+
readonly blurTarget: RenderTarget;
|
|
67
|
+
readonly color: Color;
|
|
68
|
+
readonly depthMaterial: Material;
|
|
69
|
+
readonly horizontal: BlurPass;
|
|
70
|
+
readonly vertical: BlurPass;
|
|
71
|
+
readonly quad: Mesh<PlaneGeometry, Material>;
|
|
72
|
+
readonly quadScene: Scene;
|
|
73
|
+
readonly quadCamera: OrthographicCamera;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const up = new Vector3(0, 1, 0);
|
|
77
|
+
const aim = new Vector3();
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The plane's geometry: flat, facing up, its texture's v running towards +z as the shadow
|
|
81
|
+
* camera's image does (already so where the renderer reads targets flipped).
|
|
82
|
+
*/
|
|
83
|
+
const groundGeometry = (width: number, depth: number, flipped: boolean): PlaneGeometry => {
|
|
84
|
+
const geometry = new PlaneGeometry(width, depth).rotateX(-Math.PI / 2);
|
|
85
|
+
if (flipped) return geometry;
|
|
86
|
+
const uv = geometry.getAttribute('uv');
|
|
87
|
+
for (let i = 0; i < uv.count; i++) uv.setY(i, 1 - uv.getY(i));
|
|
88
|
+
return geometry;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Whether a change needs new targets, geometry or camera rather than an update. */
|
|
92
|
+
const reshaped = (a: ContactShadows, b: ContactShadows): boolean =>
|
|
93
|
+
a.resolution !== b.resolution || a.size[0] !== b.size[0] || a.size[1] !== b.size[1];
|
|
94
|
+
|
|
95
|
+
/** Creates the contact shadow passes for one scene graph and renderer, with `materials`. */
|
|
96
|
+
export const createContactShadowPasses = (
|
|
97
|
+
graph: ShadowedScene,
|
|
98
|
+
renderer: SceneRenderer,
|
|
99
|
+
materials: ShadowMaterials,
|
|
100
|
+
): ContactShadowPasses => {
|
|
101
|
+
let shown: Shown | undefined;
|
|
102
|
+
|
|
103
|
+
const release = (): void => {
|
|
104
|
+
if (shown === undefined) return;
|
|
105
|
+
shown.plane.removeFromParent();
|
|
106
|
+
shown.plane.geometry.dispose();
|
|
107
|
+
shown.plane.material.dispose();
|
|
108
|
+
shown.target.dispose();
|
|
109
|
+
shown.blurTarget.dispose();
|
|
110
|
+
shown.depthMaterial.dispose();
|
|
111
|
+
shown.horizontal.material.dispose();
|
|
112
|
+
shown.vertical.material.dispose();
|
|
113
|
+
shown.quad.geometry.dispose();
|
|
114
|
+
shown = undefined;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const build = (description: ContactShadows): Shown => {
|
|
118
|
+
const [width, depth] = description.size;
|
|
119
|
+
const target = renderer.createTarget(description.resolution, description.resolution);
|
|
120
|
+
const blurTarget = renderer.createTarget(description.resolution, description.resolution);
|
|
121
|
+
const material = new MeshBasicMaterial({
|
|
122
|
+
map: target.texture,
|
|
123
|
+
transparent: true,
|
|
124
|
+
depthWrite: false,
|
|
125
|
+
});
|
|
126
|
+
const plane = new Mesh(groundGeometry(width, depth, materials.flippedTargets), material);
|
|
127
|
+
// Shadows are scenery: never picked, never in the way of an overlay.
|
|
128
|
+
plane.raycast = () => undefined;
|
|
129
|
+
graph.scene.add(plane);
|
|
130
|
+
const color = new Color();
|
|
131
|
+
const quad = new Mesh<PlaneGeometry, Material>(new PlaneGeometry(2, 2));
|
|
132
|
+
// In front of the quad camera, between its near and far planes.
|
|
133
|
+
quad.position.z = -0.5;
|
|
134
|
+
const quadScene = new Scene();
|
|
135
|
+
quadScene.add(quad);
|
|
136
|
+
return {
|
|
137
|
+
description,
|
|
138
|
+
plane,
|
|
139
|
+
camera: new OrthographicCamera(-width / 2, width / 2, depth / 2, -depth / 2, 0, 1),
|
|
140
|
+
target,
|
|
141
|
+
blurTarget,
|
|
142
|
+
color,
|
|
143
|
+
depthMaterial: materials.depth(color),
|
|
144
|
+
horizontal: materials.blur('horizontal'),
|
|
145
|
+
vertical: materials.blur('vertical'),
|
|
146
|
+
quad,
|
|
147
|
+
quadScene,
|
|
148
|
+
quadCamera: new OrthographicCamera(-1, 1, 1, -1, 0, 1),
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Applies the fields that change in place: placement, reach, colour and opacity. */
|
|
153
|
+
const update = (current: Shown, description: ContactShadows): Shown => {
|
|
154
|
+
current.plane.position.fromArray(description.position);
|
|
155
|
+
current.plane.material.opacity = description.opacity;
|
|
156
|
+
current.color.setHex(description.color);
|
|
157
|
+
current.camera.position.fromArray(description.position);
|
|
158
|
+
// Looking straight up, with +z as the image's up: its v runs towards +z, as the plane's.
|
|
159
|
+
current.camera.up.set(0, 0, 1);
|
|
160
|
+
current.camera.lookAt(aim.copy(current.camera.position).add(up));
|
|
161
|
+
current.camera.near = description.near;
|
|
162
|
+
current.camera.far = description.far;
|
|
163
|
+
current.camera.updateProjectionMatrix();
|
|
164
|
+
current.camera.updateMatrixWorld();
|
|
165
|
+
return { ...current, description };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** One blur: horizontally into the spare target, then vertically back. */
|
|
169
|
+
const blurPass = (current: Shown, amount: number): void => {
|
|
170
|
+
const { horizontal, vertical, quad, quadScene, quadCamera } = current;
|
|
171
|
+
quad.material = horizontal.material;
|
|
172
|
+
horizontal.set(current.target.texture, amount / 256);
|
|
173
|
+
renderer.render(quadScene, quadCamera, undefined, current.blurTarget);
|
|
174
|
+
quad.material = vertical.material;
|
|
175
|
+
vertical.set(current.blurTarget.texture, amount / 256);
|
|
176
|
+
renderer.render(quadScene, quadCamera, undefined, current.target);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const render = (current: Shown): void => {
|
|
180
|
+
const { scene } = graph;
|
|
181
|
+
const background = scene.background;
|
|
182
|
+
const override = scene.overrideMaterial;
|
|
183
|
+
current.plane.visible = false;
|
|
184
|
+
scene.background = null;
|
|
185
|
+
scene.overrideMaterial = current.depthMaterial;
|
|
186
|
+
renderer.render(scene, current.camera, undefined, current.target);
|
|
187
|
+
scene.overrideMaterial = override;
|
|
188
|
+
scene.background = background;
|
|
189
|
+
current.plane.visible = true;
|
|
190
|
+
blurPass(current, current.description.blur);
|
|
191
|
+
if (current.description.smooth) blurPass(current, current.description.blur * 0.4);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
draw: () => {
|
|
196
|
+
const description = graph.contactShadows();
|
|
197
|
+
if (description === null) {
|
|
198
|
+
release();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (shown !== undefined && reshaped(shown.description, description)) release();
|
|
202
|
+
shown = update(shown ?? build(description), description);
|
|
203
|
+
render(shown);
|
|
204
|
+
},
|
|
205
|
+
dispose: release,
|
|
206
|
+
};
|
|
207
|
+
};
|