# Orthographic comparison renders + overlays for the S-7.5 Prospector v2. # exec(open(r"D:\HADAL - The Descent\models\render_views_v2.py").read()) (set ROUND first, or edit it here) # # For each blueprint view this renders the model with an orthographic camera framed exactly like the crop in # models/reference_v2/.png (same crop box, same px/m per axis as listed in VIEWS), as white surfaces with # black Freestyle linework, and writes # renders_v2/round_.png same pixel size as the crop # renders_v2/round__x3.png 3x for reading detail # renders_v2/round_overlay_.png 3x: model lines in BLUE, blueprint lines in RED (overlap = dark) # renders_v2/round_beauty_front.png / _beauty_rear.png shaded perspective shots # Views "front_norm" / "rear_norm" give round_overlay_front_norm.png etc. (vertically normalised, see VIEWS). import bpy, os, numpy as np from math import radians from mathutils import Vector, Matrix ROUND = bpy.app.driver_namespace.get("S75V2_ROUND", 1) ONLY = bpy.app.driver_namespace.get("S75V2_ONLY", None) # e.g. ["side"] to re-render one view; None = all BEAUTY = bpy.app.driver_namespace.get("S75V2_BEAUTY", True) SHEET = r"D:\HADAL - The Descent\Assets\allinone.png" REF_DIR = r"D:\HADAL - The Descent\models\reference_v2" OUT_DIR = r"D:\HADAL - The Descent\models\renders_v2" COLL = "S75_Prospector_v2" RIG = "S75v2_RenderRig" K = 3 # supersample / detail factor # view: crop box (sheet px), origin (sheet px), px/m along image x, px/m along image y, # camera axes in world space (right, up, toward-viewer), True if image-x is the ship's length axis VIEWS = { "side": dict(box=(30, 100, 1056, 417), ox=61, oy=304, ppx=128.7, ppy=128.7, R=(0, 1, 0), U=(0, 0, 1), Z=(1, 0, 0), along=True), "top": dict(box=(30, 455, 1056, 776), ox=57, oy=621, ppx=129.3, ppy=117.5, R=(0, 1, 0), U=(-1, 0, 0), Z=(0, 0, 1), along=True), "bottom": dict(box=(30, 800, 1056, 1098), ox=57, oy=949, ppx=129.2, ppy=112.7, R=(0, 1, 0), U=(1, 0, 0), Z=(0, 0, -1), along=True), "front": dict(box=(90, 1090, 516, 1442), ox=298, oy=1307, ppx=146.2, ppy=146.2, R=(1, 0, 0), U=(0, 0, 1), Z=(0, -1, 0), along=False), "rear": dict(box=(560, 1038, 1010, 1406), ox=778, oy=1264, ppx=158.8, ppy=158.8, R=(-1, 0, 0), U=(0, 0, 1), Z=(0, 1, 0), along=False), # "normalised" front / rear (Critique ruling R2): the sheet squeezes heights in these views, so the model is # rendered with the vertical px/m that makes the side-view heights land on the drawing. "front_norm": dict(box=(90, 1108, 516, 1376), ox=298, oy=1270.3, ppx=146.2, ppy=93.3, R=(1, 0, 0), U=(0, 0, 1), Z=(0, -1, 0), along=False, noref=True), "rear_norm": dict(box=(560, 1108, 1010, 1392), ox=778, oy=1274.6, ppx=158.8, ppy=113.2, R=(-1, 0, 0), U=(0, 0, 1), Z=(0, 1, 0), along=False, noref=True), } os.makedirs(REF_DIR, exist_ok=True); os.makedirs(OUT_DIR, exist_ok=True) def load_px(path): img = bpy.data.images.load(path, check_existing=False) w, h = img.size a = np.empty(w * h * 4, dtype=np.float32); img.pixels.foreach_get(a) bpy.data.images.remove(img) return a.reshape(h, w, 4)[::-1].copy() def save_px(arr, path): h, w = arr.shape[:2] if arr.shape[2] == 3: arr = np.concatenate([arr, np.ones((h, w, 1), np.float32)], axis=2) im = bpy.data.images.new("s75v2_tmp", w, h, alpha=True) im.pixels.foreach_set(np.ascontiguousarray(arr[::-1]).astype(np.float32).ravel()) im.filepath_raw = path; im.file_format = 'PNG'; im.save(); bpy.data.images.remove(im) def resize_rows(a, h_new): h = a.shape[0] if h == h_new: return a y = (np.arange(h_new) + 0.5) * h / h_new - 0.5 y0 = np.clip(np.floor(y).astype(int), 0, h - 1); y1 = np.clip(y0 + 1, 0, h - 1) f = np.clip(y - y0, 0, 1)[:, None, None] return a[y0] * (1 - f) + a[y1] * f def box_down(a, k): h, w = a.shape[0] // k * k, a.shape[1] // k * k return a[:h, :w].reshape(h // k, k, w // k, k, a.shape[2]).mean(axis=(1, 3)) sheet = load_px(SHEET) for name, v in VIEWS.items(): # (re)write the reference crops x0, y0, x1, y1 = v["box"] if not v.get("noref"): save_px(sheet[y0:y1, x0:x1], os.path.join(REF_DIR, name + ".png")) # ----------------------------------------------------------------------------- rig scene = bpy.context.scene rig = bpy.data.collections.get(RIG) if rig: for ob in list(rig.objects): bpy.data.objects.remove(ob, do_unlink=True) else: rig = bpy.data.collections.new(RIG); scene.collection.children.link(rig) cam_data = bpy.data.cameras.get("S75v2_Cam") or bpy.data.cameras.new("S75v2_Cam") cam = bpy.data.objects.new("S75v2_Cam", cam_data); rig.objects.link(cam) scene.camera = cam for eng in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'): try: scene.render.engine = eng; break except Exception: pass scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.resolution_percentage = 100 scene.render.film_transparent = False try: scene.view_settings.view_transform = 'Standard' except Exception: pass world = bpy.data.worlds.get("S75v2_World") or bpy.data.worlds.new("S75v2_World") scene.world = world try: world.use_nodes = True except Exception: pass bg = next((n for n in world.node_tree.nodes if n.type == 'BACKGROUND'), None) white = bpy.data.materials.get("S75v2_LineWhite") or bpy.data.materials.new("S75v2_LineWhite") try: white.use_nodes = True except Exception: pass nt = white.node_tree; nt.nodes.clear() em = nt.nodes.new("ShaderNodeEmission"); em.inputs[0].default_value = (1, 1, 1, 1); em.inputs[1].default_value = 1.0 mo = nt.nodes.new("ShaderNodeOutputMaterial"); nt.links.new(em.outputs[0], mo.inputs[0]) vl = bpy.context.view_layer def setup_lines(on): scene.render.use_freestyle = on vl.use_freestyle = on vl.material_override = white if on else None if bg: bg.inputs[0].default_value = (1, 1, 1, 1) if on else (0.55, 0.62, 0.70, 1); bg.inputs[1].default_value = 1.0 if on else 0.9 if on: fs = vl.freestyle_settings fs.crease_angle = radians(140) while len(fs.linesets) > 0: fs.linesets.remove(fs.linesets[0]) ls = fs.linesets.new("S75v2_Lines") ls.select_silhouette = True; ls.select_border = True; ls.select_crease = True ls.select_material_boundary = True; ls.select_external_contour = True; ls.select_contour = True ls.select_edge_mark = False ls.linestyle.thickness = 2.2 ls.linestyle.color = (0, 0, 0) scene.render.line_thickness_mode = 'ABSOLUTE' def render_view(name): v = VIEWS[name] x0, y0, x1, y1 = v["box"]; W, H = x1 - x0, y1 - y0 rc = ((x0 + x1) / 2 - v["ox"]) / v["ppx"] - (3.75 if v["along"] else 0.0) uc = (v["oy"] - (y0 + y1) / 2) / v["ppy"] Rv, Uv, Zv = Vector(v["R"]), Vector(v["U"]), Vector(v["Z"]) M = Matrix((Rv, Uv, Zv)).transposed().to_4x4() M.translation = Rv * rc + Uv * uc + Zv * 25.0 cam.matrix_world = M cam_data.type = 'ORTHO'; cam_data.sensor_fit = 'HORIZONTAL' cam_data.ortho_scale = W / v["ppx"] cam_data.clip_start = 0.1; cam_data.clip_end = 60 h_iso = int(round(H / v["ppy"] * v["ppx"] * K)) scene.render.resolution_x = W * K; scene.render.resolution_y = h_iso tmp = os.path.join(OUT_DIR, "_tmp_%s.png" % name) scene.render.filepath = tmp bpy.ops.render.render(write_still=True) a = load_px(tmp)[:, :, :3] os.remove(tmp) a = resize_rows(a, H * K) save_px(a, os.path.join(OUT_DIR, "round%d_%s_x3.png" % (ROUND, name))) save_px(box_down(a, K), os.path.join(OUT_DIR, "round%d_%s.png" % (ROUND, name))) # overlay: model lines blue, blueprint lines red ref = sheet[y0:y1, x0:x1, :3] ref = np.repeat(np.repeat(ref, K, axis=0), K, axis=1) md = np.clip(1.0 - a.mean(axis=2), 0, 1)[:, :, None] bd = np.clip((0.85 - ref.mean(axis=2)) / 0.6, 0, 1)[:, :, None] out = 1.0 - md * (1 - np.array((0.05, 0.25, 1.0), np.float32)) - bd * (1 - np.array((1.0, 0.1, 0.05), np.float32)) save_px(np.clip(out, 0, 1), os.path.join(OUT_DIR, "round%d_overlay_%s.png" % (ROUND, name))) setup_lines(True) for name in VIEWS: if ONLY is None or name in ONLY: render_view(name) # ----------------------------------------------------------------------------- beauty shots if BEAUTY: setup_lines(False) for nm, rot, en in (("S75v2_Key", (50, 0, 35), 2.4), ("S75v2_Fill", (65, 0, -140), 1.0)): ld = bpy.data.lights.get(nm) or bpy.data.lights.new(nm, 'SUN') ld.energy = en lo = bpy.data.objects.new(nm, ld); rig.objects.link(lo) lo.rotation_euler = [radians(a) for a in rot] for vt in ('AgX', 'Filmic'): try: scene.view_settings.view_transform = vt; break except Exception: pass cam_data.type = 'PERSP'; cam_data.lens = 45; cam_data.sensor_fit = 'AUTO' scene.render.resolution_x = 1600; scene.render.resolution_y = 1000 def look(frm, to): d = (Vector(to) - Vector(frm)).normalized() cam.matrix_world = Matrix.Translation(Vector(frm)) @ d.to_track_quat('-Z', 'Y').to_matrix().to_4x4() for nm, frm, to in (("beauty_front", (5.0, -6.6, 2.6), (0, 0.1, 0.1)), ("beauty_rear", (-4.8, 7.2, 2.9), (0, 0.5, 0.1))): look(frm, to) scene.render.filepath = os.path.join(OUT_DIR, "round%d_%s.png" % (ROUND, nm)) bpy.ops.render.render(write_still=True) try: scene.view_settings.view_transform = 'Standard' except Exception: pass setup_lines(True)