lccDecode.ts

examples/lcc/lccDecode.ts · raw · open the example · all examples · site home

Exports: decodeLccShdecodeBlocksplitEnvironment

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
// LCC records into Radix's ext splats, after the LCC whitepaper and
// PlayCanvas splat-transform's reader. The Radix core module is passed in
// (the decode workers load radix-core.module.js by URL, the Node checks
// import the sources), so this file imports types only.
// data.bin, 32 bytes per splat as 8 uint32 words:
//   0-2  center, float32 xyz
//   3    r, g, b, opacity bytes (base color and linear opacity, / 255)
//   4    scale x u16 | scale y u16 << 16    (mix(scaleMin, scaleMax, u / 65535))
//   5    scale z u16 | rotation low 16 << 16
//   6    rotation high 16 | normal x u16 << 16
//   7    normal y u16 | normal z u16 << 16
// The rotation word is three 10-bit components of the quaternion (xyzw)
// without its largest, which is positive, then that component's index.
// shcoef.bin, 16 words per splat: word k < 15 is SH coefficient k + 1 as
// r 11 | g 10 << 11 | b 11 << 21 bits over [shMin, shMax] per channel,
// decoded (decodeLccSh) into Radix's extSh.
// environment.bin holds data.bin records, each followed by its 16 SH words
// in Quality files (96 bytes per splat), quantized over the env* ranges.
import type * as Core from "@sparkjsdev/radix/core";
import type { LccRanges } from "./LccScene.js";

const SQRT2 = Math.SQRT2;
const RSQRT2 = Math.SQRT1_2;

// The SH coefficients of splat i (45 values, 3DGS order) from its 16 words
// at words[i * 16], over the block's [shMin, shMax]
function lccShInto(words: Uint32Array, i: number, { shMin, shMax }: Pick<LccRanges, "shMin" | "shMax">, out: Float32Array) {
  const [r0, g0, b0] = shMin;
  const dr = shMax[0] - r0;
  const dg = shMax[1] - g0;
  const db = shMax[2] - b0;
  for (let k = 0; k < 15; k += 1) {
    const word = words[i * 16 + k];
    out[k * 3] = r0 + dr * ((word & 2047) / 2047);
    out[k * 3 + 1] = g0 + dg * (((word >>> 11) & 1023) / 1023);
    out[k * 3 + 2] = b0 + db * (((word >>> 21) & 2047) / 2047);
  }
}

const shWordsOf = (sh: Uint8Array, count: number) => {
  if (sh.byteLength < count * 64) {
    throw new Error(`Block SH too short: ${sh.byteLength} bytes for ${count} splats`);
  }
  const aligned = sh.byteOffset % 4 === 0 ? sh : sh.slice();
  return new Uint32Array(aligned.buffer, aligned.byteOffset, count * 16);
};

// A block's SH as floats, count * 45 (the exact values, for the checks)
export function decodeLccSh(sh: Uint8Array, count: number, ranges: Pick<LccRanges, "shMin" | "shMax">): Float32Array {
  const words = shWordsOf(sh, count);
  const out = new Float32Array(count * 45);
  const coeffs = new Float32Array(45);
  for (let i = 0; i < count; i += 1) {
    lccShInto(words, i, ranges, coeffs);
    out.set(coeffs, i * 45);
  }
  return out;
}

