@react-three/drei
Version:
useful add-ons for react-three-fiber
507 lines (492 loc) • 21.2 kB
JavaScript
import _extends from '@babel/runtime/helpers/esm/extends';
import * as THREE from 'three';
import * as React from 'react';
import { extend, useThree, useLoader, useFrame } from '@react-three/fiber';
import { shaderMaterial } from './shaderMaterial.js';
import { version } from '../helpers/constants.js';
const SplatMaterial = /* @__PURE__ */shaderMaterial({
alphaTest: 0,
viewport: /* @__PURE__ */new THREE.Vector2(1980, 1080),
focal: 1000.0,
centerAndScaleTexture: null,
covAndColorTexture: null
}, /*glsl*/`
precision highp sampler2D;
precision highp usampler2D;
out vec4 vColor;
out vec3 vPosition;
uniform vec2 resolution;
uniform vec2 viewport;
uniform float focal;
attribute uint splatIndex;
uniform sampler2D centerAndScaleTexture;
uniform usampler2D covAndColorTexture;
vec2 unpackInt16(in uint value) {
int v = int(value);
int v0 = v >> 16;
int v1 = (v & 0xFFFF);
if((v & 0x8000) != 0)
v1 |= 0xFFFF0000;
return vec2(float(v1), float(v0));
}
void main () {
ivec2 texSize = textureSize(centerAndScaleTexture, 0);
ivec2 texPos = ivec2(splatIndex%uint(texSize.x), splatIndex/uint(texSize.x));
vec4 centerAndScaleData = texelFetch(centerAndScaleTexture, texPos, 0);
vec4 center = vec4(centerAndScaleData.xyz, 1);
vec4 camspace = modelViewMatrix * center;
vec4 pos2d = projectionMatrix * camspace;
float bounds = 1.2 * pos2d.w;
if (pos2d.z < -pos2d.w || pos2d.x < -bounds || pos2d.x > bounds
|| pos2d.y < -bounds || pos2d.y > bounds) {
gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
return;
}
uvec4 covAndColorData = texelFetch(covAndColorTexture, texPos, 0);
vec2 cov3D_M11_M12 = unpackInt16(covAndColorData.x) * centerAndScaleData.w;
vec2 cov3D_M13_M22 = unpackInt16(covAndColorData.y) * centerAndScaleData.w;
vec2 cov3D_M23_M33 = unpackInt16(covAndColorData.z) * centerAndScaleData.w;
mat3 Vrk = mat3(
cov3D_M11_M12.x, cov3D_M11_M12.y, cov3D_M13_M22.x,
cov3D_M11_M12.y, cov3D_M13_M22.y, cov3D_M23_M33.x,
cov3D_M13_M22.x, cov3D_M23_M33.x, cov3D_M23_M33.y
);
mat3 J = mat3(
focal / camspace.z, 0., -(focal * camspace.x) / (camspace.z * camspace.z),
0., focal / camspace.z, -(focal * camspace.y) / (camspace.z * camspace.z),
0., 0., 0.
);
mat3 W = transpose(mat3(modelViewMatrix));
mat3 T = W * J;
mat3 cov = transpose(T) * Vrk * T;
vec2 vCenter = vec2(pos2d) / pos2d.w;
float diagonal1 = cov[0][0] + 0.3;
float offDiagonal = cov[0][1];
float diagonal2 = cov[1][1] + 0.3;
float mid = 0.5 * (diagonal1 + diagonal2);
float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal));
float lambda1 = mid + radius;
float lambda2 = max(mid - radius, 0.1);
vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1));
vec2 v1 = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector;
vec2 v2 = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x);
uint colorUint = covAndColorData.w;
vColor = vec4(
float(colorUint & uint(0xFF)) / 255.0,
float((colorUint >> uint(8)) & uint(0xFF)) / 255.0,
float((colorUint >> uint(16)) & uint(0xFF)) / 255.0,
float(colorUint >> uint(24)) / 255.0
);
vPosition = position;
gl_Position = vec4(
vCenter
+ position.x * v2 / viewport * 2.0
+ position.y * v1 / viewport * 2.0, pos2d.z / pos2d.w, 1.0);
}
`, /*glsl*/`
#include <alphatest_pars_fragment>
#include <alphahash_pars_fragment>
in vec4 vColor;
in vec3 vPosition;
void main () {
float A = -dot(vPosition.xy, vPosition.xy);
if (A < -4.0) discard;
float B = exp(A) * vColor.a;
vec4 diffuseColor = vec4(vColor.rgb, B);
#include <alphatest_fragment>
#include <alphahash_fragment>
gl_FragColor = diffuseColor;
#include <tonemapping_fragment>
#include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
}
`);
function createWorker(self) {
let matrices = null;
let offset = 0;
function sortSplats(view, hashed = false) {
const vertexCount = matrices.length / 16;
const threshold = -0.0001;
let maxDepth = -Infinity;
let minDepth = Infinity;
const depthList = new Float32Array(vertexCount);
const sizeList = new Int32Array(depthList.buffer);
const validIndexList = new Int32Array(vertexCount);
let validCount = 0;
for (let i = 0; i < vertexCount; i++) {
// Sign of depth is reversed
const depth = view[0] * matrices[i * 16 + 12] + view[1] * matrices[i * 16 + 13] + view[2] * matrices[i * 16 + 14] + view[3];
// Skip behind of camera and small, transparent splat
if (hashed || depth < 0 && matrices[i * 16 + 15] > threshold * depth) {
depthList[validCount] = depth;
validIndexList[validCount] = i;
validCount++;
if (depth > maxDepth) maxDepth = depth;
if (depth < minDepth) minDepth = depth;
}
}
// This is a 16 bit single-pass counting sort
const depthInv = (256 * 256 - 1) / (maxDepth - minDepth);
const counts0 = new Uint32Array(256 * 256);
for (let i = 0; i < validCount; i++) {
sizeList[i] = (depthList[i] - minDepth) * depthInv | 0;
counts0[sizeList[i]]++;
}
const starts0 = new Uint32Array(256 * 256);
for (let i = 1; i < 256 * 256; i++) starts0[i] = starts0[i - 1] + counts0[i - 1];
const depthIndex = new Uint32Array(validCount);
for (let i = 0; i < validCount; i++) depthIndex[starts0[sizeList[i]]++] = validIndexList[i];
return depthIndex;
}
self.onmessage = e => {
if (e.data.method == 'push') {
if (offset === 0) matrices = new Float32Array(e.data.length);
const new_matrices = new Float32Array(e.data.matrices);
matrices.set(new_matrices, offset);
offset += new_matrices.length;
} else if (e.data.method == 'sort') {
if (matrices !== null) {
const indices = sortSplats(new Float32Array(e.data.view), e.data.hashed);
// @ts-ignore
self.postMessage({
indices,
key: e.data.key
}, [indices.buffer]);
}
}
};
}
class SplatLoader extends THREE.Loader {
constructor(...args) {
super(...args);
// WebGLRenderer, needs to be filled out!
this.gl = null;
// Default chunk size for lazy loading
this.chunkSize = 25000;
}
load(url, onLoad, onProgress, onError) {
const shared = {
gl: this.gl,
url: this.manager.resolveURL(url),
worker: new Worker(URL.createObjectURL(new Blob(['(', createWorker.toString(), ')(self)'], {
type: 'application/javascript'
}))),
manager: this.manager,
update: (target, camera, hashed) => update(camera, shared, target, hashed),
connect: target => connect(shared, target),
loading: false,
loaded: false,
loadedVertexCount: 0,
chunkSize: this.chunkSize,
totalDownloadBytes: 0,
numVertices: 0,
rowLength: 3 * 4 + 3 * 4 + 4 + 4,
maxVertexes: 0,
bufferTextureWidth: 0,
bufferTextureHeight: 0,
stream: null,
centerAndScaleData: null,
covAndColorData: null,
covAndColorTexture: null,
centerAndScaleTexture: null,
onProgress
};
load(shared).then(onLoad).catch(e => {
onError == null || onError(e);
shared.manager.itemError(shared.url);
});
}
}
async function load(shared) {
shared.manager.itemStart(shared.url);
const data = await fetch(shared.url);
if (data.body === null) throw 'Failed to fetch file';
let _totalDownloadBytes = data.headers.get('Content-Length');
const totalDownloadBytes = _totalDownloadBytes ? parseInt(_totalDownloadBytes) : undefined;
if (totalDownloadBytes == undefined) throw 'Failed to get content length';
shared.stream = data.body.getReader();
shared.totalDownloadBytes = totalDownloadBytes;
shared.numVertices = Math.floor(shared.totalDownloadBytes / shared.rowLength);
const context = shared.gl.getContext();
let maxTextureSize = context.getParameter(context.MAX_TEXTURE_SIZE);
shared.maxVertexes = maxTextureSize * maxTextureSize;
if (shared.numVertices > shared.maxVertexes) shared.numVertices = shared.maxVertexes;
shared.bufferTextureWidth = maxTextureSize;
shared.bufferTextureHeight = Math.floor((shared.numVertices - 1) / maxTextureSize) + 1;
shared.centerAndScaleData = new Float32Array(shared.bufferTextureWidth * shared.bufferTextureHeight * 4);
shared.covAndColorData = new Uint32Array(shared.bufferTextureWidth * shared.bufferTextureHeight * 4);
shared.centerAndScaleTexture = new THREE.DataTexture(shared.centerAndScaleData, shared.bufferTextureWidth, shared.bufferTextureHeight, THREE.RGBAFormat, THREE.FloatType);
shared.centerAndScaleTexture.needsUpdate = true;
shared.covAndColorTexture = new THREE.DataTexture(shared.covAndColorData, shared.bufferTextureWidth, shared.bufferTextureHeight, THREE.RGBAIntegerFormat, THREE.UnsignedIntType);
shared.covAndColorTexture.internalFormat = 'RGBA32UI';
shared.covAndColorTexture.needsUpdate = true;
return shared;
}
async function lazyLoad(shared) {
shared.loading = true;
let bytesDownloaded = 0;
let bytesProcessed = 0;
const chunks = [];
let lastReportedProgress = 0;
const lengthComputable = shared.totalDownloadBytes !== 0;
while (true) {
try {
const {
value,
done
} = await shared.stream.read();
if (done) break;
bytesDownloaded += value.length;
if (shared.totalDownloadBytes != undefined) {
const percent = bytesDownloaded / shared.totalDownloadBytes * 100;
if (shared.onProgress && percent - lastReportedProgress > 1) {
const event = new ProgressEvent('progress', {
lengthComputable,
loaded: bytesDownloaded,
total: shared.totalDownloadBytes
});
shared.onProgress(event);
lastReportedProgress = percent;
}
}
chunks.push(value);
const bytesRemains = bytesDownloaded - bytesProcessed;
if (shared.totalDownloadBytes != undefined && bytesRemains > shared.rowLength * shared.chunkSize) {
let vertexCount = Math.floor(bytesRemains / shared.rowLength);
const concatenatedChunksbuffer = new Uint8Array(bytesRemains);
let offset = 0;
for (const chunk of chunks) {
concatenatedChunksbuffer.set(chunk, offset);
offset += chunk.length;
}
chunks.length = 0;
if (bytesRemains > vertexCount * shared.rowLength) {
const extra_data = new Uint8Array(bytesRemains - vertexCount * shared.rowLength);
extra_data.set(concatenatedChunksbuffer.subarray(bytesRemains - extra_data.length, bytesRemains), 0);
chunks.push(extra_data);
}
const buffer = new Uint8Array(vertexCount * shared.rowLength);
buffer.set(concatenatedChunksbuffer.subarray(0, buffer.byteLength), 0);
const matrices = pushDataBuffer(shared, buffer.buffer, vertexCount);
shared.worker.postMessage({
method: 'push',
src: shared.url,
length: shared.numVertices * 16,
matrices: matrices.buffer
}, [matrices.buffer]);
bytesProcessed += vertexCount * shared.rowLength;
if (shared.onProgress) {
const event = new ProgressEvent('progress', {
lengthComputable,
loaded: shared.totalDownloadBytes,
total: shared.totalDownloadBytes
});
shared.onProgress(event);
}
}
} catch (error) {
console.error(error);
break;
}
}
if (bytesDownloaded - bytesProcessed > 0) {
// Concatenate the chunks into a single Uint8Array
let concatenatedChunks = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
let offset = 0;
for (const chunk of chunks) {
concatenatedChunks.set(chunk, offset);
offset += chunk.length;
}
let numVertices = Math.floor(concatenatedChunks.byteLength / shared.rowLength);
const matrices = pushDataBuffer(shared, concatenatedChunks.buffer, numVertices);
shared.worker.postMessage({
method: 'push',
src: shared.url,
length: numVertices * 16,
matrices: matrices.buffer
}, [matrices.buffer]);
}
shared.loaded = true;
shared.manager.itemEnd(shared.url);
}
function update(camera, shared, target, hashed) {
camera.updateMatrixWorld();
shared.gl.getCurrentViewport(target.viewport);
// @ts-ignore
target.material.viewport.x = target.viewport.z;
// @ts-ignore
target.material.viewport.y = target.viewport.w;
target.material.focal = target.viewport.w / 2.0 * Math.abs(camera.projectionMatrix.elements[5]);
if (target.ready) {
if (hashed && target.sorted) return;
target.ready = false;
const view = new Float32Array([target.modelViewMatrix.elements[2], -target.modelViewMatrix.elements[6], target.modelViewMatrix.elements[10], target.modelViewMatrix.elements[14]]);
shared.worker.postMessage({
method: 'sort',
src: shared.url,
key: target.uuid,
view: view.buffer,
hashed
}, [view.buffer]);
if (hashed && shared.loaded) target.sorted = true;
}
}
function connect(shared, target) {
if (!shared.loading) lazyLoad(shared);
target.ready = false;
target.pm = new THREE.Matrix4();
target.vm1 = new THREE.Matrix4();
target.vm2 = new THREE.Matrix4();
target.viewport = new THREE.Vector4();
let splatIndexArray = new Uint32Array(shared.bufferTextureWidth * shared.bufferTextureHeight);
const splatIndexes = new THREE.InstancedBufferAttribute(splatIndexArray, 1, false);
splatIndexes.setUsage(THREE.DynamicDrawUsage);
const geometry = target.geometry = new THREE.InstancedBufferGeometry();
const positionsArray = new Float32Array(6 * 3);
const positions = new THREE.BufferAttribute(positionsArray, 3);
geometry.setAttribute('position', positions);
positions.setXYZ(2, -2.0, 2.0, 0.0);
positions.setXYZ(1, 2.0, 2.0, 0.0);
positions.setXYZ(0, -2.0, -2.0, 0.0);
positions.setXYZ(5, -2.0, -2.0, 0.0);
positions.setXYZ(4, 2.0, 2.0, 0.0);
positions.setXYZ(3, 2.0, -2.0, 0.0);
positions.needsUpdate = true;
geometry.setAttribute('splatIndex', splatIndexes);
geometry.instanceCount = 1;
function listener(e) {
if (target && e.data.key === target.uuid) {
let indexes = new Uint32Array(e.data.indices);
// @ts-ignore
geometry.attributes.splatIndex.set(indexes);
geometry.attributes.splatIndex.needsUpdate = true;
geometry.instanceCount = indexes.length;
target.ready = true;
}
}
shared.worker.addEventListener('message', listener);
async function wait() {
while (true) {
const centerAndScaleTextureProperties = shared.gl.properties.get(shared.centerAndScaleTexture);
const covAndColorTextureProperties = shared.gl.properties.get(shared.covAndColorTexture);
if (centerAndScaleTextureProperties != null && centerAndScaleTextureProperties.__webglTexture && covAndColorTextureProperties != null && covAndColorTextureProperties.__webglTexture && shared.loadedVertexCount > 0) break;
await new Promise(resolve => setTimeout(resolve, 10));
}
target.ready = true;
}
wait();
return () => shared.worker.removeEventListener('message', listener);
}
function pushDataBuffer(shared, buffer, vertexCount) {
const context = shared.gl.getContext();
if (shared.loadedVertexCount + vertexCount > shared.maxVertexes) vertexCount = shared.maxVertexes - shared.loadedVertexCount;
if (vertexCount <= 0) throw 'Failed to parse file';
const u_buffer = new Uint8Array(buffer);
const f_buffer = new Float32Array(buffer);
const matrices = new Float32Array(vertexCount * 16);
const covAndColorData_uint8 = new Uint8Array(shared.covAndColorData.buffer);
const covAndColorData_int16 = new Int16Array(shared.covAndColorData.buffer);
for (let i = 0; i < vertexCount; i++) {
const quat = new THREE.Quaternion(-(u_buffer[32 * i + 28 + 1] - 128) / 128.0, (u_buffer[32 * i + 28 + 2] - 128) / 128.0, (u_buffer[32 * i + 28 + 3] - 128) / 128.0, -(u_buffer[32 * i + 28 + 0] - 128) / 128.0);
quat.invert();
const center = new THREE.Vector3(f_buffer[8 * i + 0], f_buffer[8 * i + 1], -f_buffer[8 * i + 2]);
const scale = new THREE.Vector3(f_buffer[8 * i + 3 + 0], f_buffer[8 * i + 3 + 1], f_buffer[8 * i + 3 + 2]);
const mtx = new THREE.Matrix4();
mtx.makeRotationFromQuaternion(quat);
mtx.transpose();
mtx.scale(scale);
const mtx_t = mtx.clone();
mtx.transpose();
mtx.premultiply(mtx_t);
mtx.setPosition(center);
const cov_indexes = [0, 1, 2, 5, 6, 10];
let max_value = 0.0;
for (let j = 0; j < cov_indexes.length; j++) if (Math.abs(mtx.elements[cov_indexes[j]]) > max_value) max_value = Math.abs(mtx.elements[cov_indexes[j]]);
let destOffset = shared.loadedVertexCount * 4 + i * 4;
shared.centerAndScaleData[destOffset + 0] = center.x;
shared.centerAndScaleData[destOffset + 1] = -center.y;
shared.centerAndScaleData[destOffset + 2] = center.z;
shared.centerAndScaleData[destOffset + 3] = max_value / 32767.0;
destOffset = shared.loadedVertexCount * 8 + i * 4 * 2;
for (let j = 0; j < cov_indexes.length; j++) covAndColorData_int16[destOffset + j] = mtx.elements[cov_indexes[j]] * 32767.0 / max_value;
// RGBA
destOffset = shared.loadedVertexCount * 16 + (i * 4 + 3) * 4;
const col = new THREE.Color(u_buffer[32 * i + 24 + 0] / 255, u_buffer[32 * i + 24 + 1] / 255, u_buffer[32 * i + 24 + 2] / 255);
col.convertSRGBToLinear();
covAndColorData_uint8[destOffset + 0] = col.r * 255;
covAndColorData_uint8[destOffset + 1] = col.g * 255;
covAndColorData_uint8[destOffset + 2] = col.b * 255;
covAndColorData_uint8[destOffset + 3] = u_buffer[32 * i + 24 + 3];
// Store scale and transparent to remove splat in sorting process
mtx.elements[15] = Math.max(scale.x, scale.y, scale.z) * u_buffer[32 * i + 24 + 3] / 255.0;
for (let j = 0; j < 16; j++) matrices[i * 16 + j] = mtx.elements[j];
}
while (vertexCount > 0) {
let width = 0;
let height = 0;
const xoffset = shared.loadedVertexCount % shared.bufferTextureWidth;
const yoffset = Math.floor(shared.loadedVertexCount / shared.bufferTextureWidth);
if (shared.loadedVertexCount % shared.bufferTextureWidth != 0) {
width = Math.min(shared.bufferTextureWidth, xoffset + vertexCount) - xoffset;
height = 1;
} else if (Math.floor(vertexCount / shared.bufferTextureWidth) > 0) {
width = shared.bufferTextureWidth;
height = Math.floor(vertexCount / shared.bufferTextureWidth);
} else {
width = vertexCount % shared.bufferTextureWidth;
height = 1;
}
const centerAndScaleTextureProperties = shared.gl.properties.get(shared.centerAndScaleTexture);
context.bindTexture(context.TEXTURE_2D, centerAndScaleTextureProperties.__webglTexture);
context.texSubImage2D(context.TEXTURE_2D, 0, xoffset, yoffset, width, height, context.RGBA, context.FLOAT, shared.centerAndScaleData, shared.loadedVertexCount * 4);
const covAndColorTextureProperties = shared.gl.properties.get(shared.covAndColorTexture);
context.bindTexture(context.TEXTURE_2D, covAndColorTextureProperties.__webglTexture);
context.texSubImage2D(context.TEXTURE_2D, 0, xoffset, yoffset, width, height,
// @ts-ignore
context.RGBA_INTEGER, context.UNSIGNED_INT, shared.covAndColorData, shared.loadedVertexCount * 4);
shared.gl.resetState();
shared.loadedVertexCount += width * height;
vertexCount -= width * height;
}
return matrices;
}
function Splat({
src,
toneMapped = false,
alphaTest = 0,
alphaHash = false,
chunkSize = 25000,
...props
}) {
extend({
SplatMaterial
});
const ref = React.useRef(null);
const gl = useThree(state => state.gl);
const camera = useThree(state => state.camera);
// Shared state, globally memoized, the same url re-uses the same daza
const shared = useLoader(SplatLoader, src, loader => {
loader.gl = gl;
loader.chunkSize = chunkSize;
});
// Listen to worker results, apply them to the target mesh
React.useLayoutEffect(() => shared.connect(ref.current), [src]);
// Update the worker
useFrame(() => shared.update(ref.current, camera, alphaHash));
return /*#__PURE__*/React.createElement("mesh", _extends({
ref: ref,
frustumCulled: false
}, props), /*#__PURE__*/React.createElement("splatMaterial", {
key: `${src}/${alphaTest}/${alphaHash}${SplatMaterial.key}`,
transparent: !alphaHash,
depthTest: true,
alphaTest: alphaHash ? 0 : alphaTest,
centerAndScaleTexture: shared.centerAndScaleTexture,
covAndColorTexture: shared.covAndColorTexture,
depthWrite: alphaHash ? true : alphaTest > 0,
blending: alphaHash ? THREE.NormalBlending : THREE.CustomBlending,
blendSrcAlpha: THREE.OneFactor,
alphaHash: !!alphaHash,
toneMapped: toneMapped
}));
}
export { Splat };