"""Preview renders of the Remora (Cycles). Run: blender -b Tender_Remora.blend --python render_previews.py -- [views] [mode] mode: studio (grey cyclorama and soft boxes), clay (one grey material), water (dark blue water like the reference).""" import bpy, math, os, sys from mathutils import Vector argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] TAG = argv[0] if argv else "check" VIEWS = argv[1].split(",") if len(argv) > 1 and argv[1] != "all" else None MODE = argv[2] if len(argv) > 2 else "studio" SAMPLES = int(argv[3]) if len(argv) > 3 else 64 OUT = os.path.join(os.path.dirname(bpy.data.filepath), "previews") os.makedirs(OUT, exist_ok=True) sc = bpy.context.scene def use_gpu(): sc.render.engine = 'CYCLES' try: prefs = bpy.context.preferences.addons['cycles'].preferences for kind in ('OPTIX', 'CUDA', 'HIP', 'ONEAPI'): try: prefs.compute_device_type = kind except Exception: continue prefs.refresh_devices() if any(d.type == kind for d in prefs.devices): for d in prefs.devices: d.use = d.type == kind sc.cycles.device = 'GPU' return kind except Exception as ex: print("GPU not set:", ex) sc.cycles.device = 'CPU' return 'CPU' print("device", use_gpu()) studio = bpy.data.collections.get("Studio") or bpy.data.collections.new("Studio") if studio.name not in [c.name for c in sc.collection.children]: sc.collection.children.link(studio) for o in list(studio.objects): bpy.data.objects.remove(o) for c in bpy.data.collections: # collision hulls and parts libraries stay out of the picture if c.name.startswith(("REMORA | Collision", "REMORA | Parts")): for o in c.objects: o.hide_render = True def light(name, kind, loc, rot, energy, size=1.0, color=(1, 1, 1)): ld = bpy.data.lights.new(name, kind) ld.energy = energy ld.color = color if kind == 'AREA': ld.size = size if kind == 'SUN': ld.angle = math.radians(size) o = bpy.data.objects.new(name, ld) studio.objects.link(o) o.location = loc o.rotation_euler = [math.radians(a) for a in rot] return o w = bpy.data.worlds.get("Remora_World") or bpy.data.worlds.new("Remora_World") sc.world = w w.use_nodes = True nt = w.node_tree nt.nodes.clear() bg = nt.nodes.new('ShaderNodeBackground') out = nt.nodes.new('ShaderNodeOutputWorld') nt.links.new(bg.outputs[0], out.inputs[0]) if MODE == "water": tc = nt.nodes.new('ShaderNodeTexCoord'); sep = nt.nodes.new('ShaderNodeSeparateXYZ') nt.links.new(tc.outputs["Generated"], sep.inputs[0]) ramp = nt.nodes.new('ShaderNodeValToRGB') ramp.color_ramp.elements[0].position = 0.35; ramp.color_ramp.elements[0].color = (0.002, 0.012, 0.022, 1) ramp.color_ramp.elements[1].position = 0.75; ramp.color_ramp.elements[1].color = (0.030, 0.120, 0.170, 1) nt.links.new(sep.outputs["Z"], ramp.inputs[0]) nt.links.new(ramp.outputs[0], bg.inputs[0]) bg.inputs[1].default_value = 1.0 light("Sun", 'SUN', (0, 0, 20), (8, -12, 0), 2.2, 6, (0.75, 0.92, 1.0)) light("Fill", 'AREA', (6, -6, 3), (65, 0, 45), 150, 8, (0.35, 0.65, 0.85)) light("Rim", 'AREA', (-7, 7, 4), (60, 0, -135), 300, 6, (0.5, 0.8, 1.0)) else: bg.inputs[0].default_value = (0.18, 0.19, 0.20, 1) bg.inputs[1].default_value = 0.6 light("Key", 'AREA', (5, -7, 7), (50, 0, 35), 1600, 5) light("Fill", 'AREA', (-7, -3, 4), (65, 0, -65), 500, 6) light("Top", 'AREA', (0, 0, 9), (0, 0, 0), 900, 8) light("Rim", 'AREA', (-3, 9, 5), (60, 0, -160), 900, 4) fm = bpy.data.meshes.new("Floor") fm.from_pydata([(-60, -60, -1.16), (60, -60, -1.16), (60, 60, -1.16), (-60, 60, -1.16)], [], [(0, 1, 2, 3)]) fl = bpy.data.objects.new("Floor", fm) studio.objects.link(fl) fmat = bpy.data.materials.get("Studio floor") or bpy.data.materials.new("Studio floor") fmat.use_nodes = True fmat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.32, 0.33, 0.34, 1) fmat.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.6 fm.materials.append(fmat) if MODE == "clay": clay = bpy.data.materials.get("_clay") or bpy.data.materials.new("_clay") clay.use_nodes = True clay.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.5, 0.5, 0.52, 1) clay.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.45 for o in sc.objects: if o.type == 'MESH' and o.name != "Floor": for i in range(len(o.material_slots)): o.material_slots[i].link = 'OBJECT' o.material_slots[i].material = clay cam_d = bpy.data.cameras.new("Cam") cam = bpy.data.objects.new("Cam", cam_d) studio.objects.link(cam) sc.camera = cam tgt = bpy.data.objects.new("CamTarget", None) studio.objects.link(tgt) con = cam.constraints.new('TRACK_TO') con.target = tgt con.track_axis = 'TRACK_NEGATIVE_Z' con.up_axis = 'UP_Y' VIEW = { # name: (camera location, target, lens mm) "hero": ((-6.8, -9.0, 2.6), (0.0, -0.6, 0.1), 50), "port": ((14.0, 0.0, 0.6), (0.0, 0.0, 0.2), 55), "top": ((0.0, 0.0, 24.0), (0.0, 0.0, 0.0), 50), "rear": ((7.5, 8.5, 3.2), (0.0, 0.5, 0.0), 50), "front": ((0.0, -12.5, 0.4), (0.0, 0.0, 0.0), 55), "nose": ((-2.4, -7.6, 0.5), (0.0, -3.6, -0.25), 55), "canopy": ((3.2, -4.6, 3.0), (0.0, -1.9, 1.0), 50), "collar": ((2.6, 1.4, 3.6), (0.0, 0.3, 1.2), 50), "stern": ((-3.2, 6.2, 0.6), (-1.0, 2.2, -0.1), 45), "below": ((-5.5, -6.5, -3.2), (0.0, -0.3, -0.6), 45), "cabin": ((0.0, -1.74, 1.02), (0.0, -3.6, -0.05), 20), } sc.render.resolution_x, sc.render.resolution_y = 1600, 1000 sc.render.resolution_percentage = 100 sc.cycles.samples = SAMPLES sc.cycles.use_denoising = True sc.view_settings.view_transform = 'AgX' sc.view_settings.look = 'AgX - Medium High Contrast' if MODE != "clay" else 'None' for name, (loc, t, lens) in VIEW.items(): if VIEWS and name not in VIEWS: continue cam.location = loc tgt.location = t cam_d.lens = lens cam_d.clip_start = 0.02 sc.render.filepath = os.path.join(OUT, "%s_%s.png" % (TAG, name)) bpy.ops.render.render(write_still=True) print("wrote", sc.render.filepath)