diff --git a/webclient/src/components/ObjectViewInternal.tsx b/webclient/src/components/ObjectViewInternal.tsx index 901f83f..b224a35 100644 --- a/webclient/src/components/ObjectViewInternal.tsx +++ b/webclient/src/components/ObjectViewInternal.tsx @@ -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(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 ( - - + - + ); }); diff --git a/webclient/src/utils/graphics/greedyMesh.ts b/webclient/src/utils/graphics/greedyMesh.ts new file mode 100644 index 0000000..f1da52a --- /dev/null +++ b/webclient/src/utils/graphics/greedyMesh.ts @@ -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(); + 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; +}