"""Cycles pictures of the nutrition feeder in a studio like the user's picture (grey wall, soft light from the upper left), Standard view transform so colours read as in the picture. Saves the studio into the file (collection "Studio", not exported). blender.exe -b ../NutritionFeeder.blend --python nf_render.py -- [tag] [shots=front,q_left,...] [samples=N] Pictures: previews/NutritionFeeder__.png; previews/compare/lookalike_.png sets the picture beside the front shot at the same framing. """ import bpy, bmesh, math, os, sys from mathutils import Vector HERE = os.path.dirname(os.path.abspath(__file__)) FOLDER = os.path.dirname(HERE) OUTD = os.path.join(FOLDER, "previews") argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] TAG = next((a for a in argv if "=" not in a), "v01") SAMPLES = int(next((a.split("=")[1] for a in argv if a.startswith("samples=")), "96")) KEY = float(next((a.split("=")[1] for a in argv if a.startswith("key=")), "30")) S, U0, V0 = 0.0005, 513.0, 1345.0 P = lambda u, v, f: Vector(((u - U0) * S, -f * 0.001, (V0 - v) * S)) # name: (camera, target, lens, shift, resolution, inject) SHOTS = { "front": (P(675, 972, 1305), P(675, 972, 0), 39.06, (-0.106, 0.133), (1024, 1536), 1.0), "q_left": (P(120, 640, 820), P(560, 760, 30), 38, (0, 0), (1600, 1200), 1.0), "q_right_low": (P(1300, 1300, 650), P(650, 880, 30), 38, (0, 0), (1600, 1200), 1.0), "cavity": (P(560, 830, 470), P(674, 975, 0), 42, (0, 0), (1600, 1200), 1.0), "injector": (P(560, 760, 250), P(676, 840, 35), 55, (0, 0), (1600, 1200), 1.0), "injector_away": (P(560, 760, 250), P(676, 840, 35), 55, (0, 0), (1600, 1200), 0.0), "edge": (P(-260, 220, 330), P(140, 330, 40), 48, (0, 0), (1600, 1200), 1.0), } only = next((a.split("=")[1].split(",") for a in argv if a.startswith("shots=")), None) if only: SHOTS = {k: v for k, v in SHOTS.items() if k in only} def studio(): sc = bpy.context.scene col = bpy.data.collections.get("Studio") if col is None: col = bpy.data.collections.new("Studio") sc.collection.children.link(col) for o in list(col.objects): bpy.data.objects.remove(o) # the wall: a big plane with a hole the panel covers (the tunnel runs into the wall) me = bpy.data.meshes.new("Studio_Wall") bm = bmesh.new() big = [(-2000, -1500), (3000, -1500), (3000, 3000), (-2000, 3000)] hole = [(70, 125), (956, 125), (956, 1320), (70, 1320)] ob_ = [bm.verts.new(P(u, v, -0.3)) for u, v in big] ih = [bm.verts.new(P(u, v, -0.3)) for u, v in hole] for i in range(4): bm.faces.new([ob_[i], ob_[(i + 1) % 4], ih[(i + 1) % 4], ih[i]]) bm.to_mesh(me) bm.free() wall = bpy.data.objects.new("Studio_Wall", me) col.objects.link(wall) wm = bpy.data.materials.get("Studio_WallPaint") or bpy.data.materials.new("Studio_WallPaint") wm.use_nodes = True bsdf = wm.node_tree.nodes.get("Principled BSDF") bsdf.inputs["Base Color"].default_value = (0.42, 0.42, 0.43, 1) bsdf.inputs["Roughness"].default_value = 0.8 me.materials.append(wm) # lights centre = P(513, 720, 40) def area(name, loc, size, power, color=(1, 1, 1)): ld = bpy.data.lights.new(name, 'AREA') ld.shape = 'DISK' ld.size = size ld.energy = power ld.color = color o = bpy.data.objects.new(name, ld) col.objects.link(o) o.location = loc o.rotation_euler = (centre - loc).normalized().to_track_quat('-Z', 'Y').to_euler() return o area("Studio_Key", centre + Vector((-0.7, -1.2, 1.05)), 1.6, KEY) area("Studio_Fill", centre + Vector((1.1, -1.4, 0.0)), 1.8, KEY * 0.45) area("Studio_Top", centre + Vector((0.0, -0.5, 1.4)), 1.0, KEY * 0.2) w = sc.world or bpy.data.worlds.new("World") sc.world = w w.use_nodes = True w.node_tree.nodes["Background"].inputs["Color"].default_value = (0.10, 0.10, 0.105, 1) w.node_tree.nodes["Background"].inputs["Strength"].default_value = 1.0 cam = bpy.data.objects.new("Studio_Camera", bpy.data.cameras.new("Studio_Camera")) col.objects.link(cam) sc.camera = cam return cam def setup(sc): sc.render.engine = 'CYCLES' try: prefs = bpy.context.preferences.addons['cycles'].preferences prefs.compute_device_type = 'HIP' prefs.get_devices() for d in prefs.devices: d.use = d.type == 'HIP' sc.cycles.device = 'GPU' except Exception as ex: print("GPU not set:", ex) sc.cycles.samples = SAMPLES sc.cycles.use_denoising = True sc.view_settings.view_transform = 'Standard' sc.view_settings.look = 'None' sc.view_settings.exposure = 0.0 sc.render.image_settings.file_format = 'PNG' sc.render.image_settings.color_mode = 'RGB' sc.render.film_transparent = False def main(): sc = bpy.context.scene cam = studio() setup(sc) for o in bpy.data.objects: if o.name.startswith("UCX_"): o.hide_render = True root = bpy.data.objects["NutritionFeeder_Root"] act = root.animation_data.action if root.animation_data else None if root.animation_data: root.animation_data.action = None done = [] os.makedirs(OUTD, exist_ok=True) for nm, (loc, tgt, lens, shift, res, inj) in SHOTS.items(): root["inject"] = inj root.update_tag() sc.frame_set(2) sc.frame_set(1) bpy.context.view_layer.update() sc.render.resolution_x, sc.render.resolution_y = res sc.render.resolution_percentage = 100 cam.data.sensor_fit = 'VERTICAL' if nm == "front" else 'AUTO' cam.location = loc cam.rotation_euler = (tgt - loc).normalized().to_track_quat('-Z', 'Y').to_euler() cam.data.lens = lens cam.data.shift_x, cam.data.shift_y = shift cam.data.clip_start = 0.01 sc.render.filepath = os.path.join(OUTD, "NutritionFeeder_%s_%s.png" % (TAG, nm)) bpy.ops.render.render(write_still=True) done.append(sc.render.filepath) if root.animation_data: root.animation_data.action = act root["inject"] = 0.0 sc.frame_set(1) bpy.ops.wm.save_mainfile() print("RENDERS OK", done) main()