mikronova/kyokkou
docker/agent/scenes/fluid_pipes.py
"""
Runs inside: blender --background --python fluid_pipes.py -- --config /in/config.json
bpy is always available here — it IS Blender's Python.
No external deps: json and math are stdlib.
"""
import bpy
import json
import math
import sys
def parse_config() -> dict:
argv = sys.argv
argv = argv[argv.index("--") + 1:] if "--" in argv else []
path = "/in/config.json"
for i, arg in enumerate(argv):
if arg == "--config" and i + 1 < len(argv):
path = argv[i + 1]
with open(path) as f:
return json.load(f)
METAL_COLORS = {
"gold": (1.0, 0.78, 0.18),
"steel": (0.70, 0.70, 0.75),
"chrome": (0.90, 0.90, 0.95),
"copper": (0.90, 0.44, 0.18),
}
def render(config: dict) -> None:
output_path = config.get("output_path", "/out/render.png")
samples = int(config.get("render_samples", 128))
metal = config.get("metal", "chrome")
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.render.engine = "CYCLES"
scene.cycles.samples = samples
scene.render.resolution_x = 1920
scene.render.resolution_y = 1080
scene.render.filepath = output_path
scene.render.image_settings.file_format = "PNG"
# Pipe shell
bpy.ops.mesh.primitive_cylinder_add(radius=0.30, depth=4.0, vertices=64)
pipe = bpy.context.active_object
pipe.name = "Pipe"
sol = pipe.modifiers.new("Solidify", "SOLIDIFY")
sol.thickness = 0.025
_glass_mat(pipe, "GlassPipe", color=(0.85, 0.92, 1.0, 1.0), ior=1.46)
# Liquid column
bpy.ops.mesh.primitive_cylinder_add(radius=0.27, depth=4.0, vertices=64)
liquid = bpy.context.active_object
liquid.name = "Liquid"
_metal_mat(liquid, "Liquid", metal)
# Camera
bpy.ops.object.camera_add(location=(3.5, -5.5, 2.2))
cam = bpy.context.active_object
cam.rotation_euler = (math.radians(68), 0.0, math.radians(32))
scene.camera = cam
# Key light
bpy.ops.object.light_add(type="AREA", location=(4.0, -2.0, 6.0))
key = bpy.context.active_object
key.data.energy = 900
key.data.size = 3.0
key.data.color = (1.0, 0.95, 0.85)
# Fill light
bpy.ops.object.light_add(type="AREA", location=(-4.0, 2.0, 3.0))
fill = bpy.context.active_object
fill.data.energy = 250
fill.data.size = 6.0
fill.data.color = (0.8, 0.9, 1.0)
bpy.ops.render.render(write_still=True)
def _glass_mat(obj, name: str, color: tuple, ior: float) -> None:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nt = mat.node_tree
nt.nodes.clear()
out = nt.nodes.new("ShaderNodeOutputMaterial")
glass = nt.nodes.new("ShaderNodeBsdfGlass")
glass.inputs["Color"].default_value = color
glass.inputs["IOR"].default_value = ior
nt.links.new(glass.outputs["BSDF"], out.inputs["Surface"])
obj.data.materials.append(mat)
def _metal_mat(obj, name: str, metal: str) -> None:
r, g, b = METAL_COLORS.get(metal, METAL_COLORS["chrome"])
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nt = mat.node_tree
nt.nodes.clear()
out = nt.nodes.new("ShaderNodeOutputMaterial")
prin = nt.nodes.new("ShaderNodeBsdfPrincipled")
prin.inputs["Base Color"].default_value = (r, g, b, 1.0)
prin.inputs["Metallic"].default_value = 1.0
prin.inputs["Roughness"].default_value = 0.04
nt.links.new(prin.outputs["BSDF"], out.inputs["Surface"])
obj.data.materials.append(mat)
if __name__ == "__main__":
render(parse_config())