splat / main.js
antimatter15
initial commit
930af7c
raw
history blame
33.2 kB
let cameras = [
{
id: 0,
img_name: "00001",
width: 1959,
height: 1090,
position: [
-3.0089893469241797, -0.11086489695181866, -3.7527640949141428,
],
rotation: [
[0.876134201218856, 0.06925962026449776, 0.47706599800804744],
[-0.04747421839895102, 0.9972110940209488, -0.057586739349882114],
[-0.4797239414934443, 0.027805376500959853, 0.8769787916452908],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 1,
img_name: "00009",
width: 1959,
height: 1090,
position: [
-2.5199776022057296, -0.09704735754873686, -3.6247725540304545,
],
rotation: [
[0.9982731285632193, -0.011928707708098955, -0.05751927260507243],
[0.0065061360949636325, 0.9955928229282383, -0.09355533724430458],
[0.058381769258182864, 0.09301955098900708, 0.9939511719154457],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 2,
img_name: "00017",
width: 1959,
height: 1090,
position: [
-0.7737533667465242, -0.3364271945329695, -2.9358969417573753,
],
rotation: [
[0.9998813418672372, 0.013742375651625236, -0.0069605529394208224],
[-0.014268370388586709, 0.996512943252834, -0.08220929105659476],
[0.00580653013657589, 0.08229885200307129, 0.9965907801935302],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 3,
img_name: "00025",
width: 1959,
height: 1090,
position: [
1.2198221749590001, -0.2196687861401182, -2.3183162007028453,
],
rotation: [
[0.9208648867765482, 0.0012010625395201253, 0.389880004297208],
[-0.06298204172269357, 0.987319521752825, 0.14571693239364383],
[-0.3847611242348369, -0.1587410451475895, 0.9092635249821667],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 4,
img_name: "00033",
width: 1959,
height: 1090,
position: [
1.742387858893817, -0.13848225198886954, -2.0566370113193146,
],
rotation: [
[0.24669889292141334, -0.08370189346592856, -0.9654706879349405],
[0.11343747891376445, 0.9919082664242816, -0.05700815184573074],
[0.9624300466054861, -0.09545671285663988, 0.2541976029815521],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 5,
img_name: "00041",
width: 1959,
height: 1090,
position: [
3.6567309419223935, -0.16470990600750707, -1.3458085590422042,
],
rotation: [
[0.2341293058324528, -0.02968330457755884, -0.9717522161434825],
[0.10270823606832301, 0.99469554638321, -0.005638106875665722],
[0.9667649592295676, -0.09848690996657204, 0.2359360976431732],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 6,
img_name: "00049",
width: 1959,
height: 1090,
position: [
3.9013554243203497, -0.2597500978038105, -0.8106154188297828,
],
rotation: [
[0.6717235545638952, -0.015718162115524837, -0.7406351366386528],
[0.055627354673906296, 0.9980224478387622, 0.029270992841185218],
[0.7387104058127439, -0.060861588786650656, 0.6712695459756353],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 7,
img_name: "00057",
width: 1959,
height: 1090,
position: [4.742994605467533, -0.05591660945412069, 0.9500365976084458],
rotation: [
[-0.17042655709210375, 0.01207080756938, -0.9852964448542146],
[0.1165090336695526, 0.9931575292530063, -0.00798543433078162],
[0.9784581921120181, -0.1161568667478904, -0.1706667764862097],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 8,
img_name: "00065",
width: 1959,
height: 1090,
position: [4.34676307626522, 0.08168160516967145, 1.0876221470355405],
rotation: [
[-0.003575447631888379, -0.044792503246552894, -0.9989899137764799],
[0.10770152645126597, 0.9931680875192705, -0.04491693593046672],
[0.9941768441149182, -0.10775333677534978, 0.0012732004866391048],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
{
id: 9,
img_name: "00073",
width: 1959,
height: 1090,
position: [3.264984351114202, 0.078974937336732, 1.0117200284114904],
rotation: [
[-0.026919994628162257, -0.1565891128261527, -0.9872968974090509],
[0.08444552208239385, 0.983768234577625, -0.1583319754069128],
[0.9960643893290491, -0.0876350978794554, -0.013259786205163005],
],
fy: 1164.6601287484507,
fx: 1159.5880733038064,
},
];
const camera = cameras[0];
function getProjectionMatrix(fx, fy, width, height) {
const znear = 0.2;
const zfar = 200;
return [
[(2 * fx) / width, 0, 0, 0],
[0, (2 * fy) / height, 0, 0],
[0, 0, zfar / (zfar - znear), 1],
[0, 0, -(zfar * znear) / (zfar - znear), 0],
].flat();
}
function getViewMatrix(camera) {
const R = camera.rotation.flat();
const t = camera.position;
const camToWorld = [
[R[0], R[1], R[2], 0],
[R[3], R[4], R[5], 0],
[R[6], R[7], R[8], 0],
[
-t[0] * R[0] - t[1] * R[3] - t[2] * R[6],
-t[0] * R[1] - t[1] * R[4] - t[2] * R[7],
-t[0] * R[2] - t[1] * R[5] - t[2] * R[8],
1,
],
].flat();
return camToWorld;
}
function multiply4(a, b) {
return [
b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12],
b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13],
b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14],
b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15],
b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12],
b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13],
b[4] * a[2] + b[5] * a[6] + b[6] * a[10] + b[7] * a[14],
b[4] * a[3] + b[5] * a[7] + b[6] * a[11] + b[7] * a[15],
b[8] * a[0] + b[9] * a[4] + b[10] * a[8] + b[11] * a[12],
b[8] * a[1] + b[9] * a[5] + b[10] * a[9] + b[11] * a[13],
b[8] * a[2] + b[9] * a[6] + b[10] * a[10] + b[11] * a[14],
b[8] * a[3] + b[9] * a[7] + b[10] * a[11] + b[11] * a[15],
b[12] * a[0] + b[13] * a[4] + b[14] * a[8] + b[15] * a[12],
b[12] * a[1] + b[13] * a[5] + b[14] * a[9] + b[15] * a[13],
b[12] * a[2] + b[13] * a[6] + b[14] * a[10] + b[15] * a[14],
b[12] * a[3] + b[13] * a[7] + b[14] * a[11] + b[15] * a[15],
];
}
function invert4(a) {
let b00 = a[0] * a[5] - a[1] * a[4];
let b01 = a[0] * a[6] - a[2] * a[4];
let b02 = a[0] * a[7] - a[3] * a[4];
let b03 = a[1] * a[6] - a[2] * a[5];
let b04 = a[1] * a[7] - a[3] * a[5];
let b05 = a[2] * a[7] - a[3] * a[6];
let b06 = a[8] * a[13] - a[9] * a[12];
let b07 = a[8] * a[14] - a[10] * a[12];
let b08 = a[8] * a[15] - a[11] * a[12];
let b09 = a[9] * a[14] - a[10] * a[13];
let b10 = a[9] * a[15] - a[11] * a[13];
let b11 = a[10] * a[15] - a[11] * a[14];
let det =
b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
if (!det) return null;
return [
(a[5] * b11 - a[6] * b10 + a[7] * b09) / det,
(a[2] * b10 - a[1] * b11 - a[3] * b09) / det,
(a[13] * b05 - a[14] * b04 + a[15] * b03) / det,
(a[10] * b04 - a[9] * b05 - a[11] * b03) / det,
(a[6] * b08 - a[4] * b11 - a[7] * b07) / det,
(a[0] * b11 - a[2] * b08 + a[3] * b07) / det,
(a[14] * b02 - a[12] * b05 - a[15] * b01) / det,
(a[8] * b05 - a[10] * b02 + a[11] * b01) / det,
(a[4] * b10 - a[5] * b08 + a[7] * b06) / det,
(a[1] * b08 - a[0] * b10 - a[3] * b06) / det,
(a[12] * b04 - a[13] * b02 + a[15] * b00) / det,
(a[9] * b02 - a[8] * b04 - a[11] * b00) / det,
(a[5] * b07 - a[4] * b09 - a[6] * b06) / det,
(a[0] * b09 - a[1] * b07 + a[2] * b06) / det,
(a[13] * b01 - a[12] * b03 - a[14] * b00) / det,
(a[8] * b03 - a[9] * b01 + a[10] * b00) / det,
];
}
function rotate4(a, rad, x, y, z) {
let len = Math.hypot(x, y, z);
x /= len;
y /= len;
z /= len;
let s = Math.sin(rad);
let c = Math.cos(rad);
let t = 1 - c;
let b00 = x * x * t + c;
let b01 = y * x * t + z * s;
let b02 = z * x * t - y * s;
let b10 = x * y * t - z * s;
let b11 = y * y * t + c;
let b12 = z * y * t + x * s;
let b20 = x * z * t + y * s;
let b21 = y * z * t - x * s;
let b22 = z * z * t + c;
return [
a[0] * b00 + a[4] * b01 + a[8] * b02,
a[1] * b00 + a[5] * b01 + a[9] * b02,
a[2] * b00 + a[6] * b01 + a[10] * b02,
a[3] * b00 + a[7] * b01 + a[11] * b02,
a[0] * b10 + a[4] * b11 + a[8] * b12,
a[1] * b10 + a[5] * b11 + a[9] * b12,
a[2] * b10 + a[6] * b11 + a[10] * b12,
a[3] * b10 + a[7] * b11 + a[11] * b12,
a[0] * b20 + a[4] * b21 + a[8] * b22,
a[1] * b20 + a[5] * b21 + a[9] * b22,
a[2] * b20 + a[6] * b21 + a[10] * b22,
a[3] * b20 + a[7] * b21 + a[11] * b22,
...a.slice(12, 16),
];
}
function translate4(a, x, y, z) {
return [
...a.slice(0, 12),
a[0] * x + a[4] * y + a[8] * z + a[12],
a[1] * x + a[5] * y + a[9] * z + a[13],
a[2] * x + a[6] * y + a[10] * z + a[14],
a[3] * x + a[7] * y + a[11] * z + a[15],
];
}
function createWorker(self) {
let buffer;
let vertexCount = 0;
let viewProj;
// 6*4 + 4 + 4 = 8*4
// XYZ - Position (Float32)
// XYZ - Scale (Float32)
// RGBA - colors (uint8)
// IJKL - quaternion/rot (uint8)
const rowLength = 3 * 4 + 3 * 4 + 4 + 4;
const runSort = (viewProj) => {
if (!buffer) return;
// console.time("sort");
const f_buffer = new Float32Array(buffer);
const u_buffer = new Uint8Array(buffer);
const depthList = new Float32Array(vertexCount);
const depthIndex = new Uint32Array(vertexCount);
for (let j = 0; j < vertexCount; j++) {
// For some reason dividing by wumbo actually causes
// problems, so this is the unnormalized camera space homo
depthList[j] =
viewProj[2] * f_buffer[8 * j + 0] +
viewProj[6] * f_buffer[8 * j + 1] +
viewProj[10] * f_buffer[8 * j + 2];
depthIndex[j] = j;
}
depthIndex.sort((a, b) => depthList[a] - depthList[b]);
const quat = new Float32Array(4 * vertexCount);
const scale = new Float32Array(3 * vertexCount);
const center = new Float32Array(3 * vertexCount);
const color = new Float32Array(4 * vertexCount);
for (let j = 0; j < vertexCount; j++) {
const i = depthIndex[j];
quat[4 * j + 0] = (u_buffer[32 * i + 28 + 0] - 128) / 128;
quat[4 * j + 1] = (u_buffer[32 * i + 28 + 1] - 128) / 128;
quat[4 * j + 2] = (u_buffer[32 * i + 28 + 2] - 128) / 128;
quat[4 * j + 3] = (u_buffer[32 * i + 28 + 3] - 128) / 128;
center[3 * j + 0] = f_buffer[8 * i + 0];
center[3 * j + 1] = f_buffer[8 * i + 1];
center[3 * j + 2] = f_buffer[8 * i + 2];
color[4 * j + 0] = u_buffer[32 * i + 24 + 0] / 255;
color[4 * j + 1] = u_buffer[32 * i + 24 + 1] / 255;
color[4 * j + 2] = u_buffer[32 * i + 24 + 2] / 255;
color[4 * j + 3] = u_buffer[32 * i + 24 + 3] / 255;
scale[3 * j + 0] = f_buffer[8 * i + 3 + 0];
scale[3 * j + 1] = f_buffer[8 * i + 3 + 1];
scale[3 * j + 2] = f_buffer[8 * i + 3 + 2];
}
self.postMessage({ quat, center, color, scale }, [
quat.buffer,
center.buffer,
color.buffer,
scale.buffer,
]);
// console.timeEnd("sort");
};
function processPlyBuffer(inputBuffer) {
const ubuf = new Uint8Array(inputBuffer);
// 10KB ought to be enough for a header...
const header = new TextDecoder().decode(ubuf.slice(0, 1024 * 10));
const header_end = "end_header\n";
const header_end_index = header.indexOf(header_end);
if (header_end_index < 0)
throw new Error("Unable to read .ply file header");
const vertexCount = parseInt(/element vertex (\d+)\n/.exec(header)[1]);
console.log("Vertex Count", vertexCount);
let row_offset = 0,
offsets = {},
types = {};
const TYPE_MAP = {
double: "getFloat64",
int: "getInt32",
uint: "getUint32",
float: "getFloat32",
short: "getInt16",
ushort: "getUint16",
uchar: "getUint8",
};
for (let prop of header
.slice(0, header_end_index)
.split("\n")
.filter((k) => k.startsWith("property "))) {
const [p, type, name] = prop.split(" ");
const arrayType = TYPE_MAP[type] || "getInt8";
types[name] = arrayType;
offsets[name] = row_offset;
row_offset += parseInt(arrayType.replace(/[^\d]/g, "")) / 8;
}
console.log("Bytes per row", row_offset, types, offsets);
let dataView = new DataView(
inputBuffer,
header_end_index + header_end.length,
);
let row = 0;
const attrs = new Proxy(
{},
{
get(target, prop) {
if (!types[prop]) throw new Error(prop + " not found");
return dataView[types[prop]](
row * row_offset + offsets[prop],
true,
);
},
},
);
console.time("calculate importance");
let sizeList = new Float32Array(vertexCount);
let sizeIndex = new Uint32Array(vertexCount);
for (row = 0; row < vertexCount; row++) {
sizeIndex[row] = row;
if (!types["scale_0"]) continue;
const size =
Math.exp(attrs.scale_0) *
Math.exp(attrs.scale_1) *
Math.exp(attrs.scale_2);
const opacity = 1 / (1 + Math.exp(-attrs.opacity));
sizeList[row] = size * opacity;
}
console.timeEnd("calculate importance");
console.time("sort");
sizeIndex.sort((b, a) => sizeList[a] - sizeList[b]);
console.timeEnd("sort");
// 6*4 + 4 + 4 = 8*4
// XYZ - Position (Float32)
// XYZ - Scale (Float32)
// RGBA - colors (uint8)
// IJKL - quaternion/rot (uint8)
const rowLength = 3 * 4 + 3 * 4 + 4 + 4;
const buffer = new ArrayBuffer(rowLength * vertexCount);
console.time("build buffer");
for (let j = 0; j < vertexCount; j++) {
row = sizeIndex[j];
const position = new Float32Array(buffer, j * rowLength, 3);
const scales = new Float32Array(buffer, j * rowLength + 4 * 3, 3);
const rgba = new Uint8ClampedArray(
buffer,
j * rowLength + 4 * 3 + 4 * 3,
4,
);
const rot = new Uint8ClampedArray(
buffer,
j * rowLength + 4 * 3 + 4 * 3 + 4,
4,
);
if (types["scale_0"]) {
const qlen = Math.sqrt(
attrs.rot_0 ** 2 +
attrs.rot_1 ** 2 +
attrs.rot_2 ** 2 +
attrs.rot_3 ** 2,
);
rot[0] = (attrs.rot_0 / qlen) * 128 + 128;
rot[1] = (attrs.rot_1 / qlen) * 128 + 128;
rot[2] = (attrs.rot_2 / qlen) * 128 + 128;
rot[3] = (attrs.rot_3 / qlen) * 128 + 128;
scales[0] = Math.exp(attrs.scale_0);
scales[1] = Math.exp(attrs.scale_1);
scales[2] = Math.exp(attrs.scale_2);
} else {
scales[0] = 0.01;
scales[1] = 0.01;
scales[2] = 0.01;
rot[0] = 255;
rot[1] = 0;
rot[2] = 0;
rot[3] = 0;
}
position[0] = attrs.x;
position[1] = attrs.y;
position[2] = attrs.z;
if (types["f_dc_0"]) {
// rgba[0] = (1 / (1 + Math.exp(-attrs.f_dc_0))) * 255;
// rgba[1] = (1 / (1 + Math.exp(-attrs.f_dc_1))) * 255;
// rgba[2] = (1 / (1 + Math.exp(-attrs.f_dc_2))) * 255;
const SH_C0 = 0.28209479177387814;
rgba[0] = (0.5 + SH_C0 * attrs.f_dc_0) * 255;
rgba[1] = (0.5 + SH_C0 * attrs.f_dc_1) * 255;
rgba[2] = (0.5 + SH_C0 * attrs.f_dc_2) * 255;
} else {
rgba[0] = attrs.red;
rgba[1] = attrs.green;
rgba[2] = attrs.blue;
}
if (types["opacity"]) {
rgba[3] = (1 / (1 + Math.exp(-attrs.opacity))) * 255;
} else {
rgba[3] = 255;
}
}
console.timeEnd("build buffer");
return buffer;
}
const throttledSort = () => {
if (!sortRunning) {
sortRunning = true;
let lastView = viewProj;
runSort(lastView);
setTimeout(() => {
sortRunning = false;
if (lastView !== viewProj) {
throttledSort();
}
}, 0);
}
};
let sortRunning;
self.onmessage = (e) => {
if (e.data.ply) {
vertexCount = 0;
runSort(viewProj);
buffer = processPlyBuffer(e.data.ply);
vertexCount = Math.floor(buffer.byteLength / rowLength);
postMessage({ buffer: buffer });
} else if (e.data.buffer) {
buffer = e.data.buffer;
vertexCount = e.data.vertexCount;
} else if (e.data.vertexCount) {
vertexCount = e.data.vertexCount;
} else if (e.data.view) {
viewProj = e.data.view;
throttledSort();
}
};
}
const vertexShaderSource = `
precision mediump float;
attribute vec2 position;
attribute vec4 color;
attribute vec4 quat;
attribute vec3 scale;
attribute vec3 center;
uniform mat4 projection, view;
uniform vec2 focal;
varying vec4 vColor;
varying vec3 vConic;
varying vec2 vCenter;
varying vec2 vPosition;
uniform vec2 viewport;
mat3 transpose(mat3 m) {
return mat3(
m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2]
);
}
mat3 compute_cov3d(vec3 scale, vec4 rot) {
mat3 S = mat3(
scale.x, 0.0, 0.0,
0.0, scale.y, 0.0,
0.0, 0.0, scale.z
);
mat3 R = mat3(
1.0 - 2.0 * (rot.z * rot.z + rot.w * rot.w), 2.0 * (rot.y * rot.z - rot.x * rot.w), 2.0 * (rot.y * rot.w + rot.x * rot.z),
2.0 * (rot.y * rot.z + rot.x * rot.w), 1.0 - 2.0 * (rot.y * rot.y + rot.w * rot.w), 2.0 * (rot.z * rot.w - rot.x * rot.y),
2.0 * (rot.y * rot.w - rot.x * rot.z), 2.0 * (rot.z * rot.w + rot.x * rot.y), 1.0 - 2.0 * (rot.y * rot.y + rot.z * rot.z)
);
mat3 M = S * R;
return transpose(M) * M;
}
vec3 compute_cov2d(vec3 center, vec3 scale, vec4 rot){
mat3 Vrk = compute_cov3d(scale, rot);
vec4 t = view * vec4(center, 1.0);
vec2 lims = 1.3 * 0.5 * viewport / focal;
t.xy = min(lims, max(-lims, t.xy / t.z)) * t.z;
mat3 J = mat3(
focal.x / t.z, 0., -(focal.x * t.x) / (t.z * t.z),
0., focal.y / t.z, -(focal.y * t.y) / (t.z * t.z),
0., 0., 0.
);
mat3 W = transpose(mat3(view));
mat3 T = W * J;
mat3 cov = transpose(T) * transpose(Vrk) * T;
return vec3(cov[0][0] + 0.3, cov[0][1], cov[1][1] + 0.3);
}
void main () {
vec4 camspace = view * vec4(center, 1);
vec4 pos2d = projection * mat4(1,0,0,0,0,-1,0,0,0,0,1,0,0,0,0,1) * camspace;
vec3 cov2d = compute_cov2d(center, scale, quat);
float det = cov2d.x * cov2d.z - cov2d.y * cov2d.y;
vec3 conic = vec3(cov2d.z, cov2d.y, cov2d.x) / det;
float mid = 0.5 * (cov2d.x + cov2d.z);
float lambda1 = mid + sqrt(max(0.1, mid * mid - det));
float lambda2 = mid - sqrt(max(0.1, mid * mid - det));
vec2 v1 = 7.0 * sqrt(lambda1) * normalize(vec2(cov2d.y, lambda1 - cov2d.x));
vec2 v2 = 7.0 * sqrt(lambda2) * normalize(vec2(-(lambda1 - cov2d.x),cov2d.y));
vColor = color;
vConic = conic;
vCenter = vec2(pos2d) / pos2d.w;
vPosition = vec2(vCenter + position.x * (position.y < 0.0 ? v1 : v2) / viewport);
gl_Position = vec4(vPosition, pos2d.z / pos2d.w, 1);
}
`;
const fragmentShaderSource = `
precision mediump float;
varying vec4 vColor;
varying vec3 vConic;
varying vec2 vCenter;
uniform vec2 viewport;
uniform vec2 focal;
void main () {
vec2 d = (vCenter - 2.0 * (gl_FragCoord.xy/viewport - vec2(0.5, 0.5))) * viewport * 0.5;
float power = -0.5 * (vConic.x * d.x * d.x + vConic.z * d.y * d.y) - vConic.y * d.x * d.y;
if (power > 0.0) discard;
float alpha = min(0.99, vColor.a * exp(power));
if(alpha < 0.02) discard;
gl_FragColor = vec4(alpha * vColor.rgb, alpha);
}
`;
// let viewMatrix = getViewMatrix(camera);
let defaultViewMatrix = [
0.47, 0.04, 0.88, 0, -0.11, 0.99, 0.02, 0, -0.88, -0.11, 0.47, 0, 0.07,
0.03, 6.55, 1,
];
let viewMatrix = defaultViewMatrix;
async function main() {
let carousel = true;
const params = new URLSearchParams(location.search);
try {
viewMatrix = JSON.parse(location.hash.slice(1));
carousel = false;
} catch (err) {}
const url = new URL(
params.get("url") || "train.splat",
"https://antimatter15.com/splat-data/",
);
const req = await fetch(url, {
mode: "cors", // no-cors, *cors, same-origin
credentials: "omit", // include, *same-origin, omit
});
console.log(req);
if (req.status != 200)
throw new Error("Unable to load " + req.url + ": " + req.statusText);
const rowLength = 3 * 4 + 3 * 4 + 4 + 4;
const reader = req.body.getReader();
let splatData = new Uint8Array(req.headers.get("content-length"));
const worker = new Worker(
URL.createObjectURL(
new Blob(["(", createWorker.toString(), ")(self)"], {
type: "application/javascript",
}),
),
);
const canvas = document.createElement("canvas");
canvas.width = innerWidth / 2;
canvas.height = innerHeight / 2;
canvas.style.position = "absolute";
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.width = "100%";
canvas.style.height = "100%";
document.body.appendChild(canvas);
const fps = document.createElement("div");
fps.style.position = "absolute";
fps.style.bottom = "10px";
fps.style.right = "10px";
document.body.appendChild(fps);
let projectionMatrix = getProjectionMatrix(
camera.fx / 2,
camera.fy / 2,
canvas.width,
canvas.height,
);
const gl = canvas.getContext("webgl");
const ext = gl.getExtension("ANGLE_instanced_arrays");
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS))
console.error(gl.getShaderInfoLog(vertexShader));
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS))
console.error(gl.getShaderInfoLog(fragmentShader));
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
console.error(gl.getProgramInfoLog(program));
gl.disable(gl.DEPTH_TEST); // Disable depth testing
// Enable blending
gl.enable(gl.BLEND);
// Set blending function
gl.blendFuncSeparate(
gl.ONE_MINUS_DST_ALPHA,
gl.ONE,
gl.ONE_MINUS_DST_ALPHA,
gl.ONE,
);
// Set blending equation
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
// projection
const u_projection = gl.getUniformLocation(program, "projection");
gl.uniformMatrix4fv(u_projection, false, projectionMatrix);
// viewport
const u_viewport = gl.getUniformLocation(program, "viewport");
gl.uniform2fv(u_viewport, new Float32Array([canvas.width, canvas.height]));
// focal
const u_focal = gl.getUniformLocation(program, "focal");
gl.uniform2fv(u_focal, new Float32Array([camera.fx / 2, camera.fy / 2]));
// view
const u_view = gl.getUniformLocation(program, "view");
gl.uniformMatrix4fv(u_view, false, viewMatrix);
// positions
const triangleVertices = new Float32Array([1, -1, 1, 1, -1, 1, -1, -1]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW);
const a_position = gl.getAttribLocation(program, "position");
gl.enableVertexAttribArray(a_position);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0);
// center
const centerBuffer = gl.createBuffer();
// gl.bindBuffer(gl.ARRAY_BUFFER, centerBuffer);
// gl.bufferData(gl.ARRAY_BUFFER, center, gl.STATIC_DRAW);
const a_center = gl.getAttribLocation(program, "center");
gl.enableVertexAttribArray(a_center);
gl.bindBuffer(gl.ARRAY_BUFFER, centerBuffer);
gl.vertexAttribPointer(a_center, 3, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(a_center, 1); // Use the extension here
// color
const colorBuffer = gl.createBuffer();
// gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
// gl.bufferData(gl.ARRAY_BUFFER, color, gl.STATIC_DRAW);
const a_color = gl.getAttribLocation(program, "color");
gl.enableVertexAttribArray(a_color);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(a_color, 4, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(a_color, 1); // Use the extension here
// quat
const quatBuffer = gl.createBuffer();
// gl.bindBuffer(gl.ARRAY_BUFFER, quatBuffer);
// gl.bufferData(gl.ARRAY_BUFFER, quat, gl.STATIC_DRAW);
const a_quat = gl.getAttribLocation(program, "quat");
gl.enableVertexAttribArray(a_quat);
gl.bindBuffer(gl.ARRAY_BUFFER, quatBuffer);
gl.vertexAttribPointer(a_quat, 4, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(a_quat, 1); // Use the extension here
// scale
const scaleBuffer = gl.createBuffer();
// gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer);
// gl.bufferData(gl.ARRAY_BUFFER, scale, gl.STATIC_DRAW);
const a_scale = gl.getAttribLocation(program, "scale");
gl.enableVertexAttribArray(a_scale);
gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer);
gl.vertexAttribPointer(a_scale, 3, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(a_scale, 1); // Use the extension here
worker.onmessage = (e) => {
if (e.data.buffer) {
splatData = new Uint8Array(e.data.buffer);
const blob = new Blob([splatData.buffer], {
type: "application/octet-stream",
});
const link = document.createElement("a");
link.download = "model.splat";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
} else {
let { quat, scale, center, color } = e.data;
vertexCount = quat.length / 4;
gl.bindBuffer(gl.ARRAY_BUFFER, centerBuffer);
gl.bufferData(gl.ARRAY_BUFFER, center, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, color, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, quatBuffer);
gl.bufferData(gl.ARRAY_BUFFER, quat, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer);
gl.bufferData(gl.ARRAY_BUFFER, scale, gl.STATIC_DRAW);
}
};
let activeKeys = [];
window.addEventListener("keydown", (e) => {
carousel = false;
if (!activeKeys.includes(e.key)) activeKeys.push(e.key);
if (/\d/.test(e.key)) {
viewMatrix = getViewMatrix(cameras[parseInt(e.key)]);
}
if (e.key == "v") {
location.hash =
"#" +
JSON.stringify(
viewMatrix.map((k) => Math.round(k * 100) / 100),
);
} else if (e.key === "p") {
carousel = true;
}
});
window.addEventListener("keyup", (e) => {
activeKeys = activeKeys.filter((k) => k !== e.key);
});
window.addEventListener(
"wheel",
(e) => {
carousel = false;
e.preventDefault();
const lineHeight = 10;
const scale =
e.deltaMode == 1
? lineHeight
: e.deltaMode == 2
? innerHeight
: 1;
const dy = e.deltaY * scale;
let inv = invert4(viewMatrix);
inv = translate4(inv, 0, 0, (-5 * dy) / innerHeight);
viewMatrix = invert4(inv);
},
{ passive: false },
);
let startX, startY, down;
document.addEventListener("mousedown", (e) => {
carousel = false;
e.preventDefault();
startX = e.clientX;
startY = e.clientY;
down = 1;
});
document.addEventListener("contextmenu", (e) => {
carousel = false;
e.preventDefault();
startX = e.clientX;
startY = e.clientY;
down = 2;
});
document.addEventListener("mousemove", (e) => {
e.preventDefault();
if (down == 1) {
let inv = invert4(viewMatrix);
inv = rotate4(
inv,
(-2 * (e.clientX - startX)) / innerWidth,
0,
1,
0,
);
inv = translate4(
inv,
(5 * (e.clientX - startX)) / innerWidth,
0,
0,
);
inv = translate4(
inv,
0,
0,
(10 * (e.clientY - startY)) / innerHeight,
);
viewMatrix = invert4(inv);
startX = e.clientX;
startY = e.clientY;
} else if (down == 2) {
let inv = invert4(viewMatrix);
// inv = rotateY(inv, );
inv = translate4(
inv,
(-10 * (e.clientX - startX)) / innerWidth,
(-10 * (e.clientY - startY)) / innerHeight,
0,
);
viewMatrix = invert4(inv);
startX = e.clientX;
startY = e.clientY;
}
});
document.addEventListener("mouseup", (e) => {
e.preventDefault();
down = false;
startX = 0;
startY = 0;
});
document.addEventListener(
"touchstart",
(e) => {
e.preventDefault();
if (e.touches.length === 1) {
carousel = false;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
down = 1;
}
},
{ passive: false },
);
document.addEventListener(
"touchmove",
(e) => {
e.preventDefault();
if (e.touches.length === 1 && down) {
let inv = invert4(viewMatrix);
inv = rotate4(
inv,
(-0.6 * (e.touches[0].clientX - startX)) / innerWidth,
0,
1,
0,
);
inv = translate4(
inv,
0,
0,
(25 * (e.touches[0].clientY - startY)) / innerHeight,
);
viewMatrix = invert4(inv);
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}
},
{ passive: false },
);
document.addEventListener(
"touchend",
(e) => {
e.preventDefault();
down = false;
startX = 0;
startY = 0;
},
{ passive: false },
);
let jumpDelta = 0;
let vertexCount = 0;
let lastFrame = 0;
let avgFps = 0;
let start = Date.now() + 2000;
const frame = (now) => {
let inv = invert4(viewMatrix);
// let preY = inv[13];
if (activeKeys.includes("ArrowUp")) inv = translate4(inv, 0, 0, 0.1);
if (activeKeys.includes("ArrowDown")) inv = translate4(inv, 0, 0, -0.1);
if (activeKeys.includes("ArrowLeft"))
inv = rotate4(inv, -0.01, 0, 1, 0);
if (activeKeys.includes("ArrowRight"))
inv = rotate4(inv, 0.01, 0, 1, 0);
if (activeKeys.includes("a")) inv = translate4(inv, -0.02, 0, 0);
if (activeKeys.includes("d")) inv = translate4(inv, 0.02, 0, 0);
if (activeKeys.includes("q")) inv = rotate4(inv, 0.01, 0, 0, 1);
if (activeKeys.includes("e")) inv = rotate4(inv, -0.01, 0, 0, 1);
if (activeKeys.includes("w")) inv = rotate4(inv, 0.005, 1, 0, 0);
if (activeKeys.includes("s")) inv = rotate4(inv, -0.005, 1, 0, 0);
// inv[13] = preY;
viewMatrix = invert4(inv);
if (carousel) {
let inv = invert4(defaultViewMatrix);
const t = Math.sin((Date.now() - start) / 5000);
inv = translate4(inv, 2.5 * t, 0, 10 * (1 - Math.cos(t)));
inv = rotate4(inv, -0.4 * t, 0, 1, 0);
viewMatrix = invert4(inv);
}
if (activeKeys.includes(" ")) {
jumpDelta = Math.min(1, jumpDelta + 0.05);
} else {
jumpDelta = Math.max(0, jumpDelta - 0.05);
}
let inv2 = invert4(viewMatrix);
inv2[13] -= jumpDelta;
inv2 = rotate4(inv2, -0.2 * jumpDelta, 1, 0, 0);
let actualViewMatrix = invert4(inv2);
const viewProj = multiply4(projectionMatrix, actualViewMatrix);
worker.postMessage({ view: viewProj });
const currentFps = 1000 / (now - lastFrame) || 0;
avgFps = avgFps * 0.9 + currentFps * 0.1;
if (vertexCount > 0) {
document.getElementById("spinner").style.display = "none";
gl.uniformMatrix4fv(u_view, false, actualViewMatrix);
// gl.clearColor(0.0, 0.0, 0.0, 1.0); // Set the clear color to black with full opacity
// gl.clear(gl.COLOR_BUFFER_BIT);
// gl.clearDepth(100000.0);
// gl.clear(gl.DEPTH_BUFFER_BIT);
ext.drawArraysInstancedANGLE(gl.TRIANGLE_STRIP, 0, 4, vertexCount);
} else {
gl.clear(gl.COLOR_BUFFER_BIT);
document.getElementById("spinner").style.display = "";
}
const progress = (100 * vertexCount) / (splatData.length / rowLength);
if (progress < 100) {
document.getElementById("progress").style.width = progress + "%";
} else {
document.getElementById("progress").style.display = "none";
}
fps.innerText =
Math.round(avgFps) +
" fps";
lastFrame = now;
requestAnimationFrame(frame);
};
frame();
const selectFile = (file) => {
const fr = new FileReader();
if (/\.json$/i.test(file.name)) {
fr.onload = () => {
cameras = JSON.parse(fr.result);
viewMatrix = getViewMatrix(cameras[0]);
projectionMatrix = getProjectionMatrix(
camera.fx / 2,
camera.fy / 2,
canvas.width,
canvas.height,
);
gl.uniformMatrix4fv(u_projection, false, projectionMatrix);
console.log("Loaded Cameras");
};
fr.readAsText(file);
} else {
stopLoading = true;
fr.onload = () => {
splatData = new Uint8Array(fr.result);
console.log("Loaded", Math.floor(splatData.length / rowLength));
if (
splatData[0] == 112 &&
splatData[1] == 108 &&
splatData[2] == 121 &&
splatData[3] == 10
) {
// ply file magic header means it should be handled differently
worker.postMessage({ ply: splatData.buffer });
} else {
worker.postMessage({
buffer: splatData.buffer,
vertexCount: Math.floor(splatData.length / rowLength),
});
}
};
fr.readAsArrayBuffer(file);
}
};
window.addEventListener("hashchange", (e) => {
try {
viewMatrix = JSON.parse(location.hash.slice(1));
carousel = false;
} catch (err) {}
});
document.addEventListener("dragenter", (e) => {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener("dragover", (e) => {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener("dragleave", (e) => {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener("drop", (e) => {
e.preventDefault();
e.stopPropagation();
selectFile(e.dataTransfer.files[0]);
});
let bytesRead = 0;
let lastVertexCount = -1;
let stopLoading = false;
while (true) {
const { done, value } = await reader.read();
if (done || stopLoading) break;
splatData.set(value, bytesRead);
bytesRead += value.length;
if (vertexCount > lastVertexCount) {
worker.postMessage({
buffer: splatData.buffer,
vertexCount: Math.floor(bytesRead / rowLength),
});
lastVertexCount = vertexCount;
}
}
if (!stopLoading)
worker.postMessage({
buffer: splatData.buffer,
vertexCount: Math.floor(bytesRead / rowLength),
});
}
main().catch((err) => {
document.getElementById("spinner").style.display = "none";
document.getElementById("message").innerText = err.toString();
});