"""Build three original IWT2-inspired, rigged low-poly boss assets.""" from __future__ import annotations import json import math from pathlib import Path import bpy from mathutils import Vector ROOT = Path(__file__).resolve().parents[2] OUT_ROOT = ROOT / "game_assets/models/original/bosses" FPS = 30 PARTS: list[bpy.types.Object] = [] RUNTIME_MATERIALS: list[bpy.types.Material] = [] def reset_scene() -> None: global PARTS, RUNTIME_MATERIALS PARTS = [] RUNTIME_MATERIALS = [] if bpy.context.object and bpy.context.object.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.select_all(action="SELECT") bpy.ops.object.delete(use_global=False) for blocks in (bpy.data.meshes, bpy.data.armatures, bpy.data.materials, bpy.data.cameras, bpy.data.lights, bpy.data.actions): for block in list(blocks): blocks.remove(block) def material(name, color, metallic=0.0, roughness=0.5, emission=None, strength=0.0): mat = bpy.data.materials.new(name) mat.use_nodes = True mat.diffuse_color = color bsdf = mat.node_tree.nodes.get("Principled BSDF") bsdf.inputs["Base Color"].default_value = color bsdf.inputs["Metallic"].default_value = metallic bsdf.inputs["Roughness"].default_value = roughness if emission: bsdf.inputs["Emission Color"].default_value = emission bsdf.inputs["Emission Strength"].default_value = strength return mat def prepare_materials(specs): mats = {name: material(f"M_{name}", **settings) for name, settings in specs.items()} RUNTIME_MATERIALS.extend(mats.values()) return mats def finish(obj, name, mat, bone, smooth=True): obj.name = name obj.data.name = f"{name}_Mesh" for runtime_mat in RUNTIME_MATERIALS: obj.data.materials.append(runtime_mat) mat_index = next(index for index, candidate in enumerate(RUNTIME_MATERIALS) if candidate.name == mat.name) for polygon in obj.data.polygons: polygon.material_index = mat_index polygon.use_smooth = smooth bone_group = obj.vertex_groups.new(name=bone) bone_group.add(range(len(obj.data.vertices)), 1.0, "REPLACE") mat_group = obj.vertex_groups.new(name=f"__MAT_{mat_index}") mat_group.add(range(len(obj.data.vertices)), 1.0, "REPLACE") bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) PARTS.append(obj) return obj def ellipsoid(name, location, scale, mat, bone, subdivisions=1, rotation=(0, 0, 0)): bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions, radius=1, location=location, rotation=rotation) obj = bpy.context.object obj.scale = scale return finish(obj, name, mat, bone) def cone(name, start, end, radius_start, radius_end, mat, bone, vertices=7): start_v, end_v = Vector(start), Vector(end) direction = end_v - start_v bpy.ops.mesh.primitive_cone_add( vertices=vertices, radius1=radius_start, radius2=radius_end, depth=direction.length, location=(start_v + end_v) * 0.5, ) obj = bpy.context.object obj.rotation_mode = "QUATERNION" obj.rotation_quaternion = direction.to_track_quat("Z", "Y") obj.rotation_mode = "XYZ" bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) return finish(obj, name, mat, bone, smooth=False) def plate(name, location, scale, rotation, mat, bone, vertices=6): bpy.ops.mesh.primitive_cone_add(vertices=vertices, radius1=1, radius2=0.58, depth=0.42, location=location, rotation=rotation) obj = bpy.context.object obj.scale = scale return finish(obj, name, mat, bone, smooth=False) def armature(name, specs): data = bpy.data.armatures.new(f"{name}_Rig") rig = bpy.data.objects.new(f"{name}_Rig", data) bpy.context.collection.objects.link(rig) bpy.context.view_layer.objects.active = rig rig.select_set(True) bpy.ops.object.mode_set(mode="EDIT") for bone_name, head, tail, parent in specs: bone = data.edit_bones.new(bone_name) bone.head, bone.tail = head, tail if parent: bone.parent = data.edit_bones[parent] bpy.ops.object.mode_set(mode="POSE") for bone in rig.pose.bones: bone.rotation_mode = "XYZ" bpy.ops.object.mode_set(mode="OBJECT") return rig def join_parts(name, rig): bpy.ops.object.select_all(action="DESELECT") for obj in PARTS: obj.select_set(True) bpy.context.view_layer.objects.active = PARTS[0] bpy.ops.object.join() body = bpy.context.object body.name = f"{name}_Body" body.data.name = f"{name}_Body_Mesh" material_groups = {g.index: int(g.name.removeprefix("__MAT_")) for g in body.vertex_groups if g.name.startswith("__MAT_")} face_materials = [] for polygon in body.data.polygons: vertex = body.data.vertices[polygon.vertices[0]] face_materials.append(next(material_groups[a.group] for a in vertex.groups if a.group in material_groups)) body.data.materials.clear() for mat in RUNTIME_MATERIALS: body.data.materials.append(mat) for polygon, index in zip(body.data.polygons, face_materials, strict=True): polygon.material_index = index for group in [g for g in body.vertex_groups if g.name.startswith("__MAT_")]: body.vertex_groups.remove(group) modifier = body.modifiers.new(f"{name}_Armature", "ARMATURE") modifier.object = rig body.parent = rig return body def reset_pose(rig): for bone in rig.pose.bones: bone.location = (0, 0, 0) bone.rotation_euler = (0, 0, 0) bone.scale = (1, 1, 1) def key_pose(rig, frame, rotations=None, locations=None, scales=None): reset_pose(rig) for name, value in (rotations or {}).items(): rig.pose.bones[name].rotation_euler = tuple(math.radians(component) for component in value) for name, value in (locations or {}).items(): rig.pose.bones[name].location = value for name, value in (scales or {}).items(): rig.pose.bones[name].scale = value for bone in rig.pose.bones: bone.keyframe_insert("location", frame=frame, group=bone.name) bone.keyframe_insert("rotation_euler", frame=frame, group=bone.name) bone.keyframe_insert("scale", frame=frame, group=bone.name) def actions(rig, definitions): rig.animation_data_create() metadata = {} for name, end_frame, loop, poses in definitions: action = bpy.data.actions.new(name) action.use_fake_user = True action.use_frame_range = True action.frame_start, action.frame_end, action.use_cyclic = 1, end_frame, loop rig.animation_data.action = action for pose in poses: key_pose(rig, **pose) rig.animation_data.action = None metadata[name] = {"frames": end_frame, "seconds": round(end_frame / FPS, 3), "loop": loop} return metadata def select_only(objects): bpy.ops.object.select_all(action="DESELECT") for obj in objects: obj.hide_set(False) obj.select_set(True) bpy.context.view_layer.objects.active = objects[0] def collision(asset_id, specs, out_dir): objects = [] for name, location, scale in specs: bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=1, location=location) obj = bpy.context.object obj.name = f"COLLISION_{name}" obj.scale = scale bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) objects.append(obj) select_only(objects) bpy.ops.export_scene.gltf( filepath=str(out_dir / f"{asset_id}_collision.glb"), export_format="GLB", use_selection=True, export_animations=False, export_skins=False, export_materials="NONE", export_apply=True, export_yup=True, ) for obj in objects: obj.hide_render = True obj.hide_set(True) return objects def look_at(obj, target): obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler() def studio(): scene = bpy.context.scene scene.render.engine = "BLENDER_EEVEE" scene.render.resolution_x = scene.render.resolution_y = 760 scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = "PNG" scene.render.fps = FPS scene.world.use_nodes = True scene.world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.005, 0.007, 0.012, 1) scene.world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.3 camera_data = bpy.data.cameras.new("PreviewCamera") camera = bpy.data.objects.new("PreviewCamera", camera_data) bpy.context.collection.objects.link(camera) scene.camera = camera for name, location, color, energy, size in [ ("Key", (5, -7, 7), (1, 0.34, 0.1), 1200, 5), ("Fill", (-5, -3, 4), (0.12, 0.32, 1), 800, 4), ("Rim", (1, 6, 6), (1, 0.12, 0.02), 1150, 3), ]: data = bpy.data.lights.new(name, "AREA") data.energy, data.color, data.shape, data.size = energy, color, "DISK", size obj = bpy.data.objects.new(name, data) obj.location = location bpy.context.collection.objects.link(obj) look_at(obj, (0, 0, 1.5)) floor_mat = material("M_PreviewFloor", color=(0.014, 0.018, 0.026, 1), roughness=0.85) bpy.ops.mesh.primitive_plane_add(size=28, location=(0, 0, 0)) floor = bpy.context.object floor.data.materials.append(floor_mat) return camera def render_previews(rig, camera, out_dir, asset_id, target, distance, special_clip, special_frame): preview_dir = out_dir / "previews" preview_dir.mkdir(parents=True, exist_ok=True) for filename, position, clip, frame in [ (f"{asset_id}_three_quarter.png", (distance * 0.62, -distance, distance * 0.48), "Idle", 15), (f"{asset_id}_side.png", (distance, 0, distance * 0.42), "Idle", 15), (f"{asset_id}_{special_clip.lower()}.png", (distance * 0.62, -distance, distance * 0.48), special_clip, special_frame), ]: rig.animation_data.action = bpy.data.actions[clip] bpy.context.scene.frame_set(frame) camera.location = position camera.data.lens = 58 look_at(camera, target) bpy.context.scene.render.filepath = str(preview_dir / filename) bpy.ops.render.render(write_still=True) rig.animation_data.action = None def export_asset(asset_id, display_name, rig, body, clips, collision_specs, preview_target, preview_distance, special_clip, special_frame): out_dir = OUT_ROOT / asset_id out_dir.mkdir(parents=True, exist_ok=True) select_only([rig, body]) bpy.ops.export_scene.gltf( filepath=str(out_dir / f"{asset_id}.glb"), export_format="GLB", use_selection=True, export_animations=True, export_animation_mode="ACTIONS", export_frame_range=True, export_skins=True, export_morph=False, export_yup=True, export_apply=False, export_cameras=False, export_lights=False, ) collision(asset_id, collision_specs, out_dir) triangles = sum(len(poly.vertices) - 2 for poly in body.data.polygons) metadata = { "name": display_name, "assetId": asset_id, "authoringTool": bpy.app.version_string, "format": "glTF 2.0 binary (GLB)", "runtime": { "renderMesh": body.name, "armature": rig.name, "collisionAsset": f"{asset_id}_collision.glb", "materials": [mat.name for mat in body.data.materials], "trianglesApprox": triangles, }, "animations": clips, } (out_dir / f"{asset_id}.asset.json").write_text(json.dumps(metadata, indent=2) + "\n") camera = studio() render_previews(rig, camera, out_dir, asset_id, preview_target, preview_distance, special_clip, special_frame) bpy.ops.wm.save_as_mainfile(filepath=str(out_dir / f"{asset_id}.blend")) print(f"BUILT={asset_id} TRIANGLES={triangles} CLIPS={','.join(clips)}") def build_ram(): reset_scene() mats = prepare_materials({ "Obsidian": {"color": (0.025, 0.028, 0.034, 1), "metallic": 0.28, "roughness": 0.4}, "Basalt": {"color": (0.10, 0.09, 0.09, 1), "metallic": 0.14, "roughness": 0.62}, "Armor": {"color": (0.18, 0.16, 0.16, 1), "metallic": 0.22, "roughness": 0.48}, "Lava": {"color": (1, 0.08, 0.003, 1), "roughness": 0.2, "emission": (1, 0.025, 0, 1), "strength": 9}, }) specs = [ ("Root", (0, 0, 0), (0, 0, 0.5), None), ("Body", (0, 0, 1.35), (0, 0, 2.5), "Root"), ("Head", (0, -0.75, 2.05), (0, -1.75, 2.05), "Body"), ("Horn.L", (-0.45, -1.45, 2.35), (-1.2, -1.5, 2.55), "Head"), ("Horn.R", (0.45, -1.45, 2.35), (1.2, -1.5, 2.55), "Head"), ("Leg.FL", (-0.7, -0.75, 1.45), (-0.78, -0.8, 0.35), "Body"), ("Leg.FR", (0.7, -0.75, 1.45), (0.78, -0.8, 0.35), "Body"), ("Leg.BL", (-0.7, 0.8, 1.35), (-0.78, 0.8, 0.3), "Body"), ("Leg.BR", (0.7, 0.8, 1.35), (0.78, 0.8, 0.3), "Body"), ("Tail", (0, 1.2, 1.75), (0, 2.25, 1.35), "Body"), ] rig = armature("ObsidianRam", specs) ellipsoid("BodyCore", (0, 0.1, 1.95), (1.18, 1.48, 0.86), mats["Basalt"], "Body", 2) ellipsoid("ShoulderMass", (0, -0.62, 2.08), (1.28, 0.82, 1.0), mats["Obsidian"], "Body", 2) ellipsoid("Head", (0, -1.48, 1.93), (0.72, 0.78, 0.62), mats["Armor"], "Head", 2) cone("Snout", (0, -1.7, 1.92), (0, -2.25, 1.72), 0.48, 0.22, mats["Obsidian"], "Head") ellipsoid("Eye.L", (-0.47, -1.9, 2.08), (0.075, 0.05, 0.075), mats["Lava"], "Head") ellipsoid("Eye.R", (0.47, -1.9, 2.08), (0.075, 0.05, 0.075), mats["Lava"], "Head") for side, suffix in [(-1, "L"), (1, "R")]: points = [(side * 0.48, -1.55, 2.33), (side * 1.05, -1.62, 2.67), (side * 1.45, -1.44, 2.35), (side * 1.43, -1.25, 1.93), (side * 1.02, -1.45, 1.78)] for index in range(len(points) - 1): cone(f"Horn{suffix}_{index}", points[index], points[index + 1], 0.19 - index * 0.025, 0.15 - index * 0.025, mats["Obsidian"], f"Horn.{suffix}", 8) for front, y, bone in [(True, -0.72, f"Leg.F{suffix}"), (False, 0.82, f"Leg.B{suffix}")]: x = side * 0.78 cone(f"Leg{suffix}_{'F' if front else 'B'}", (x, y, 1.45), (x * 1.05, y, 0.35), 0.34, 0.24, mats["Basalt"], bone, 7) ellipsoid(f"Hoof{suffix}_{'F' if front else 'B'}", (x * 1.06, y - 0.18, 0.25), (0.34, 0.46, 0.24), mats["Obsidian"], bone) cone("Tail", (0, 1.1, 1.72), (0, 2.22, 1.3), 0.38, 0.12, mats["Basalt"], "Tail", 7) for index, y in enumerate((-0.7, -0.15, 0.4, 0.95)): plate(f"BackPlate{index}", (0, y, 2.72 + index * 0.03), (0.92 - index * 0.08, 0.62, 0.32), (math.radians(82), 0, 0), mats["Armor"], "Body") cone(f"BackSpike{index}", (0, y, 2.86), (0, y + 0.05, 3.38 - index * 0.06), 0.17, 0, mats["Obsidian"], "Body", 6) for side in (-1, 1): cone(f"ShoulderSpike{side}", (side * 0.9, -0.55, 2.55), (side * 1.48, -0.5, 2.82), 0.18, 0, mats["Obsidian"], "Body", 6) for index, (start, end) in enumerate([((-0.12, -1.96, 2.22), (0.1, -1.98, 1.78)), ((-0.55, -0.98, 2.54), (-0.35, -0.99, 1.93)), ((0.48, 0.42, 2.55), (0.25, 0.45, 1.95))]): cone(f"LavaCrack{index}", start, end, 0.035, 0.02, mats["Lava"], "Head" if index == 0 else "Body", 5) body = join_parts("ObsidianRam", rig) clips = actions(rig, ram_actions()) export_asset("obsidian-ram-golem", "Obsidian Ram Golem", rig, body, clips, [("Body", (0, 0, 1.7), (1.4, 1.75, 1.35)), ("Head", (0, -1.55, 1.85), (1.45, 0.85, 0.8))], (0, 0, 1.6), 8.5, "Quake", 16) def ram_actions(): return [ ("Idle", 60, True, [{"frame": 1}, {"frame": 16, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail": (-4, 0, 0)}}, {"frame": 31}, {"frame": 46, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail": (-4, 0, 0)}}, {"frame": 60}]), ("Walk", 32, True, [{"frame": 1, "rotations": {"Leg.FL": (-18, 0, 0), "Leg.BR": (-18, 0, 0), "Leg.FR": (18, 0, 0), "Leg.BL": (18, 0, 0)}}, {"frame": 9, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 17, "rotations": {"Leg.FL": (18, 0, 0), "Leg.BR": (18, 0, 0), "Leg.FR": (-18, 0, 0), "Leg.BL": (-18, 0, 0)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 32, "rotations": {"Leg.FL": (-18, 0, 0), "Leg.BR": (-18, 0, 0), "Leg.FR": (18, 0, 0), "Leg.BL": (18, 0, 0)}}]), ("Charge", 30, False, [{"frame": 1}, {"frame": 8, "locations": {"Root": (0, 0.08, -0.12)}, "rotations": {"Head": (28, 0, 0), "Body": (8, 0, 0)}}, {"frame": 15, "locations": {"Root": (0, -0.18, 0.03)}, "rotations": {"Head": (18, 0, 0), "Leg.FL": (-24, 0, 0), "Leg.FR": (24, 0, 0)}}, {"frame": 23, "locations": {"Root": (0, -0.12, 0.08)}, "rotations": {"Head": (12, 0, 0), "Leg.FL": (24, 0, 0), "Leg.FR": (-24, 0, 0)}}, {"frame": 30}]), ("Quake", 36, False, [{"frame": 1}, {"frame": 12, "locations": {"Root": (0, 0.08, 0.18)}, "rotations": {"Body": (-18, 0, 0), "Head": (-20, 0, 0)}}, {"frame": 17, "locations": {"Root": (0, -0.08, -0.18)}, "rotations": {"Body": (24, 0, 0), "Head": (34, 0, 0)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.03)}, "rotations": {"Body": (-5, 0, 0)}}, {"frame": 36}]), ("ArmorShatter", 42, False, [{"frame": 1}, {"frame": 10, "scales": {"Body": (0.94, 0.94, 0.94)}, "rotations": {"Head": (-12, 0, 0)}}, {"frame": 17, "scales": {"Body": (1.08, 1.08, 1.08)}, "rotations": {"Body": (0, 0, 10), "Horn.L": (0, -12, 0), "Horn.R": (0, 12, 0)}}, {"frame": 23, "rotations": {"Body": (0, 0, -8)}}, {"frame": 31, "rotations": {"Body": (0, 0, 4)}}, {"frame": 42}]), ("Stagger", 26, False, [{"frame": 1}, {"frame": 5, "locations": {"Root": (0, 0.16, -0.08)}, "rotations": {"Body": (-15, 0, 12), "Head": (20, 0, -10)}}, {"frame": 13, "rotations": {"Body": (7, 0, -6)}}, {"frame": 26}]), ("Death", 70, False, [{"frame": 1}, {"frame": 18, "locations": {"Root": (0.15, 0.1, -0.35)}, "rotations": {"Root": (0, 35, 32), "Head": (26, 0, 10)}}, {"frame": 42, "locations": {"Root": (0.25, 0.1, -1.15)}, "rotations": {"Root": (0, 58, 82), "Head": (42, 0, 18), "Leg.FL": (30, 0, 0), "Leg.BL": (-24, 0, 0)}}, {"frame": 70, "locations": {"Root": (0.25, 0.1, -1.2)}, "rotations": {"Root": (0, 58, 82), "Head": (44, 0, 18), "Leg.FL": (30, 0, 0), "Leg.BL": (-24, 0, 0)}}]), ] def build_cinderback(): reset_scene() mats = prepare_materials({ "Charcoal": {"color": (0.035, 0.03, 0.03, 1), "metallic": 0.18, "roughness": 0.55}, "Plate": {"color": (0.16, 0.10, 0.075, 1), "metallic": 0.2, "roughness": 0.48}, "Underbody": {"color": (0.18, 0.018, 0.01, 1), "roughness": 0.66}, "Lava": {"color": (1, 0.07, 0.002, 1), "roughness": 0.18, "emission": (1, 0.02, 0, 1), "strength": 10}, "Claw": {"color": (0.55, 0.32, 0.15, 1), "roughness": 0.4}, }) specs = [ ("Root", (0, 0, 0), (0, 0, 0.45), None), ("Body", (0, 0, 1.0), (0, 0, 1.95), "Root"), ("Head", (0, -1.0, 1.25), (0, -1.9, 1.18), "Body"), ("Tail.1", (0, 1.05, 1.2), (0, 1.8, 1.05), "Body"), ("Tail.2", (0, 1.75, 1.05), (0, 2.45, 0.8), "Tail.1"), ("Leg.FL", (-0.65, -0.75, 0.95), (-0.82, -0.82, 0.25), "Body"), ("Leg.FR", (0.65, -0.75, 0.95), (0.82, -0.82, 0.25), "Body"), ("Leg.BL", (-0.65, 0.65, 0.95), (-0.82, 0.7, 0.25), "Body"), ("Leg.BR", (0.65, 0.65, 0.95), (0.82, 0.7, 0.25), "Body"), ] rig = armature("Cinderback", specs) ellipsoid("Underbody", (0, 0, 1.12), (1.05, 1.62, 0.65), mats["Underbody"], "Body", 2) ellipsoid("Head", (0, -1.42, 1.2), (0.68, 0.78, 0.54), mats["Plate"], "Head", 2) cone("Snout", (0, -1.58, 1.2), (0, -2.05, 1.05), 0.4, 0.18, mats["Charcoal"], "Head") ellipsoid("Eye.L", (-0.42, -1.72, 1.37), (0.07, 0.05, 0.07), mats["Lava"], "Head") ellipsoid("Eye.R", (0.42, -1.72, 1.37), (0.07, 0.05, 0.07), mats["Lava"], "Head") for index, y in enumerate((-1.02, -0.58, -0.12, 0.34, 0.78, 1.16)): width = 0.93 - abs(index - 2.5) * 0.055 plate(f"ShellPlate{index}", (0, y, 1.73 + math.sin(index / 5 * math.pi) * 0.18), (width, 0.53, 0.28), (math.radians(82), 0, 0), mats["Plate"], "Body") cone(f"ShellSpike{index}", (0, y, 1.9), (0, y, 2.35 + math.sin(index / 5 * math.pi) * 0.15), 0.13, 0, mats["Charcoal"], "Body", 6) for side, suffix in [(-1, "L"), (1, "R")]: for front, y, bone in [(True, -0.68, f"Leg.F{suffix}"), (False, 0.68, f"Leg.B{suffix}")]: x = side * 0.7 ellipsoid(f"LegMass{suffix}{front}", (x, y, 0.78), (0.38, 0.48, 0.42), mats["Charcoal"], bone) cone(f"Leg{suffix}{front}", (x, y, 0.72), (side * 0.92, y - 0.12, 0.22), 0.22, 0.14, mats["Underbody"], bone) for toe in (-0.12, 0, 0.12): cone(f"Toe{suffix}{front}{toe}", (side * 0.92 + toe, y - 0.22, 0.2), (side * 0.95 + toe, y - 0.58, 0.11), 0.05, 0, mats["Claw"], bone, 5) cone("Tail1", (0, 1.0, 1.12), (0, 1.85, 0.88), 0.42, 0.27, mats["Plate"], "Tail.1") cone("Tail2", (0, 1.78, 0.88), (0, 2.48, 0.68), 0.29, 0.06, mats["Charcoal"], "Tail.2") for index, (x, y, z) in enumerate([(-0.48, -0.82, 1.56), (0.45, -0.28, 1.68), (-0.42, 0.28, 1.72), (0.38, 0.8, 1.55)]): cone(f"LavaCrack{index}", (x, y, z), (x * 0.65, y + 0.18, z - 0.32), 0.035, 0.018, mats["Lava"], "Body", 5) body = join_parts("Cinderback", rig) clips = actions(rig, cinderback_actions()) export_asset("cinderback-ricochet", "Cinderback Ricochet", rig, body, clips, [("Body", (0, 0, 1.05), (1.25, 1.85, 0.95)), ("Roll", (0, 0, 1.2), (1.45, 1.45, 1.45))], (0, 0, 1.15), 8.3, "ArmorSlam", 15) def cinderback_actions(): return [ ("Idle", 60, True, [{"frame": 1}, {"frame": 16, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail.2": (-5, 0, 0)}}, {"frame": 31}, {"frame": 46, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail.2": (-5, 0, 0)}}, {"frame": 60}]), ("Walk", 32, True, [{"frame": 1, "rotations": {"Leg.FL": (-14, 0, 0), "Leg.BR": (-14, 0, 0), "Leg.FR": (14, 0, 0), "Leg.BL": (14, 0, 0)}}, {"frame": 9, "locations": {"Root": (0, 0, 0.04)}}, {"frame": 17, "rotations": {"Leg.FL": (14, 0, 0), "Leg.BR": (14, 0, 0), "Leg.FR": (-14, 0, 0), "Leg.BL": (-14, 0, 0)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.04)}}, {"frame": 32, "rotations": {"Leg.FL": (-14, 0, 0), "Leg.BR": (-14, 0, 0), "Leg.FR": (14, 0, 0), "Leg.BL": (14, 0, 0)}}]), ("Curl", 28, False, [{"frame": 1}, {"frame": 9, "locations": {"Root": (0, 0, 0.12)}, "rotations": {"Head": (-42, 0, 0), "Tail.1": (50, 0, 0), "Tail.2": (65, 0, 0), "Leg.FL": (45, 0, 0), "Leg.FR": (45, 0, 0), "Leg.BL": (-45, 0, 0), "Leg.BR": (-45, 0, 0)}}, {"frame": 17, "scales": {"Body": (1.02, 1.02, 1.02)}, "rotations": {"Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0), "Leg.FL": (58, 0, 0), "Leg.FR": (58, 0, 0), "Leg.BL": (-58, 0, 0), "Leg.BR": (-58, 0, 0)}}, {"frame": 28, "rotations": {"Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0), "Leg.FL": (58, 0, 0), "Leg.FR": (58, 0, 0), "Leg.BL": (-58, 0, 0), "Leg.BR": (-58, 0, 0)}}]), ("Ricochet", 30, True, [{"frame": 1, "rotations": {"Root": (0, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 8, "rotations": {"Root": (90, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 16, "rotations": {"Root": (180, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 23, "rotations": {"Root": (270, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 30, "rotations": {"Root": (360, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}]), ("ArmorSlam", 38, False, [{"frame": 1}, {"frame": 11, "locations": {"Root": (0, 0.1, 0.22)}, "rotations": {"Body": (-22, 0, 0), "Head": (-18, 0, 0)}}, {"frame": 16, "locations": {"Root": (0, -0.08, -0.16)}, "rotations": {"Body": (28, 0, 0), "Head": (34, 0, 0)}}, {"frame": 24, "rotations": {"Body": (-8, 0, 0)}}, {"frame": 38}]), ("Recover", 24, False, [{"frame": 1, "locations": {"Root": (0, 0, -0.12)}, "rotations": {"Body": (18, 0, 0)}}, {"frame": 8, "rotations": {"Body": (-5, 0, 0)}}, {"frame": 16, "rotations": {"Body": (2, 0, 0)}}, {"frame": 24}]), ("Stagger", 26, False, [{"frame": 1}, {"frame": 5, "locations": {"Root": (0, 0.14, -0.08)}, "rotations": {"Body": (-12, 0, 11), "Head": (22, 0, -10)}}, {"frame": 13, "rotations": {"Body": (6, 0, -5)}}, {"frame": 26}]), ("Death", 66, False, [{"frame": 1}, {"frame": 18, "locations": {"Root": (0.1, 0.1, -0.25)}, "rotations": {"Root": (0, 28, 35), "Head": (24, 0, 0)}}, {"frame": 42, "locations": {"Root": (0.18, 0.1, -0.8)}, "rotations": {"Root": (0, 50, 78), "Head": (38, 0, 0), "Tail.1": (-25, 0, 0)}}, {"frame": 66, "locations": {"Root": (0.18, 0.1, -0.84)}, "rotations": {"Root": (0, 50, 78), "Head": (40, 0, 0), "Tail.1": (-28, 0, 0)}}]), ] def build_scorpion(): reset_scene() mats = prepare_materials({ "Obsidian": {"color": (0.025, 0.025, 0.03, 1), "metallic": 0.2, "roughness": 0.5}, "Ochre": {"color": (0.42, 0.19, 0.045, 1), "metallic": 0.16, "roughness": 0.44}, "Gold": {"color": (0.85, 0.38, 0.035, 1), "metallic": 0.3, "roughness": 0.28}, "Glass": {"color": (1, 0.38, 0.02, 1), "roughness": 0.12, "emission": (1, 0.16, 0.01, 1), "strength": 4}, "Bone": {"color": (0.58, 0.38, 0.17, 1), "roughness": 0.38}, }) specs = [ ("Root", (0, 0, 0), (0, 0, 0.4), None), ("Body", (0, 0, 0.85), (0, 0, 1.55), "Root"), ("Head", (0, -0.65, 1.0), (0, -1.35, 0.92), "Body"), ("Claw.L", (-0.55, -0.8, 1.0), (-1.45, -1.6, 0.9), "Body"), ("Claw.R", (0.55, -0.8, 1.0), (1.45, -1.6, 0.9), "Body"), ("LegFront.L", (-0.5, -0.25, 0.8), (-1.4, -0.55, 0.25), "Body"), ("LegFront.R", (0.5, -0.25, 0.8), (1.4, -0.55, 0.25), "Body"), ("LegBack.L", (-0.5, 0.45, 0.8), (-1.4, 0.75, 0.25), "Body"), ("LegBack.R", (0.5, 0.45, 0.8), (1.4, 0.75, 0.25), "Body"), ("Tail.1", (0, 0.65, 1.0), (0, 1.35, 1.35), "Body"), ("Tail.2", (0, 1.3, 1.35), (0, 1.75, 2.0), "Tail.1"), ("Tail.3", (0, 1.7, 2.0), (0, 1.5, 2.68), "Tail.2"), ("Tail.4", (0, 1.5, 2.68), (0, 0.82, 3.0), "Tail.3"), ("Stinger", (0, 0.82, 3.0), (0, 0.18, 2.52), "Tail.4"), ] rig = armature("SandglassScorpion", specs) ellipsoid("Abdomen", (0, 0.28, 1.0), (0.92, 1.12, 0.54), mats["Obsidian"], "Body", 2) ellipsoid("Torso", (0, -0.55, 0.98), (0.76, 0.7, 0.48), mats["Ochre"], "Body", 2) ellipsoid("Head", (0, -1.12, 0.9), (0.52, 0.48, 0.36), mats["Obsidian"], "Head", 1) ellipsoid("Hourglass", (0, 0.15, 1.48), (0.42, 0.55, 0.24), mats["Glass"], "Body", 2) ellipsoid("HourglassWaist", (0, 0.15, 1.5), (0.44, 0.13, 0.26), mats["Obsidian"], "Body", 1) for side, suffix in [(-1, "L"), (1, "R")]: ellipsoid(f"Eye{suffix}", (side * 0.31, -1.42, 1.02), (0.06, 0.04, 0.06), mats["Glass"], "Head") cone(f"ClawArm{suffix}", (side * 0.45, -0.65, 1.0), (side * 1.25, -1.3, 0.9), 0.22, 0.16, mats["Ochre"], f"Claw.{suffix}") ellipsoid(f"ClawBody{suffix}", (side * 1.55, -1.55, 0.9), (0.62, 0.75, 0.42), mats["Gold"], f"Claw.{suffix}", 1, rotation=(0, 0, math.radians(side * 8))) ellipsoid(f"ClawGlass{suffix}", (side * 1.55, -1.68, 0.95), (0.37, 0.42, 0.22), mats["Glass"], f"Claw.{suffix}") cone(f"ClawTipOuter{suffix}", (side * 1.62, -1.98, 0.92), (side * 1.9, -2.45, 0.8), 0.2, 0, mats["Bone"], f"Claw.{suffix}") cone(f"ClawTipInner{suffix}", (side * 1.42, -1.98, 0.88), (side * 1.25, -2.32, 0.76), 0.14, 0, mats["Bone"], f"Claw.{suffix}") for index, (y, z) in enumerate([(-0.65, 0.7), (-0.18, 0.62), (0.32, 0.62), (0.78, 0.68)]): bone = f"LegFront.{suffix}" if index < 2 else f"LegBack.{suffix}" start = (side * 0.52, y, z + 0.18) joint = (side * (1.08 + index * 0.08), y + (index - 1.5) * 0.12, z) end = (side * (1.45 + index * 0.09), y + (index - 1.5) * 0.24, 0.16) cone(f"Leg{suffix}{index}A", start, joint, 0.14, 0.1, mats["Ochre"], bone, 6) cone(f"Leg{suffix}{index}B", joint, end, 0.1, 0.035, mats["Obsidian"], bone, 6) tail_points = [(0, 0.62, 1.02), (0, 1.35, 1.36), (0, 1.75, 2.0), (0, 1.5, 2.68), (0, 0.82, 3.0), (0, 0.18, 2.52)] tail_bones = ["Tail.1", "Tail.2", "Tail.3", "Tail.4", "Stinger"] for index, bone in enumerate(tail_bones): cone(f"TailSegment{index}", tail_points[index], tail_points[index + 1], 0.32 - index * 0.045, 0.25 - index * 0.04, mats["Ochre"] if index < 4 else mats["Gold"], bone, 8) if index < 4: plate(f"TailPlate{index}", tail_points[index + 1], (0.38 - index * 0.03, 0.32, 0.2), (0, 0, 0), mats["Gold"], bone) cone("StingerTip", (0, 0.18, 2.52), (0, -0.15, 2.18), 0.18, 0, mats["Glass"], "Stinger", 7) body = join_parts("SandglassScorpion", rig) clips = actions(rig, scorpion_actions()) export_asset("sandglass-scorpion", "Sandglass Scorpion", rig, body, clips, [("Body", (0, -0.1, 0.85), (1.15, 1.45, 0.8)), ("Claws", (0, -1.55, 0.9), (2.15, 1.0, 0.65)), ("Tail", (0, 1.0, 2.0), (0.65, 1.4, 1.35))], (0, -0.1, 1.3), 9.5, "Eruption", 16) def scorpion_actions(): return [ ("Idle", 60, True, [{"frame": 1}, {"frame": 16, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Tail.3": (0, 0, 4), "Claw.L": (0, 0, -3), "Claw.R": (0, 0, 3)}}, {"frame": 31}, {"frame": 46, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Tail.3": (0, 0, -4), "Claw.L": (0, 0, 3), "Claw.R": (0, 0, -3)}}, {"frame": 60}]), ("Walk", 32, True, [{"frame": 1, "rotations": {"LegFront.L": (-16, 0, 5), "LegBack.R": (-16, 0, -5), "LegFront.R": (16, 0, -5), "LegBack.L": (16, 0, 5)}}, {"frame": 9, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 17, "rotations": {"LegFront.L": (16, 0, -5), "LegBack.R": (16, 0, 5), "LegFront.R": (-16, 0, 5), "LegBack.L": (-16, 0, -5)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 32, "rotations": {"LegFront.L": (-16, 0, 5), "LegBack.R": (-16, 0, -5), "LegFront.R": (16, 0, -5), "LegBack.L": (16, 0, 5)}}]), ("ClawAttack", 28, False, [{"frame": 1}, {"frame": 9, "rotations": {"Claw.L": (-10, 18, 30), "Claw.R": (-10, -18, -30)}}, {"frame": 14, "locations": {"Root": (0, -0.08, 0)}, "rotations": {"Claw.L": (16, -10, -38), "Claw.R": (16, 10, 38), "Head": (-8, 0, 0)}}, {"frame": 21, "rotations": {"Claw.L": (5, 0, -12), "Claw.R": (5, 0, 12)}}, {"frame": 28}]), ("Burrow", 34, False, [{"frame": 1}, {"frame": 10, "locations": {"Root": (0, 0, -0.15)}, "rotations": {"Claw.L": (35, 0, 0), "Claw.R": (35, 0, 0), "Tail.1": (-20, 0, 0)}}, {"frame": 20, "locations": {"Root": (0, 0, -0.62)}, "scales": {"Body": (0.92, 0.92, 0.92)}, "rotations": {"Claw.L": (48, 0, 0), "Claw.R": (48, 0, 0), "Tail.1": (-32, 0, 0)}}, {"frame": 34, "locations": {"Root": (0, 0, -0.72)}, "rotations": {"Claw.L": (48, 0, 0), "Claw.R": (48, 0, 0), "Tail.1": (-32, 0, 0)}}]), ("Eruption", 38, False, [{"frame": 1}, {"frame": 11, "rotations": {"Tail.2": (-18, 0, 0), "Tail.3": (-22, 0, 0), "Tail.4": (24, 0, 0), "Stinger": (18, 0, 0)}}, {"frame": 16, "locations": {"Root": (0, -0.05, 0.1)}, "rotations": {"Tail.2": (22, 0, 0), "Tail.3": (34, 0, 0), "Tail.4": (-42, 0, 0), "Stinger": (-38, 0, 0)}}, {"frame": 24, "rotations": {"Tail.3": (10, 0, 0), "Tail.4": (-12, 0, 0)}}, {"frame": 38}]), ("Hourglass", 44, False, [{"frame": 1}, {"frame": 12, "locations": {"Root": (0, 0, 0.1)}, "rotations": {"Body": (-10, 0, 0), "Claw.L": (0, 0, -24), "Claw.R": (0, 0, 24), "Tail.3": (-18, 0, 0)}}, {"frame": 20, "scales": {"Body": (1.08, 1.08, 1.08)}, "rotations": {"Claw.L": (0, 0, 38), "Claw.R": (0, 0, -38), "Tail.3": (22, 0, 0)}}, {"frame": 30, "scales": {"Body": (0.96, 0.96, 0.96)}, "rotations": {"Claw.L": (0, 0, -16), "Claw.R": (0, 0, 16)}}, {"frame": 44}]), ("Stagger", 26, False, [{"frame": 1}, {"frame": 5, "locations": {"Root": (0, 0.12, -0.08)}, "rotations": {"Body": (-12, 0, 10), "Claw.L": (-15, 0, -18), "Claw.R": (-15, 0, 18), "Tail.2": (18, 0, 0)}}, {"frame": 13, "rotations": {"Body": (6, 0, -5)}}, {"frame": 26}]), ("Death", 70, False, [{"frame": 1}, {"frame": 18, "locations": {"Root": (0.1, 0.1, -0.3)}, "rotations": {"Root": (0, 30, 38), "Tail.2": (-28, 0, 0), "Claw.L": (25, 0, -30), "Claw.R": (-10, 0, 30)}}, {"frame": 44, "locations": {"Root": (0.18, 0.1, -0.78)}, "rotations": {"Root": (0, 52, 82), "Tail.2": (-48, 0, 0), "Tail.3": (-32, 0, 0), "Claw.L": (38, 0, -42), "Claw.R": (12, 0, 38)}}, {"frame": 70, "locations": {"Root": (0.18, 0.1, -0.82)}, "rotations": {"Root": (0, 52, 82), "Tail.2": (-50, 0, 0), "Tail.3": (-34, 0, 0), "Claw.L": (40, 0, -44), "Claw.R": (14, 0, 40)}}]), ] def main(): bpy.context.preferences.filepaths.save_version = 0 OUT_ROOT.mkdir(parents=True, exist_ok=True) build_ram() build_cinderback() build_scorpion() if __name__ == "__main__": main()