import { unit } from "./math.js"; export function sphere(level = 5) { if (!Number.isInteger(level) || level < 4 || level > 6) throw new Error("Choose a supported resolution."); const t = (1 + Math.sqrt(5)) / 2; const vertices = [ [-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0], [0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t], [t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1], ].map(unit); let faces = [ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], ]; for (let l = 0; l < level; l++) { const cache = new Map(); const midpoint = (a, b) => { const key = Math.min(a, b) * 100000 + Math.max(a, b); if (!cache.has(key)) { cache.set(key, vertices.length); vertices.push(unit(vertices[a].map((v, i) => v + vertices[b][i]))); } return cache.get(key); }; faces = faces.flatMap(([a, b, c]) => { const ab = midpoint(a, b), bc = midpoint(b, c), ca = midpoint(c, a); return [ [a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca], ]; }); } const positions = new Float32Array(vertices.flat()); const indices = new Uint32Array(faces.flat()); const incident = Array.from({ length: vertices.length }, () => []); const neighbors = Array.from({ length: vertices.length }, () => new Set()); faces.forEach((face, f) => face.forEach((v) => { incident[v].push(f); for (const other of face) if (other !== v) neighbors[v].add(other); }), ); const mesh = { level, positions, indices, normals: new Float32Array(positions.length), faceNormals: new Float32Array(indices.length), incident: incident.map((a) => Uint32Array.from(a)), neighbors: neighbors.map((a) => Uint32Array.from(a)), }; mesh.faceMarks = new Uint32Array(faces.length); mesh.vertexMarks = new Uint32Array(vertices.length); mesh.generation = 0; updateNormals(mesh); return mesh; } // Include the unchanged incident faces when recomputing a boundary vertex normal. export function updateNormals(mesh, moved) { const { positions: p, indices: idx, normals: n, faceNormals: fn } = mesh; const faces = [], vertices = []; const gen = ++mesh.generation; if (moved) { for (const v of moved) for (const f of mesh.incident[v]) { if (mesh.faceMarks[f] !== gen) { mesh.faceMarks[f] = gen; faces.push(f); } } } else for (let f = 0; f < idx.length / 3; f++) faces.push(f); for (const f of faces) { const a = idx[f * 3], b = idx[f * 3 + 1], c = idx[f * 3 + 2]; const ax = p[b * 3] - p[a * 3], ay = p[b * 3 + 1] - p[a * 3 + 1], az = p[b * 3 + 2] - p[a * 3 + 2]; const bx = p[c * 3] - p[a * 3], by = p[c * 3 + 1] - p[a * 3 + 1], bz = p[c * 3 + 2] - p[a * 3 + 2]; fn[f * 3] = ay * bz - az * by; fn[f * 3 + 1] = az * bx - ax * bz; fn[f * 3 + 2] = ax * by - ay * bx; for (let j = 0; j < 3; j++) { const v = idx[f * 3 + j]; if (mesh.vertexMarks[v] !== gen) { mesh.vertexMarks[v] = gen; vertices.push(v); } } } for (const v of vertices) { let x = 0, y = 0, z = 0; for (const f of mesh.incident[v]) { x += fn[f * 3]; y += fn[f * 3 + 1]; z += fn[f * 3 + 2]; } const length = Math.hypot(x, y, z) || 1; n[v * 3] = x / length; n[v * 3 + 1] = y / length; n[v * 3 + 2] = z / length; } return { faces, vertices }; }