# vr_decals.py - stencil decals (textures/T_VR_Decals.png, made by vr_decal_atlas.py). # Each decal is a quad cast onto its plate by a ray, 0.8 mm off the surface, skinned to the # plate's bone. Right-side decals are cast separately (mirrored ray), so no text reads mirrored. # Run after vr_build.build(): exec(...); place_all(ns) (ns = vr_build's namespace) import bpy, bmesh, json, math from mathutils import Vector as V ROOT = r"D:\HADAL - The Descent\models\veiled-robot" ATLAS = ROOT + r"\textures\T_VR_Decals.png" def decal_mat(): m = bpy.data.materials.get("M_VR_Decal") or bpy.data.materials.new("M_VR_Decal") m.use_nodes = True; nt = m.node_tree; N = nt.nodes; L = nt.links.new for n in list(N): N.remove(n) out = N.new("ShaderNodeOutputMaterial"); bs = N.new("ShaderNodeBsdfPrincipled") tx = N.new("ShaderNodeTexImage"); tx.image = bpy.data.images.load(ATLAS, check_existing=True) tx.image.colorspace_settings.name = "sRGB"; tx.interpolation = 'Cubic' tc = N.new("ShaderNodeTexCoord") noi = N.new("ShaderNodeTexNoise"); noi.inputs["Scale"].default_value = 160; noi.inputs["Detail"].default_value = 10 L(tc.outputs["Object"], noi.inputs["Vector"]) wear = N.new("ShaderNodeMapRange") # paint rubbed off in small flecks wear.inputs["From Min"].default_value = 0.30; wear.inputs["From Max"].default_value = 0.40 L(noi.outputs["Fac"], wear.inputs["Value"]) a = N.new("ShaderNodeMath"); a.operation = 'MULTIPLY' L(tx.outputs["Alpha"], a.inputs[0]); L(wear.outputs["Result"], a.inputs[1]) a2 = N.new("ShaderNodeMath"); a2.operation = 'MULTIPLY'; a2.inputs[1].default_value = 0.92 L(a.outputs["Value"], a2.inputs[0]) bs.inputs["Base Color"].default_value = (0.72, 0.73, 0.70, 1); bs.inputs["Roughness"].default_value = 0.55 L(a2.outputs["Value"], bs.inputs["Alpha"]) L(bs.outputs["BSDF"], out.inputs["Surface"]) m.diffuse_color = (0.8, 0.8, 0.8, 1) return m def cast(part, origin, direction, up, region, width, name, coll): ob = bpy.data.objects[part] ok, loc, nrm, _ = ob.ray_cast(V(origin), V(direction).normalized()) if not ok: return None n = nrm.normalized(); u = V(up); u = (u - n * u.dot(n)).normalized(); r = u.cross(n) meta = json.load(open(ROOT + r"\textures\T_VR_Decals.json")) S = meta["size"]; x0, y0, x1, y1 = meta["regions"][region] h = width * (y1 - y0) / (x1 - x0) c = loc + n * 0.0008 pts = [c - r * width / 2 - u * h / 2, c + r * width / 2 - u * h / 2, c + r * width / 2 + u * h / 2, c - r * width / 2 + u * h / 2] uvs = [(x0 / S, 1 - y1 / S), (x1 / S, 1 - y1 / S), (x1 / S, 1 - y0 / S), (x0 / S, 1 - y0 / S)] old = bpy.data.objects.get(name) if old: me_o = old.data; bpy.data.objects.remove(old); bpy.data.meshes.remove(me_o) me = bpy.data.meshes.new(name); bm = bmesh.new() f = bm.faces.new([bm.verts.new(p) for p in pts]) uvl = bm.loops.layers.uv.new("UVMap") for lp, uv in zip(f.loops, uvs): lp[uvl].uv = uv bm.to_mesh(me); bm.free() me.materials.append(bpy.data.materials["M_VR_Decal"]) o = bpy.data.objects.new(name, me); coll.objects.link(o) bone = ob["vr_bone"] vg = o.vertex_groups.new(name=bone); vg.add(range(4), 1.0, 'REPLACE') rig = bpy.data.objects["VR_Rig"]; o.parent = rig md = o.modifiers.new("Armature", 'ARMATURE'); md.object = rig o.visible_shadow = False o["vr_bone"] = bone return o def place_all(ns): decal_mat() top = bpy.data.collections["VeiledRobot"] coll = bpy.data.collections.get("VR | Decals") or bpy.data.collections.new("VR | Decals") if coll.name not in [c.name for c in top.children]: top.children.link(coll) for o in list(coll.objects): bpy.data.objects.remove(o) F_UA, F_LA, F_TH, F_CA = ns["F_UA"], ns["F_LA"], ns["F_TH"], ns["F_CA"] R3 = lambda M, v: M.to_3x3() @ V(v) jobs = [ # part, origin, direction, up, region, width ("VR_ChestPlate", (0, -0.5, 1.455), (0, 1, 0), (0, 0, 1), "unit", 0.15), ("VR_ChestPlate", (0.105, -0.5, 1.352), (0, 1, 0), (0, 0, 1), "badge", 0.034), ("VR_ChestPlate", (-0.105, -0.5, 1.352), (0, 1, 0), (0, 0, 1), "badge", 0.034), ("VR_Abdomen", (0, -0.5, 1.19), (0, 1, 0), (0, 0, 1), "emblem", 0.055), ("VR_BackUnit", (0, 0.5, 1.43), (0, -1, 0), (0, 0, 1), "frame07", 0.075), ("VR_BackUnit", (0, 0.5, 1.345), (0, -1, 0), (0, 0, 1), "ruler", 0.09), ("VR_Head", (-0.5, 0.06, 1.74), (1, 0, 0), (0, 0, 1), "code", 0.06), ("VR_Pelvis", (0, -0.5, 1.015), (0, 1, 0), (0, 0, 1), "code", 0.07), ] side = [ ("VR_UpperArm_l", F_UA @ V((0.5, 0, 0.10)), R3(F_UA, (-1, 0, 0)), R3(F_UA, (0, 1, 0)), "unit", 0.13), ("VR_UpperArm_l", F_UA @ V((0, -0.5, 0.06)), R3(F_UA, (0, 1, 0)), R3(F_UA, (0, 0, -1)), "badge", 0.03), ("VR_Forearm_l", F_LA @ V((0.5, 0, 0.15)), R3(F_LA, (-1, 0, 0)), R3(F_LA, (0, 1, 0)), "code", 0.075), ("VR_Thigh_l", F_TH @ V((0, 0.5, 0.17)), R3(F_TH, (0, -1, 0)), R3(F_TH, (0, 0, -1)), "ruler", 0.09), ("VR_Thigh_l", F_TH @ V((0.5, 0.03, 0.15)), R3(F_TH, (-1, 0, 0)), R3(F_TH, (0, 0, -1)), "frame07", 0.05), ("VR_Shin_l", F_CA @ V((0, 0.5, 0.15)), R3(F_CA, (0, -1, 0)), R3(F_CA, (0, 0, -1)), "chevron", 0.06), ("VR_HipPlate_l", (0.5, 0.0, 1.105), (-1, 0, 0), (0, 0, 1), "badge", 0.035), ("VR_Shin_l", F_CA @ V((0.5, 0.02, 0.24)), R3(F_CA, (-1, 0, 0)), R3(F_CA, (0, 0, -1)), "code", 0.06), ] mir = lambda v: V((-v[0], v[1], v[2])) for p, o, d, u, reg, w in side: jobs.append((p, o, d, u, reg, w)) jobs.append((p[:-2] + "_r", mir(o), mir(d), mir(u), reg, w)) made, missed = 0, [] for i, (p, o, d, u, reg, w) in enumerate(jobs): ob = cast(p, o, d, u, reg, w, f"VR_Decal_{i:02d}_{p[3:]}", coll) if ob: made += 1 else: missed.append(p) return {"decals": made, "missed": missed}