lcc2Select.ts
examples/lcc/lcc2Select.ts · raw · open the example · all examples · site home
Exports: Lcc2SelectSettingsVec3LikeLcc2CutselectCutleafCoverage
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
// The LoD cut of an LCC2 tree (Lcc2Scene.ts) for a camera: the nodes to
// draw, covering every leaf exactly once. A node is drawn when it is a leaf
// or when its distance asks for a level at least its own; otherwise its
// children are visited. Distances are as in examples/sogs/lod.ts (after
// PlayCanvas 2.23): to the nearest point of the node's box, longer behind
// the camera (behindPenalty), scaled by the field of view (fovScale). A node
// at distance d asks for level 1 + floor(log2(d / (s * lodDistance))), as
// LCC v1's cells do (level 0 within lodDistance, one level coarser per
// doubling), with one scale s for the whole tree fitted to the splat
// budget: node n is split exactly when ln s > x(n) = ln(d / lodDistance) -
// (level - 1) ln 2, and only once its parent is, so its split point is
// e(n) = max(x(n), e(parent)). Splitting n adds its children's splats less
// its own; sorted by e, the splits are taken in order while the total fits,
// which is the histogram balancer of sogs/lod.ts made exact (trees are a few
// hundred nodes). "limit" keeps s <= 1 (the distances are the most detail
// allowed), "target" lets detail rise to fill the budget. A fixed level
// splits every node above it, nearest first under the budget. Pure code, so
// the Node check tests it.
import type { Lcc2Scene } from "./Lcc2Scene.js";
export interface Lcc2SelectSettings {
// "auto": by distance; a number: that level everywhere (the budget still caps)
level: "auto" | number;
lodDistance: number;
behindPenalty: number;
// Most splats drawn (0: no cap)
budget: number;
budgetMode: "limit" | "target";
}
export interface Vec3Like {
x: number;
y: number;
z: number;
}
export interface Lcc2Cut {
// The drawn nodes, depth-first
nodes: number[];
// Their splats
splats: number;
// ln s of the cut: nodes split exactly when their split point is below it
logScale: number;
// Per node: the distance (penalized, scaled) and the split point
distance: Float32Array;
split: Float64Array;
}
const LN2 = Math.LN2;
// A fixed level's splits sort below any distance's
const FIXED = 1e6;
// The splats a node's children hold, less its own
function splitDelta(scene: Lcc2Scene, node: number) {
let sum = 0;
for (let k = 0; k < scene.childCount[node]; k += 1) {
sum += scene.count[scene.children[scene.firstChild[node] + k]];
}
return sum - scene.count[node];
}
export function selectCut(scene: Lcc2Scene, eye: Vec3Like, forward: Vec3Like, s: Lcc2SelectSettings, fov = 1, out?: Lcc2Cut): Lcc2Cut {
const n = scene.nodes;
const cut: Lcc2Cut = out ?? { nodes: [], splats: 0, logScale: 0, distance: new Float32Array(n), split: new Float64Array(n) };
const { bounds, parent, level } = scene;
const penalty = Math.max(0, s.behindPenalty - 1);
const logBase = Math.log(Math.max(1e-6, s.lodDistance));
const fixed = s.level !== "auto";
// Split points, parents before children (depth-first order)
for (let node = 0; node < n; node += 1) {
const b = node * 6;
const dx = Math.max(bounds[b], Math.min(eye.x, bounds[b + 3])) - eye.x;
const dy = Math.max(bounds[b + 1], Math.min(eye.y, bounds[b + 4])) - eye.y;
const dz = Math.max(bounds[b + 2], Math.min(eye.z, bounds[b + 5])) - eye.z;
let distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (penalty > 0 && distance > 0.01) {
const facing = (dx * forward.x + dy * forward.y + dz * forward.z) / distance;
if (facing < 0) {
distance *= 1 - facing * penalty;
}
}
distance *= fov;
cut.distance[node] = distance;
let x: number;
if (node === 0 || scene.isLeaf(node)) {
// The root holds no splats (always split); leaves never split
x = node === 0 ? -Infinity : Infinity;
} else if (fixed) {
x = level[node] > (s.level as number) ? Math.log(Math.max(distance, 1e-6)) - FIXED : Infinity;
} else {
x = Math.log(Math.max(distance, 1e-6)) - logBase - (level[node] - 1) * LN2;
}
cut.split[node] = parent[node] >= 0 ? Math.max(x, cut.split[parent[node]]) : x;
}
// The cut: limit mode starts at s = 1 (fixed levels below it), target at
// no limit; then the budget lowers it to the first split that does not fit
let logScale = fixed ? 0 : s.budgetMode === "limit" ? 0 : Infinity;
const budget = s.budget > 0 ? s.budget : Infinity;
let total = 0;
for (let k = 0; k < scene.childCount[0]; k += 1) {
total += scene.count[scene.children[scene.firstChild[0] + k]];
}
if (budget < Infinity) {
const order: number[] = [];
for (let node = 1; node < n; node += 1) {
if (cut.split[node] < logScale) {
order.push(node);
}
}
order.sort((a, b) => cut.split[a] - cut.split[b]);
for (const node of order) {
const delta = splitDelta(scene, node);
if (total + delta > budget) {
logScale = cut.split[node];
break;
}
total += delta;
}
}
cut.logScale = logScale;
// The drawn nodes: those not split whose parent is
cut.nodes.length = 0;
cut.splats = 0;
const stack = [0];
while (stack.length > 0) {
const node = stack.pop()!;
if (node === 0 || cut.split[node] < logScale) {
for (let k = scene.childCount[node] - 1; k >= 0; k -= 1) {
stack.push(scene.children[scene.firstChild[node] + k]);
}
} else {
cut.nodes.push(node);
cut.splats += scene.count[node];
}
}
return cut;
}
// The leaves under each drawn node, counted (checks: every leaf exactly once)
export function leafCoverage(scene: Lcc2Scene, nodes: number[]): Uint32Array {
const covered = new Uint32Array(scene.nodes);
const stack = [...nodes];
while (stack.length > 0) {
const node = stack.pop()!;
if (scene.isLeaf(node)) {
covered[node] += 1;
}
for (let k = 0; k < scene.childCount[node]; k += 1) {
stack.push(scene.children[scene.firstChild[node] + k]);
}
}
return covered;
}