greedy meshing
This commit is contained in:
@@ -1,35 +1,23 @@
|
|||||||
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
|
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
|
||||||
import type { VoxelGroup } from "../types/runtime/voxelGroup";
|
import type { VoxelGroup } from "../types/runtime/voxelGroup";
|
||||||
import { useEffect, useImperativeHandle, useLayoutEffect, useRef, type Ref, memo } from "react";
|
import { useEffect, useImperativeHandle, useRef, type Ref, memo, useMemo } from "react";
|
||||||
import { Euler, Matrix4, Quaternion, type Group, type InstancedMesh } from "three";
|
import { Euler, Quaternion, type Group } from "three";
|
||||||
import type { ThreeEvent } from "@react-three/fiber";
|
import type { ThreeEvent } from "@react-three/fiber";
|
||||||
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier";
|
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier";
|
||||||
import { SyncRigidBody } from "./SyncRigidBody";
|
import { SyncRigidBody } from "./SyncRigidBody";
|
||||||
import { reaction, runInAction } from "mobx";
|
import { reaction, runInAction } from "mobx";
|
||||||
import { v3toRapier } from "../utils";
|
import { v3toRapier } from "../utils";
|
||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
|
import { greedyMesh } from "../utils/graphics/greedyMesh";
|
||||||
const _m4 = new Matrix4();
|
|
||||||
|
|
||||||
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg }: { vg: VoxelGroup }) {
|
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg }: { vg: VoxelGroup }) {
|
||||||
const ref = useRef<InstancedMesh>(null);
|
const geo = useMemo(() => greedyMesh(vg.positions), [vg]);
|
||||||
|
useEffect(() => () => { geo.dispose(); }, [geo]);
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<instancedMesh ref={ref} args={[undefined, undefined, vg.positions.length]}>
|
<mesh geometry={geo}>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
|
||||||
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
|
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
|
||||||
</instancedMesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user