"""Basaltbastion: weighted Glutpakt tank with magma seams and authored clips. Blender Z up, -Y forward. Run in a fresh background Blender process. """ from pathlib import Path import bpy, bmesh, math, random, json, hashlib from mathutils import Vector, Quaternion from math import sin, cos, pi P = Path(__file__).resolve().parents[2] OUT = P / 'assets/characters' OUT.mkdir(exist_ok=True) SRC = Path(__file__).resolve().parent random.seed(63) bpy.context.preferences.filepaths.save_version = 0 bpy.ops.wm.read_factory_settings(use_empty=True) S = 1.0 parts = [] def coord(v): return Vector(v) * S def material(name, color, rough=.8, metal=0.0, emission=None): m = bpy.data.materials.new('BB_' + name) m.diffuse_color = (*color, 1) m.use_nodes = True n = m.node_tree.nodes l = m.node_tree.links b = n.get('Principled BSDF') b.inputs['Base Color'].default_value = (*color, 1) b.inputs['Roughness'].default_value = rough b.inputs['Metallic'].default_value = metal if emission is not None: b.inputs['Emission Color'].default_value = (*emission, 1) b.inputs['Emission Strength'].default_value = .65 else: v = n.new('ShaderNodeVertexColor') v.layer_name = 'SurfaceColor' l.new(v.outputs['Color'], b.inputs['Base Color']) noise = n.new('ShaderNodeTexNoise') noise.inputs['Scale'].default_value = 18 noise.inputs['Detail'].default_value = 3.5 bump = n.new('ShaderNodeBump') bump.inputs['Strength'].default_value = .35 bump.inputs['Distance'].default_value = .02 l.new(noise.outputs['Fac'], bump.inputs['Height']) l.new(bump.outputs['Normal'], b.inputs['Normal']) return m M = { 'basalt': material('Basalt', (.040, .049, .059), .92), 'basalt2': material('BasaltLight', (.085, .101, .119), .88), 'crack': material('Magma', (.78, .19, .022), .55, emission=(.95, .11, .009)), 'iron': material('Iron', (.28, .30, .33), .4, .8), 'brass': material('Brass', (.55, .38, .14), .38, .7), 'ember': material('Ember', (.95, .32, .045), .4, emission=(1.0, .24, .018)), } def mesh(name, verts, faces, kind, bone, uv=None, colors=None, weights=None, smooth=True): me = bpy.data.meshes.new(name) me.from_pydata([coord(v) for v in verts], [], faces) me.update() ob = bpy.data.objects.new(name, me) bpy.context.collection.objects.link(ob) me.materials.append(M[kind]) parts.append(ob) for f in me.polygons: f.use_smooth = smooth tex = me.uv_layers.new(name='UVMap') col = me.color_attributes.new(name='SurfaceColor', type='FLOAT_COLOR', domain='CORNER') base = M[kind].diffuse_color[:3] for f in me.polygons: for li in f.loop_indices: i = me.loops[li].vertex_index v = verts[i] tex.data[li].uv = uv[i] if uv else (v[0] * .8 + v[1] * .2, v[2] * .9) fac = .88 + .12 * sin(v[0] * 17 + v[2] * 11) + .04 * cos(v[1] * 41) c = colors[i] if colors else tuple(max(.01, x * fac) for x in base) col.data[li].color = (*c, 1) groups = {} for i in range(len(verts)): ws = weights[i] if weights else {bone: 1.0} total = sum(ws.values()) for key, w in ws.items(): if key not in groups: groups[key] = ob.vertex_groups.new(name=key) groups[key].add([i], w / total, 'REPLACE') return ob def mixweights(t, bones): if len(bones) == 1: return {bones[0]: 1} t = max(0, min(.999999, t)) * (len(bones) - 1) i = int(t) w = t - i return {bones[i]: 1 - w, bones[i + 1]: w} def tube(name, points, radii, kind, bones, sides=20, steps=36, flatten=1.0, groove=.06): points = [Vector(x) for x in points] vs, uv, ws, loops, fs = [], [], [], [], [] def point(t): a = min(len(points) - 2, int(t * (len(points) - 1))) u = t * (len(points) - 1) - a p0, p1, p2, p3 = points[max(a - 1, 0)], points[a], points[a + 1], points[min(a + 2, len(points) - 1)] p = .5 * ((2 * p1) + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u * u + (-p0 + 3 * p1 - 3 * p2 + p3) * u * u * u) return p, radii[a] * (1 - u) + radii[a + 1] * u for j in range(steps + 1): t = j / steps p, r = point(t) tan = (point(min(1, t + .002))[0] - point(max(0, t - .002))[0]).normalized() u = tan.cross(Vector((0, 1, 0))).normalized() v = tan.cross(u) loop = [] for k in range(sides): a = 2 * pi * k / sides mod = 1 + groove * (sin(a * 6 + t * 5) + .35 * sin(a * 11 - t * 7)) vs.append(p + r * mod * (u * cos(a) + v * sin(a) * flatten)) uv.append((k / sides, t)) ws.append(mixweights(t, bones)) loop.append(len(vs) - 1) loops.append(loop) for a, b in zip(loops, loops[1:]): for k in range(sides): q = (k + 1) % sides fs.append((a[k], a[q], b[q], b[k])) fs.extend([tuple(reversed(loops[0])), tuple(loops[-1])]) return mesh(name, vs, fs, kind, bones[0], uv, weights=ws) def ell(name, c, r, kind, bone, segments=22, rings=12): vs = [(c[0], c[1], c[2] + r[2])] fs = [] for j in range(1, rings): th = pi * j / rings for k in range(segments): a = 2 * pi * k / segments f = 1 + .05 * sin(a * 4 + j * 1.3) vs.append((c[0] + r[0] * sin(th) * cos(a) * f, c[1] + r[1] * sin(th) * sin(a) * f, c[2] + r[2] * cos(th))) vs.append((c[0], c[1], c[2] - r[2])) end = len(vs) - 1 for k in range(segments): fs.append((0, 1 + k, 1 + (k + 1) % segments)) for j in range(rings - 2): for k in range(segments): a = 1 + j * segments + k b = 1 + j * segments + (k + 1) % segments fs.append((a, a + segments, b + segments, b)) for k in range(segments): fs.append((end, end - 1 - k, end - 1 - (k + 1) % segments)) return mesh(name, vs, fs, kind, bone) def plate(name, c, w, h, d, bone, tilt=0.0, cant=0.0, yaw=0.0, kind='basalt2'): """Chipped basalt lamina: irregular ten-sided outline and broad fracture facets. Three offset perimeter rings create a real bevel and broken corners. Each slab has a stable seeded outline, rather than a box with random shading. """ from mathutils import Euler rng = random.Random(name) rot = Euler((tilt, cant, yaw)).to_matrix() n = 10 outline = [] for k in range(n): a = 2 * pi * k / n + .08 f = rng.uniform(.88, 1.08) outline.append((cos(a) * w * .5 * f, sin(a) * h * .5 * f)) vs, fs, colors = [], [], [] base = M[kind].diffuse_color[:3] for ring, (scale, y) in enumerate([(.80, d * .5), (1, d * .06), (.83, -d * .5)]): for k, (x, z) in enumerate(outline): p = rot @ Vector((x * scale, y + rng.uniform(-.03, .03) * d, z * scale)) + Vector(c) vs.append(p) f = [.68, .83, 1.04][ring] * rng.uniform(.93, 1.09) colors.append(tuple(v * f for v in base)) fs.append(tuple(reversed(range(n)))) for ring in range(2): for k in range(n): j = (k + 1) % n fs.append((ring * n + k, ring * n + j, (ring + 1) * n + j, (ring + 1) * n + k)) # Off-center ridge gives three large readable fracture planes on the face. p = rot @ Vector((w * .03, -d * .54, h * -.05)) + Vector(c) vs.append(p) colors.append(tuple(v * 1.06 for v in base)) for k in range(n): fs.append((2 * n + k, 2 * n + (k + 1) % n, 3 * n)) return mesh(name, vs, fs, kind, bone, colors=colors, smooth=False) def seam(name, points, bone, width=.007): """Narrow fissure in a recessed dark channel, never a surface rope.""" radii = [width * (.65 if k in (0, len(points) - 1) else 1) for k in range(len(points))] tube(name + '_recess', [(x, y + .003, z) for x, y, z in points], [r * 2.6 for r in radii], 'basalt', [bone], 6, 12, groove=0) return tube(name, points, radii, 'crack', [bone], 6, 12, groove=0) # ---- Compact, asymmetric guardian built from interlocking basalt strata ---- # Deep core volumes stay recessed under the overlapping hard plates. plate('Rib_Core', (0, .035, 1.09), .89, .72, .53, 'Spine', kind='basalt') plate('Pelvic_Core', (0, .015, .72), .70, .43, .45, 'Pelvis', kind='basalt') # Broad diagonal chest strata, with gaps instead of a rectangular breastplate. plate('Pectoral_L', (-.20, -.19, 1.26), .56, .38, .28, 'Chest', cant=-.18, yaw=-.12) plate('Pectoral_R', (.21, -.18, 1.22), .53, .40, .26, 'Chest', cant=.23, yaw=.13) plate('Sternum_Keystone', (-.01, -.22, 1.02), .31, .36, .26, 'Spine', cant=-.11) plate('Rib_Low_L', (-.22, -.17, .91), .38, .30, .20, 'Spine', cant=-.20, yaw=-.20, kind='basalt') plate('Rib_Low_R', (.23, -.17, .87), .39, .30, .22, 'Spine', cant=.26, yaw=.16) plate('Hip_Keystone', (0, -.13, .69), .54, .25, .30, 'Pelvis', cant=.10) for s, side in [(-1, 'L'), (1, 'R')]: for j in range(3): plate('Flank_%s%d' % (side, j), (s * (.34 - j * .025), .08, 1.14 - j * .18), .24, .27, .36, 'Chest' if j == 0 else 'Spine', cant=s * .25, yaw=s * .32, kind='basalt') seam('Sternum_Fault', [(-.045, -.343, 1.39), (-.025, -.335, 1.27), (.016, -.326, 1.19), (-.016, -.36, 1.10)], 'Chest', .006) seam('Lower_Fault', [(.055, -.36, 1.11), (.071, -.365, 1.00), (.042, -.35, .90)], 'Spine', .006) seam('Chest_Fault_L', [(-.38, -.27, 1.35), (-.25, -.321, 1.30), (-.14, -.326, 1.31)], 'Chest', .005) seam('Chest_Fault_R', [(.14, -.309, 1.13), (.23, -.310, 1.17), (.38, -.28, 1.12)], 'Chest', .005) # The recessed neck and slanted mask make the head part of the stone mass. plate('Neck', (0, .045, 1.47), .35, .30, .33, 'Chest', kind='basalt') plate('Cranium', (0, .015, 1.73), .53, .51, .46, 'Head', cant=-.035, kind='basalt') plate('Temple_L', (-.185, -.13, 1.76), .20, .33, .18, 'Head', cant=-.13, yaw=-.22) plate('Temple_R', (.185, -.13, 1.76), .21, .34, .18, 'Head', cant=.17, yaw=.18) plate('Jaw', (0, -.175, 1.57), .35, .21, .22, 'Head', cant=.025) plate('Nose_Ridge', (0, -.225, 1.72), .075, .27, .13, 'Head', kind='basalt2') for s, side in [(-1, 'L'), (1, 'R')]: plate('Eye_Recess_' + side, (s * .115, -.208, 1.785), .174, .080, .08, 'Head', cant=s * .10, kind='basalt') plate('Eye_' + side, (s * .115, -.253, 1.786), .111, .023, .012, 'Head', cant=s * .11, kind='ember') plate('Brow_' + side, (s * .113, -.214, 1.849), .252, .10, .13, 'Head', cant=s * .14) plate('Cheek_' + side, (s * .118, -.211, 1.679), .18, .17, .13, 'Head', cant=-s * .30) # Broken geological crown: no symmetric animal horns. plate('Crown_Shard_L', (-.16, .08, 1.98), .21, .38, .28, 'Head', cant=-.23, tilt=.14) plate('Crown_Shard_R', (.13, .10, 1.92), .23, .24, .29, 'Head', cant=.28, tilt=.12, kind='basalt') plate('Crown_Shard_Back', (.02, .185, 1.87), .25, .31, .23, 'Head', cant=-.10, tilt=-.20) seam('Jaw_Fissure', [(.020, -.29, 11.63), (.001, -.298, 1.58), (.022, -.26, 1.53)], 'Head', .004) # Arms have dark narrow joints, layered mineral deltoids and hefty split fists. for s, side in [(-1, 'L'), (1, 'R')]: upper, fore, hand = ['UpperArm.' + side, 'Forearm.' + side, 'Hand.' + side] tube('UpperArm_Core_' + side, [(s * .39, .02, 1.30), (s * .57, -.02, 1.06)], [.145, .12], 'basalt', [upper], 10, 12, groove=.035) tube('Elbow_Sleeve_' + side, [(s * .55, -.012, 1.12), (s * .61, -.055, .98)], [.105, .10], 'basalt', [upper, fore], 10, 8, groove=.04) plate('UpperArm_Shell_' + side, (s * .52, -.01, 1.18), .31, .37, .30, upper, cant=s * .20, kind='basalt') large = 1.18 if side == 'L' else .92 plate('Shoulder_Cap_' + side, (s * .50, .005, 1.40), .50 * large, .34 * large, .48, upper, cant=s * .20) plate('Shoulder_Upper_Stratum_' + side, (s * .50, .035, 1.53), .35 * large, .17, .37, upper, cant=s * .22, kind='basalt') plate('Shoulder_Outer_Stratum_' + side, (s * .65, .015, 11.33), .27, .31, .33, upper, cant=-s * .20) plate('Forearm_Core_' + side, (s * .64, -.09, .91), .31, .36, .31, fore, cant=s * .13, kind='basalt') plate('Gauntlet_Front_' + side, (s * .665, -.20, .90), .32, .33, .22, fore, cant=s * .18) plate('Gauntlet_Outer_' + side, (s * .77, -.08, .89), .19, .30, .30, fore, cant=-s * .12, kind='basalt') plate('Fist_' + side, (s * .72, -.19, .665), .37, .29, .34, hand, cant=-s * .08) for k in range(3): plate('Knuckle_%s%d' % (side, k), (s * .72 + (k - 1) * .10, -.342, .69), .11, .12, .10, hand, cant=.08 * (k - 1), kind='basalt') plate('Thumb_' + side, (s * .58, -.19, .65), .13, .20, .20, hand, cant=s * .20) seam('Forearm_Fault_' + side, [(s * .65, -.308, 1.025), (s * .685, -.312, .92), (s * .65, -.306, .83)], fore, .0055) plate('Bastion_Shoulder_Shard', (-.64, .08, 1.61), .20, .35, .32, 'UpperArm.L', cant=-.24, tilt=.12) # Broad feet touch the floor; segmented leg armor does not bridge rotating joints. for s, side in [(-1, 'L'), (1, 'R')]: thigh, shin, foot = ['Thigh.' + side, 'Shin.' + side, 'Foot.' + side] plate('Thigh_' + side, (s * .225, .01, .56), .35, .39, .36, thigh, cant=s * .04, kind='basalt') plate('Thigh_Stratum_' + side, (s * .23, -.12, .62), .31, .22, .18, thigh, cant=s * .15) tube('Knee_Joint_' + side, [(s * .24, 0, .47), (s * .245, -.006, .36)], [.125, .12], 'basalt', [thigh, shin], 10, 8, groove=.025) plate('Shin_' + side, (s * .25, -.01, .255), .33, .36, .33, shin, cant=-s * .04) plate('Knee_' + side, (s * .24, -.15, .43), .27, .21, .18, shin, cant=s * .12, kind='basalt') plate('Foot_' + side, (s * .25, -.10, .086), .40, .135, .49, foot, kind='basalt2') for k in range(2): plate('Toe_%s%d' % (side, k), (s * .25 + (k - .5) * .15, -.285, .065), .18, .10, .17, foot, cant=.08 * (k - .5), kind='basalt') seam('Shin_Fault_' + side, [(s * .23, -.177, .36), (s * .26, -.186, .26), (s * .24, -.16, .17)], shin, .005) # Back reads as stacked columnar basalt; the vent glows between strata. for j in range(4): plate('Back_Stratum_%d' % j, ((-.03 if j % 2 else .02), .235, .86 + j * .86 + j * .16), .62 - j * .025, .24, .28, 'Spine' if j < 2 else 'Chest', tilt=.10, cant=.10 * (-1) ** j, kind='basalt') for s, side in [(-1, 'L'), (1, 'R')]: seam('Back_Vent_' + side, [(s * .08, .365, 1.02), (s * .11, .38, 1.17), (s * .07, .37, 1.32)], 'Chest', .006) # Cleanup and skinning. for ob in parts: bm = bmesh.new() bm.from_mesh(ob.data) bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=.0000005) bmesh.ops.triangulate(bm, faces=list(bm.faces)) bmesh.ops.dissolve_degenerate(bm, edges=list(bm.edges), dist=.0000001) 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 = 'BasaltRig' ed = rig.data.edit_bones ed.remove(ed[0]) defs = {} def bone(name, head, tail, parent=None): b = ed.new(name) b.head = coord(head) b.tail = coord(tail) if parent: b.parent = ed[parent] defs[name] = (head, tail, parent) bone('Root', (0, 0, 0), (0, 0, .15)) bone('Pelvis', (0, .02, .70), (0, .02, .95), 'Root') bone('Spine', (0, .02, .95), (0, .02, 1.25), 'Pelvis') bone('Chest', (0, .02, 1.25), (0, .04, 1.50), 'Spine') bone('Head', (0, .04, 1.50), (0, 0, 1.95), 'Chest') for s, side in [(-1, 'L'), (1, 'R')]: bone('UpperArm.' + side, (s * .38, .02, 1.30), (s * .58, -.02, 1.05), 'Chest') bone('Forearm.' + side, (s * .58, -.02, 1.05), (s * .68, -.12, .78), 'UpperArm.' + side) bone('Hand.' + side, (s * .68, -.12, .78), (s * .72, -.22, .65), 'Forearm.' + side) bone('Thigh.' + side, (s * .20, .02, .72), (s * .24, 0, .42), 'Pelvis') bone('Shin.' + side, (s * .24, 0, .42), (s * .25, -.02, .12), 'Thigh.' + side) bone('Foot.' + side, (s * .25, -.02, .12), (s * .25, -.22, .06), 'Shin.' + side) bpy.ops.object.mode_set(mode='OBJECT') for ob in parts: mod = ob.modifiers.new('Weighted basalt', 'ARMATURE') mod.object = rig ob.parent = rig clips = {'idle': (3.0, True), 'start': (.4, False), 'walk': (1.5, True), 'stop': (.4, False), 'attack_a': (1.3, False), 'attack_b': (1.3, False), 'hit': (.5, False), 'ability': (1.7, False), 'death': (2.0, False)} def curve(t, keys): for i in range(len(keys) - 1): a, x = keys[i] b, y = keys[i + 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 animate(name, t, dur): rotations = {} locs = {} def rot(b, x=0, y=0, z=0): rotations[b] = (x, y, z) if name == 'idle': q = sin(2 * pi * t / dur) rot('Spine', .01 * q) rot('Chest', -.015 * q, 0, .008 * q) rot('Head', .018 * q, 0, -.01 * q) for s, side in [(-1, 'L'), (1, 'R')]: rot('Forearm.' + side, .015 * q) 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) phase = t / 1.5 * 2 * pi roll = sin(phase) * fade rot('Pelvis', .012, 0, .03 * roll) rot('Spine', -.04 * fade, 0, -.04 * roll) rot('Chest', .015 * fade, 0, .02 * roll) rot('Head', .03 * fade, 0, -.015 * roll) for s, side in [(-1, 'L'), (1, 'R')]: q = sin(phase + (0 if s < 0 else pi)) lift = max(0, q) rot('Thigh.' + side, .30 * q * fade) rot('Shin.' + side, -.36 * lift * fade) rot('Foot.' + side, (-.30 * q + .36 * lift) * fade) rot('UpperArm.' + side, -.10 * q * fade, 0, s * .02 * fade) rot('Forearm.' + side, -.08 * fade) locs['Root'] = (0, 0, (-.01 + .01 * cos(phase * 2)) * fade) elif name == 'attack_a': # Left magma punch, contact at 0.70 q = curve(t / dur, [(0, 0), (.30, -.35), (.52, -.55), (.70, 1), (.80, .82), (1, 0)]) rot('Spine', -.10 * q, .08 * q, .06 * q) rot('Chest', -.08 * q, .06 * q, .10 * q) rot('Head', .10 * q, 0, -.06 * q) rot('UpperArm.L', -.95 * q, -.08 * q, .10 * q) rot('Forearm.L', -.30 * q) rot('Hand.L', .20 * q) rot('UpperArm.R', .15 * q) rot('Thigh.L', .05 * q) locs['Root'] = (0, -.04 * q, -.01 * max(0, q)) elif name == 'attack_b': # Right slam, contact at 0.70 q = curve(t / dur, [(0, 0), (.30, -.35), (.52, -.55), (.70, 1), (.80, .82), (1, 0)]) rot('Spine', -.10 * q, -.08 * q, -.06 * q) rot('Chest', -.08 * q, -.06 * q, -.10 * q) rot('Head', .10 * q, 0, .06 * q) rot('UpperArm.R', -.95 * q, .08 * q, -.10 * q) rot('Forearm.R', -.30 * q) rot('Hand.R', .20 * q) rot('UpperArm.L', .15 * q) rot('Thigh.R', .05 * q) locs['Root'] = (0, -.04 * q, -.01 * max(0, q)) elif name == 'hit': q = curve(t, [(0, 0), (.08, 1), (.22, .5), (.5, 0)]) rot('Spine', .10 * q, 0, -.06 * q) rot('Head', .12 * q, 0, .05 * q) elif name == 'ability': # Defensive stance: arms cross, magma vents flare. q = curve(t / dur, [(0, 0), (.35, 1), (1.1, .9), (1.7, 0)]) rot('Spine', .06 * q) rot('Chest', .04 * q) rot('UpperArm.L', -.45 * q, -.30 * q, .55 * q) rot('Forearm.L', -.70 * q) rot('UpperArm.R', -.45 * q, .30 * q, -.55 * q) rot('Forearm.R', -.70 * q) rot('Head', -.08 * q) locs['Root'] = (0, 0, -.03 * q) elif name == 'death': q = curve(t, [(0, 0), (.2, .08), (.7, .45), (1.25, 1), (1.55, .92), (2.0, 1)]) rot('Root', -.80 * q, 0, .10 * q) locs['Root'] = (0, -.12 * q, -.10 * q) rot('Pelvis', -.15 * q) rot('Spine', -.20 * q) rot('Head', -.16 * q) for s, side in [(-1, 'L'), (1, 'R')]: rot('UpperArm.' + side, -.25 * q, 0, s * .12 * q) rot('Forearm.' + side, -.35 * q) rot('Thigh.' + side, .20 * q) rot('Shin.' + side, -.32 * q) for pb in rig.pose.bones: pb.rotation_mode = 'QUATERNION' r = rotations.get(pb.name, (0, 0, 0)) 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() pb.rotation_quaternion = basis.inverted() @ q @ basis pb.location = pb.bone.matrix_local.to_3x3().inverted() @ (Vector(locs.get(pb.name, (0, 0, 0))) * S) for name, (dur, loop) in clips.items(): rig.animation_data_clear() action = bpy.data.actions.new(name) action.use_fake_user = True rig.animation_data_create() rig.animation_data.action = action for frame in range(round(dur * 30) + 1): animate(name, frame / 30, dur) for pb in rig.pose.bones: pb.keyframe_insert('rotation_quaternion', frame=frame) pb.keyframe_insert('location', frame=frame) action['loop'] = loop rig.animation_data_clear() rig.animation_data_create() rig.animation_data.action = bpy.data.actions['idle'] for pb in rig.pose.bones: pb.rotation_quaternion = Quaternion() pb.location = (0, 0, 0) bpy.context.scene.render.fps = 30 bpy.context.scene.frame_set(0) bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'basaltbastion.blend')) bpy.ops.file.make_paths_relative() bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'basaltbastion.blend')) bpy.ops.object.select_all(action='DESELECT') for ob in parts: ob.select_set(True) bpy.context.view_layer.objects.active = parts[0] bpy.ops.object.join() body = bpy.context.object body.name = 'BasaltBody' body.select_set(True) rig.select_set(True) bpy.context.view_layer.objects.active = rig bpy.ops.export_scene.gltf( filepath=str(OUT / 'basaltbastion.glb'), 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 = (OUT / 'basaltbastion.glb').read_bytes() import struct doc = json.loads(raw[20:20 + struct.unpack_from('