novo corpo

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-14 22:42:46 -03:00
1 parent 270cdbb965
commit 12342f442d
5 files changed
+230 -125

No files matched your search

@@ -0,0 +1,56 @@
# Boneco realista de Planos Anatômicos
`frontend/public/mannequin/body-male-realistic.glb` (usado pelo componente
`Mannequin` em `frontend/src/components/viewer/Mannequin.tsx`, exclusivo da
página `/planos-anatomicos`) não é um asset do Z-Anatomy — é um corpo humano
esculpido de verdade (não geometria procedural tipo caixa/cilindro), mas
extraído fora do dataset de anatomia real por dois motivos: (1) o Z-Anatomy é
pesado demais pra um boneco decorativo que só serve pra indicar planos/eixos,
e (2) essa página nunca precisou de fidelidade anatômica interna (músculos,
órgãos) — só de uma silhueta externa correta.
## Fonte e licença
- **Origem**: pacote oficial **Human Base Meshes** da Blender Studio
(equipe por trás dos "Blender Open Movies"), distribuído gratuitamente em
<https://www.blender.org/download/demo-files/#assets>.
- **Licença**: CC0 (domínio público) — sem exigência de atribuição, mas
citamos a fonte aqui por transparência/manutenção futura.
- **Malha usada**: objeto `GEO-body_male_realistic` dentro do bundle
v1.4.1 (`human_base_meshes_bundle.blend`), nível 1 do modificador
Multires (~42k triângulos antes da otimização final — ver abaixo).
## O que foi alterado
A malha original vem numa pose relaxada tipo "A" (braços afastados do
corpo em ~20-23°), não em posição anatômica (braços estendidos ao lado do
corpo). Como o arquivo não tem armature/rig, a repose foi feita rotacionando
os vértices do braço em torno do ombro (ângulo medido a partir da própria
malha, não chutado), com uma transição suave (`smoothstep`) na axila pra
não deixar costura — ver `repose_and_export.py`.
## Como reproduzir
Precisa do Blender (`brew install --cask blender` no macOS, ou o pacote da
distro no Linux) e do Node do `data-pipeline` já instalado (`npm install`).
```sh
# 1. Baixar o bundle oficial (~50MB)
curl -L -o /tmp/human-base-meshes.zip \
"https://mirror.blender.org/demo/asset-bundles/human-base-meshes/human-base-meshes-bundle-v1.4.1.zip"
unzip -o /tmp/human-base-meshes.zip -d /tmp/hbm
# 2. Reposicionar os braços e exportar o GLB bruto
blender --background \
"/tmp/hbm/human-base-meshes-bundle-v1.4.1/human_base_meshes_bundle.blend" \
--python data-pipeline/scripts/mannequin-asset/repose_and_export.py
# 3. Otimizar (weld + dedup + quantize + remover UV não usada — sem
# textura, a malha usa só uma cor sólida) — reduz de ~3.3MB pra ~1.3MB
cd data-pipeline
node scripts/mannequin-asset/optimize.mjs
# 4. Copiar pro frontend
cp /tmp/mannequin-realistic.optimized.glb \
../frontend/public/mannequin/body-male-realistic.glb
```
@@ -0,0 +1,29 @@
// Otimiza o GLB bruto exportado pelo repose_and_export.py: solda vértices
// duplicados, remove dados não usados e quantiza as posições/normais pra
// menos bytes por vértice — sem precisar de um decoder novo no frontend
// (KHR_mesh_quantization já é suportado nativamente pelo GLTFLoader do
// three.js). A malha não tem textura (só cor sólida), então a UV é
// descartada antes de tudo — ela não serve pra nada e ainda impedia o
// quantize de comprimir aquele atributo.
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { prune, dedup, weld, quantize } from "@gltf-transform/functions";
const INPUT = "/tmp/mannequin-realistic.glb";
const OUTPUT = "/tmp/mannequin-realistic.optimized.glb";
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const doc = await io.read(INPUT);
for (const mesh of doc.getRoot().listMeshes()) {
for (const prim of mesh.listPrimitives()) {
if (prim.getAttribute("TEXCOORD_0")) {
prim.setAttribute("TEXCOORD_0", null);
}
}
}
await doc.transform(weld({ tolerance: 0.0001 }), dedup(), quantize(), prune());
await io.write(OUTPUT, doc);
console.log(`Otimizado: ${INPUT} -> ${OUTPUT}`);
@@ -0,0 +1,113 @@
"""Reposiciona os bracos do 'GEO-body_male_realistic' (bundle Human Base
Meshes da Blender Studio, CC0) da pose relaxada original para posicao
anatomica (bracos retos ao lado do corpo), e exporta pra GLB.
Rodar de dentro do Blender, sem interface:
blender --background human_base_meshes_bundle.blend \
--python repose_and_export.py
A malha nao tem armature, entao a repose e feita rotacionando os vertices
do braco em torno de um pivo no ombro. O angulo de rotacao e MEDIDO na
propria malha (distancia/posicao do vertice mais distante do ombro dentro
da faixa de altura do braco), nao e um valor chutado, entao continua
funcionando mesmo que uma versao futura do bundle mude a pose de origem.
Uma janela suave (smoothstep) evita costura na axila.
"""
import math
import bpy
BODY_OBJECT = "GEO-body_male_realistic"
EYE_OBJECTS = ["GEO-body_male_realistic.eye.L", "GEO-body_male_realistic.eye.R"]
MULTIRES_LEVEL = 1 # ~42k triangulos: mantem a definicao muscular visivel a distancia normal de camera, bem mais leve que o nivel 3 (~677k) do arquivo original
OUTPUT_PATH = "/tmp/mannequin-realistic.glb"
SKIN_COLOR = (0.56, 0.64, 0.78, 1.0) # mesmo tom usado no Mannequin.tsx anterior
def smoothstep(edge0, edge1, x):
t = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0)))
return t * t * (3 - 2 * t)
def repose_arms(obj):
mesh = obj.data
zs = [v.co.z for v in mesh.vertices]
z_min, z_max = min(zs), max(zs)
height = z_max - z_min
centerline_x = sum(v.co.x for v in mesh.vertices) / len(mesh.vertices)
shoulder_z = z_min + 0.84 * height
# comfortably above o alargamento do pe/tornozelo, abaixo de onde a mao
# fica nesta pose relaxada — deixa uma folga clara entre perna e braço
arm_low_z = z_min + 0.30 * height
torso_half_width = 0.20 # metade da largura biacromial aproximada, nesta escala
for side in (1, -1):
pivot_x = centerline_x + side * torso_half_width * 0.85
candidates = [
v
for v in mesh.vertices
if side * (v.co.x - centerline_x) > torso_half_width
and arm_low_z < v.co.z < shoulder_z - 0.02
]
far = max(
candidates,
key=lambda v: (v.co.x - pivot_x) ** 2 + (v.co.z - shoulder_z) ** 2,
)
angle = math.atan2(far.co.x - pivot_x, -(far.co.z - shoulder_z))
for v in mesh.vertices:
dx = v.co.x - centerline_x
if side * dx <= 0 or v.co.z >= shoulder_z + 0.03:
continue
weight_x = smoothstep(torso_half_width * 0.7, torso_half_width * 1.15, abs(dx))
weight_z = smoothstep(arm_low_z - 0.08, arm_low_z + 0.07, v.co.z)
weight = weight_x * weight_z
if weight <= 0.001:
continue
rot = -weight * angle
cos_a, sin_a = math.cos(rot), math.sin(rot)
px, pz = v.co.x - pivot_x, v.co.z - shoulder_z
v.co.x = pivot_x + (px * cos_a - pz * sin_a)
v.co.z = shoulder_z + (px * sin_a + pz * cos_a)
mesh.update()
def main():
body = bpy.data.objects[BODY_OBJECT]
repose_arms(body)
for mod in body.modifiers:
if mod.type == "MULTIRES":
mod.levels = MULTIRES_LEVEL
mat = bpy.data.materials.new("MannequinSkin")
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = SKIN_COLOR
mat.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.6
targets = [body] + [bpy.data.objects[n] for n in EYE_OBJECTS if n in bpy.data.objects]
for obj in targets:
obj.data.materials.clear()
obj.data.materials.append(mat)
bpy.ops.object.select_all(action="DESELECT")
for obj in targets:
obj.select_set(True)
bpy.context.view_layer.objects.active = body
bpy.ops.export_scene.gltf(
filepath=OUTPUT_PATH,
export_format="GLB",
use_selection=True,
export_apply=True,
export_yup=True,
)
print(f"Exportado para {OUTPUT_PATH}")
main()