# Preset blender-product — photoreal studio turntable of a hero object (Cycles, Metal GPU). # Usage: blender -b --factory-startup -P scene.py -- --out /frames/f_ --seconds 10 --fps 30 \ # --samples 64 --res 1920x1080 [--accent 0071e3] [--glyph "/"] [--preview] # Renders a PNG sequence (never MP4 directly); encode/overlay afterwards with ffmpeg or HyperFrames. import bpy, sys, math, argparse argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] ap = argparse.ArgumentParser() ap.add_argument("--out", required=True) ap.add_argument("--seconds", type=float, default=10) ap.add_argument("--fps", type=int, default=30) ap.add_argument("--samples", type=int, default=64) ap.add_argument("--res", default="1920x1080") ap.add_argument("--accent", default="0071e3") ap.add_argument("--glyph", default="/") ap.add_argument("--preview", action="store_true", help="render frames 1, mid, last at 25%%") a = ap.parse_args(argv) W, H = map(int, a.res.split("x")) FR = int(a.seconds * a.fps) hexrgb = lambda h: tuple(int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)) srgb2lin = lambda c: c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 ACC = tuple(srgb2lin(c) for c in hexrgb(a.accent)) + (1,) bpy.ops.wm.read_factory_settings(use_empty=True) sc = bpy.context.scene sc.render.engine = "CYCLES" prefs = bpy.context.preferences.addons["cycles"].preferences prefs.compute_device_type = "METAL" prefs.get_devices() for d in prefs.devices: d.use = True sc.cycles.device = "GPU" sc.cycles.samples = a.samples sc.cycles.use_denoising = True sc.cycles.use_adaptive_sampling = True sc.cycles.adaptive_threshold = 0.03 sc.render.resolution_x, sc.render.resolution_y = W, H sc.render.resolution_percentage = 25 if a.preview else 100 sc.render.fps = a.fps sc.frame_start, sc.frame_end = 1, FR sc.view_settings.view_transform = "AgX" sc.view_settings.look = "AgX - Medium High Contrast" sc.render.image_settings.file_format = "PNG" sc.render.filepath = a.out sc.render.use_overwrite = False # re-running resumes: finished frames are skipped sc.render.use_placeholder = False # a crash must not leave an empty frame that resume would skip def mat(name, base, rough, metal=0.0, coat=0.0, trans=0.0, emit=None): m = bpy.data.materials.new(name) m.use_nodes = True p = m.node_tree.nodes["Principled BSDF"] p.inputs["Base Color"].default_value = base p.inputs["Roughness"].default_value = rough p.inputs["Metallic"].default_value = metal p.inputs["Coat Weight"].default_value = coat p.inputs["Transmission Weight"].default_value = trans if emit: p.inputs["Emission Color"].default_value = emit[0] p.inputs["Emission Strength"].default_value = emit[1] return m # Cyclorama: floor curving into the back wall (seamless studio sweep) bpy.ops.mesh.primitive_plane_add(size=1) cyc = bpy.context.object me = cyc.data bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.delete(type="VERT") bpy.ops.object.mode_set(mode="OBJECT") verts, faces, prof = [], [], [] for i in range(10): # profile in YZ: flat floor (no duplicate vertex → no degenerate black faces) prof.append((-12 + i * 1.4, 0.0)) for j in range(1, 17): # quarter circle up into the back wall ang = j / 16 * (math.pi / 2) prof.append((0.6 + 6 * math.sin(ang), 6 - 6 * math.cos(ang))) for x in (-30, 30): for (y, z) in prof: verts.append((x, y, z)) n = len(prof) for i in range(n - 1): faces.append((i, i + 1, n + i + 1, n + i)) me.from_pydata(verts, [], faces) me.update() for f in me.polygons: f.use_smooth = True cyc.data.materials.append(mat("Cyc", (0.92, 0.925, 0.94, 1), 0.55)) # Hero: rounded tile (app-icon proportion) in frosted glass on a satin base, accent glyph inside bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 1.15)) tile = bpy.context.object tile.scale = (1.7, 0.42, 1.7) bev = tile.modifiers.new("bev", "BEVEL"); bev.width = 0.12; bev.segments = 10 tile.modifiers.new("sub", "SUBSURF").levels = 1 bpy.ops.object.shade_smooth() tile.data.materials.append(mat("Glass", (0.96, 0.97, 1.0, 1), 0.08, trans=1.0, coat=1.0)) bpy.ops.object.text_add(location=(0, -0.02, 1.15), rotation=(math.pi / 2, 0, 0)) glyph = bpy.context.object glyph.data.body = a.glyph glyph.data.align_x = "CENTER"; glyph.data.align_y = "CENTER" glyph.data.size = 1.25; glyph.data.extrude = 0.09; glyph.data.bevel_depth = 0.015 glyph.data.materials.append(mat("Accent", ACC, 0.25, coat=0.6, emit=(ACC, 0.6))) bpy.ops.mesh.primitive_cylinder_add(radius=1.25, depth=0.16, location=(0, 0, 0.08), vertices=128) base = bpy.context.object base.modifiers.new("bev", "BEVEL").width = 0.04 bpy.ops.object.shade_smooth() base.data.materials.append(mat("Satin", (0.2, 0.2, 0.22, 1), 0.32, metal=0.9)) hero = bpy.data.objects.new("Hero", None) sc.collection.objects.link(hero) for o in (tile, glyph): o.parent = hero hero.rotation_euler = (0, 0, 0) hero.keyframe_insert("rotation_euler", frame=1) hero.rotation_euler = (0, 0, math.radians(-360)) # full turn → seamless loop, glyph ends front-facing hero.keyframe_insert("rotation_euler", frame=FR) # linear spin so the loop point is invisible try: for fc in hero.animation_data.action.fcurves: for k in fc.keyframe_points: k.interpolation = "LINEAR" except AttributeError: # Blender 5 layered actions for layer in hero.animation_data.action.layers: for strip in layer.strips: for bag in strip.channelbags: for fc in bag.fcurves: for k in fc.keyframe_points: k.interpolation = "LINEAR" # Lights: big soft key, rim, and a cool fill def area(name, loc, rot, size, power, color=(1, 1, 1)): l = bpy.data.lights.new(name, "AREA"); l.size = size; l.energy = power; l.color = color o = bpy.data.objects.new(name, l); o.location = loc; o.rotation_euler = rot sc.collection.objects.link(o) area("Key", (-4, -4, 5), (math.radians(50), 0, math.radians(-45)), 5, 900) area("Rim", (3.5, 3, 3.5), (math.radians(-60), 0, math.radians(140)), 3, 600, (0.85, 0.9, 1)) area("Fill", (5, -3, 1.5), (math.radians(80), 0, math.radians(60)), 4, 200, (0.9, 0.95, 1)) world = bpy.data.worlds.new("W"); sc.world = world world.use_nodes = True world.node_tree.nodes["Background"].inputs[0].default_value = (0.9, 0.92, 0.95, 1) world.node_tree.nodes["Background"].inputs[1].default_value = 0.35 # Camera: slow push-in with shallow depth of field cam = bpy.data.objects.new("Cam", bpy.data.cameras.new("Cam")) sc.collection.objects.link(cam); sc.camera = cam cam.data.lens = 65 cam.data.dof.use_dof = True; cam.data.dof.aperture_fstop = 2.8; cam.data.dof.focus_object = tile track = cam.constraints.new("TRACK_TO"); track.target = tile cam.location = (2.2, -10.5, 2.4); cam.keyframe_insert("location", frame=1) cam.location = (1.0, -8.6, 1.9); cam.keyframe_insert("location", frame=FR) if a.preview: for f in (1, FR // 2, FR): sc.frame_set(f); sc.render.filepath = f"{a.out}preview_{f:04d}" bpy.ops.render.render(write_still=True) else: bpy.ops.render.render(animation=True)