# Captain's quarters for the CANTICLE -- full build. blender -b --factory-startup --python build_quarters.py # Reference: the user's picture of a black cabin with a padded bunk alcove, a window on the left and a chair. # Room frame: +X along the bunk wall (to the right when facing the bunk), +Y into the bunk wall, Z up, metres. # Origin: floor centre of the 7.2 x 4.4 footprint (the size of "Quarters A" in the vessel interior). import bpy, bmesh, os, sys, math, json, random from mathutils import Vector, Matrix HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) sys.path.insert(0, HERE) import importlib, cq_lib importlib.reload(cq_lib) from cq_lib import Frame, Part, rect, shape, circle, offset, get_coll, audit TEX = os.path.join(ROOT, 'textures') OUT = os.path.join(ROOT, 'CaptainsQuarters.blend') HX, YD, YB, YO, H = 3.6, -2.2, 1.10, 2.2, 2.6 T, G = 0.014, 0.006 # plate thickness, gap between plates FB = Frame((-HX, YB, 0), (1, 0, 0), (0, 0, 1)) # bunk wall (w = -Y) FW = Frame((-HX, YD, 0), (0, 1, 0), (0, 0, 1)) # window wall (w = +X) FD = Frame((HX, YD, 0), (-1, 0, 0), (0, 0, 1)) # door wall (w = +Y) FL = Frame((HX, YB, 0), (0, -1, 0), (0, 0, 1)) # locker wall (w = -X) FF = Frame((-HX, YD, 0), (1, 0, 0), (0, 1, 0)) # floor (w = +Z) FC = Frame((-HX, YB, H), (1, 0, 0), (0, -1, 0)) # ceiling (w = -Z) DEPTH = YB - YD # 3.3 SAT, MAT_, BACK, GUN, STEEL, FLOOR = 'M_CQ_Satin', 'M_CQ_Matte', 'M_CQ_Backing', 'M_CQ_Gunmetal', 'M_CQ_Steel', 'M_CQ_Floor' QUILT, SHEET, MATT, GLASS, SCREEN, DECAL = 'M_CQ_Quilt', 'M_CQ_Sheet', 'M_CQ_Mattress', 'M_CQ_Glass', 'M_CQ_Screen', 'M_CQ_Decals' LWARM, LCOOL, LRED, LGREEN, LWIN, WELL, GRILLE = ('M_CQ_LightWarm', 'M_CQ_LightCool', 'M_CQ_LightRed', 'M_CQ_LightGreen', 'M_CQ_Window', 'M_CQ_LightWell', 'M_CQ_Grille') # decal atlas cells (u0, v0, u1, v1), written by make_textures.py ATLAS = {} _ap = os.path.join(TEX, 'atlas.json') if os.path.exists(_ap): ATLAS = json.load(open(_ap)) # ------------------------------------------------------------------------------------------------ materials def tex(name, colorspace='sRGB'): p = os.path.join(TEX, name) if not os.path.exists(p): return None img = bpy.data.images.load(p, check_existing=True) img.colorspace_settings.name = colorspace return img def MAT(name, color=(0.5, 0.5, 0.5), metal=0.0, rough=0.5, emit=None, emit_str=0.0, alpha=1.0, sheen=0.0, coat=0.0, transmission=0.0, bc=None, nrm=None, rgh=None, nrm_str=1.0, scale=1.0, rgh_range=None, emit_tex=None, alpha_tex=False, bc_mix=1.0): m = bpy.data.materials.new(name) try: m.use_nodes = True except Exception: pass nt = m.node_tree b = next((n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED'), None) if b is None: nt.nodes.clear() b = nt.nodes.new('ShaderNodeBsdfPrincipled') o = nt.nodes.new('ShaderNodeOutputMaterial') nt.links.new(b.outputs[0], o.inputs[0]) I = b.inputs I['Base Color'].default_value = (*color, 1) I['Metallic'].default_value = metal I['Roughness'].default_value = rough I['Alpha'].default_value = alpha for k, v in (('Sheen Weight', sheen), ('Coat Weight', coat), ('Transmission Weight', transmission)): if k in I: I[k].default_value = v if emit: I['Emission Color'].default_value = (*emit, 1) I['Emission Strength'].default_value = emit_str mp = None def mapping(): nonlocal mp if mp is None: tc = nt.nodes.new('ShaderNodeTexCoord'); mp = nt.nodes.new('ShaderNodeMapping') mp.inputs['Scale'].default_value = (scale, scale, scale) nt.links.new(tc.outputs['UV'], mp.inputs['Vector']) return mp def img_node(image): n = nt.nodes.new('ShaderNodeTexImage'); n.image = image nt.links.new(mapping().outputs['Vector'], n.inputs['Vector']) return n if bc: im = tex(bc) if im: n = img_node(im) if bc_mix >= 1.0: nt.links.new(n.outputs['Color'], I['Base Color']) else: # multiply the flat colour by the texture mx = nt.nodes.new('ShaderNodeMix'); mx.data_type = 'RGBA'; mx.blend_type = 'MULTIPLY' mx.inputs[0].default_value = 1.0 mx.inputs[6].default_value = (*color, 1) nt.links.new(n.outputs['Color'], mx.inputs[7]) nt.links.new(mx.outputs[2], I['Base Color']) if alpha_tex: nt.links.new(n.outputs['Alpha'], I['Alpha']) if emit_tex: nt.links.new(n.outputs['Color'], I['Emission Color']) I['Emission Strength'].default_value = emit_str if rgh: im = tex(rgh, 'Non-Color') if im: n = img_node(im) lo, hi = rgh_range or (rough - 0.12, rough + 0.12) mr = nt.nodes.new('ShaderNodeMapRange') mr.inputs['To Min'].default_value = lo; mr.inputs['To Max'].default_value = hi nt.links.new(n.outputs['Color'], mr.inputs['Value']) nt.links.new(mr.outputs['Result'], I['Roughness']) if nrm: im = tex(nrm, 'Non-Color') if im: n = img_node(im) nm = nt.nodes.new('ShaderNodeNormalMap'); nm.inputs['Strength'].default_value = nrm_str nt.links.new(n.outputs['Color'], nm.inputs['Color']) nt.links.new(nm.outputs['Normal'], I['Normal']) if alpha < 1.0 or alpha_tex: try: m.surface_render_method = 'BLENDED' except Exception: pass m.use_backface_culling = False return m def make_materials(): MAT(SAT, (0.018, 0.019, 0.022), 0.0, 0.34, rgh='T_CQ_Panel_R.png', rgh_range=(0.29, 0.40), scale=0.7) MAT(MAT_, (0.010, 0.0105, 0.012), 0.0, 0.60, rgh='T_CQ_Panel_R.png', rgh_range=(0.54, 0.66), scale=0.7) MAT(BACK, (0.006, 0.006, 0.007), 0.0, 0.85) MAT(GUN, (0.15, 0.158, 0.172), 1.0, 0.36, rgh='T_CQ_Panel_R.png', rgh_range=(0.30, 0.42), scale=1.0) MAT(STEEL, (0.62, 0.63, 0.65), 1.0, 0.24, rgh='T_CQ_Panel_R.png', rgh_range=(0.16, 0.38), scale=2.0) MAT(FLOOR, (0.014, 0.0148, 0.017), 0.0, 0.28, rgh='T_CQ_Panel_R.png', rgh_range=(0.23, 0.34), scale=0.5) MAT(QUILT, (0.012, 0.012, 0.0135), 0.0, 0.62, sheen=0.08, nrm='T_CQ_Quilt_N.png', nrm_str=1.0) MAT(SHEET, (0.026, 0.027, 0.030), 0.0, 0.78, sheen=0.15, bc='T_CQ_Camo_BC.png', bc_mix=0.5, nrm='T_CQ_Fabric_N.png', nrm_str=0.3, scale=1.6) MAT(MATT, (0.016, 0.016, 0.018), 0.0, 0.80, sheen=0.1, nrm='T_CQ_Fabric_N.png', nrm_str=0.12, scale=1.0) MAT(GLASS, (0.02, 0.03, 0.035), 0.0, 0.03, alpha=0.18) MAT(SCREEN, (0.004, 0.005, 0.006), 0.0, 0.30, emit=(1, 1, 1), emit_str=3.0, bc='T_CQ_Screens.png', emit_tex=True) MAT(DECAL, (1, 1, 1), 0.0, 0.5, bc='T_CQ_Decals.png', alpha_tex=True) MAT(LWARM, (1.0, 0.86, 0.66), 0.0, 0.4, emit=(1.0, 0.80, 0.56), emit_str=14.0) MAT(LCOOL, (0.9, 0.95, 1.0), 0.0, 0.4, emit=(0.86, 0.93, 1.0), emit_str=3.0) MAT(LRED, (1.0, 0.10, 0.06), 0.0, 0.4, emit=(1.0, 0.06, 0.03), emit_str=10.0) MAT(LGREEN, (0.2, 1.0, 0.5), 0.0, 0.4, emit=(0.15, 1.0, 0.45), emit_str=6.0) MAT(LWIN, (0.9, 0.95, 1.0), 0.0, 0.5, emit=(0.90, 0.95, 1.0), emit_str=9.0) MAT(WELL, (0.75, 0.77, 0.80), 0.0, 0.5) MAT(GRILLE, (0.012, 0.012, 0.013), 0.3, 0.55, nrm='T_CQ_Perf_N.png', nrm_str=1.0, bc='T_CQ_Perf_BC.png', scale=40.0) # ------------------------------------------------------------------------------------------------ helpers def backing(part, fr, u0, v0, u1, v1, holes=(), mat=BACK, w=0.0): """Dark surface behind the plates, tiled round rectangular holes (no boolean).""" us = sorted({u0, u1, *[h[0] for h in holes], *[h[2] for h in holes]}) vs = sorted({v0, v1, *[h[1] for h in holes], *[h[3] for h in holes]}) cache = {} def vert(u, v): k = (round(u, 5), round(v, 5)) if k not in cache: cache[k] = part.bm.verts.new(fr.p(u, v, w)) return cache[k] mi = part.mi(mat) for i in range(len(us) - 1): for j in range(len(vs) - 1): cu = (us[i] + us[i + 1]) / 2; cv = (vs[j] + vs[j + 1]) / 2 if any(h[0] < cu < h[2] and h[1] < cv < h[3] for h in holes): continue f = part.bm.faces.new((vert(us[i], vs[j]), vert(us[i + 1], vs[j]), vert(us[i + 1], vs[j + 1]), vert(us[i], vs[j + 1]))) f.material_index = mi def plates(part, fr, cells, mat=SAT, t=T, c=0.003, gap=G, w0=0.0): """cells: (u0, v0, u1, v1[, mat[, thickness]]); each becomes a plate with half a gap all round.""" g = gap / 2 for cell in cells: u0, v0, u1, v1 = cell[:4] m = cell[4] if len(cell) > 4 and cell[4] else mat tt = cell[5] if len(cell) > 5 else t part.prism(fr, rect(u0 + g, v0 + g, u1 - g, v1 - g), w0, w0 + tt, m, c=c) def cells_grid(us, vs, mat=None, t=None): out = [] for i in range(len(us) - 1): for j in range(len(vs) - 1): c = [us[i], vs[j], us[i + 1], vs[j + 1]] if mat or t: c += [mat, t if t else T] out.append(tuple(c)) return out def slats(part, fr, u0, v0, u1, v1, n, w0, w1, mat, horizontal=True, fill=0.5): """n thin bars across a rectangle (vent).""" for i in range(n): if horizontal: h = (v1 - v0) / n part.box(fr, u0, v0 + h * i + h * (1 - fill) / 2, u1, v0 + h * i + h * (1 + fill) / 2, w0, w1, mat, c=0.0015) else: h = (u1 - u0) / n part.box(fr, u0 + h * i + h * (1 - fill) / 2, v0, u0 + h * i + h * (1 + fill) / 2, v1, w0, w1, mat, c=0.0015) def at(name, default=(0, 0, 1, 1)): return ATLAS.get(name, default) def quilt_h(cols, rows, puff, seed): rnd = random.Random(seed) amp = [[rnd.uniform(0.80, 1.15) for _ in range(cols)] for _ in range(rows)] sk = [[(rnd.uniform(-0.15, 0.15), rnd.uniform(-0.15, 0.15)) for _ in range(cols)] for _ in range(rows)] def h(a, b): x = a * cols; y = b * rows i = min(int(x), cols - 1); j = min(int(y), rows - 1) fx = min(max(x - i, 0.0), 1.0); fy = min(max(y - j, 0.0), 1.0) sx, sy = sk[j][i] px = math.sin(math.pi * fx ** (1 + sx)); py = math.sin(math.pi * fy ** (1 + sy)) return 0.004 + puff * amp[j][i] * (max(px, 0.0) ** 0.55) * (max(py, 0.0) ** 0.55) return h def quilt(part, fr, u0, v0, u1, v1, cols, rows, puff=0.04, seed=1): part.grid(fr, u0, v0, u1, v1, cols * 8, rows * 6, quilt_h(cols, rows, puff, seed), QUILT, uvfun=lambda a, b: (a * cols / 3.0, b * rows / 3.0)) # ------------------------------------------------------------------------------------------------ bunk wall BZ = (0.50, 0.30, 3.02, 2.06) # bezel outer (u = x + 3.6, v = z) OP = (0.66, 0.46, 2.86, 1.90) # opening NI = (0.63, 0.46, 2.89, 1.93) # niche interior DKO = (4.42, 0.62, 6.78, 2.04) # desk nook bezel outer DKH = (4.52, 0.72, 6.68, 1.94) # desk nook opening DKN = (4.50, 0.72, 6.70, 1.96) # desk nook interior NICHE_D, DESK_D = 1.00, 0.50 def wall_bunk(P): backing(P, FB, 0, 0, 7.2, H, holes=[(OP[0] - 0.02, OP[1] - 0.02, OP[2] + 0.02, OP[3] + 0.02), (DKH[0] - 0.02, DKH[1] - 0.02, DKH[2] + 0.02, DKH[3] + 0.02)]) # --- base plates cells = [] cells += [(0.46, 0.08, 1.32, 0.29, MAT_, 0.022), (1.32, 0.08, 2.20, 0.29, MAT_, 0.022), (2.20, 0.08, 3.06, 0.29, MAT_, 0.022)] cells += [(0.46, 0.29, 0.64, 2.08), (2.88, 0.29, 3.06, 2.08), (0.64, 0.29, 2.88, 0.44), (0.64, 1.92, 2.88, 2.08)] cells += [(0.46, 2.08, 3.06, 2.57, MAT_)] cells += cells_grid([3.06, 3.73, 4.40], [0.08, 0.62, 1.17]) cells += cells_grid([3.06, 3.73, 4.40], [1.53, 2.05, 2.57]) cells += cells_grid([4.40, 5.20, 6.00, 6.80], [0.08, 0.60]) cells += [(4.40, 0.60, 4.50, 2.06), (6.70, 0.60, 6.80, 2.06), (4.50, 0.60, 6.70, 0.70), (4.50, 1.96, 6.70, 2.06)] cells += cells_grid([4.40, 5.60, 6.80], [2.06, 2.57]) plates(P, FB, cells) # --- pillar on the left with a handle slot P.ring(FB, shape(0.02, T + 0.001, 0.44, H - T - 0.001, r=0.014, seg=3), shape(0.335, 0.95, 0.379, 1.75, r=0.021, seg=3), 0.0, 0.08, SAT, c=0.008, segs=2, reveal_w=0.045, reveal_mat=MAT_, floor_mat=MAT_) P.box(FB, 0.08, 0.22, 0.28, 0.80, 0.08, 0.087, MAT_, c=0.003, r=0.025, seg=3) P.box(FB, 0.08, 1.92, 0.28, 2.40, 0.08, 0.087, MAT_, c=0.003, r=0.025, seg=3) P.box(FB, 0.145, 1.50, 0.215, 1.512, 0.08, 0.083, LRED, c=0.001) # --- header over the alcove: a bridge-shaped plate with two vent cartridges in its notch hd = [(0.46, 2.10), (0.90, 2.10), (0.90, 2.26), (0.98, 2.34), (2.12, 2.34), (2.20, 2.26), (2.20, 2.10), (3.06, 2.10), (3.06, 2.49), (2.98, 2.57), (0.54, 2.57), (0.46, 2.49)] P.prism(FB, hd, T, 0.050, SAT, c=0.008, segs=2) for u0 in (1.00, 1.58): P.box(FB, u0, 2.135, u0 + 0.50, 2.305, T, 0.040, GUN, c=0.006, segs=2, r=0.02, seg=3) slats(P, FB, u0 + 0.04, 2.16, u0 + 0.36, 2.28, 5, 0.040, 0.045, MAT_) P.box(FB, u0 + 0.40, 2.212, u0 + 0.46, 2.228, 0.040, 0.043, LRED, c=0.001) # --- the alcove's surround and its tunnel through the wall Pi = shape(*OP, r=0.11, seg=8) # collar: a second, flatter frame behind the surround, top-left corner cut at 45 degrees as in the picture P.ring(FB, shape(0.455, 0.295, 3.065, 2.075, bl=-0.02, br=-0.02, tr=-0.02, tl=-0.17), shape(0.60, 0.40, 2.92, 1.96, r=-0.03), T, 0.034, MAT_, c=0.005, reveal_w=T) P.ring(FB, shape(*BZ, r=0.25, seg=8), Pi, 0.034, 0.090, SAT, c=0.016, segs=3, ci=0.030, reveal_w=-0.10) Pn = shape(*NI, r=0.012, seg=8) P.bridge(FB, Pi, -0.10, Pn, -0.10, MAT_) # back of the lip, seen from the bed P.tunnel(FB, Pn, -0.10, -NICHE_D, MAT_, back_mat=MAT_) # --- section right of the alcove: equipment rail and the wall unit P.prism(FB, shape(3.70, 1.20, 4.40, 1.50, br=-0.05, tr=-0.05), 0.0, 0.050, MAT_, c=0.006, segs=2) P.box(FB, 3.76, 1.325, 4.30, 1.375, 0.050, 0.056, GUN, c=0.002) P.ring(FB, shape(3.10, 1.18, 3.68, 1.52, r=0.03, seg=4), shape(3.39, 1.22, 3.63, 1.48, r=0.02, seg=4), 0.0, 0.17, GUN, c=0.012, segs=2, reveal_w=0.158, reveal_mat=MAT_, floor_mat=GRILLE) P.box(FB, 3.15, 1.37, 3.34, 1.47, 0.17, 0.173, MAT_, c=0.001, r=0.01, seg=3) for i, m in enumerate((MAT_, MAT_, LGREEN)): P.prism(FB, circle(3.175 + 0.055 * i, 1.27, 0.014 if i < 2 else 0.006, 12), 0.17, 0.175, m, c=0.002) # --- desk nook: surround, tunnel, lining Ph = shape(*DKH, r=0.07, seg=6) P.ring(FB, shape(*DKO, r=0.14, seg=6), Ph, T, 0.058, SAT, c=0.012, segs=2, reveal_w=-0.06) Pd = shape(*DKN, r=0.012, seg=6) P.bridge(FB, Ph, -0.06, Pd, -0.06, MAT_) P.tunnel(FB, Pd, -0.06, -DESK_D, MAT_, back_mat=MAT_) Fk = FB.at(dw=-DESK_D) P.ring(Fk, shape(4.98, 1.00, 6.22, 1.64, r=0.03, seg=4), shape(5.01, 1.03, 6.19, 1.61, r=0.015, seg=4), 0.0, 0.035, MAT_, c=0.004, reveal_w=0.031, floor_mat=BACK) x0, y0, x1, y1 = at('screen_desk') # the screen stays inside the frame's rounded corners P.quad([Fk.p(5.016, 1.036, 0.0315), Fk.p(6.184, 1.036, 0.0315), Fk.p(6.184, 1.604, 0.0315), Fk.p(5.016, 1.604, 0.0315)], SCREEN, uvs=[(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) P.box(Fk, 4.56, 1.735, 6.64, 1.765, 0.0, 0.20, SAT, c=0.006, segs=2) # shelf P.box(Fk, 4.70, 1.905, 6.50, 1.925, 0.0, 0.022, LWARM, c=0.002) # light bar under the nook's top P.box(Fk, 4.56, 0.76, 4.86, 1.50, 0.0, 0.03, SAT, c=0.004, r=0.02, seg=3) # side panel left P.box(Fk, 6.34, 0.76, 6.64, 1.50, 0.0, 0.03, SAT, c=0.004, r=0.02, seg=3) slats(P, Fk, 4.60, 0.84, 4.82, 1.10, 6, 0.03, 0.034, MAT_) # desk slab, resting in the opening and reaching 0.44 m into the room Fs = Frame((-HX, YB, 0.72), (1, 0, 0), (0, 1, 0)) P.prism(Fs, shape(4.60, -0.44, 6.60, 0.46, bl=0.08, br=0.08, seg=4), 0.0, 0.04, SAT, c=0.008, segs=2, back=True) # --- column in the far corner P.box(FB, 6.86, T + 0.001, 7.18, H - T - 0.001, 0.0, 0.05, SAT, c=0.008, segs=2, r=0.014, seg=3) P.box(FB, 6.92, 1.86, 7.12, 2.34, 0.05, 0.056, MAT_, c=0.002, r=0.02, seg=3) slats(P, FB, 6.94, 1.90, 7.10, 2.30, 9, 0.056, 0.060, GUN) def bunk_interior(P): # padded back wall, corner posts, light cove Fq = FB.at(dw=-NICHE_D) quilt(P, Fq, NI[0] + 0.036, 0.50, NI[2] - 0.036, 1.80, 9, 9, puff=0.050, seed=7) for u0 in (NI[0], NI[2] - 0.035): P.box(Fq, u0, 0.475, u0 + 0.035, 1.80, 0.0, 0.035, MAT_, c=0.006, segs=2) # starts above the tunnel's corner fillet P.box(Fq, NI[0] + 0.001, 1.80, NI[2] - 0.001, 1.917, 0.0, 0.085, SAT, c=0.008, segs=2) P.box(Fq, 0.80, 1.825, 2.72, 1.853, 0.085, 0.093, LWARM, c=0.002) # left side wall: a lining with an inset hatch Fl = Frame((-HX + NI[0], YB + 0.10, 0), (0, 1, 0), (0, 0, 1)) # w = +X P.ring(Fl, shape(0.04, 0.72, 0.84, 1.78, r=0.03, seg=4), shape(0.22, 1.26, 0.66, 1.70, r=0.045, seg=4), 0.0, 0.022, SAT, c=0.004, reveal_w=0.008, floor_mat=MAT_) P.box(Fl, 0.36, 1.04, 0.52, 1.075, 0.022, 0.034, GUN, c=0.004, r=0.012, seg=3) P.box(Fl, 0.22, 0.80, 0.66, 0.96, 0.022, 0.027, MAT_, c=0.002, r=0.02, seg=3) # right side wall: a control panel with a small screen and a reading light Fr = Frame((-HX + NI[2], YB + NICHE_D, 0), (0, -1, 0), (0, 0, 1)) # w = -X P.box(Fr, 0.10, 0.72, 0.86, 1.78, 0.0, 0.022, SAT, c=0.004, r=0.03, seg=4) P.ring(Fr, shape(0.26, 1.28, 0.66, 1.56, r=0.02, seg=3), shape(0.285, 1.305, 0.635, 1.535, r=0.01, seg=3), 0.022, 0.034, MAT_, c=0.003, reveal_w=0.030, floor_mat=BACK) x0, y0, x1, y1 = at('screen_bunk') P.quad([Fr.p(0.290, 1.310, 0.0305), Fr.p(0.630, 1.310, 0.0305), Fr.p(0.630, 1.530, 0.0305), Fr.p(0.290, 1.530, 0.0305)], SCREEN, uvs=[(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) for i in range(3): P.prism(Fr, circle(0.32 + 0.07 * i, 1.19, 0.016, 12), 0.022, 0.028, GUN, c=0.002) P.prism(Fr, circle(0.46, 1.67, 0.045, 16), 0.022, 0.040, GUN, c=0.006, segs=2) P.prism(Fr, circle(0.46, 1.67, 0.030, 16), 0.040, 0.042, LWARM, c=0.001) # ------------------------------------------------------------------------------------------------ window wall WO = (0.30, 0.26, 3.06, 2.50) WH = (0.52, 0.48, 2.84, 2.28) WM = (0.585, 0.545, 2.775, 2.215) def wall_window(P): backing(P, FW, 0, 0, DEPTH, H, holes=[(WH[0] - 0.02, WH[1] - 0.02, WH[2] + 0.02, WH[3] + 0.02)]) cells = [(0.02, 0.08, 0.28, 2.57), (3.08, 0.08, 3.20, 2.57), (0.28, 2.50, 3.08, 2.57), (0.28, 0.08, 3.08, 0.26, MAT_, 0.02), (0.28, 0.26, 0.50, 2.50), (2.86, 0.26, 3.08, 2.50), (0.50, 0.26, 2.86, 0.46), (0.50, 2.30, 2.86, 2.50)] plates(P, FW, cells) slats(P, FW, 0.60, 0.105, 2.76, 0.235, 5, 0.02, 0.026, GUN) Pi = shape(*WH, r=0.16, seg=8) P.ring(FW, shape(*WO, r=0.30, seg=8), Pi, T, 0.095, SAT, c=0.02, segs=3, reveal_w=-0.22) # inner frame carrying the glass Pm = shape(*WM, r=0.10, seg=8) P.ring(FW, Pi, Pm, -0.12, -0.085, MAT_, c=0.006, segs=1, reveal_w=-0.12, outer=False, back=True) P._cap(P._ring(FW, Pm, -0.10), GLASS) # light well behind the glass (in the game: the sea, or a screen) P.tunnel(FW, Pi, -0.22, -0.46, WELL, back_mat=LWIN) # ------------------------------------------------------------------------------------------------ door wall DOOR = (4.90, 0.0, 5.90, 2.10) def wall_door(P): backing(P, FD, 0, 0, 7.2, H, holes=[(DOOR[0], DOOR[1], DOOR[2], DOOR[3])]) cells = [] cells += cells_grid([0.02, 0.85, 2.05, 3.25, 3.80, 4.68], [0.08, 1.18]) cells += cells_grid([0.02, 0.85, 3.25, 3.80, 4.68], [1.18, 2.22]) cells += cells_grid([0.02, 2.05, 3.80, 4.68], [2.22, 2.57]) cells += cells_grid([6.12, 7.18], [0.08, 1.18, 2.22, 2.57]) cells += [(4.68, 2.32, 6.12, 2.57)] plates(P, FD, cells) z0 = T + 0.001 fr_ = [(4.70, z0), (4.90, z0), (4.90, 2.02), (4.98, 2.10), (5.82, 2.10), (5.90, 2.02), (5.90, z0), (6.10, z0), (6.10, 2.16), (5.96, 2.30), (4.84, 2.30), (4.70, 2.16)] P.prism(FD, fr_, 0.0, 0.06, SAT, c=0.012, segs=2) # reveal of the doorway through the wall dj = [(4.90, 0.0), (4.90, 2.02), (4.98, 2.10), (5.82, 2.10), (5.90, 2.02), (5.90, 0.0)] for a, b in zip(dj[:-1], dj[1:]): P.quad([FD.p(a[0], a[1], 0.0), FD.p(a[0], a[1], -0.03), FD.p(b[0], b[1], -0.03), FD.p(b[0], b[1], 0.0)], MAT_) P.box(FD, 5.20, 2.175, 5.60, 2.225, 0.06, 0.066, MAT_, c=0.002, r=0.012, seg=3) P.box(FD, 5.36, 2.193, 5.44, 2.207, 0.066, 0.068, LGREEN, c=0.0008) # control pad P.box(FD, 4.50, 1.16, 4.62, 1.38, T, 0.036, GUN, c=0.005, segs=2, r=0.015, seg=3) P.box(FD, 4.52, 1.27, 4.60, 1.36, 0.036, 0.038, LGREEN, c=0.001, r=0.006, seg=3) for i in range(2): P.prism(FD, circle(4.54 + 0.04 * i, 1.21, 0.012, 10), 0.036, 0.040, MAT_, c=0.0015) # chart display over the bench P.ring(FD, shape(0.91, 1.26, 3.19, 2.08, r=0.05, seg=4), shape(0.98, 1.33, 3.12, 2.01, r=0.025, seg=4), T, 0.045, SAT, c=0.008, segs=2, reveal_w=0.030, floor_mat=BACK) x0, y0, x1, y1 = at('screen_chart') P.quad([FD.p(0.989, 1.339, 0.0305), FD.p(3.111, 1.339, 0.0305), FD.p(3.111, 2.001, 0.0305), FD.p(0.989, 2.001, 0.0305)], SCREEN, uvs=[(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) # bench: plinth, body, two cushions, padded back P.box(FD, 0.91, T + 0.001, 3.19, 0.10, T, 0.54, MAT_, c=0.004) P.box(FD, 0.85, 0.10, 3.25, 0.36, T, 0.60, SAT, c=0.012, segs=2) Fs = Frame(FD.p(0, 0.36, 0), FD.w, FD.u) # u = depth, v = along, w = +Z for v0 in (0.87, 2.06): P.prism(Fs, shape(0.10, v0, 0.59, v0 + 1.17, r=0.035, seg=4), 0.0, 0.10, MATT, c=0.03, segs=3) quilt(P, FD.at(dw=0.03), 0.871, 0.501, 3.229, 1.039, 9, 4, puff=0.040, seed=11) P.box(FD, 0.85, 0.46, 3.25, 0.499, T, 0.05, SAT, c=0.004) P.box(FD, 0.85, 1.041, 3.25, 1.085, T, 0.06, SAT, c=0.006, segs=2) for u0 in (0.85, 3.231): P.box(FD, u0, 0.50, u0 + 0.019, 1.04, T, 0.05, SAT, c=0.003) P.box(FD, 0.87, 0.50, 3.23, 1.04, T, 0.03, MAT_, c=0.0) # board behind the padding def door_leaf(P): """Sliding leaf, built at the origin of its own object: bottom centre, back face on the wall plane.""" F = Frame((0.56, 0, 0), (-1, 0, 0), (0, 0, 1)) # same facing as the door wall P.prism(F, [(0.0, 0.0), (1.12, 0.0), (1.12, 2.06), (1.02, 2.16), (0.10, 2.16), (0.0, 2.06)], 0.0, 0.04, SAT, c=0.004, back=True) P.prism(F, [(0.08, 0.10), (1.04, 0.10), (1.04, 0.98), (0.92, 1.10), (0.08, 1.10)], 0.04, 0.052, MAT_, c=0.004) P.prism(F, [(0.08, 1.16), (0.90, 1.16), (1.04, 1.02 + 0.02), (1.04, 2.00), (0.98, 2.06), (0.14, 2.06), (0.08, 2.00)], 0.04, 0.052, MAT_, c=0.004) P.ring(F, shape(0.16, 1.24, 0.44, 1.62, r=0.03, seg=3), shape(0.20, 1.28, 0.40, 1.58, r=0.02, seg=3), 0.052, 0.062, GUN, c=0.004, reveal_w=0.054, floor_mat=MAT_) P.box(F, 0.27, 1.34, 0.33, 1.52, 0.054, 0.066, STEEL, c=0.004, r=0.012, seg=3) P.box(F, 0.62, 1.60, 0.92, 1.615, 0.052, 0.054, LRED, c=0.0008) # ------------------------------------------------------------------------------------------------ locker wall LK = [(0.62, 1.42), (1.43, 2.23), (2.24, 3.04)] def wall_locker(P): backing(P, FL, 0, 0, DEPTH, H) cells = [(0.06, 0.08, 0.61, 1.02), (0.06, 1.02, 0.61, 1.70), (0.06, 1.70, 0.61, 2.57), (3.05, 0.08, 3.28, 2.57), (0.61, 2.26, 3.05, 2.57), (0.61, 0.02, 3.05, 0.09, MAT_, 0.03)] plates(P, FL, cells) # locker carcass edges between the doors for u in (0.615, 1.425, 2.235, 3.045): P.box(FL, u - 0.004, 0.09, u + 0.004, 2.26, 0.0, 0.03, MAT_, c=0.0) slats(P, FL, 0.82, 2.34, 2.84, 2.50, 6, T, T + 0.006, GUN) # emergency kit box, on the bunk-wall side so that every locker door can swing 90 degrees P.box(FL, 0.14, 1.10, 0.50, 1.62, T, 0.13, GUN, c=0.012, segs=2, r=0.03, seg=4) P.box(FL, 0.18, 1.14, 0.46, 1.58, 0.13, 0.136, MAT_, c=0.003, r=0.02, seg=3) P.box(FL, 0.285, 1.40, 0.355, 1.44, 0.136, 0.150, STEEL, c=0.004, r=0.01, seg=3) P.box(FL, 0.22, 1.18, 0.42, 1.195, 0.136, 0.138, LRED, c=0.0008) def locker_door(P, width=0.80): """Hinged door built at its own origin: the hinge line is its front edge on the bunk-wall side, so the leaf never swings back into the carcass. Opens by turning to -90 degrees about Z.""" F = Frame((0, 0, 0), (0, -1, 0), (0, 0, 1)).at(dw=-0.03) # same facing as the locker wall P.ring(F, shape(0.004, 0.0, width - 0.004, 2.15, r=0.015, seg=3), shape(width - 0.11, 0.95, width - 0.065, 1.27, r=0.022, seg=3), 0.0, 0.03, SAT, c=0.004, reveal_w=0.008, reveal_mat=MAT_, floor_mat=MAT_, back=True) P.box(F, 0.08, 1.74, width - 0.16, 2.05, 0.03, 0.034, MAT_, c=0.002, r=0.02, seg=3) slats(P, F, 0.12, 1.78, width - 0.20, 2.01, 7, 0.034, 0.037, GUN) P.box(F, 0.08, 0.10, width - 0.16, 0.42, 0.03, 0.034, MAT_, c=0.002, r=0.02, seg=3) # ------------------------------------------------------------------------------------------------ floor, ceiling CHAIR = (-2.72, -0.52) CHAIR_YAW = math.radians(-28) def floor(P): backing(P, FF, 0, 0, 7.2, DEPTH) cells = [] cells += cells_grid([0.02, 3.6, 7.18], [0.02, 0.46], FLOOR) cells += cells_grid([0.02, 1.2, 2.4, 3.6, 4.8, 6.0, 7.18], [0.46, 1.64, 2.82], FLOOR) cells += cells_grid([0.02, 0.46, 3.08, 7.18], [2.82, 3.28], FLOOR) plates(P, FF, cells, mat=FLOOR, c=0.003) # sill in front of the bunk P.prism(FF, shape(0.50, 2.90, 3.02, 3.295, bl=-0.12, br=-0.12), T, 0.062, SAT, c=0.010, segs=2) P.box(FF, 0.80, 2.96, 2.72, 2.99, 0.062, 0.066, GUN, c=0.0015) # tie-down fittings for (u, v) in ((3.95, 1.05), (5.60, 2.25), (1.95, 2.25)): P.prism(FF, circle(u, v, 0.075, 24), T, 0.020, STEEL, c=0.003) P.prism(FF, circle(u, v, 0.050, 24), 0.020, 0.023, MAT_, c=0.002) P.box(FF, u - 0.035, v - 0.006, u + 0.035, v + 0.006, 0.023, 0.026, STEEL, c=0.001) def ceiling(P): backing(P, FC, 0, 0, 7.2, DEPTH) us = [0.02, 1.2, 2.4, 3.6, 4.8, 6.0, 7.18] cells = cells_grid(us, [0.02, 0.74]) + cells_grid(us, [1.06, 2.24]) + cells_grid(us, [2.56, 3.28]) for v0 in (0.74, 2.24): cells += [(0.02, v0, 0.90, v0 + 0.32), (2.70, v0, 4.50, v0 + 0.32), (6.30, v0, 7.18, v0 + 0.32)] for u0 in (0.90, 4.50): P.ring(FC, shape(u0 + 0.004, v0 + 0.004, u0 + 1.796, v0 + 0.316, r=0.03, seg=3), shape(u0 + 0.05, v0 + 0.07, u0 + 1.75, v0 + 0.25, r=0.02, seg=3), 0.0, 0.034, MAT_, c=0.005, reveal_w=0.018, floor_mat=LCOOL) plates(P, FC, cells) P.box(FC, 3.20, 1.42, 4.00, 1.88, T, T + 0.012, MAT_, c=0.004, r=0.03, seg=3) slats(P, FC, 3.25, 1.47, 3.95, 1.83, 9, T + 0.012, T + 0.018, GUN) # ------------------------------------------------------------------------------------------------ chair def chair_base(P): Ff = Frame((0, 0, 0), (1, 0, 0), (0, 1, 0)) P.prism(Ff, shape(-0.26, -0.18, 0.26, 0.18, r=-0.035), 0.0, 0.016, STEEL, c=0.003) P.box(Ff, -0.20, -0.055, 0.20, 0.055, 0.016, 0.030, GUN, c=0.004, r=0.012, seg=3) for (x, y) in ((0.21, 0.13), (0.21, -0.13), (-0.21, 0.13), (-0.21, -0.13)): P.prism(Ff, circle(x, y, 0.022, 16), 0.016, 0.019, STEEL, c=0.001) P.prism(Ff, circle(x, y, 0.014, 6), 0.019, 0.029, GUN, c=0.002) Fp = Frame((0, 0.04, 0), (1, 0, 0), (0, 0, 1)) # w = -Y arm = [(-0.105, 0.03), (-0.035, 0.03), (-0.035, 0.108), (0.065, 0.268), (0.065, 0.34), (-0.005, 0.34), (-0.005, 0.292), (-0.105, 0.132)] P.prism(Fp, arm, 0.0, 0.08, GUN, c=0.008, segs=2, back=True) P.prism(Ff.at(dw=0.34), circle(0.03, 0.0, 0.075, 24), 0.0, 0.03, GUN, c=0.006, segs=2) def chair_seat(P): """Swivel part; its origin is the top of the pedestal.""" W = 0.46 Fp = Frame((0.0, W / 2, 0), (1, 0, 0), (0, 0, 1)) # side profile, w = -Y shell = [(-0.20, 0.0), (0.24, 0.0), (0.28, 0.035), (0.28, 0.062), (-0.15, 0.062), (-0.235, 0.58), (-0.275, 0.60), (-0.295, 0.56), (-0.26, 0.07)] P.prism(Fp, shell, 0.0, W, MAT_, c=0.0, back=True) wing = [(-0.22, -0.012), (0.26, -0.012), (0.31, 0.04), (0.31, 0.105), (0.22, 0.165), (0.02, 0.165), (-0.10, 0.255), (-0.20, 0.62), (-0.24, 0.66), (-0.30, 0.66), (-0.33, 0.60), (-0.30, 0.09)] for y in (W / 2 + 0.045, -W / 2): P.prism(Frame((0.0, y, 0), (1, 0, 0), (0, 0, 1)), wing, 0.0, 0.045, SAT, c=0.008, segs=2, back=True) Fs = Frame((0.0, 0, 0.062), (1, 0, 0), (0, 1, 0)) P.prism(Fs, shape(-0.13, -0.215, 0.27, 0.215, r=0.035, seg=4), 0.0, 0.045, MATT, c=0.022, segs=3) d = Vector((-0.085, 0.0, 0.518)).normalized() Fb = Frame(Vector((-0.15, -0.205, 0.062)) + d * 0.07, (0, 1, 0), d) P.prism(Fb, shape(0.0, 0.0, 0.41, 0.42, r=0.035, seg=4), 0.0, 0.04, MATT, c=0.02, segs=3) # ------------------------------------------------------------------------------------------------ bedding MAT_BOX = (-2.92, 1.205, -0.76, 2.03, 0.46, 0.62) # behind the surround's rounded reveal, inside the tunnel # mattress x0, y0, x1, y1, z0, z1 def mattress(P): x0, y0, x1, y1, z0, z1 = MAT_BOX F = Frame((x0, y0, z0), (1, 0, 0), (0, 1, 0)) P.prism(F, shape(0, 0, x1 - x0, y1 - y0, r=0.06, seg=4), 0.0, z1 - z0, MATT, c=0.04, segs=3) def load_cloth(name, mat, coll): """Blanket and pillow come from sim_bedding.py as cached meshes.""" p = os.path.join(HERE, 'cache', name + '.json') if not os.path.exists(p): return None d = json.load(open(p)) me = bpy.data.meshes.new(name) me.from_pydata(d['verts'], [], d['faces']) uv = me.uv_layers.new(name='UVMap') for li, l in enumerate(me.loops): uv.data[li].uv = d['uvs'][l.vertex_index] me.materials.append(bpy.data.materials[mat]) for pl in me.polygons: pl.use_smooth = True ob = bpy.data.objects.new(name, me) coll.objects.link(ob) return ob # ------------------------------------------------------------------------------------------------ decals, props def decals(P): def d(fr, cu, cv, h, name, w): x0, y0, x1, y1 = at(name) asp = ATLAS.get(name + '_aspect', 1.0) wd = h * asp P.decal(fr, cu - wd / 2, cv - h / 2, cu + wd / 2, cv + h / 2, w, DECAL, (x0, y0, x1, y1)) e = 0.0012 d(FB, 0.18, 1.30, 0.13, 'warn_tri', 0.08 + e) d(FB, 0.18, 1.18, 0.022, 'txt_caution', 0.08 + e) d(FB, 3.56, 1.86, 0.17, 'txt_cq01', T + e) d(FB, 3.56, 1.725, 0.030, 'txt_quarters', T + e) d(FB, 0.72, 2.46, 0.045, 'tri_down', 0.05 + e) d(FB, 2.80, 2.46, 0.045, 'tri_down', 0.05 + e) d(FB, 1.56, 2.455, 0.03, 'txt_berth', 0.05 + e) d(FB, 5.60, 2.31, 0.032, 'txt_station', T + e) d(FD, 5.40, 2.44, 0.05, 'txt_exit', T + e) d(FD, 6.62, 1.70, 0.11, 'arrow_left', T + e) d(FL, 0.32, 1.80, 0.07, 'cross', T + e) d(FL, 0.32, 1.72, 0.022, 'txt_medkit', T + e) d(FW, 0.15, 1.40, 0.022, 'txt_viewport', T + e) d(FF, 3.95, 1.25, 0.03, 'txt_tie', T + e) def props(P): Fs = Frame((-HX, YB, 0.76), (1, 0, 0), (0, 1, 0)) sl = Frame(Fs.p(5.15, -0.22, 0), Vector((0.96, 0.28, 0)), Vector((-0.28, 0.96, 0))) P.prism(sl, shape(-0.13, -0.09, 0.13, 0.09, r=0.016, seg=3), 0.0, 0.011, GUN, c=0.003) P.box(sl, -0.115, -0.075, 0.115, 0.075, 0.011, 0.012, MAT_, c=0.0) P.prism(Fs, circle(6.25, -0.20, 0.040, 20), 0.0, 0.095, SAT, c=0.004) P.prism(Fs, circle(6.25, -0.20, 0.033, 20), 0.095, 0.0955, BACK, c=0.0) # ------------------------------------------------------------------------------------------------ collision def ucx(coll, owner, idx, lo, hi, parent=None): name = 'UCX_%s_%02d' % (owner, idx) me = bpy.data.meshes.new(name) bm = bmesh.new() bmesh.ops.create_cube(bm, size=1.0) c = (Vector(lo) + Vector(hi)) / 2; s = Vector(hi) - Vector(lo) for v in bm.verts: v.co = Vector((v.co.x * s.x, v.co.y * s.y, v.co.z * s.z)) + c bm.to_mesh(me); bm.free() ob = bpy.data.objects.new(name, me) ob.display_type = 'WIRE' coll.objects.link(ob) if parent: ob.parent = parent return ob def collision(coll): n = {} def b(owner, lo, hi): n[owner] = n.get(owner, 0) + 1 ucx(coll, owner, n[owner], lo, hi) b('SM_CQ_Floor', (-HX, YD, -0.1), (HX, YO, T)) b('SM_CQ_Floor', (-HX + 0.50, YB - 0.40, T), (-HX + 3.02, YB, 0.062)) # sill in front of the bunk b('SM_CQ_Ceiling', (-HX, YD, H - T), (HX, YO, H + 0.1)) b('SM_CQ_Wall_Window', (-HX - 0.5, YD, 0), (-HX + T, YO, H)) b('SM_CQ_Wall_Locker', (HX - 0.06, YD, 0), (HX + 0.1, YO, H)) xd0, xd1 = HX - DOOR[2], HX - DOOR[0] W = 'SM_CQ_Wall_Door' b(W, (-HX, YD - 0.1, 0), (xd0, YD + T, H)) # left of the doorway b(W, (xd1, YD - 0.1, 0), (HX, YD + T, H)) b(W, (xd0, YD - 0.1, DOOR[3]), (xd1, YD + T, H)) b(W, (HX - 3.25, YD, 0), (HX - 0.85, YD + 0.60, 0.46)) # bench W = 'SM_CQ_Wall_Bunk' # round the alcove and the desk nook b(W, (-HX, YB - 0.08, 0), (-HX + OP[0], YO, H)) b(W, (-HX + OP[0], YB - 0.02, 0), (-HX + OP[2], YO, OP[1])) # bed base b(W, (-HX + OP[0], YB - 0.02, OP[3]), (-HX + OP[2], YO, H)) b(W, (-HX + OP[0], YB + NICHE_D, OP[1]), (-HX + OP[2], YO, OP[3])) b(W, (-HX + OP[2], YB - 0.02, 0), (-HX + DKH[0], YO, H)) b(W, (-HX + DKH[0], YB - 0.02, 0), (-HX + DKH[2], YO, DKH[1])) b(W, (-HX + DKH[0], YB - 0.02, DKH[3]), (-HX + DKH[2], YO, H)) b(W, (-HX + DKH[0], YB + DESK_D, DKH[1]), (-HX + DKH[2], YO, DKH[3])) b(W, (-HX + DKH[2], YB - 0.05, 0), (HX, YO, H)) b(W, (-HX + 4.60, YB - 0.44, 0.72), (-HX + 6.60, YB + 0.46, 0.76)) # desk slab x0, y0, x1, y1, z0, z1 = MAT_BOX b('SM_CQ_Mattress', (x0, y0, z0), (x1, y1, z1)) def ucx_bbox(coll, ob): """One box round a moving part, in the part's own space.""" cs = [v.co for v in ob.data.vertices] lo = Vector((min(c.x for c in cs), min(c.y for c in cs), min(c.z for c in cs))) hi = Vector((max(c.x for c in cs), max(c.y for c in cs), max(c.z for c in cs))) ucx(coll, ob.name, 1, lo, hi, parent=ob) def studio(sc): """Camera on the reference picture's view, the daylight behind the window, a weak fill. Not part of the asset.""" c = get_coll('CQ | Studio (not exported)') cd = bpy.data.cameras.new('CQ_Cam'); cd.lens = 28; cd.clip_start = 0.02 cam = bpy.data.objects.new('CQ_Cam', cd); c.objects.link(cam) cam.location = (0.50, -1.55, 1.28) cam.rotation_euler = (Vector((-2.30, 1.30, 1.02)) - Vector(cam.location)).to_track_quat('-Z', 'Y').to_euler() sc.camera = cam for name, loc, rot, sx, sy, watts in (('L_Window', (-3.95, -0.52, 1.38), (0, math.radians(-90), 0), 1.7, 2.2, 420), ('L_Fill', (0.6, -0.6, 2.50), (0, 0, 0), 3.0, 1.6, 10)): d = bpy.data.lights.new(name, 'AREA'); d.shape = 'RECTANGLE'; d.size = sx; d.size_y = sy; d.energy = watts d.color = (0.92, 0.96, 1.0) o = bpy.data.objects.new(name, d); o.location = loc; o.rotation_euler = rot; c.objects.link(o) w = bpy.data.worlds.new('CQ_World') try: w.use_nodes = True except Exception: pass bg = next(n for n in w.node_tree.nodes if n.type == 'BACKGROUND') bg.inputs[0].default_value = (0.02, 0.022, 0.026, 1) sc.world = w sc.render.engine = 'CYCLES' sc.cycles.samples = 128; sc.cycles.use_denoising = True sc.render.resolution_x = sc.render.resolution_y = 1300 sc.view_settings.view_transform = 'AgX' try: sc.view_settings.look = 'AgX - Medium High Contrast' except Exception: pass # ------------------------------------------------------------------------------------------------ assemble def main(): bpy.ops.wm.read_factory_settings(use_empty=True) sc = bpy.context.scene sc.name = 'Captains Quarters' sc.unit_settings.system = 'METRIC' make_materials() c_room = get_coll('CQ | Room (static)') c_move = get_coll('CQ | Moving parts') c_bed = get_coll('CQ | Bedding') c_ucx = get_coll('CQ | Collision (UCX)') report = {} def done(P, coll, **kw): ob = P.build(coll, **kw) report[ob.name] = audit(ob) return ob for name, fn in (('SM_CQ_Wall_Bunk', wall_bunk), ('SM_CQ_Bunk_Interior', bunk_interior), ('SM_CQ_Wall_Window', wall_window), ('SM_CQ_Wall_Door', wall_door), ('SM_CQ_Wall_Locker', wall_locker), ('SM_CQ_Floor', floor), ('SM_CQ_Ceiling', ceiling), ('SM_CQ_Props', props)): P = Part(name); fn(P); done(P, c_room) P = Part('SM_CQ_Decals'); decals(P); done(P, c_room, weighted=False) # moving parts: own origin on the axis, rotation 0, scale 1 movers = [] P = Part('SM_CQ_Door_Leaf'); door_leaf(P); ob = done(P, c_move) ob.location = FD.p((DOOR[0] + DOOR[2]) / 2, 0.0, -0.10) ob['motion'] = 'slides 1.05 m along world -X (into the wall toward the window corner)' movers.append(ob) for i, (u0, u1) in enumerate(LK): P = Part('SM_CQ_Locker_Door_%d' % (i + 1)); locker_door(P, u1 - u0); ob = done(P, c_move) ob.location = FL.p(u0, 0.10, 0.05) ob['motion'] = 'hinge on local Z at the origin; open = rotate to -90 degrees' movers.append(ob) P = Part('SM_CQ_Chair_Base'); chair_base(P); cb = done(P, c_move) cb.location = (CHAIR[0], CHAIR[1], T); cb.rotation_euler = (0, 0, CHAIR_YAW) P = Part('SM_CQ_Chair_Seat'); chair_seat(P); cs = done(P, c_move) cs.parent = cb; cs.location = (0.03, 0, 0.37) cs['motion'] = 'swivels freely about local Z' cb['motion'] = 'fixed to the floor; the scene places it turned %d degrees' % round(math.degrees(CHAIR_YAW)) movers += [cb, cs] P = Part('SM_CQ_Mattress'); mattress(P); done(P, c_bed) for nm in ('SM_CQ_Blanket', 'SM_CQ_Pillow'): ob = load_cloth(nm, SHEET, c_bed) if ob: report[ob.name] = audit(ob) collision(c_ucx) for ob in movers: ucx_bbox(c_ucx, ob) c_ucx.hide_render = True c_ucx.hide_viewport = True studio(sc) # apply the weighted normals so the meshes are final for ob in list(sc.objects): if ob.type == 'MESH' and ob.modifiers: with bpy.context.temp_override(object=ob, active_object=ob, selected_objects=[ob]): for md in list(ob.modifiers): try: bpy.ops.object.modifier_apply(modifier=md.name) except Exception as e: print('modifier_apply failed', ob.name, e) report['_total_triangles'] = sum(v['triangles'] for k, v in report.items() if not k.startswith('_')) json.dump(report, open(os.path.join(HERE, 'build_report.json'), 'w'), indent=1) bpy.ops.wm.save_as_mainfile(filepath=OUT) bpy.ops.file.make_paths_relative() # textures stay in ../textures, found from wherever the folder is bpy.ops.wm.save_mainfile() print('BUILD OK', report['_total_triangles']) if __name__ == '__main__': main()