r3f-peridot 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/LICENSE +22 -0
- package/README.md +333 -0
- package/dist/index.d.mts +125 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +817 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +789 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
import { forwardRef, useRef, useEffect, useImperativeHandle } from 'react';
|
|
2
|
+
import { useThree, useFrame } from '@react-three/fiber';
|
|
3
|
+
import * as THREE2 from 'three';
|
|
4
|
+
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
|
|
5
|
+
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
|
|
6
|
+
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
|
|
7
|
+
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
|
|
8
|
+
import { Pass, FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
|
|
9
|
+
|
|
10
|
+
// src/components/OutlineEffect.tsx
|
|
11
|
+
var FindSurfaces = class {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.surfaceId = 0;
|
|
14
|
+
this.surfaceId = 0;
|
|
15
|
+
}
|
|
16
|
+
/*
|
|
17
|
+
* Returns the surface Ids as a Float32Array that can be inserted as a vertex attribute
|
|
18
|
+
*/
|
|
19
|
+
getSurfaceIdAttribute(mesh) {
|
|
20
|
+
const bufferGeometry = mesh.geometry;
|
|
21
|
+
const numVertices = bufferGeometry.attributes.position.count;
|
|
22
|
+
const vertexIdToSurfaceId = this._generateSurfaceIds(mesh);
|
|
23
|
+
const colors = [];
|
|
24
|
+
for (let i = 0; i < numVertices; i++) {
|
|
25
|
+
const vertexId = i;
|
|
26
|
+
const surfaceId = vertexIdToSurfaceId[vertexId];
|
|
27
|
+
colors.push(surfaceId, 0, 0, 1);
|
|
28
|
+
}
|
|
29
|
+
const colorsTypedArray = new Float32Array(colors);
|
|
30
|
+
return colorsTypedArray;
|
|
31
|
+
}
|
|
32
|
+
/*
|
|
33
|
+
* Returns a `vertexIdToSurfaceId` map
|
|
34
|
+
* given a vertex, returns the surfaceId
|
|
35
|
+
*/
|
|
36
|
+
_generateSurfaceIds(mesh) {
|
|
37
|
+
const bufferGeometry = mesh.geometry;
|
|
38
|
+
if (!bufferGeometry.index) {
|
|
39
|
+
const indices = [];
|
|
40
|
+
const numVertices = bufferGeometry.attributes.position.count;
|
|
41
|
+
for (let i = 0; i < numVertices; i++) {
|
|
42
|
+
indices.push(i);
|
|
43
|
+
}
|
|
44
|
+
bufferGeometry.setIndex(indices);
|
|
45
|
+
}
|
|
46
|
+
const numIndices = bufferGeometry.index.count;
|
|
47
|
+
const indexBuffer = bufferGeometry.index.array;
|
|
48
|
+
const vertexMap = {};
|
|
49
|
+
for (let i = 0; i < numIndices; i += 3) {
|
|
50
|
+
const i1 = indexBuffer[i + 0];
|
|
51
|
+
const i2 = indexBuffer[i + 1];
|
|
52
|
+
const i3 = indexBuffer[i + 2];
|
|
53
|
+
add(i1, i2);
|
|
54
|
+
add(i1, i3);
|
|
55
|
+
add(i2, i3);
|
|
56
|
+
}
|
|
57
|
+
function add(a, b) {
|
|
58
|
+
if (vertexMap[a] == void 0) vertexMap[a] = [];
|
|
59
|
+
if (vertexMap[b] == void 0) vertexMap[b] = [];
|
|
60
|
+
if (vertexMap[a].indexOf(b) == -1) vertexMap[a].push(b);
|
|
61
|
+
if (vertexMap[b].indexOf(a) == -1) vertexMap[b].push(a);
|
|
62
|
+
}
|
|
63
|
+
const frontierNodes = Object.keys(vertexMap).map((v) => Number(v));
|
|
64
|
+
const exploredNodes = {};
|
|
65
|
+
const vertexIdToSurfaceId = {};
|
|
66
|
+
while (frontierNodes.length > 0) {
|
|
67
|
+
const node = frontierNodes.pop();
|
|
68
|
+
if (exploredNodes[node]) continue;
|
|
69
|
+
const surfaceVertices = getNeighborsNonRecursive(node);
|
|
70
|
+
for (const v of surfaceVertices) {
|
|
71
|
+
exploredNodes[v] = true;
|
|
72
|
+
vertexIdToSurfaceId[v] = this.surfaceId;
|
|
73
|
+
}
|
|
74
|
+
this.surfaceId += 1;
|
|
75
|
+
}
|
|
76
|
+
function getNeighborsNonRecursive(node) {
|
|
77
|
+
const frontier = [node];
|
|
78
|
+
const explored = {};
|
|
79
|
+
const result = [];
|
|
80
|
+
while (frontier.length > 0) {
|
|
81
|
+
const currentNode = frontier.pop();
|
|
82
|
+
if (explored[currentNode]) continue;
|
|
83
|
+
const neighbors = vertexMap[currentNode];
|
|
84
|
+
result.push(currentNode);
|
|
85
|
+
explored[currentNode] = true;
|
|
86
|
+
for (const n of neighbors) {
|
|
87
|
+
if (!explored[n]) {
|
|
88
|
+
frontier.push(n);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
return vertexIdToSurfaceId;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var FindSurfaces_default = FindSurfaces;
|
|
98
|
+
function getSurfaceIdMaterial() {
|
|
99
|
+
return new THREE2.ShaderMaterial({
|
|
100
|
+
uniforms: {
|
|
101
|
+
maxSurfaceId: { value: 1 }
|
|
102
|
+
},
|
|
103
|
+
vertexShader: getVertexShader(),
|
|
104
|
+
fragmentShader: getFragmentShader(),
|
|
105
|
+
vertexColors: true
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function getVertexShader() {
|
|
109
|
+
return `
|
|
110
|
+
varying vec2 v_uv;
|
|
111
|
+
varying vec4 vColor;
|
|
112
|
+
|
|
113
|
+
void main() {
|
|
114
|
+
v_uv = uv;
|
|
115
|
+
#ifdef USE_COLOR_ALPHA
|
|
116
|
+
vColor = color;
|
|
117
|
+
#else
|
|
118
|
+
vColor = vec4(color, 1.0);
|
|
119
|
+
#endif
|
|
120
|
+
|
|
121
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
function getFragmentShader() {
|
|
126
|
+
return `
|
|
127
|
+
varying vec2 v_uv;
|
|
128
|
+
varying vec4 vColor;
|
|
129
|
+
uniform float maxSurfaceId;
|
|
130
|
+
|
|
131
|
+
void main() {
|
|
132
|
+
// Normalize the surfaceId when writing to texture
|
|
133
|
+
// Surface ID needs rounding as precision can be lost in perspective correct interpolation
|
|
134
|
+
// - see https://github.com/OmarShehata/webgl-outlines/issues/9 for other solutions eg. flat interpolation.
|
|
135
|
+
float surfaceId = round(vColor.r) / maxSurfaceId;
|
|
136
|
+
gl_FragColor = vec4(surfaceId, 0.0, 0.0, 1.0);
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
function getDebugSurfaceIdMaterial() {
|
|
141
|
+
return new THREE2.ShaderMaterial({
|
|
142
|
+
uniforms: {},
|
|
143
|
+
vertexShader: getVertexShader(),
|
|
144
|
+
fragmentShader: `
|
|
145
|
+
varying vec2 v_uv;
|
|
146
|
+
varying vec4 vColor;
|
|
147
|
+
|
|
148
|
+
void main() {
|
|
149
|
+
int surfaceId = int(round(vColor.r) * 100.0);
|
|
150
|
+
float R = float(surfaceId % 255) / 255.0;
|
|
151
|
+
float G = float((surfaceId + 50) % 255) / 255.0;
|
|
152
|
+
float B = float((surfaceId * 20) % 255) / 255.0;
|
|
153
|
+
|
|
154
|
+
gl_FragColor = vec4(R, G, B, 1.0);
|
|
155
|
+
}
|
|
156
|
+
`,
|
|
157
|
+
vertexColors: true
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/utils/CustomOutlinePass.ts
|
|
162
|
+
var CustomOutlinePass = class extends Pass {
|
|
163
|
+
constructor(resolution, scene, camera) {
|
|
164
|
+
super();
|
|
165
|
+
this.renderScene = scene;
|
|
166
|
+
this.renderCamera = camera;
|
|
167
|
+
this.resolution = new THREE2.Vector2(resolution.x, resolution.y);
|
|
168
|
+
this.fsQuad = new FullScreenQuad(null);
|
|
169
|
+
this.fsQuad.material = this.createOutlinePostProcessMaterial();
|
|
170
|
+
const surfaceBuffer = new THREE2.WebGLRenderTarget(this.resolution.x, this.resolution.y);
|
|
171
|
+
surfaceBuffer.texture.format = THREE2.RGBAFormat;
|
|
172
|
+
surfaceBuffer.texture.type = THREE2.HalfFloatType;
|
|
173
|
+
surfaceBuffer.texture.minFilter = THREE2.NearestFilter;
|
|
174
|
+
surfaceBuffer.texture.magFilter = THREE2.NearestFilter;
|
|
175
|
+
surfaceBuffer.texture.generateMipmaps = false;
|
|
176
|
+
surfaceBuffer.stencilBuffer = false;
|
|
177
|
+
this.surfaceBuffer = surfaceBuffer;
|
|
178
|
+
this.normalOverrideMaterial = new THREE2.MeshNormalMaterial();
|
|
179
|
+
this.surfaceIdOverrideMaterial = getSurfaceIdMaterial();
|
|
180
|
+
this.surfaceIdDebugOverrideMaterial = getDebugSurfaceIdMaterial();
|
|
181
|
+
}
|
|
182
|
+
dispose() {
|
|
183
|
+
this.surfaceBuffer.dispose();
|
|
184
|
+
this.fsQuad.dispose();
|
|
185
|
+
}
|
|
186
|
+
updateMaxSurfaceId(maxSurfaceId) {
|
|
187
|
+
this.surfaceIdOverrideMaterial.uniforms.maxSurfaceId.value = maxSurfaceId;
|
|
188
|
+
}
|
|
189
|
+
setSize(width, height) {
|
|
190
|
+
this.surfaceBuffer.setSize(width, height);
|
|
191
|
+
this.resolution.set(width, height);
|
|
192
|
+
const screenSize = new THREE2.Vector4(
|
|
193
|
+
this.resolution.x,
|
|
194
|
+
this.resolution.y,
|
|
195
|
+
1 / this.resolution.x,
|
|
196
|
+
1 / this.resolution.y
|
|
197
|
+
);
|
|
198
|
+
const material = this.fsQuad.material;
|
|
199
|
+
material.uniforms.screenSize.value.set(screenSize.x, screenSize.y, screenSize.z, screenSize.w);
|
|
200
|
+
}
|
|
201
|
+
getDebugVisualizeValue() {
|
|
202
|
+
return this.fsQuad.material.uniforms.debugVisualize.value;
|
|
203
|
+
}
|
|
204
|
+
isUsingSurfaceIds() {
|
|
205
|
+
const debugVisualize = this.getDebugVisualizeValue();
|
|
206
|
+
return debugVisualize == 0 || // Main outlines v2 mode
|
|
207
|
+
debugVisualize == 5 || // Render just surfaceID debug buffer
|
|
208
|
+
debugVisualize == 6;
|
|
209
|
+
}
|
|
210
|
+
render(renderer, writeBuffer, readBuffer) {
|
|
211
|
+
const debugVisualize = this.getDebugVisualizeValue();
|
|
212
|
+
const isUsingSurfaceIds = this.isUsingSurfaceIds();
|
|
213
|
+
const depthBufferValue = writeBuffer.depthBuffer;
|
|
214
|
+
writeBuffer.depthBuffer = false;
|
|
215
|
+
renderer.setRenderTarget(this.surfaceBuffer);
|
|
216
|
+
const overrideMaterialValue = this.renderScene.overrideMaterial;
|
|
217
|
+
if (isUsingSurfaceIds) {
|
|
218
|
+
if (debugVisualize == 5) {
|
|
219
|
+
this.renderScene.overrideMaterial = this.surfaceIdDebugOverrideMaterial;
|
|
220
|
+
} else {
|
|
221
|
+
this.renderScene.overrideMaterial = this.surfaceIdOverrideMaterial;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
this.renderScene.overrideMaterial = this.normalOverrideMaterial;
|
|
225
|
+
}
|
|
226
|
+
renderer.render(this.renderScene, this.renderCamera);
|
|
227
|
+
this.renderScene.overrideMaterial = overrideMaterialValue;
|
|
228
|
+
const material = this.fsQuad.material;
|
|
229
|
+
material.uniforms["depthBuffer"].value = readBuffer.depthTexture;
|
|
230
|
+
material.uniforms["surfaceBuffer"].value = this.surfaceBuffer.texture;
|
|
231
|
+
material.uniforms["sceneColorBuffer"].value = readBuffer.texture;
|
|
232
|
+
if (this.renderToScreen) {
|
|
233
|
+
renderer.setRenderTarget(null);
|
|
234
|
+
this.fsQuad.render(renderer);
|
|
235
|
+
} else {
|
|
236
|
+
renderer.setRenderTarget(writeBuffer);
|
|
237
|
+
this.fsQuad.render(renderer);
|
|
238
|
+
}
|
|
239
|
+
writeBuffer.depthBuffer = depthBufferValue;
|
|
240
|
+
}
|
|
241
|
+
get vertexShader() {
|
|
242
|
+
return `
|
|
243
|
+
varying vec2 vUv;
|
|
244
|
+
void main() {
|
|
245
|
+
vUv = uv;
|
|
246
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
247
|
+
}
|
|
248
|
+
`;
|
|
249
|
+
}
|
|
250
|
+
get fragmentShader() {
|
|
251
|
+
return `
|
|
252
|
+
#include <packing>
|
|
253
|
+
// The above include imports "perspectiveDepthToViewZ"
|
|
254
|
+
// and other GLSL functions from ThreeJS we need for reading depth.
|
|
255
|
+
uniform sampler2D sceneColorBuffer;
|
|
256
|
+
uniform sampler2D depthBuffer;
|
|
257
|
+
uniform sampler2D surfaceBuffer;
|
|
258
|
+
uniform float cameraNear;
|
|
259
|
+
uniform float cameraFar;
|
|
260
|
+
uniform vec4 screenSize;
|
|
261
|
+
uniform vec3 outlineColor;
|
|
262
|
+
uniform vec4 multiplierParameters;
|
|
263
|
+
uniform int debugVisualize;
|
|
264
|
+
|
|
265
|
+
varying vec2 vUv;
|
|
266
|
+
|
|
267
|
+
// Helper functions for reading from depth buffer.
|
|
268
|
+
float readDepth (sampler2D depthSampler, vec2 coord) {
|
|
269
|
+
float fragCoordZ = texture2D(depthSampler, coord).x;
|
|
270
|
+
float viewZ = perspectiveDepthToViewZ( fragCoordZ, cameraNear, cameraFar );
|
|
271
|
+
return viewZToOrthographicDepth( viewZ, cameraNear, cameraFar );
|
|
272
|
+
}
|
|
273
|
+
float getLinearDepth(vec3 pos) {
|
|
274
|
+
return -(viewMatrix * vec4(pos, 1.0)).z;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
float getLinearScreenDepth(sampler2D map) {
|
|
278
|
+
vec2 uv = gl_FragCoord.xy * screenSize.zw;
|
|
279
|
+
return readDepth(map,uv);
|
|
280
|
+
}
|
|
281
|
+
// Helper functions for reading normals and depth of neighboring pixels.
|
|
282
|
+
float getPixelDepth(int x, int y) {
|
|
283
|
+
// screenSize.zw is pixel size
|
|
284
|
+
// vUv is current position
|
|
285
|
+
return readDepth(depthBuffer, vUv + screenSize.zw * vec2(x, y));
|
|
286
|
+
}
|
|
287
|
+
// "surface value" is either the normal or the "surfaceID"
|
|
288
|
+
vec3 getSurfaceValue(int x, int y) {
|
|
289
|
+
vec3 val = texture2D(surfaceBuffer, vUv + screenSize.zw * vec2(x, y)).rgb;
|
|
290
|
+
return val;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
float saturateValue(float num) {
|
|
294
|
+
return clamp(num, 0.0, 1.0);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
float getSufaceIdDiff(vec3 surfaceValue) {
|
|
298
|
+
float surfaceIdDiff = 0.0;
|
|
299
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(1, 0));
|
|
300
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(0, 1));
|
|
301
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(0, 1));
|
|
302
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(0, -1));
|
|
303
|
+
|
|
304
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(1, 1));
|
|
305
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(1, -1));
|
|
306
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(-1, 1));
|
|
307
|
+
surfaceIdDiff += distance(surfaceValue, getSurfaceValue(-1, -1));
|
|
308
|
+
return surfaceIdDiff;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
void main() {
|
|
312
|
+
vec4 sceneColor = texture2D(sceneColorBuffer, vUv);
|
|
313
|
+
float depth = getPixelDepth(0, 0);
|
|
314
|
+
// "surfaceValue" is either the normal or the surfaceId
|
|
315
|
+
vec3 surfaceValue = getSurfaceValue(0, 0);
|
|
316
|
+
|
|
317
|
+
// Get the difference between depth of neighboring pixels and current.
|
|
318
|
+
float depthDiff = 0.0;
|
|
319
|
+
depthDiff += abs(depth - getPixelDepth(1, 0));
|
|
320
|
+
depthDiff += abs(depth - getPixelDepth(-1, 0));
|
|
321
|
+
depthDiff += abs(depth - getPixelDepth(0, 1));
|
|
322
|
+
depthDiff += abs(depth - getPixelDepth(0, -1));
|
|
323
|
+
|
|
324
|
+
// Get the difference between surface values of neighboring pixels
|
|
325
|
+
// and current
|
|
326
|
+
float surfaceValueDiff = getSufaceIdDiff(surfaceValue);
|
|
327
|
+
|
|
328
|
+
// Apply multiplier & bias to each
|
|
329
|
+
float depthBias = multiplierParameters.x;
|
|
330
|
+
float depthMultiplier = multiplierParameters.y;
|
|
331
|
+
float normalBias = multiplierParameters.z;
|
|
332
|
+
float normalMultiplier = multiplierParameters.w;
|
|
333
|
+
|
|
334
|
+
depthDiff = depthDiff * depthMultiplier;
|
|
335
|
+
depthDiff = saturateValue(depthDiff);
|
|
336
|
+
depthDiff = pow(depthDiff, depthBias);
|
|
337
|
+
|
|
338
|
+
if (debugVisualize != 0 && debugVisualize != 6) {
|
|
339
|
+
// Apply these params when using
|
|
340
|
+
// normals instead of surfaceIds
|
|
341
|
+
surfaceValueDiff = surfaceValueDiff * normalMultiplier;
|
|
342
|
+
surfaceValueDiff = saturateValue(surfaceValueDiff);
|
|
343
|
+
surfaceValueDiff = pow(surfaceValueDiff, normalBias);
|
|
344
|
+
} else {
|
|
345
|
+
if (surfaceValueDiff != 0.0) surfaceValueDiff = 1.0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
float outline = saturateValue(surfaceValueDiff + depthDiff);
|
|
349
|
+
|
|
350
|
+
// Combine outline with scene color.
|
|
351
|
+
vec4 outlineColor = vec4(outlineColor, 1.0);
|
|
352
|
+
gl_FragColor = vec4(mix(sceneColor, outlineColor, outline));
|
|
353
|
+
|
|
354
|
+
//// For debug visualization of the different inputs to this shader.
|
|
355
|
+
if (debugVisualize == 2) {
|
|
356
|
+
gl_FragColor = sceneColor;
|
|
357
|
+
}
|
|
358
|
+
if (debugVisualize == 3) {
|
|
359
|
+
gl_FragColor = vec4(vec3(depth), 1.0);
|
|
360
|
+
}
|
|
361
|
+
if (debugVisualize == 4 || debugVisualize == 5) {
|
|
362
|
+
// 4 visualizes the normal buffer
|
|
363
|
+
// 5 visualizes the surfaceID buffer
|
|
364
|
+
// Either way they are the same buffer, we change
|
|
365
|
+
// what we render into it
|
|
366
|
+
gl_FragColor = vec4(surfaceValue, 1.0);
|
|
367
|
+
}
|
|
368
|
+
if (debugVisualize == 6 || debugVisualize == 7) {
|
|
369
|
+
// Outlines only
|
|
370
|
+
gl_FragColor = vec4(vec3(outline * outlineColor), 1.0);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
createOutlinePostProcessMaterial() {
|
|
376
|
+
const camera = this.renderCamera;
|
|
377
|
+
return new THREE2.ShaderMaterial({
|
|
378
|
+
uniforms: {
|
|
379
|
+
debugVisualize: { value: 0 },
|
|
380
|
+
sceneColorBuffer: { value: null },
|
|
381
|
+
depthBuffer: { value: null },
|
|
382
|
+
surfaceBuffer: { value: null },
|
|
383
|
+
outlineColor: { value: new THREE2.Color(16777215) },
|
|
384
|
+
//4 scalar values packed in one uniform: depth multiplier, depth bias, and same for normals.
|
|
385
|
+
multiplierParameters: {
|
|
386
|
+
value: new THREE2.Vector4(0.9, 20, 1, 1)
|
|
387
|
+
},
|
|
388
|
+
cameraNear: { value: camera.near },
|
|
389
|
+
cameraFar: { value: camera.far },
|
|
390
|
+
screenSize: {
|
|
391
|
+
value: new THREE2.Vector4(
|
|
392
|
+
this.resolution.x,
|
|
393
|
+
this.resolution.y,
|
|
394
|
+
1 / this.resolution.x,
|
|
395
|
+
1 / this.resolution.y
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
vertexShader: this.vertexShader,
|
|
400
|
+
fragmentShader: this.fragmentShader
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// src/components/OutlineEffect.tsx
|
|
406
|
+
var OutlineEffect = forwardRef(
|
|
407
|
+
({
|
|
408
|
+
enabled = true,
|
|
409
|
+
outlineColor = "#ffffff",
|
|
410
|
+
depthBias = 0.9,
|
|
411
|
+
depthMultiplier = 20,
|
|
412
|
+
normalBias = 1,
|
|
413
|
+
normalMultiplier = 1,
|
|
414
|
+
debugVisualize = 0
|
|
415
|
+
}, ref) => {
|
|
416
|
+
const { gl, scene, camera, size, invalidate } = useThree();
|
|
417
|
+
const composerRef = useRef(null);
|
|
418
|
+
const outlinePassRef = useRef(null);
|
|
419
|
+
const fxaaPassRef = useRef(null);
|
|
420
|
+
const surfaceIdsComputedRef = useRef(false);
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
if (!enabled) {
|
|
423
|
+
if (composerRef.current) {
|
|
424
|
+
composerRef.current.dispose();
|
|
425
|
+
composerRef.current = null;
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const width = Math.max(1, size.width * gl.getPixelRatio());
|
|
430
|
+
const height = Math.max(1, size.height * gl.getPixelRatio());
|
|
431
|
+
const depthTexture = new THREE2.DepthTexture(width, height);
|
|
432
|
+
const renderTarget = new THREE2.WebGLRenderTarget(width, height, {
|
|
433
|
+
depthTexture,
|
|
434
|
+
depthBuffer: true
|
|
435
|
+
});
|
|
436
|
+
const composer = new EffectComposer(gl, renderTarget);
|
|
437
|
+
composerRef.current = composer;
|
|
438
|
+
const renderPass = new RenderPass(scene, camera);
|
|
439
|
+
composer.addPass(renderPass);
|
|
440
|
+
const customOutline = new CustomOutlinePass(new THREE2.Vector2(width, height), scene, camera);
|
|
441
|
+
customOutline.renderToScreen = true;
|
|
442
|
+
outlinePassRef.current = customOutline;
|
|
443
|
+
composer.addPass(customOutline);
|
|
444
|
+
const effectFXAA = new ShaderPass(FXAAShader);
|
|
445
|
+
effectFXAA.uniforms["resolution"].value.set(1 / width, 1 / height);
|
|
446
|
+
effectFXAA.renderToScreen = true;
|
|
447
|
+
customOutline.renderToScreen = false;
|
|
448
|
+
fxaaPassRef.current = effectFXAA;
|
|
449
|
+
composer.addPass(effectFXAA);
|
|
450
|
+
return () => {
|
|
451
|
+
composer.dispose();
|
|
452
|
+
renderTarget.dispose();
|
|
453
|
+
depthTexture.dispose();
|
|
454
|
+
};
|
|
455
|
+
}, [enabled, scene, camera, gl, size.width, size.height, invalidate]);
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
if (!enabled) return;
|
|
458
|
+
const isUsingSurfaceIds = debugVisualize === 0 || debugVisualize === 5 || debugVisualize === 6;
|
|
459
|
+
if (isUsingSurfaceIds && !surfaceIdsComputedRef.current) {
|
|
460
|
+
const findSurfaces = new FindSurfaces_default();
|
|
461
|
+
let maxSurfaceId = 0;
|
|
462
|
+
scene.traverse((object) => {
|
|
463
|
+
if (object instanceof THREE2.Mesh && object.geometry) {
|
|
464
|
+
try {
|
|
465
|
+
const geometry = object.geometry;
|
|
466
|
+
const surfaceIdAttribute = findSurfaces.getSurfaceIdAttribute(object);
|
|
467
|
+
geometry.setAttribute("color", new THREE2.BufferAttribute(surfaceIdAttribute, 4));
|
|
468
|
+
maxSurfaceId = Math.max(maxSurfaceId, findSurfaces.surfaceId);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.warn("Failed to compute surface IDs for mesh:", error);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
if (outlinePassRef.current && maxSurfaceId > 0) {
|
|
475
|
+
outlinePassRef.current.updateMaxSurfaceId(maxSurfaceId + 1);
|
|
476
|
+
}
|
|
477
|
+
surfaceIdsComputedRef.current = true;
|
|
478
|
+
} else if (!isUsingSurfaceIds && surfaceIdsComputedRef.current) {
|
|
479
|
+
scene.traverse((object) => {
|
|
480
|
+
if (object instanceof THREE2.Mesh && object.geometry) {
|
|
481
|
+
object.geometry.deleteAttribute("color");
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
surfaceIdsComputedRef.current = false;
|
|
485
|
+
}
|
|
486
|
+
}, [enabled, scene, debugVisualize]);
|
|
487
|
+
useFrame(() => {
|
|
488
|
+
if (!enabled || !outlinePassRef.current || !composerRef.current) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const outlinePass = outlinePassRef.current;
|
|
492
|
+
const material = outlinePass.fsQuad.material;
|
|
493
|
+
const uniforms = material.uniforms;
|
|
494
|
+
if (typeof outlineColor === "string") {
|
|
495
|
+
uniforms.outlineColor.value.set(outlineColor);
|
|
496
|
+
} else {
|
|
497
|
+
uniforms.outlineColor.value.copy(outlineColor);
|
|
498
|
+
}
|
|
499
|
+
uniforms.multiplierParameters.value.set(
|
|
500
|
+
depthBias,
|
|
501
|
+
depthMultiplier,
|
|
502
|
+
normalBias,
|
|
503
|
+
normalMultiplier
|
|
504
|
+
);
|
|
505
|
+
uniforms.debugVisualize.value = debugVisualize;
|
|
506
|
+
const cam = camera;
|
|
507
|
+
if ("near" in cam) {
|
|
508
|
+
uniforms.cameraNear.value = cam.near;
|
|
509
|
+
}
|
|
510
|
+
if ("far" in cam) {
|
|
511
|
+
uniforms.cameraFar.value = cam.far;
|
|
512
|
+
}
|
|
513
|
+
composerRef.current.render();
|
|
514
|
+
}, 1);
|
|
515
|
+
useEffect(() => {
|
|
516
|
+
if (!composerRef.current || !outlinePassRef.current || !fxaaPassRef.current) return;
|
|
517
|
+
const width = Math.max(1, size.width * gl.getPixelRatio());
|
|
518
|
+
const height = Math.max(1, size.height * gl.getPixelRatio());
|
|
519
|
+
composerRef.current.setSize(width, height);
|
|
520
|
+
outlinePassRef.current.setSize(width, height);
|
|
521
|
+
fxaaPassRef.current.uniforms["resolution"].value.set(1 / width, 1 / height);
|
|
522
|
+
}, [size, gl]);
|
|
523
|
+
useImperativeHandle(ref, () => ({
|
|
524
|
+
updateMaxSurfaceId: (maxSurfaceId) => {
|
|
525
|
+
if (outlinePassRef.current) {
|
|
526
|
+
outlinePassRef.current.updateMaxSurfaceId(maxSurfaceId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}));
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
OutlineEffect.displayName = "OutlineEffect";
|
|
534
|
+
|
|
535
|
+
// src/utils/VertexWelder.ts
|
|
536
|
+
function weldVertices(vertices, indices, thresholdAngle = 1) {
|
|
537
|
+
const mergedMap = {};
|
|
538
|
+
const vertexAliases = {};
|
|
539
|
+
function merge(i1, i2) {
|
|
540
|
+
if (mergedMap[i1] == void 0) mergedMap[i1] = [];
|
|
541
|
+
mergedMap[i1].push(i2);
|
|
542
|
+
}
|
|
543
|
+
function aliasDeletedVertex(deletedVertex, remainingVertex) {
|
|
544
|
+
if (deletedVertex == remainingVertex) return;
|
|
545
|
+
vertexAliases[deletedVertex] = remainingVertex;
|
|
546
|
+
}
|
|
547
|
+
const edgesToMerge = computeEdgesToMerge(vertices, indices, thresholdAngle);
|
|
548
|
+
const edgesToMergeMap = {};
|
|
549
|
+
for (let i = 0; i < edgesToMerge.length; i++) {
|
|
550
|
+
const edgesList = edgesToMerge[i];
|
|
551
|
+
for (const index of edgesList) {
|
|
552
|
+
const key = `${index[0]}_${index[1]}`;
|
|
553
|
+
edgesToMergeMap[key] = edgesList;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
557
|
+
const i1 = indices[i + 0];
|
|
558
|
+
const i2 = indices[i + 1];
|
|
559
|
+
const i3 = indices[i + 2];
|
|
560
|
+
const edges = [];
|
|
561
|
+
edges.push([i1, i2]);
|
|
562
|
+
edges.push([i1, i3]);
|
|
563
|
+
edges.push([i2, i3]);
|
|
564
|
+
for (const edge of edges) {
|
|
565
|
+
let index0 = edge[0];
|
|
566
|
+
let index1 = edge[1];
|
|
567
|
+
const reverseEdge = [index1, index0];
|
|
568
|
+
let isReverse = false;
|
|
569
|
+
let edgeToMerge;
|
|
570
|
+
const edgeKey = `${edge[0]}_${edge[1]}`;
|
|
571
|
+
const reverseEdgeKey = `${reverseEdge[0]}_${reverseEdge[1]}`;
|
|
572
|
+
if (edgesToMergeMap[edgeKey]) {
|
|
573
|
+
edgeToMerge = edge;
|
|
574
|
+
}
|
|
575
|
+
if (edgesToMergeMap[reverseEdgeKey]) {
|
|
576
|
+
edgeToMerge = reverseEdge;
|
|
577
|
+
isReverse = true;
|
|
578
|
+
}
|
|
579
|
+
if (edgeToMerge) {
|
|
580
|
+
const edgeKeyToUse = isReverse ? reverseEdgeKey : edgeKey;
|
|
581
|
+
const possibleEdges = edgesToMergeMap[edgeKeyToUse];
|
|
582
|
+
const possibleEdge1 = possibleEdges[0];
|
|
583
|
+
const possibleEdge2 = possibleEdges[1];
|
|
584
|
+
let otherEdge = possibleEdge1;
|
|
585
|
+
let originalEdge = possibleEdge2;
|
|
586
|
+
if (possibleEdge1[0] == index0 && possibleEdge1[1] == index1 || possibleEdge1[0] == index1 && possibleEdge1[1] == index0) {
|
|
587
|
+
otherEdge = possibleEdge2;
|
|
588
|
+
originalEdge = possibleEdge1;
|
|
589
|
+
}
|
|
590
|
+
let index2 = otherEdge[0];
|
|
591
|
+
let index3 = otherEdge[1];
|
|
592
|
+
index0 = originalEdge[0];
|
|
593
|
+
index1 = originalEdge[1];
|
|
594
|
+
if (index0 == index2 && index1 == index3) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const v0 = getVertexFromIndexBuffer(index0, vertices);
|
|
598
|
+
const v2 = getVertexFromIndexBuffer(index2, vertices);
|
|
599
|
+
if (v0.distanceTo(v2) > 0.1) {
|
|
600
|
+
const tmp = index3;
|
|
601
|
+
index3 = index2;
|
|
602
|
+
index2 = tmp;
|
|
603
|
+
}
|
|
604
|
+
if (vertexAliases[index0]) index0 = vertexAliases[index0];
|
|
605
|
+
if (vertexAliases[index1]) index1 = vertexAliases[index1];
|
|
606
|
+
if (vertexAliases[index2]) index2 = vertexAliases[index2];
|
|
607
|
+
if (vertexAliases[index3]) index3 = vertexAliases[index3];
|
|
608
|
+
merge(index0, index2);
|
|
609
|
+
merge(index1, index3);
|
|
610
|
+
aliasDeletedVertex(index2, index0);
|
|
611
|
+
aliasDeletedVertex(index3, index1);
|
|
612
|
+
const mergedEdgeKey = `${index2}_${index3}`;
|
|
613
|
+
delete edgesToMergeMap[edgeKeyToUse];
|
|
614
|
+
delete edgesToMergeMap[mergedEdgeKey];
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const finalMergeMap = fillOutMergeMap(mergedMap);
|
|
619
|
+
const newIndexBuffer = [];
|
|
620
|
+
for (let i = 0; i < indices.length; i++) {
|
|
621
|
+
const index = indices[i];
|
|
622
|
+
let newIndex = index;
|
|
623
|
+
if (finalMergeMap[index] != void 0) {
|
|
624
|
+
newIndex = finalMergeMap[index];
|
|
625
|
+
}
|
|
626
|
+
newIndexBuffer.push(newIndex);
|
|
627
|
+
}
|
|
628
|
+
return newIndexBuffer;
|
|
629
|
+
}
|
|
630
|
+
function getVertexFromIndexBuffer(index, positionAttr) {
|
|
631
|
+
return new Vector3(
|
|
632
|
+
positionAttr[index * 3 + 0],
|
|
633
|
+
positionAttr[index * 3 + 1],
|
|
634
|
+
positionAttr[index * 3 + 2]
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
function fillOutMergeMap(mergeMap) {
|
|
638
|
+
const newMergeMap = {};
|
|
639
|
+
for (let i = 0; i < Object.keys(mergeMap).length; i++) {
|
|
640
|
+
const key = Number(Object.keys(mergeMap)[i]);
|
|
641
|
+
const indices = mergeMap[key];
|
|
642
|
+
for (const ind of indices) {
|
|
643
|
+
newMergeMap[ind] = key;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return newMergeMap;
|
|
647
|
+
}
|
|
648
|
+
var Vector3 = class _Vector3 {
|
|
649
|
+
constructor(x = 0, y = 0, z = 0) {
|
|
650
|
+
this.x = x;
|
|
651
|
+
this.y = y;
|
|
652
|
+
this.z = z;
|
|
653
|
+
}
|
|
654
|
+
clone() {
|
|
655
|
+
return new _Vector3(this.x, this.y, this.z);
|
|
656
|
+
}
|
|
657
|
+
set(x, y, z) {
|
|
658
|
+
this.x = x;
|
|
659
|
+
this.y = y;
|
|
660
|
+
this.z = z;
|
|
661
|
+
return this;
|
|
662
|
+
}
|
|
663
|
+
distanceTo(v) {
|
|
664
|
+
const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;
|
|
665
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
666
|
+
}
|
|
667
|
+
lengthSq() {
|
|
668
|
+
return this.x * this.x + this.y * this.y + this.z * this.z;
|
|
669
|
+
}
|
|
670
|
+
multiplyScalar(scalar) {
|
|
671
|
+
this.x *= scalar;
|
|
672
|
+
this.y *= scalar;
|
|
673
|
+
this.z *= scalar;
|
|
674
|
+
return this;
|
|
675
|
+
}
|
|
676
|
+
subVectors(a, b) {
|
|
677
|
+
this.x = a.x - b.x;
|
|
678
|
+
this.y = a.y - b.y;
|
|
679
|
+
this.z = a.z - b.z;
|
|
680
|
+
return this;
|
|
681
|
+
}
|
|
682
|
+
dot(v) {
|
|
683
|
+
return this.x * v.x + this.y * v.y + this.z * v.z;
|
|
684
|
+
}
|
|
685
|
+
cross(v) {
|
|
686
|
+
return this.crossVectors(this, v);
|
|
687
|
+
}
|
|
688
|
+
crossVectors(a, b) {
|
|
689
|
+
const ax = a.x, ay = a.y, az = a.z;
|
|
690
|
+
const bx = b.x, by = b.y, bz = b.z;
|
|
691
|
+
this.x = ay * bz - az * by;
|
|
692
|
+
this.y = az * bx - ax * bz;
|
|
693
|
+
this.z = ax * by - ay * bx;
|
|
694
|
+
return this;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
var _v0 = new Vector3();
|
|
698
|
+
var _normal = new Vector3();
|
|
699
|
+
var precisionPoints = 4;
|
|
700
|
+
var precision = Math.pow(10, precisionPoints);
|
|
701
|
+
function hashVertex(v) {
|
|
702
|
+
return `${Math.round(v.x * precision)},${Math.round(v.y * precision)},${Math.round(
|
|
703
|
+
v.z * precision
|
|
704
|
+
)}`;
|
|
705
|
+
}
|
|
706
|
+
function getNormal(a, b, c, resultNormal) {
|
|
707
|
+
resultNormal.subVectors(c, b);
|
|
708
|
+
_v0.subVectors(a, b);
|
|
709
|
+
resultNormal.cross(_v0);
|
|
710
|
+
const targetLengthSq = resultNormal.lengthSq();
|
|
711
|
+
if (targetLengthSq > 0) {
|
|
712
|
+
return resultNormal.multiplyScalar(1 / Math.sqrt(targetLengthSq));
|
|
713
|
+
}
|
|
714
|
+
return resultNormal.set(0, 0, 0);
|
|
715
|
+
}
|
|
716
|
+
function computeEdgesToMerge(vertices, indices, thresholdAngle = 1) {
|
|
717
|
+
const DEG2RAD = Math.PI / 180;
|
|
718
|
+
const thresholdDot = Math.cos(DEG2RAD * thresholdAngle);
|
|
719
|
+
const indexCount = indices.length;
|
|
720
|
+
const indexArr = [0, 0, 0];
|
|
721
|
+
const hashes = new Array(3);
|
|
722
|
+
const edgeData = {};
|
|
723
|
+
const edgesToMerge = [];
|
|
724
|
+
for (let i = 0; i < indexCount; i += 3) {
|
|
725
|
+
indexArr[0] = indices[i];
|
|
726
|
+
indexArr[1] = indices[i + 1];
|
|
727
|
+
indexArr[2] = indices[i + 2];
|
|
728
|
+
const a = getVertexFromIndexBuffer(indexArr[0], vertices);
|
|
729
|
+
const b = getVertexFromIndexBuffer(indexArr[1], vertices);
|
|
730
|
+
const c = getVertexFromIndexBuffer(indexArr[2], vertices);
|
|
731
|
+
getNormal(a, b, c, _normal);
|
|
732
|
+
hashes[0] = hashVertex(a);
|
|
733
|
+
hashes[1] = hashVertex(b);
|
|
734
|
+
hashes[2] = hashVertex(c);
|
|
735
|
+
if (hashes[0] === hashes[1] || hashes[1] === hashes[2] || hashes[2] === hashes[0]) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
for (let j = 0; j < 3; j++) {
|
|
739
|
+
const jNext = (j + 1) % 3;
|
|
740
|
+
const vecHash0 = hashes[j];
|
|
741
|
+
const vecHash1 = hashes[jNext];
|
|
742
|
+
const hash = `${vecHash0}_${vecHash1}`;
|
|
743
|
+
const reverseHash = `${vecHash1}_${vecHash0}`;
|
|
744
|
+
if (reverseHash in edgeData && edgeData[reverseHash]) {
|
|
745
|
+
if (_normal.dot(edgeData[reverseHash].normal) > thresholdDot) {
|
|
746
|
+
const edge1 = [edgeData[reverseHash].index0, edgeData[reverseHash].index1];
|
|
747
|
+
const edge2 = [indexArr[j], indexArr[jNext]];
|
|
748
|
+
edgesToMerge.push([edge1, edge2]);
|
|
749
|
+
}
|
|
750
|
+
edgeData[reverseHash] = null;
|
|
751
|
+
} else if (!(hash in edgeData)) {
|
|
752
|
+
edgeData[hash] = {
|
|
753
|
+
index0: indexArr[j],
|
|
754
|
+
index1: indexArr[jNext],
|
|
755
|
+
normal: _normal.clone()
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return edgesToMerge;
|
|
761
|
+
}
|
|
762
|
+
function useSurfaceIds(enabled = true) {
|
|
763
|
+
const { scene } = useThree();
|
|
764
|
+
useEffect(() => {
|
|
765
|
+
if (!enabled) return;
|
|
766
|
+
const findSurfaces = new FindSurfaces_default();
|
|
767
|
+
let maxSurfaceId = 0;
|
|
768
|
+
scene.traverse((object) => {
|
|
769
|
+
if (object instanceof THREE2.Mesh && object.geometry) {
|
|
770
|
+
const geometry = object.geometry;
|
|
771
|
+
const surfaceIdAttribute = findSurfaces.getSurfaceIdAttribute(object);
|
|
772
|
+
geometry.setAttribute("color", new THREE2.BufferAttribute(surfaceIdAttribute, 4));
|
|
773
|
+
maxSurfaceId = Math.max(maxSurfaceId, findSurfaces.surfaceId);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
return () => {
|
|
777
|
+
scene.traverse((object) => {
|
|
778
|
+
if (object instanceof THREE2.Mesh && object.geometry) {
|
|
779
|
+
object.geometry.deleteAttribute("color");
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
}, [scene, enabled]);
|
|
784
|
+
}
|
|
785
|
+
var useSurfaceIds_default = useSurfaceIds;
|
|
786
|
+
|
|
787
|
+
export { CustomOutlinePass, FindSurfaces_default as FindSurfaces, OutlineEffect, getDebugSurfaceIdMaterial, getSurfaceIdMaterial, useSurfaceIds_default as useSurfaceIds, weldVertices };
|
|
788
|
+
//# sourceMappingURL=index.mjs.map
|
|
789
|
+
//# sourceMappingURL=index.mjs.map
|