retrabalho muscular
This commit is contained in:
1 parent
086befc148
commit
5e6d870410
2 files changed
+256
-1
No files matched your search
@@ -106,6 +106,230 @@ function muscleColorVariant(baseHex, variantKey) {
|
||||
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");
|
||||
|
||||
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 includedBones = new Set(); // dedupe de osso+lado no sistema esquelético
|
||||
const cosmeticGrooveTargets = []; // {node, kind, colorHex, slug} — só sistema muscular
|
||||
let countThisFile = 0;
|
||||
|
||||
function visit(node) {
|
||||
@@ -446,6 +671,10 @@ for (const { fbx, key } of SOURCES) {
|
||||
// GLTFLoader, diferentes do nome original — extras não sofre isso.
|
||||
node.setExtras({ slug });
|
||||
|
||||
if (key === "muscular" && muscleColorKey && MUSCULAR_COSMETIC_GROOVES[muscleColorKey]) {
|
||||
cosmeticGrooveTargets.push({ node, kind: MUSCULAR_COSMETIC_GROOVES[muscleColorKey], slug, muscleColorKey });
|
||||
}
|
||||
|
||||
manifest.push({
|
||||
system: systemSlug,
|
||||
slug,
|
||||
@@ -509,6 +738,32 @@ for (const { fbx, key } of SOURCES) {
|
||||
// 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);
|
||||
await io.write(outPath, doc);
|
||||
console.log(` ✓ ${target.modelFile} salvo em seed/models/`);
|
||||
|
||||
@@ -428,7 +428,7 @@ const EXACT_PT = {
|
||||
"splenius capitis muscle": "Esplênio da Cabeça",
|
||||
"splenius colli muscle": "Esplênio do Pescoço",
|
||||
"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",
|
||||
"sternothyroid muscle": "Esternotireóideo",
|
||||
"straight part of cricothyroid muscle": "Porção Reta do Cricotireóideo",
|
||||
|
||||
Reference in new issue
Block a user