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,
    )
)