@opsydyn/effect-three-foldkit 0.0.0-stage → 0.1.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 +56 -0
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/dist/assets.d.ts +53 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/dom/animationFrames.d.ts +12 -0
- package/dist/dom/animationFrames.d.ts.map +1 -0
- package/dist/dom/canvas.d.ts +26 -0
- package/dist/dom/canvas.d.ts.map +1 -0
- package/dist/dom/latch.d.ts +15 -0
- package/dist/dom/latch.d.ts.map +1 -0
- package/dist/dom/models.d.ts +10 -0
- package/dist/dom/models.d.ts.map +1 -0
- package/dist/dom/overlays.d.ts +24 -0
- package/dist/dom/overlays.d.ts.map +1 -0
- package/dist/dom/pointer.d.ts +49 -0
- package/dist/dom/pointer.d.ts.map +1 -0
- package/dist/dom/postprocessing.d.ts +22 -0
- package/dist/dom/postprocessing.d.ts.map +1 -0
- package/dist/dom/property.d.ts +15 -0
- package/dist/dom/property.d.ts.map +1 -0
- package/dist/dom/reconciled.d.ts +10 -0
- package/dist/dom/reconciled.d.ts.map +1 -0
- package/dist/dom/renderSettings.d.ts +58 -0
- package/dist/dom/renderSettings.d.ts.map +1 -0
- package/dist/dom/typedScenes.d.ts +14 -0
- package/dist/dom/typedScenes.d.ts.map +1 -0
- package/dist/dom/updates.d.ts +8 -0
- package/dist/dom/updates.d.ts.map +1 -0
- package/dist/dom/viewport.d.ts +40 -0
- package/dist/dom/viewport.d.ts.map +1 -0
- package/dist/dom/webgl.d.ts +12 -0
- package/dist/dom/webgl.d.ts.map +1 -0
- package/dist/dom/webgpu.d.ts +23 -0
- package/dist/dom/webgpu.d.ts.map +1 -0
- package/dist/dom/webgpuPostprocessing.d.ts +4 -0
- package/dist/dom/webgpuPostprocessing.d.ts.map +1 -0
- package/dist/dom/xr.d.ts +53 -0
- package/dist/dom/xr.d.ts.map +1 -0
- package/dist/dom/xrGlide.d.ts +49 -0
- package/dist/dom/xrGlide.d.ts.map +1 -0
- package/dist/dom/xrHub.d.ts +45 -0
- package/dist/dom/xrHub.d.ts.map +1 -0
- package/dist/dom/xrInputs.d.ts +40 -0
- package/dist/dom/xrInputs.d.ts.map +1 -0
- package/dist/dom/xrSpaces.d.ts +33 -0
- package/dist/dom/xrSpaces.d.ts.map +1 -0
- package/dist/dom/xrTarget.d.ts +36 -0
- package/dist/dom/xrTarget.d.ts.map +1 -0
- package/dist/element.d.ts +771 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/environment.d.ts +64 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2037 -0
- package/dist/index.js.map +38 -0
- package/dist/mount.d.ts +14 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/overlayView.d.ts +23 -0
- package/dist/overlayView.d.ts.map +1 -0
- package/dist/view.d.ts +179 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/xr.d.ts +34 -0
- package/dist/xr.d.ts.map +1 -0
- package/package.json +72 -4
- package/src/assets.ts +151 -0
- package/src/dom/animationFrames.ts +32 -0
- package/src/dom/canvas.ts +54 -0
- package/src/dom/latch.ts +36 -0
- package/src/dom/models.ts +37 -0
- package/src/dom/overlays.ts +63 -0
- package/src/dom/pointer.ts +163 -0
- package/src/dom/postprocessing.ts +116 -0
- package/src/dom/property.ts +43 -0
- package/src/dom/reconciled.ts +51 -0
- package/src/dom/renderSettings.ts +117 -0
- package/src/dom/typedScenes.ts +21 -0
- package/src/dom/updates.ts +14 -0
- package/src/dom/viewport.ts +97 -0
- package/src/dom/webgl.ts +216 -0
- package/src/dom/webgpu.ts +204 -0
- package/src/dom/webgpuPostprocessing.ts +95 -0
- package/src/dom/xr.ts +211 -0
- package/src/dom/xrGlide.ts +114 -0
- package/src/dom/xrHub.ts +118 -0
- package/src/dom/xrInputs.ts +309 -0
- package/src/dom/xrSpaces.ts +287 -0
- package/src/dom/xrTarget.ts +71 -0
- package/src/element.ts +114 -0
- package/src/environment.ts +78 -0
- package/src/index.ts +71 -0
- package/src/mount.ts +305 -0
- package/src/overlayView.ts +71 -0
- package/src/view.ts +254 -0
- package/src/xr.ts +36 -0
package/src/dom/webgl.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebGL2 renderer behind the core's backend-neutral renderer contract.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { RendererFactory, RenderSettings, RenderTarget } from '@opsydyn/effect-three';
|
|
7
|
+
import { webglKit } from '@opsydyn/effect-three/webgl';
|
|
8
|
+
import {
|
|
9
|
+
type Camera,
|
|
10
|
+
HalfFloatType,
|
|
11
|
+
LinearFilter,
|
|
12
|
+
type Object3D,
|
|
13
|
+
PMREMGenerator,
|
|
14
|
+
type Scene,
|
|
15
|
+
Vector2,
|
|
16
|
+
WebGLRenderer,
|
|
17
|
+
type WebGLRendererParameters,
|
|
18
|
+
WebGLRenderTarget,
|
|
19
|
+
} from 'three';
|
|
20
|
+
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
21
|
+
|
|
22
|
+
import { createPostProcessing } from './postprocessing.ts';
|
|
23
|
+
import {
|
|
24
|
+
colorSpaces,
|
|
25
|
+
forgetPresentable,
|
|
26
|
+
recompileMaterials,
|
|
27
|
+
registerPresentable,
|
|
28
|
+
shadowTypes,
|
|
29
|
+
toneMappings,
|
|
30
|
+
} from './renderSettings.ts';
|
|
31
|
+
|
|
32
|
+
/** Options passed through to three's `WebGLRenderer`. The canvas is always the scene's own. */
|
|
33
|
+
export type WebGLOptions = Omit<WebGLRendererParameters, 'canvas'>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Applies scene render settings; a change of shadow mode re-renders the shadow maps.
|
|
37
|
+
* Returns whether shadows were switched on or off, which materials must be recompiled for.
|
|
38
|
+
*/
|
|
39
|
+
const configure = (renderer: WebGLRenderer, settings: RenderSettings): boolean => {
|
|
40
|
+
const shadowMap = renderer.shadowMap;
|
|
41
|
+
const enabled = settings.shadows !== 'off';
|
|
42
|
+
const switched = shadowMap.enabled !== enabled;
|
|
43
|
+
const type = enabled ? shadowTypes[settings.shadows] : shadowMap.type;
|
|
44
|
+
if (switched || shadowMap.type !== type) shadowMap.needsUpdate = true;
|
|
45
|
+
shadowMap.enabled = enabled;
|
|
46
|
+
shadowMap.type = type;
|
|
47
|
+
renderer.toneMapping = toneMappings[settings.toneMapping];
|
|
48
|
+
renderer.toneMappingExposure = settings.exposure;
|
|
49
|
+
renderer.outputColorSpace = colorSpaces[settings.colorSpace];
|
|
50
|
+
return switched;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const drawingSize = new Vector2();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A render texture's target, as Drei's `useFBO` makes one for `RenderTexture` (drei
|
|
57
|
+
* `bf6f4ad`, `src/core/Fbo.tsx`): half-float colour, linear filtering, multisampled, with
|
|
58
|
+
* a depth buffer.
|
|
59
|
+
*/
|
|
60
|
+
const createTarget = (
|
|
61
|
+
targets: WeakMap<RenderTarget, WebGLRenderTarget>,
|
|
62
|
+
width: number,
|
|
63
|
+
height: number,
|
|
64
|
+
): RenderTarget => {
|
|
65
|
+
const target = new WebGLRenderTarget(width, height, {
|
|
66
|
+
minFilter: LinearFilter,
|
|
67
|
+
magFilter: LinearFilter,
|
|
68
|
+
type: HalfFloatType,
|
|
69
|
+
samples: 8,
|
|
70
|
+
});
|
|
71
|
+
const handle = { texture: target.texture, dispose: () => target.dispose() };
|
|
72
|
+
targets.set(handle, target);
|
|
73
|
+
return handle;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* three.js's `RoomEnvironment`, prefiltered for image-based lighting, as three's examples
|
|
78
|
+
* set it up (`PMREMGenerator.fromScene(new RoomEnvironment(), 0.04)`).
|
|
79
|
+
*/
|
|
80
|
+
const createRoom = (renderer: WebGLRenderer): RenderTarget => {
|
|
81
|
+
const generator = new PMREMGenerator(renderer);
|
|
82
|
+
const room = new RoomEnvironment();
|
|
83
|
+
const target = generator.fromScene(room, 0.04);
|
|
84
|
+
room.dispose();
|
|
85
|
+
generator.dispose();
|
|
86
|
+
return { texture: target.texture, dispose: () => target.dispose() };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Draws a frame of an immersive session. In AR the room shows through: three clears to
|
|
91
|
+
* transparent there, but a background (an environment shown as one) would cover it, so it
|
|
92
|
+
* is left out of the frame.
|
|
93
|
+
*/
|
|
94
|
+
const drawPresenting = (renderer: WebGLRenderer, scene: Scene, camera: Camera): void => {
|
|
95
|
+
const passthrough = renderer.xr.getEnvironmentBlendMode() !== 'opaque';
|
|
96
|
+
if (!passthrough || scene.background === null) {
|
|
97
|
+
renderer.render(scene, camera);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const background = scene.background;
|
|
101
|
+
scene.background = null;
|
|
102
|
+
renderer.render(scene, camera);
|
|
103
|
+
scene.background = background;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** A factory for a WebGL2 renderer drawing into `canvas`. `create` throws if WebGL2 is unavailable. */
|
|
107
|
+
export const webglRendererFactory = (
|
|
108
|
+
canvas: HTMLCanvasElement,
|
|
109
|
+
options: WebGLOptions = {},
|
|
110
|
+
): RendererFactory => ({
|
|
111
|
+
// The GLSL parts of a scene (sky, shader grid, contact shadows) go with this renderer.
|
|
112
|
+
kit: webglKit,
|
|
113
|
+
create: () => {
|
|
114
|
+
const renderer = new WebGLRenderer({ antialias: true, ...options, canvas });
|
|
115
|
+
registerPresentable(canvas, renderer, {
|
|
116
|
+
xr: renderer.xr,
|
|
117
|
+
setXrLoop: (callback) => renderer.xr.setAnimationLoop(callback),
|
|
118
|
+
});
|
|
119
|
+
const post = createPostProcessing(renderer);
|
|
120
|
+
const targets = new WeakMap<RenderTarget, WebGLRenderTarget>();
|
|
121
|
+
// The pixel ratio is the viewport's, scaled by the scene's resolution setting.
|
|
122
|
+
let pixelRatio = 1;
|
|
123
|
+
let resolution = 1;
|
|
124
|
+
let scissored = false;
|
|
125
|
+
// Counts shadow switches; each scene drawn (main or render texture) recompiles its
|
|
126
|
+
// materials once after a switch.
|
|
127
|
+
let shadowSwitches = 0;
|
|
128
|
+
const compiledAt = new WeakMap<Object3D, number>();
|
|
129
|
+
const applyPixelRatio = (): void => {
|
|
130
|
+
if (renderer.getPixelRatio() !== pixelRatio * resolution) {
|
|
131
|
+
renderer.setPixelRatio(pixelRatio * resolution);
|
|
132
|
+
}
|
|
133
|
+
const { width, height } = renderer.getSize(drawingSize);
|
|
134
|
+
post.setSize(width, height, pixelRatio * resolution);
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
render: (scene, camera, area, target) => {
|
|
138
|
+
if (shadowSwitches > 0 && compiledAt.get(scene) !== shadowSwitches) {
|
|
139
|
+
recompileMaterials(scene);
|
|
140
|
+
compiledAt.set(scene, shadowSwitches);
|
|
141
|
+
}
|
|
142
|
+
const webglTarget = target === undefined ? undefined : targets.get(target);
|
|
143
|
+
if (webglTarget !== undefined) {
|
|
144
|
+
// As Drei's RenderTexture draws (src/core/RenderTexture.tsx): into the target, then
|
|
145
|
+
// back to whatever was bound. A target keeps its own viewport and scissor.
|
|
146
|
+
// Targets start transparent, whatever the canvas clears to: what the scene does
|
|
147
|
+
// not cover stays see-through (a render texture without a background, contact
|
|
148
|
+
// shadows away from objects).
|
|
149
|
+
// Offscreen passes keep their own camera, even while presenting in XR (three
|
|
150
|
+
// would otherwise draw them from the headset).
|
|
151
|
+
const previous = renderer.getRenderTarget();
|
|
152
|
+
const clearAlpha = renderer.getClearAlpha();
|
|
153
|
+
const xrEnabled = renderer.xr.enabled;
|
|
154
|
+
renderer.xr.enabled = false;
|
|
155
|
+
renderer.setClearAlpha(0);
|
|
156
|
+
renderer.setRenderTarget(webglTarget);
|
|
157
|
+
renderer.render(scene, camera);
|
|
158
|
+
renderer.setRenderTarget(previous);
|
|
159
|
+
renderer.setClearAlpha(clearAlpha);
|
|
160
|
+
renderer.xr.enabled = xrEnabled;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Presenting in XR, three draws the headset's views itself: no areas, no effects.
|
|
164
|
+
if (renderer.xr.isPresenting) {
|
|
165
|
+
drawPresenting(renderer, scene, camera);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const { width, height } = renderer.getSize(drawingSize);
|
|
169
|
+
if (area === undefined) {
|
|
170
|
+
if (scissored) {
|
|
171
|
+
renderer.setScissorTest(false);
|
|
172
|
+
renderer.setViewport(0, 0, width, height);
|
|
173
|
+
scissored = false;
|
|
174
|
+
}
|
|
175
|
+
if (!post.render(scene, camera)) renderer.render(scene, camera);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// three's viewport and scissor origin is the bottom left; areas are from the top.
|
|
179
|
+
const bottom = height - area.y - area.height;
|
|
180
|
+
renderer.setViewport(area.x, bottom, area.width, area.height);
|
|
181
|
+
renderer.setScissor(area.x, bottom, area.width, area.height);
|
|
182
|
+
renderer.setScissorTest(true);
|
|
183
|
+
scissored = true;
|
|
184
|
+
// Effects apply to the single full view only; views draw straight to the screen.
|
|
185
|
+
renderer.render(scene, camera);
|
|
186
|
+
},
|
|
187
|
+
createTarget: (width, height) => createTarget(targets, width, height),
|
|
188
|
+
createEnvironment: () => createRoom(renderer),
|
|
189
|
+
configure: (settings) => {
|
|
190
|
+
if (configure(renderer, settings)) shadowSwitches++;
|
|
191
|
+
resolution = settings.resolution;
|
|
192
|
+
post.configure(settings.effects);
|
|
193
|
+
applyPixelRatio();
|
|
194
|
+
},
|
|
195
|
+
setViewport: (viewport) => {
|
|
196
|
+
pixelRatio = viewport.pixelRatio;
|
|
197
|
+
applyPixelRatio();
|
|
198
|
+
renderer.setSize(viewport.width, viewport.height, false);
|
|
199
|
+
post.setSize(viewport.width, viewport.height, pixelRatio * resolution);
|
|
200
|
+
},
|
|
201
|
+
memory: () => ({
|
|
202
|
+
geometries: renderer.info.memory.geometries,
|
|
203
|
+
textures: renderer.info.memory.textures,
|
|
204
|
+
}),
|
|
205
|
+
// dispose() frees three's GPU objects but not the WebGL context. Browsers cap live
|
|
206
|
+
// contexts (about 16 in Chromium), so release it now rather than at garbage collection.
|
|
207
|
+
dispose: () => {
|
|
208
|
+
forgetPresentable(canvas);
|
|
209
|
+
void renderer.xr.getSession()?.end();
|
|
210
|
+
post.dispose();
|
|
211
|
+
renderer.dispose();
|
|
212
|
+
renderer.forceContextLoss();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* three.js's `WebGPURenderer` behind the core's renderer contract: WebGPU where the browser
|
|
3
|
+
* has it, otherwise its WebGL 2 backend (or always, with `forceWebGL`), drawing the same
|
|
4
|
+
* node materials either way. It carries the WebGPU render kit (the sky, shader grid and
|
|
5
|
+
* contact shadows in TSL), post-processes with `RenderPipeline`, and presents in WebXR.
|
|
6
|
+
* It starts asynchronously (`init()`), which the runtime waits for.
|
|
7
|
+
*
|
|
8
|
+
* Not drawn under it: labels, which troika makes from WebGL shaders; a label engine's
|
|
9
|
+
* labels are silently empty here.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { RendererFactory, RenderSettings, RenderTarget } from '@opsydyn/effect-three';
|
|
14
|
+
import { webgpuKit } from '@opsydyn/effect-three/webgpu';
|
|
15
|
+
import {
|
|
16
|
+
type Camera,
|
|
17
|
+
HalfFloatType,
|
|
18
|
+
LinearFilter,
|
|
19
|
+
type Object3D,
|
|
20
|
+
RenderTarget as ThreeRenderTarget,
|
|
21
|
+
type Scene,
|
|
22
|
+
Vector2,
|
|
23
|
+
} from 'three';
|
|
24
|
+
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
25
|
+
import { PMREMGenerator, WebGPURenderer } from 'three/webgpu';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
colorSpaces,
|
|
29
|
+
forgetPresentable,
|
|
30
|
+
recompileMaterials,
|
|
31
|
+
registerPresentable,
|
|
32
|
+
shadowTypes,
|
|
33
|
+
toneMappings,
|
|
34
|
+
} from './renderSettings.ts';
|
|
35
|
+
import { createWebGPUPostProcessing } from './webgpuPostprocessing.ts';
|
|
36
|
+
|
|
37
|
+
/** Options for three's `WebGPURenderer`. The canvas is always the scene's own. */
|
|
38
|
+
export type WebGPUOptions = {
|
|
39
|
+
/** Use the WebGL 2 backend even where WebGPU is available. */
|
|
40
|
+
readonly forceWebGL?: boolean;
|
|
41
|
+
readonly alpha?: boolean;
|
|
42
|
+
readonly antialias?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Applies scene render settings; returns whether shadows were switched on or off. */
|
|
46
|
+
const configure = (renderer: WebGPURenderer, settings: RenderSettings): boolean => {
|
|
47
|
+
const shadowMap = renderer.shadowMap;
|
|
48
|
+
const enabled = settings.shadows !== 'off';
|
|
49
|
+
const switched = shadowMap.enabled !== enabled;
|
|
50
|
+
shadowMap.enabled = enabled;
|
|
51
|
+
if (enabled) shadowMap.type = shadowTypes[settings.shadows];
|
|
52
|
+
renderer.toneMapping = toneMappings[settings.toneMapping];
|
|
53
|
+
renderer.toneMappingExposure = settings.exposure;
|
|
54
|
+
renderer.outputColorSpace = colorSpaces[settings.colorSpace];
|
|
55
|
+
return switched;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const drawingSize = new Vector2();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A render texture's target, as the WebGL renderer makes one (Drei's `useFBO`): half-float
|
|
62
|
+
* colour, linear filtering, multisampled (4, WebGPU's only multisample count), with depth.
|
|
63
|
+
*/
|
|
64
|
+
const createTarget = (
|
|
65
|
+
targets: WeakMap<RenderTarget, ThreeRenderTarget>,
|
|
66
|
+
width: number,
|
|
67
|
+
height: number,
|
|
68
|
+
): RenderTarget => {
|
|
69
|
+
const target = new ThreeRenderTarget(width, height, {
|
|
70
|
+
minFilter: LinearFilter,
|
|
71
|
+
magFilter: LinearFilter,
|
|
72
|
+
type: HalfFloatType,
|
|
73
|
+
samples: 4,
|
|
74
|
+
});
|
|
75
|
+
const handle = { texture: target.texture, dispose: () => target.dispose() };
|
|
76
|
+
targets.set(handle, target);
|
|
77
|
+
return handle;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** three.js's `RoomEnvironment`, prefiltered by the WebGPU renderer's PMREM generator. */
|
|
81
|
+
const createRoom = (renderer: WebGPURenderer): RenderTarget => {
|
|
82
|
+
const generator = new PMREMGenerator(renderer);
|
|
83
|
+
const room = new RoomEnvironment();
|
|
84
|
+
const target = generator.fromScene(room, 0.04);
|
|
85
|
+
room.dispose();
|
|
86
|
+
generator.dispose();
|
|
87
|
+
return { texture: target.texture, dispose: () => target.dispose() };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Draws a frame of an immersive session. In AR the room shows through, so a background
|
|
92
|
+
* (an environment shown as one) is left out of the frame, as the WebGL renderer does.
|
|
93
|
+
*/
|
|
94
|
+
const drawPresenting = (renderer: WebGPURenderer, scene: Scene, camera: Camera): void => {
|
|
95
|
+
const passthrough = renderer.xr.getEnvironmentBlendMode() !== 'opaque';
|
|
96
|
+
if (!passthrough || scene.background === null) {
|
|
97
|
+
renderer.render(scene, camera);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const background = scene.background;
|
|
101
|
+
scene.background = null;
|
|
102
|
+
renderer.render(scene, camera);
|
|
103
|
+
scene.background = background;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** A factory for three's `WebGPURenderer` drawing into `canvas`. */
|
|
107
|
+
export const webgpuRendererFactory = (
|
|
108
|
+
canvas: HTMLCanvasElement,
|
|
109
|
+
options: WebGPUOptions = {},
|
|
110
|
+
): RendererFactory => ({
|
|
111
|
+
// The TSL parts of a scene (sky, shader grid, contact shadows) go with this renderer.
|
|
112
|
+
kit: webgpuKit,
|
|
113
|
+
create: () => {
|
|
114
|
+
// Opaque by default, as three's WebGLRenderer is (WebGPURenderer's own default is alpha).
|
|
115
|
+
const renderer = new WebGPURenderer({ antialias: true, alpha: false, ...options, canvas });
|
|
116
|
+
registerPresentable(canvas, renderer, {
|
|
117
|
+
xr: renderer.xr,
|
|
118
|
+
setXrLoop: (callback) => {
|
|
119
|
+
void renderer.setAnimationLoop(callback);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const post = createWebGPUPostProcessing(renderer);
|
|
123
|
+
const targets = new WeakMap<RenderTarget, ThreeRenderTarget>();
|
|
124
|
+
let pixelRatio = 1;
|
|
125
|
+
let resolution = 1;
|
|
126
|
+
let scissored = false;
|
|
127
|
+
let shadowSwitches = 0;
|
|
128
|
+
const compiledAt = new WeakMap<Object3D, number>();
|
|
129
|
+
const applyPixelRatio = (): void => {
|
|
130
|
+
if (renderer.getPixelRatio() !== pixelRatio * resolution) {
|
|
131
|
+
renderer.setPixelRatio(pixelRatio * resolution);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
ready: () => renderer.init().then(() => undefined),
|
|
136
|
+
render: (scene, camera, area, target) => {
|
|
137
|
+
if (shadowSwitches > 0 && compiledAt.get(scene) !== shadowSwitches) {
|
|
138
|
+
recompileMaterials(scene);
|
|
139
|
+
compiledAt.set(scene, shadowSwitches);
|
|
140
|
+
}
|
|
141
|
+
const offscreen = target === undefined ? undefined : targets.get(target);
|
|
142
|
+
if (offscreen !== undefined) {
|
|
143
|
+
// As the WebGL renderer: into the target, transparent, never from the headset.
|
|
144
|
+
const previous = renderer.getRenderTarget();
|
|
145
|
+
const clearAlpha = renderer.getClearAlpha();
|
|
146
|
+
const xrEnabled = renderer.xr.enabled;
|
|
147
|
+
renderer.xr.enabled = false;
|
|
148
|
+
renderer.setClearAlpha(0);
|
|
149
|
+
renderer.setRenderTarget(offscreen);
|
|
150
|
+
renderer.render(scene, camera);
|
|
151
|
+
renderer.setRenderTarget(previous);
|
|
152
|
+
renderer.setClearAlpha(clearAlpha);
|
|
153
|
+
renderer.xr.enabled = xrEnabled;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Presenting in XR, three draws the headset's views itself: no areas, no effects.
|
|
157
|
+
if (renderer.xr.isPresenting) {
|
|
158
|
+
drawPresenting(renderer, scene, camera);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { width, height } = renderer.getSize(drawingSize);
|
|
162
|
+
if (area === undefined) {
|
|
163
|
+
if (scissored) {
|
|
164
|
+
renderer.setScissorTest(false);
|
|
165
|
+
renderer.setViewport(0, 0, width, height);
|
|
166
|
+
scissored = false;
|
|
167
|
+
}
|
|
168
|
+
if (!post.render(scene, camera)) renderer.render(scene, camera);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// three's viewport and scissor origin is the bottom left; areas are from the top.
|
|
172
|
+
const bottom = height - area.y - area.height;
|
|
173
|
+
renderer.setViewport(area.x, bottom, area.width, area.height);
|
|
174
|
+
renderer.setScissor(area.x, bottom, area.width, area.height);
|
|
175
|
+
renderer.setScissorTest(true);
|
|
176
|
+
scissored = true;
|
|
177
|
+
renderer.render(scene, camera);
|
|
178
|
+
},
|
|
179
|
+
createTarget: (width, height) => createTarget(targets, width, height),
|
|
180
|
+
createEnvironment: () => createRoom(renderer),
|
|
181
|
+
configure: (settings) => {
|
|
182
|
+
if (configure(renderer, settings)) shadowSwitches++;
|
|
183
|
+
resolution = settings.resolution;
|
|
184
|
+
post.configure(settings.effects);
|
|
185
|
+
applyPixelRatio();
|
|
186
|
+
},
|
|
187
|
+
setViewport: (viewport) => {
|
|
188
|
+
pixelRatio = viewport.pixelRatio;
|
|
189
|
+
applyPixelRatio();
|
|
190
|
+
renderer.setSize(viewport.width, viewport.height, false);
|
|
191
|
+
},
|
|
192
|
+
memory: () => ({
|
|
193
|
+
geometries: renderer.info.memory.geometries,
|
|
194
|
+
textures: renderer.info.memory.textures,
|
|
195
|
+
}),
|
|
196
|
+
dispose: () => {
|
|
197
|
+
forgetPresentable(canvas);
|
|
198
|
+
void renderer.xr.getSession()?.end();
|
|
199
|
+
post.dispose();
|
|
200
|
+
void renderer.dispose();
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-processing for the WebGPU renderer: three.js's `RenderPipeline` with TSL nodes for
|
|
3
|
+
* the same effects the WebGL renderer composes (`postprocessing.ts`): bloom
|
|
4
|
+
* (`tsl/display/BloomNode.js`, added to the scene as three's examples add it), film
|
|
5
|
+
* (`tsl/display/FilmNode.js`'s grain, written here, with grayscale by luminance as
|
|
6
|
+
* `FilmPass` offers) and
|
|
7
|
+
* vignette (three's `VignetteShader`, as TSL). The pipeline's output transform applies
|
|
8
|
+
* tone mapping and the output colour space, as the WebGL composer's `OutputPass` does.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { effectsEqual, type PostEffect } from '@opsydyn/effect-three';
|
|
13
|
+
import type { Camera, Scene } from 'three';
|
|
14
|
+
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
|
|
15
|
+
import {
|
|
16
|
+
clamp,
|
|
17
|
+
dot,
|
|
18
|
+
fract,
|
|
19
|
+
luminance,
|
|
20
|
+
mix,
|
|
21
|
+
pass,
|
|
22
|
+
rand,
|
|
23
|
+
screenUV,
|
|
24
|
+
time,
|
|
25
|
+
uv,
|
|
26
|
+
vec3,
|
|
27
|
+
vec4,
|
|
28
|
+
} from 'three/tsl';
|
|
29
|
+
import { type Node, RenderPipeline, type WebGPURenderer } from 'three/webgpu';
|
|
30
|
+
|
|
31
|
+
import type { PostProcessing } from './postprocessing.ts';
|
|
32
|
+
|
|
33
|
+
type Colour = Node<'vec4'>;
|
|
34
|
+
|
|
35
|
+
/** One effect applied to the colour so far. */
|
|
36
|
+
const applied = (input: Colour, effect: PostEffect): Colour => {
|
|
37
|
+
switch (effect._tag) {
|
|
38
|
+
case 'Bloom':
|
|
39
|
+
return input.add(bloom(input, effect.strength, effect.radius, effect.threshold));
|
|
40
|
+
case 'Vignette': {
|
|
41
|
+
// three's VignetteShader: towards `1 − darkness` with distance from the middle.
|
|
42
|
+
const centred = screenUV.sub(0.5).mul(effect.offset);
|
|
43
|
+
return vec4(mix(input.rgb, vec3(1 - effect.darkness), dot(centred, centred)), input.a);
|
|
44
|
+
}
|
|
45
|
+
case 'Film': {
|
|
46
|
+
// three's FilmNode: noise added in proportion to the colour, mixed in by intensity.
|
|
47
|
+
const base = input.rgb;
|
|
48
|
+
const noise = rand(fract(uv().add(time)));
|
|
49
|
+
const grained = mix(base, base.add(base.mul(clamp(noise.add(0.1), 0, 1))), effect.intensity);
|
|
50
|
+
return vec4(effect.grayscale ? vec3(luminance(grained)) : grained, input.a);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const createWebGPUPostProcessing = (renderer: WebGPURenderer): PostProcessing => {
|
|
56
|
+
let effects: ReadonlyArray<PostEffect> = [];
|
|
57
|
+
let pipeline: RenderPipeline | undefined;
|
|
58
|
+
// The scene and camera the pipeline's scene pass draws: a new one rebuilds it.
|
|
59
|
+
let drawn: { readonly scene: Scene; readonly camera: Camera } | undefined;
|
|
60
|
+
|
|
61
|
+
const dispose = (): void => {
|
|
62
|
+
pipeline?.dispose();
|
|
63
|
+
pipeline = undefined;
|
|
64
|
+
drawn = undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const build = (scene: Scene, camera: Camera): RenderPipeline => {
|
|
68
|
+
dispose();
|
|
69
|
+
const output = effects.reduce<Colour>(applied, pass(scene, camera));
|
|
70
|
+
const next = new RenderPipeline(renderer, output);
|
|
71
|
+
pipeline = next;
|
|
72
|
+
drawn = { scene, camera };
|
|
73
|
+
return next;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
configure: (next) => {
|
|
78
|
+
if (effectsEqual(next, effects)) return;
|
|
79
|
+
effects = next;
|
|
80
|
+
dispose();
|
|
81
|
+
},
|
|
82
|
+
// The pipeline follows the renderer's size.
|
|
83
|
+
setSize: () => undefined,
|
|
84
|
+
render: (scene, camera) => {
|
|
85
|
+
if (effects.length === 0) return false;
|
|
86
|
+
const current =
|
|
87
|
+
pipeline !== undefined && drawn?.scene === scene && drawn.camera === camera
|
|
88
|
+
? pipeline
|
|
89
|
+
: build(scene, camera);
|
|
90
|
+
current.render();
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
dispose,
|
|
94
|
+
};
|
|
95
|
+
};
|