import math import pathlib import sys import bpy from mathutils import Vector def configure_scene() -> bpy.types.Scene: bpy.ops.wm.read_factory_settings(use_empty=True) scene = bpy.context.scene scene.name = "M11 Render Reference" scene.render.engine = "BLENDER_EEVEE" scene.render.resolution_x = 256 scene.render.resolution_y = 256 scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = "PNG" scene.render.image_settings.color_mode = "RGBA" scene.render.image_settings.color_depth = "8" scene.render.film_transparent = False scene.render.use_file_extension = True scene.render.dither_intensity = 0 scene.eevee.taa_render_samples = 1 scene.view_settings.look = "None" scene.view_settings.exposure = 0 scene.view_settings.gamma = 1 world = bpy.data.worlds.new("M11 Reference World") world.use_nodes = True background = world.node_tree.nodes.get("Background") background.inputs["Color"].default_value = (0.0508760884, 0.0508760884, 0.0508760884, 1) background.inputs["Strength"].default_value = 1 scene.world = world bpy.ops.mesh.primitive_cube_add(size=3, location=(0, 0, 0)) cube = bpy.context.object cube.name = "M11 Reference Cube" material = bpy.data.materials.new("M11 Reference Black") material.use_nodes = True principled = material.node_tree.nodes.get("Principled BSDF") principled.inputs["Base Color"].default_value = (0, 0, 0, 1) principled.inputs["Metallic"].default_value = 0 principled.inputs["Roughness"].default_value = 1 principled.inputs["Specular IOR Level"].default_value = 0 cube.data.materials.append(material) bpy.ops.object.camera_add() camera = bpy.context.object camera.name = "M11 Reference Camera" camera.data.name = "M11 Reference Camera" yaw = -math.pi / 4 pitch = 0.55 distance = 7 three_position = Vector(( distance * math.cos(pitch) * math.cos(yaw), distance * math.cos(pitch) * math.sin(yaw), distance * math.sin(pitch), )) camera.location = (three_position.x, -three_position.z, three_position.y) camera.rotation_euler = (-camera.location).to_track_quat("-Z", "Y").to_euler() camera.data.type = "PERSP" camera.data.lens = 50 camera.data.sensor_width = 36 camera.data.sensor_fit = "HORIZONTAL" camera.data.clip_start = 0.1 camera.data.clip_end = 1000 scene.camera = camera return scene def main() -> None: if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1:]) not in (2, 3): raise SystemExit("usage: blender --background --factory-startup --python generate-m11-render-reference.py -- FIXTURE OUTPUT [EXPECTED]") arguments = sys.argv[sys.argv.index("--") + 1:] fixture_arg, output_arg = arguments[:2] fixture = pathlib.Path(fixture_arg).resolve() output = pathlib.Path(output_arg).resolve() fixture.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True) scene = configure_scene() bpy.ops.wm.save_as_mainfile(filepath=str(fixture), compress=True) scene.render.filepath = str(output) bpy.ops.render.render(write_still=True) if len(arguments) == 3: expected = pathlib.Path(arguments[2]).resolve() actual_image = bpy.data.images.load(str(output), check_existing=False) expected_image = bpy.data.images.load(str(expected), check_existing=False) if actual_image.size[:] != expected_image.size[:]: raise RuntimeError(f"reference dimensions differ: {actual_image.size[:]} != {expected_image.size[:]}") actual_pixels = actual_image.pixels[:] expected_pixels = expected_image.pixels[:] maximum = max(abs(actual - reference) for actual, reference in zip(actual_pixels, expected_pixels)) if maximum != 0: raise RuntimeError(f"reference decoded pixels differ: max={maximum}") print(f"m11-render-reference-pixels width={actual_image.size[0]} height={actual_image.size[1]} max=0") print(f"m11-render-reference fixture={fixture} output={output}") main()