sh.ts
examples/sogs/sh.ts · raw · open the example · all examples · site home
Exports: RANGES_BUNDLEMAX_RANGESrangeLookupSogDatasogGeneratorSogArrayspaletteOfsogObject
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
// Drawing SOG splats with their spherical harmonics: each splat's label
// (bundle shLabel, beside the splats) picks an entry of the file's palette
// (an ext ShPalette's table), through Radix's built-in SH
// (SplatGenerator.fromSplats with a palette; the color is seen from each
// view's origin in the object's space). One generator per (layout, codec,
// degree, remap) serves every object of that kind; an object's degree is
// objectParams.maxShDegree, so changing it compiles nothing. bind() points it
// at an object's arrays, ranges, palette and tint before the object's
// dispatch.
import * as THREE from "three";
import {
GpuArrayReader, Sh, SplatGenerator, TSL, defineBundle,
type AnySplatObject, type GpuArrays, type ShCodecName, type ShDegree, type SplatLayout, type StoredSplats, type TslNode,
} from "@sparkjsdev/radix";
const { If, Loop, uint, uniform } = TSL;
// An object drawing ranges of a file's splats: one row per range, x the
// object's first index in it (rows in increasing x, the first 0), y the
// range's first stored splat
export const RANGES_BUNDLE = defineBundle({ range: "uint32x2" });
// Ranges a table may hold: 2^RANGE_STEPS
const RANGE_STEPS = 12;
export const MAX_RANGES = 1 << RANGE_STEPS;
// The stored splat of object index `index`: a binary search for the last
// range starting at or before it (a reader of RANGES_BUNDLE)
export function rangeLookup(ranges: GpuArrayReader, index: TslNode): TslNode {
const base = uint(0).toVar();
const n = ranges.count.toVar();
Loop(RANGE_STEPS, () => {
If(n.greaterThan(uint(1)), () => {
const half = n.shiftRight(uint(1)).toVar();
If(ranges.fetch("range", base.add(half)).x.lessThanEqual(index), () => {
base.addAssign(half);
});
n.subAssign(half);
});
});
const range = ranges.load("range", base).toVar();
return range.y.add(index.sub(range.x));
}
// What a SOG generator binds per object
export interface SogData {
// Splats, with bundle shLabel when the file has SH
splats: GpuArrays;
// Remap "ranges": the ranges of stored splats this object draws
ranges?: GpuArrays;
// The SH palette (an ShPalette's table on the GPU)
palette?: GpuArrays;
// Color multiplier, read at every bind (default white)
tint?: THREE.Color;
}
const WHITE = new THREE.Color(1, 1, 1);
function makeGenerator(layout: SplatLayout, sh: { codec: ShCodecName; degree: ShDegree } | undefined, remap: boolean): SplatGenerator<SogData> {
const ranges = remap ? new GpuArrayReader(RANGES_BUNDLE) : undefined;
const base = SplatGenerator.fromSplats({
layout, remap: ranges ? (i) => rangeLookup(ranges, i) : undefined, sh: sh && { ...sh, palette: true },
});
const tint = uniform(new THREE.Color(1, 1, 1));
const tinted = (splat: TslNode) => {
const out = splat.toVar();
out.get("rgb").assign(out.get("rgb").mul(tint));
return out;
};
return {
generateSplat: (index, context) => tinted(base.generateSplat!(index, context)),
generateCovSplat: (index, context) => tinted(base.generateCovSplat!(index, context)),
bind(data, context) {
const stored: StoredSplats = { splats: data.splats, shPalette: data.palette };
base.bind!(stored, context);
ranges?.bind(data.ranges, "ranges");
tint.value.copy(data.tint ?? WHITE);
},
};
}
const generators = new Map<string, SplatGenerator<SogData>>();
// The shared generator of objects of `layout`, with a palette of `sh`
// (none: base colors), whole or through a table of ranges
export function sogGenerator({ layout = "extSplats", sh, remap = false }: { layout?: SplatLayout; sh?: { codec: ShCodecName; degree: ShDegree }; remap?: boolean } = {}) {
const key = `${layout}|${sh ? `${sh.codec}|${sh.degree}` : ""}|${remap}`;
if (!generators.has(key)) {
generators.set(key, makeGenerator(layout, sh, remap));
}
return generators.get(key)!;
}
// A SOG file on the GPU
export interface SogArrays {
arrays: GpuArrays;
// An ShPalette's table on the GPU (bundle shPalette)
palette?: GpuArrays;
degree: number;
layout: SplatLayout;
}
// The codec and degree of a file's palette
export function paletteOf(file: SogArrays): { codec: ShCodecName; degree: ShDegree } | undefined {
const meta = file.palette?.bundle("shPalette").meta;
return meta && { codec: Sh.codec(meta.codec as string).name, degree: meta.degree as ShDegree };
}
// A splat object drawing `file` (all of it, or the `count` splats of a
// RANGES_BUNDLE table `ranges`) through SH degree min(degree, the file's),
// placed by `matrixWorld`
export function sogObject(file: SogArrays, { degree = 3, matrixWorld, ranges, count, tint, release }: {
degree?: number;
matrixWorld?: THREE.Matrix4;
ranges?: GpuArrays;
count?: number;
tint?: THREE.Color;
release?: () => void;
} = {}): AnySplatObject {
const sh = paletteOf(file);
const data: SogData = { splats: file.arrays, ranges, palette: file.palette, tint };
return {
generator: sogGenerator({ layout: file.layout, sh, remap: ranges !== undefined }),
data,
count: count ?? file.arrays.count,
matrixWorld,
params: { maxShDegree: Math.min(degree, sh?.degree ?? 0) },
release,
};
}