greedy meshing

This commit is contained in:
azykov@mail.ru
2026-06-13 20:08:10 +03:00
parent 4fcb542eb8
commit 7ac154dd58
2 changed files with 140 additions and 19 deletions
@@ -1,35 +1,23 @@
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
import type { VoxelGroup } from "../types/runtime/voxelGroup";
import { useEffect, useImperativeHandle, useLayoutEffect, useRef, type Ref, memo } from "react";
import { Euler, Matrix4, Quaternion, type Group, type InstancedMesh } from "three";
import { useEffect, useImperativeHandle, useRef, type Ref, memo, useMemo } from "react";
import { Euler, Quaternion, type Group } from "three";
import type { ThreeEvent } from "@react-three/fiber";
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody";
import { reaction, runInAction } from "mobx";
import { v3toRapier } from "../utils";
import { state } from "../state";
const _m4 = new Matrix4();
import { greedyMesh } from "../utils/graphics/greedyMesh";
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg }: { vg: VoxelGroup }) {
const ref = useRef<InstancedMesh>(null);
useLayoutEffect(() => {
const mesh = ref.current;
if (!mesh)
return;
vg.positions.forEach(([x, y, z], i) => {
mesh.setMatrixAt(i, _m4.makeTranslation(x, y, z));
});
mesh.instanceMatrix.needsUpdate = true;
mesh.computeBoundingSphere();
}, [vg]);
const geo = useMemo(() => greedyMesh(vg.positions), [vg]);
useEffect(() => () => { geo.dispose(); }, [geo]);
return (
<instancedMesh ref={ref} args={[undefined, undefined, vg.positions.length]}>
<boxGeometry args={[1, 1, 1]} />
<mesh geometry={geo}>
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
</instancedMesh>
</mesh>
);
});
+133
View File
@@ -0,0 +1,133 @@
import { BufferGeometry, Float32BufferAttribute } from 'three';
import type { V3 } from '../../types';
// tangent axes for each main axis
const U_OF = [1, 0, 0] as const;
const V_OF = [2, 2, 1] as const;
/**
* Greedy meshing: merges face-adjacent same-material voxels into minimal quads.
* A 50×1×50 flat layer produces 6 quads (12 triangles), not 50×50×6×2.
*
* Only face-adjacency counts — diagonal neighbours do not merge.
*/
export function greedyMesh(positions: V3[]): BufferGeometry {
if (!positions.length)
return new BufferGeometry();
const occ = new Set<string>();
let x0 = Infinity, y0 = Infinity, z0 = Infinity;
let x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
for (const [x, y, z] of positions) {
occ.add(`${x},${y},${z}`);
if (x < x0)
x0 = x;
if (x > x1)
x1 = x;
if (y < y0)
y0 = y;
if (y > y1)
y1 = y;
if (z < z0)
z0 = z;
if (z > z1)
z1 = z;
}
const has = (x: number, y: number, z: number) => occ.has(`${x},${y},${z}`);
const lo = [x0, y0, z0];
const hi = [x1, y1, z1];
const verts: number[] = [];
const inds: number[] = [];
for (let axis = 0; axis < 3; axis++) {
const u = U_OF[axis];
const v = V_OF[axis];
const dLen = hi[axis] - lo[axis] + 1;
const uLen = hi[u] - lo[u] + 1;
const vLen = hi[v] - lo[v] + 1;
const mask = new Uint8Array(uLen * vLen);
const done = new Uint8Array(uLen * vLen);
for (let dir = -1; dir <= 1; dir += 2) {
// For corner order (u0,v0)(u1,v0)(u1,v1)(u0,v1),
// cross product = +e_axis when axis≠Y, −e_Y when axis=Y.
// flip reverses winding to point outward for the given dir.
const flip = axis === 1 ? dir > 0 : dir < 0;
for (let di = 0; di < dLen; di++) {
const D = lo[axis] + di;
mask.fill(0);
done.fill(0);
for (let vi = 0; vi < vLen; vi++) {
for (let ui = 0; ui < uLen; ui++) {
const p: [number, number, number] = [0, 0, 0];
p[axis] = D;
p[u] = lo[u] + ui;
p[v] = lo[v] + vi;
if (!has(p[0], p[1], p[2]))
continue;
const np: [number, number, number] = [p[0], p[1], p[2]];
np[axis] += dir;
if (!has(np[0], np[1], np[2]))
mask[ui + vi * uLen] = 1;
}
}
for (let vi = 0; vi < vLen; vi++) {
for (let ui = 0; ui < uLen; ui++) {
if (!mask[ui + vi * uLen] || done[ui + vi * uLen])
continue;
let wu = 1;
while (ui + wu < uLen && mask[ui + wu + vi * uLen] && !done[ui + wu + vi * uLen])
wu++;
let wv = 1;
expand: while (vi + wv < vLen) {
for (let k = 0; k < wu; k++)
if (!mask[ui + k + (vi + wv) * uLen] || done[ui + k + (vi + wv) * uLen])
break expand;
wv++;
}
for (let dv = 0; dv < wv; dv++)
for (let du = 0; du < wu; du++)
done[ui + du + (vi + dv) * uLen] = 1;
const dp = D + (dir > 0 ? 0.5 : -0.5);
const uA = lo[u] + ui - 0.5;
const uB = uA + wu;
const vA = lo[v] + vi - 0.5;
const vB = vA + wv;
const base = verts.length / 3;
for (const [cu, cv] of [[uA, vA], [uB, vA], [uB, vB], [uA, vB]] as const) {
const pt: [number, number, number] = [0, 0, 0];
pt[axis] = dp;
pt[u] = cu;
pt[v] = cv;
verts.push(pt[0], pt[1], pt[2]);
}
if (flip) {
inds.push(base, base + 2, base + 1, base, base + 3, base + 2);
} else {
inds.push(base, base + 1, base + 2, base, base + 2, base + 3);
}
}
}
}
}
}
const geo = new BufferGeometry();
geo.setAttribute('position', new Float32BufferAttribute(verts, 3));
geo.setIndex(inds);
geo.computeVertexNormals();
return geo;
}