159 lines
4.9 KiB
TypeScript
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;
|
|
}
|