Spaces:
Running
Running
| // Test subjects, generated rather than downloaded. | |
| // | |
| // A splat reconstruction of a real object would do, but every published one we | |
| // could use carries research-only or unstated terms, so the shapes here are made | |
| // from scratch: each returns gaussian centres plus per-kernel sigma, which is all | |
| // the simulation and the renderer need. | |
| function rngFrom(seed) { | |
| let s = seed >>> 0 || 1; | |
| return () => { | |
| s ^= s << 13; s >>>= 0; | |
| s ^= s >> 17; | |
| s ^= s << 5; s >>>= 0; | |
| return s / 4294967296; | |
| }; | |
| } | |
| /** Fill a shape by rejection sampling, so density is uniform in volume. */ | |
| function fill(inside, count, bounds, rng) { | |
| const pos = [], [lo, hi] = bounds; | |
| let guard = 0; | |
| while (pos.length < count * 3 && guard < count * 400) { | |
| guard++; | |
| const x = lo[0] + rng() * (hi[0] - lo[0]); | |
| const y = lo[1] + rng() * (hi[1] - lo[1]); | |
| const z = lo[2] + rng() * (hi[2] - lo[2]); | |
| if (inside(x, y, z)) pos.push(x, y, z); | |
| } | |
| return new Float32Array(pos); | |
| } | |
| const SHAPES = { | |
| cube: { | |
| label: 'Cube', | |
| make: (rng, n) => fill((x, y, z) => | |
| Math.abs(x - 0.5) < 0.13 && Math.abs(z - 0.5) < 0.13 && y > 0.52 && y < 0.78, | |
| n, [[0.3, 0.45, 0.3], [0.7, 0.85, 0.7]], rng), | |
| }, | |
| ball: { | |
| label: 'Ball', | |
| make: (rng, n) => fill((x, y, z) => | |
| (x - 0.5) ** 2 + (y - 0.66) ** 2 + (z - 0.5) ** 2 < 0.145 ** 2, | |
| n, [[0.34, 0.5, 0.34], [0.66, 0.82, 0.66]], rng), | |
| }, | |
| column: { | |
| label: 'Column', | |
| make: (rng, n) => fill((x, y, z) => | |
| (x - 0.5) ** 2 + (z - 0.5) ** 2 < 0.09 ** 2 && y > 0.2 && y < 0.86, | |
| n, [[0.4, 0.2, 0.4], [0.6, 0.86, 0.6]], rng), | |
| }, | |
| arch: { | |
| label: 'Arch', | |
| make: (rng, n) => fill((x, y, z) => { | |
| if (Math.abs(z - 0.5) > 0.09) return false; | |
| const r = Math.hypot(x - 0.5, y - 0.42); | |
| if (y > 0.42) return r > 0.14 && r < 0.24; | |
| return Math.abs(Math.abs(x - 0.5) - 0.19) < 0.05 && y > 0.2; | |
| }, n, [[0.24, 0.2, 0.4], [0.76, 0.7, 0.6]], rng), | |
| }, | |
| tower: { | |
| label: 'Tower', | |
| make: (rng, n) => fill((x, y, z) => { | |
| const t = (y - 0.2) / 0.62; | |
| if (t < 0 || t > 1) return false; | |
| const half = 0.16 * (1 - t * 0.55); | |
| return Math.abs(x - 0.5) < half && Math.abs(z - 0.5) < half; | |
| }, n, [[0.3, 0.2, 0.3], [0.7, 0.85, 0.7]], rng), | |
| }, | |
| }; | |
| export const SHAPE_LIST = Object.entries(SHAPES).map(([id, s]) => ({ id, label: s.label })); | |
| /** | |
| * Build a gaussian cloud for a shape. Sigma is tied to the spacing implied by the | |
| * particle count so the kernels just overlap — the same reason a splat | |
| * reconstruction looks solid rather than like beads. | |
| */ | |
| export function makeAsset(shape = 'cube', count = 4200, seed = 7) { | |
| const rng = rngFrom(seed); | |
| const spec = SHAPES[shape] || SHAPES.cube; | |
| const pos = spec.make(rng, count); | |
| const n = pos.length / 3; | |
| const sigma = new Float32Array(n * 3); | |
| const base = 0.5 * Math.cbrt(0.02 / Math.max(1, n)); // ~volume per particle | |
| for (let i = 0; i < n; i++) { | |
| const s = base * (2.6 + rng() * 0.8); | |
| sigma[i * 3] = s; | |
| sigma[i * 3 + 1] = s; | |
| sigma[i * 3 + 2] = s; | |
| } | |
| // Colour is banded by height so deformation is legible: a uniform blob tells | |
| // you nothing about which part of it stretched. | |
| const color = new Float32Array(n * 3); | |
| let minY = Infinity, maxY = -Infinity; | |
| for (let i = 0; i < n; i++) { minY = Math.min(minY, pos[i * 3 + 1]); maxY = Math.max(maxY, pos[i * 3 + 1]); } | |
| for (let i = 0; i < n; i++) { | |
| const t = (pos[i * 3 + 1] - minY) / Math.max(1e-6, maxY - minY); | |
| color[i * 3] = 0.55 + 0.4 * t; | |
| color[i * 3 + 1] = 0.62 + 0.22 * Math.sin(t * 6.28); | |
| color[i * 3 + 2] = 0.95 - 0.45 * t; | |
| } | |
| return { positions: pos, sigma, color, count: n }; | |
| } | |