meshoptimizer
Version:
Mesh optimization library that makes meshes smaller and faster to render
461 lines (378 loc) • 14.9 kB
JavaScript
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2025, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
// This is the reference decoder implementation by Jasper St. Pierre.
// It follows the decoder interface and should be a drop-in replacement for the actual decoder from meshopt_decoder.js
// It is provided for educational value and is not recommended for use in production because it's not performance-optimized.
const MeshoptDecoder = {};
MeshoptDecoder.supported = true;
MeshoptDecoder.ready = Promise.resolve();
function assert(cond) {
if (!cond) {
throw new Error('Assertion failed');
}
}
function dezig(v) {
return (v & 1) !== 0 ? ~(v >>> 1) : v >>> 1;
}
MeshoptDecoder.decodeVertexBuffer = (target, elementCount, byteStride, source, filter) => {
assert(source[0] === 0xa0 || source[0] === 0xa1);
const version = source[0] & 0x0f;
const maxBlockElements = Math.min((0x2000 / byteStride) & ~0x000f, 0x100);
const deltas = new Uint8Array(maxBlockElements * byteStride);
const tailSize = version === 0 ? byteStride : byteStride + byteStride / 4;
const tailDataOffs = source.length - tailSize;
// What deltas are stored relative to
const tempData = source.slice(tailDataOffs, tailDataOffs + byteStride);
// Channel modes for v1
const channels = version === 0 ? null : source.slice(tailDataOffs + byteStride, tailDataOffs + tailSize);
let srcOffs = 1; // Skip header byte
const headerModes = [
[0, 2, 4, 8], // v0
[0, 1, 2, 4], // v1, when control is 0
[1, 2, 4, 8], // v1, when control is 1
];
// Attribute blocks
for (let dstElemBase = 0; dstElemBase < elementCount; dstElemBase += maxBlockElements) {
const attrBlockElementCount = Math.min(elementCount - dstElemBase, maxBlockElements);
const groupCount = ((attrBlockElementCount + 0x0f) & ~0x0f) >>> 4;
const headerByteCount = ((groupCount + 0x03) & ~0x03) >>> 2;
// Control modes for v1
const controlBitsOffs = srcOffs;
srcOffs += version === 0 ? 0 : byteStride / 4;
// Zero out deltas to simplify logic
deltas.fill(0x00);
// Data blocks
for (let byte = 0; byte < byteStride; byte++) {
const deltaBase = byte * attrBlockElementCount;
// Control mode for current byte for v1
const controlMode = version === 0 ? 0 : (source[controlBitsOffs + (byte >>> 2)] >>> ((byte & 0x03) << 1)) & 0x03;
if (controlMode === 2) {
// All byte deltas are 0; no data is stored for this byte
continue;
} else if (controlMode === 3) {
// Byte deltas are stored uncompressed with no header bits
deltas.set(source.subarray(srcOffs, srcOffs + attrBlockElementCount), deltaBase);
srcOffs += attrBlockElementCount;
continue;
}
// Header bits are omitted for v1 when using control modes 2/3
const headerBitsOffs = srcOffs;
srcOffs += headerByteCount;
for (let group = 0; group < groupCount; group++) {
const mode = (source[headerBitsOffs + (group >>> 2)] >>> ((group & 0x03) << 1)) & 0x03;
const modeBits = headerModes[version === 0 ? 0 : controlMode + 1][mode];
const deltaOffs = deltaBase + (group << 4);
if (modeBits === 0) {
// All 16 byte deltas are 0; the size of the encoded block is 0 bytes
} else if (modeBits === 1) {
// Deltas are using 1-bit sentinel encoding; the size of the encoded block is [2..18] bytes
const srcBase = srcOffs;
srcOffs += 0x02;
for (let m = 0; m < 0x10; m++) {
// Bits are stored from least significant to most significant for 1-bit encoding
const shift = m & 0x07;
let delta = (source[srcBase + (m >>> 3)] >>> shift) & 0x01;
if (delta === 1) delta = source[srcOffs++];
deltas[deltaOffs + m] = delta;
}
} else if (modeBits === 2) {
// Deltas are using 2-bit sentinel encoding; the size of the encoded block is [4..20] bytes
const srcBase = srcOffs;
srcOffs += 0x04;
for (let m = 0; m < 0x10; m++) {
// 0 = >>> 6, 1 = >>> 4, 2 = >>> 2, 3 = >>> 0
const shift = 6 - ((m & 0x03) << 1);
let delta = (source[srcBase + (m >>> 2)] >>> shift) & 0x03;
if (delta === 3) delta = source[srcOffs++];
deltas[deltaOffs + m] = delta;
}
} else if (modeBits === 4) {
// Deltas are using 4-bit sentinel encoding; the size of the encoded block is [8..24] bytes
const srcBase = srcOffs;
srcOffs += 0x08;
for (let m = 0; m < 0x10; m++) {
// 0 = >>> 6, 1 = >>> 4, 2 = >>> 2, 3 = >>> 0
const shift = 4 - ((m & 0x01) << 2);
let delta = (source[srcBase + (m >>> 1)] >>> shift) & 0x0f;
if (delta === 0xf) delta = source[srcOffs++];
deltas[deltaOffs + m] = delta;
}
} else {
// All 16 byte deltas are stored verbatim; the size of the encoded block is 16 bytes
deltas.set(source.subarray(srcOffs, srcOffs + 0x10), deltaOffs);
srcOffs += 0x10;
}
}
}
// Go through and apply deltas to data
for (let elem = 0; elem < attrBlockElementCount; elem++) {
const dstElem = dstElemBase + elem;
for (let byteGroup = 0; byteGroup < byteStride; byteGroup += 4) {
let channelMode = version === 0 ? 0 : channels[byteGroup >>> 2] & 0x03;
assert(channelMode !== 0x03);
if (channelMode === 0) {
// Channel 0 (byte deltas): Byte deltas are stored as zigzag-encoded differences between the byte values of the element and the byte values of the previous element in the same position.
for (let byte = byteGroup; byte < byteGroup + 4; byte++) {
const delta = dezig(deltas[byte * attrBlockElementCount + elem]);
const temp = (tempData[byte] + delta) & 0xff; // wrap around
const dstOffs = dstElem * byteStride + byte;
target[dstOffs] = tempData[byte] = temp;
}
} else if (channelMode === 1) {
// Channel 1 (2-byte deltas): 2-byte deltas are computed as zigzag-encoded differences between 16-bit values of the element and the previous element in the same position.
for (let byte = byteGroup; byte < byteGroup + 4; byte += 2) {
const delta = dezig(deltas[byte * attrBlockElementCount + elem] + (deltas[(byte + 1) * attrBlockElementCount + elem] << 8));
let temp = tempData[byte] + (tempData[byte + 1] << 8);
temp = (temp + delta) & 0xffff; // wrap around
const dstOffs = dstElem * byteStride + byte;
target[dstOffs] = tempData[byte] = temp & 0xff;
target[dstOffs + 1] = tempData[byte + 1] = temp >>> 8;
}
} else if (channelMode === 2) {
// Channel 2 (4-byte XOR deltas): 4-byte deltas are computed as XOR between 32-bit values of the element and the previous element in the same position, with an additional rotation applied based on the high 4 bits of the channel mode byte.
const byte = byteGroup;
const delta =
deltas[byte * attrBlockElementCount + elem] +
(deltas[(byte + 1) * attrBlockElementCount + elem] << 8) +
(deltas[(byte + 2) * attrBlockElementCount + elem] << 16) +
(deltas[(byte + 3) * attrBlockElementCount + elem] << 24);
let temp = tempData[byte] + (tempData[byte + 1] << 8) + (tempData[byte + 2] << 16) + (tempData[byte + 3] << 24);
const rot = channels[byteGroup >>> 2] >>> 4;
temp = temp ^ ((delta >>> rot) | (delta << (32 - rot))); // rotate and XOR
const dstOffs = dstElem * byteStride + byte;
target[dstOffs] = tempData[byte] = temp & 0xff;
target[dstOffs + 1] = tempData[byte + 1] = (temp >>> 8) & 0xff;
target[dstOffs + 2] = tempData[byte + 2] = (temp >>> 16) & 0xff;
target[dstOffs + 3] = tempData[byte + 3] = temp >>> 24;
}
}
}
}
const tailSizePadded = Math.max(tailSize, version === 0 ? 32 : 24);
assert(srcOffs == source.length - tailSizePadded);
// Filters - only applied if filter isn't undefined or NONE
if (filter === 'OCTAHEDRAL') {
assert(byteStride === 4 || byteStride === 8);
const dst = byteStride === 4 ? new Int8Array(target.buffer) : new Int16Array(target.buffer);
const maxInt = byteStride === 4 ? 127 : 32767;
for (let i = 0; i < 4 * elementCount; i += 4) {
let x = dst[i + 0],
y = dst[i + 1],
one = dst[i + 2];
x /= one;
y /= one;
const z = 1.0 - Math.abs(x) - Math.abs(y);
const t = Math.max(-z, 0.0);
x -= x >= 0 ? t : -t;
y -= y >= 0 ? t : -t;
const h = maxInt / Math.hypot(x, y, z);
dst[i + 0] = Math.round(x * h);
dst[i + 1] = Math.round(y * h);
dst[i + 2] = Math.round(z * h);
// keep dst[i + 3] as is
}
} else if (filter === 'QUATERNION') {
assert(byteStride === 8);
const dst = new Int16Array(target.buffer);
for (let i = 0; i < 4 * elementCount; i += 4) {
const inputW = dst[i + 3];
const maxComponent = inputW & 0x03;
const s = Math.SQRT1_2 / (inputW | 0x03);
let x = dst[i + 0] * s;
let y = dst[i + 1] * s;
let z = dst[i + 2] * s;
let w = Math.sqrt(Math.max(0.0, 1.0 - x ** 2 - y ** 2 - z ** 2));
dst[i + ((maxComponent + 1) % 4)] = Math.round(x * 32767);
dst[i + ((maxComponent + 2) % 4)] = Math.round(y * 32767);
dst[i + ((maxComponent + 3) % 4)] = Math.round(z * 32767);
dst[i + ((maxComponent + 0) % 4)] = Math.round(w * 32767);
}
} else if (filter === 'EXPONENTIAL') {
assert((byteStride & 0x03) === 0x00);
const src = new Int32Array(target.buffer);
const dst = new Float32Array(target.buffer);
for (let i = 0; i < (byteStride * elementCount) / 4; i++) {
const v = src[i],
exp = v >> 24,
mantissa = (v << 8) >> 8;
dst[i] = 2.0 ** exp * mantissa;
}
} else if (filter === 'COLOR') {
assert(byteStride === 4 || byteStride === 8);
const maxInt = (1 << (byteStride * 2)) - 1;
const data = byteStride === 4 ? new Uint8Array(target.buffer) : new Uint16Array(target.buffer, 0, elementCount * 4);
const dataSigned = byteStride === 4 ? new Int8Array(target.buffer) : new Int16Array(target.buffer, 0, elementCount * 4);
for (let i = 0; i < elementCount * 4; i += 4) {
const y = data[i + 0];
const co = dataSigned[i + 1];
const cg = dataSigned[i + 2];
const alphaInput = data[i + 3];
// Recover scale from alpha high bit - find highest bit set
const alphaBit = 31 - Math.clz32(alphaInput);
const as = (1 << (alphaBit + 1)) - 1;
// YCoCg to RGB conversion
const r = y + co - cg;
const g = y + cg;
const b = y - co - cg;
// Expand alpha by one bit, replicating last bit
let a = alphaInput & (as >> 1);
a = (a << 1) | (a & 1);
// Scale to full range
const ss = maxInt / as;
// Store result
data[i + 0] = Math.round(r * ss);
data[i + 1] = Math.round(g * ss);
data[i + 2] = Math.round(b * ss);
data[i + 3] = Math.round(a * ss);
}
}
};
function pushfifo(fifo, n) {
for (let i = fifo.length - 1; i > 0; i--) fifo[i] = fifo[i - 1];
fifo[0] = n;
}
MeshoptDecoder.decodeIndexBuffer = (target, count, byteStride, source) => {
assert(source[0] === 0xe1);
assert(count % 3 === 0);
assert(byteStride === 2 || byteStride === 4);
let dst;
if (byteStride === 2) dst = new Uint16Array(target.buffer);
else dst = new Uint32Array(target.buffer);
const triCount = count / 3;
let codeOffs = 0x01;
let dataOffs = codeOffs + triCount;
let codeauxOffs = source.length - 0x10;
function readLEB128() {
let n = 0;
for (let i = 0; ; i += 7) {
const b = source[dataOffs++];
n |= (b & 0x7f) << i;
if (b < 0x80) return n;
}
}
let next = 0,
last = 0;
const edgefifo = new Uint32Array(32);
const vertexfifo = new Uint32Array(16);
function decodeIndex(v) {
return (last += dezig(v));
}
let dstOffs = 0;
for (let i = 0; i < triCount; i++) {
const code = source[codeOffs++];
const b0 = code >>> 4,
b1 = code & 0x0f;
if (b0 < 0x0f) {
const a = edgefifo[(b0 << 1) + 0],
b = edgefifo[(b0 << 1) + 1];
let c = -1;
if (b1 === 0x00) {
c = next++;
pushfifo(vertexfifo, c);
} else if (b1 < 0x0d) {
c = vertexfifo[b1];
} else if (b1 === 0x0d) {
c = --last;
pushfifo(vertexfifo, c);
} else if (b1 === 0x0e) {
c = ++last;
pushfifo(vertexfifo, c);
} else if (b1 === 0x0f) {
const v = readLEB128();
c = decodeIndex(v);
pushfifo(vertexfifo, c);
}
// fifo pushes happen backwards
pushfifo(edgefifo, b);
pushfifo(edgefifo, c);
pushfifo(edgefifo, c);
pushfifo(edgefifo, a);
dst[dstOffs++] = a;
dst[dstOffs++] = b;
dst[dstOffs++] = c;
} else {
// b0 === 0x0F
let a = -1,
b = -1,
c = -1;
if (b1 < 0x0e) {
const e = source[codeauxOffs + b1];
const z = e >>> 4,
w = e & 0x0f;
a = next++;
if (z === 0x00) b = next++;
else b = vertexfifo[z - 1];
if (w === 0x00) c = next++;
else c = vertexfifo[w - 1];
pushfifo(vertexfifo, a);
if (z === 0x00) pushfifo(vertexfifo, b);
if (w === 0x00) pushfifo(vertexfifo, c);
} else {
const e = source[dataOffs++];
if (e === 0x00) next = 0;
const z = e >>> 4,
w = e & 0x0f;
if (b1 === 0x0e) a = next++;
else a = decodeIndex(readLEB128());
if (z === 0x00) b = next++;
else if (z === 0x0f) b = decodeIndex(readLEB128());
else b = vertexfifo[z - 1];
if (w === 0x00) c = next++;
else if (w === 0x0f) c = decodeIndex(readLEB128());
else c = vertexfifo[w - 1];
pushfifo(vertexfifo, a);
if (z === 0x00 || z === 0x0f) pushfifo(vertexfifo, b);
if (w === 0x00 || w === 0x0f) pushfifo(vertexfifo, c);
}
pushfifo(edgefifo, a);
pushfifo(edgefifo, b);
pushfifo(edgefifo, b);
pushfifo(edgefifo, c);
pushfifo(edgefifo, c);
pushfifo(edgefifo, a);
dst[dstOffs++] = a;
dst[dstOffs++] = b;
dst[dstOffs++] = c;
}
}
};
MeshoptDecoder.decodeIndexSequence = (target, count, byteStride, source) => {
assert(source[0] === 0xd1);
assert(byteStride === 2 || byteStride === 4);
let dst;
if (byteStride === 2) dst = new Uint16Array(target.buffer);
else dst = new Uint32Array(target.buffer);
let dataOffs = 0x01;
function readLEB128() {
let n = 0;
for (let i = 0; ; i += 7) {
const b = source[dataOffs++];
n |= (b & 0x7f) << i;
if (b < 0x80) return n;
}
}
const last = new Uint32Array(2);
for (let i = 0; i < count; i++) {
const v = readLEB128();
const b = v & 0x01;
const delta = dezig(v >>> 1);
dst[i] = last[b] += delta;
}
};
MeshoptDecoder.decodeGltfBuffer = (target, count, size, source, mode, filter) => {
const table = {
ATTRIBUTES: MeshoptDecoder.decodeVertexBuffer,
TRIANGLES: MeshoptDecoder.decodeIndexBuffer,
INDICES: MeshoptDecoder.decodeIndexSequence,
};
assert(table[mode] !== undefined);
table[mode](target, count, size, source, filter);
};
MeshoptDecoder.decodeGltfBufferAsync = (count, size, source, mode, filter) => {
const target = new Uint8Array(count * size);
MeshoptDecoder.decodeGltfBuffer(target, count, size, source, mode, filter);
return Promise.resolve(target);
};
// node.js interface:
// for (let k in MeshoptDecoder) exports[k] = MeshoptDecoder[k];
export { MeshoptDecoder };