tegumentar3

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-10 19:42:05 -03:00
1 parent 063bd629c2
commit 9f2fe75ff6
11 files changed
+221 -537

No files matched your search

-118
View File
@@ -1,118 +0,0 @@
import { Document, NodeIO } from "@gltf-transform/core";
import { readFileSync, mkdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { buildGeometry } from "./geometry.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const manifestPath = path.resolve(
__dirname,
process.argv[2] ?? "../seed/manifest-tegumentar.json",
);
const outDir = path.resolve(__dirname, "../seed/models");
mkdirSync(outDir, { recursive: true });
const { structures } = JSON.parse(readFileSync(manifestPath, "utf-8"));
function eulerToQuat([x, y, z]) {
const cx = Math.cos(x / 2), sx = Math.sin(x / 2);
const cy = Math.cos(y / 2), sy = Math.sin(y / 2);
const cz = Math.cos(z / 2), sz = Math.sin(z / 2);
return [
sx * cy * cz - cx * sy * sz,
cx * sy * cz + sx * cy * sz,
cx * cy * sz - sx * sy * cz,
cx * cy * cz + sx * sy * sz,
];
}
function hexToRgb(hex) {
const n = parseInt(hex.replace("#", ""), 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1];
}
const bySystem = new Map();
for (const s of structures) {
if (!bySystem.has(s.system)) bySystem.set(s.system, []);
bySystem.get(s.system).push(s);
}
const systemColors = {
esqueletico: "#e8e2d0",
muscular: "#b5433a",
nervoso: "#f2c14e",
cardiovascular: "#c1121f",
respiratorio: "#4f7cff",
tegumentar: "#e0a96d",
};
for (const [systemSlug, items] of bySystem) {
const doc = new Document();
const buffer = doc.createBuffer();
const scene = doc.createScene(systemSlug);
doc.getRoot().setDefaultScene(scene);
// Cada item pode pedir uma cor própria (opts.color) pra se distinguir dos
// demais (ex: glândula amarela, vaso vermelho/azul, pelo castanho) em vez
// de todo o sistema sair de uma cor só — cache por hex evita recriar
// material repetido.
const materialCache = new Map();
function materialFor(hex) {
if (materialCache.has(hex)) return materialCache.get(hex);
const mat = doc
.createMaterial(hex)
.setBaseColorFactor(hexToRgb(hex))
.setRoughnessFactor(0.6)
.setMetallicFactor(0.05)
// O bloco de pele usa caixas com a face da frente removida (ver
// openFrontBox em geometry.mjs) pra dar pra ver o conteúdo por
// dentro — sem dupla face, as paredes de trás/lados ficariam
// invisíveis quando vistas "de dentro" da caixa aberta.
.setDoubleSided(true);
materialCache.set(hex, mat);
return mat;
}
const defaultHex = systemColors[systemSlug] ?? "#cccccc";
for (const item of items) {
const material = materialFor(item.color ?? defaultHex);
const geo = buildGeometry(item.shape);
const posAccessor = doc
.createAccessor()
.setType("VEC3")
.setArray(geo.positions)
.setBuffer(buffer);
const normAccessor = doc
.createAccessor()
.setType("VEC3")
.setArray(geo.normals)
.setBuffer(buffer);
const idxAccessor = doc
.createAccessor()
.setType("SCALAR")
.setArray(geo.indices)
.setBuffer(buffer);
const prim = doc
.createPrimitive()
.setAttribute("POSITION", posAccessor)
.setAttribute("NORMAL", normAccessor)
.setIndices(idxAccessor)
.setMaterial(material);
const mesh = doc.createMesh(item.meshName).addPrimitive(prim);
const node = doc
.createNode(item.meshName)
.setMesh(mesh)
.setTranslation(item.position)
.setRotation(eulerToQuat(item.rotation ?? [0, 0, 0]))
.setExtras({ slug: item.slug });
scene.addChild(node);
}
const io = new NodeIO();
const outFile = path.join(outDir, `${systemSlug}.glb`);
await io.write(outFile, doc);
console.log(`✓ ${outFile} (${items.length} estruturas)`);
}
-146
View File
@@ -1,146 +0,0 @@
// Minimal parametric mesh generators (box/sphere/cylinder), dependency-free.
// Returns { positions: Float32Array, normals: Float32Array, indices: Uint32Array }.
// skipNormals: normais (como "0,-1,0") das faces a NÃO gerar — usado pelo
// bloco de pele em corte (openFrontBox abaixo) pra deixar a face da frente
// aberta, como uma caixa de diorama, revelando o conteúdo interno em vez de
// escondê-lo atrás de uma parede opaca.
export function box(w, h, d, skipNormals = []) {
const x = w / 2, y = h / 2, z = d / 2;
const positions = [];
const normals = [];
const indices = [];
const faces = [
// [normal, 4 corners CCW]
[[0, 0, 1], [[-x, -y, z], [x, -y, z], [x, y, z], [-x, y, z]]],
[[0, 0, -1], [[x, -y, -z], [-x, -y, -z], [-x, y, -z], [x, y, -z]]],
[[0, 1, 0], [[-x, y, z], [x, y, z], [x, y, -z], [-x, y, -z]]],
[[0, -1, 0], [[-x, -y, -z], [x, -y, -z], [x, -y, z], [-x, -y, z]]],
[[1, 0, 0], [[x, -y, z], [x, -y, -z], [x, y, -z], [x, y, z]]],
[[-1, 0, 0], [[-x, -y, -z], [-x, -y, z], [-x, y, z], [-x, y, -z]]],
];
const skip = new Set(skipNormals.map((n) => n.join(",")));
for (const [n, corners] of faces) {
if (skip.has(n.join(","))) continue;
const base = positions.length / 3;
for (const c of corners) {
positions.push(...c);
normals.push(...n);
}
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
}
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
indices: new Uint32Array(indices),
};
}
// Caixa "de diorama": todas as faces normais, exceto a de baixo em Y
// (nosso eixo "frente/trás" no bloco de pele) — deixa a frente aberta pra
// dar pra ver o conteúdo (folículos, glândulas, vasos) por dentro, em vez
// de uma parede opaca escondendo tudo.
export function openFrontBox(w, h, d) {
return box(w, h, d, [[0, -1, 0]]);
}
export function sphere(radius, widthSegments = 16, heightSegments = 12) {
const positions = [];
const normals = [];
const indices = [];
for (let y = 0; y <= heightSegments; y++) {
const v = y / heightSegments;
const theta = v * Math.PI;
for (let x = 0; x <= widthSegments; x++) {
const u = x / widthSegments;
const phi = u * Math.PI * 2;
const nx = -Math.cos(phi) * Math.sin(theta);
const ny = Math.cos(theta);
const nz = Math.sin(phi) * Math.sin(theta);
positions.push(radius * nx, radius * ny, radius * nz);
normals.push(nx, ny, nz);
}
}
const rowSize = widthSegments + 1;
for (let y = 0; y < heightSegments; y++) {
for (let x = 0; x < widthSegments; x++) {
const a = y * rowSize + x;
const b = a + rowSize;
indices.push(a, b, a + 1, b, b + 1, a + 1);
}
}
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
indices: new Uint32Array(indices),
};
}
export function cylinder(radiusTop, radiusBottom, height, radialSegments = 12) {
const positions = [];
const normals = [];
const indices = [];
const halfH = height / 2;
// Side wall.
for (let y = 0; y <= 1; y++) {
const r = y === 0 ? radiusTop : radiusBottom;
const py = y === 0 ? halfH : -halfH;
for (let x = 0; x <= radialSegments; x++) {
const u = (x / radialSegments) * Math.PI * 2;
const nx = Math.sin(u);
const nz = Math.cos(u);
positions.push(r * nx, py, r * nz);
const slope = (radiusBottom - radiusTop) / height;
const len = Math.sqrt(1 + slope * slope);
normals.push(nx / len, -slope / len, nz / len);
}
}
const rowSize = radialSegments + 1;
for (let x = 0; x < radialSegments; x++) {
const a = x, b = a + rowSize;
indices.push(a, b, a + 1, b, b + 1, a + 1);
}
// Caps.
function cap(y, r, py, flip) {
const base = positions.length / 3;
const ny = flip ? 1 : -1;
positions.push(0, py, 0);
normals.push(0, ny, 0);
for (let x = 0; x <= radialSegments; x++) {
const u = (x / radialSegments) * Math.PI * 2;
positions.push(r * Math.sin(u), py, r * Math.cos(u));
normals.push(0, ny, 0);
}
for (let x = 0; x < radialSegments; x++) {
if (flip) indices.push(base, base + x + 2, base + x + 1);
else indices.push(base, base + x + 1, base + x + 2);
}
}
if (radiusTop > 0) cap(0, radiusTop, halfH, true);
if (radiusBottom > 0) cap(1, radiusBottom, -halfH, false);
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
indices: new Uint32Array(indices),
};
}
export function buildGeometry(shape) {
switch (shape.type) {
case "box": return box(shape.w, shape.h, shape.d);
case "openFrontBox": return openFrontBox(shape.w, shape.h, shape.d);
case "sphere": return sphere(shape.r);
case "cylinder": return cylinder(shape.rt, shape.rb, shape.h);
default: throw new Error(`Unknown shape type: ${shape.type}`);
}
}