"""Cycles pictures of the canister station in a white studio like the user's picture (a seamless light ground, a big soft key from the front and above, a fill from the right), Standard view transform so colours read as in the picture. Saves the studio into the file (collection "Studio", not exported). blender.exe -b ../CanisterStation.blend --python cs_render.py -- [tag] [shots=picture,q_left,...] [samples=N] previews/CanisterStation__.png; "picture" is the picture's own camera and framing (CS_PictureCamera). """ 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=")), "128")) KEY = float(next((a.split("=")[1] for a in argv if a.startswith("key=")), "140")) C = Vector((0.0, 0.0, 0.75)) # the machine's middle V = lambda x, y, z: Vector((x, y, z)) # name: (camera, target, lens, resolution, eject) ("picture" uses CS_PictureCamera) SHOTS = { "picture": (None, None, None, (1199, 1363), 0.0), "q_front_left": (V(-2.2, -3.4, 1.9), V(0.15, 0.0, 0.72), 50, (1600, 1600), 0.0), "q_back_right": (V(3.0, 2.9, 1.8), V(0.10, 0.0, 0.75), 50, (1600, 1600), 0.0), "front": (V(3.6, -0.6, 1.0), V(0.15, 0.0, 0.70), 50, (1600, 1600), 0.0), "cap": (V(1.75, -1.35, 2.05), V(0.78, 0.0, 1.30), 50, (1600, 1200), 0.0), "screen_hub": (V(0.55, -1.75, 0.75), V(0.08, -0.15, 0.40), 45, (1600, 1200), 0.0), "eject": (V(1.2, -4.3, 1.9), V(0.25, 0.0, 0.85), 60, (1600, 1600), 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): if o.name != "CS_PictureCamera": bpy.data.objects.remove(o) # a seamless ground: a floor that curves up into a back wall far behind me = bpy.data.meshes.new("Studio_Cyc") bm = bmesh.new() prof = [(-9.0, 0.0), (4.0, 0.0)] + [(4.0 + 2.0 * math.sin(a), 2.0 - 2.0 * math.cos(a)) for a in [math.radians(k * 10) for k in range(1, 10)]] + [(6.0, 8.0)] rows = [] for x in (-10.0, 10.0): rows.append([bm.verts.new((x, y, z)) for (y, z) in prof]) for i in range(len(prof) - 1): bm.faces.new([rows[0][i], rows[1][i], rows[1][i + 1], rows[0][i + 1]]) bm.to_mesh(me) bm.free() cyc = bpy.data.objects.new("Studio_Cyc", me) col.objects.link(cyc) for p in me.polygons: p.use_smooth = True wm = bpy.data.materials.get("Studio_White") or bpy.data.materials.new("Studio_White") wm.use_nodes = True b = wm.node_tree.nodes.get("Principled BSDF") b.inputs["Base Color"].default_value = (0.80, 0.80, 0.80, 1) b.inputs["Roughness"].default_value = 0.9 me.materials.append(wm) def area(name, loc, size, power): ld = bpy.data.lights.new(name, 'AREA') ld.shape = 'DISK' ld.size = size ld.energy = power o = bpy.data.objects.new(name, ld) col.objects.link(o) o.location = loc o.rotation_euler = (C - loc).normalized().to_track_quat('-Z', 'Y').to_euler() area("Studio_Key", C + V(-1.2, -3.0, 2.6), 3.0, KEY) area("Studio_Fill", C + V(3.0, -2.2, 0.6), 3.0, KEY * 0.40) area("Studio_Top", C + V(0.3, 0.6, 3.2), 2.5, KEY * 0.35) area("Studio_Rim", C + V(-1.0, 3.0, 1.6), 2.0, KEY * 0.25) 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.85, 0.85, 0.86, 1) w.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.35 cam = bpy.data.objects.get("CS_View") or bpy.data.objects.new("CS_View", bpy.data.cameras.new("CS_View")) if cam.name not in col.objects: col.objects.link(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 view = studio() setup(sc) for o in bpy.data.objects: if o.name.startswith("UCX_"): o.hide_render = True root = bpy.data.objects["CanisterStation_Root"] act = root.animation_data.action if root.animation_data else None if root.animation_data: root.animation_data.action = None pic = bpy.data.objects["CS_PictureCamera"] done = [] os.makedirs(OUTD, exist_ok=True) for nm, (loc, tgt, lens, res, ej) in SHOTS.items(): root["eject"] = ej 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 if loc is None: sc.camera = pic else: sc.camera = view view.location = loc view.rotation_euler = (tgt - loc).normalized().to_track_quat('-Z', 'Y').to_euler() view.data.lens = lens view.data.sensor_fit = 'AUTO' view.data.clip_start = 0.01 sc.render.filepath = os.path.join(OUTD, "CanisterStation_%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["eject"] = 0.0 sc.camera = pic sc.frame_set(1) bpy.ops.wm.save_mainfile() print("RENDERS OK", done) main()