Release v0.1.4 2026-07-12

This commit is contained in:
Warren H
2026-07-12 13:49:46 -04:00
parent b48b3a4f8f
commit bef71d391a
803 changed files with 3800 additions and 358322 deletions
@@ -0,0 +1,213 @@
"""Build Crystal Bat Matriarch as a rigged, animated, runtime-ready GLB.
Original 3D interpretation of IWT2's Crystal Bat Matriarch concept.
Run with:
/Applications/Blender.app/Contents/MacOS/Blender --background --factory-startup \
--python scripts/blender/build_crystal_bat_matriarch.py
"""
from __future__ import annotations
import json
import math
import sys
from pathlib import Path
import bpy
sys.path.insert(0, str(Path(__file__).parent))
from build_iwt2_boss_trio import ( # noqa: E402
OUT_ROOT,
actions,
armature,
cone,
ellipsoid,
export_asset,
finish,
join_parts,
plate,
prepare_materials,
reset_scene,
)
ASSET_ID = "crystal-bat-matriarch"
def wing_panel(name: str, side: int, material: bpy.types.Material, bone: str) -> bpy.types.Object:
"""Create a broad, faceted, low-poly wing membrane with a shallow volume."""
outline = [
(side * 0.55, 2.82),
(side * 1.04, 3.85),
(side * 2.08, 4.28),
(side * 3.76, 4.1),
(side * 3.42, 2.72),
(side * 2.55, 1.75),
(side * 1.52, 2.12),
]
front_y, back_y = -0.04, 0.18
vertices = [(x, front_y, z) for x, z in outline] + [(x, back_y, z) for x, z in outline]
count = len(outline)
faces = [tuple(range(count)), tuple(range(count, count * 2))[::-1]]
for index in range(count):
next_index = (index + 1) % count
faces.append((index, next_index, count + next_index, count + index))
mesh = bpy.data.meshes.new(f"{name}_Mesh")
mesh.from_pydata(vertices, [], faces)
mesh.validate()
mesh.update()
object = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(object)
return finish(object, name, material, bone, smooth=False)
def sonic_ring(name: str, location: tuple[float, float, float], radius: float, material: bpy.types.Material) -> bpy.types.Object:
bpy.ops.mesh.primitive_torus_add(
major_radius=radius,
minor_radius=0.028,
major_segments=12,
minor_segments=4,
location=location,
rotation=(math.radians(90), 0, 0),
)
return finish(bpy.context.object, name, material, "Head", smooth=False)
def build_crystal_bat() -> None:
reset_scene()
mats = prepare_materials({
"Fur": {"color": (0.095, 0.045, 0.16, 1), "metallic": 0.06, "roughness": 0.7},
"Membrane": {"color": (0.035, 0.022, 0.07, 1), "metallic": 0.12, "roughness": 0.42},
"Amethyst": {"color": (0.42, 0.10, 0.76, 1), "metallic": 0.32, "roughness": 0.22},
"Ice": {"color": (0.25, 0.86, 1, 1), "metallic": 0.22, "roughness": 0.18, "emission": (0.04, 0.52, 1, 1), "strength": 2.8},
"Mirror": {"color": (0.6, 0.86, 1, 1), "metallic": 0.85, "roughness": 0.12},
"Mouth": {"color": (0.26, 0.015, 0.08, 1), "roughness": 0.5},
"Claw": {"color": (0.58, 0.64, 0.82, 1), "metallic": 0.3, "roughness": 0.28},
})
specs = [
("Root", (0, 0, 0), (0, 0, 0.65), None),
("Body", (0, 0, 2.35), (0, 0, 3.12), "Root"),
("Head", (0, -0.54, 2.6), (0, -1.23, 2.74), "Body"),
("Wing.L", (-0.56, 0.04, 2.9), (-3.7, 0.2, 3.95), "Body"),
("Wing.R", (0.56, 0.04, 2.9), (3.7, 0.2, 3.95), "Body"),
("Arm.L", (-0.46, -0.38, 2.18), (-1.34, -1.58, 1.55), "Body"),
("Arm.R", (0.46, -0.38, 2.18), (1.34, -1.58, 1.55), "Body"),
("Tail.1", (0, 0.52, 2.2), (0, 1.32, 1.84), "Body"),
("Tail.2", (0, 1.28, 1.86), (0, 2.08, 2.22), "Tail.1"),
]
rig = armature("CrystalBatMatriarch", specs)
ellipsoid("CrystalBatMatriarch_Body", (0, 0.08, 2.34), (1.03, 0.76, 0.78), mats["Fur"], "Body", 2)
ellipsoid("ChestRuff", (0, -0.42, 2.44), (0.92, 0.52, 0.7), mats["Claw"], "Body", 2)
ellipsoid("NeckRuff", (0, -0.72, 2.62), (0.62, 0.36, 0.58), mats["Fur"], "Head", 1)
for index, y in enumerate((-0.04, 0.28, 0.58)):
plate(
f"BackCrystal{index}", (0, y, 3.05 + index * 0.02),
(0.64 - index * 0.07, 0.42, 0.34), (math.radians(86), 0, 0),
mats["Amethyst"], "Body",
)
cone(f"BackSpike{index}", (0, y, 3.22), (0, y + 0.04, 3.8 - index * 0.1), 0.15, 0, mats["Ice"], "Body", 5)
ellipsoid("CrystalBatMatriarch_Head", (0, -1.04, 2.7), (0.68, 0.68, 0.56), mats["Fur"], "Head", 2)
ellipsoid("Muzzle", (0, -1.58, 2.52), (0.5, 0.3, 0.28), mats["Mouth"], "Head", 1)
cone("OpenMouth", (0, -1.58, 2.52), (0, -2.07, 2.42), 0.3, 0.12, mats["Mouth"], "Head", 6)
for side, suffix in ((-1, "L"), (1, "R")):
ellipsoid(f"Eye{suffix}", (side * 0.34, -1.5, 2.84), (0.09, 0.055, 0.09), mats["Ice"], "Head", 1)
cone(f"Ear{suffix}", (side * 0.36, -0.98, 3.08), (side * 0.75, -0.84, 3.78), 0.19, 0, mats["Amethyst"], "Head", 5)
cone(f"CrownCrystal{suffix}", (side * 0.22, -1.02, 3.16), (side * 0.55, -1.08, 3.82), 0.16, 0, mats["Ice"], "Head", 5)
cone(f"Fang{suffix}", (side * 0.2, -1.77, 2.47), (side * 0.24, -1.88, 2.24), 0.055, 0, mats["Claw"], "Head", 5)
cone("CrownCrystalCenter", (0, -1.02, 3.2), (0, -1.16, 3.96), 0.19, 0, mats["Amethyst"], "Head", 5)
for index, radius in enumerate((0.34, 0.51, 0.68)):
sonic_ring(f"SonicRing{index}", (0, -2.05 - index * 0.12, 2.48), radius, mats["Ice"])
for side, suffix in ((-1, "L"), (1, "R")):
wing = f"Wing.{suffix}"
wing_panel(f"WingPanel{suffix}", side, mats["Membrane"], wing)
cone(f"WingArm{suffix}", (side * 0.5, 0.05, 2.92), (side * 1.35, 0.06, 3.88), 0.18, 0.1, mats["Fur"], wing, 6)
cone(f"WingFingerA{suffix}", (side * 1.15, 0.07, 3.78), (side * 3.62, 0.08, 4.08), 0.09, 0.025, mats["Amethyst"], wing, 5)
cone(f"WingFingerB{suffix}", (side * 1.1, 0.08, 3.52), (side * 3.35, 0.08, 2.72), 0.075, 0.018, mats["Amethyst"], wing, 5)
for index, (x, z, scale) in enumerate(((1.54, 4.26, 0.22), (2.48, 3.95, 0.18), (3.16, 3.15, 0.15))):
cone(f"WingCrystal{suffix}{index}", (side * x, -0.03, z), (side * (x + 0.2), -0.04, z + scale * 2.2), scale, 0, mats["Ice"], wing, 5)
arm = f"Arm.{suffix}"
cone(f"ArmUpper{suffix}", (side * 0.5, -0.38, 2.24), (side * 0.92, -1.04, 1.86), 0.18, 0.12, mats["Fur"], arm, 6)
cone(f"ArmLower{suffix}", (side * 0.9, -1.02, 1.86), (side * 1.28, -1.45, 1.48), 0.12, 0.075, mats["Fur"], arm, 6)
ellipsoid(f"Palm{suffix}", (side * 1.3, -1.46, 1.46), (0.18, 0.17, 0.16), mats["Amethyst"], arm, 1)
for finger in (-0.14, 0, 0.14):
cone(f"Claw{suffix}{finger}", (side * 1.3, -1.5, 1.45), (side * 1.55, -1.78, 1.14 + finger), 0.05, 0.007, mats["Claw"], arm, 5)
cone("TailSegment1", (0, 0.48, 2.18), (0, 1.3, 1.82), 0.32, 0.2, mats["Fur"], "Tail.1", 7)
cone("TailSegment2", (0, 1.27, 1.82), (0, 2.04, 2.15), 0.2, 0.08, mats["Fur"], "Tail.2", 7)
cone("TailCrystal", (0, 2.0, 2.14), (0, 2.46, 2.52), 0.16, 0, mats["Amethyst"], "Tail.2", 5)
for index, (x, y, z, rotation) in enumerate(((-1.55, -0.4, 3.52, -0.35), (1.52, -0.66, 3.18, 0.42), (-2.35, -0.3, 2.6, -0.5), (2.32, -0.5, 2.42, 0.38))):
ellipsoid(f"MirrorShard{index}", (x, y, z), (0.2, 0.06, 0.42), mats["Mirror"], "Body", 1, rotation=(0, rotation, 0.18 * index))
body = join_parts("CrystalBatMatriarch", rig)
clips = actions(rig, crystal_bat_actions())
export_asset(
ASSET_ID, "Crystal Bat Matriarch", rig, body, clips,
[
("Core", (0, 0.02, 2.38), (1.22, 0.92, 0.95)),
("Head", (0, -1.08, 2.68), (0.78, 0.78, 0.65)),
("Wings", (0, 0.05, 3.06), (3.82, 0.46, 1.45)),
],
(0, -0.02, 2.55), 11.5, "MirrorShatter", 22,
)
metadata_path = OUT_ROOT / ASSET_ID / f"{ASSET_ID}.asset.json"
metadata = json.loads(metadata_path.read_text())
metadata["sourceConcept"] = "Original 3D interpretation of IWT2 Crystal Bat Matriarch concept art"
metadata["runtime"]["forward"] = "-Y"
metadata["runtime"]["unit"] = "meters"
metadata_path.write_text(json.dumps(metadata, indent=2) + "\n")
def crystal_bat_actions():
return [
("Idle", 60, True, [
{"frame": 1},
{"frame": 14, "locations": {"Root": (0, 0, 0.14)}, "rotations": {"Body": (3, 0, 0), "Head": (2, 4, 0), "Wing.L": (8, 13, 4), "Wing.R": (8, -13, -4), "Tail.2": (0, 0, 7)}},
{"frame": 30, "locations": {"Root": (0, 0, 0.04)}, "rotations": {"Head": (0, -4, 0), "Wing.L": (2, 5, 0), "Wing.R": (2, -5, 0), "Tail.2": (0, 0, -7)}},
{"frame": 45, "locations": {"Root": (0, 0, 0.14)}, "rotations": {"Body": (-3, 0, 0), "Head": (2, -4, 0), "Wing.L": (8, 13, 4), "Wing.R": (8, -13, -4), "Tail.2": (0, 0, 7)}},
{"frame": 60},
]),
("Swoop", 36, True, [
{"frame": 1, "rotations": {"Wing.L": (14, 25, 6), "Wing.R": (14, -25, -6)}},
{"frame": 9, "locations": {"Root": (0, -0.06, 0.18)}, "rotations": {"Body": (-12, 0, 0), "Wing.L": (26, 46, 10), "Wing.R": (26, -46, -10)}},
{"frame": 18, "locations": {"Root": (0, -0.12, 0.04)}, "rotations": {"Body": (8, 0, 0), "Wing.L": (-8, 12, 0), "Wing.R": (-8, -12, 0)}},
{"frame": 27, "locations": {"Root": (0, -0.06, 0.18)}, "rotations": {"Body": (-12, 0, 0), "Wing.L": (26, 46, 10), "Wing.R": (26, -46, -10)}},
{"frame": 36, "rotations": {"Wing.L": (14, 25, 6), "Wing.R": (14, -25, -6)}},
]),
("SonicPulse", 38, False, [
{"frame": 1},
{"frame": 10, "locations": {"Root": (0, 0, 0.12)}, "rotations": {"Head": (-10, 0, 0), "Body": (-7, 0, 0), "Wing.L": (10, 32, 5), "Wing.R": (10, -32, -5)}},
{"frame": 17, "locations": {"Root": (0, -0.08, 0.26)}, "scales": {"Body": (1.1, 1.1, 1.1)}, "rotations": {"Head": (23, 0, 0), "Wing.L": (24, 62, 11), "Wing.R": (24, -62, -11), "Arm.L": (-22, 0, -16), "Arm.R": (-22, 0, 16)}},
{"frame": 25, "scales": {"Body": (0.97, 0.97, 0.97)}, "rotations": {"Head": (-4, 0, 0), "Wing.L": (5, 15, 0), "Wing.R": (5, -15, 0)}},
{"frame": 38},
]),
("MirrorShatter", 46, False, [
{"frame": 1},
{"frame": 12, "locations": {"Root": (0, 0, 0.12)}, "rotations": {"Body": (-10, 0, 0), "Wing.L": (-18, -12, 0), "Wing.R": (-18, 12, 0), "Tail.1": (-12, 0, 0)}},
{"frame": 22, "locations": {"Root": (0, -0.1, 0.25)}, "rotations": {"Body": (18, 0, 0), "Head": (18, 0, 0), "Wing.L": (32, 72, 14), "Wing.R": (32, -72, -14), "Tail.1": (18, 0, 0), "Tail.2": (-22, 0, 0)}},
{"frame": 31, "rotations": {"Wing.L": (8, 22, 0), "Wing.R": (8, -22, 0), "Tail.2": (8, 0, 0)}},
{"frame": 46},
]),
("Stagger", 28, False, [
{"frame": 1},
{"frame": 6, "locations": {"Root": (0.14, 0.1, -0.08)}, "rotations": {"Body": (-14, 0, 15), "Head": (21, 0, -12), "Wing.L": (22, 0, -14), "Wing.R": (22, 0, 14)}},
{"frame": 15, "rotations": {"Body": (7, 0, -7), "Head": (-8, 0, 5)}},
{"frame": 28},
]),
("Death", 70, False, [
{"frame": 1},
{"frame": 18, "locations": {"Root": (0.14, 0.1, -0.3)}, "rotations": {"Root": (0, 22, 26), "Body": (18, 0, 12), "Head": (28, 0, -12), "Wing.L": (28, 34, 8), "Wing.R": (24, -34, -8)}},
{"frame": 44, "locations": {"Root": (0.34, 0.1, -1.12)}, "rotations": {"Root": (0, 50, 78), "Body": (32, 0, 26), "Head": (48, 0, -24), "Wing.L": (62, 76, 18), "Wing.R": (56, -76, -18), "Tail.1": (-24, 0, 0)}},
{"frame": 70, "locations": {"Root": (0.34, 0.1, -1.14)}, "rotations": {"Root": (0, 50, 78), "Body": (32, 0, 26), "Head": (48, 0, -24), "Wing.L": (62, 76, 18), "Wing.R": (56, -76, -18), "Tail.1": (-24, 0, 0)}},
]),
]
if __name__ == "__main__":
OUT_ROOT.mkdir(parents=True, exist_ok=True)
build_crystal_bat()
+1 -1
View File
@@ -22,7 +22,7 @@ from mathutils import Vector
ROOT = Path(__file__).resolve().parents[2]
MODEL_ROOT = ROOT / "game_assets/models/downloaded/yugioh"
MODEL_ROOT = ROOT / "game_assets/models/shelved/yugioh"
FPS = 30
CLIP_SPECS = {
"Idle": {"frames": 60, "loop": True},
+1 -1
View File
@@ -12,7 +12,7 @@ from mathutils import Vector
ROOT = Path(__file__).resolve().parents[2]
MODEL_ROOT = ROOT / "game_assets/models/downloaded/yugioh"
MODEL_ROOT = ROOT / "game_assets/models/shelved/yugioh"
def reset_scene() -> None:
@@ -12,7 +12,7 @@ from mathutils import Vector
ROOT = Path(__file__).resolve().parents[2]
MODEL_ROOT = ROOT / "game_assets/models/downloaded/yugioh"
MODEL_ROOT = ROOT / "game_assets/models/shelved/yugioh"
EXPECTED_CLIPS = {"Idle", "Move", "Attack", "HitReact", "Death"}