Zum Inhalt

viewer/tests/unit/marching-cubes.test.mjs

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { marchingCubes } from '../../js/collision/marching-cubes.js';

const bounds = { min: { x: 0, y: 0, z: 0 }, max: { x: 1, y: 1, z: 1 } };

test('marchingCubes: all-zero grid → empty mesh', () => {
  // All corners ≤ iso → cubeIndex = 255 → case 255 has no triangles.
  const density = new Float32Array(4 ** 3);
  const mesh = marchingCubes({ density, resolution: 4, bounds, iso: 0.5 });
  assert.equal(mesh.positions.length, 0);
  assert.equal(mesh.indices.length, 0);
});

test('marchingCubes: all-one grid → empty mesh (no inside-out surface)', () => {
  // All corners > iso → cubeIndex = 0 → case 0 has no triangles.
  const density = new Float32Array(4 ** 3).fill(1);
  const mesh = marchingCubes({ density, resolution: 4, bounds, iso: 0.5 });
  assert.equal(mesh.positions.length, 0);
});

test('marchingCubes: 2x2x2 single cell with corner 0 above iso → 1 triangle', () => {
  // 2x2x2 grid = exactly one MC cell.
  // Bourke convention: bit set when density ≤ iso.
  // Corner 0 has density 1.0 (> iso=0.5) → bit 0 = 0
  // Corners 1..7 have density 0 (≤ iso) → bits 1..7 = 1
  // cubeIndex = 0b11111110 = 254 → triTable[254] = [0, 3, 8, -1, ...] (one triangle)
  const density = new Float32Array(8);
  density[0] = 1.0;
  const mesh = marchingCubes({
    density, resolution: 2, bounds, iso: 0.5,
  });
  assert.equal(mesh.indices.length, 3);
  assert.equal(mesh.positions.length, 3 * 3);
});

test('marchingCubes: sphere density → closed-ish surface near unit radius', () => {
  const r = 16;
  const density = new Float32Array(r ** 3);
  const cx = (r - 1) / 2, cy = cx, cz = cx;
  const radius = r * 0.3;
  for (let k = 0; k < r; k++) {
    for (let j = 0; j < r; j++) {
      for (let i = 0; i < r; i++) {
        const dx = i - cx, dy = j - cy, dz = k - cz;
        const d = Math.hypot(dx, dy, dz);
        density[k * r * r + j * r + i] = Math.max(0, 1 - d / radius);
      }
    }
  }
  const mesh = marchingCubes({
    density, resolution: r,
    bounds: { min: { x: -1, y: -1, z: -1 }, max: { x: 1, y: 1, z: 1 } },
    iso: 0.5,
  });
  assert.ok(mesh.indices.length > 300,
    `expected > 100 tris, got ${mesh.indices.length / 3}`);

  // Vertices should lie roughly on a sphere of world-radius proportional
  // to the cell extent. cellExtent = 2/r in world units; sphere radius in
  // cells = `radius` (in cell units); world radius = `radius` * cellExtent.
  // Iso = 0.5 cuts at half-way through `1 - d/radius` falloff, i.e. at
  // d = radius/2 cells.
  const cellExtent = 2 / r;
  const expectedR = radius * 0.5 * cellExtent;
  let inRange = 0;
  for (let v = 0; v < mesh.positions.length; v += 3) {
    const x = mesh.positions[v], y = mesh.positions[v + 1], z = mesh.positions[v + 2];
    const d = Math.hypot(x, y, z);
    if (d > expectedR * 0.6 && d < expectedR * 1.4) inRange++;
  }
  const ratio = inRange / (mesh.positions.length / 3);
  assert.ok(ratio > 0.8,
    `${(ratio * 100).toFixed(1)}% of vertices in [0.6R, 1.4R] (expected > 80%, R=${expectedR.toFixed(3)})`);
});