mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Blender defaults to the AgX view transform, a photoreal film emulation that rolls off highlights and desaturates. On the flat-lit item renders the 3D route produces, it turned gold into beige and washed out every material -- which is why row C read as unusable in the bake-off. Measured on an identical coin scene where only view_transform differed: mean saturation 0.282 (AgX) vs 0.787 (Standard), a 2.8x increase. This makes row C a fair test, not a good one: the geometry is still low-poly primitives and ring/bone remain nearly invisible at 32px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
154 lines
4.9 KiB
Python
154 lines
4.9 KiB
Python
"""Route (d) 3D: a Blender scene script, run headless.
|
|
|
|
This module is *emitted* and executed by ``blender --background --python``, so
|
|
it must not import anything from this package -- the Blender interpreter has
|
|
no path to it. It is kept here as a data file rather than a library so the
|
|
repo's linters still see it as Python.
|
|
|
|
The 3D cell exists to answer one question: does a rendered mesh, put through
|
|
the same finishing stage as the vector route, clear the pixel gates? The
|
|
render is continuous-tone by nature, so the expectation is the same as every
|
|
other continuous route -- it needs the finisher.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Final
|
|
|
|
# Rendered square edge length before downscaling. Kept small: the finisher
|
|
# reduces to 32px anyway, and Cycles time scales with pixel count.
|
|
RENDER_SIZE: Final = 256
|
|
|
|
# Executed inside Blender's own Python. Written as a template rather than
|
|
# imported, because Blender cannot see this package.
|
|
SCENE_TEMPLATE: Final = """
|
|
import sys
|
|
import bpy
|
|
import mathutils
|
|
|
|
SUBJECT = sys.argv[-2]
|
|
OUT = sys.argv[-1]
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
scene = bpy.context.scene
|
|
scene.render.engine = "BLENDER_EEVEE"
|
|
scene.render.resolution_x = {size}
|
|
scene.render.resolution_y = {size}
|
|
scene.render.film_transparent = True
|
|
scene.render.image_settings.file_format = "PNG"
|
|
scene.render.image_settings.color_mode = "RGBA"
|
|
scene.render.filepath = OUT
|
|
|
|
# Blender defaults to the AgX view transform, which is built for photoreal film:
|
|
# it rolls off highlights and desaturates hard. On flat-lit item renders that
|
|
# turned gold into beige (measured mean saturation 0.28 vs 0.79 for the same
|
|
# scene), which is why this route's first pass read as washed out. Game icons
|
|
# want the raw shaded colour, so bypass tone mapping entirely.
|
|
scene.view_settings.view_transform = "Standard"
|
|
|
|
|
|
def material(name, rgba):
|
|
mat = bpy.data.materials.new(name)
|
|
mat.use_nodes = True
|
|
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
|
bsdf.inputs["Base Color"].default_value = rgba
|
|
bsdf.inputs["Roughness"].default_value = 0.45
|
|
return mat
|
|
|
|
|
|
GOLD = (0.84, 0.64, 0.24, 1.0)
|
|
CYAN = (0.36, 0.69, 0.75, 1.0)
|
|
RED = (0.69, 0.24, 0.24, 1.0)
|
|
BONE = (0.78, 0.73, 0.67, 1.0)
|
|
|
|
|
|
def add(obj, rgba):
|
|
obj.data.materials.append(material(obj.name, rgba))
|
|
return obj
|
|
|
|
|
|
def build_coin():
|
|
bpy.ops.mesh.primitive_cylinder_add(radius=1.1, depth=0.16, vertices=48)
|
|
add(bpy.context.object, GOLD)
|
|
bpy.ops.mesh.primitive_torus_add(major_radius=1.0, minor_radius=0.08)
|
|
add(bpy.context.object, GOLD)
|
|
|
|
|
|
def build_gem():
|
|
bpy.ops.mesh.primitive_cone_add(radius1=1.0, depth=1.2, vertices=8)
|
|
add(bpy.context.object, CYAN)
|
|
bpy.ops.mesh.primitive_cone_add(radius1=1.0, depth=0.7, vertices=8)
|
|
bpy.context.object.rotation_euler = (3.14159, 0, 0)
|
|
bpy.context.object.location = (0, 0, 0.95)
|
|
add(bpy.context.object, CYAN)
|
|
|
|
|
|
def build_ring():
|
|
bpy.ops.mesh.primitive_torus_add(major_radius=0.85, minor_radius=0.16)
|
|
add(bpy.context.object, GOLD)
|
|
bpy.ops.mesh.primitive_ico_sphere_add(radius=0.34, subdivisions=1)
|
|
bpy.context.object.location = (0, 0, 0.95)
|
|
add(bpy.context.object, CYAN)
|
|
|
|
|
|
def build_bone():
|
|
bpy.ops.mesh.primitive_cylinder_add(radius=0.22, depth=1.8, vertices=16)
|
|
add(bpy.context.object, BONE)
|
|
for z in (0.9, -0.9):
|
|
for x in (-0.22, 0.22):
|
|
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.34, segments=16)
|
|
bpy.context.object.location = (x, 0, z)
|
|
add(bpy.context.object, BONE)
|
|
|
|
|
|
def build_bomb():
|
|
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.0, segments=32)
|
|
add(bpy.context.object, (0.16, 0.15, 0.17, 1.0))
|
|
bpy.ops.mesh.primitive_cylinder_add(radius=0.16, depth=0.6, vertices=12)
|
|
bpy.context.object.location = (0.25, 0, 1.15)
|
|
add(bpy.context.object, RED)
|
|
|
|
|
|
BUILDERS = {{
|
|
"coin": build_coin,
|
|
"gem": build_gem,
|
|
"ring": build_ring,
|
|
"bone": build_bone,
|
|
"bomb": build_bomb,
|
|
}}
|
|
|
|
BUILDERS[SUBJECT]()
|
|
|
|
bpy.ops.object.camera_add(location=(0, -4.2, 2.6))
|
|
camera = bpy.context.object
|
|
camera.rotation_euler = mathutils.Euler((1.02, 0, 0))
|
|
camera.data.type = "ORTHO"
|
|
camera.data.ortho_scale = 3.0
|
|
scene.camera = camera
|
|
|
|
bpy.ops.object.light_add(type="SUN", location=(2.5, -3.0, 5.0))
|
|
bpy.context.object.data.energy = 4.0
|
|
bpy.ops.object.light_add(type="AREA", location=(-3.0, -2.0, 2.0))
|
|
bpy.context.object.data.energy = 90.0
|
|
|
|
bpy.ops.render.render(write_still=True)
|
|
"""
|
|
|
|
|
|
def scene_script(size: int = RENDER_SIZE) -> str:
|
|
"""Return the Blender scene script for a given render size.
|
|
|
|
Args:
|
|
size: Square render edge length in pixels.
|
|
|
|
Returns:
|
|
Python source to hand to ``blender --python``.
|
|
"""
|
|
return SCENE_TEMPLATE.format(size=size)
|
|
|
|
|
|
# Subjects with a 3D build. Deliberately a subset: a mesh for "scroll" or
|
|
# "meat" would be modelling work, not a route comparison, and the cell's
|
|
# purpose is to test whether render->finish clears the gates at all.
|
|
SUBJECTS_3D: Final = ("coin", "gem", "ring", "bone", "bomb")
|