# Robotic arm for The Descent -- procedural Blender build (run inside Blender, tested on 5.2). # Original geometry; look is inspired by "Robotic Arm" by Dries Deryckere on Sketchfab (reference only, nothing imported). # Every joint is its own object with the origin on its axis, so three.js can rotate them directly: # RA_Root > RA_UpperArm > RA_Forearm (hinge Y) > RA_Wrist (roll X) > RA_Hand (flex Y) > RA__1/2/3 (curl +Y) # RA_PistonRod slides on its local X inside the forearm. # Arm points along +X, palm faces -Z, thumb on +Y. Units are metres (about 0.78 m shoulder to fingertip). # v2: layered angular plates, chamfered armour edges, bolts, panel lines, grille, elbow pod, strut, knuckle discs. import bpy, bmesh, math from math import pi, radians, cos, sin from mathutils import Vector, Matrix # ---------- collection (rerunnable: wipes only its own objects) ---------- COL = 'RoboArm' col = bpy.data.collections.get(COL) if not col: col = bpy.data.collections.new(COL); bpy.context.scene.collection.children.link(col) for o in list(col.objects): me = o.data; bpy.data.objects.remove(o) if me and me.users == 0 and isinstance(me, bpy.types.Mesh): bpy.data.meshes.remove(me) def mat(name, color, metal, rough): m = bpy.data.materials.get(name) or bpy.data.materials.new(name) if not m.node_tree: m.use_nodes = True b = next((n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None) if b: b.inputs['Base Color'].default_value = (*color, 1); b.inputs['Metallic'].default_value = metal; b.inputs['Roughness'].default_value = rough m.diffuse_color = (*color, 1); m.metallic = metal; m.roughness = rough return m MATS = [mat('RA_ArmorWhite', (0.80, 0.80, 0.78), 0.1, 0.35), mat('RA_Rubber', (0.025, 0.035, 0.05), 0.0, 0.55), mat('RA_Frame', (0.07, 0.07, 0.075), 0.6, 0.45), mat('RA_Steel', (0.55, 0.56, 0.58), 1.0, 0.25), mat('RA_HoseRed', (0.45, 0.03, 0.03), 0.0, 0.4), mat('RA_Brass', (0.6, 0.42, 0.15), 1.0, 0.3), mat('RA_ArmorGrey', (0.42, 0.43, 0.44), 0.2, 0.4)] WHITE, RUBBER, FRAME, STEEL, RED, BRASS, GREY = range(7) AX = {'Z': Matrix.Identity(4), 'Y': Matrix.Rotation(-pi/2, 4, 'X'), 'X': Matrix.Rotation(pi/2, 4, 'Y')} def T(x, y, z, ax='Z'): return Matrix.Translation((x, y, z)) @ AX[ax] # ---------- sections: (x, ry, rz, cy, cz) superellipse slices along X ---------- def sup(th, ry, rz, n): c, s, e = cos(th), sin(th), 2.0/n return math.copysign(abs(c)**e, c)*ry, math.copysign(abs(s)**e, s)*rz def S(x, ry, rz, cy=0, cz=0): return (x, ry, rz, cy, cz) def resample(secs, sub): # radii/offsets ease between sections; anything past index 4 (plate angles) stays linear so cut edges are straight out = [] for a, b in zip(secs, secs[1:]): for k in range(sub): t = k/sub; u = t*t*(3-2*t) out.append(tuple([a[0]+(b[0]-a[0])*t] + [a[j]+(b[j]-a[j])*(u if j < 5 else t) for j in range(1, len(a))])) out.append(tuple(secs[-1])); return out def ev(secs, x): for a, b in zip(secs, secs[1:]): if a[0]-1e-9 <= x <= b[0]+1e-9: t = (x-a[0])/(b[0]-a[0]); u = t*t*(3-2*t) return tuple([x] + [a[j]+(b[j]-a[j])*u for j in range(1, 5)]) raise ValueError(x) def cut(secs, x0, x1, k=4): return [ev(secs, x0+(x1-x0)*i/k) for i in range(k+1)] def lifted(secs, d): return [(x, ry+d, rz+d, cy, cz) for (x, ry, rz, cy, cz) in secs] def lin(v, m): if not isinstance(v, (list, tuple)): return [v]*m if len(v) == m: return list(v) out = [] for i in range(m): f = i*(len(v)-1)/(m-1); j = min(int(f), len(v)-2); out.append(v[j]+(v[j+1]-v[j])*(f-j)) return out def on(secs, x, ang, n=2.0, lift=0.0, spin=0.0): """Matrix that sits a detail on the surface of a section list at (x, angle), local Z = surface normal.""" _, ry, rz, cy, cz = ev(secs, x) y, z = sup(radians(ang), ry, rz, n) nr = Vector((0, math.copysign(abs(y/ry)**(n-1), y)/ry, math.copysign(abs(z/rz)**(n-1), z)/rz)).normalized() q = Vector((0, 0, 1)).rotation_difference(nr).to_matrix().to_4x4() return Matrix.Translation(Vector((x, cy+y, cz+z))+nr*lift) @ q @ Matrix.Rotation(radians(spin), 4, 'Z') # ---------- primitive generators -> (verts, faces) ---------- def loft(secs, seg=16, n=2.0, sub=3): rs = resample(secs, sub); V = []; F = [] for (x, ry, rz, cy, cz) in rs: for k in range(seg): y, z = sup(2*pi*k/seg, ry, rz, n); V.append((x, cy+y, cz+z)) m = len(rs) for i in range(m-1): for k in range(seg): a = i*seg+k; b = i*seg+(k+1) % seg; F.append((a, b, b+seg, a+seg)) F.append(tuple(range(seg))); F.append(tuple(range((m-1)*seg, m*seg))) return V, F def shell(secs, a0, a1, t, seg=10, n=2.0, sub=3): """Armour plate: the a0..a1 degree sector (90 = +Z) of a loft, thickness t. a0/a1 may be per-section lists.""" A0, A1 = lin(a0, len(secs)), lin(a1, len(secs)) rs = resample([s + (A0[i], A1[i]) for i, s in enumerate(secs)], sub); V = []; F = []; w = seg+1 for layer in (0, 1): for (x, ry, rz, cy, cz, b0, b1) in rs: for k in range(w): y, z = sup(radians(b0+(b1-b0)*k/seg), ry-layer*t, rz-layer*t, n); V.append((x, cy+y, cz+z)) m = len(rs); off = m*w for i in range(m-1): for k in range(seg): a = i*w+k; F.append((a, a+1, a+1+w, a+w)); F.append((off+a, off+a+w, off+a+1+w, off+a+1)) for k in (0, seg): a = i*w+k; F.append((a, a+w, off+a+w, off+a)) for i in (0, m-1): for k in range(seg): a = i*w+k; F.append((a, a+1, off+a+1, off+a)) return V, F def cyl(r, h, seg=16, r2=None): r2 = r if r2 is None else r2 V = [(r*cos(2*pi*k/seg), r*sin(2*pi*k/seg), -h/2) for k in range(seg)] + [(r2*cos(2*pi*k/seg), r2*sin(2*pi*k/seg), h/2) for k in range(seg)] F = [(k, (k+1) % seg, seg+(k+1) % seg, seg+k) for k in range(seg)] + [tuple(range(seg)), tuple(range(seg, 2*seg))] return V, F def box(sx, sy, sz): V = [(x*sx/2, y*sy/2, z*sz/2) for x in (-1, 1) for y in (-1, 1) for z in (-1, 1)] return V, [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)] def tube(pts, r, seg=8): pts = [Vector(p) for p in pts]; V = []; F = []; m = len(pts) for i, p in enumerate(pts): t = (pts[min(i+1, m-1)]-pts[max(i-1, 0)]).normalized() u = t.cross(Vector((0, 0, 1))) if u.length < 1e-4: u = t.cross(Vector((0, 1, 0))) u.normalize(); v = t.cross(u) for k in range(seg): a = 2*pi*k/seg; V.append(tuple(p+(u*cos(a)+v*sin(a))*r)) for i in range(m-1): for k in range(seg): a = i*seg+k; b = i*seg+(k+1) % seg; F.append((a, b, b+seg, a+seg)) F.append(tuple(range(seg))); F.append(tuple(range((m-1)*seg, m*seg))) return V, F def bulge(x0, x1, r, cy=0, cz=0, steps=7, p=0.6): return [S(x0+(x1-x0)*t, r*sin(pi*t)**p, r*sin(pi*t)**p, cy, cz) for t in [0.02+0.96*i/(steps-1) for i in range(steps)]] # ---------- part = one object, origin on its joint axis ---------- class Part: def __init__(s, name): s.name = name; s.bm = bmesh.new() def add(s, vf, mi, M=None, bev=0.0): V, F = vf; fs = [] vs = [s.bm.verts.new(M @ Vector(v) if M else v) for v in V] for f in F: try: fa = s.bm.faces.new([vs[i] for i in f]); fa.material_index = mi; fs.append(fa) except ValueError: pass if bev: # chamfer the hard rims so armour edges catch light bmesh.ops.recalc_face_normals(s.bm, faces=fs); s.bm.normal_update() es = list({e for f in fs for e in f.edges if len(e.link_faces) == 2 and e.calc_face_angle(0) > radians(60)}) if es: bmesh.ops.bevel(s.bm, geom=es, offset=bev, offset_type='OFFSET', segments=1, profile=0.5, affect='EDGES') return s def plate(s, secs, n, x0, x1, a0, a1, lift, t, mi, seg=6, bev=0.001, k=4): """Raised plate (or, thin and dark, a panel line) following an existing surface between x0..x1.""" return s.add(shell(lifted(cut(secs, x0, x1, k), lift), lin(a0, k+1), lin(a1, k+1), t, seg, n, 1), mi, bev=bev) def bolts(s, secs, n, pts, r=0.0026, h=0.003, mi=STEEL, lift=0.0): for x, a in pts: s.add(cyl(r, h, 8), mi, on(secs, x, a, n, lift)) return s def finish(s, parent, loc=(0, 0, 0), rot=(0, 0, 0)): bm = s.bm; bmesh.ops.recalc_face_normals(bm, faces=bm.faces) for f in bm.faces: f.smooth = True for e in bm.edges: if len(e.link_faces) == 2: e.smooth = e.calc_face_angle(0) < radians(38) me = bpy.data.meshes.new(s.name); bm.to_mesh(me); bm.free() for m in MATS: me.materials.append(m) ob = bpy.data.objects.new(s.name, me); col.objects.link(ob) ob.parent = parent; ob.location = loc; ob.rotation_euler = [radians(a) for a in rot] return ob root = bpy.data.objects.new('RA_Root', None); root.empty_display_size = 0.05; col.objects.link(root) # ---------- upper arm (origin: shoulder) ---------- UL = 0.30 p = Part('RA_UpperArm') p.add(loft(bulge(-0.066, 0.07, 0.064, steps=9, p=0.5), seg=20), RUBBER) # shoulder ball for sy in (-1, 1): p.add(cyl(0.028, 0.012, 20), STEEL, T(0, sy*0.062, -0.006, 'Y'), bev=0.0015) p.add(cyl(0.011, 0.016, 12), RED, T(0, sy*0.062, -0.006, 'Y')) for k in range(6): p.add(cyl(0.0022, 0.003, 6), FRAME, T(0.02*cos(k*pi/3), sy*0.0685, -0.006+0.02*sin(k*pi/3), 'Y')) PAUL, PN = [S(-0.05, 0.060, 0.064), S(0.0, 0.071, 0.073), S(0.045, 0.066, 0.068), S(0.08, 0.055, 0.058)], 2.4 p.add(shell(PAUL, [42, 26, 28, 46], [138, 154, 152, 134], 0.006, 12, PN), WHITE, bev=0.0012) # pauldron p.plate(PAUL, PN, -0.032, 0.062, [72, 62, 74], [108, 118, 106], 0.004, 0.004, WHITE) # raised crest p.plate(PAUL, PN, 0.020, 0.0215, 30, 150, 0.0008, 0.0015, FRAME, bev=0, k=1) # panel line p.bolts(PAUL, PN, [(-0.03, 50), (-0.03, 130), (0.06, 42), (0.06, 138)]) p.add(loft([S(0.0, 0.03, 0.03), S(UL, 0.028, 0.028)], 12), FRAME) # core for ang, r, x0, x1 in ((70, 0.029, 0.03, 0.245), (140, 0.026, 0.04, 0.24), (205, 0.028, 0.03, 0.245), (270, 0.029, 0.045, 0.25), (335, 0.027, 0.035, 0.24), (20, 0.022, 0.05, 0.235)): a = radians(ang); p.add(loft(bulge(x0, x1, r, 0.032*cos(a), 0.032*sin(a), 8, 0.45), 10), RUBBER) # muscle bundles UPS, UN = [S(0.088, 0.05, 0.058), S(0.13, 0.058, 0.064), S(0.19, 0.056, 0.062), S(0.235, 0.05, 0.054)], 2.6 p.add(shell(UPS, [64, 48, 48, 60], [116, 132, 132, 120], 0.005, 8, UN), WHITE, bev=0.0012) # outer plate, angled ends p.plate(UPS, UN, 0.118, 0.205, [80, 72, 82], [100, 108, 98], 0.003, 0.003, GREY) for i in range(3): p.add(box(0.004, 0.014, 0.004), FRAME, on(UPS, 0.14+i*0.011, 122, UN, spin=-12)) p.bolts(UPS, UN, [(0.102, 62), (0.102, 118), (0.224, 64), (0.224, 116)]) p.plate(UPS, UN, 0.10, 0.22, [252, 240, 252], [288, 300, 288], 0.0, 0.005, WHITE) # inner plate p.bolts(UPS, UN, [(0.115, 250), (0.115, 290), (0.205, 250), (0.205, 290)], mi=BRASS) p.add(loft([S(0.226, 0.049, 0.051), S(0.236, 0.049, 0.051)], 20), STEEL) CUFF, CN = [S(0.236, 0.047, 0.050), S(0.262, 0.049, 0.052), S(0.286, 0.044, 0.046)], 3.0 p.add(loft(CUFF, 20, CN), WHITE, bev=0.0012) # elbow cuff for a in range(15, 360, 30): p.add(box(0.012, 0.003, 0.003), FRAME, on(CUFF, 0.25, a, CN)) # cuff notches p.bolts(CUFF, CN, [(0.274, a) for a in (60, 120, 240, 300)], r=0.003, mi=BRASS) for sy in (-1, 1): # elbow clevis p.add(loft([S(0.262, 0.008, 0.034, sy*0.043), S(0.305, 0.008, 0.032, sy*0.043), S(0.332, 0.008, 0.016, sy*0.043)], 12, 4.0), WHITE, bev=0.001) p.add(cyl(0.028, 0.012, 20), STEEL, T(UL, sy*0.054, 0, 'Y'), bev=0.0012) p.add(cyl(0.019, 0.018, 16), WHITE, T(UL, sy*0.054, 0, 'Y'), bev=0.0012) p.add(cyl(0.007, 0.022, 10), FRAME, T(UL, sy*0.054, 0, 'Y')) for k in range(6): p.add(cyl(0.0022, 0.003, 6), FRAME, T(UL+0.0235*cos(k*pi/3), sy*0.0605, 0.0235*sin(k*pi/3), 'Y')) upper = p.finish(root) # ---------- forearm (origin: elbow axis, hinge = local Y) ---------- FL = 0.27 p = Part('RA_Forearm') p.add(cyl(0.027, 0.07, 20), FRAME, T(0, 0, 0, 'Y')) p.add(loft([S(0.0, 0.026, 0.028), S(0.05, 0.033, 0.038), S(0.10, 0.036, 0.044), S(0.17, 0.033, 0.038), S(0.262, 0.026, 0.026)], 14, 3.0), FRAME) # inner body TOP, TN = [S(0.028, 0.040, 0.044), S(0.085, 0.050, 0.055), S(0.16, 0.046, 0.048), S(0.25, 0.036, 0.034)], 2.6 p.add(shell(TOP, [46, 18, 22, 40], [134, 162, 158, 140], 0.006, 12, TN), WHITE, bev=0.0012) # top shell, angled ends p.plate(TOP, TN, 0.198, 0.25, [34, 18, 18], [146, 162, 162], 0.003, 0.004, WHITE, seg=10) # wrist gauntlet overlap p.plate(TOP, TN, 0.095, 0.19, [78, 68, 80], [102, 112, 100], 0.003, 0.003, GREY) # spine plate p.plate(TOP, TN, 0.148, 0.1495, 24, 156, 0.0008, 0.0015, FRAME, seg=10, bev=0, k=1) # panel line for i in range(3): p.add(box(0.0035, 0.02, 0.004), FRAME, on(TOP, 0.058+i*0.011, 124, TN, spin=-15)) # slanted vents p.bolts(TOP, TN, [(0.04, 60), (0.04, 120), (0.092, 28), (0.092, 152), (0.185, 32), (0.185, 148)]) p.bolts(TOP, TN, [(0.245, 40), (0.245, 140), (0.21, 30), (0.21, 150)], lift=0.003, mi=BRASS) M = on(TOP, 0.218, 90, TN, lift=0.004) # port p.add(cyl(0.013, 0.012, 14, 0.011), WHITE, M, bev=0.001); p.add(cyl(0.0085, 0.014, 12), FRAME, M); p.add(cyl(0.004, 0.017, 8), STEEL, M) p.add(loft(bulge(0.036, 0.122, 0.021, -0.026, 0.036, 7, 0.55), 12), GREY) # elbow pod BOT, BN = [S(0.045, 0.042, 0.046), S(0.10, 0.049, 0.054), S(0.17, 0.045, 0.047), S(0.25, 0.036, 0.034)], 2.6 p.add(shell(BOT, [216, 200, 200, 212], [324, 340, 340, 328], 0.006, 12, BN), WHITE, bev=0.0012) # bottom shell p.plate(BOT, BN, 0.086, 0.15, 204, 240, 0.003, 0.003, WHITE) # grille panel for i in range(5): p.add(box(0.003, 0.016, 0.0012), FRAME, on(BOT, 0.100+i*0.009, 222, BN, lift=0.003)) p.plate(BOT, BN, 0.152, 0.242, [246, 234, 250], [294, 306, 290], 0.0035, 0.0035, GREY) # belly plate p.plate(BOT, BN, 0.158, 0.1595, 202, 338, 0.0008, 0.0015, FRAME, seg=10, bev=0, k=1) p.bolts(BOT, BN, [(0.062, 226), (0.062, 314), (0.2, 210), (0.2, 330), (0.24, 222), (0.24, 318), (0.09, 208), (0.146, 208)]) PY, PZ = -0.043, 0.003 # piston line, -Y slot p.add(cyl(0.0095, 0.085, 12), STEEL, T(0.0925, PY, PZ, 'X')) p.add(cyl(0.012, 0.010, 12), FRAME, T(0.052, PY, PZ, 'X')); p.add(cyl(0.011, 0.008, 12), FRAME, T(0.135, PY, PZ, 'X')) p.add(box(0.014, 0.016, 0.016), FRAME, T(0.226, PY+0.006, PZ)) # rod anchor # tie rod: runs under the piston (barrel/caps bottom out at z -0.009) and never crosses it -- the old diagonal clipped through the barrel and rod SA, SB = (0.066, -0.041, -0.0145), (0.212, -0.041, -0.0175) p.add(tube([SA, SB], 0.0028, 8), STEEL) p.add(cyl(0.0045, 0.008, 8), FRAME, T(*SA, 'Y')); p.add(cyl(0.0045, 0.008, 8), FRAME, T(*SB, 'Y')) for dz, r, m in ((0.010, 0.0042, RED), (-0.001, 0.0036, RED), (-0.011, 0.003, STEEL)): # hoses, +Y slot p.add(tube([(0.02+0.215*t, 0.037+0.009*sin(pi*t)-0.008*t, dz+0.003*sin(2*pi*t)) for t in [i/12 for i in range(13)]], r, 8), m) for x in (0.06, 0.12, 0.18): p.add(box(0.006, 0.006, 0.03), FRAME, T(x, 0.040+0.009*sin(pi*(x-0.02)/0.215)-0.008*(x-0.02)/0.215, 0.0)) # hose clamps p.add(loft([S(0.25, 0.034, 0.031), S(0.264, 0.034, 0.031)], 20, 2.6), STEEL, bev=0.001) forearm = p.finish(upper, (UL, 0, 0)) p = Part('RA_PistonRod') # slides on local X p.add(cyl(0.0045, 0.092, 10), STEEL, T(0.046, 0, 0, 'X')).add(cyl(0.007, 0.006, 10), FRAME, T(0.004, 0, 0, 'X')) p.finish(forearm, (0.135, PY, PZ)) # ---------- wrist (origin: wrist, roll = local X) ---------- p = Part('RA_Wrist') WR = [S(-0.006, 0.029, 0.025), S(0.010, 0.031, 0.024), S(0.024, 0.028, 0.018)] p.add(loft(WR, 16, 2.6), RUBBER) for x in (0.0, 0.007, 0.014): _, ry, rz, _, _ = ev(WR, x); p.add(loft([S(x, ry+0.002, rz+0.002), S(x+0.0035, ry+0.002, rz+0.002)], 16, 2.6), STEEL) wrist = p.finish(forearm, (FL, 0, 0)) # ---------- hand (origin: wrist flex axis = local Y; palm faces -Z, thumb on +Y) ---------- p = Part('RA_Hand') p.add(cyl(0.012, 0.05, 12), FRAME, T(0, 0, 0, 'Y')) p.add(loft([S(0.0, 0.028, 0.013), S(0.045, 0.042, 0.015), S(0.097, 0.043, 0.012)], 16, 4.0), FRAME, bev=0.001) # palm block BACK, HN = [S(0.004, 0.031, 0.018), S(0.045, 0.045, 0.020), S(0.088, 0.046, 0.017)], 4.0 p.add(shell(BACK, [30, 22, 22], [150, 158, 158], 0.004, 10, HN), WHITE, bev=0.001) # back plate p.add(loft([S(0.020, 0.024, 0.003, 0, 0.0195), S(0.072, 0.030, 0.003, 0, 0.0195)], 12, 5.0), FRAME) # panel outline p.add(loft([S(0.023, 0.021, 0.003, 0, 0.0215), S(0.069, 0.027, 0.003, 0, 0.0215)], 12, 5.0), WHITE, bev=0.0008) # raised panel p.bolts(BACK, HN, [(0.012, 50), (0.012, 130), (0.082, 36), (0.082, 144)], r=0.0022) for sy in (-1, 1): p.add(loft([S(0.03, 0.0012, 0.004, sy*0.0445, 0.004), S(0.042, 0.0016, 0.006, sy*0.0445, 0.004), S(0.054, 0.0012, 0.004, sy*0.0445, 0.004)], 10), BRASS) p.add(loft([S(0.014, 0.022, 0.005, 0, -0.013), S(0.05, 0.034, 0.007, 0, -0.014), S(0.086, 0.036, 0.005, 0, -0.012)], 12, 3.0), RUBBER) # palm pad p.add(loft(bulge(0.012, 0.062, 0.011, 0.02, -0.012, 6, 0.6), 10), RUBBER) # thumb-base pad for x in (0.05, 0.07): p.add(box(0.002, 0.052, 0.002), FRAME, T(x, 0, -0.0208)) # palm creases FING = { # name: (base x, base y, radius, (L1,L2,L3), splay deg) 'Index': (0.097, 0.0315, 0.0095, (0.042, 0.026, 0.023), 4), 'Middle': (0.100, 0.0105, 0.0100, (0.046, 0.029, 0.024), 1), 'Ring': (0.097, -0.0105, 0.0095, (0.043, 0.027, 0.023), -2), 'Pinky': (0.090, -0.0315, 0.0083, (0.034, 0.021, 0.020), -6)} for bx, by, R, Ls, sp in FING.values(): p.add(loft([S(bx-0.022, R*0.95, 0.006, by, 0.013), S(bx-0.004, R*0.95, 0.007, by, 0.013), S(bx+0.004, R*0.8, 0.004, by, 0.011)], 10, 3.5), WHITE, bev=0.0007) # knuckle guard p.add(cyl(0.0088, R*2.06, 14), GREY, T(bx, by, 0, 'Y'), bev=0.0008) # knuckle disc hand = p.finish(wrist, (0.022, 0, 0)) def phalanx(name, parent, loc, rot, L, R, tip): p = Part(name) p.add(cyl(R*0.8, R*1.7, 12), STEEL, T(0, 0, 0, 'Y')).add(cyl(R*0.3, R*1.8, 8), FRAME, T(0, 0, 0, 'Y')) # knuckle pin if tip: body = [S(R*0.5, R*0.9, R*0.9), S(L*0.45, R, R), S(L*0.78, R*0.92, R*0.88), S(L*0.95, R*0.7, R*0.6), S(L, R*0.3, R*0.25)] else: body = [S(R*0.5, R*0.9, R*0.9), S(L*0.4, R, R), S(L-R*0.45, R*0.9, R*0.88)] p.add(loft(body, 12, 4.2), WHITE, bev=0.0007) p.add(loft([S(L*0.28, R*0.6, R*0.3, 0, -R*0.82), S(L*0.52, R*0.72, R*0.38, 0, -R*0.82), S(L*0.74, R*0.5, R*0.28, 0, -R*0.76)], 10, 2.5), RUBBER) # grip pad if tip: p.add(loft([S(L*0.3, R*0.4, R*0.16, 0, R*0.88), S(L*0.5, R*0.52, R*0.18, 0, R*0.88), S(L*0.7, R*0.42, R*0.16, 0, R*0.84)], 10, 2.5), FRAME) # nail inset else: p.add(loft([S(L*0.24, R*0.55, R*0.14, 0, R*0.97), S(L*0.66, R*0.64, R*0.14, 0, R*0.97)], 8, 4.0), GREY) # dorsal plate xb = L*0.8; p.add(loft([S(xb-0.0007, R*0.97, R*0.95), S(xb+0.0007, R*0.97, R*0.95)], 12, 4.2), FRAME) # segment seam return p.finish(parent, loc, rot) for fname, (bx, by, R, Ls, sp) in FING.items(): a = phalanx(f'RA_{fname}_1', hand, (bx, by, 0), (0, 12, sp), Ls[0], R, False) b = phalanx(f'RA_{fname}_2', a, (Ls[0], 0, 0), (0, 18, 0), Ls[1], R*0.95, False) phalanx(f'RA_{fname}_3', b, (Ls[1], 0, 0), (0, 12, 0), Ls[2], R*0.9, True) a = phalanx('RA_Thumb_1', hand, (0.022, 0.034, -0.006), (-55, 18, 50), 0.042, 0.0115, False) b = phalanx('RA_Thumb_2', a, (0.042, 0, 0), (0, 14, 0), 0.032, 0.0105, False) phalanx('RA_Thumb_3', b, (0.032, 0, 0), (0, 10, 0), 0.028, 0.0098, True) # ---------- preview rig (camera + sun; not exported) ---------- c = bpy.data.objects.get('Cube') if c: c.hide_viewport = True; c.hide_render = True def look(ob, frm, to): ob.location = frm; ob.rotation_euler = (Vector(to)-Vector(frm)).to_track_quat('-Z', 'Y').to_euler() cam = bpy.data.objects.get('RA_Cam') if not cam: cam = bpy.data.objects.new('RA_Cam', bpy.data.cameras.new('RA_Cam')) if cam.name not in col.objects: col.objects.link(cam) sun = bpy.data.objects.get('RA_Sun') if not sun: sun = bpy.data.objects.new('RA_Sun', bpy.data.lights.new('RA_Sun', 'SUN')) if sun.name not in col.objects: col.objects.link(sun) sun.data.energy = 4; look(sun, (0.2, -1, 1.5), (0.4, 0, 0)) cam.data.lens = 50; look(cam, (1.1, -0.7, 0.45), (0.38, 0, -0.02)) sc = bpy.context.scene; sc.camera = cam; sc.render.resolution_x = 1400; sc.render.resolution_y = 800; sc.render.resolution_percentage = 100 bpy.context.view_layer.update()