# 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; so does the camouflage net ("also without that # fiber mesh on its head"). No maker names or logos from the renders. # # Version 2 (the user: "Try to do it more precise pixel by pixel according to the pictures"): # proportions measured on the two full front renders (4.jpg, 5.jpg) on a pixel grid, scale # from the full height 1.925 m (1.37 mm/px on 5.jpg, 1.48 mm/px on 4.jpg): # head top 1.925 | shoulder caps 1.68-1.70 | chest plate top 1.555, crease 1.435, lip 1.355 # abdomen 1.18-1.31 | waist 1.08-1.16 | hip blocks top 1.08, 0.16-0.30 off centre # knee 0.61 | upper ankle joint 0.165, lower 0.11 | upper arm 0.33, forearm 0.30, hand 0.18 # chest 0.35 wide at the top, 0.20 at the lip | forearm 0.11 thick | shin up to 0.15 wide # Shapes from the close-ups (3.jpg chest and arms, 2.jpg and 1.jpg hips and legs). # # Run inside Blender (MCP): ns = {}; exec(open(r"...\vr_build.py").read(), ns); ns["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 from mathutils.bvhtree import BVHTree ROOT = r"D:\HADAL - The Descent\models\veiled-robot" PFX = "VR_" # ------------------------------------------------------------------ joints (rest pose) S = V((0.27, 0.0, 1.60)) # shoulder D1 = V((0.5, 0, -0.866)) # A-pose, upper arm 60 deg below horizontal E = S + D1 * 0.33 # elbow D2 = V((0.5, -0.12, -0.866)).normalized() # forearm, a little forward W = E + D2 * 0.30 # wrist C = V((0.06, 0.0, 1.57)) # clavicle root H = V((0.22, 0.0, 1.00)) # hip (wide pelvis: the hip blocks sit 0.16-0.30 out) K = V((0.235, -0.01, 0.61)) # knee A = V((0.25, 0.02, 0.165)) # ankle B = V((0.25, -0.09, 0.03)) # ball T = V((0.25, -0.215, 0.03)) # 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(C, S, (0, 0, 1)) # fingers: (name, y in hand frame, segment lengths) - big hands, as in 1.jpg FINGERS = [("index", 0.039, (0.045, 0.030, 0.024)), ("middle", 0.013, (0.048, 0.032, 0.025)), ("ring", -0.013, (0.045, 0.030, 0.024)), ("pinky", -0.039, (0.038, 0.026, 0.020))] FW = 0.0105 # finger half width THUMB = (V((-0.008, 0.062, 0.04)), V((-0.35, 0.55, 0.76)).normalized(), (0.04, 0.03, 0.024)) 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, 1.00), (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.32), "spine_01", (0, -1, 0)), "spine_03": ((0, 0, 1.32), (0, 0, 1.57), "spine_02", (0, -1, 0)), "neck_01": ((0, 0, 1.57), (0, 0, 1.64), "spine_03", (0, -1, 0)), "head": ((0, 0, 1.64), (0, 0, 1.85), "neck_01", (0, -1, 0)), "clavicle_l": (tuple(C), 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.117)); 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 RR(z, x0, x1, y0, y1, r, seg=3): """Rounded rectangle, 4 * (seg + 1) points, counter-clockwise seen from +Z.""" pts = [] for cx, cy, a0 in ((x1 - r, y0 + r, -90), (x1 - r, y1 - r, 0), (x0 + r, y1 - r, 90), (x0 + r, y0 + r, 180)): for k in range(seg + 1): a = math.radians(a0 + 90 * k / seg) pts.append(V((cx + r * math.cos(a), cy + r * math.sin(a), z))) return pts def TRAP(y, xb0, xb1, xt0, xt1, z0, z1, cb, ct): """Section in the XZ plane at y: bottom xb0..xb1, top xt0..xt1 (a foot that is wider at the sole).""" return [V((xb0 + cb, y, z0)), V((xb1 - cb, y, z0)), V((xb1, y, z0 + cb)), V((xt1, y, z1 - ct)), V((xt1 - ct, y, z1)), V((xt0 + ct, y, z1)), V((xt0, y, z1 - ct)), V((xb0, y, z0 + cb))] 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 ball(bm, c, r, n=12, rings=8): c = V(c) secs = [[c + V((r * math.sin(a) * math.cos(b), r * math.sin(a) * math.sin(b), -r * math.cos(a))) for b in [k * 2 * math.pi / n for k in range(n)]] for a in [j * math.pi / rings for j in range(1, rings)]] 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) def box(bm, x0, x1, y0, y1, z0, z1, c=0.002): return loft(bm, [R(z0, x0, x1, y0, y1, c), R(z1, x0, x1, y0, y1, c)]) # ------------------------------------------------------------------ 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") if p: 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)) BEVEL = {"armor": 0.005, "mech": 0.003, "red": 0.0015, "eye": 0.0} def emit(name, bm, bone, mkey, M=None, coll_=None, bevel=None, smooth=None): if M is not None: bmesh.ops.transform(bm, matrix=M, verts=bm.verts) # armour plates are flat facets (crisp, as in the references); the bevel modifier's hardened # normals round only the edges. Mechanics (round parts) are smooth. sm = (mkey != "armor") if smooth is None else smooth for f in bm.faces: f.smooth = sm 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 def slats(bm, M, p0, p1, n, size): """n thin bars between local points p0 and p1 (louvres in a vent), bars sized (sx, sy, sz).""" p0, p1 = V(p0), V(p1) for i in range(n): c = p0.lerp(p1, (i + 0.5) / n); sx, sy, sz = size box(bm, c.x - sx / 2, c.x + sx / 2, c.y - sy / 2, c.y + sy / 2, c.z - sz / 2, c.z + sz / 2, 0.0015) # ------------------------------------------------------------------ torso (world coordinates, front = -Y) def part_torso(): out = [] # chest plate (3.jpg): wide straight top under the head, near-vertical sides, a crease at # 1.435 below which the lower band folds back to a lip at 1.355; open underneath bm = bmesh.new() fs = loft(bm, [R(1.355, -0.10, 0.10, -0.152, -0.065, 0.03, 0.01), R(1.40, -0.155, 0.155, -0.178, -0.05, 0.045, 0.012), R(1.455, -0.175, 0.175, -0.19, -0.042, 0.05, 0.012), # crease just above the badges R(1.51, -0.18, 0.18, -0.18, -0.042, 0.05, 0.012), R(1.555, -0.175, 0.175, -0.152, -0.045, 0.042, 0.012)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.016, depth=0.002) # the text panel out.append(emit(PFX + "ChestPlate", bm, "spine_03", "armor")) # back unit bm = bmesh.new() fs = loft(bm, [R(1.36, -0.11, 0.11, 0.055, 0.11, 0.01, 0.03), R(1.45, -0.15, 0.15, 0.055, 0.15, 0.01, 0.045), R(1.56, -0.14, 0.14, 0.055, 0.13, 0.01, 0.04)]) recess(bm, fs, (0, 1, 0), k=2) out.append(emit(PFX + "BackUnit", bm, "spine_03", "armor")) # spine column, roll-cage tubes and the hoop under the chest lip (3.jpg) bm = bmesh.new() box(bm, -0.04, 0.04, -0.03, 0.05, 1.362, 1.55, 0.008) paths = [[(0.10, -0.112, 1.36), (0.118, -0.085, 1.342), (0.088, -0.062, 1.332), (0.052, -0.066, 1.337)], [(0.158, -0.06, 1.40), (0.175, -0.01, 1.39), (0.14, 0.035, 1.375), (0.10, 0.045, 1.37)], [(0.17, -0.05, 1.50), (0.188, 0.0, 1.485), (0.15, 0.045, 1.47), (0.115, 0.05, 1.46)], [(0.13, -0.06, 1.37), (0.135, -0.02, 1.43), (0.12, 0.03, 1.50)]] for pth in paths: tube(bm, pth, 0.01) tube(bm, [(-x, y, z) for x, y, z in pth], 0.01) out.append(emit(PFX + "RibFrame", bm, "spine_03", "mech")) # connector box with a latch on top of the abdomen (3.jpg) bm = bmesh.new() fs = loft(bm, [R(1.324, -0.046, 0.046, -0.105, -0.03, 0.008), R(1.356, -0.046, 0.046, -0.105, -0.03, 0.008)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.009, depth=0.006) out.append(emit(PFX + "Connector", bm, "spine_03", "armor")) # abdomen box: flat front with a framed centre panel (3.jpg) bm = bmesh.new() fs = loft(bm, [R(1.18, -0.068, 0.068, -0.085, 0.055, 0.02), R(1.20, -0.088, 0.088, -0.10, 0.07, 0.022), R(1.30, -0.09, 0.09, -0.10, 0.07, 0.022), R(1.31, -0.08, 0.08, -0.09, 0.06, 0.018)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.016, depth=0.002) 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 and turntable, a small slotted box in front (5.jpg) bm = bmesh.new() box(bm, -0.045, 0.045, -0.05, 0.045, 1.083, 1.15, 0.012) cyl(bm, (0, 0, 1.155), (0, 0, 1), 0.03, 0.01, n=16) out.append(emit(PFX + "Waist", bm, "spine_01", "mech")) bm = bmesh.new() fs = loft(bm, [R(1.09, -0.032, 0.032, -0.075, -0.045, 0.006), R(1.142, -0.032, 0.032, -0.075, -0.045, 0.006)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.008, depth=0.005) out.append(emit(PFX + "WaistBox", bm, "spine_01", "armor")) # pelvis: a wide block between the hip blocks, axles out to the hip balls bm = bmesh.new() fs = loft(bm, [R(0.95, -0.09, 0.09, -0.06, 0.06, 0.025), R(0.985, -0.133, 0.133, -0.08, 0.075, 0.03), R(1.045, -0.133, 0.133, -0.085, 0.08, 0.03), R(1.075, -0.10, 0.10, -0.07, 0.065, 0.025)]) out.append(emit(PFX + "Pelvis", bm, "pelvis", "mech")) bm = bmesh.new() fs = loft(bm, [R(0.955, -0.05, 0.05, -0.083, -0.064, 0.008, 0.002), R(1.0, -0.075, 0.075, -0.095, -0.08, 0.012, 0.002), R(1.06, -0.07, 0.07, -0.098, -0.083, 0.012, 0.002)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.01) out.append(emit(PFX + "PelvisPlate", bm, "pelvis", "armor")) bm = bmesh.new() # collar on top of the chest, round the neck fs = loft(bm, [RR(1.548, -0.088, 0.088, -0.105, 0.065, 0.03), RR(1.566, -0.074, 0.074, -0.08, 0.052, 0.024)]) out.append(emit(PFX + "Collar", bm, "spine_03", "armor")) # neck: column and two cables bm = bmesh.new() cyl(bm, (0, 0.0, 1.596), (0, 0, 1), 0.031, 0.012, n=16) # bends 25 deg clear of the collar, head nods 20 cyl(bm, (0, 0.0, 1.583), (0, 0, 1), 0.02, 0.016, n=12) tube(bm, [(0.026, 0.022, 1.592), (0.036, 0.02, 1.6), (0.026, 0.012, 1.6)], 0.005) tube(bm, [(-0.026, 0.022, 1.592), (-0.036, 0.02, 1.6), (-0.026, 0.012, 1.6)], 0.005) out.append(emit(PFX + "Neck", bm, "neck_01", "mech")) bm = bmesh.new() # tall block at the chest's upper corner, beside the head (5.jpg overlay, 3.jpg) fs = loft(bm, [R(1.558, 0.138, 0.196, -0.06, 0.085, 0.014), # sits on the chest top, clear of the R(1.625, 0.138, 0.196, -0.07, 0.095, 0.018), # shoulder's whole swing (joint sweep) R(1.66, 0.146, 0.188, -0.05, 0.075, 0.016)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.008) out.append(emit(PFX + "Trapezius_l", bm, "clavicle_l", "armor")) return out # ------------------------------------------------------------------ head (world) def part_head(): """The net hid the head; under it (3.jpg, 1.jpg): a rounded dome, deep front to back, two small orange eyes low on the dome, a narrower face column below.""" out = [] bm = bmesh.new() fs = loft(bm, [RR(1.615, -0.044, 0.044, -0.098, 0.045, 0.02), RR(1.655, -0.056, 0.056, -0.11, 0.085, 0.028), RR(1.70, -0.076, 0.076, -0.116, 0.115, 0.045), RR(1.76, -0.092, 0.092, -0.118, 0.13, 0.065), RR(1.83, -0.09, 0.09, -0.112, 0.125, 0.07), RR(1.88, -0.076, 0.076, -0.095, 0.10, 0.065), RR(1.91, -0.048, 0.048, -0.068, 0.07, 0.045), RR(1.925, -0.02, 0.02, -0.03, 0.035, 0.018)]) recess(bm, fs, (0, -1, 0), k=1, inset=0.006, depth=0.004, align=0.8) # face vent tree = BVHTree.FromBMesh(bm) out.append(emit(PFX + "Head", bm, "head", "armor", bevel=0.003, smooth=True)) eyes, lens = bmesh.new(), bmesh.new() for x in (-0.034, 0.034): loc, n, _, _ = tree.ray_cast(V((x, -0.5, 1.748)), V((0, 1, 0))) cyl(eyes, loc + n * 0.003, n, 0.012, 0.008, n=16) cyl(lens, loc + n * 0.0075, n, 0.0075, 0.002, n=16) out.append(emit(PFX + "EyeHousings", eyes, "head", "mech")) out.append(emit(PFX + "Eyes", lens, "head", "eye")) return out # ------------------------------------------------------------------ leg (left; thigh/calf frames: x out, y forward, z down) def part_leg_l(): out = [] bm = bmesh.new() ball(bm, (0, 0, 0), 0.04) cyl(bm, (-0.0475, 0, 0), (1, 0, 0), 0.026, 0.055) # axle in towards the pelvis out.append(emit(PFX + "HipBall_l", bm, "thigh_l", "mech", F_TH)) # thigh: the big hip block at the top (1.jpg, 2.jpg) tapering to the knee bm = bmesh.new() fs = loft(bm, [R(-0.075, -0.055, 0.07, -0.09, 0.09, 0.035), R(-0.02, -0.06, 0.085, -0.125, 0.115, 0.05), R(0.08, -0.06, 0.085, -0.12, 0.105, 0.05), R(0.20, -0.055, 0.065, -0.085, 0.085, 0.035), R(0.27, -0.05, 0.057, -0.062, 0.072, 0.025), R(0.30, -0.045, 0.05, -0.048, 0.062, 0.02)]) # knee bends past 110 deg recess(bm, fs, (1, 0, 0), k=2); recess(bm, fs, (0, 1, 0), k=1, inset=0.014, depth=0.007) recess(bm, fs, (0, -1, 0), k=1) out.append(emit(PFX + "Thigh_l", bm, "thigh_l", "armor", F_TH)) # knee hub: big disc on the inner side (5.jpg, 2.jpg), small cap outside bm = bmesh.new() cyl(bm, (0, 0, 0), (1, 0, 0), 0.042, 0.096, n=16) cyl(bm, (-0.058, 0, 0), (1, 0, 0), 0.052, 0.016, n=20) cyl(bm, (0.053, 0, 0), (1, 0, 0), 0.03, 0.01, n=16) out.append(emit(PFX + "Knee_l", bm, "calf_l", "mech", F_CA)) # shin: its plate rises over the knee front; the calf flares wide, then tapers to the ankle bm = bmesh.new() fs = loft(bm, [R(-0.05, -0.038, 0.04, 0.05, 0.083, 0.012), R(0.0, -0.046, 0.05, 0.05, 0.093, 0.015), R(0.045, -0.054, 0.06, 0.03, 0.093, 0.012, 0.028), R(0.075, -0.06, 0.068, -0.05, 0.092, 0.025, 0.03), R(0.13, -0.068, 0.078, -0.10, 0.088, 0.04, 0.035), R(0.22, -0.064, 0.072, -0.095, 0.08, 0.035, 0.03), R(0.33, -0.045, 0.05, -0.06, 0.06, 0.025), R(0.40, -0.034, 0.038, -0.042, 0.045, 0.016)]) recess(bm, fs, (-1, 0, 0), k=1, inset=0.012, depth=0.009) # louvre vent, inner side recess(bm, fs, (1, 0, 0), k=1); recess(bm, fs, (0, 1, 0), k=1) out.append(emit(PFX + "Shin_l", bm, "calf_l", "armor", F_CA)) bm = bmesh.new() slats(bm, None, (-0.062, 0.0, 0.14), (-0.062, 0.0, 0.21), 5, (0.006, 0.07, 0.005)) out.append(emit(PFX + "CalfVent_l", bm, "calf_l", "mech", F_CA)) # ankle (5.jpg): pitch hub with discs, a roll joint below, on a tall heel bm = bmesh.new() # 5.jpg: a disc facing forward at the ankle, on the shin's end cyl(bm, A, (0, 1, 0), 0.033, 0.05, n=20) cyl(bm, A + V((0, -0.027, 0)), (0, 1, 0), 0.02, 0.006, n=16) out.append(emit(PFX + "AnkleHousing_l", bm, "calf_l", "mech")) bm = bmesh.new() # the foot's fork either side, roll joint below for dx in (-0.046, 0.046): cyl(bm, A + V((dx, 0, 0)), (1, 0, 0), 0.03, 0.012, n=16) loft(bm, [R(0.07, A.x + dx - 0.006, A.x + dx + 0.006, A.y - 0.022, A.y + 0.022, 0.004), R(A.z, A.x + dx - 0.006, A.x + dx + 0.006, A.y - 0.018, A.y + 0.018, 0.004)]) cyl(bm, (A.x, A.y + 0.005, 0.104), (0, 1, 0), 0.023, 0.07, n=16) out.append(emit(PFX + "Ankle_l", bm, "foot_l", "mech")) x = A.x bm = bmesh.new() # foot: a wedge, wide at the sole, sloping to the toe fs = loft(bm, [TRAP(0.085, x - 0.05, x + 0.05, x - 0.035, x + 0.035, 0.014, 0.065, 0.004, 0.02), TRAP(0.05, x - 0.062, x + 0.062, x - 0.045, x + 0.045, 0.014, 0.09, 0.004, 0.025), TRAP(0.0, x - 0.064, x + 0.064, x - 0.045, x + 0.045, 0.014, 0.085, 0.004, 0.025), TRAP(-0.04, x - 0.062, x + 0.062, x - 0.042, x + 0.042, 0.014, 0.06, 0.004, 0.02), TRAP(-0.083, x - 0.058, x + 0.058, x - 0.038, x + 0.038, 0.014, 0.035, 0.004, 0.012)]) recess(bm, fs, (1, 0, 0), k=1, inset=0.008); 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, [TRAP(0.09, x - 0.05, x + 0.05, x - 0.05, x + 0.05, 0.0, 0.014, 0.004, 0.002), TRAP(-0.083, x - 0.058, x + 0.058, x - 0.058, x + 0.058, 0.0, 0.014, 0.004, 0.002)]) out.append(emit(PFX + "Sole_l", bm, "foot_l", "mech")) bm = bmesh.new() loft(bm, [TRAP(-0.088, x - 0.056, x + 0.056, x - 0.037, x + 0.037, 0.014, 0.034, 0.004, 0.012), TRAP(-0.16, x - 0.05, x + 0.05, x - 0.03, x + 0.03, 0.014, 0.025, 0.004, 0.009), TRAP(-0.215, x - 0.032, x + 0.032, x - 0.02, x + 0.02, 0.012, 0.016, 0.003, 0.003)]) out.append(emit(PFX + "Toe_l", bm, "ball_l", "armor")) bm = bmesh.new() loft(bm, [TRAP(-0.088, x - 0.056, x + 0.056, x - 0.056, x + 0.056, 0.0, 0.014, 0.004, 0.002), TRAP(-0.215, x - 0.032, x + 0.032, x - 0.032, x + 0.032, 0.0, 0.012, 0.003, 0.002)]) out.append(emit(PFX + "ToeSole_l", bm, "ball_l", "mech")) return out # ------------------------------------------------------------------ arm (left; frames: x up-out, y forward, z along) def part_arm_l(): out = [] bm = bmesh.new() ball(bm, (0, 0, 0), 0.042, n=16, rings=10) # a ball: the same at every angle out.append(emit(PFX + "ShoulderJoint_l", bm, "upperarm_l", "mech", F_UA)) # shoulder cap on top of the arm (3.jpg: faceted block with a sticker on its front-outer face) bm = bmesh.new() fs = loft(bm, [R(-0.06, -0.01, 0.07, -0.08, 0.08, 0.028), # wider than tall (5.jpg: 0.14 x 0.09) R(-0.02, -0.03, 0.10, -0.095, 0.095, 0.04), R(0.035, -0.05, 0.10, -0.10, 0.10, 0.04), R(0.07, -0.045, 0.085, -0.09, 0.09, 0.034)]) recess(bm, fs, (0, 1, 0), k=1); recess(bm, fs, (1, 0, 0), k=1) out.append(emit(PFX + "ShoulderCap_l", bm, "upperarm_l", "armor", F_UA)) # upper arm block: wide, faceted, text along its outer side (3.jpg) bm = bmesh.new() fs = loft(bm, [R(0.08, -0.058, 0.08, -0.085, 0.085, 0.032), R(0.165, -0.07, 0.095, -0.095, 0.095, 0.04), R(0.25, -0.058, 0.075, -0.082, 0.072, 0.034, 0.03), R(0.285, -0.044, 0.054, -0.066, 0.03, 0.025, 0.012)]) # inside of the elbow cut back recess(bm, fs, (1, 0, 0), k=1); recess(bm, fs, (0, 1, 0), k=1, inset=0.012, depth=0.006) recess(bm, fs, (0, -1, 0), k=1) 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.09, n=16) out.append(emit(PFX + "Elbow_l", bm, "lowerarm_l", "mech", F_LA)) # forearm: long square block, 0.11 thick, cut-outs near the elbow, a latch slot (1.jpg) bm = bmesh.new() fs = loft(bm, [R(0.075, -0.046, 0.05, -0.055, 0.01, 0.022, 0.006), # 0.125 thick (5.jpg overlay) R(0.14, -0.06, 0.064, -0.064, 0.064, 0.027), R(0.215, -0.062, 0.066, -0.066, 0.066, 0.027), R(0.245, -0.057, 0.061, -0.062, 0.064, 0.025), R(0.26, -0.044, 0.048, -0.048, 0.05, 0.018)]) # wrist bends 50 deg clear recess(bm, fs, (1, 0, 0), k=2, inset=0.012, depth=0.008) recess(bm, fs, (0, 1, 0), k=1, inset=0.016, depth=0.006); recess(bm, fs, (-1, 0, 0), k=1) out.append(emit(PFX + "Forearm_l", bm, "lowerarm_l", "armor", F_LA)) # hand: dark palm, armoured back plate bm = bmesh.new() cyl(bm, (0, 0, 0), (0, 1, 0), 0.026, 0.05) loft(bm, [R(0.02, -0.012, 0.016, -0.026, 0.026, 0.008), R(0.035, -0.016, 0.021, -0.04, 0.04, 0.008), R(0.06, -0.017, 0.022, -0.05, 0.05, 0.008), R(0.107, -0.015, 0.02, -0.052, 0.052, 0.008)]) out.append(emit(PFX + "Palm_l", bm, "hand_l", "mech", F_HA)) bm = bmesh.new() fs = loft(bm, [R(0.042, 0.022, 0.034, -0.042, 0.042, 0.006), R(0.102, 0.02, 0.032, -0.05, 0.05, 0.006)]) recess(bm, fs, (1, 0, 0), k=1, inset=0.008) out.append(emit(PFX + "HandPlate_l", bm, "hand_l", "armor", F_HA)) # fingers, each segment on its own bone; red caps on the middle segments' backs (1.jpg) bl = bones_left() hc = coll("VR | Hands", coll("VR | Body")) for f in [f for f, _, _ in FINGERS] + ["thumb"]: 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 w = FW if f != "thumb" else 0.012 bm = bmesh.new() cyl(bm, (0, 0, 0), (0, 1, 0), 0.0095, 2 * w * 0.8, n=10) end = L - 0.0004 if i == 3 else L - 0.0105 # clear of the next knuckle loft(bm, [R(0.006, -0.0105, 0.011, -w * 0.85, w * 0.85, 0.003), R(end, -0.01, 0.0102, -w * 0.8, w * 0.8, 0.003)]) out.append(emit(f"{PFX}Finger_{f}_{i:02d}_l", bm, bn, "mech", M, hc)) if i == 2 and f != "thumb": bm = bmesh.new() loft(bm, [R(0.008, 0.009, 0.0145, -w * 0.7, w * 0.7, 0.002), R(end - 0.002, 0.009, 0.0135, -w * 0.65, w * 0.65, 0.002)]) out.append(emit(f"{PFX}FingerCap_{f}_l", bm, bn, "red", M, hc)) 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(): for o in [o for o in bpy.data.objects if "vr_bone" in o.keys()]: # parts of the last build me = o.data; bpy.data.objects.remove(o) if me and me.users == 0: bpy.data.meshes.remove(me) top = coll("VeiledRobot") coll("VR | Body", 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}