"""The Schlundkrone, superboss of Champions of Luneraria (G2): an ancient crystal-bound titan of Luneraria that the old Lunarians chained in the Mondschlund. Design (the silhouette carries it, not the glow): * a hunched, broad basalt titan that walks on its knuckles: huge shoulder masses, very long forearms and short pillar legs - no other unit stands like this; * the "Krone": a broken crescent halo of crystal shards that floats behind its low head; * the "Schlund": its chest opens like a maw (two plates with moonstone teeth) around a glowing heart crystal; * crescent horns, a crystal cluster that grows from the left shoulder only, broken bronze shackles of the old Lunarians on both wrists; * lunar veins in the basalt glow in the colour of the team it fights for (violet in the grotto), so the same model reads as neutral boss and as lane reinforcement. The footprint stays compact (the simulation keeps attackers 1.2-1.5 m from its centre). Rig: rigid stone parts weighted 1.0 to 22 bones (Root, Hips, Spine, Chest, Neck, Head, Jaw, Crown, Maw.L/R, Shoulder/UpperArm/Forearm/Hand.L/R, Thigh/Shin/Foot.L/R). The bone names used by SkeletalFigure's overlays (Spine, Head, Forearm.L/R) exist. Clips at 30 fps: idle, start, walk, stop, attack_a, attack_b (contact at 0.90 s as SkeletalFigure expects), hit, ability, death, roar (the grotto boss wakes), victory (claim). Animations are presentation only; the rules decide every hit. Rest positions are written to UV (x, z) and UV2 (y) so the runtime hide shader does not swim on the skinned mesh. Model units are metres at view scale 1; the game shows it at 1.7. Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_schlundkrone.py Godot +z is the front (glTF +Z forward, Blender -Y), .L sits at +x. """ import sys, json, hashlib, struct from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import lune_bpy as L from lune_bpy import V, pi, sin, cos, sqrt import bpy, bmesh from mathutils import Matrix, Quaternion FPS = 30 CONTACT = .90 STRIDE = 2.0 VIEW_SCALE = 1.7 CLIPS = {'idle': (3.0, True), 'start': (.4, False), 'walk': (1.6, True), 'stop': (.4, False), 'attack_a': (1.3, False), 'attack_b': (1.3, False), 'hit': (.5, False), 'ability': (1.3, False), 'death': (2.2, False), 'roar': (2.0, False), 'victory': (2.0, False)} SIDES = (('L', 1), ('R', -1)) # Bones in Godot coordinates: head, tail, parent. BONES = {'Root': ((0, 0, 0), (0, .3, 0), None), 'Hips': ((0, .98, -.12), (0, 1.2, -.06), 'Root'), 'Spine': ((0, 1.2, -.06), (0, 1.4, .04), 'Hips'), 'Chest': ((0, 1.4, .04), (0, 1.84, .2), 'Spine'), 'Neck': ((0, 1.7, .3), (0, 1.72, .46), 'Chest'), 'Head': ((0, 1.72, .46), (0, 1.75, .78), 'Neck'), 'Jaw': ((0, 1.64, .52), (0, 1.58, .78), 'Head'), 'Crown': ((0, 2.1, -.16), (0, 2.42, -.16), 'Chest')} for _side, _sx in SIDES: BONES['Maw.' + _side] = ((_sx * .28, 1.5, .45), (_sx * .28, 1.22, .47), 'Chest') BONES['Shoulder.' + _side] = ((_sx * .18, 1.78, .08), (_sx * .6, 1.84, .06), 'Chest') BONES['UpperArm.' + _side] = ((_sx * .6, 1.84, .06), (_sx * .82, 1.22, .22), 'Shoulder.' + _side) BONES['Forearm.' + _side] = ((_sx * .82, 1.22, .22), (_sx * .84, .5, .42), 'UpperArm.' + _side) BONES['Hand.' + _side] = ((_sx * .84, .5, .42), (_sx * .84, .16, .5), 'Forearm.' + _side) BONES['Thigh.' + _side] = ((_sx * .3, .98, -.12), (_sx * .38, .52, .04), 'Hips') BONES['Shin.' + _side] = ((_sx * .38, .52, .04), (_sx * .38, .14, -.08), 'Thigh.' + _side) BONES['Foot.' + _side] = ((_sx * .38, .14, -.08), (_sx * .38, .06, .22), 'Shin.' + _side) parts = [] def add(ob, bone): vg = ob.vertex_groups.new(name=bone) vg.add(list(range(len(ob.data.vertices))), 1.0, 'REPLACE') parts.append(ob) return ob def orient(ob, pivot, direction): """Turns an object built upright at the origin (Godot +y) towards `direction` and moves it to `pivot` (Godot coordinates).""" d = L.G(*direction).normalized() rot = V((0, 0, 1)).rotation_difference(d).to_matrix().to_4x4() ob.data.transform(Matrix.Translation(L.G(*pivot)) @ rot) ob.data.update() return ob def rock(name, a, b, r, bone, mat='LN_Boss_Hide', seed=0.0, squash=1.0, subdiv=2, cuts=7, amp=.16): """Faceted stone segment between Godot points a and b.""" a, b = V(a), V(b) half = (b - a).length * .5 ob = L.cliff(name, (0, 0, 0), (r, half * 1.08, r * squash), mat, seed=seed, subdiv=subdiv, cuts=cuts, amp=amp) return add(orient(ob, tuple((a + b) * .5), tuple(b - a)), bone) def mass(name, center, radii, bone, mat='LN_Boss_Hide', seed=0.0, lean=(0.0, 0.0), yaw=0.0, subdiv=2, cuts=7, amp=.16): return add(L.cliff(name, center, radii, mat, seed=seed, yaw=yaw, lean=lean, subdiv=subdiv, cuts=cuts, amp=amp), bone) def spike(name, base, direction, radius, length, bone, mat='LN_Crystal_Boss', sides=6, seed=0): ob = L.crystal(name, (0, 0, 0), radius, length, length * .22, mat, sides=sides, seed=seed) return add(orient(ob, base, direction), bone) def claw(name, points, width, bone, mat='LN_Boss_Bone', steps=10): n = len(points) widths = [width * (1 - .85 * k / (n - 1)) for k in range(n)] return add(L.sweep(name, points, widths, [w * .85 for w in widths], mat, up_hint=(0, 1, 0), steps=steps, lens=.35), bone) # ================================================================ body def build_body(): # Pelvis, belly and the leaning chest block, the back hump. mass('Pelvis', (0, .98, -.1), (.42, .26, .32), 'Hips', seed=1.0) mass('Loin', (0, .84, .14), (.26, .18, .1), 'Hips', seed=2.0) mass('Belly', (0, 1.2, .02), (.38, .25, .3), 'Spine', seed=3.0) mass('ChestMass', (0, 1.6, .12), (.6, .36, .42), 'Chest', seed=4.0, lean=(.3, 0)) mass('Hump', (0, 1.88, -.16), (.5, .28, .36), 'Chest', seed=5.0, lean=(-.2, 0)) for side, sx in SIDES: mass('Flank.' + side, (sx * .3, 1.72, -.3), (.3, .26, .26), 'Chest', seed=6.0 + sx) # The Schlund: two chest plates with moonstone teeth that open around the heart. mass('MawPlate.' + side, (sx * .16, 1.36, .5), (.15, .21, .075), 'Maw.' + side, seed=8.0 + sx, subdiv=1, cuts=5) for k in range(4): y = 1.23 + k * .085 claw('Tooth.%s.%d' % (side, k), [(sx * .045, y, .55), (-sx * .015, y - .02, .57), (-sx * .05, y - .04, .565)], .05, 'Maw.' + side, steps=5) # The heart inside the Schlund and the dark hollow around it. mass('Hollow', (0, 1.37, .38), (.2, .24, .1), 'Chest', mat='LN_Boss_Hide', seed=9.0, subdiv=1, cuts=4) spike('Heart', (0, 1.26, .43), (0, 1, .12), .11, .26, 'Chest', seed=3) # Neck, skull, brow, eyes, crescent horns and the jaw with fangs. mass('NeckMass', (0, 1.72, .36), (.17, .14, .15), 'Neck', seed=10.0) mass('Skull', (0, 1.76, .62), (.19, .16, .22), 'Head', seed=11.0, lean=(.15, 0)) mass('Brow', (0, 1.83, .77), (.17, .05, .06), 'Head', mat='LN_Boss_Bone', seed=12.0, subdiv=1, cuts=4) for side, sx in SIDES: spike('Eye.' + side, (sx * .075, 1.775, .795), (sx * .3, .2, 1), .032, .05, 'Head', sides=5, seed=4 + sx) claw('Horn.' + side, [(sx * .13, 1.83, .58), (sx * .34, 2.02, .52), (sx * .43, 2.2, .66), (sx * .32, 2.22, .92)], .16, 'Head', steps=14) claw('Fang.' + side, [(sx * .08, 1.6, .74), (sx * .08, 1.69, .77), (sx * .07, 1.74, .78)], .05, 'Jaw', steps=5) mass('JawMass', (0, 1.62, .66), (.15, .06, .16), 'Jaw', seed=13.0, subdiv=1, cuts=5) # Shoulders: huge pauldron masses; crystals grow from the left one only (and a few from # the right), so the silhouette is asymmetric and unmistakable. for side, sx in SIDES: mass('Pauldron.' + side, (sx * .64, 1.93, .02), (.37, .29, .37), 'Shoulder.' + side, seed=14.0 + sx, lean=(0, -sx * .25)) mass('PauldronRim.' + side, (sx * .8, 1.78, .08), (.2, .16, .3), 'Shoulder.' + side, mat='LN_Boss_Bone', seed=15.0 + sx, subdiv=1, cuts=5) for k, (base, direction, length) in enumerate([((.7, 2.1, .0), (.75, .75, -.15), .78), ((.86, 1.98, .12), (1, .35, .15), .5), ((.62, 2.08, -.2), (.55, .6, -.6), .55)]): spike('ShoulderCrystal.L.%d' % k, base, direction, .09 - .012 * k, length, 'Shoulder.L', seed=20 + k) spike('ShoulderCrystal.R.0', (-.74, 2.06, -.06), (-.8, .7, -.2), .06, .34, 'Shoulder.R', seed=30) # Arms: upper arm, the massive forearm with crystal spurs and a bronze shackle, the fist # with moonstone claws that the titan walks on. for side, sx in SIDES: sh, el, wr, fi = [V(p) for p in (BONES['UpperArm.' + side][0], BONES['Forearm.' + side][0], BONES['Hand.' + side][0], BONES['Hand.' + side][1])] rock('UpperArm.' + side, sh + V((0, .06, 0)), el, .2, 'UpperArm.' + side, seed=50 + sx) mass('Elbow.' + side, tuple(el + V((sx * .04, 0, -.05))), (.17, .15, .17), 'Forearm.' + side, seed=52 + sx, subdiv=1, cuts=5) rock('Forearm.' + side, el, wr + V((0, -.02, 0)), .25, 'Forearm.' + side, seed=54 + sx, squash=.92) along = (wr - el).normalized() out = V((sx, 0, -.35)).normalized() for k in range(3): base = el + (wr - el) * (.25 + .25 * k) + out * .2 spike('ArmSpur.%s.%d' % (side, k), tuple(base), tuple((out * 1.0 - along * .6 + V((0, .3, 0))).normalized()), .05, .32 - .06 * k, 'Forearm.' + side, sides=5, seed=60 + k + (3 if sx < 0 else 0)) cuff = L.lathe('Shackle.' + side, [(.255, -.07), (.27, -.055), (.27, .055), (.255, .07), (.235, .07), (.235, -.07)], 10, 'LN_Boss_Bronze', cap_top=False, cap_bottom=False) add(orient(cuff, tuple(wr + (el - wr).normalized() * .14), tuple(wr - el)), 'Forearm.' + side) link = L.lathe('ShackleLink.' + side, [(.07, -.03), (.07, .03), (.045, .03), (.045, -.03)], 8, 'LN_Boss_Bronze', cap_top=False, cap_bottom=False) add(orient(link, tuple(wr + (el - wr).normalized() * .14 + V((sx * .3, -.1, -.05))), (1, .2, 0)), 'Forearm.' + side) mass('Fist.' + side, tuple(wr + V((0, -.17, .05))), (.2, .19, .22), 'Hand.' + side, seed=70 + sx) for k in range(3): x = sx * (.1 - .1 * k) claw('Claw.%s.%d' % (side, k), [tuple(fi + V((x, .02, .08))), tuple(fi + V((x * 1.1, -.08, .2))), tuple(fi + V((x * 1.1, -.15, .24)))], .075, 'Hand.' + side, steps=6) # Legs: short stone pillars, knee plates, broad feet with moonstone toes. for side, sx in SIDES: hip, knee, ankle, toe = [V(p) for p in (BONES['Thigh.' + side][0], BONES['Shin.' + side][0], BONES['Foot.' + side][0], BONES['Foot.' + side][1])] rock('Thigh.' + side, hip, knee, .22, 'Thigh.' + side, seed=80 + sx) mass('Knee.' + side, tuple(knee + V((sx * .02, .02, .12))), (.15, .13, .1), 'Thigh.' + side, mat='LN_Boss_Bone', seed=82 + sx, subdiv=1, cuts=5) rock('Shin.' + side, knee, ankle, .19, 'Shin.' + side, seed=84 + sx) mass('FootMass.' + side, (sx * .38, .1, .06), (.2, .1, .27), 'Foot.' + side, seed=86 + sx, subdiv=1, cuts=5) for k in range(3): x = sx * (.38 + (k - 1) * .11) claw('Toe.%s.%d' % (side, k), [(x, .08, .26), (x, .05, .37), (x, .01, .42)], .07, 'Foot.' + side, steps=5) # ================================================================ crown def build_crown(): """A broken crescent halo of crystal shards behind the head, bound by an old bronze band.""" center = V((0, 2.1, -.16)) radius = .66 band = [] for k in range(15): a = pi * (.1 + .8 * k / 14) band.append(center + V((cos(a) * (radius - .05), sin(a) * (radius - .05), 0))) add(L.sweep('CrownBand', band, [.07] * 5, [.05] * 5, 'LN_Boss_Bronze', up_hint=(0, 0, 1), steps=28), 'Crown') # One shard is missing (the broken crown), the others grow longer towards the apex. for k, a in enumerate([.13, .27, .42, .5, .58, .73]): ang = pi * a direction = V((cos(ang), sin(ang), -.08)) length = .3 + .26 * (1 - abs(a - .5) * 2.4) base = center + V((cos(ang) * radius, sin(ang) * radius, 0)) spike('CrownShard.%d' % k, tuple(base), tuple(direction), .07 + .025 * (1 - abs(a - .5) * 2), length, 'Crown', seed=90 + k) # ================================================================ rig def build_rig(): bpy.ops.object.armature_add(enter_editmode=True) rig = bpy.context.object rig.name = 'SchlundkroneRig' ed = rig.data.edit_bones ed.remove(ed[0]) for name, (head, tail, parent) in BONES.items(): b = ed.new(name) b.head, b.tail = L.G(*head), L.G(*tail) if parent: b.parent = ed[parent] bpy.ops.object.mode_set(mode='OBJECT') return rig def pose_rotation(pb, r): """Rotation r = (x, y, z) about the armature axes (Blender), turned into the bone's frame.""" 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 curve(t, keys): for (a, x), (b, y) in zip(keys, keys[1:]): if t <= b: u = max(0.0, (t - a) / (b - a)) u = u * u * (3 - 2 * u) return x * (1 - u) + y * u return keys[-1][1] # ================================================================ clips def animate(name, t, dur): """Returns rotations (armature axes: x pitch, y roll, z yaw), locations (Blender axes, z up, +y backwards) and scales per bone. Upward bones lean forward with +x; hanging limbs swing forward with -x; the left arm spreads outward with -y, the right arm with +y.""" rs, locs, scl = {}, {}, {} def rot(b, x=0.0, y=0.0, z=0.0): o = rs.get(b, (0, 0, 0)) rs[b] = (o[0] + x, o[1] + y, o[2] + z) def loc(b, x=0.0, y=0.0, z=0.0): o = locs.get(b, (0, 0, 0)) locs[b] = (o[0] + x, o[1] + y, o[2] + z) def spread(side, amount): rot('UpperArm.' + side, 0, -amount if side == 'L' else amount, 0) def maw(amount): rot('Maw.L', 0, 0, .95 * amount) rot('Maw.R', 0, 0, -.95 * amount) # The crown always hovers a little behind the head. hover = sin(2 * pi * t / 1.5) loc('Crown', 0, 0, .035 * hover) rot('Crown', 0, .03 * sin(2 * pi * t / 3.0), 0) if name == 'idle': b = sin(2 * pi * t / dur) rot('Chest', .03 * b) rot('Spine', -.012 * b) rot('Head', -.02 * b, 0, .14 * sin(2 * pi * t / dur + .6)) rot('Neck', 0, 0, .05 * sin(2 * pi * t / dur + .6)) rot('UpperArm.L', .035 * b) rot('UpperArm.R', .035 * sin(2 * pi * t / dur + 1.1)) maw(.05 + .05 * b) rot('Jaw', .04 + .04 * b) loc('Hips', 0, 0, -.012 * b) elif name in ('walk', 'start', 'stop'): fade = 1.0 if name == 'walk' else (sin(pi * .5 * t / dur) if name == 'start' else cos(pi * .5 * t / dur)) ph = 2 * pi * t / CLIPS['walk'][0] for side, sx in SIDES: q = ph + (0 if side == 'L' else pi) thigh = -.36 * sin(q) * fade knee = (.5 * max(0.0, cos(q)) + .05) * fade rot('Thigh.' + side, thigh) rot('Shin.' + side, knee) rot('Foot.' + side, -(thigh + knee) * .6) # Knuckle walk: the arm swings against its leg, the forearm folds on the swing. rot('UpperArm.' + side, .32 * sin(q) * fade) rot('Forearm.' + side, -.22 * max(0.0, -sin(q)) * fade) loc('Hips', 0, 0, -.05 * (1 - cos(2 * ph)) * .5 * fade) rot('Hips', 0, 0, .06 * sin(ph) * fade) rot('Spine', .04 * fade, .04 * sin(ph) * fade, 0) rot('Chest', 0, 0, -.06 * sin(ph) * fade) rot('Head', 0, 0, .05 * sin(ph) * fade) loc('Crown', 0, 0, .03 * sin(2 * ph - .8) * fade) rot('Crown', 0, .05 * sin(ph - .6) * fade, 0) elif name == 'attack_a': # Double fist smash: both arms rise overhead, then slam down at contact (0.90 s). arm = curve(t, [(0, 0), (.55, -2.3), (.8, -2.45), (.9, -.95), (1.05, -.95), (1.3, 0)]) fore = curve(t, [(0, 0), (.55, -.6), (.8, -.7), (.9, -.1), (1.05, -.1), (1.3, 0)]) chest = curve(t, [(0, 0), (.55, -.28), (.8, -.32), (.9, .42), (1.05, .42), (1.3, 0)]) crouch = curve(t, [(0, 0), (.55, .04), (.85, 0), (.9, -.11), (1.05, -.11), (1.3, 0)]) for side, sx in SIDES: rot('UpperArm.' + side, arm) rot('Forearm.' + side, fore) spread(side, -.12 * min(0.0, arm) / 2.3) rot('Thigh.' + side, -1.2 * crouch) rot('Shin.' + side, 2.0 * crouch) rot('Foot.' + side, -.8 * crouch) rot('Chest', chest) rot('Spine', chest * .45) rot('Head', -chest * .5) loc('Hips', 0, 0, crouch) maw(.35 * max(0.0, -chest) / .32) loc('Crown', 0, 0, .1 * max(0.0, -chest) / .32) elif name == 'attack_b': # Backhand sweep with the right arm: winds up across the body, strikes at 0.90 s. wind = curve(t, [(0, 0), (.6, 1), (.82, 1.05), (.9, -1), (1.05, -1.05), (1.3, 0)]) rot('Chest', 0, 0, .45 * wind) rot('Spine', 0, 0, .2 * wind) rot('Hips', 0, 0, .08 * wind) rot('Head', 0, 0, -.3 * wind) # Raised to shoulder height, the right arm swings across the body (yaw), then out. rot('UpperArm.R', -1.0 * min(1.0, abs(wind) * 1.6), 0, .9 * max(0.0, wind) - 1.0 * max(0.0, -wind)) rot('Forearm.R', -.55 * max(0.0, wind) - .15 * max(0.0, -wind)) rot('UpperArm.L', .2 * wind) rot('Chest', .1 * abs(wind)) elif name == 'hit': q = curve(t, [(0, 0), (.066667, 1), (.2, .4), (.5, 0)]) rot('Chest', -.18 * q) rot('Head', -.2 * q, 0, .1 * q) loc('Hips', 0, .05 * q, 0) loc('Crown', 0, .04 * q, .06 * q) rot('UpperArm.L', .15 * q) rot('UpperArm.R', .15 * q) maw(-.04 * q) elif name == 'ability': # "Mondschrei": the Schlund tears open, arms spread, the crown rises and flares. o = curve(t, [(0, 0), (.5, 1), (.9, 1.15), (1.1, 1), (1.3, 0)]) maw(.9 * o) spread('L', .7 * o) spread('R', .7 * o) rot('UpperArm.L', -.5 * o) rot('UpperArm.R', -.5 * o) rot('Chest', -.3 * o) rot('Head', -.4 * o) rot('Jaw', .5 * o) loc('Crown', 0, 0, .18 * o) scl['Crown'] = tuple([1 + .25 * o] * 3) loc('Hips', 0, 0, .05 * o) elif name == 'death': stagger = curve(t, [(0, 0), (.35, 1), (.6, .6), (1.0, 0)]) buckle = curve(t, [(0, 0), (.5, 0), (1.1, 1), (2.2, 1)]) fall = curve(t, [(0, 0), (.9, 0), (1.6, 1), (1.8, .95), (2.2, 1)]) crown = curve(t, [(0, 0), (.3, 0), (1.25, 1), (1.4, .96), (2.2, 1)]) rot('Chest', -.25 * stagger + .2 * fall) loc('Hips', 0, .1 * stagger, -.42 * buckle) for side, sx in SIDES: rot('Thigh.' + side, -.9 * buckle) rot('Shin.' + side, 1.5 * buckle) rot('Foot.' + side, -.6 * buckle) rot('UpperArm.' + side, -.6 * fall + .2 * stagger) spread(side, .25 * fall) rot('Root', 1.1 * fall) loc('Root', 0, 0, -.08 * fall) rot('Head', .3 * fall, 0, .3 * fall) maw(-.05 * fall) # The crown breaks away and drops behind the titan. loc('Crown', 0, .55 * crown, -1.55 * crown) rot('Crown', 1.1 * crown, .6 * crown, 0) elif name == 'roar': rear = curve(t, [(0, 0), (.6, 1), (1.4, 1), (1.6, -.3), (1.75, -.3), (2.0, 0)]) up = max(0.0, rear) rot('Spine', -.35 * up + .2 * max(0.0, -rear)) rot('Chest', -.25 * rear) for side, sx in SIDES: rot('UpperArm.' + side, -1.4 * up - .8 * max(0.0, -rear) / .3) spread(side, .6 * up) rot('Forearm.' + side, -.4 * up) rot('Head', -.5 * up) rot('Jaw', .6 * up) maw(.8 * up) loc('Crown', 0, 0, .2 * up) scl['Crown'] = tuple([1 + .3 * up] * 3) loc('Hips', 0, 0, .08 * up) elif name == 'victory': def pulse(center): return max(0.0, 1 - abs(t - center) / .22) left = pulse(.5) + pulse(1.3) right = pulse(.9) for side, beat in (('L', left), ('R', right)): # Fist to the chest: raise forward, fold the forearm, turn inwards (yaw). rot('UpperArm.' + side, -1.15 * beat, 0, (-.55 if side == 'L' else .55) * beat) rot('Forearm.' + side, -1.3 * beat) rot('Head', -.3 * curve(t, [(0, 0), (.3, 1), (1.7, 1), (2.0, 0)])) rot('Jaw', .4 * curve(t, [(0, 0), (.3, 1), (1.7, 1), (2.0, 0)])) loc('Crown', 0, 0, .1 * (left + right)) rot('Chest', -.12 * curve(t, [(0, 0), (.3, 1), (1.7, 1), (2.0, 0)])) return rs, locs, scl def bake(rig): 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 * FPS) + 1): rs, locs, scl = animate(name, frame / FPS, dur) for pb in rig.pose.bones: pb.rotation_mode = 'QUATERNION' pb.rotation_quaternion = 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)) pb.keyframe_insert('rotation_quaternion', frame=frame) pb.keyframe_insert('location', frame=frame) if pb.name == 'Crown': pb.keyframe_insert('scale', frame=frame) action['loop'] = loop action['contact_seconds'] = CONTACT if name in ('attack_a', 'attack_b') 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 = FPS bpy.context.scene.frame_set(0) def rest_uvs(ob): """UV = rest (x, z), UV2 = (rest y, 0) in Godot coordinates for the hide shader.""" me = ob.data while me.uv_layers: me.uv_layers.remove(me.uv_layers[0]) first = me.uv_layers.new(name='UVMap') second = me.uv_layers.new(name='UVRest') for loop in me.loops: g = L.to_godot(me.vertices[loop.vertex_index].co) first.data[loop.index].uv = (g[0], g[2]) second.data[loop.index].uv = (g[1], 0.0) def build(): L.reset() build_body() build_crown() rocks = [ob for ob in parts if not any(m.name.startswith(('LN_Crystal', 'LN_Boss_Bronze')) for m in ob.data.materials)] rest = [ob for ob in parts if ob not in rocks] L.finalize(rocks, sharp_angle=16.0, occluders=parts, reach=.45, ground=0.0, rays=20) L.finalize(rest, occluders=parts, reach=.45, ground=0.0, rays=20) for ob in parts: L.triangulate(ob) 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.view_layer.objects.active body.name = body.data.name = 'SchlundkroneBody' rest_uvs(body) rig = build_rig() mod = body.modifiers.new('Rig', 'ARMATURE') mod.object = rig body.parent = rig bake(rig) source_count = len(parts) bpy.ops.wm.save_as_mainfile(filepath=str(L.SRC / 'schlundkrone.blend')) bpy.ops.object.select_all(action='DESELECT') body.select_set(True) rig.select_set(True) bpy.context.view_layer.objects.active = rig path = L.OUT / 'schlundkrone.glb' 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, export_vertex_color='ACTIVE', export_texcoords=True, export_normals=True) raw = path.read_bytes() doc = json.loads(raw[20:20 + struct.unpack_from('