retrabalho muscular

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-11 22:02:24 -03:00
1 parent 086befc148
commit 5e6d870410
2 files changed
+256 -1

No files matched your search

+255
View File
@@ -106,6 +106,230 @@ function muscleColorVariant(baseHex, variantKey) {
return hslToHex(h + hueShift, s, Math.min(0.85, Math.max(0.25, l + lightShift))); return hslToHex(h + hueShift, s, Math.min(0.85, Math.max(0.25, l + lightShift)));
} }
// Tom mais escuro da MESMA cor do músculo — usado nos sulcos cosméticos
// (ver MUSCULAR_COSMETIC_GROOVES), pra parecer uma "dobra"/sombra na
// própria superfície do músculo em vez de um elemento estranho.
function darkenHex(hex, amount) {
const [h, s, l] = hexToHsl(hex);
return hslToHex(h, s, Math.max(0.06, l - amount));
}
// ---- Sulcos cosméticos (só peitoral/abdome) --------------------------
//
// Confirmamos (comparando com a exportação bruta E com o app oficial do
// Z-Anatomy ao vivo, incluindo a versão completa do projeto Unity, não só
// a amostra) que reto do abdome, serrátil anterior e oblíquo externo do
// abdome são cada um UMA malha lisa e contínua no dado de origem — sem as
// intersecções tendíneas ("tanquinho") nem as digitações nas costelas que
// o atlas de Moore desenha. Não existe uma versão mais detalhada pra
// buscar. A pedido do usuário, desenhamos essas linhas por cima da malha
// real como sulcos decorativos — pequenos prismas finos, um pouco à
// frente da superfície real — só para dar a leitura visual de divisão.
// Cada sulco carrega o MESMO slug do músculo real ao qual pertence (via
// extras), então: (1) o frontend não o esconde por "não catalogado" e
// (2) clicar nele seleciona e mostra o nome do músculo de verdade — não
// existe uma "sub-estrutura" nova, é só decoração por cima da mesma.
const MUSCULAR_COSMETIC_GROOVES = {
"rectus abdominis muscle": "bands",
"serratus anterior muscle": "digitations",
"external abdominal oblique muscle": "digitations",
};
function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }
function vecAdd(a, b) { return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }
function vecScale(a, s) { return [a[0] * s, a[1] * s, a[2] * s]; }
function vecCross(a, b) {
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
}
function vecLen(a) { return Math.hypot(a[0], a[1], a[2]); }
function vecNorm(a) {
const l = vecLen(a) || 1;
return [a[0] / l, a[1] / l, a[2] / l];
}
// Aplica um quaternion [x,y,z,w] a um vetor — usado só pra descobrir, em
// espaço de MUNDO, pra que lado cada eixo LOCAL do nó aponta (cada
// músculo do Z-Anatomy pode vir rotacionado de um jeito diferente).
function rotateByQuat(q, v) {
const [qx, qy, qz, qw] = q;
const tx = 2 * (qy * v[2] - qz * v[1]);
const ty = 2 * (qz * v[0] - qx * v[2]);
const tz = 2 * (qx * v[1] - qy * v[0]);
return [
v[0] + qw * tx + (qy * tz - qz * ty),
v[1] + qw * ty + (qz * tx - qx * tz),
v[2] + qw * tz + (qx * ty - qy * tx),
];
}
// Descobre qual eixo LOCAL (0=x,1=y,2=z) do nó aponta pra "cima" (mundo
// +Y) e qual aponta pra "frente" (mundo +Z) — cada músculo pode ter uma
// rotação de nó diferente (confirmamos isso comparando alguns músculos
// reais: um vem sem rotação, outro vem rotacionado -90° no eixo X).
function localAxisMapping(node) {
const q = node.getRotation();
const worldOf = [rotateByQuat(q, [1, 0, 0]), rotateByQuat(q, [0, 1, 0]), rotateByQuat(q, [0, 0, 1])];
let vertIdx = 0, vertSign = 1, best = -1;
for (let i = 0; i < 3; i++) {
const abs = Math.abs(worldOf[i][1]);
if (abs > best) { best = abs; vertIdx = i; vertSign = worldOf[i][1] >= 0 ? 1 : -1; }
}
let depthIdx = -1, depthSign = 1;
best = -1;
for (let i = 0; i < 3; i++) {
if (i === vertIdx) continue;
const abs = Math.abs(worldOf[i][2]);
if (abs > best) { best = abs; depthIdx = i; depthSign = worldOf[i][2] >= 0 ? 1 : -1; }
}
const lateralIdx = [0, 1, 2].find((i) => i !== vertIdx && i !== depthIdx);
return { vertIdx, vertSign, depthIdx, depthSign, lateralIdx };
}
function meshLocalBBox(mesh) {
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
for (const prim of mesh.listPrimitives()) {
const accessor = prim.getAttribute("POSITION");
if (!accessor) continue;
const arr = accessor.getArray();
const count = accessor.getCount();
for (let i = 0; i < count; i++) {
for (let axis = 0; axis < 3; axis++) {
const v = arr[i * 3 + axis];
if (v < min[axis]) min[axis] = v;
if (v > max[axis]) max[axis] = v;
}
}
}
return { min, max };
}
// Monta um "prisma fino" (caixa alongada) indo do ponto p0 ao p1, com
// espessura `thickness` no plano perpendicular ao segmento e
// `depthThickness` ao longo de `depthUnit` — é o sulco decorativo em si.
// Dupla face (setDoubleSided) evita se preocupar com a ordem de
// enrolamento exata de cada uma das 6 faces (irrelevante pra um detalhe
// decorativo pequeno).
function buildGrooveMesh(doc, material, p0, p1, thickness, depthThickness, depthUnit) {
const dir = vecSub(p1, p0);
const len = vecLen(dir);
if (len < 1e-6) return null;
const dirN = vecScale(dir, 1 / len);
let perp = vecCross(dirN, depthUnit);
if (vecLen(perp) < 1e-6) perp = vecCross(dirN, [1, 0, 0]);
perp = vecNorm(perp);
const halfT = thickness / 2;
const halfD = depthThickness / 2;
const corner = (pt, sPerp, sDepth) =>
vecAdd(vecAdd(pt, vecScale(perp, sPerp * halfT)), vecScale(depthUnit, sDepth * halfD));
const c = [
corner(p0, -1, -1), corner(p1, -1, -1), corner(p1, 1, -1), corner(p0, 1, -1),
corner(p0, -1, 1), corner(p1, -1, 1), corner(p1, 1, 1), corner(p0, 1, 1),
];
const negDepth = vecScale(depthUnit, -1);
const negPerp = vecScale(perp, -1);
const negDir = vecScale(dirN, -1);
const faces = [
{ idx: [0, 1, 2, 3], n: negDepth },
{ idx: [7, 6, 5, 4], n: depthUnit },
{ idx: [4, 5, 1, 0], n: negPerp },
{ idx: [3, 2, 6, 7], n: perp },
{ idx: [0, 3, 7, 4], n: negDir },
{ idx: [1, 5, 6, 2], n: dirN },
];
const positions = [];
const normals = [];
const indices = [];
for (const face of faces) {
const base = positions.length / 3;
for (const i of face.idx) {
positions.push(...c[i]);
normals.push(...face.n);
}
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
}
const posAccessor = doc.createAccessor().setType("VEC3").setArray(new Float32Array(positions));
const normAccessor = doc.createAccessor().setType("VEC3").setArray(new Float32Array(normals));
const idxAccessor = doc.createAccessor().setType("SCALAR").setArray(new Uint16Array(indices));
const primitive = doc
.createPrimitive()
.setAttribute("POSITION", posAccessor)
.setAttribute("NORMAL", normAccessor)
.setIndices(idxAccessor)
.setMaterial(material);
return doc.createMesh().addPrimitive(primitive);
}
// Adiciona os sulcos decorativos como filhos do próprio nó do músculo —
// herdam a transformação do pai automaticamente, então as coordenadas
// aqui usam o MESMO espaço local da malha real (o bounding box calculado
// em cima dela). `slug` é o do músculo real: o sulco não é uma estrutura
// própria, só decoração com a MESMA identidade.
function addCosmeticGrooves(doc, targetNode, kind, material, slug) {
const mesh = targetNode.getMesh();
if (!mesh) return;
const { min, max } = meshLocalBBox(mesh);
const { vertIdx, vertSign, depthIdx, depthSign, lateralIdx } = localAxisMapping(targetNode);
const vRange = max[vertIdx] - min[vertIdx];
const dRange = max[depthIdx] - min[depthIdx];
const lRange = max[lateralIdx] - min[lateralIdx];
if (vRange <= 0 || dRange <= 0 || lRange <= 0) return;
const vertAt = (frac) => (vertSign > 0 ? min[vertIdx] + frac * vRange : max[vertIdx] - frac * vRange);
const lateralAt = (frac) => min[lateralIdx] + frac * lRange;
const frontDepth = (depthSign > 0 ? max[depthIdx] : min[depthIdx]) + depthSign * dRange * 0.08;
const assemble = (lateralVal, vertVal, depthVal) => {
const p = [0, 0, 0];
p[lateralIdx] = lateralVal;
p[vertIdx] = vertVal;
p[depthIdx] = depthVal;
return p;
};
const depthUnit = assemble(0, 0, 1).map((v, i) => (i === depthIdx ? depthSign : 0));
const grooveMeshes = [];
if (kind === "bands") {
// Intersecções tendíneas do reto do abdome — aproximação: 3 faixas
// horizontais igualmente espaçadas (a posição real varia de pessoa
// pra pessoa, então não há um "certo" a buscar aqui).
for (const frac of [0.3, 0.55, 0.78]) {
const v = vertAt(frac);
const p0 = assemble(lateralAt(0.12), v, frontDepth);
const p1 = assemble(lateralAt(0.88), v, frontDepth);
const m = buildGrooveMesh(doc, material, p0, p1, vRange * 0.03, dRange * 0.2, depthUnit);
if (m) grooveMeshes.push(m);
}
} else if (kind === "digitations") {
// Digitações (serrátil anterior / oblíquo externo nas costelas) —
// aproximação: marcas diagonais CURTAS (relativas ao próprio espaço
// entre elas, não ao tamanho total do músculo — a malha real desses
// dois costuma ter um bounding box bem maior que a parte visível por
// fora, então usar frações grandes do bbox inteiro fazia as marcas
// saírem enormes, passando da silhueta do corpo).
const count = 4;
const step = lRange / count;
for (let i = 0; i < count; i++) {
const latCenter = lateralAt((i + 0.5) / count);
const dx = step * 0.14;
const vBase = vertAt(0.72);
const vTop = vertAt(0.72 + (vRange > 0 ? Math.min(0.1, (step * 0.28) / vRange) : 0.1));
const p0 = assemble(latCenter - dx, vBase, frontDepth);
const p1 = assemble(latCenter + dx, vTop, frontDepth);
const m = buildGrooveMesh(doc, material, p0, p1, step * 0.22, dRange * 0.2, depthUnit);
if (m) grooveMeshes.push(m);
}
}
grooveMeshes.forEach((mesh, i) => {
const grooveNode = doc.createNode(`${targetNode.getName()} groove ${i}`).setMesh(mesh).setExtras({ slug });
targetNode.addChild(grooveNode);
});
}
const COMBINING_MARKS = new RegExp("[\\u0300-\\u036f]", "g"); const COMBINING_MARKS = new RegExp("[\\u0300-\\u036f]", "g");
function slugify(s) { function slugify(s) {
@@ -361,6 +585,7 @@ for (const { fbx, key } of SOURCES) {
const nameCounts = new Map(); // dedupe de nomes brutos duplicados no MESMO arquivo const nameCounts = new Map(); // dedupe de nomes brutos duplicados no MESMO arquivo
const includedBones = new Set(); // dedupe de osso+lado no sistema esquelético const includedBones = new Set(); // dedupe de osso+lado no sistema esquelético
const cosmeticGrooveTargets = []; // {node, kind, colorHex, slug} — só sistema muscular
let countThisFile = 0; let countThisFile = 0;
function visit(node) { function visit(node) {
@@ -446,6 +671,10 @@ for (const { fbx, key } of SOURCES) {
// GLTFLoader, diferentes do nome original — extras não sofre isso. // GLTFLoader, diferentes do nome original — extras não sofre isso.
node.setExtras({ slug }); node.setExtras({ slug });
if (key === "muscular" && muscleColorKey && MUSCULAR_COSMETIC_GROOVES[muscleColorKey]) {
cosmeticGrooveTargets.push({ node, kind: MUSCULAR_COSMETIC_GROOVES[muscleColorKey], slug, muscleColorKey });
}
manifest.push({ manifest.push({
system: systemSlug, system: systemSlug,
slug, slug,
@@ -509,6 +738,32 @@ for (const { fbx, key } of SOURCES) {
// com bits de quantização mais altos. // com bits de quantização mais altos.
); );
// Sulcos cosméticos (ver MUSCULAR_COSMETIC_GROOVES) — adicionados DEPOIS
// do weld/simplify/prune pra não serem afetados pela simplificação
// (são poucos triângulos, gerados por nós, não precisam disso) e não
// atrapalharem o cálculo do bounding box (que precisa da malha REAL,
// sem os sulcos, já convertida — os sulcos usam side-effect de node
// rotation/bbox calculados a partir dela).
if (cosmeticGrooveTargets.length > 0) {
const grooveMaterialCache = new Map();
for (const { node, kind, slug, muscleColorKey } of cosmeticGrooveTargets) {
if (!grooveMaterialCache.has(muscleColorKey)) {
const baseHex = systemBySlug.get("muscular")?.colorHex ?? "#cccccc";
const muscleHex = muscleColorVariant(baseHex, muscleColorKey);
const grooveHex = darkenHex(muscleHex, 0.16);
const mat = doc
.createMaterial(`groove::${muscleColorKey}`)
.setBaseColorFactor(hexToRgb(grooveHex))
.setRoughnessFactor(0.7)
.setMetallicFactor(0.05)
.setDoubleSided(true);
grooveMaterialCache.set(muscleColorKey, mat);
}
addCosmeticGrooves(doc, node, kind, grooveMaterialCache.get(muscleColorKey), slug);
}
console.log(` + sulcos cosméticos adicionados em ${cosmeticGrooveTargets.length} músculo(s) do tronco.`);
}
const outPath = path.join(MODELS_OUT_DIR, target.modelFile); const outPath = path.join(MODELS_OUT_DIR, target.modelFile);
await io.write(outPath, doc); await io.write(outPath, doc);
console.log(` ✓ ${target.modelFile} salvo em seed/models/`); console.log(` ✓ ${target.modelFile} salvo em seed/models/`);
+1 -1
View File
@@ -428,7 +428,7 @@ const EXACT_PT = {
"splenius capitis muscle": "Esplênio da Cabeça", "splenius capitis muscle": "Esplênio da Cabeça",
"splenius colli muscle": "Esplênio do Pescoço", "splenius colli muscle": "Esplênio do Pescoço",
"sternocleidomastoid muscle": "Esternocleidomastóideo", "sternocleidomastoid muscle": "Esternocleidomastóideo",
"sternocostal head of pectoralis major muscle": "Cabeça Esternocostal do Peitoral Maior", "sternocostal head of pectoralis major muscle": "Peitoral Maior",
"sternohyoid muscle": "Esternoióideo", "sternohyoid muscle": "Esternoióideo",
"sternothyroid muscle": "Esternotireóideo", "sternothyroid muscle": "Esternotireóideo",
"straight part of cricothyroid muscle": "Porção Reta do Cricotireóideo", "straight part of cricothyroid muscle": "Porção Reta do Cricotireóideo",