// The splats of one block as ext splats (bundle extSplats) and, with
// `sh`, their SH (bundle extSh)
export function decodeBlock(core: typeof Core, data: Uint8Array, sh: Uint8Array | undefined, count: number, ranges: LccRanges, normalRange?: { min: number[]; max: number[] }) {
  if (data.byteLength < count * 32) {
    throw new Error(`Block data too short: ${data.byteLength} bytes for ${count} splats`);
  }
  const aligned = data.byteOffset % 4 === 0 ? data : data.slice();
  const words = new Uint32Array(aligned.buffer, aligned.byteOffset, count * 8);
  const floats = new Float32Array(aligned.buffer, aligned.byteOffset, count * 8);

  const out = core.Splats.make(count);
  const encode = core.Splats.encoder(out.bundles.extSplats);
  const s = core.Splats.makeExtSplat();
  const [sx0, sy0, sz0] = ranges.scaleMin;
  const sdx = ranges.scaleMax[0] - sx0;
  const sdy = ranges.scaleMax[1] - sy0;
  const sdz = ranges.scaleMax[2] - sz0;
  const nMin = normalRange?.min ?? [0, 0, 0];
  const nMax = normalRange?.max ?? [0, 0, 0];
  const hasNormals = nMin.some((v, k) => v !== 0 || nMax[k] !== 0);

  for (let i = 0; i < count; i += 1) {
    const w = i * 8;
    s.center.x = floats[w];
    s.center.y = floats[w + 1];
    s.center.z = floats[w + 2];

    const color = words[w + 3];
    s.rgb.r = (color & 255) / 255;
    s.rgb.g = ((color >>> 8) & 255) / 255;
    s.rgb.b = ((color >>> 16) & 255) / 255;
    s.opacity = (color >>> 24) / 255;

    const w4 = words[w + 4];
    const w5 = words[w + 5];
    const w6 = words[w + 6];
    s.scales.x = sx0 + sdx * ((w4 & 0xffff) / 65535);
    s.scales.y = sy0 + sdy * ((w4 >>> 16) / 65535);
    s.scales.z = sz0 + sdz * ((w5 & 0xffff) / 65535);

    const rotation = ((w5 >>> 16) | (w6 << 16)) >>> 0;
    const a = (rotation & 1023) / 1023 * SQRT2 - RSQRT2;
    const b = ((rotation >>> 10) & 1023) / 1023 * SQRT2 - RSQRT2;
    const c = ((rotation >>> 20) & 1023) / 1023 * SQRT2 - RSQRT2;
    const d = Math.sqrt(1 - Math.min(1, a * a + b * b + c * c));
    const q = s.quaternion;
    switch (rotation >>> 30) {
      case 0: q.x = d; q.y = a; q.z = b; q.w = c; break;
      case 1: q.x = a; q.y = d; q.z = b; q.w = c; break;
      case 2: q.x = a; q.y = b; q.z = d; q.w = c; break;
      default: q.x = a; q.y = b; q.z = c; q.w = d;
    }

    if (hasNormals) {
      const w7 = words[w + 7];
      const nx = nMin[0] + (nMax[0] - nMin[0]) * ((w6 >>> 16) / 65535);
      const ny = nMin[1] + (nMax[1] - nMin[1]) * ((w7 & 0xffff) / 65535);
      const nz = nMin[2] + (nMax[2] - nMin[2]) * ((w7 >>> 16) / 65535);
      const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
      s.normal.x = length > 0 ? nx / length : 0;
      s.normal.y = length > 0 ? ny / length : 0;
      s.normal.z = length > 0 ? nz / length : 0;
    }
    encode(i, s);
  }

  if (sh) {
    const shWords = shWordsOf(sh, count);
    const encodeSh = core.Sh.prepare(out as unknown as Core.ArrayData, 3)!;
    const coeffs = new Float32Array(core.Sh.VALUES);
    for (let i = 0; i < count; i += 1) {
      lccShInto(shWords, i, ranges, coeffs);
      encodeSh(i, coeffs);
    }
  }
  return out;
}

// environment.bin as decodeBlock's inputs: its records, and with `sh` the
// SH bytes split from them
export function splitEnvironment(bytes: Uint8Array, sh: boolean): { count: number; data: Uint8Array; sh?: Uint8Array } {
  if (!sh) {
    return { count: Math.floor(bytes.byteLength / 32), data: bytes };
  }
  const count = Math.floor(bytes.byteLength / 96);
  const data = new Uint8Array(count * 32);
  const shBytes = new Uint8Array(count * 64);
  for (let i = 0; i < count; i += 1) {
    data.set(bytes.subarray(i * 96, i * 96 + 32), i * 32);
    shBytes.set(bytes.subarray(i * 96 + 32, i * 96 + 96), i * 64);
  }
  return { count, data, sh: shBytes };
}