Files
bworld/client/workers/translucent_sort.ts
T
2026-09-25 15:01:03 -03:00

159 lines
4.9 KiB
TypeScript

// translucent quad sorting, the way sodium does it (its "translucency sorting"), simplified for quads
// that all face along an axis.
// every chunk's translucent mesh gets a sort type when it's meshed:
// - none: the order can't matter, one quad or all of them in a single plane
// - static: all quads face along one axis, so sorting them by distance along their normal once is right
// from anywhere the camera can see them
// - dynamic: sorted by distance to the camera, and sorted again only when the camera crosses one of the
// planes the chunk's quads lie on, the only time the order between them can change (sodium's GFNI)
export type SortType = "none" | "static" | "dynamic";
// same order as the mesher's faces
export const FACE_NORMALS = [
[0, 1, 0], // top
[0, -1, 0], // bottom
[0, 0, 1], // front
[0, 0, -1], // back
[-1, 0, 0], // left
[1, 0, 0], // right
] as const;
export const FACE_AXIS = [1, 1, 2, 2, 0, 0] as const;
// for each quad: its center, and which face it is
export interface TranslucentQuads {
centers: Float32Array;
faces: Uint8Array;
count: number;
}
export function choose_sort_type(quads: TranslucentQuads): SortType {
if (quads.count <= 1) {
return "none";
}
let axes = 0;
for (let q = 0; q < quads.count; q++) {
axes |= 1 << FACE_AXIS[quads.faces[q]];
}
// more than one axis, the order depends on where the camera is
if (axes & (axes - 1)) {
return "dynamic";
}
// quads facing opposite ways on the same axis are never both visible over each other, so only
// the distance along the axis matters. if they all share a plane, nothing can overlap at all
const axis = FACE_AXIS[quads.faces[0]];
const plane = quads.centers[axis];
for (let q = 1; q < quads.count; q++) {
if (quads.centers[q * 3 + axis] !== plane) {
return "static";
}
}
return "none";
}
// the unique coordinates of the planes the quads lie on, per axis, sorted. the camera crossing one of
// these is what triggers a dynamic sort
export function quad_planes(quads: TranslucentQuads): [Float32Array, Float32Array, Float32Array] {
const sets = [new Set<number>(), new Set<number>(), new Set<number>()];
for (let q = 0; q < quads.count; q++) {
const axis = FACE_AXIS[quads.faces[q]];
sets[axis].add(quads.centers[q * 3 + axis]);
}
return sets.map((set) => Float32Array.from(set).sort()) as [Float32Array, Float32Array, Float32Array];
}
// whether moving the camera from a to b crosses any of the planes
export function crosses_planes(planes: [Float32Array, Float32Array, Float32Array], a: number[], b: number[]) {
for (let axis = 0; axis < 3; axis++) {
if (a[axis] === b[axis] || planes[axis].length === 0) {
continue;
}
if (count_below(planes[axis], a[axis]) !== count_below(planes[axis], b[axis])) {
return true;
}
}
return false;
}
function count_below(sorted: Float32Array, value: number) {
let lo = 0;
let hi = sorted.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (sorted[mid] < value) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
// back to front, for the camera at camera_x/y/z when the sort type is dynamic
export function sort_quads(
quads: TranslucentQuads,
sort_type: SortType,
camera_x: number,
camera_y: number,
camera_z: number,
): Uint32Array {
const { centers, faces, count } = quads;
if (sort_type === "dynamic") {
return quad_indices(sort_by_distance(centers, count, camera_x, camera_y, camera_z));
}
const order = new Uint32Array(count);
for (let q = 0; q < count; q++) {
order[q] = q;
}
if (sort_type === "static") {
// for quads facing the camera, the ones further along their normal are closer to it
const keys = new Float32Array(count);
for (let q = 0; q < count; q++) {
const [nx, ny, nz] = FACE_NORMALS[faces[q]];
keys[q] = centers[q * 3] * nx + centers[q * 3 + 1] * ny + centers[q * 3 + 2] * nz;
}
order.sort((a, b) => keys[a] - keys[b]);
}
return quad_indices(order);
}
// dynamic sorting only needs the centers, so resorting doesn't need the whole mesh
export function sort_by_distance(
centers: Float32Array,
count: number,
camera_x: number,
camera_y: number,
camera_z: number,
): Uint32Array {
const order = new Uint32Array(count);
const distances = new Float32Array(count);
for (let q = 0; q < count; q++) {
order[q] = q;
const dx = centers[q * 3] - camera_x;
const dy = centers[q * 3 + 1] - camera_y;
const dz = centers[q * 3 + 2] - camera_z;
distances[q] = dx * dx + dy * dy + dz * dz;
}
return order.sort((a, b) => distances[b] - distances[a]);
}
// two triangles per quad, drawn in the given order
export function quad_indices(order: Uint32Array): Uint32Array {
const indices = new Uint32Array(order.length * 6);
for (let i = 0; i < order.length; i++) {
const v = order[i] * 4;
const o = i * 6;
indices[o] = v;
indices[o + 1] = v + 1;
indices[o + 2] = v + 2;
indices[o + 3] = v;
indices[o + 4] = v + 2;
indices[o + 5] = v + 3;
}
return indices;
}