encoding.ts
src/core/encoding.ts · raw · all examples · site home
Exports: Encoding
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
import type { Quat, Rgb, Vec3 } from "./math";
const floatViews = new WeakMap<Uint32Array, Float32Array<ArrayBuffer>>();
const f32 = new Float32Array(1);
const u32 = new Uint32Array(f32.buffer);
const Float16 = (globalThis as { Float16Array?: new (length: number) => { [index: number]: number; buffer: ArrayBuffer } }).Float16Array;
const f16 = Float16 ? new Float16(1) : undefined;
const u16 = f16 ? new Uint16Array(f16.buffer) : undefined;
interface OctCodec {
encode(v: Vec3): number;
decode(code: number, out: Vec3): Vec3;
}
interface QuatCodec {
encode(q: Quat): number;
decode(code: number, out: Quat): Quat;
}
// Octahedral direction, `bits` per axis: u | v << bits. The direction is
// folded onto the octahedron and each coordinate rounded. Code 0 means "no
// vector": a direction rounding to 0 (-Z) gets the all-ones code instead,
// which decodes to the same direction.
function makeOctCodec(bits: number): OctCodec {
if (bits < 1 || bits > 16) {
throw new Error(`Octahedral codes take 1 to 16 bits per axis, not ${bits}`);
}
const max = 2 ** bits - 1;
const allOnes = (max | (max << bits)) >>> 0;
return {
encode(v) {
const l1 = Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
if (!(l1 > 0)) {
return 0;
}
let px = v.x / l1;
let py = v.y / l1;
if (v.z < 0) {
const ax = Math.abs(px);
px = (1 - Math.abs(py)) * (px >= 0 ? 1 : -1);
py = (1 - ax) * (py >= 0 ? 1 : -1);
}
const code = (Math.round((px * 0.5 + 0.5) * max) | (Math.round((py * 0.5 + 0.5) * max) << bits)) >>> 0;
return code === 0 ? allOnes : code;
},
decode(code, out) {
if (code === 0) {
out.x = 0;
out.y = 0;
out.z = 0;
return out;
}
let x = ((code & max) / max) * 2 - 1;
let y = (((code >>> bits) & max) / max) * 2 - 1;
const z = 1 - Math.abs(x) - Math.abs(y);
const t = Math.max(-z, 0);
x += x >= 0 ? -t : t;
y += y >= 0 ? -t : t;
const length = Math.sqrt(x * x + y * y + z * z);
out.x = x / length;
out.y = y / length;
out.z = z / length;
return out;
},
};
}
// Axis (octahedral, 2 * axisBits) in the low bits, angle 0..pi above, for
// the quaternion with w >= 0; a zero quaternion encodes as the identity (0).
// At most 32 bits, so the code unpacks with shifts and masks.
function makeQuatCodec(axisBits: number, angleBits: number): QuatCodec {
const axisCodeBits = 2 * axisBits;
if (axisCodeBits + angleBits > 32) {
throw new Error(`Quaternion codes hold at most 32 bits, not ${axisCodeBits + angleBits}`);
}
const oct = makeOctCodec(axisBits);
const axisMask = 2 ** axisCodeBits - 1;
const angleMax = 2 ** angleBits - 1;
const axis: Vec3 = { x: 0, y: 0, z: 0 };
return {
encode(q) {
// Not Math.hypot: it guards against overflow and is several times slower
const length = Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
if (!(length > 0)) {
return 0;
}
const s = (q.w < 0 ? -1 : 1) / length;
axis.x = q.x * s;
axis.y = q.y * s;
axis.z = q.z * s;
const angle = 2 * Math.acos(Math.min(1, q.w * s));
const angleCode = Math.round((angle / Math.PI) * angleMax);
return (oct.encode(axis) | (angleCode << axisCodeBits)) >>> 0;
},
decode(code, out) {
oct.decode(code & axisMask, out);
const angle = (((code >>> axisCodeBits) & angleMax) / angleMax) * Math.PI;
const s = Math.sin(angle / 2);
out.x *= s;
out.y *= s;
out.z *= s;
out.w = Math.cos(angle / 2);
return out;
},
};
}
const octCodecs = new Map<number, OctCodec>();
const quatCodecs = new Map<string, QuatCodec>();
function octCodec(bits: number) {
let codec = octCodecs.get(bits);
if (!codec) {
codec = makeOctCodec(bits);
octCodecs.set(bits, codec);
}
return codec;
}
function quatCodec(axisBits: number, angleBits: number) {
const key = `${axisBits}:${angleBits}`;
let codec = quatCodecs.get(key);
if (!codec) {
codec = makeQuatCodec(axisBits, angleBits);
quatCodecs.set(key, codec);
}
return codec;
}
const oct24 = octCodec(12);
const quatOct32 = quatCodec(10, 12);
const quatOct24 = quatCodec(8, 8);
// Scalar and vector packers of the splat codecs (GpuEncoding is the TSL
// twin). Fixed-size codecs name the total bits of the code: Oct24 is a 12 +
// 12 bit octahedral direction, QuatOct32 a 10 + 10 bit octahedral axis and a
// 12-bit angle, QuatOct24 8 + 8 and 8.
export class Encoding {
// The same memory as floats, cached per array
static floatView(u32: Uint32Array<ArrayBuffer>): Float32Array<ArrayBuffer> {
let view = floatViews.get(u32);
if (!view) {
view = new Float32Array(u32.buffer, u32.byteOffset, u32.length);
floatViews.set(u32, view);
}
return view;
}
// Half-float bits of f, rounded to nearest even (like the GPU's packHalf2x16)
static encodeHalf(f: number): number {
if (f16 && u16) {
f16[0] = f;
return u16[0];
}
f32[0] = f;
const bits = u32[0];
const sign = (bits >>> 16) & 0x8000;
const exponent = (bits >>> 23) & 0xff;
const mantissa = bits & 0x7fffff;
if (exponent === 0xff) {
return sign | 0x7c00 | (mantissa ? 0x200 : 0);
}
const e = exponent - 127 + 15;
if (e >= 0x1f) {
return sign | 0x7c00;
}
if (e <= 0) {
// Subnormal: multiples of 2^-24, ties to even
const m = Math.abs(f32[0]) * 2 ** 24;
let rounded = Math.floor(m);
const fraction = m - rounded;
if (fraction > 0.5 || (fraction === 0.5 && (rounded & 1))) {
rounded += 1;
}
return sign | rounded;
}
const half = sign | (e << 10) | (mantissa >> 13);
const rest = mantissa & 0x1fff;
return rest > 0x1000 || (rest === 0x1000 && (half & 1)) ? half + 1 : half;
}
static decodeHalf(h: number): number {
const sign = h & 0x8000 ? -1 : 1;
const exponent = (h >> 10) & 0x1f;
const mantissa = h & 0x3ff;
if (exponent === 0) {
return sign * mantissa * 2 ** -24;
}
if (exponent === 0x1f) {
return mantissa ? Number.NaN : sign * Number.POSITIVE_INFINITY;
}
return sign * (1 + mantissa / 1024) * 2 ** (exponent - 15);
}
static encodeUnorm8(v: number) {
return Math.max(0, Math.min(255, Math.round(v * 255)));
}
// 128 = 1.0; merged LoD nodes can exceed 1, up to 255 / 128
static encodeLodOpacity(opacity: number) {
return Math.max(0, Math.min(255, Math.round(opacity * 128)));
}
static decodeLodOpacity(byte: number) {
return byte / 128;
}
// 8-bit magnitudes, 3 sign bits and a 5-bit shared exponent, so colors may
// exceed 1 or be negative
static encodeExtRgb({ r, g, b }: Rgb): number {
const maxAbs = Math.max(Math.abs(r), Math.abs(g), Math.abs(b));
const base = Math.max(0, Math.min(31, (maxAbs > 0 ? Math.ceil(Math.log2(maxAbs)) : 0) + 15));
// Magnitudes are |v| / (2^e / 255), as |v| * (255 * 2^-e): exact, so a
// value halfway between two steps rounds up here and on the GPU alike
const scale = 255 * 2 ** (15 - base);
const mr = Math.min(255, Math.round(Math.abs(r) * scale));
const mg = Math.min(255, Math.round(Math.abs(g) * scale));
const mb = Math.min(255, Math.round(Math.abs(b) * scale));
const signs = (r < 0 ? 1 : 0) | (g < 0 ? 2 : 0) | (b < 0 ? 4 : 0);
return (mr | (mg << 8) | (mb << 16) | (((base << 3) | signs) << 24)) >>> 0;
}
static decodeExtRgb(encoded: number, out: Rgb): Rgb {
const unit = 2 ** (((encoded >>> 27) & 0x1f) - 15) / 255;
out.r = (encoded & 0xff) * unit * (encoded & 0x1000000 ? -1 : 1);
out.g = ((encoded >>> 8) & 0xff) * unit * (encoded & 0x2000000 ? -1 : 1);
out.b = ((encoded >>> 16) & 0xff) * unit * (encoded & 0x4000000 ? -1 : 1);
return out;
}
// Octahedral direction with `bits` per axis (1 to 16)
static encodeOct(v: Vec3, bits: number): number {
return octCodec(bits).encode(v);
}
static decodeOct(code: number, bits: number, out: Vec3): Vec3 {
return octCodec(bits).decode(code, out);
}
// Octahedral axis with `axisBits` per axis plus an `angleBits` angle (at
// most 32 bits in all)
static encodeQuatOct(q: Quat, axisBits: number, angleBits: number): number {
return quatCodec(axisBits, angleBits).encode(q);
}
static decodeQuatOct(code: number, axisBits: number, angleBits: number, out: Quat): Quat {
return quatCodec(axisBits, angleBits).decode(code, out);
}
static readonly encodeOct24: (v: Vec3) => number = oct24.encode;
static readonly decodeOct24: (code: number, out: Vec3) => Vec3 = oct24.decode;
static readonly encodeQuatOct32: (q: Quat) => number = quatOct32.encode;
static readonly decodeQuatOct32: (code: number, out: Quat) => Quat = quatOct32.decode;
static readonly encodeQuatOct24: (q: Quat) => number = quatOct24.encode;
static readonly decodeQuatOct24: (code: number, out: Quat) => Quat = quatOct24.decode;
}