"""Renders of Tender_Aethel.blend. blender -b Tender_Aethel.blend --python build/ae_render.py -- [prefix] mode 'views' : Workbench, orthographic SIDE / FRONT / TOP framed exactly as the sheet's crops (for ae_compare.py), plus alpha masks. mode 'studio' : Cycles, bright studio light, Standard view transform: side, front, top, back, two three-quarters. mode 'topo' : Workbench wireframe over the shading, three-quarter views. mode 'water' : Cycles, dark blue water, lights on. """ import os import sys import math import json import bpy from mathutils import Vector, Euler HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import ae_lib as L ROOT = os.path.dirname(HERE) PREV = os.path.join(ROOT, 'previews') os.makedirs(os.path.join(PREV, 'compare'), exist_ok=True) argv = sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else [] MODE = argv[0] if argv else 'views' PFX = argv[1] if len(argv) > 1 else 'v01' S = L.S # sheet crops (x0, y0, x1, y1) and the cameras that frame them CROPS = {'side': (175, 85, 712, 400), 'front': (795, 85, 1135, 400), 'top': (150, 455, 680, 773)} def cam(name, loc, rot, ortho=None, lens=50): c = bpy.data.cameras.new(name) if ortho: c.type = 'ORTHO'; c.ortho_scale = ortho else: c.lens = lens c.clip_start = 0.05; c.clip_end = 200 o = bpy.data.objects.new(name, c) bpy.context.scene.collection.objects.link(o) o.location = loc; o.rotation_euler = Euler([math.radians(a) for a in rot]) return o def sheet_cams(): out = {} x0, y0, x1, y1 = CROPS['side'] out['side'] = (cam('CAM_side', ((0 + 20), ((x0 + x1) / 2 - L.V0) * S, (L.Y0 - (y0 + y1) / 2) * S), (90, 0, 90), ortho=(x1 - x0) * S), ((x1 - x0) * 2, (y1 - y0) * 2)) x0, y0, x1, y1 = CROPS['front'] out['front'] = (cam('CAM_front', (((x0 + x1) / 2 - L.FRONT_CX) * S, -20, (L.Y0 - (y0 + y1) / 2) * S), (90, 0, 0), ortho=(x1 - x0) * S), ((x1 - x0) * 2, (y1 - y0) * 2)) x0, y0, x1, y1 = CROPS['top'] k = 469.0 / 429.0 sx0, sx1 = L.top_to_side(x0), L.top_to_side(x1) w = int(round((x1 - x0) * 2 * k)) out['top'] = (cam('CAM_top', (((y0 + y1) / 2 - L.TOP_CY) * S, ((sx0 + sx1) / 2 - L.V0) * S, 20), (0, 0, 90), ortho=(sx1 - sx0) * S), (w, (y1 - y0) * 2)) return out def setup_workbench(outline=True): sc = bpy.context.scene sc.render.engine = 'BLENDER_WORKBENCH' sh = sc.display.shading sh.light = 'STUDIO'; sh.color_type = 'MATERIAL' sh.show_object_outline = outline; sh.object_outline_color = (0, 0, 0) sh.show_cavity = False sc.render.film_transparent = True sc.view_settings.view_transform = 'Standard' def studio_world(strength=1.0, dark=False): w = bpy.data.worlds.new('W'); bpy.context.scene.world = w w.use_nodes = True bg = w.node_tree.nodes['Background'] bg.inputs['Color'].default_value = (0.01, 0.025, 0.045, 1) if dark else (0.62, 0.64, 0.67, 1) bg.inputs['Strength'].default_value = strength def area(name, loc, size, power, target=(0, 0, 1.2)): l = bpy.data.lights.new(name, 'AREA'); l.size = size; l.energy = power o = bpy.data.objects.new(name, l); bpy.context.scene.collection.objects.link(o) o.location = loc d = Vector(target) - Vector(loc) o.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler() def setup_cycles(samples=96): sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = samples sc.cycles.use_denoising = True try: prefs = bpy.context.preferences.addons['cycles'].preferences for t in ('OPTIX', 'CUDA', 'HIP', 'ONEAPI'): try: prefs.compute_device_type = t; prefs.get_devices() if any(d.type == t for d in prefs.devices): for d in prefs.devices: d.use = d.type == t sc.cycles.device = 'GPU' break except Exception: continue except Exception: pass sc.view_settings.view_transform = 'Standard' sc.render.film_transparent = False def render(camobj, res, path): sc = bpy.context.scene sc.camera = camobj sc.render.resolution_x, sc.render.resolution_y = res sc.render.resolution_percentage = 100 sc.render.filepath = path bpy.ops.render.render(write_still=True) def persp(name, az, el, dist, target=(0, 0, 1.4), lens=55): t = Vector(target) d = Vector((math.cos(math.radians(el)) * math.cos(math.radians(az)), math.cos(math.radians(el)) * math.sin(math.radians(az)), math.sin(math.radians(el)))) loc = t + d * dist c = cam(name, loc, (0, 0, 0), lens=lens) c.rotation_euler = (t - loc).to_track_quat('-Z', 'Y').to_euler() return c def main(): sc = bpy.context.scene sc.frame_set(1) for o in bpy.data.objects: # collision hulls never render if o.name.startswith('UCX_'): o.hide_render = True; o.hide_viewport = True if MODE == 'views': setup_workbench() for v, (c, res) in sheet_cams().items(): render(c, res, os.path.join(PREV, 'compare', f'{PFX}_wb_{v}.png')) elif MODE in ('studio', 'water'): dark = MODE == 'water' setup_cycles(64 if not dark else 96) studio_world(0.55 if not dark else 0.6, dark) if not dark: area('Key', (6, -7, 7), 6, 900); area('Fill', (-7, -3, 4), 8, 450); area('Rim', (0, 9, 6), 6, 600) area('Under', (0, 0, -5), 8, 150) cams = sheet_cams() shots = [('side', cams['side']), ('front', cams['front']), ('top', cams['top'])] shots.append(('q_fore', (persp('CQ1', -55, 18, 11.5), (1600, 1000)))) shots.append(('q_aft', (persp('CQ2', 130, 22, 11.5), (1600, 1000)))) shots.append(('q_low', (persp('CQ3', -75, -12, 10.0, target=(0, 0, 1.2)), (1600, 1000)))) shots.append(('cabin', (persp('CQ4', -20, 12, 3.6, target=(0.1, -1.4, 1.75), lens=32), (1600, 1000)))) for name, (c, res) in shots: render(c, res, os.path.join(PREV, f'{PFX}_{MODE}_{name}.png')) elif MODE in ('concept_wb', 'concept_cy'): # the camera fitted to the user's concept (build/ae_concept_fit.py): for comparing the model with it cj = json.load(open(os.path.join(HERE, 'concept_cam.json'))) c = cam('CAM_concept', cj['location'], [math.degrees(a) for a in cj['rotation_euler_xyz']], lens=cj['lens_mm_36']) c.data.sensor_fit = 'HORIZONTAL'; c.data.sensor_width = 36.0 if MODE == 'concept_wb': setup_workbench(outline=True) else: setup_cycles(96) studio_world(0.35, dark=True) area('Key', (8, -9, 9), 5, 600); area('Rim', (-6, 8, 6), 6, 900, target=(0, 0, 1.4)) sc.render.film_transparent = True render(c, tuple(cj['resolution']), os.path.join(PREV, 'compare', f'{PFX}_{MODE}.png')) elif MODE == 'cull': # backface culling on: faces seen from behind vanish, as Unreal draws one-sided materials setup_workbench(outline=False) sc.render.film_transparent = False sh = sc.display.shading; sh.light = 'STUDIO'; sh.color_type = 'SINGLE'; sh.single_color = (0.8, 0.8, 0.8) sh.show_backface_culling = True sc.world = bpy.data.worlds.new('W') if sc.world is None else sc.world for name, args in (('q_fore', (-55, 18, 11.5)), ('q_aft', (130, 22, 11.5)), ('low', (-75, -15, 10.0)), ('q_aft_low', (150, -20, 10.0)), ('stbd_fore', (-120, 15, 11.0)), ('port_aft', (50, 10, 11.0)), ('top', (-90, 80, 11.0)), ('below', (-90, -80, 11.0))): c = persp('C' + name, *args) for cull in (True, False): sh.show_backface_culling = cull render(c, (1200, 750), os.path.join(PREV, '_cull', f'{PFX}_{name}_{int(cull)}.png')) elif MODE == 'topo': setup_workbench(outline=False) sc.render.film_transparent = False sh = sc.display.shading; sh.light = 'STUDIO'; sh.color_type = 'SINGLE'; sh.single_color = (0.8, 0.8, 0.8) for o in bpy.data.objects: if o.type == 'MESH': m = o.modifiers.new('wf', 'WIREFRAME'); m.thickness = 0.004; m.use_replace = False for name, args in (('q_fore', (-55, 18, 9.5)), ('q_aft', (130, 22, 9.5)), ('nose', (-80, 8, 5.0)), ('low', (-75, -15, 8.0))): c = persp('T' + name, *args, target=(0, -1.5 if name == 'nose' else 0, 1.4)) render(c, (1600, 1000), os.path.join(PREV, f'{PFX}_topo_{name}.png')) main()