import bpy, sys, mathutils src, front, back, out = sys.argv[-4], sys.argv[-3], sys.argv[-2], sys.argv[-1] bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.gltf(filepath=src) for o in bpy.data.objects: if o.animation_data: o.animation_data_clear() if o.type == 'ARMATURE': for pb in o.pose.bones: pb.location=(0,0,0); pb.rotation_quaternion=(1,0,0,0); pb.scale=(1,1,1) bpy.context.view_layer.update() meshes = [o for o in bpy.data.objects if o.type == 'MESH' and 'Body' in o.name] or [o for o in bpy.data.objects if o.type == 'MESH'] m = max(meshes, key=lambda o: len(o.data.vertices)) # dominant mesh, not a helper marker mn = mathutils.Vector((1e9,)*3); mx = mathutils.Vector((-1e9,)*3) for v in m.bound_box: w = m.matrix_world @ mathutils.Vector(v) mn = mathutils.Vector(map(min, mn, w)); mx = mathutils.Vector(map(max, mx, w)) c = (mn+mx)/2; h = (mx-mn).z; scale = h*1.1 imgF = bpy.data.images.load(front); imgB = bpy.data.images.load(back) tgt = bpy.data.images.new('butcher_skin', 1024, 1024, alpha=False) mat = bpy.data.materials.new('bakeproj'); mat.use_nodes = True nt = mat.node_tree; nt.nodes.clear() def node(t, **kw): n = nt.nodes.new(t) for k, v in kw.items(): setattr(n, k, v) return n geo = node('ShaderNodeNewGeometry') sep = node('ShaderNodeSeparateXYZ'); nt.links.new(sep.inputs[0], geo.outputs['Position']) def axis_map(out_sock, center, flip=False): sub = node('ShaderNodeMath', operation='SUBTRACT'); sub.inputs[1].default_value = center nt.links.new(sub.inputs[0], out_sock) div = node('ShaderNodeMath', operation='DIVIDE'); div.inputs[1].default_value = scale nt.links.new(div.inputs[0], sub.outputs[0]) if flip: mul = node('ShaderNodeMath', operation='MULTIPLY'); mul.inputs[1].default_value = -1 nt.links.new(mul.inputs[0], div.outputs[0]); last = mul else: last = div add = node('ShaderNodeMath', operation='ADD'); add.inputs[1].default_value = 0.5 nt.links.new(add.inputs[0], last.outputs[0]) return add.outputs[0] uF = axis_map(sep.outputs['X'], c.x); vv = axis_map(sep.outputs['Z'], c.z) uB = axis_map(sep.outputs['X'], c.x, flip=True) cmbF = node('ShaderNodeCombineXYZ'); nt.links.new(cmbF.inputs[0], uF); nt.links.new(cmbF.inputs[1], vv) cmbB = node('ShaderNodeCombineXYZ'); nt.links.new(cmbB.inputs[0], uB); nt.links.new(cmbB.inputs[1], vv) texF = node('ShaderNodeTexImage'); texF.image = imgF; texF.extension = 'EXTEND' texB = node('ShaderNodeTexImage'); texB.image = imgB; texB.extension = 'EXTEND' nt.links.new(texF.inputs[0], cmbF.outputs[0]); nt.links.new(texB.inputs[0], cmbB.outputs[0]) sepN = node('ShaderNodeSeparateXYZ'); nt.links.new(sepN.inputs[0], geo.outputs['Normal']) lt = node('ShaderNodeMath', operation='LESS_THAN'); lt.inputs[1].default_value = 0.0 nt.links.new(lt.inputs[0], sepN.outputs['Y']) # n.y < 0 = front-facing mix = node('ShaderNodeMix', data_type='RGBA') nt.links.new(mix.inputs['Factor'], lt.outputs[0]) nt.links.new(mix.inputs[6], texB.outputs['Color']) # A = back nt.links.new(mix.inputs[7], texF.outputs['Color']) # B = front (factor 1 when n.y<0) # --- keep the original head ------------------------------------------------------------- # Front/back planar projection has no idea a head is round: side-facing skull polygons sample # whatever front-plate pixel sits at their x/z, which paints a second ghost face down the side # of the head. We're repainting CLOTHES, so above the neck we just keep the body's own texture. # Cutoff is a fraction of body height measured from the feet (~0.87 ≈ chin on a standing figure); # pass -1 to disable and bake the whole body. KEEP_HEAD_ABOVE = float(sys.argv[-5]) if len(sys.argv) >= 6 and sys.argv[-5].replace('.', '', 1).replace('-', '', 1).isdigit() else 0.87 orig_tex = None for slot in (m.data.materials[0] if m.data.materials else None,): if slot and slot.use_nodes: for n in slot.node_tree.nodes: if n.type == 'TEX_IMAGE' and n.image: orig_tex = n.image break if KEEP_HEAD_ABOVE > 0 and orig_tex is not None: neck_z = mn.z + (mx.z - mn.z) * KEEP_HEAD_ABOVE # original texture sampled through the mesh's REAL UVs (not the projection) uvmap = node('ShaderNodeUVMap') if m.data.uv_layers: uvmap.uv_map = m.data.uv_layers[0].name texO = node('ShaderNodeTexImage'); texO.image = orig_tex nt.links.new(texO.inputs[0], uvmap.outputs[0]) above = node('ShaderNodeMath', operation='GREATER_THAN'); above.inputs[1].default_value = neck_z nt.links.new(above.inputs[0], sep.outputs['Z']) headmix = node('ShaderNodeMix', data_type='RGBA') nt.links.new(headmix.inputs['Factor'], above.outputs[0]) nt.links.new(headmix.inputs[6], mix.outputs[2]) # below neck: the projected garment nt.links.new(headmix.inputs[7], texO.outputs['Color']) # above neck: the untouched original colour_out = headmix.outputs[2] print(f'keeping original head above z={neck_z:.3f} ({KEEP_HEAD_ABOVE:.2f} of height)') else: colour_out = mix.outputs[2] print('baking whole body (no head preserve)') bsdf = node('ShaderNodeBsdfDiffuse'); nt.links.new(bsdf.inputs['Color'], colour_out) outn = node('ShaderNodeOutputMaterial'); nt.links.new(outn.inputs['Surface'], bsdf.outputs[0]) baketex = node('ShaderNodeTexImage'); baketex.image = tgt; nt.nodes.active = baketex m.data.materials.clear(); m.data.materials.append(mat) sc = bpy.context.scene; sc.render.engine = 'CYCLES' sc.cycles.samples = 16; sc.cycles.device = 'CPU' bpy.ops.object.select_all(action='DESELECT'); m.select_set(True) bpy.context.view_layer.objects.active = m bpy.ops.object.bake(type='DIFFUSE', pass_filter={'COLOR'}, use_clear=True, margin=8) tgt.filepath_raw = out; tgt.file_format = 'PNG'; tgt.save() print('baked ->', out)