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)));
|
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/`);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user