# vr_build.py - the veiled robot, after the user's five reference renders (2026-10-19, # models/veiled-robot/reference/1-5.jpg). The compound bow is left out (the user's word); # the arrows and the back quiver go with it. No maker names or logos from the renders. # # Run inside Blender (MCP): exec(open(r"...\vr_build.py").read()); build() # Metres. Faces -Y, the character's left is +X (Unreal mannequin convention in Blender). # Bones carry the UE5 mannequin names so the game's retargeted animations can drive it. # Every part is its own object, skinned 100 % to one bone (rigid robot segments). import bpy, bmesh, math from mathutils import Vector as V, Matrix ROOT = r"D:\HADAL - The Descent\models\veiled-robot" PFX = "VR_" # ------------------------------------------------------------------ joints (rest pose) S = V((0.285, 0.02, 1.46)) # shoulder D1 = V((math.cos(math.radians(47)), 0, -math.sin(math.radians(47)))) # A-pose, 47 deg down E = S + D1 * 0.30 # elbow D2 = V((0.682, -0.08, -0.731)).normalized() # forearm, a little forward W = E + D2 * 0.28 # wrist H = V((0.105, 0.0, 0.94)) # hip K = V((0.115, -0.01, 0.515)) # knee A = V((0.125, 0.02, 0.10)) # ankle B = V((0.125, -0.0875, 0.02)) # ball T = V((0.125, -0.19, 0.02)) # toe end def upout(d): # the axis at right angles to d in the XZ plane, pointing up and out return V((-d.z, 0, d.x)).normalized() if d.x >= 0 else V((d.z, 0, -d.x)).normalized() def frame(h, t, xh): h, t = V(h), V(t) z = (t - h).normalized(); xh = V(xh) x = (xh - z * xh.dot(z)).normalized(); y = z.cross(x) M = Matrix((x, y, z)).transposed().to_4x4(); M.translation = h return M F_UA = frame(S, E, upout(D1)) F_LA = frame(E, W, upout(D2)) F_HA = frame(W, W + D2 * 0.1, upout(D2)) F_TH = frame(H, K, (1, 0, 0)) F_CA = frame(K, A, (1, 0, 0)) F_CL = frame((0.04, 0.01, 1.46), S, (0, 0, 1)) # fingers: (name, y in hand frame, segment lengths) FINGERS = [("index", 0.033, (0.038, 0.026, 0.020)), ("middle", 0.011, (0.042, 0.028, 0.022)), ("ring", -0.011, (0.038, 0.026, 0.020)), ("pinky", -0.033, (0.032, 0.022, 0.018))] THUMB = (V((-0.006, 0.056, 0.035)), V((-0.35, 0.55, 0.76)).normalized(), (0.034, 0.026, 0.020)) def bones_left(): """name -> (head, tail, parent, roll axis) for the centre line and the left side.""" b = { "root": ((0, 0, 0), (0, 0.15, 0), None, (0, 0, 1)), "pelvis": ((0, 0, 0.98), (0, 0, 1.08), "root", (0, -1, 0)), "spine_01": ((0, 0, 1.08), (0, 0, 1.17), "pelvis", (0, -1, 0)), "spine_02": ((0, 0, 1.17), (0, 0, 1.28), "spine_01", (0, -1, 0)), "spine_03": ((0, 0, 1.28), (0, 0, 1.50), "spine_02", (0, -1, 0)), "neck_01": ((0, 0, 1.50), (0, 0, 1.60), "spine_03", (0, -1, 0)), "head": ((0, 0, 1.60), (0, 0, 1.80), "neck_01", (0, -1, 0)), "clavicle_l": ((0.04, 0.01, 1.46), tuple(S), "spine_03", (0, 0, 1)), "upperarm_l": (tuple(S), tuple(E), "clavicle_l", tuple(upout(D1))), "lowerarm_l": (tuple(E), tuple(W), "upperarm_l", tuple(upout(D2))), "hand_l": (tuple(W), tuple(W + D2 * 0.10), "lowerarm_l", tuple(upout(D2))), "thigh_l": (tuple(H), tuple(K), "pelvis", (1, 0, 0)), "calf_l": (tuple(K), tuple(A), "thigh_l", (1, 0, 0)), "foot_l": (tuple(A), tuple(B), "calf_l", (0, 0, 1)), "ball_l": (tuple(B), tuple(T), "foot_l", (0, 0, 1)), } xh = upout(D2) for f, y, segs in FINGERS: p = F_HA @ V((0, y, 0.11)); par = "hand_l" for i, L in enumerate(segs, 1): q = p + D2 * L b[f"{f}_{i:02d}_l"] = (tuple(p), tuple(q), par, tuple(xh)) par = f"{f}_{i:02d}_l"; p = q p = F_HA @ THUMB[0]; d = (F_HA.to_3x3() @ THUMB[1]).normalized(); par = "hand_l" for i, L in enumerate(THUMB[2], 1): q = p + d * L b[f"thumb_{i:02d}_l"] = (tuple(p), tuple(q), par, tuple(xh)) par = f"thumb_{i:02d}_l"; p = q return b # ------------------------------------------------------------------ mesh helpers def R(z, x0, x1, y0, y1, c0, c1=None): """Chamfered rectangle (8 points) at height z; c0 chamfers the y0 corners, c1 the y1 corners.""" if c1 is None: c1 = c0 return [V((x0 + c0, y0, z)), V((x1 - c0, y0, z)), V((x1, y0 + c0, z)), V((x1, y1 - c1, z)), V((x1 - c1, y1, z)), V((x0 + c1, y1, z)), V((x0, y1 - c1, z)), V((x0, y0 + c0, z))] def RXZ(y, x0, x1, z0, z1, cb, ct=None): return [V((p.x, y, p.y)) for p in R(0, x0, x1, z0, z1, cb, ct)] def loft(bm, secs): rings = [[bm.verts.new(p) for p in s] for s in secs] n = len(secs[0]); faces = [] for a, b in zip(rings, rings[1:]): for i in range(n): faces.append(bm.faces.new((a[i], a[(i + 1) % n], b[(i + 1) % n], b[i]))) for r in (rings[0], rings[-1]): # quad fan caps (n even, convex) for i in range(1, n - 2, 2): faces.append(bm.faces.new((r[0], r[i], r[i + 1], r[i + 2]))) bmesh.ops.recalc_face_normals(bm, faces=faces) return faces def cyl(bm, c, ax, r, L, n=12): c = V(c); ax = V(ax).normalized() u = ax.orthogonal().normalized(); w = ax.cross(u) secs = [[c + ax * (s * L / 2) + (u * math.cos(2 * math.pi * k / n) + w * math.sin(2 * math.pi * k / n)) * r for k in range(n)] for s in (-1, 1)] return loft(bm, secs) def tube(bm, pts, r, n=8, steps=6): """Round tube along a Catmull-Rom curve through pts.""" P = [V(p) for p in pts] path = [] for i in range(len(P) - 1): p0 = P[max(i - 1, 0)]; p1 = P[i]; p2 = P[i + 1]; p3 = P[min(i + 2, len(P) - 1)] for s in range(steps): t = s / steps path.append(0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (-p0 + 3 * p1 - 3 * p2 + p3) * t ** 3)) path.append(P[-1]) u = None; secs = [] for i, p in enumerate(path): t = (path[min(i + 1, len(path) - 1)] - path[max(i - 1, 0)]).normalized() u = t.orthogonal().normalized() if u is None else (u - t * u.dot(t)).normalized() w = t.cross(u) secs.append([p + (u * math.cos(2 * math.pi * k / n) + w * math.sin(2 * math.pi * k / n)) * r for k in range(n)]) return loft(bm, secs) def recess(bm, faces, d, k=1, inset=0.011, depth=0.003, align=0.85): """Sink the k largest faces whose normal points along d into shallow panels.""" d = V(d).normalized() cand = [f for f in faces if f.is_valid and f.normal.dot(d) > align and len(f.verts) == 4 and min(e.calc_length() for e in f.edges) > inset * 3.2] cand.sort(key=lambda f: -f.calc_area()) sel = cand[:k] if sel: bmesh.ops.inset_individual(bm, faces=sel, thickness=inset, depth=-depth, use_even_offset=True) return len(sel) # ------------------------------------------------------------------ scene helpers def coll(name, parent=None): c = bpy.data.collections.get(name) or bpy.data.collections.new(name) par = parent or bpy.context.scene.collection if c.name not in [x.name for x in par.children]: par.children.link(c) return c def mat(name, rgb, metal=0.0, rough=0.5, emit=None): m = bpy.data.materials.get(name) if m is None: m = bpy.data.materials.new(name); m.use_nodes = True p = m.node_tree.nodes.get("Principled BSDF") p.inputs["Base Color"].default_value = (*rgb, 1) p.inputs["Metallic"].default_value = metal p.inputs["Roughness"].default_value = rough if emit: p.inputs["Emission Color"].default_value = (*emit, 1) p.inputs["Emission Strength"].default_value = 6.0 m.diffuse_color = (*rgb, 1) return m MATS = {} def make_mats(): MATS["armor"] = mat("M_VR_Armor", (0.045, 0.052, 0.042), 0.35, 0.45) MATS["mech"] = mat("M_VR_Mech", (0.012, 0.012, 0.013), 0.7, 0.35) MATS["red"] = mat("M_VR_Red", (0.45, 0.012, 0.01), 0.0, 0.32) MATS["eye"] = mat("M_VR_Eye", (0.3, 0.03, 0.0), 0.0, 0.3, emit=(1.0, 0.18, 0.02)) MATS["net"] = mat("M_VR_Net", (0.035, 0.05, 0.03), 0.0, 0.8) BEVEL = {"armor": 0.005, "mech": 0.003, "red": 0.0015, "eye": 0.001} def emit(name, bm, bone, mkey, M=None, coll_=None, bevel=None): if M is not None: bmesh.ops.transform(bm, matrix=M, verts=bm.verts) for f in bm.faces: f.smooth = True old = bpy.data.objects.get(name) if old: me_old = old.data; bpy.data.objects.remove(old); bpy.data.meshes.remove(me_old) me = bpy.data.meshes.new(name); bm.to_mesh(me); bm.free() me.materials.append(MATS[mkey]) ob = bpy.data.objects.new(name, me) (coll_ or coll("VR | Body")).objects.link(ob) vg = ob.vertex_groups.new(name=bone) vg.add(range(len(me.vertices)), 1.0, 'REPLACE') w = BEVEL[mkey] if bevel is None else bevel if w: bv = ob.modifiers.new("Bevel", 'BEVEL') bv.limit_method = 'ANGLE'; bv.angle_limit = math.radians(30) bv.width = w; bv.segments = 2; bv.harden_normals = True; bv.use_clamp_overlap = True ob["vr_bone"] = bone; ob["vr_mat"] = mkey return ob # ------------------------------------------------------------------ parts def part_torso(): out = [] # chest plate: shield shape, deep faceted front (front = -Y here) bm = bmesh.new() fs = loft(bm, [R(1.25, -0.035, 0.035, -0.14, -0.085, 0.015, 0.008), R(1.31, -0.13, 0.13, -0.185, -0.055, 0.05, 0.015), R(1.40, -0.21, 0.21, -0.205, -0.045, 0.085, 0.015), R(1.48, -0.205, 0.205, -0.19, -0.045, 0.08, 0.015), R(1.54, -0.15, 0.15, -0.135, -0.045, 0.05, 0.012)]) recess(bm, fs, (0, -1, 0), k=2); recess(bm, fs, (0.7, -0.7, 0), k=2, align=0.8); recess(bm, fs, (-0.7, -0.7, 0), k=2, align=0.8) out.append(emit(PFX + "ChestPlate", bm, "spine_03", "armor")) # back unit bm = bmesh.new() fs = loft(bm, [R(1.29, -0.10, 0.10, 0.045, 0.11, 0.01, 0.03), R(1.38, -0.13, 0.13, 0.045, 0.135, 0.01, 0.04), R(1.50, -0.13, 0.13, 0.045, 0.13, 0.01, 0.04), R(1.53, -0.09, 0.09, 0.045, 0.10, 0.01, 0.03)]) recess(bm, fs, (0, 1, 0), k=2) out.append(emit(PFX + "BackUnit", bm, "spine_03", "armor")) # roll cage tubes between chest plate, back unit and abdomen bm = bmesh.new() paths = [[(0.12, -0.07, 1.30), (0.155, -0.02, 1.30), (0.14, 0.04, 1.31), (0.09, 0.07, 1.31)], [(0.075, -0.09, 1.275), (0.10, -0.06, 1.26), (0.105, -0.01, 1.255), (0.07, 0.04, 1.26)], [(0.18, -0.06, 1.38), (0.20, -0.005, 1.39), (0.165, 0.055, 1.41), (0.12, 0.08, 1.42)], [(0.10, -0.045, 1.46), (0.11, 0.0, 1.33), (0.085, 0.05, 1.268)]] for pth in paths: tube(bm, pth, 0.011) tube(bm, [(-x, y, z) for x, y, z in pth], 0.011) out.append(emit(PFX + "RibFrame", bm, "spine_03", "mech")) # abdomen block bm = bmesh.new() fs = loft(bm, [R(1.14, -0.058, 0.058, -0.065, 0.05, 0.018), # narrow foot: the torso leans 25 deg R(1.19, -0.088, 0.088, -0.092, 0.07, 0.028, 0.02), R(1.222, -0.074, 0.074, -0.08, 0.062, 0.025, 0.02)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.014) recess(bm, fs, (1, 0, 0), k=1); recess(bm, fs, (-1, 0, 0), k=1) out.append(emit(PFX + "Abdomen", bm, "spine_02", "armor")) # waist column (mechanics) bm = bmesh.new() loft(bm, [R(1.07, -0.055, 0.055, -0.05, 0.05, 0.015), R(1.105, -0.06, 0.06, -0.055, 0.055, 0.018)]) cyl(bm, (0, 0.0, 1.114), (0, 0, 1), 0.03, 0.018, n=16) # turntable under the abdomen out.append(emit(PFX + "Waist", bm, "spine_01", "mech")) # pelvis core bm = bmesh.new() fs = loft(bm, [R(0.905, -0.038, 0.038, -0.055, 0.05, 0.012), R(0.965, -0.056, 0.056, -0.072, 0.068, 0.02), R(1.025, -0.10, 0.10, -0.088, 0.082, 0.032, 0.025), R(1.065, -0.10, 0.10, -0.08, 0.075, 0.03, 0.02)]) recess(bm, fs, (0, -1, 0), k=1) out.append(emit(PFX + "Pelvis", bm, "pelvis", "armor")) # hip plates (left; mirrored later) bm = bmesh.new() fs = loft(bm, [R(1.055, 0.108, 0.168, -0.08, 0.10, 0.03), R(1.10, 0.108, 0.19, -0.095, 0.115, 0.035), R(1.16, 0.108, 0.16, -0.075, 0.09, 0.03)]) recess(bm, fs, (1, 0, 0), k=1) out.append(emit(PFX + "HipPlate_l", bm, "pelvis", "armor")) # neck bm = bmesh.new() cyl(bm, (0, -0.002, 1.54), (0, 0, 1), 0.036, 0.08) tube(bm, [(0.03, 0.03, 1.50), (0.045, 0.02, 1.55), (0.035, 0.0, 1.572)], 0.007) tube(bm, [(-0.03, 0.03, 1.50), (-0.045, 0.02, 1.55), (-0.035, 0.0, 1.572)], 0.007) out.append(emit(PFX + "Neck", bm, "neck_01", "mech")) return out def part_head(): out = [] bm = bmesh.new() fs = loft(bm, [R(1.585, -0.05, 0.05, -0.06, 0.06, 0.02), R(1.62, -0.07, 0.07, -0.092, 0.11, 0.03, 0.045), R(1.69, -0.08, 0.08, -0.105, 0.13, 0.035, 0.055), R(1.77, -0.082, 0.082, -0.104, 0.13, 0.035, 0.055), R(1.84, -0.076, 0.076, -0.094, 0.115, 0.034, 0.05), R(1.89, -0.06, 0.06, -0.074, 0.085, 0.028, 0.038), R(1.925, -0.034, 0.034, -0.042, 0.045, 0.014, 0.018)]) recess(bm, fs, (1, 0, 0), k=2); recess(bm, fs, (-1, 0, 0), k=2); recess(bm, fs, (0, 1, 0), k=1) out.append(emit(PFX + "Head", bm, "head", "armor")) bm = bmesh.new() # face plate: a raised shield over the front, eyes under its brow fs = loft(bm, [R(1.64, -0.04, 0.04, -0.112, -0.098, 0.006, 0.002), R(1.71, -0.058, 0.058, -0.118, -0.098, 0.008, 0.002), R(1.725, -0.058, 0.058, -0.118, -0.098, 0.008, 0.002)]) loft(bm, [R(1.75, -0.06, 0.06, -0.12, -0.098, 0.008, 0.002), R(1.83, -0.05, 0.05, -0.112, -0.09, 0.008, 0.002)]) out.append(emit(PFX + "FacePlate", bm, "head", "armor", bevel=0.002)) bm = bmesh.new() # ear pods for x in (-0.087, 0.087): cyl(bm, (x, 0.02, 1.73), (1, 0, 0), 0.032, 0.016, n=16) cyl(bm, (x * 1.12, 0.02, 1.73), (1, 0, 0), 0.016, 0.012, n=12) out.append(emit(PFX + "EarPods", bm, "head", "mech")) bm = bmesh.new() for x in (-0.032, 0.032): loft(bm, [R(1.731, x - 0.011, x + 0.011, -0.113, -0.100, 0.002), R(1.743, x - 0.011, x + 0.011, -0.113, -0.100, 0.002)]) out.append(emit(PFX + "Eyes", bm, "head", "eye")) return out def part_leg_l(): out = [] bm = bmesh.new() # hip joint, turns with the thigh loft(bm, [[V((0.034 * math.sin(a) * math.cos(b), 0.034 * math.sin(a) * math.sin(b), -0.034 * math.cos(a))) for b in [k * math.pi / 6 for k in range(12)]] for a in [j * math.pi / 8 for j in range(1, 8)]]) out.append(emit(PFX + "HipJoint_l", bm, "thigh_l", "mech", F_TH)) bm = bmesh.new() fs = loft(bm, [R(0.05, -0.04, 0.085, -0.03, 0.055, 0.012, 0.02), # front plates; the frame shows behind R(0.12, -0.04, 0.108, -0.04, 0.10, 0.015, 0.04), R(0.24, -0.052, 0.098, -0.035, 0.095, 0.015, 0.035), R(0.33, -0.046, 0.074, -0.03, 0.08, 0.012, 0.03), R(0.37, -0.04, 0.062, -0.025, 0.07, 0.01, 0.025)]) recess(bm, fs, (0, 1, 0), k=2); recess(bm, fs, (1, 0, 0), k=2) out.append(emit(PFX + "Thigh_l", bm, "thigh_l", "armor", F_TH)) bm = bmesh.new() # thigh frame behind the plates loft(bm, [R(0.045, -0.036, 0.05, -0.075, -0.03, 0.02, 0.004), R(0.18, -0.046, 0.058, -0.088, -0.035, 0.022, 0.004), R(0.26, -0.032, 0.042, -0.052, -0.03, 0.012, 0.004)]) for x in (-0.026, 0.04): cyl(bm, (x, -0.094, 0.155), (0, 0, 1), 0.009, 0.17, n=8) out.append(emit(PFX + "ThighFrame_l", bm, "thigh_l", "mech", F_TH)) bm = bmesh.new() # knee hinge with side discs cyl(bm, (0, 0, 0), (1, 0, 0), 0.045, 0.11) for x in (-0.061, 0.061): cyl(bm, (x, 0, 0), (1, 0, 0), 0.038, 0.012, n=16) out.append(emit(PFX + "Knee_l", bm, "calf_l", "mech", F_CA)) bm = bmesh.new() fs = loft(bm, [R(-0.04, -0.045, 0.045, 0.05, 0.08, 0.01, 0.025), R(0.0, -0.05, 0.05, 0.055, 0.095, 0.01, 0.03), R(0.045, -0.04, 0.04, 0.055, 0.08, 0.01, 0.025)]) out.append(emit(PFX + "KneeCap_l", bm, "calf_l", "armor", F_CA)) bm = bmesh.new() fs = loft(bm, [R(0.055, -0.05, 0.056, -0.025, 0.07, 0.01, 0.028), R(0.12, -0.058, 0.064, -0.03, 0.075, 0.012, 0.03), R(0.22, -0.052, 0.06, -0.028, 0.068, 0.012, 0.028), R(0.32, -0.04, 0.046, -0.022, 0.055, 0.01, 0.022), R(0.365, -0.034, 0.04, -0.02, 0.048, 0.008, 0.018)]) recess(bm, fs, (0, 1, 0), k=2); recess(bm, fs, (1, 0, 0), k=2); recess(bm, fs, (-1, 0, 0), k=1) out.append(emit(PFX + "Shin_l", bm, "calf_l", "armor", F_CA)) bm = bmesh.new() # calf frame behind the shin plates fs = loft(bm, [R(0.095, -0.036, 0.038, -0.05, -0.025, 0.012, 0.004), # starts low: knee bends past 110 deg R(0.15, -0.046, 0.048, -0.095, -0.025, 0.026, 0.004), R(0.30, -0.03, 0.032, -0.052, -0.022, 0.012, 0.004), R(0.37, -0.025, 0.027, -0.042, -0.02, 0.01, 0.004)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.008) out.append(emit(PFX + "CalfFrame_l", bm, "calf_l", "mech", F_CA)) # foot (world coordinates, front = -Y) bm = bmesh.new() cyl(bm, A, (1, 0, 0), 0.032, 0.08) for dx in (-0.046, 0.046): cyl(bm, A + V((dx, 0, 0)), (1, 0, 0), 0.028, 0.012, n=16) out.append(emit(PFX + "Ankle_l", bm, "foot_l", "mech")) bm = bmesh.new() fs = loft(bm, [RXZ(0.075, 0.085, 0.165, 0.0, 0.07, 0.005, 0.03), RXZ(0.045, 0.072, 0.178, 0.0, 0.062, 0.005, 0.025), RXZ(-0.03, 0.074, 0.178, 0.0, 0.06, 0.005, 0.025), RXZ(-0.085, 0.08, 0.172, 0.0, 0.045, 0.005, 0.02)]) recess(bm, fs, (1, 0, 0), k=1, inset=0.008) out.append(emit(PFX + "Foot_l", bm, "foot_l", "armor")) bm = bmesh.new() loft(bm, [RXZ(-0.09, 0.08, 0.172, 0.0, 0.044, 0.005, 0.02), RXZ(-0.15, 0.082, 0.17, 0.0, 0.036, 0.005, 0.016), RXZ(-0.19, 0.092, 0.16, 0.0, 0.022, 0.004, 0.01)]) out.append(emit(PFX + "Toe_l", bm, "ball_l", "armor")) return out def part_arm_l(): out = [] bm = bmesh.new() loft(bm, [R(0.09, -0.032, 0.032, -0.035, 0.035, 0.012), R(0.14, -0.035, 0.035, -0.035, 0.035, 0.012)]) out.append(emit(PFX + "ShoulderMount_l", bm, "clavicle_l", "mech", F_CL)) bm = bmesh.new() cyl(bm, (0, 0, 0), (0, 1, 0), 0.05, 0.09) out.append(emit(PFX + "ShoulderJoint_l", bm, "upperarm_l", "mech", F_UA)) bm = bmesh.new() fs = loft(bm, [R(-0.055, -0.04, 0.085, -0.075, 0.075, 0.03), R(0.02, -0.055, 0.12, -0.10, 0.10, 0.04), R(0.12, -0.055, 0.105, -0.095, 0.095, 0.035), R(0.205, -0.05, 0.078, -0.075, 0.06, 0.028, 0.024), # inside of the elbow cut back R(0.25, -0.042, 0.06, -0.058, 0.028, 0.022, 0.01)]) recess(bm, fs, (1, 0, 0), k=2); recess(bm, fs, (0, 1, 0), k=2); recess(bm, fs, (0, -1, 0), k=2) out.append(emit(PFX + "UpperArm_l", bm, "upperarm_l", "armor", F_UA)) bm = bmesh.new() cyl(bm, (0, 0, 0), (1, 0, 0), 0.042, 0.085) out.append(emit(PFX + "Elbow_l", bm, "lowerarm_l", "mech", F_LA)) bm = bmesh.new() fs = loft(bm, [R(0.07, -0.04, 0.046, -0.046, 0.022, 0.02, 0.008), # inside of the elbow cut back R(0.13, -0.05, 0.058, -0.055, 0.062, 0.024, 0.026), R(0.205, -0.05, 0.06, -0.056, 0.064, 0.024, 0.026), R(0.228, -0.042, 0.05, -0.046, 0.052, 0.02, 0.022)]) # wrist clearance recess(bm, fs, (1, 0, 0), k=2); recess(bm, fs, (0, 1, 0), k=1); recess(bm, fs, (-1, 0, 0), k=2, depth=0.006) out.append(emit(PFX + "Forearm_l", bm, "lowerarm_l", "armor", F_LA)) bm = bmesh.new() cyl(bm, (0, 0, 0), (0, 1, 0), 0.026, 0.05) loft(bm, [R(0.012, -0.012, 0.016, -0.03, 0.03, 0.008), R(0.035, -0.016, 0.02, -0.042, 0.042, 0.008), R(0.10, -0.014, 0.018, -0.046, 0.046, 0.008)]) out.append(emit(PFX + "Palm_l", bm, "hand_l", "mech", F_HA)) bm = bmesh.new() loft(bm, [R(0.042, 0.02, 0.031, -0.038, 0.038, 0.006), R(0.095, 0.018, 0.03, -0.044, 0.044, 0.006)]) out.append(emit(PFX + "HandPlate_l", bm, "hand_l", "armor", F_HA)) # finger segments, each on its own bone bl = bones_left() names = [f for f, _, _ in FINGERS] + ["thumb"] for f in names: for i in (1, 2, 3): bn = f"{f}_{i:02d}_l"; h, t, _, xh = bl[bn] M = frame(h, t, xh); L = (V(t) - V(h)).length bm = bmesh.new() cyl(bm, (0, 0, 0), (0, 1, 0), 0.0088, 0.0155, n=10) w = 0.0095 if f != "thumb" else 0.011 end = L - 0.0004 if i == 3 else L - 0.0098 # clear of the next knuckle loft(bm, [R(0.006, -0.010, 0.0105, -w * 0.85, w * 0.85, 0.003), R(end, -0.0095, 0.0098, -w * 0.8, w * 0.8, 0.003)]) out.append(emit(f"{PFX}Finger_{f}_{i:02d}_l", bm, bn, "red" if i == 3 else "mech", M, coll("VR | Hands", coll("VR | Body")))) return out # ------------------------------------------------------------------ mirror and rig def mirror_l(ob): me = ob.data.copy() bm = bmesh.new(); bm.from_mesh(me) bmesh.ops.scale(bm, vec=(-1, 1, 1), verts=bm.verts) bmesh.ops.reverse_faces(bm, faces=bm.faces) bm.to_mesh(me); bm.free() name = ob.name[:-2] + "_r" old = bpy.data.objects.get(name) if old: m = old.data; bpy.data.objects.remove(old); bpy.data.meshes.remove(m) me.name = name o2 = bpy.data.objects.new(name, me) for c in ob.users_collection: c.objects.link(o2) bone = ob["vr_bone"][:-2] + "_r" o2.vertex_groups.clear() # the copied mesh brings the left bone's weights along vg = o2.vertex_groups.new(name=bone); vg.add(range(len(me.vertices)), 1.0, 'REPLACE') for md in ob.modifiers: if md.type == 'BEVEL': b = o2.modifiers.new("Bevel", 'BEVEL') for a in ("limit_method", "angle_limit", "width", "segments", "harden_normals", "use_clamp_overlap"): setattr(b, a, getattr(md, a)) o2["vr_bone"] = bone; o2["vr_mat"] = ob["vr_mat"] return o2 def build_rig(): name = PFX + "Rig" old = bpy.data.objects.get(name) if old: a = old.data; bpy.data.objects.remove(old); bpy.data.armatures.remove(a) arm = bpy.data.armatures.new(name); arm.display_type = 'STICK' ob = bpy.data.objects.new(name, arm) coll("VR | Rig", coll("VeiledRobot")).objects.link(ob) bl = bones_left() allb = dict(bl) for n, (h, t, p, r) in bl.items(): if n.endswith("_l"): allb[n[:-2] + "_r"] = ((-h[0], h[1], h[2]), (-t[0], t[1], t[2]), (p[:-2] + "_r") if p and p.endswith("_l") else p, (-r[0], r[1], r[2])) vl = bpy.context.view_layer vl.objects.active = ob with bpy.context.temp_override(active_object=ob, object=ob, selected_objects=[ob]): bpy.ops.object.mode_set(mode='EDIT') eb = arm.edit_bones for n, (h, t, p, r) in allb.items(): b = eb.new(n); b.head = h; b.tail = t b.align_roll(V(r)); b.use_deform = n != "root" for n, (h, t, p, r) in allb.items(): if p: eb[n].parent = eb[p] bpy.ops.object.mode_set(mode='OBJECT') return ob def build(): top = coll("VeiledRobot") for c in ("VR | Body",): coll(c, top) make_mats() objs = part_torso() + part_head() + part_leg_l() + part_arm_l() objs += [mirror_l(o) for o in list(objs) if o.name.endswith("_l")] rig = build_rig() for o in objs: o.parent = rig md = o.modifiers.new("Armature", 'ARMATURE'); md.object = rig tris = sum(sum(len(p.vertices) - 2 for p in o.data.polygons) for o in objs) return {"objects": len(objs), "bones": len(rig.data.bones), "base_tris": tris}