Spaces:
Paused
Paused
File size: 6,798 Bytes
381de33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """Bake the front reference into the existing UV atlas, retaining unseen source texture."""
import argparse
from pathlib import Path
import sys
import json
import bpy
import numpy as np
from mathutils import Vector
from mathutils.bvhtree import BVHTree
p = argparse.ArgumentParser()
p.add_argument("--mesh", required=True)
p.add_argument("--reference", required=True)
p.add_argument("--output", required=True)
a = p.parse_args(sys.argv[sys.argv.index("--") + 1 :])
out = Path(a.output)
out.mkdir(parents=True, exist_ok=True)
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
bpy.ops.import_scene.gltf(filepath=str(Path(a.mesh).resolve()))
objects = [o for o in bpy.context.scene.objects if o.type == "MESH"]
for obj in objects:
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
obj.select_set(False)
vertices = []
triangles = []
offset = 0
for obj in objects:
obj.data.calc_loop_triangles()
vertices.extend(tuple(v.co) for v in obj.data.vertices)
triangles.extend(tuple(offset + i for i in t.vertices) for t in obj.data.loop_triangles)
offset += len(obj.data.vertices)
vertices = np.array(vertices)
lo = vertices.min(0)
hi = vertices.max(0)
ref = bpy.data.images.load(str(Path(a.reference).resolve()))
w, h = ref.size
pixels = np.array(ref.pixels[:]).reshape(h, w, 4)
mask = pixels[:, :, :3].min(2) < 0.88
ys, xs = np.where(mask)
uvmin = np.array([xs.min() / w, ys.min() / h])
uvmax = np.array([xs.max() / w, ys.max() / h])
sx = (uvmax[0] - uvmin[0]) / (hi[0] - lo[0])
sz = (uvmax[1] - uvmin[1]) / (hi[2] - lo[2])
tx = uvmin[0] - lo[0] * sx
tz = uvmin[1] - lo[2] * sz
bvh = BVHTree.FromPolygons(vertices.tolist(), triangles, all_triangles=True)
old_mat = objects[0].data.materials[0]
old_tex = (
next(
n.image
for n in old_mat.node_tree.nodes
if n.type == "TEX_IMAGE" and n.image and "baseColor" in n.image.name
)
if any(n.type == "TEX_IMAGE" and n.image and "baseColor" in n.image.name for n in old_mat.node_tree.nodes)
else next(n.image for n in old_mat.node_tree.nodes if n.type == "TEX_IMAGE" and n.image)
)
# Resolve the actual base-color connection instead of relying on material node order.
bsdf = next(n for n in old_mat.node_tree.nodes if n.type == "BSDF_PRINCIPLED")
if bsdf.inputs["Base Color"].is_linked:
old_tex = bsdf.inputs["Base Color"].links[0].from_node.image
mat = bpy.data.materials.new("Reference projection bake")
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
output = nodes.new("ShaderNodeOutputMaterial")
emit = nodes.new("ShaderNodeEmission")
links.new(emit.outputs[0], output.inputs["Surface"])
original = nodes.new("ShaderNodeTexImage")
original.image = old_tex
projected = nodes.new("ShaderNodeTexImage")
projected.image = ref
projected.extension = "CLIP"
geom = nodes.new("ShaderNodeNewGeometry")
sep = nodes.new("ShaderNodeSeparateXYZ")
links.new(geom.outputs["Position"], sep.inputs[0])
combine = nodes.new("ShaderNodeCombineXYZ")
for channel, scale, translate in [("X", sx, tx), ("Z", sz, tz)]:
mult = nodes.new("ShaderNodeMath")
mult.operation = "MULTIPLY_ADD"
mult.inputs[1].default_value = scale
mult.inputs[2].default_value = translate
links.new(sep.outputs[channel], mult.inputs[0])
links.new(mult.outputs[0], combine.inputs["X" if channel == "X" else "Y"])
links.new(combine.outputs[0], projected.inputs["Vector"])
attribute = nodes.new("ShaderNodeAttribute")
attribute.attribute_name = "projection_weight"
mix = nodes.new("ShaderNodeMixRGB")
links.new(attribute.outputs["Fac"], mix.inputs[0])
links.new(original.outputs["Color"], mix.inputs[1])
links.new(projected.outputs["Color"], mix.inputs[2])
links.new(mix.outputs[0], emit.inputs["Color"])
baked = bpy.data.images.new("Astronaut reference atlas", 4096, 4096, alpha=True)
target = nodes.new("ShaderNodeTexImage")
target.image = baked
nodes.active = target
weights = []
bpy.ops.object.select_all(action="DESELECT")
for obj in objects:
attr = obj.data.attributes.new("projection_weight", "FLOAT", "POINT")
for v in obj.data.vertices:
origin = Vector((v.co.x, -2.0, v.co.z))
hit, normal, index, distance = bvh.ray_cast(origin, Vector((0, 1, 0)))
visible = hit is not None and abs(hit.y - v.co.y) < 0.004
facing = max(0.0, min(1.0, (-v.normal.y - 0.15) / 0.5))
facing = facing * facing * (3 - 2 * facing)
u = v.co.x * sx + tx
vv = v.co.z * sz + tz
px = int(np.clip(u * w, 0, w - 1))
py = int(np.clip(vv * h, 0, h - 1))
foreground = pixels[py, px, :3].min() < 0.94
weight = float(visible and foreground) * facing
attr.data[v.index].value = weight
weights.append(weight)
obj.data.materials.clear()
obj.data.materials.append(mat)
obj.select_set(True)
# A joined bake copy writes one atlas; original component objects remain separate.
bpy.context.view_layer.objects.active = objects[0]
bpy.ops.object.duplicate()
copies = list(bpy.context.selected_objects)
bpy.context.view_layer.objects.active = copies[0]
bpy.ops.object.join()
proxy = bpy.context.object
scene = bpy.context.scene
scene.render.engine = "CYCLES"
scene.cycles.samples = 1
scene.render.bake.margin = 16
scene.render.bake.use_clear = True
bpy.ops.object.bake(type="EMIT")
baked.filepath_raw = str((out / "reference-atlas.png").resolve())
baked.file_format = "PNG"
baked.save()
bpy.data.objects.remove(proxy, do_unlink=True)
final = bpy.data.materials.new("Astronaut painted suit")
final.use_nodes = True
node = final.node_tree.nodes.get("Principled BSDF")
node.inputs["Roughness"].default_value = 0.7
node.inputs["Metallic"].default_value = 0
tex = final.node_tree.nodes.new("ShaderNodeTexImage")
tex.image = baked
final.node_tree.links.new(tex.outputs["Color"], node.inputs["Base Color"])
for obj in objects:
obj.data.materials.clear()
obj.data.materials.append(final)
bpy.ops.object.select_all(action="DESELECT")
for obj in objects:
obj.select_set(True)
bpy.context.view_layer.objects.active = objects[0]
bpy.ops.export_scene.gltf(
filepath=str((out / "character-static.glb").resolve()), export_format="GLB", use_selection=True
)
(out / "texture-transfer.json").write_text(
json.dumps(
{
"method": "Visibility- and normal-weighted projection of original reference, baked to existing UVs; original texture retained on unseen surfaces",
"resolution": 4096,
"projection": [float(sx), float(sz), float(tx), float(tz)],
"vertices_with_reference": sum(x > 0.5 for x in weights),
"vertices": len(weights),
"roughness": 0.7,
},
indent=2,
)
)
|