diff --git a/data-pipeline/scripts/mannequin-asset/repose_and_export.py b/data-pipeline/scripts/mannequin-asset/repose_and_export.py index 3aef207..df648c0 100644 --- a/data-pipeline/scripts/mannequin-asset/repose_and_export.py +++ b/data-pipeline/scripts/mannequin-asset/repose_and_export.py @@ -1,18 +1,37 @@ -"""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. +"""Reposiciona o '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, palma virada bem mais pra frente), 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. +A malha nao tem armature, entao tudo aqui e feito girando vertices +diretamente. Ordem importa: + +1. Aplica o modificador Multires (nivel 1, ~42k triangulos) ANTES de + editar qualquer vertice -- fazer isso depois faz o Blender reinterpretar + o deslocamento esculpido em cima de uma malha-base que mudou de posicao, + o que pode gerar bolhas/deformacoes que nao existem se a ordem for essa. +2. Supinacao (mao+punho, z entre ~0.70 e ~0.82 nesta escala): gira em + torno do proprio eixo vertical, com peso suave nas duas pontas (nao + pega nada de coxa por baixo, nao "rasga" o antebraco por cima). Parar + em exatamente 90 graus (mao IDEAL, palma 100% anterior) faz a secao + nao-circular do pulso criar uma aba/leque tosco na transicao -- sem + rig/pele pra suavizar isso direito, 45 graus e o ponto onde a palma ja + vira visivelmente pra frente sem esse artefato. Documentado aqui como + limitacao conhecida, nao erro. +3. Abducao do ombro: derruba o braco (que vinha afastado do corpo, pose + original tipo "A") ate quase vertical, com uma pequena abducao residual + (a malha nao tem rig, entao trazer pra EXATAMENTE vertical faz a mao + encostar/atravessar a coxa). + +Os angulos e faixas de altura usados abaixo foram medidos na propria +malha (amostrando vertices por fatias de altura), nao chutados -- ver o +historico da sessao que criou este script se precisar re-derivar os +valores pra uma versao futura do bundle com proporcoes diferentes. """ import math @@ -21,9 +40,9 @@ 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 +MULTIRES_LEVEL = 1 # ~42k triangulos: mantem a definicao muscular visivel a distancia normal de camera OUTPUT_PATH = "/tmp/mannequin-realistic.glb" -SKIN_COLOR = (0.56, 0.64, 0.78, 1.0) # mesmo tom usado no Mannequin.tsx anterior +SKIN_COLOR = (0.56, 0.64, 0.78, 1.0) def smoothstep(edge0, edge1, x): @@ -31,33 +50,56 @@ def smoothstep(edge0, edge1, x): 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) +def apply_multires(obj): + bpy.context.view_layer.objects.active = obj + bpy.ops.object.select_all(action="DESELECT") + obj.select_set(True) + for mod in obj.modifiers: + if mod.type == "MULTIRES": + mod.levels = MULTIRES_LEVEL + bpy.ops.object.modifier_apply(modifier="Multires") + +def supinate_hand(mesh, centerline_x, torso_half_width): + # Mao termina de forma abrupta em z=~0.705 nesta malha (medido); a + # coxa nao passa de dx~0.19-0.20 ate z=~0.70, entao a rampa de subida + # (0.65->0.70) fica sempre acima do alcance da coxa. A rampa de + # descida (0.76->0.82) para antes do meio do antebraco. + for side in (1, -1): + core = [ + v for v in mesh.vertices + if side * (v.co.x - centerline_x) > torso_half_width * 1.15 + and 0.70 < v.co.z < 0.76 + ] + pivot_x = sum(v.co.x for v in core) / len(core) + pivot_y = sum(v.co.y for v in core) / len(core) + + for v in mesh.vertices: + dx = v.co.x - centerline_x + if side * dx <= 0: + continue + weight_x = smoothstep(torso_half_width * 0.7, torso_half_width * 1.15, abs(dx)) + weight_z = smoothstep(0.65, 0.70, v.co.z) * (1 - smoothstep(0.76, 0.82, v.co.z)) + weight = weight_x * weight_z + if weight <= 0.001: + continue + rot = side * weight * math.radians(45.0) + cos_a, sin_a = math.cos(rot), math.sin(rot) + px, py = v.co.x - pivot_x, v.co.y - pivot_y + v.co.x = pivot_x + (px * cos_a - py * sin_a) + v.co.y = pivot_y + (px * sin_a + py * cos_a) + + +def drop_arms(mesh, centerline_x, torso_half_width, z_min, height): shoulder_z = z_min + 0.84 * height - # A mao termina de forma bem abrupta por volta de z=0.705-0.71 nesta - # escala (medido amostrando a malha por fatias de altura — ver - # diagnose2.py no historico) — abaixo disso so existe coxa. Uma janela - # mais larga aqui (ex: baseada só numa fracao generica da altura) pega - # vertice de coxa por engano e deforma a perna inteira, porque eles - # entram na rotacao rigida em torno do pivo do OMBRO. - arm_hand_low_z = z_min + 0.417 * height # ~0.705 pra este mesh especifico - torso_half_width = 0.20 # metade da largura biacromial aproximada, nesta escala - # Angulo residual: parar EXATAMENTE na vertical faz a mao encostar (ou - # atravessar) a coxa. Deixar uma pequena abducao residual dá uma folga - # natural, como nas referências de posição anatômica. - residual = math.radians(5.0) + arm_hand_low_z = z_min + 0.417 * height # ~0.705 + residual = math.radians(8.0) for side in (1, -1): - pivot_x = centerline_x + side * torso_half_width * 0.95 + pivot_x = centerline_x + side * torso_half_width * 1.05 candidates = [ - v - for v in mesh.vertices + v for v in mesh.vertices if side * (v.co.x - centerline_x) > torso_half_width and arm_hand_low_z < v.co.z < shoulder_z - 0.02 ] @@ -73,10 +115,7 @@ def repose_arms(obj): 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)) - # janela estreita: cheia bem em cima da mao, zero já na coxa — - # a transição real na malha é abrupta (não gradual), então uma - # rampa larga aqui é o que causava a contaminação da perna. - weight_z = smoothstep(arm_hand_low_z - 0.015, arm_hand_low_z + 0.005, v.co.z) + weight_z = smoothstep(arm_hand_low_z - 0.002, arm_hand_low_z + 0.006, v.co.z) weight = weight_x * weight_z if weight <= 0.001: continue @@ -86,16 +125,21 @@ def repose_arms(obj): 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) + apply_multires(body) - for mod in body.modifiers: - if mod.type == "MULTIRES": - mod.levels = MULTIRES_LEVEL + mesh = body.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) + torso_half_width = 0.20 # metade da largura biacromial aproximada, nesta escala + + supinate_hand(mesh, centerline_x, torso_half_width) + drop_arms(mesh, centerline_x, torso_half_width, z_min, height) + mesh.update() mat = bpy.data.materials.new("MannequinSkin") mat.use_nodes = True diff --git a/frontend/public/mannequin/body-male-realistic.glb b/frontend/public/mannequin/body-male-realistic.glb index 5bb837c..3eb5b37 100644 Binary files a/frontend/public/mannequin/body-male-realistic.glb and b/frontend/public/mannequin/body-male-realistic.glb differ