492 lines
32 KiB
Python
492 lines
32 KiB
Python
"""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()
|