saumwacht-godot/assets-source/luneraria/build_peoples.py
Martin Stickelbroeck faf0fbdef0 M3: Balance, Kartenprops, sieben Volksrepräsentanten und Abnahme
Bots planen Items/Champions vor der Armee und helfen deterministisch,
damit Spiegelmatches fair bleiben. Boss und Profile neu abgestimmt;
Balance-, Stress- und Volks-Werkzeuge sowie Kartenprops ergänzt.
2026-09-28 00:41:05 +02:00

1457 lines
91 KiB
Python

"""Seven people representatives for Champions of Luneraria, one weighted skeleton each:
Mensch (Ritter), Elfe (Waldläuferin), Zwerg (Schildträger), Troll (Steinhauer),
Fee (Lichtheilerin), Ork (Kriegshäuptling), Biestmensch (Klauenjäger).
Built like the Funkenlanze (phase 18): continuous deforming limbs and torso, bevelled armour
plates, rings, shards and closed hands. Hands hold their weapons through analytic two-bone
arm IK solved per sample and baked to FK, so grips never slide. Attack contact is 0.90 s as
SkeletalFigure expects. LP_* materials carry a SurfaceColor vertex layer for the shared
runtime surface shader. Colours are authored as display (sRGB) values and stored linear.
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_peoples.py [-- p_elf ...]
Blender Z-up, figures face -Y (glTF +Z forward). Rigify side convention: .L at +X.
"""
from pathlib import Path
import bpy, bmesh, json, hashlib, struct, sys
from math import sin, cos, pi, sqrt
from mathutils import Vector, Quaternion
P = Path(__file__).resolve().parents[2]
SRC = Path(__file__).resolve().parent
OUT = P / 'assets/luneraria/peoples'
OUT.mkdir(parents=True, exist_ok=True)
(SRC / 'peoples').mkdir(exist_ok=True)
CLIPS = {'idle': (3.0, True), 'start': (.4, False), 'walk': (1.1, True), 'stop': (.4, False), 'attack_a': (1.3, False),
'attack_b': (1.3, False), 'hit': (.5, False), 'ability': (1.3, False), 'death': (2.0, False)}
CONTACT = .90
SIDES = (('L', 1), ('R', -1))
V = Vector
KIND_NAMES = {'skin': 'Skin', 'cloth': 'Cloth', 'cloth2': 'ClothAccent', 'leather': 'Leather', 'metal': 'Metal', 'trim': 'TrimMetal',
'dark': 'Dark', 'hair': 'Hair', 'wood': 'Wood', 'stone': 'Stone', 'bone': 'Bone', 'fur': 'Fur', 'fur2': 'FurLight',
'glow': 'Glow', 'eye': 'EyeGlow'}
# Palettes: display colour, roughness, metallic, emission.
BASE = {'dark': ((.07, .065, .07), .7, .1, 0), 'bone': ((.86, .82, .70), .55, 0, 0), 'leather': ((.40, .27, .17), .78, 0, 0),
'wood': ((.42, .27, .15), .82, 0, 0), 'stone': ((.46, .45, .44), .9, 0, 0)}
PEOPLES = {
'p_mensch': {'tag': 'Mensch', 'name': 'Ritter', 'people': 'Menschen', 'klasse': 'Ritter', 'rolle': 'front',
's': 1.0, 'w': 1.02, 'd': 1.0, 'limb': 1.05, 'arm': 1.0, 'leg': 1.0, 'head': 1.0, 'style': 'blade_shield',
'weapon': 'sword', 'shield': 'kite',
'pal': {'skin': ((.86, .68, .56), .6, 0, 0), 'cloth': ((.16, .26, .60), .86, 0, 0), 'cloth2': ((.90, .88, .82), .86, 0, 0),
'metal': ((.72, .74, .78), .3, .85, 0), 'trim': ((.88, .68, .30), .32, .8, 0)}},
'p_elf': {'tag': 'Elf', 'name': 'Waldläuferin', 'people': 'Elfen', 'klasse': 'Waldläuferin', 'rolle': 'ranged',
's': 1.06, 'w': .84, 'd': .9, 'limb': .82, 'arm': 1.02, 'leg': 1.1, 'head': .94, 'style': 'bow',
'pal': {'skin': ((.86, .70, .58), .55, 0, 0), 'cloth': ((.20, .44, .26), .86, 0, 0), 'cloth2': ((.12, .26, .16), .88, 0, 0),
'trim': ((.78, .74, .52), .3, .75, 0), 'hair': ((.84, .66, .36), .7, 0, 0), 'glow': ((.55, 1.0, .62), .3, 0, 1.6),
'leather': ((.46, .33, .20), .76, 0, 0)}},
'p_zwerg': {'tag': 'Zwerg', 'name': 'Schildträger', 'people': 'Zwerge', 'klasse': 'Schildträger', 'rolle': 'front',
's': .78, 'w': 1.34, 'd': 1.22, 'limb': 1.32, 'arm': .9, 'leg': .78, 'head': 1.2, 'style': 'blade_shield',
'weapon': 'hammer', 'shield': 'round',
'pal': {'skin': ((.84, .60, .48), .6, 0, 0), 'cloth': ((.50, .13, .10), .86, 0, 0), 'metal': ((.52, .52, .56), .34, .82, 0),
'trim': ((.82, .54, .22), .32, .8, 0), 'hair': ((.74, .33, .12), .72, 0, 0), 'glow': ((.40, .86, 1.0), .3, 0, 1.8),
'cloth2': ((.30, .10, .08), .86, 0, 0)}},
'p_troll': {'tag': 'Troll', 'name': 'Steinhauer', 'people': 'Trolle', 'klasse': 'Steinhauer', 'rolle': 'splash',
's': 1.3, 'w': 1.42, 'd': 1.3, 'limb': 1.5, 'arm': 1.25, 'leg': .86, 'head': .92, 'hunch': .42, 'style': 'club2h',
'pal': {'skin': ((.40, .54, .58), .72, 0, 0), 'leather': ((.44, .34, .24), .82, 0, 0), 'eye': ((1.0, .80, .30), .3, 0, 2.2),
'hair': ((.20, .22, .20), .8, 0, 0), 'stone': ((.50, .50, .52), .92, 0, 0)}},
'p_fee': {'tag': 'Fee', 'name': 'Lichtheilerin', 'people': 'Feen', 'klasse': 'Lichtheilerin', 'rolle': 'support',
's': .7, 'w': .78, 'd': .82, 'limb': .74, 'arm': 1.0, 'leg': 1.06, 'head': 1.14, 'hover': .32, 'style': 'staff',
'pal': {'skin': ((.90, .76, .72), .5, 0, 0), 'cloth': ((.62, .46, .88), .8, 0, 0), 'cloth2': ((.92, .90, .96), .8, 0, 0),
'trim': ((.94, .86, .60), .3, .75, 0), 'hair': ((.99, .78, .90), .7, 0, 0), 'glow': ((.62, .95, 1.0), .3, 0, 1.5),
'wood': ((.78, .66, .48), .7, 0, 0)}},
'p_ork': {'tag': 'Ork', 'name': 'Kriegshäuptling', 'people': 'Orks', 'klasse': 'Kriegshäuptling', 'rolle': 'melee',
's': 1.12, 'w': 1.3, 'd': 1.16, 'limb': 1.3, 'arm': 1.06, 'leg': .95, 'head': 1.04, 'hunch': .16, 'style': 'axe',
'pal': {'skin': ((.42, .58, .28), .66, 0, 0), 'metal': ((.38, .37, .37), .38, .8, 0), 'trim': ((.66, .14, .10), .8, 0, 0),
'hair': ((.10, .09, .08), .8, 0, 0), 'fur': ((.36, .28, .20), .95, 0, 0), 'leather': ((.36, .23, .15), .8, 0, 0)}},
'p_biest': {'tag': 'Biest', 'name': 'Klauenjäger', 'people': 'Biestmenschen', 'klasse': 'Klauenjäger', 'rolle': 'melee',
's': 1.1, 'w': 1.12, 'd': 1.1, 'limb': 1.18, 'arm': 1.08, 'leg': 1.02, 'head': 1.06, 'hunch': .22, 'style': 'claws',
'digitigrade': True,
'pal': {'fur': ((.56, .38, .24), .95, 0, 0), 'fur2': ((.82, .72, .56), .95, 0, 0), 'cloth': ((.18, .44, .44), .86, 0, 0),
'metal': ((.62, .46, .28), .34, .78, 0), 'eye': ((1.0, .70, .20), .3, 0, 2.2), 'skin': ((.30, .22, .18), .7, 0, 0)}},
}
def lin(c):
return tuple(x ** 2.2 for x in c)
def blend(a, b, t):
t = max(0.0, min(1.0, t))
return {a: 1 - t, b: t} if a != b else {a: 1}
def catmull(points, t):
seg = min(len(points) - 2, int(t * (len(points) - 1)))
u = t * (len(points) - 1) - seg
a, b, c, d = [points[max(0, min(len(points) - 1, q))] for q in (seg - 1, seg, seg + 1, seg + 2)]
return .5 * ((2 * b) + (-a + c) * u + (2 * a - 5 * b + 4 * c - d) * u * u + (-a + 3 * b - 3 * c + d) * u * u * u)
def curve(t, keys):
for (a, x), (b, y) in zip(keys, keys[1:]):
if t <= b:
u = max(0, (t - a) / (b - a))
u = u * u * (3 - 2 * u)
return x * (1 - u) + y * u
return keys[-1][1]
def ik_elbow(a, c, l1, l2, pole):
"""Two-bone solve; an unreachable target is clamped and the shortfall returned."""
delta = c - a
d = delta.length
error = 0.0
hi, lo = l1 + l2 - 1e-4, abs(l1 - l2) + 1e-4
if d > hi:
error, c, d = d - hi, a + delta.normalized() * hi, hi
elif d < lo:
error, c, d = lo - d, a + delta.normalized() * lo, lo
line = (c - a) / d
v = V(pole) - a
perpendicular = (v - line * v.dot(line)).normalized()
along = (l1 * l1 - l2 * l2 + d * d) / (2 * d)
return a + line * along + perpendicular * sqrt(max(0, l1 * l1 - along * along)), c, error
def frame(u, hint):
u = V(u).normalized()
f = V(hint) - u * V(hint).dot(u)
f.normalize()
return u, f, u.cross(f)
class Figure:
def __init__(self, key, sp):
self.key, self.sp = key, sp
self.parts, self.defs, self.mats, self.errors = [], {}, {}, []
self.s = sp['s']
self.pal = dict(BASE)
self.pal.update(sp['pal'])
self.ankle = .085
self.hip = self.ankle + .84 * sp['leg']
self.knee = self.ankle + .84 * sp['leg'] * .52
self.waist = self.hip + .13
self.chest = self.hip + .38
self.shoulder = self.hip + .52
self.neck = self.hip + .60
self.head_c = self.neck + .10 + .085 * sp['head']
self.hover = sp.get('hover', 0.0)
self.extra_bones = []
self.scaled = set()
# ------------------------------------------------------------ space
def X(self, p):
"""Pre-space (upright human proportions) -> final space: bend, scale, hover."""
x, y, z = p
a = self.sp.get('hunch', 0.0)
if a and z > self.waist:
t = min(1.0, (z - self.waist) / (self.shoulder - self.waist))
ang = a * t * t * (3 - 2 * t)
dz = z - self.waist
y, z = y * cos(ang) - dz * sin(ang), self.waist + y * sin(ang) + dz * cos(ang)
return V((x * self.s, y * self.s, z * self.s + self.hover))
# ------------------------------------------------------------ materials and meshes
def mat(self, kind):
name = 'LP_%s_%s' % (self.sp['tag'], KIND_NAMES[kind])
if name in self.mats:
return self.mats[name]
color, rough, metal, emission = self.pal[kind]
color = lin(color)
m = bpy.data.materials.new(name)
m.diffuse_color = (*color, 1)
m.use_nodes = True
n, links = m.node_tree.nodes, m.node_tree.links
bs = n.get('Principled BSDF')
bs.inputs['Base Color'].default_value = (*color, 1)
bs.inputs['Metallic'].default_value = metal
bs.inputs['Roughness'].default_value = rough
if not emission:
vertex = n.new('ShaderNodeVertexColor')
vertex.layer_name = 'SurfaceColor'
links.new(vertex.outputs['Color'], bs.inputs['Base Color'])
if emission:
bs.inputs['Emission Color'].default_value = (*color, 1)
bs.inputs['Emission Strength'].default_value = emission
else:
tex = n.new('ShaderNodeTexNoise')
tex.inputs['Scale'].default_value = 65 if metal else 30
bump = n.new('ShaderNodeBump')
bump.inputs['Strength'].default_value = .12 if metal else .2
bump.inputs['Distance'].default_value = .006
links.new(tex.outputs['Fac'], bump.inputs['Height'])
links.new(bump.outputs['Normal'], bs.inputs['Normal'])
self.mats[name] = m
return m
def mesh(self, name, vs, fs, kind, bone, weights=None, smooth=True, uv=None, pre=True):
vs = [self.X(v) if pre else V(v) for v in vs]
me = bpy.data.meshes.new(name)
me.from_pydata([tuple(v) for v in vs], [], fs)
me.update()
ob = bpy.data.objects.new(name, me)
bpy.context.collection.objects.link(ob)
self.parts.append(ob)
m = self.mat(kind)
me.materials.append(m)
col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER')
tex = me.uv_layers.new(name='UVMap')
base = m.diffuse_color[:3]
for face in me.polygons:
face.use_smooth = smooth
for li in face.loop_indices:
vi = me.loops[li].vertex_index
x, y, z = vs[vi]
variation = .93 + .05 * sin(17 * x + 11 * z) + .025 * cos(24 * y - 7 * z)
col.data[li].color = (*(min(1, c * variation) for c in base), 1)
tex.data[li].uv = uv[vi] if uv else (x * 1.7 + y * .3, z * 1.5)
groups = {}
for i in range(len(vs)):
ws = weights[i] if weights else {bone: 1.0}
total = sum(ws.values())
for key, weight in ws.items():
if weight <= 1e-8:
continue
groups.setdefault(key, ob.vertex_groups.get(key) or ob.vertex_groups.new(name=key))
groups[key].add([i], weight / total, 'REPLACE')
return ob
def tube(self, name, points, radii, kind, bone, sides=16, steps=20, flatten=1.0, weight_fn=None, groove=0.0,
smooth=True, pre=True, ref=None, lobes=5):
pts = [V(x) for x in points]
vs, fs, ws, uv = [], [], [], []
for j in range(steps + 1):
t = j / steps
p = catmull(pts, t)
seg = min(len(radii) - 2, int(t * (len(radii) - 1)))
a = t * (len(radii) - 1) - seg
radius = radii[seg] * (1 - a) + radii[seg + 1] * a
tangent = (catmull(pts, min(1, t + .002)) - catmull(pts, max(0, t - .002))).normalized()
r = V(ref) if ref else (V((0, 1, 0)) if abs(tangent.y) < .9 else V((1, 0, 0)))
u = tangent.cross(r).normalized()
v = tangent.cross(u)
for k in range(sides):
angle = 2 * pi * k / sides
detail = 1 + groove * cos(lobes * angle + t * 4)
vs.append(p + radius * detail * (u * cos(angle) + v * sin(angle) * flatten))
ws.append(weight_fn(t) if weight_fn else {bone: 1})
uv.append((k / sides, t))
if j:
for k in range(sides):
a0, b0 = (j - 1) * sides + k, (j - 1) * sides + (k + 1) % sides
fs.append((a0, b0, b0 + sides, a0 + sides))
fs += [tuple(reversed(range(sides))), tuple(range(steps * sides, (steps + 1) * sides))]
return self.mesh(name, vs, fs, kind, bone, ws, smooth, uv, pre)
def ell(self, name, c, r, kind, bone, seg=18, rings=11, smooth=True, pre=True, axes=None, bumps=0.0, weights=None):
ax = [V(a).normalized() for a in axes] if axes else [V((1, 0, 0)), V((0, 1, 0)), V((0, 0, 1))]
c = V(c)
def at(th, a):
w = 1 + bumps * sin(3 * a + 5 * th) * sin(2 * th)
return c + ax[0] * r[0] * sin(th) * cos(a) * w + ax[1] * r[1] * sin(th) * sin(a) * w + ax[2] * r[2] * cos(th) * w
vs = [c + ax[2] * r[2]]
for j in range(1, rings):
for k in range(seg):
vs.append(at(pi * j / rings, 2 * pi * k / seg))
vs.append(c - ax[2] * r[2])
fs = [(0, 1 + k, 1 + (k + 1) % seg) for k in range(seg)]
for j in range(rings - 2):
for k in range(seg):
a0 = 1 + j * seg + k
b0 = 1 + j * seg + (k + 1) % seg
fs.append((a0, a0 + seg, b0 + seg, b0))
fs += [(len(vs) - 1, len(vs) - 2 - k, len(vs) - 2 - (k + 1) % seg) for k in range(seg)]
ws = [weights(v) for v in vs] if weights else None
return self.mesh(name, vs, fs, kind, bone, ws, smooth, None, pre)
def plate(self, name, outline, center, depth, kind, bone, normal=(0, -1, 0), pre=True, weights=None):
"""Closed tapered plate with bevel belt and a proud central ridge, never a box."""
c, n = V(center), V(normal).normalized()
front = [V(p) for p in outline]
vs = front + [p.lerp(c, .13) + n * depth * .32 for p in front] + [c + n * depth] + [p - n * depth * .22 for p in front]
k = len(front)
fs = []
for i in range(k):
q = (i + 1) % k
fs += [(i, q, k + q, k + i), (k + i, k + q, 2 * k), (i, 2 * k + 1 + i, 2 * k + 1 + q, q)]
fs.append(tuple(reversed(range(2 * k + 1, 3 * k + 1))))
ws = [weights(v) for v in vs] if weights else None
return self.mesh(name, vs, fs, kind, bone, ws, False, None, pre)
def ring(self, name, p, axis, radius, width, kind, bone, sides=16, pre=True):
axis, p = V(axis).normalized(), V(p)
return self.tube(name, [p - axis * width / 2, p + axis * width / 2], [radius, radius], kind, bone, sides, 3, smooth=False, pre=pre)
def shard(self, name, points, radius, kind, bone, pre=True, sides=6):
return self.tube(name, points, [radius, .74 * radius, .002], kind, bone, sides, 5, smooth=False, pre=pre)
def cone(self, name, base, tip, r, kind, bone, sides=8, pre=True):
return self.tube(name, [base, V(base).lerp(V(tip), .5), tip], [r, r * .55, .0015], kind, bone, sides, 6, smooth=True, pre=pre)
def loft(self, name, sections, kind, weight_fn, sides=24, steps=36, front_bulge=0.0, y0=0.0, smooth=True, scallop=0.0):
"""Elliptic loft in pre-space. sections: (z, rx, ry); front_bulge rounds the chest."""
vs, fs, ws = [], [], []
for j in range(steps + 1):
t = j / steps
f = t * (len(sections) - 1)
idx = min(len(sections) - 2, int(f))
u = f - idx
z, rx, ry = [sections[idx][q] * (1 - u) + sections[idx + 1][q] * u for q in range(3)]
for k in range(sides):
a = 2 * pi * k / sides
bulge = 1 + front_bulge * max(0.0, -sin(a)) ** 2 * abs(cos(a)) * 1.6
edge = 1 + scallop * (cos(a * 9) * .5 + .5) * t
vs.append((rx * cos(a) * edge, y0 + ry * sin(a) * bulge * edge, z))
ws.append(weight_fn(z, cos(a) * rx))
if j:
for k in range(sides):
a0, b0 = (j - 1) * sides + k, (j - 1) * sides + (k + 1) % sides
fs.append((a0, b0, b0 + sides, a0 + sides))
fs += [tuple(reversed(range(sides))), tuple(range(steps * sides, (steps + 1) * sides))]
return self.mesh(name, vs, fs, kind, 'Spine', ws, smooth)
# ------------------------------------------------------------ weights
def torso_weights(self, z, x=0.0):
if z < self.waist - .05:
return {'Pelvis': 1}
if z < self.waist + .07:
return blend('Pelvis', 'Spine', (z - self.waist + .05) / .12)
if z < self.chest - .06:
return {'Spine': 1}
if z < self.chest + .05:
return blend('Spine', 'Chest', (z - self.chest + .06) / .11)
if z < self.neck - .02:
return {'Chest': 1}
return blend('Chest', 'Neck', (z - self.neck + .02) / .06)
def skirt_weights(self, top, bottom, share=.45):
"""Hanging cloth: pelvis at the belt, partly following the nearer thigh below."""
def fn(z, x):
t = max(0.0, min(1.0, (top - z) / max(.01, top - bottom)))
side = 'Thigh.L' if x > 0 else 'Thigh.R'
return {'Pelvis': 1 - share * t, side: share * t}
return fn
# ------------------------------------------------------------ body
def build_body(self, torso_kind='skin', arm_kind='skin', leg_kind='cloth', boot_kind='leather', chest=1.0, belly=1.0):
sp = self.sp
w, d, lb, H = sp['w'], sp['d'], sp['limb'], self.hip
secs = [(-.08, .14, .105), (0, .165, .118), (.06, .158, .112 * belly), (.13, .142, .104 * belly), (.22, .158, .112 * belly),
(.32, .19 * chest, .124 * chest), (.42, .212 * chest, .128 * chest), (.49, .205 * chest, .118), (.55, .15, .095), (.60, .072, .066)]
secs = [(H + z, rx * w, ry * d) for z, rx, ry in secs]
self.loft('Torso', secs, torso_kind, self.torso_weights, front_bulge=.06)
self.torso_sections = secs
self.tube('Neck', [(0, .0, self.neck - .03), (0, -.004, self.neck + .05), (0, -.012, self.head_c - .05)],
[.058 * lb, .054 * lb, .05 * lb], torso_kind if torso_kind not in ('metal', 'cloth', 'cloth2', 'leather') else 'skin', 'Neck', 16, 10,
weight_fn=lambda t: blend('Chest', 'Neck', t * 2) if t < .5 else blend('Neck', 'Head', (t - .5) * 2))
# Arms in final space from IK against the style's grips.
s = self.s
self.l1, self.l2 = .285 * sp['arm'] * s, .265 * sp['arm'] * s
self.L = self.l1 + self.l2
self.sh = {side: self.X((sx * .19 * w, .012, self.shoulder - .015)) for side, sx in SIDES}
self.bind_grips()
self.elbow, self.W = {}, {}
for side, sx in SIDES:
g = self.G[side]
e0, _, _ = ik_elbow(self.sh[side], g, self.l1, self.l2, self.pole[side])
fore = (g - e0).normalized()
w0 = g - fore * .035 * s * lb
elbow, w0, err = ik_elbow(self.sh[side], w0, self.l1, self.l2, self.pole[side])
self.elbow[side], self.W[side] = elbow, w0
a, b, c = self.sh[side], elbow, w0
upper, fore_b, hand = 'UpperArm.' + side, 'Forearm.' + side, 'Hand.' + side
def arm_weights(t, upper=upper, fore_b=fore_b, hand=hand, side=side):
if t < .12:
return blend('Clavicle.' + side, upper, t / .12)
if t < .42:
return {upper: 1}
if t < .58:
return blend(upper, fore_b, (t - .42) / .16)
if t < .9:
return {fore_b: 1}
return blend(fore_b, hand, (t - .9) / .1)
self.tube('Arm_' + side, [a, a.lerp(b, .45), b, b.lerp(c, .45), c],
[.056 * lb * s, .055 * lb * s, .041 * lb * s, .047 * lb * s, .033 * lb * s], arm_kind, upper, 16, 30,
weight_fn=arm_weights, pre=False)
self.ell('Deltoid_' + side, a + V((sx * .012 * s, 0, .008 * s)), (.066 * lb * s, .066 * lb * s, .07 * lb * s), arm_kind,
'UpperArm.' + side, 16, 10, pre=False,
weights=lambda v, side=side: blend('Clavicle.' + side, 'UpperArm.' + side, .6))
# Legs in pre-space (no bending below the waist).
self.leg = {}
for side, sx in SIDES:
hp = V((sx * .092 * w, .005, H - .03))
if sp.get('digitigrade'):
kn = V((sx * .10 * w, -.12, self.knee + .06))
an = V((sx * .105 * w, .10, .21))
toe = V((sx * .11 * w, -.07, .03))
else:
kn = V((sx * .10 * w, -.035, self.knee))
an = V((sx * .105 * w, .012, self.ankle))
toe = V((sx * .11 * w, -.16, .025))
if self.hover:
kn += V((0, -.04 if sx > 0 else .03, .04))
toe = an + V((0, -.05, -.1))
self.leg[side] = (hp, kn, an, toe)
thigh, shin, foot = 'Thigh.' + side, 'Shin.' + side, 'Foot.' + side
def leg_weights(t, thigh=thigh, shin=shin, foot=foot):
if t < .08:
return blend('Pelvis', thigh, t / .08)
if t < .42:
return {thigh: 1}
if t < .6:
return blend(thigh, shin, (t - .42) / .18)
if t < .92:
return {shin: 1}
return blend(shin, foot, (t - .92) / .08)
self.tube('Leg_' + side, [hp, hp.lerp(kn, .46), kn, kn.lerp(an, .45), an],
[.088 * lb, .08 * lb, .056 * lb, .062 * lb, .04 * lb], leg_kind, thigh, 16, 30, weight_fn=leg_weights)
if boot_kind:
heel = an + V((0, .035, -.045 if not sp.get('digitigrade') else -.03))
self.tube('Boot_' + side, [heel, an.lerp(toe, .45) + V((0, 0, .012)), toe + V((0, -.012, .012))],
[.05 * lb, .058 * lb, .03 * lb], boot_kind, foot, 12, 12, flatten=.72, smooth=True)
self.ring('BootCuff_' + side, an + V((0, 0, .045)), (0, 0, 1), .052 * lb, .03, boot_kind, 'Shin.' + side, 14)
# ------------------------------------------------------------ hands
def fist(self, side, kind, grip_r):
"""Closed hand around the grip axis in final space (rigid on the hand bone)."""
s, lb = self.s, self.sp['limb']
g, u, f = self.G[side], self.u[side], self.f[side]
fore = (self.W[side] - self.elbow[side]).normalized()
back = (fore - u * fore.dot(u)).normalized()
across = u.cross(back)
hand = 'Hand.' + side
self.ell('Palm_' + side, g - back * (grip_r + .012 * s * lb) * .4, (.036 * s * lb, .048 * s * lb, .036 * s * lb), kind, hand, 14, 9,
pre=False, axes=[across, u, back])
for k in range(4):
off = u * ((k - 1.5) * .02 * s * lb)
pts = []
for q in range(6):
ang = -.5 + q * (3.5 / 5)
pts.append(g + off + (back * cos(ang) + across * sin(ang)) * (grip_r + .011 * s * lb))
self.tube('Finger_%s%d' % (side, k), pts, [.012 * s * lb, .013 * s * lb, .011 * s * lb], kind, hand, 8, 10, pre=False)
self.tube('Thumb_' + side, [g - back * grip_r * .8 - across * .03 * s * lb + u * .03 * s * lb,
g - across * (grip_r + .012 * s * lb) + u * .03 * s * lb,
g + back * grip_r * .4 - across * (grip_r + .004 * s) + u * .036 * s * lb],
[.014 * s * lb, .013 * s * lb, .01 * s * lb], kind, hand, 8, 8, pre=False)
def open_hand(self, side, kind, claws=None, spread=.12):
"""Open, slightly curled hand pointing along u with palm facing -f; optional claws."""
s, lb = self.s, self.sp['limb']
g, u, f = self.G[side], self.u[side], self.f[side]
across = u.cross(f)
hand = 'Hand.' + side
self.ell('Palm_' + side, g, (.04 * s * lb, .05 * s * lb, .02 * s * lb), kind, hand, 14, 9, pre=False, axes=[across, u, f])
for k in range(4):
base = g + u * .04 * s * lb + across * ((k - 1.5) * .022 * s * lb)
dirn = (u + across * (k - 1.5) * spread).normalized()
p1 = base + dirn * .035 * s * lb - f * .008 * s
p2 = p1 + dirn * .03 * s * lb - f * .018 * s
self.tube('Finger_%s%d' % (side, k), [base, p1, p2], [.012 * s * lb, .011 * s * lb, .009 * s * lb], kind, hand, 8, 8, pre=False)
if claws:
self.cone('Claw_%s%d' % (side, k), p2, p2 + dirn * .05 * s * lb - f * .03 * s, .01 * s * lb, claws, hand, 6, pre=False)
inner = -1 if side == 'L' else 1
tb = g + across * .035 * s * lb * inner
self.tube('Thumb_' + side, [g + across * .02 * s * lb * inner, tb + u * .03 * s * lb, tb + u * .055 * s * lb - f * .01 * s],
[.014 * s * lb, .012 * s * lb, .01 * s * lb], kind, hand, 8, 8, pre=False)
if claws:
self.cone('ThumbClaw_' + side, tb + u * .055 * s * lb, tb + u * .09 * s * lb - f * .02 * s, .009 * s * lb, claws, hand, 6, pre=False)
def fist_free(self, side, kind):
s, lb = self.s, self.sp['limb']
g = self.G[side]
self.ell('Fist_' + side, g, (.045 * s * lb, .05 * s * lb, .055 * s * lb), kind, 'Hand.' + side, 14, 9, pre=False)
for k in range(4):
self.ell('Knuckle_%s%d' % (side, k), g + V(((k - 1.5) * .02 * s * lb, -.04 * s * lb, .012 * s)), (.013 * s * lb, .014 * s * lb, .014 * s * lb),
kind, 'Hand.' + side, 8, 6, pre=False)
# ------------------------------------------------------------ head
def face(self, kind='skin', eyes='dark', ears='human', nose=1.0, jaw=1.0, brow=1.0):
hd, hc = self.sp['head'], self.head_c
self.ell('Skull', (0, .0, hc), (.088 * hd, .1 * hd, .108 * hd), kind, 'Head', 20, 14)
self.ell('Jaw', (0, -.042 * hd, hc - .058 * hd), (.064 * hd * jaw, .062 * hd, .05 * hd * jaw), kind, 'Head', 16, 10)
self.ell('Brow', (0, -.078 * hd, hc + .026 * hd), (.068 * hd, .028 * hd * brow, .017 * hd * brow), kind, 'Head', 14, 8)
self.tube('Nose', [(0, -.088 * hd, hc + .02 * hd), (0, -.106 * hd * nose, hc - .012 * hd), (0, -.1 * hd * nose, hc - .03 * hd)],
[.012 * hd * nose, .018 * hd * nose, .014 * hd * nose], kind, 'Head', 10, 8)
for side, sx in SIDES:
self.ell('Eye_' + side, (sx * .032 * hd, -.084 * hd, hc + .006 * hd), (.014 * hd, .008 * hd, .011 * hd), eyes, 'Head', 10, 7)
if ears == 'human':
self.ell('Ear_' + side, (sx * .088 * hd, .0, hc - .002), (.012 * hd, .024 * hd, .034 * hd), kind, 'Head', 10, 7)
elif ears == 'elf':
self.tube('Ear_' + side, [(sx * .082 * hd, .01, hc - .02 * hd), (sx * .13 * hd, .04 * hd, hc + .03 * hd), (sx * .19 * hd, .08 * hd, hc + .1 * hd)],
[.022 * hd, .016 * hd, .002], kind, 'Head', 8, 8, flatten=.4, ref=(0, 0, 1))
elif ears == 'orc':
self.tube('Ear_' + side, [(sx * .085 * hd, .01, hc - .01 * hd), (sx * .13 * hd, .03 * hd, hc + .02 * hd), (sx * .17 * hd, .05 * hd, hc + .05 * hd)],
[.026 * hd, .02 * hd, .002], kind, 'Head', 8, 8, flatten=.4, ref=(0, 0, 1))
elif ears == 'troll':
self.tube('Ear_' + side, [(sx * .085 * hd, .0, hc), (sx * .15 * hd, .03 * hd, hc - .02 * hd), (sx * .22 * hd, .06 * hd, hc - .07 * hd)],
[.032 * hd, .026 * hd, .003], kind, 'Head', 8, 8, flatten=.35, ref=(0, 0, 1))
# ------------------------------------------------------------ styles: grips, weapons
def bind_grips(self):
st, s, L = self.sp['style'], self.s, self.L
shR, shL = self.sh['R'], self.sh['L']
self.G, self.u, self.f, self.pole = {}, {}, {}, {}
self.target = {'R': 'Weapon', 'L': 'Offhand'}
self.offhand_parent = 'Chest'
self.weapon_parent = 'Chest'
if st == 'blade_shield':
self.G['R'] = shR + L * V((.03, -.50, -.60))
self.u['R'], self.f['R'], _ = frame((0, -.55, 1), (0, -1, 0))
self.G['L'] = shL + L * V((-.02, -.54, -.50))
self.u['L'], self.f['L'], _ = frame((0, 0, 1), (0, -1, 0))
elif st == 'bow':
self.G['L'] = shL + L * V((-.12, -.84, -.12))
self.u['L'], self.f['L'], _ = frame((.1, 0, 1), (0, -1, 0))
self.G['R'] = self.G['L'] + V((0, .30 * L, .012 * s))
self.u['R'], self.f['R'], _ = frame((0, -1, 0), (-1, 0, 0))
self.weapon_parent = 'Offhand'
elif st == 'staff':
self.G['R'] = shR + L * V((-.14, -.30, -.60))
self.u['R'], self.f['R'], _ = frame((-.08, -.12, 1), (0, -1, 0))
self.G['L'] = shL + L * V((-.02, -.60, -.42))
self.u['L'], self.f['L'], _ = frame((0, -1, .35), (0, 0, -1))
elif st == 'club2h':
self.G['R'] = shR + L * V((.30, -.55, -.60))
self.u['R'], self.f['R'], _ = frame((.25, -.5, 1), (0, -1, 0))
self.G['L'] = self.G['R'] + self.u['R'] * .17 * s
self.u['L'], self.f['L'] = self.u['R'], self.f['R']
self.target['L'] = 'Weapon'
elif st == 'axe':
self.G['R'] = shR + L * V((-.12, -.42, -.64))
self.u['R'], self.f['R'], _ = frame((-.3, -.5, 1), (0, -1, 0))
self.G['L'] = shL + L * V((-.06, -.40, -.62))
self.u['L'], self.f['L'], _ = frame((0, 0, 1), (0, -1, 0))
elif st == 'claws':
for side, sx in SIDES:
self.G[side] = self.sh[side] + L * V((sx * -.06, -.58, -.46))
self.u[side], self.f[side], _ = frame((sx * -.1, -1, -.25), (0, 0, -1))
for side, sx in SIDES:
self.pole[side] = self.sh[side] + L * V((sx * .8, .35, -.35))
if st == 'bow':
self.pole['R'] = shR + L * V((-.8, .7, .2))
self.pole['L'] = shL + L * V((.7, .1, -.7))
def weapon_frame(self, side):
g, u, f = self.G[side], self.u[side], self.f[side]
side_v = u.cross(f)
return lambda x, y, z: g + side_v * x * self.s + f * y * self.s + u * z * self.s
def build_weapon(self):
sp, st, s = self.sp, self.sp['style'], self.s
if st == 'blade_shield':
F = self.weapon_frame('R')
u, f = self.u['R'], self.f['R']
if sp['weapon'] == 'sword':
self.tube('Sword_Grip', [F(0, 0, -.07), F(0, 0, .07)], [.019 * s, .019 * s], 'leather', 'Weapon', 10, 4, groove=.12, lobes=8, pre=False)
self.ell('Sword_Pommel', F(0, 0, -.09), (.03 * s, .03 * s, .026 * s), 'trim', 'Weapon', 12, 8, pre=False, axes=[u.cross(f), f, u])
self.plate('Sword_Guard', [F(-.12, 0, .075), F(-.02, 0, .09), F(.02, 0, .09), F(.12, 0, .075), F(.02, 0, .065), F(-.02, 0, .065)],
F(0, 0, .077), .026 * s, 'trim', 'Weapon', normal=f, pre=False)
blade = [F(-.028, 0, .09), F(.028, 0, .09), F(.024, 0, .62), F(0, 0, .72), F(-.024, 0, .62)]
self.plate('Sword_Blade', blade, F(0, 0, .38), .016 * s, 'metal', 'Weapon', normal=f, pre=False)
self.plate('Sword_Blade_Back', blade, F(0, 0, .38), .016 * s, 'metal', 'Weapon', normal=-f, pre=False)
self.tube('Sword_Fuller', [F(0, -.008, .12), F(0, -.008, .5)], [.004 * s, .003 * s], 'dark', 'Weapon', 6, 4, pre=False)
grip_r = .019 * s
else:
self.tube('Hammer_Haft', [F(0, 0, -.14), F(0, 0, .2), F(0, 0, .46)], [.022 * s, .021 * s, .024 * s], 'wood', 'Weapon', 10, 10, groove=.05, pre=False)
for k, z in enumerate((-.03, .1, .38)):
self.ring('Hammer_Band_%d' % k, F(0, 0, z), u, .026 * s, .018 * s, 'trim', 'Weapon', 12, pre=False)
side_v = u.cross(f)
self.tube('Hammer_Head', [F(-.13, 0, .5), F(0, 0, .5), F(.13, 0, .5)], [.058 * s, .066 * s, .058 * s], 'metal', 'Weapon', 8, 8,
pre=False, ref=u, smooth=False)
for q in (-1, 1):
self.ring('Hammer_Face_%d' % q, F(q * .14, 0, .5), side_v, .062 * s, .02 * s, 'trim', 'Weapon', 8, pre=False)
self.plate('Hammer_Rune', [F(-.03, -.07, .47), F(.03, -.07, .47), F(.03, -.07, .53), F(-.03, -.07, .53)], F(0, -.07, .5), .01 * s,
'glow', 'Weapon', normal=f, pre=False)
grip_r = .022 * s
self.fist('R', 'leather' if sp['tag'] == 'Mensch' else 'skin', grip_r)
# Shield on the left arm.
G = self.weapon_frame('L')
fL = self.f['L']
self.tube('Shield_Handle', [G(0, 0, -.06), G(0, 0, .06)], [.018 * s, .018 * s], 'leather', 'Offhand', 8, 3, pre=False)
self.fist('L', 'leather' if sp['tag'] == 'Mensch' else 'skin', .018 * s)
if sp['shield'] == 'kite':
outline = [(0, .28), (.19, .22), (.2, .03), (.11, -.2), (0, -.36), (-.11, -.2), (-.2, .03), (-.19, .22)]
rim = [G(x * 1.07, .05, z * 1.06) for x, z in outline]
face = [G(x, .068, z) for x, z in outline]
self.plate('Shield_Rim', rim, G(0, .05, -.02), .03 * s, 'metal', 'Offhand', normal=fL, pre=False)
self.plate('Shield_Face', face, G(0, .07, -.02), .03 * s, 'cloth', 'Offhand', normal=fL, pre=False)
self.plate('Shield_Cross_V', [G(-.028, .1, .22), G(.028, .1, .22), G(.028, .1, -.28), G(-.028, .1, -.28)], G(0, .1, -.03),
.012 * s, 'trim', 'Offhand', normal=fL, pre=False)
self.plate('Shield_Cross_H', [G(-.15, .1, .07), G(.15, .1, .07), G(.15, .1, .02), G(-.15, .1, .02)], G(0, .1, .045),
.012 * s, 'trim', 'Offhand', normal=fL, pre=False)
else:
outline = [(cos(2 * pi * k / 20) * .27, sin(2 * pi * k / 20) * .27) for k in range(20)]
self.plate('Shield_Face', [G(x, .06, z) for x, z in outline], G(0, .07, 0), .045 * s, 'wood', 'Offhand', normal=fL, pre=False)
self.ring('Shield_Rim', G(0, .055, 0), fL, .275 * s, .045 * s, 'metal', 'Offhand', 24, pre=False)
self.ell('Shield_Boss', G(0, .1, 0), (.07 * s, .07 * s, .04 * s), 'trim', 'Offhand', 14, 8, pre=False,
axes=[fL.cross(self.u['L']), self.u['L'], fL])
for k in range(8):
a = 2 * pi * k / 8
self.ell('Shield_Rivet_%d' % k, G(cos(a) * .22, .1, sin(a) * .22), (.014 * s, .014 * s, .012 * s), 'trim', 'Offhand', 8, 5, pre=False)
for k, (x0, z0, x1, z1) in enumerate([(-.1, .12, -.02, -.1), (.02, .12, .1, -.1), (-.06, .0, .06, .0)]):
self.tube('Shield_Rune_%d' % k, [G(x0, .1, z0), G(x1, .1, z1)], [.009 * s, .009 * s], 'glow', 'Offhand', 6, 3, pre=False)
elif st == 'bow':
B = self.weapon_frame('L')
fL = self.f['L']
pts = [B(0, -.07, -.62), B(0, .02, -.5), B(0, -.02, -.25), B(0, 0, 0), B(0, -.02, .25), B(0, .02, .5), B(0, -.07, .62)]
self.tube('Bow_Limbs', pts, [.009 * s, .016 * s, .02 * s, .024 * s, .02 * s, .016 * s, .009 * s], 'wood', 'Offhand', 8, 34, flatten=.7, pre=False)
self.tube('Bow_Grip', [B(0, 0, -.07), B(0, 0, .07)], [.026 * s, .026 * s], 'leather', 'Offhand', 10, 4, groove=.1, lobes=8, pre=False)
for q in (-1, 1):
self.cone('Bow_Tip_%d' % q, B(0, -.07, q * .6), B(0, -.11, q * .66), .012 * s, 'trim', 'Offhand', 6, pre=False)
self.plate('Bow_Leaf_%d' % q, [B(-.035, .0, q * .16), B(0, .01, q * .1), B(.035, .0, q * .16), B(0, .01, q * .26)],
B(0, .02, q * .17), .008 * s, 'trim', 'Offhand', normal=fL, pre=False)
# String: tips on the bow, middle on the draw hand; the weights stretch it when drawn.
top, bottom, nock = B(0, -.08, .6), B(0, -.08, -.6), self.G['R'].copy()
vs, fs, ws = [], [], []
n = 24
for j in range(n + 1):
t = j / n
p = top.lerp(nock, t * 2) if t < .5 else nock.lerp(bottom, (t - .5) * 2)
pull = 1 - abs(t - .5) * 2
for k in range(4):
a = 2 * pi * k / 4
vs.append(p + V((cos(a), sin(a), 0)) * .003 * s)
ws.append({'Offhand': 1 - pull, 'Weapon': pull} if pull < 1 else {'Weapon': 1})
if j:
for k in range(4):
a0, b0 = (j - 1) * 4 + k, (j - 1) * 4 + (k + 1) % 4
fs.append((a0, b0, b0 + 4, a0 + 4))
self.mesh('Bow_String', vs, fs, 'cloth2', 'Offhand', ws, True, None, False)
# Draw hand pinching the nock, and the arrow on its own bone (hidden after release).
self.fist('L', 'leather', .026 * s)
self.open_hand('R', 'leather', spread=.04)
aim = (self.G['L'] - self.G['R']).normalized()
tip = self.G['R'] + aim * .7 * s
self.tube('Arrow_Shaft', [self.G['R'] - aim * .02 * s, tip], [.006 * s, .006 * s], 'wood', 'Arrow', 6, 4, pre=False)
self.cone('Arrow_Head', tip, tip + aim * .07 * s, .016 * s, 'trim', 'Arrow', 4, pre=False)
for k in range(3):
a = 2 * pi * k / 3
off = V((cos(a), 0, sin(a))) * .02 * s
self.plate('Arrow_Fletch_%d' % k, [self.G['R'] + aim * .03 * s, self.G['R'] + aim * .03 * s + off,
self.G['R'] + aim * .12 * s + off * .6, self.G['R'] + aim * .13 * s],
self.G['R'] + aim * .08 * s + off * .5, .003 * s, 'cloth2', 'Arrow', normal=V((sin(a), 0, -cos(a))), pre=False)
elif st == 'staff':
F = self.weapon_frame('R')
u = self.u['R']
self.tube('Staff_Shaft', [F(0, 0, -.5), F(0, .01, .1), F(0, 0, .55)], [.012 * s, .014 * s, .016 * s], 'wood', 'Weapon', 8, 16, groove=.06, pre=False)
for k, z in enumerate((-.06, .08, .5)):
self.ring('Staff_Band_%d' % k, F(0, 0, z), u, .018 * s, .014 * s, 'trim', 'Weapon', 10, pre=False)
for q in (-1, 1):
self.tube('Staff_Crescent_%d' % q, [F(0, 0, .55), F(q * .08, 0, .62), F(q * .085, 0, .74), F(q * .04, 0, .8)],
[.012 * s, .011 * s, .008 * s, .003 * s], 'trim', 'Weapon', 8, 14, pre=False)
self.ell('Staff_Orb', F(0, 0, .69), (.05 * s, .05 * s, .05 * s), 'glow', 'Weapon', 14, 10, pre=False)
for q in (-1, 1):
self.tube('Staff_Ribbon_%d' % q, [F(q * .02, 0, .5), F(q * .05, .02, .38), F(q * .04, .04, .26)], [.008 * s, .007 * s, .003 * s],
'cloth', 'Weapon', 6, 8, flatten=.25, pre=False)
self.fist('R', 'skin', .014 * s)
self.open_hand('L', 'skin', spread=.16)
elif st == 'club2h':
F = self.weapon_frame('R')
u, f = self.u['R'], self.f['R']
side_v = u.cross(f)
self.tube('Club_Haft', [F(0, 0, -.16), F(0, 0, .25), F(0, 0, .5)], [.034 * s, .036 * s, .05 * s], 'wood', 'Weapon', 10, 12, groove=.08, pre=False)
self.ell('Club_Head', F(0, 0, .66), (.12 * s, .12 * s, .2 * s), 'stone', 'Weapon', 14, 10, pre=False, axes=[side_v, f, u],
bumps=.12, smooth=False)
for k in range(7):
a = 2 * pi * k / 7
radial = side_v * cos(a) + f * sin(a)
base = F(0, 0, .62 + .06 * (k % 2)) + radial * .1 * s
self.shard('Club_Spike_%d' % k, [base, base + radial * .05 * s, base + radial * .09 * s + u * .02 * s], .03 * s, 'stone', 'Weapon', pre=False)
for k, z in enumerate((.02, .44)):
self.ring('Club_Wrap_%d' % k, F(0, 0, z), u, .042 * s, .05 * s, 'leather', 'Weapon', 12, pre=False)
self.fist('R', 'skin', .036 * s)
self.fist('L', 'skin', .036 * s)
elif st == 'axe':
F = self.weapon_frame('R')
u, f = self.u['R'], self.f['R']
self.tube('Axe_Haft', [F(0, 0, -.2), F(0, 0, .2), F(0, 0, .58)], [.022 * s, .022 * s, .025 * s], 'wood', 'Weapon', 10, 10, groove=.05, pre=False)
for k, z in enumerate((-.06, .08, .22)):
self.ring('Axe_Wrap_%d' % k, F(0, 0, z), u, .026 * s, .03 * s, 'leather', 'Weapon', 10, pre=False)
blade = [F(0, .02, .6), F(0, .16, .72), F(0, .24, .64), F(0, .25, .46), F(0, .17, .36), F(0, .02, .44)]
self.plate('Axe_Blade', blade, F(0, .13, .54), .018 * s, 'metal', 'Weapon', normal=u.cross(f), pre=False)
self.plate('Axe_Blade_Back', blade, F(0, .13, .54), .018 * s, 'metal', 'Weapon', normal=-u.cross(f), pre=False)
self.tube('Axe_Edge', [F(0, .16, .72), F(0, .235, .66), F(0, .25, .55), F(0, .24, .46), F(0, .17, .36)], [.006 * s] * 5, 'bone', 'Weapon',
6, 14, pre=False)
self.shard('Axe_Spike', [F(0, -.02, .53), F(0, -.1, .52), F(0, -.18, .55)], .03 * s, 'metal', 'Weapon', pre=False)
self.ring('Axe_Collar', F(0, 0, .53), u, .03 * s, .1 * s, 'metal', 'Weapon', 10, pre=False)
self.fist('R', 'skin', .022 * s)
self.fist_free('L', 'skin')
elif st == 'claws':
for side, _ in SIDES:
self.open_hand(side, 'skin', claws='bone', spread=.18)
# ------------------------------------------------------------ shared gear pieces
def build(self):
getattr(self, 'gear_' + self.key[2:])()
self.build_weapon()
def armour_loft(self, name, z0, z1, grow, kind, bone_fn=None, sides=24, steps=12, bulge=.07):
secs = [(z, rx + grow, ry + grow) for z, rx, ry in self.torso_sections if z0 - .001 <= z <= z1 + .001]
return self.loft(name, secs, kind, bone_fn or self.torso_weights, sides, steps, front_bulge=bulge)
def section_at(self, z):
secs = self.torso_sections
for a, b in zip(secs, secs[1:]):
if a[0] <= z <= b[0]:
t = (z - a[0]) / (b[0] - a[0])
return a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t
return secs[0][1], secs[0][2]
def pauldron(self, side, sx, kind, trim, layers=3, size=1.0, spikes=0, spike_kind='bone'):
s = self.s
a = self.sh[side]
out = V((sx, 0, 0))
for k in range(layers):
c = a + out * (.03 + .018 * k) * s * size + V((0, 0, (.035 - .035 * k) * s * size))
self.ell('Pauldron_%s%d' % (side, k), c, ((.085 - .008 * k) * s * size, (.09 - .006 * k) * s * size, (.058 - .006 * k) * s * size),
kind, 'UpperArm.' + side, 16, 9, smooth=False, pre=False)
self.ring('Pauldron_Trim_' + side, a + out * .03 * s * size, (sx * .4, 0, 1), .087 * s * size, .014 * s, trim, 'UpperArm.' + side, 18, pre=False)
for k in range(spikes):
base = a + out * (.05 * s * size) + V((0, (k - (spikes - 1) / 2) * .045 * s * size, .07 * s * size))
self.shard('Pauldron_Spike_%s%d' % (side, k), [base, base + V((sx * .05, 0, .09)) * s * size, base + V((sx * .08, 0, .16)) * s * size],
.022 * s * size, spike_kind, 'UpperArm.' + side, pre=False)
def bracer(self, side, kind, trim=None, frac=(.55, .9), grow=.012):
a, b = self.elbow[side], self.W[side]
p0, p1 = a.lerp(b, frac[0]), a.lerp(b, frac[1])
lb, s = self.sp['limb'], self.s
self.tube('Bracer_' + side, [p0, p0.lerp(p1, .5), p1], [(.046 + grow) * lb * s, (.047 + grow) * lb * s, (.037 + grow) * lb * s], kind,
'Forearm.' + side, 14, 6, smooth=False, pre=False)
if trim:
self.ring('Bracer_Trim_' + side, p1, b - a, (.04 + grow) * lb * s, .012 * s, trim, 'Forearm.' + side, 14, pre=False)
def belt(self, kind, buckle, z=None, grow=.012):
z = z if z is not None else self.waist - .02
rx, ry = self.section_at(z)
self.loft('Belt', [(z - .025, rx + grow, ry + grow), (z + .025, rx + grow, ry + grow)], kind, self.torso_weights, 24, 2)
y = -ry - grow - .004
self.plate('Buckle', [(-.03, y, z + .028), (.03, y, z + .028), (.03, y, z - .028), (-.03, y, z - .028)], (0, y, z), .012, buckle, 'Pelvis')
return rx + grow, ry + grow
def hanging(self, name, outline, center, depth, kind, normal, top, bottom, share=.5):
return self.plate(name, outline, center, depth, kind, 'Pelvis', normal=normal,
weights=lambda v: self.skirt_weights(top, bottom, share)(v[2], v[0]))
# ------------------------------------------------------------ people
def gear_mensch(self):
self.build_body('cloth', 'metal', 'metal', 'metal', chest=1.04)
H, w = self.hip, self.sp['w']
self.armour_loft('Breastplate', H + .13, H + .55, .016, 'metal', bulge=.1)
rx, ry = self.section_at(H + .13)
self.loft('Breastplate_Rim', [(H + .12, rx + .022, ry + .022), (H + .145, rx + .022, ry + .022)], 'trim', self.torso_weights, 24, 2)
self.ring('Gorget', (0, 0, self.neck - .01), (0, 0, 1), .082, .05, 'metal', 'Neck', 18)
self.ring('Gorget_Trim', (0, 0, self.neck + .018), (0, 0, 1), .078, .012, 'trim', 'Neck', 18)
# Chest emblem: a gold sun on the breastplate.
rx, ry = self.section_at(H + .40)
ey = -(ry * 1.1 + .016) - .004
self.ell('Emblem', (0, ey, H + .40), (.035, .012, .035), 'trim', 'Chest', 12, 6, smooth=False)
for k in range(8):
a = 2 * pi * k / 8
self.shard('Emblem_Ray_%d' % k, [(cos(a) * .035, ey + .002, H + .40 + sin(a) * .035), (cos(a) * .07, ey + .008, H + .40 + sin(a) * .07)], .012, 'trim', 'Chest')
# Faulds and tabard.
for k, z in enumerate((H + .08, H + .02, H - .04)):
rx, ry = self.section_at(max(H - .07, z))
g = .02 + .008 * k
self.loft('Fauld_%d' % k, [(z - .035, rx + g + .006, ry + g + .006), (z + .03, rx + g, ry + g)], 'metal', self.skirt_weights(H + .1, H - .08, .2), 24, 2,
smooth=False)
rx, ry = self.belt('leather', 'trim', H + .115)
for q, y in ((-1, -ry - .03), (1, ry + .03)):
out = [(-.12, y, H + .1), (.12, y, H + .1), (.11, y, H - .3), (0, y, H - .36), (-.11, y, H - .3)]
self.hanging('Tabard_%d' % q, out, (0, y, H - .1), .014, 'cloth', (0, q, 0), H + .1, H - .36)
trim = [(-.115, y + q * .005, H - .22), (.115, y + q * .005, H - .22), (.105, y + q * .005, H - .29), (0, y + q * .005, H - .345),
(-.105, y + q * .005, H - .29)]
self.hanging('Tabard_Trim_%d' % q, trim, (0, y + q * .005, H - .28), .008, 'trim', (0, q, 0), H + .1, H - .36)
for side, sx in SIDES:
self.pauldron(side, sx, 'metal', 'trim', 3)
self.ell('Couter_' + side, self.elbow[side], (.052 * self.s, .052 * self.s, .052 * self.s), 'metal', 'Forearm.' + side, 14, 9, pre=False, smooth=False)
self.bracer(side, 'metal', 'trim', (.5, .92), .006)
hp, kn, an, toe = self.leg[side]
self.ell('Poleyn_' + side, kn + V((0, -.03, 0)), (.058, .045, .058), 'metal', 'Shin.' + side, 14, 9, smooth=False,
weights=lambda v, side=side: blend('Thigh.' + side, 'Shin.' + side, .6))
self.ring('Poleyn_Trim_' + side, kn + V((0, -.02, .0)), (0, 1, .2), .052, .012, 'trim', 'Shin.' + side, 14)
# Great helm with visor slit, gold cross and a white plume.
hd, hc = self.sp['head'], self.head_c
self.ell('Helm', (0, -.004, hc + .004), (.1 * hd, .112 * hd, .122 * hd), 'metal', 'Head', 20, 14)
self.ring('Helm_Crown', (0, -.004, hc + .06 * hd), (0, 0, 1), .096 * hd, .018, 'trim', 'Head', 20)
self.plate('Visor_Slit', [(-.07, -.108 * hd, hc + .018), (.07, -.108 * hd, hc + .018), (.07, -.104 * hd, hc + .002), (-.07, -.104 * hd, hc + .002)],
(0, -.11 * hd, hc + .01), .008, 'dark', 'Head')
self.plate('Helm_Cross_V', [(-.012, -.115 * hd, hc + .1), (.012, -.115 * hd, hc + .1), (.012, -.112 * hd, hc - .08), (-.012, -.112 * hd, hc - .08)],
(0, -.118 * hd, hc + .01), .008, 'trim', 'Head')
for k in range(4):
self.ell('Breath_%d' % k, ((k - 1.5) * .018, -.106 * hd, hc - .045), (.005, .004, .005), 'dark', 'Head', 6, 4)
self.tube('Plume', [(0, .0, hc + .12), (0, .06, hc + .2), (0, .16, hc + .2), (0, .24, hc + .08), (0, .26, hc - .04)],
[.022, .04, .045, .03, .006], 'cloth2', 'Head', 12, 20, groove=.25, lobes=7)
def gear_elf(self):
self.build_body('leather', 'cloth', 'cloth2', 'leather', chest=.96)
H, hd, hc = self.hip, self.sp['head'], self.head_c
self.armour_loft('Jerkin', H + .1, H + .5, .008, 'cloth', bulge=.08)
# Leaf plates over chest and shoulders.
for k, (x, z, sc) in enumerate([(-.07, H + .42, 1), (.07, H + .42, 1), (-.05, H + .3, .8), (.05, H + .3, .8), (0, H + .2, .7)]):
rx, ry = self.section_at(z)
y = -ry * 1.05 - .012
self.plate('Leaf_Plate_%d' % k, [(x, y, z + .06 * sc), (x + .045 * sc, y, z + .01 * sc), (x, y, z - .06 * sc), (x - .045 * sc, y, z + .01 * sc)],
(x, y - .01, z), .014, 'cloth2', 'Chest' if z > self.chest else 'Spine')
self.tube('Leaf_Vein_%d' % k, [(x, y - .012, z + .05 * sc), (x, y - .012, z - .05 * sc)], [.004, .003], 'trim', 'Chest' if z > self.chest else 'Spine', 6, 3)
rx, ry = self.belt('leather', 'trim', H + .1)
for q, y in ((-1, -ry - .02), (1, ry + .02)):
for k, x in enumerate((-.08, .0, .08)):
out = [(x - .045, y, H + .08), (x + .045, y, H + .08), (x + .035, y, H - .2), (x, y, H - .25), (x - .035, y, H - .2)]
self.hanging('Tunic_%d_%d' % (q, k), out, (x, y, H - .06), .01, 'cloth', (0, q, 0), H + .08, H - .25, .55)
lb = self.sp['limb']
for side, sx in SIDES:
hp, kn, an, toe = self.leg[side]
self.tube('Boot_Shaft_' + side, [an + V((0, 0, .02)), an.lerp(kn, .5), kn + V((0, -.01, -.03))], [.04 * lb + .012, .062 * lb + .012, .058 * lb + .012],
'leather', 'Shin.' + side, 14, 10)
self.plate('Boot_Cuff_' + side, [kn + V((-.05, -.045, .0)), kn + V((.05, -.045, 0)), kn + V((0, -.06, .06))], kn + V((0, -.06, .01)), .012,
'trim', 'Shin.' + side)
self.bracer(side, 'leather', 'trim', (.45, .92), .004)
self.pauldron(side, sx, 'leather', 'trim', 2, .8)
# Cloak on its own bone, hood gathered at the neck.
cloak = [(0, .12, self.shoulder), (0, .18, self.waist), (0, .23, self.knee + .05)]
self.tube('Cloak', cloak, [.2, .23, .27], 'cloth2', 'Chest', 18, 16, flatten=.18, ref=(1, 0, 0),
weight_fn=lambda t: blend('Chest', 'Cloak', t * 1.2))
self.tube('Hood', [(-.11, .02, self.neck - .02), (0, .1, self.neck + .02), (.11, .02, self.neck - .02)], [.05, .065, .05], 'cloth2', 'Chest', 12, 12)
self.ring('Cloak_Clasp', (0, -.1, self.neck - .03), (0, 1, 0), .02, .01, 'trim', 'Chest', 10)
self.face('skin', 'dark', 'elf', nose=.9, jaw=.88, brow=.8)
self.ell('Hair_Cap', (0, .03 * hd, hc + .035 * hd), (.093 * hd, .092 * hd, .09 * hd), 'hair', 'Head', 18, 12)
for side, sx in SIDES:
self.tube('Hair_Strand_' + side, [(sx * .05 * hd, -.07 * hd, hc + .08 * hd), (sx * .085 * hd, -.05 * hd, hc + .02 * hd), (sx * .09 * hd, -.02 * hd, hc - .08 * hd)],
[.018 * hd, .016 * hd, .006 * hd], 'hair', 'Head', 10, 10, groove=.2, lobes=4)
self.tube('Hair_Tail', [(0, .08 * hd, hc + .02), (0, .12 * hd, hc - .1), (0, .14, self.shoulder - .05), (0, .15, self.chest - .02)],
[.04, .036, .028, .012], 'hair', 'Head', 12, 16, groove=.2, lobes=6,
weight_fn=lambda t: {'Head': 1} if t < .3 else blend('Head', 'Chest', (t - .3) / .4))
self.ring('Circlet', (0, -.005, hc + .05 * hd), (0, .15, 1), .094 * hd, .01, 'trim', 'Head', 20)
self.ell('Circlet_Gem', (0, -.1 * hd, hc + .055 * hd), (.012, .008, .014), 'glow', 'Head', 10, 6)
# Quiver with arrows across the back.
q0, q1 = V((.1, .17, self.shoulder + .06)), V((-.08, .16, self.waist + .02))
up = (q0 - q1).normalized()
self.tube('Quiver', [q0, q1], [.045, .04], 'leather', 'Chest', 12, 4)
self.ring('Quiver_Trim', q0.lerp(q1, .08), up, .048, .02, 'trim', 'Chest', 12)
for k in range(4):
off = V(((k - 1.5) * .018, .0, 0))
self.tube('Quiver_Arrow_%d' % k, [q0 + off, q0 + off + up * .1], [.005, .005], 'wood', 'Chest', 6, 3)
self.plate('Quiver_Fletch_%d' % k, [q0 + off + up * .05, q0 + off + up * .1 + V((0, -.012, 0)), q0 + off + up * .12, q0 + off + up * .07 + V((0, .012, 0))],
q0 + off + up * .085, .004, 'cloth', 'Chest', normal=(1, 0, 0))
self.extra_bones.append(('Cloak', V((0, .12, self.shoulder)), V((0, .2, self.waist)), 'Chest'))
def gear_zwerg(self):
self.build_body('cloth', 'cloth', 'cloth', 'leather', chest=1.05, belly=1.12)
H, hd, hc = self.hip, self.sp['head'], self.head_c
self.armour_loft('Cuirass', H + .1, H + .55, .02, 'metal', bulge=.12)
for k in range(3):
z = H + .16 + .1 * k
rx, ry = self.section_at(z)
self.loft('Cuirass_Band_%d' % k, [(z - .012, rx + .03, ry * 1.05 + .03), (z + .012, rx + .03, ry * 1.05 + .03)], 'trim', self.torso_weights, 24, 2)
rx, ry = self.belt('leather', 'trim', H + .08, .03)
self.plate('Belt_Plate', [(-.06, -ry - .012, H + .12), (.06, -ry - .012, H + .12), (.07, -ry - .012, H + .04), (-.07, -ry - .012, H + .04)],
(0, -ry - .03, H + .08), .025, 'trim', 'Pelvis')
w, d = self.sp['w'], self.sp['d']
self.loft('Mail_Skirt', [(H + .06, .17 * w + .02, .13 * d + .03), (H - .1, .19 * w + .03, .14 * d + .03), (H - .24, .2 * w + .035, .15 * d + .035)], 'metal',
self.skirt_weights(H + .06, H - .24, .5), 24, 8, scallop=.04)
for side, sx in SIDES:
self.pauldron(side, sx, 'metal', 'trim', 3, 1.25)
self.bracer(side, 'metal', 'trim', (.35, .92), .012)
hp, kn, an, toe = self.leg[side]
self.plate('Greave_' + side, [kn + V((-.05, -.05, -.02)), kn + V((.05, -.05, -.02)), an + V((.045, -.05, .05)), an + V((-.045, -.05, .05))],
kn.lerp(an, .5) + V((0, -.08, 0)), .03, 'metal', 'Shin.' + side)
self.ell('Knee_' + side, kn + V((0, -.045, 0)), (.058, .04, .055), 'trim', 'Shin.' + side, 12, 8, smooth=False)
self.face('skin', 'dark', 'human', nose=1.35, jaw=1.05, brow=1.4)
# Beard, braids and moustache.
self.ell('Beard', (0, -.062 * hd, hc - .09 * hd), (.09 * hd, .07 * hd, .1 * hd), 'hair', 'Head', 18, 12, bumps=.06)
for side, sx in SIDES:
self.tube('Moustache_' + side, [(0, -.1 * hd, hc - .032 * hd), (sx * .04 * hd, -.1 * hd, hc - .045 * hd), (sx * .07 * hd, -.085 * hd, hc - .085 * hd)],
[.016 * hd, .018 * hd, .008 * hd], 'hair', 'Head', 10, 10)
top = V((sx * .035 * hd, -.09 * hd, hc - .14 * hd))
self.tube('Braid_' + side, [top, top + V((0, -.02, -.07)), top + V((sx * .005, -.022, -.15))], [.022, .02, .012], 'hair', 'Head', 10, 12,
groove=.25, lobes=3, weight_fn=lambda t: blend('Head', 'Chest', t))
self.ring('Braid_Bead_' + side, top + V((sx * .004, -.022, -.12)), (0, 0, 1), .02, .025, 'trim', 'Chest', 10)
# Dome helmet with nose guard and short horns.
self.ell('Helm', (0, .004, hc + .03 * hd), (.1 * hd, .11 * hd, .094 * hd), 'metal', 'Head', 20, 12)
self.ring('Helm_Rim', (0, .0, hc + .012 * hd), (0, 0, 1), .1 * hd, .03, 'trim', 'Head', 20)
self.plate('Nose_Guard', [(-.014, -.112 * hd, hc + .05 * hd), (.014, -.112 * hd, hc + .05 * hd), (.01, -.118 * hd, hc - .03 * hd), (-.01, -.118 * hd, hc - .03 * hd)],
(0, -.122 * hd, hc + .01), .01, 'metal', 'Head')
for side, sx in SIDES:
self.tube('Horn_' + side, [(sx * .08 * hd, 0, hc + .05 * hd), (sx * .14 * hd, -.01, hc + .08 * hd), (sx * .17 * hd, -.03, hc + .15 * hd)],
[.025 * hd, .018 * hd, .002], 'bone', 'Head', 10, 10)
def gear_troll(self):
self.build_body('skin', 'skin', 'skin', None, chest=1.08, belly=1.2)
H, hd, hc = self.hip, self.sp['head'], self.head_c
rx, ry = self.belt('leather', 'bone', H + .06, .02)
for q, y in ((-1, -ry - .02), (1, ry + .02)):
out = [(-.14, y, H + .05), (.14, y, H + .05), (.12, y, H - .2), (.02, y, H - .3), (-.1, y, H - .24)]
self.hanging('Loincloth_%d' % q, out, (0, y, H - .08), .016, 'leather', (0, q, 0), H + .05, H - .3)
# Stone growths along shoulders, back and forearms.
growths = [((.12, .08, self.shoulder + .02), (.2, .14, self.shoulder + .14)), ((-.12, .08, self.shoulder + .02), (-.22, .12, self.shoulder + .12)),
((0, .13, self.chest + .05), (0, .26, self.chest + .12)), ((.06, .13, self.chest - .08), (.1, .25, self.chest - .02)),
((-.07, .12, self.chest - .06), (-.12, .23, self.chest + .02)), ((0, .13, self.waist + .05), (0, .22, self.waist + .1))]
for k, (a, b) in enumerate(growths):
self.shard('Stone_Growth_%d' % k, [a, V(a).lerp(V(b), .55), b], .05, 'stone', 'Chest' if a[2] > self.chest - .1 else 'Spine', sides=5)
for side, sx in SIDES:
a, b = self.elbow[side], self.W[side]
for k in range(2):
p = a.lerp(b, .3 + .3 * k)
out = (p - self.sh[side]).cross(V((0, 0, 1))).normalized() * (-sx)
self.shard('Arm_Stone_%s%d' % (side, k), [p, p + out * .06 * self.s + V((0, .04, .04)) * self.s], .035 * self.s, 'stone', 'Forearm.' + side,
pre=False, sides=5)
hp, kn, an, toe = self.leg[side]
for k, dx in enumerate((-.03, 0, .03)):
self.ell('Toe_%s%d' % (side, k), toe + V((dx * 1.3, -.01, .01)), (.022, .03, .02), 'skin', 'Foot.' + side, 10, 7)
self.tube('Foot_' + side, [an + V((0, .04, -.05)), an.lerp(toe, .5), toe], [.06, .07, .05], 'skin', 'Foot.' + side, 12, 10, flatten=.65)
self.ring('Ankle_Wrap_' + side, an + V((0, 0, .04)), (0, 0, 1), .06, .04, 'leather', 'Shin.' + side, 12)
self.face('skin', 'eye', 'troll', nose=1.5, jaw=1.35, brow=1.7)
for side, sx in SIDES:
self.cone('Tusk_' + side, (sx * .04 * hd, -.09 * hd, hc - .075 * hd), (sx * .055 * hd, -.12 * hd, hc - .0 * hd), .014 * hd, 'bone', 'Head', 8)
self.tube('Hair_Tuft', [(0, -.02, hc + .1 * hd), (0, .05, hc + .13 * hd), (0, .12, hc + .08 * hd)], [.02, .03, .008], 'hair', 'Head', 8, 8, groove=.3)
self.ring('Necklace', (0, 0, self.neck - .03), (0, -.3, 1), .1, .015, 'leather', 'Chest', 14)
for k in range(5):
a = pi * (.25 + .5 * k / 4)
self.cone('Necklace_Tooth_%d' % k, (cos(a) * .1, -sin(a) * .1, self.neck - .05), (cos(a) * .1, -sin(a) * .11, self.neck - .1), .012, 'bone', 'Chest', 6)
def gear_fee(self):
self.build_body('cloth2', 'skin', 'skin', 'cloth2', chest=.92)
H, hd, hc = self.hip, self.sp['head'], self.head_c
w, d = self.sp['w'], self.sp['d']
self.loft('Dress', [(self.chest + .06, .18 * w, .115 * d), (self.waist, .15 * w, .11 * d), (H, .19 * w, .15 * d), (H - .18, .25 * w, .21 * d),
(H - .32, .3 * w, .26 * d)], 'cloth', self.skirt_weights(H + .02, H - .32, .45), 28, 16, scallop=.08)
self.loft('Bodice_Trim', [(self.chest + .05, .182 * w + .006, .118 * d + .006), (self.chest + .07, .18 * w + .006, .116 * d + .006)], 'trim', self.torso_weights, 24, 2)
self.loft('Sash', [(self.waist - .02, .15 * w + .01, .11 * d + .01), (self.waist + .02, .15 * w + .01, .11 * d + .01)], 'trim', self.torso_weights, 24, 2)
for side, sx in SIDES:
self.ring('Bracelet_' + side, self.elbow[side].lerp(self.W[side], .85), self.W[side] - self.elbow[side], .036 * self.s * .74 + .004, .01, 'trim',
'Forearm.' + side, 12, pre=False)
self.ell('Puff_Sleeve_' + side, self.sh[side] + V((sx * .02, 0, -.01)) * self.s, (.07 * self.s, .07 * self.s, .06 * self.s), 'cloth', 'UpperArm.' + side, 14, 9,
pre=False, bumps=.08)
self.face('skin', 'dark', 'elf', nose=.7, jaw=.85, brow=.6)
self.ell('Hair', (0, .018, hc + .02 * hd), (.098 * hd, .108 * hd, .104 * hd), 'hair', 'Head', 18, 12, bumps=.05)
self.ell('Hair_Bun', (0, .06 * hd, hc + .1 * hd), (.05 * hd, .05 * hd, .045 * hd), 'hair', 'Head', 14, 9, bumps=.1)
for side, sx in SIDES:
self.tube('Hair_Lock_' + side, [(sx * .075 * hd, -.05 * hd, hc + .0), (sx * .085 * hd, -.05 * hd, hc - .08 * hd), (sx * .08 * hd, -.03 * hd, hc - .15 * hd)],
[.02, .018, .006], 'hair', 'Head', 10, 10, groove=.2, lobes=4)
for k in range(6):
a = pi * (-.1 + 1.2 * k / 5)
c = (cos(a) * .09 * hd, -sin(a) * .09 * hd, hc + .07 * hd)
self.ell('Flower_%d' % k, c, (.02, .02, .012), 'cloth2', 'Head', 8, 5, bumps=.3)
self.ell('Flower_Heart_%d' % k, (c[0], c[1] - .005, c[2] + .008), (.007, .007, .006), 'glow', 'Head', 6, 4)
# Two pairs of luminous wings with a vein frame.
for side, sx in SIDES:
root = V((sx * .04, .1, self.chest + .04))
for k, (span, rise, width) in enumerate(((.46, .2, .2), (.32, -.14, .13))):
tip = root + V((sx * span, .08, rise))
mid = root.lerp(tip, .55)
perp = V((0, 0, 1)) if k == 0 else V((0, 0, -1))
outline = [root, mid + perp * width * .9 + V((0, .02, 0)), tip + perp * width * .3, tip, tip - perp * width * .25, mid - perp * width * .35]
self.plate('Wing_%s%d' % (side, k), outline, mid + V((0, .012, 0)), .01, 'glow', 'Wing.' + side, normal=(0, 1, 0))
self.tube('Wing_Vein_%s%d' % (side, k), [root, mid + perp * width * .3, tip], [.008, .006, .002], 'trim', 'Wing.' + side, 6, 10)
self.extra_bones.append(('Wing.' + side, V((sx * .04, .1, self.chest + .04)), V((sx * .3, .16, self.chest + .1)), 'Chest'))
def gear_ork(self):
self.build_body('skin', 'skin', 'leather', 'leather', chest=1.1, belly=1.05)
H, hd, hc = self.hip, self.sp['head'], self.head_c
rx, ry = self.belt('leather', 'metal', H + .07, .02)
for k in range(8):
a = 2 * pi * (k + .5) / 8
x, y = cos(a) * (rx + .012), sin(a) * (ry + .012)
n = V((cos(a), sin(a), 0))
t = V((-sin(a), cos(a), 0)) * .032
out = [V((x, y, H + .06)) + t, V((x, y, H + .06)) - t, V((x, y, H - .22)) - t * .8, V((x, y, H - .26)), V((x, y, H - .22)) + t * .8]
self.hanging('Kilt_%d' % k, out, V((x, y, H - .08)), .014, 'leather' if k % 2 else 'trim', n, H + .06, H - .26)
# Harness straps crossing the chest.
for q in (-1, 1):
pts = []
for z, x in ((self.shoulder, q * .15), (self.chest, q * .02), (self.waist + .03, -q * .12)):
rx2, ry2 = self.section_at(z)
pts.append((x, -ry2 * 1.08 - .008, z))
self.tube('Strap_%d' % q, pts, [.018, .018, .018], 'leather', 'Chest', 8, 12, flatten=.4,
weight_fn=lambda t: self.torso_weights(self.shoulder - t * (self.shoulder - self.waist)))
rx2, ry2 = self.section_at(self.chest)
self.ring('Strap_Ring', (0, -ry2 * 1.08 - .012, self.chest), (0, 1, 0), .03, .012, 'metal', 'Chest', 12)
self.pauldron('L', 1, 'metal', 'trim', 3, 1.3, spikes=3, spike_kind='metal')
self.ell('Fur_Mantle', self.sh['R'] + V((.0, .03, .03)) * self.s, (.11 * self.s, .1 * self.s, .07 * self.s), 'fur', 'UpperArm.R', 16, 9, pre=False, bumps=.2)
# Skull trophy on the belt.
sk = V((-.1, -ry - .03, H + .02))
self.ell('Trophy_Skull', sk, (.04, .045, .045), 'bone', 'Pelvis', 12, 8)
for sx in (-1, 1):
self.ell('Trophy_Eye_%d' % sx, sk + V((sx * .016, -.036, .01)), (.011, .006, .01), 'dark', 'Pelvis', 8, 5)
for side, sx in SIDES:
self.bracer(side, 'metal', 'leather', (.45, .9), .01)
p = self.elbow[side].lerp(self.W[side], .65)
out = (p - self.sh[side]).cross(V((0, 0, 1))).normalized() * (-sx)
for k in range(2):
q = self.elbow[side].lerp(self.W[side], .55 + .2 * k)
self.shard('Bracer_Spike_%s%d' % (side, k), [q + out * .05 * self.s, q + out * .1 * self.s], .015 * self.s, 'metal', 'Forearm.' + side, pre=False)
hp, kn, an, toe = self.leg[side]
self.ell('Toe_Cap_' + side, toe + V((0, .03, .015)), (.05, .06, .035), 'metal', 'Foot.' + side, 12, 8, smooth=False)
self.face('skin', 'dark', 'orc', nose=1.2, jaw=1.3, brow=1.5)
for side, sx in SIDES:
self.cone('Tusk_' + side, (sx * .035 * hd, -.09 * hd, hc - .07 * hd), (sx * .045 * hd, -.11 * hd, hc - .02 * hd), .012 * hd, 'bone', 'Head', 8)
self.plate('War_Paint_' + side, [(sx * .02, -.094 * hd, hc + .03), (sx * .06, -.08 * hd, hc + .03), (sx * .055, -.082 * hd, hc - .005), (sx * .02, -.096 * hd, hc)],
(sx * .04, -.095 * hd, hc + .015), .004, 'trim', 'Head')
for k in range(6):
z = hc + .1 * hd - abs(k - 2.5) * .006
self.shard('Mohawk_%d' % k, [(0, -.06 * hd + k * .03 * hd, z), (0, -.06 * hd + k * .03 * hd + .02, z + .08 - abs(k - 2.5) * .01)], .024, 'hair', 'Head', sides=4)
def gear_biest(self):
self.build_body('fur', 'fur', 'fur', None, chest=1.08)
H, hd, hc = self.hip, self.sp['head'], self.head_c
self.armour_loft('Chest_Fur', self.chest - .1, self.chest + .12, .004, 'fur2', bulge=.12)
rx, ry = self.belt('leather', 'metal', H + .07, .016)
for q, y in ((-1, -ry - .02), (1, ry + .02)):
out = [(-.1, y, H + .06), (.1, y, H + .06), (.08, y, H - .18), (0, y, H - .24), (-.08, y, H - .18)]
self.hanging('Loincloth_%d' % q, out, (0, y, H - .06), .014, 'cloth', (0, q, 0), H + .06, H - .24)
# Mane tufts around neck and shoulders.
for k in range(14):
a = 2 * pi * k / 14
base = V((cos(a) * .12, sin(a) * .1 + .02, self.neck - .02))
tip = base + V((cos(a) * .07, sin(a) * .07 + .03, -.05 + .03 * sin(3 * a)))
self.shard('Mane_%d' % k, [base, base.lerp(tip, .5) + V((0, 0, .02)), tip], .04, 'fur2' if k % 3 == 0 else 'fur', 'Chest', sides=5)
# Bone necklace.
self.ring('Necklace', (0, -.01, self.neck - .06), (0, -.4, 1), .12, .01, 'leather', 'Chest', 14)
for k in range(5):
a = pi * (.2 + .6 * k / 4)
self.cone('Necklace_Fang_%d' % k, (cos(a) * .12, -sin(a) * .115, self.neck - .075), (cos(a) * .12, -sin(a) * .125, self.neck - .13), .01, 'bone', 'Chest', 6)
for side, sx in SIDES:
self.bracer(side, 'leather', 'metal', (.5, .9), .012)
hp, kn, an, toe = self.leg[side]
self.tube('Paw_' + side, [an + V((0, .02, -.02)), an.lerp(toe, .6), toe], [.045, .052, .04], 'fur', 'Foot.' + side, 12, 10, flatten=.7)
for k, dx in enumerate((-.025, 0, .025)):
self.cone('Foot_Claw_%s%d' % (side, k), toe + V((dx, -.02, .0)), toe + V((dx * 1.3, -.07, -.02)), .01, 'bone', 'Foot.' + side, 6)
# Beast head: long snout, ram horns, animal ears, amber eyes.
self.ell('Skull', (0, .0, hc), (.085 * hd, .1 * hd, .1 * hd), 'fur', 'Head', 20, 14)
self.ell('Snout', (0, -.1 * hd, hc - .04 * hd), (.05 * hd, .09 * hd, .045 * hd), 'fur', 'Head', 16, 10, axes=[(1, 0, 0), (0, 1, .3), (0, -.3, 1)])
self.ell('Muzzle_Light', (0, -.12 * hd, hc - .065 * hd), (.042 * hd, .07 * hd, .03 * hd), 'fur2', 'Head', 14, 8)
self.ell('Nose', (0, -.18 * hd, hc - .02 * hd), (.022 * hd, .014 * hd, .015 * hd), 'dark', 'Head', 10, 6)
self.ell('Brow', (0, -.07 * hd, hc + .03 * hd), (.07 * hd, .03 * hd, .02 * hd), 'fur', 'Head', 14, 8)
for side, sx in SIDES:
self.ell('Eye_' + side, (sx * .038 * hd, -.08 * hd, hc + .012 * hd), (.014 * hd, .008 * hd, .01 * hd), 'eye', 'Head', 10, 7)
self.cone('Fang_' + side, (sx * .025 * hd, -.16 * hd, hc - .07 * hd), (sx * .025 * hd, -.16 * hd, hc - .1 * hd), .007 * hd, 'bone', 'Head', 6)
self.tube('Ear_' + side, [(sx * .07 * hd, .02, hc + .04 * hd), (sx * .12 * hd, .04, hc + .06 * hd), (sx * .16 * hd, .06, hc + .05 * hd)],
[.025, .02, .002], 'fur', 'Head', 8, 8, flatten=.4, ref=(0, 0, 1))
pts = [V((sx * .06 * hd, .02, hc + .07 * hd))]
for q in range(1, 9):
a = q / 8 * 1.6 * pi
pts.append(V((sx * (.09 + .07 * (1 - cos(a * .5))) * hd, .06 - .08 * sin(a), hc + .08 * hd + .09 * sin(a * .5 + .3) - .06 * q / 8)))
self.tube('Horn_' + side, pts, [.03 * hd, .027 * hd, .022 * hd, .016 * hd, .004], 'bone', 'Head', 10, 30, groove=.08, lobes=3)
# Tail on two bones.
t0 = V((0, .13, H + .02))
self.tube('Tail', [t0, t0 + V((0, .14, -.08)), t0 + V((0, .26, -.22)), t0 + V((0, .3, -.4))], [.04, .032, .022, .01], 'fur', 'Tail.1', 12, 20,
weight_fn=lambda t: blend('Pelvis', 'Tail.1', t * 4) if t < .25 else blend('Tail.1', 'Tail.2', (t - .25) / .5))
self.ell('Tail_Tuft', t0 + V((0, .3, -.43)), (.04, .04, .06), 'fur2', 'Tail.2', 10, 7, bumps=.25)
self.extra_bones.append(('Tail.1', t0, t0 + V((0, .2, -.14)), 'Pelvis'))
self.extra_bones.append(('Tail.2', t0 + V((0, .2, -.14)), t0 + V((0, .3, -.4)), 'Tail.1'))
# ------------------------------------------------------------ rig
def rig(self):
for ob in self.parts:
bm = bmesh.new()
bm.from_mesh(ob.data)
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0000001)
bmesh.ops.triangulate(bm, faces=list(bm.faces))
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
if bm.calc_volume(signed=True) < 0:
bmesh.ops.reverse_faces(bm, faces=list(bm.faces))
bm.to_mesh(ob.data)
bm.free()
ob.data.update()
bpy.ops.object.armature_add(enter_editmode=True)
rig = bpy.context.object
rig.name = 'PeopleRig'
ed = rig.data.edit_bones
ed.remove(ed[0])
X, s = self.X, self.s
def bone(name, head, tail, parent=None):
b = ed.new(name)
b.head, b.tail = head, tail
if parent:
b.parent = ed[parent]
self.defs[name] = (V(head), V(tail), parent)
H = self.hip
bone('Root', (0, 0, 0), (0, 0, .15 * s))
bone('Pelvis', X((0, .0, H - .06)), X((0, .0, self.waist)), 'Root')
bone('Spine', X((0, .0, self.waist)), X((0, .0, self.chest)), 'Pelvis')
bone('Chest', X((0, .0, self.chest)), X((0, .0, self.neck)), 'Spine')
bone('Neck', X((0, .0, self.neck)), X((0, -.01, self.head_c - .05)), 'Chest')
bone('Head', X((0, -.01, self.head_c - .05)), X((0, -.01, self.head_c + .12 * self.sp['head'])), 'Neck')
for side, sx in SIDES:
bone('Clavicle.' + side, X((sx * .05, .012, self.shoulder - .02)), self.sh[side], 'Chest')
bone('UpperArm.' + side, self.sh[side], self.elbow[side], 'Clavicle.' + side)
bone('Forearm.' + side, self.elbow[side], self.W[side], 'UpperArm.' + side)
fore = (self.W[side] - self.elbow[side]).normalized()
bone('Hand.' + side, self.W[side], self.W[side] + fore * .08 * s, 'Forearm.' + side)
hp, kn, an, toe = self.leg[side]
bone('Thigh.' + side, X(hp), X(kn), 'Pelvis')
bone('Shin.' + side, X(kn), X(an), 'Thigh.' + side)
bone('Foot.' + side, X(an), X(toe), 'Shin.' + side)
# Grip bones: the hands follow these; the weapon meshes are weighted to them.
if self.weapon_parent == 'Offhand':
order = [('Offhand', 'Chest'), ('Weapon', 'Offhand')]
elif self.target['L'] == 'Weapon':
order = [('Weapon', 'Chest'), ('Offhand', 'Weapon')]
else:
order = [('Weapon', 'Chest'), ('Offhand', 'Chest')]
for name, parent in order:
side = 'R' if name == 'Weapon' else 'L'
bone(name, self.G[side], self.G[side] + self.u[side] * .25 * s, parent)
if self.sp['style'] == 'bow':
aim = (self.G['L'] - self.G['R']).normalized()
bone('Arrow', self.G['R'], self.G['R'] + aim * .3 * s, 'Weapon')
self.scaled.add('Arrow')
for name, head, tail, parent in self.extra_bones:
bone(name, X(head), X(tail), parent)
bpy.ops.object.mode_set(mode='OBJECT')
for ob in self.parts:
mod = ob.modifiers.new('Weighted body', 'ARMATURE')
mod.object = rig
ob.parent = rig
self.rig_ob = rig
def pose_rotation(self, pb, r):
q = Quaternion((0, 0, 1), r[2]) @ Quaternion((0, 1, 0), r[1]) @ Quaternion((1, 0, 0), r[0])
basis = pb.bone.matrix_local.to_quaternion()
return basis.inverted() @ q @ basis
def aim_bone(self, name, head, tail):
pb = self.rig_ob.pose.bones[name]
rest = pb.bone.matrix_local
direction = self.defs[name][1] - self.defs[name][0]
rotation = direction.rotation_difference(tail - head).to_matrix().to_4x4()
pose = rotation @ rest
pose.translation = head
pb.matrix = pose
bpy.context.view_layer.update()
def delta(self, name):
return self.rig_ob.pose.bones[name].matrix @ self.rig_ob.data.bones[name].matrix_local.inverted()
# ------------------------------------------------------------ animation
def animate(self, name, t, dur):
rs, locs, scl = {}, {}, {}
s, L = self.s, self.L
st = self.sp['style']
def rot(b, x=0, y=0, z=0):
old = rs.get(b, (0, 0, 0))
rs[b] = (old[0] + x, old[1] + y, old[2] + z)
def loc(b, x=0, y=0, z=0):
old = locs.get(b, (0, 0, 0))
locs[b] = (old[0] + x, old[1] + y, old[2] + z)
flap = 0.0
if name == 'idle':
breath = sin(2 * pi * t / dur)
rot('Spine', .012 * breath)
rot('Chest', -.014 * breath, 0, .006 * breath)
rot('Head', .01 * breath, 0, -.02 * sin(2 * pi * t / dur))
loc('Weapon', 0, 0, .006 * L * breath)
loc('Offhand', 0, 0, .005 * L * sin(2 * pi * t / dur + 1))
flap = sin(2 * pi * t * 4)
if self.hover:
loc('Root', 0, 0, .025 * s * sin(2 * pi * t * 2 / dur))
if st == 'bow':
loc('Offhand', 0, .14 * L, -.28 * L)
rot('Offhand', .35)
elif name in ('walk', 'start', 'stop'):
fade = 1 if name == 'walk' else sin(pi * .5 * t / dur) if name == 'start' else cos(pi * .5 * t / dur)
ph = 2 * pi * t / 1.1
for side, sx in SIDES:
q = ph + (0 if side == 'L' else pi)
if self.hover:
rot('Thigh.' + side, -.12 * sin(q) * fade)
rot('Shin.' + side, .18 * fade)
continue
thigh = -.42 * sin(q) * fade
knee = .62 * max(0.0, cos(q)) * fade + .06 * fade
if self.sp.get('digitigrade'):
knee = -knee
rot('Thigh.' + side, thigh)
rot('Shin.' + side, knee)
rot('Foot.' + side, -(thigh + knee) * .6)
rot('Pelvis', 0, 0, .05 * sin(ph) * fade)
rot('Spine', .03 * fade, 0, -.04 * sin(ph) * fade)
rot('Chest', 0, 0, -.02 * sin(ph) * fade)
loc('Root', 0, 0, (-.018 * s * (1 - cos(2 * ph)) * .5 if not self.hover else .02 * s * sin(2 * ph)) * fade)
loc('Weapon', 0, .015 * L * sin(ph) * fade, .012 * L * sin(2 * ph) * fade)
loc('Offhand', 0, -.015 * L * sin(ph) * fade, .01 * L * sin(2 * ph + 1) * fade)
flap = sin(2 * pi * t * 6 / 1.1)
if st == 'bow':
loc('Offhand', 0, .14 * L, -.28 * L)
rot('Offhand', .35)
rot('Cloak', .12 * fade + .04 * sin(2 * ph) * fade)
elif name in ('attack_a', 'attack_b', 'ability'):
self.attack(name, t, rot, loc, scl)
flap = sin(2 * pi * t * 5)
elif name == 'hit':
q = curve(t, [(0, 0), (.066667, 1), (.2, .4), (.5, 0)])
rot('Spine', -.1 * q, 0, -.05 * q)
rot('Chest', -.06 * q)
rot('Head', -.14 * q, 0, .08 * q)
loc('Root', 0, .04 * s * q, 0)
loc('Weapon', 0, .04 * L * q, .03 * L * q)
loc('Offhand', 0, .03 * L * q, .02 * L * q)
flap = sin(2 * pi * t * 6)
if st == 'bow':
loc('Offhand', 0, .14 * L * (1 - q), -.28 * L * (1 - q))
rot('Offhand', .35 * (1 - q))
elif name == 'death':
q = curve(t, [(0, 0), (.3, .12), (.8, .6), (1.3, 1), (1.5, .97), (2, 1)])
rot('Root', -1.38 * q)
drop = self.hover if self.hover else 0.0
loc('Root', 0, .02 * s * q, (.05 * s - drop) * q)
rot('Spine', .1 * q)
rot('Head', -.25 * q, 0, .2 * q)
loc('Weapon', 0, .08 * L * q, -.06 * L * q)
loc('Offhand', 0, .06 * L * q, -.08 * L * q)
for side, sx in SIDES:
rot('Thigh.' + side, -.35 * q)
rot('Shin.' + side, (.55 if not self.sp.get('digitigrade') else -.4) * q)
flap = sin(2 * pi * t * 6) * (1 - q)
rot('Wing.L', 0, -.4 * q)
rot('Wing.R', 0, .4 * q)
if 'Wing.L' in self.defs:
rot('Wing.L', 0, .45 * flap, -.2 * flap)
rot('Wing.R', 0, -.45 * flap, .2 * flap)
if 'Tail.1' in self.defs:
period = dur if name in ('idle', 'walk') else 1.3
rot('Tail.1', .1 * sin(2 * pi * t / period), 0, .3 * sin(2 * pi * t / period))
rot('Tail.2', .05 * sin(2 * pi * t / period), 0, .4 * sin(2 * pi * t / period - .8))
self.apply(rs, locs, scl, name, t)
def attack(self, name, t, rot, loc, scl):
st, s, L = self.sp['style'], self.s, self.L
alt, charged = name == 'attack_b', name == 'ability'
if st in ('blade_shield', 'axe'):
if charged and st == 'blade_shield':
bash = curve(t, [(0, 0), (.55, -.45), (.82, -.45), (.9, 1), (1.05, 1), (1.3, 0)])
loc('Offhand', .02 * L * bash, -.16 * L * max(0.0, bash) + .06 * L * max(0.0, -bash), .02 * L * abs(bash))
loc('Weapon', 0, .06 * L * abs(bash), .05 * L * abs(bash))
rot('Chest', 0, 0, .28 * bash)
rot('Spine', .08 * max(0.0, bash))
loc('Root', 0, -.1 * s * max(0.0, bash), 0)
rot('Thigh.L', -.25 * max(0.0, bash))
rot('Shin.L', .2 * max(0.0, bash))
return
if charged:
# War cry: both arms up, head back, then a forward stamp.
cry = curve(t, [(0, 0), (.5, 1), (.85, 1), (.9, .4), (1.1, .2), (1.3, 0)])
loc('Weapon', -.05 * L * cry, .06 * L * cry, .4 * L * cry)
rot('Weapon', -.5 * cry)
loc('Offhand', .05 * L * cry, .05 * L * cry, .42 * L * cry)
rot('Head', -.3 * cry)
rot('Chest', -.12 * cry)
stamp = curve(t, [(0, 0), (.8, 0), (.9, 1), (1.1, 1), (1.3, 0)])
rot('Thigh.R', -.3 * stamp)
loc('Root', 0, -.06 * s * stamp, 0)
return
if not alt:
up = curve(t, [(0, 0), (.45, 1), (.78, 1), (.9, -.7), (1.05, -.7), (1.3, 0)])
rot('Weapon', -1.3 * up, 0, -.3 * up)
if up > 0:
loc('Weapon', -.04 * L * up, .1 * L * up, .3 * L * up)
else:
loc('Weapon', -.08 * L * up, .16 * L * up, .02 * L * up)
rot('Chest', -.1 * min(0.0, up), 0, .28 * up)
rot('Spine', -.06 * max(0.0, up) + .1 * max(0.0, -up))
if st == 'axe':
loc('Offhand', 0, .1 * L * max(0.0, up), .05 * L * max(0.0, up))
else:
side = curve(t, [(0, 0), (.45, 1), (.78, 1), (.9, -.8), (1.05, -.8), (1.3, 0)])
sgn = 1 if st == 'blade_shield' else -1
rot('Weapon', .5 * abs(side), 1.25 * side * sgn, 0)
loc('Weapon', .14 * L * side * sgn * -1, .06 * L * side, .1 * L * abs(side))
rot('Chest', 0, 0, .32 * side * sgn)
rot('Thigh.R', .15 * curve(t, [(0, 0), (.45, 1), (.9, -1), (1.3, 0)]))
rot('Thigh.L', -.2 * curve(t, [(0, 0), (.45, 0), (.9, 1), (1.3, 0)]))
rot('Shin.L', .2 * curve(t, [(0, 0), (.45, 0), (.9, 1), (1.3, 0)]))
elif st == 'club2h':
if not alt:
amp = 1.12 if charged else 1.0
up = curve(t, [(0, 0), (.5, 1), (.8, 1), (.9, -.6), (1.1, -.6), (1.3, 0)])
rot('Weapon', -1.25 * up * amp)
if up > 0:
loc('Weapon', .04 * L * up, .08 * L * up, .3 * L * up * amp)
else:
loc('Weapon', 0, .12 * L * up, .08 * L * up)
rot('Spine', -.16 * max(0.0, up) + .14 * max(0.0, -up))
rot('Chest', -.08 * max(0.0, up) + .1 * max(0.0, -up))
if charged:
loc('Root', 0, 0, -.06 * s * max(0.0, -up))
for side, _ in SIDES:
rot('Thigh.' + side, -.2 * max(0.0, -up))
rot('Shin.' + side, .35 * max(0.0, -up))
else:
side = curve(t, [(0, 0), (.5, 1), (.8, 1), (.9, -.8), (1.1, -.8), (1.3, 0)])
rot('Weapon', .35 * abs(side), -1.0 * side, 0)
loc('Weapon', -.12 * L * side, .06 * L * side, .06 * L * abs(side))
rot('Chest', 0, 0, -.3 * side)
rot('Spine', 0, 0, -.12 * side)
elif st == 'bow':
raise_bow = curve(t, [(0, 0), (.25, 1), (1.12, 1), (1.3, 0)])
loc('Offhand', 0, .14 * L * (1 - raise_bow), -.28 * L * (1 - raise_bow))
rot('Offhand', .35 * (1 - raise_bow), -.3 * raise_bow if alt else 0, 0)
full = .45 if not charged else .5
draw = curve(t, [(0, 0), (.3, 0), (.82, 1), (.9, 1), (.933333, 1.12), (1.1, 1.12), (1.28, 0), (1.3, 0)])
loc('Weapon', 0, full * L * min(draw, 1.0), .02 * L * max(0.0, draw - 1) * 8)
rot('Chest', 0, 0, -.25 * raise_bow)
rot('Head', 0, 0, .35 * raise_bow)
rot('Spine', -.04 * raise_bow if not charged else -.08 * raise_bow)
scale = 1.0 if t < .9 or t > 1.22 else .001
scl['Arrow'] = (scale, scale, scale)
elif st == 'staff':
if charged:
up = curve(t, [(0, 0), (.6, 1), (.85, 1), (.9, 1.1), (1.1, 1), (1.3, 0)])
loc('Weapon', -.04 * L * up, .05 * L * up, .36 * L * up)
loc('Offhand', .04 * L * up, .02 * L * up, .34 * L * up)
rot('Offhand', -.8 * up)
rot('Head', -.2 * up)
loc('Root', 0, 0, .08 * s * up)
elif not alt:
th = curve(t, [(0, 0), (.5, -.5), (.8, -.6), (.9, 1), (1.05, 1), (1.3, 0)])
loc('Weapon', 0, -.2 * L * th, .06 * L * abs(th))
rot('Weapon', .5 * th)
loc('Offhand', 0, -.12 * L * max(0.0, th), .08 * L * max(0.0, th))
rot('Chest', 0, 0, -.15 * th)
else:
cast = curve(t, [(0, 0), (.5, -.4), (.82, -.5), (.9, 1), (1.05, 1), (1.3, 0)])
loc('Offhand', .04 * L * cast, -.2 * L * max(0.0, cast) + .08 * L * max(0.0, -cast), .1 * L * abs(cast))
rot('Offhand', -.4 * max(0.0, cast))
rot('Chest', 0, 0, .2 * cast)
elif st == 'claws':
if charged:
pounce = curve(t, [(0, 0), (.55, -.5), (.8, -.6), (.9, 1), (1.05, 1), (1.3, 0)])
loc('Root', 0, -.35 * s * max(0.0, pounce), -.05 * s * max(0.0, -pounce) + .04 * s * max(0.0, pounce))
rot('Spine', .2 * max(0.0, pounce) - .1 * max(0.0, -pounce))
for side, sx in SIDES:
b = 'Weapon' if side == 'R' else 'Offhand'
loc(b, 0, -.1 * L * max(0.0, pounce) + .08 * L * max(0.0, -pounce), .25 * L * max(0.0, -pounce) - .1 * L * max(0.0, pounce))
rot(b, .6 * max(0.0, pounce))
rot('Thigh.' + side, .3 * max(0.0, -pounce) - .2 * max(0.0, pounce))
rot('Shin.' + side, .4 * max(0.0, -pounce))
else:
b, sx = ('Weapon', -1) if not alt else ('Offhand', 1)
sw = curve(t, [(0, 0), (.5, 1), (.8, 1), (.9, -.8), (1.05, -.8), (1.3, 0)])
if sw > 0:
loc(b, sx * .12 * L * sw, .1 * L * sw, .28 * L * sw)
else:
loc(b, sx * .16 * L * sw, .12 * L * sw, .12 * L * sw)
rot(b, -.6 * sw, 0, -sx * .4 * sw)
rot('Chest', .08 * max(0.0, -sw), 0, -sx * .3 * sw)
rot('Spine', .06 * max(0.0, -sw))
loc('Root', 0, -.05 * s * max(0.0, -sw), 0)
def apply(self, rs, locs, scl, name, t):
rig = self.rig_ob
for pb in rig.pose.bones:
pb.rotation_mode = 'QUATERNION'
pb.rotation_quaternion = self.pose_rotation(pb, rs.get(pb.name, (0, 0, 0)))
pb.location = pb.bone.matrix_local.to_3x3().inverted() @ V(locs.get(pb.name, (0, 0, 0)))
pb.scale = scl.get(pb.name, (1, 1, 1))
bpy.context.view_layer.update()
chest = self.delta('Chest')
for side, _ in SIDES:
target = self.delta(self.target[side])
c = target @ self.W[side]
a = rig.pose.bones['UpperArm.' + side].head.copy()
elbow, reach, error = ik_elbow(a, c, self.l1, self.l2, chest @ self.pole[side])
self.aim_bone('UpperArm.' + side, a, elbow)
self.aim_bone('Forearm.' + side, elbow, reach)
hand = target @ rig.data.bones['Hand.' + side].matrix_local
hand.translation = c
rig.pose.bones['Hand.' + side].matrix = hand
bpy.context.view_layer.update()
self.errors.append((error, name, round(t, 3), side))
def bake(self):
rig = self.rig_ob
for name, (dur, loop) in CLIPS.items():
rig.animation_data_clear()
rig.animation_data_create()
action = bpy.data.actions.new(name)
action.use_fake_user = True
rig.animation_data.action = action
for frame in range(round(dur * 30) + 1):
self.animate(name, frame / 30, dur)
for pb in rig.pose.bones:
pb.keyframe_insert('rotation_quaternion', frame=frame)
pb.keyframe_insert('location', frame=frame)
if pb.name in self.scaled:
pb.keyframe_insert('scale', frame=frame)
action['loop'] = loop
action['contact_seconds'] = CONTACT if name in ('attack_a', 'attack_b', 'ability') else -1
for layer in action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for fc in bag.fcurves:
for key in fc.keyframe_points:
key.interpolation = 'LINEAR'
rig.animation_data_clear()
rig.animation_data_create()
rig.animation_data.action = bpy.data.actions['idle']
bpy.context.scene.render.fps = 30
bpy.context.scene.frame_set(0)
def export(self):
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / ('peoples/%s.blend' % self.key)))
source_parts = len(self.parts)
bpy.ops.object.select_all(action='DESELECT')
for ob in self.parts:
ob.select_set(True)
bpy.context.view_layer.objects.active = self.parts[0]
bpy.ops.object.join()
body = bpy.context.object
body.name = '%sBody' % self.sp['tag']
body.select_set(True)
self.rig_ob.select_set(True)
bpy.context.view_layer.objects.active = self.rig_ob
path = OUT / ('%s.glb' % self.key)
bpy.ops.export_scene.gltf(filepath=str(path), export_format='GLB', use_selection=True, export_animations=True,
export_animation_mode='ACTIONS', export_force_sampling=True, export_frame_range=False,
export_anim_slide_to_zero=True, export_skins=True, export_all_influences=False,
export_def_bones=True, export_anim_single_armature=True, export_materials='EXPORT', export_yup=True)
raw = path.read_bytes()
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
tris = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
mins = [min(v.co[i] for v in body.data.vertices) for i in range(3)]
maxs = [max(v.co[i] for v in body.data.vertices) for i in range(3)]
worst = max(self.errors)
record = {'id': self.key, 'name': self.sp['name'], 'people': self.sp['people'], 'klasse': self.sp['klasse'], 'rolle': self.sp['rolle'],
'resource_path': 'res://assets/luneraria/peoples/%s.glb' % self.key, 'triangles': tris, 'mesh_parts': 1,
'parts': [body.name], 'skeletal': True, 'bones': len(self.defs), 'clips': list(CLIPS),
'attack_cycle': ['attack_a', 'attack_b', 'attack_a', 'ability'], 'contact_seconds': CONTACT,
'bounds_godot': {'min': [mins[0], mins[2], -maxs[1]], 'max': [maxs[0], maxs[2], -mins[1]]},
'sha256': hashlib.sha256(raw).hexdigest(), 'source': 'assets-source/luneraria/peoples/%s.blend' % self.key}
audit = {'id': self.key, 'source_parts': source_parts, 'exported_clips': [a['name'] for a in doc['animations']],
'skins': len(doc.get('skins', [])), 'materials': [m['name'] for m in doc['materials']],
'max_wrist_contact_error_m': worst[0], 'worst_sample': list(worst[1:]), 'samples': len(self.errors),
'contact_validation': 'Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips.'}
return record, audit
def reset():
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.context.preferences.filepaths.save_version = 0
only = [a for a in sys.argv[sys.argv.index('--') + 1:]] if '--' in sys.argv else []
records, audits = [], []
for key, spec in PEOPLES.items():
if only and key not in only:
continue
reset()
figure = Figure(key, spec)
figure.build()
figure.rig()
figure.bake()
record, audit = figure.export()
records.append(record)
audits.append(audit)
print('PEOPLE_BUILD', key, record['triangles'], 'tris', record['bones'], 'bones', 'wrist error %.5f m at %s' % (audit['max_wrist_contact_error_m'],
audit['worst_sample']), flush=True)
if not only:
(P / 'assets/luneraria/peoples_manifest.json').write_text(json.dumps({'assets': records}, indent=2) + '\n')
(SRC / 'peoples-audit.json').write_text(json.dumps({'figures': audits}, indent=2) + '\n')