| 1 | #!/usr/bin/env python3 |
| 2 | """Generate the CC0 rigid FBX equipment fixture with Blender. |
| 3 | |
| 4 | Writes tests/fixtures/equipment/sword-fbx.fbx: a blade with the CC0 checker |
| 5 | texture embedded in the FBX and a plain-colour guard, and |
| 6 | sword-fbx-external.fbx: the same model referencing checker.png next to it |
| 7 | instead of embedding it. The files are exported |
| 8 | Z-up in centimetres so the importer has to perform the axis/unit |
| 9 | normalisation itself. Re-run when needed: |
| 10 | |
| 11 | blender --background --factory-startup --python scripts/make_fbx_equipment_fixture.py |
| 12 | |
| 13 | Blender is only a fixture tool; the application never calls Blender. |
| 14 | """ |
| 15 | |
| 16 | from pathlib import Path |
| 17 | |
| 18 | import bpy |
| 19 | |
| 20 | ROOT = Path(__file__).resolve().parents[1] |
| 21 | FIXTURES = ROOT / "tests" / "fixtures" / "equipment" |
| 22 | TARGET = FIXTURES / "sword-fbx.fbx" |
| 23 | EXTERNAL = FIXTURES / "sword-fbx-external.fbx" |
| 24 | |
| 25 | |
| 26 | def box(name, x0, x1, y0, y1, z0, z1, material): |
| 27 | verts = [ |
| 28 | (x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0), |
| 29 | (x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1), |
| 30 | ] |
| 31 | faces = [ |
| 32 | (0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), |
| 33 | (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7), |
| 34 | ] |
| 35 | mesh = bpy.data.meshes.new(name) |
| 36 | mesh.from_pydata(verts, [], faces) |
| 37 | uv = mesh.uv_layers.new(name="UVMap") |
| 38 | corners = [(0, 0), (1, 0), (1, 1), (0, 1)] |
| 39 | for poly in mesh.polygons: |
| 40 | for k, loop in enumerate(poly.loop_indices): |
| 41 | uv.data[loop].uv = corners[k] |
| 42 | mesh.materials.append(material) |
| 43 | mesh.update() |
| 44 | obj = bpy.data.objects.new(name, mesh) |
| 45 | bpy.context.scene.collection.objects.link(obj) |
| 46 | return obj |
| 47 | |
| 48 | |
| 49 | def main() -> None: |
| 50 | bpy.ops.wm.read_factory_settings(use_empty=True) |
| 51 | scene = bpy.context.scene |
| 52 | scene.unit_settings.system = "METRIC" |
| 53 | scene.unit_settings.scale_length = 1.0 |
| 54 | |
| 55 | image = bpy.data.images.load(str(FIXTURES / "checker.png")) |
| 56 | image.pack() |
| 57 | blade_mat = bpy.data.materials.new("blade_checker") |
| 58 | blade_mat.use_nodes = True |
| 59 | bsdf = blade_mat.node_tree.nodes["Principled BSDF"] |
| 60 | tex = blade_mat.node_tree.nodes.new("ShaderNodeTexImage") |
| 61 | tex.image = image |
| 62 | blade_mat.node_tree.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"]) |
| 63 | |
| 64 | guard_mat = bpy.data.materials.new("guard_brass") |
| 65 | guard_mat.use_nodes = True |
| 66 | guard_mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = ( |
| 67 | 0.75, 0.55, 0.2, 1.0, |
| 68 | ) |
| 69 | |
| 70 | # Blender space: Z up. Blade 0.58 m long, guard at the bottom. |
| 71 | box("blade", -0.03, 0.03, -0.01, 0.01, 0.03, 0.58, blade_mat) |
| 72 | box("guard", -0.09, 0.09, -0.015, 0.015, 0.0, 0.03, guard_mat) |
| 73 | |
| 74 | common = dict( |
| 75 | use_selection=False, |
| 76 | object_types={"MESH"}, |
| 77 | axis_forward="Y", |
| 78 | axis_up="Z", |
| 79 | global_scale=1.0, |
| 80 | apply_unit_scale=True, |
| 81 | apply_scale_options="FBX_SCALE_NONE", |
| 82 | bake_anim=False, |
| 83 | add_leaf_bones=False, |
| 84 | ) |
| 85 | bpy.ops.export_scene.fbx( |
| 86 | filepath=str(TARGET), path_mode="COPY", embed_textures=True, **common |
| 87 | ) |
| 88 | image.unpack(method="REMOVE") |
| 89 | image.filepath = str(FIXTURES / "checker.png") |
| 90 | bpy.ops.export_scene.fbx( |
| 91 | filepath=str(EXTERNAL), path_mode="RELATIVE", embed_textures=False, **common |
| 92 | ) |
| 93 | print("wrote", EXTERNAL) |
| 94 | print("wrote", TARGET) |
| 95 | |
| 96 | |
| 97 | if __name__ == "__main__": |
| 98 | main() |