
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
콜로세움은 플라비우스 왕조의 베스파시아누스 황제가 서기 70년대 초에 짓기 시작해 아들 티투스 황제 때인 서기 80년에 문을 연 원형경기장이다. 정식 이름은 플라비우스 원형경기장이며, 검투 경기와 맹수 사냥 같은 공공 구경거리가 이곳에서 열렸다. 평면은 장축 약 189m, 단축 약 156m의 타원이고 외벽 높이는 약 48m다.
외벽은 트래버틴으로 쌓은 4층 구조로, 아래 세 층은 80개씩의 아치가 이어지고 아치 사이에 토스카나식·이오니아식·코린트식 반원주가 층마다 차례로 붙는다. 맨 위 아틱 층은 벽기둥과 작은 창이 번갈아 나는 막힌 벽이며, 차양(벨라리움) 돛대를 받치던 까치발이 남아 있다. 중세 이후 지진과 석재 반출로 남쪽 외벽이 무너져, 오늘날에는 북쪽 일부만 온전한 높이로 서 있고 안쪽 고리 벽이 드러나 있다. 19세기 초에 무너진 양 끝을 벽돌 버팀벽으로 보강했다.
이 모델은 현재의 폐허 상태를 표현했다. 북쪽 외벽 32칸은 4층 전체를, 남쪽은 기초선과 교각 그루터기, 그리고 드러난 제2환·제3환 아케이드를 보여 준다. 관람석은 좌석이 사라지고 방사형 벽돌 벽만 남은 모습으로, 경기장 바닥은 지하 통로(히포게움)가 드러난 채 동쪽 일부만 나무 바닥을 복원한 모습으로 나타냈다.
만든 방법
바깥 타원의 호 길이를 따라 80개 베이를 고르게 나눈 뒤, (호 길이, 법선 오프셋, 높이) 좌표에서 만든 육면체 셀을 타원 위로 휘어 붙이는 방식으로 외벽과 안쪽 고리 아케이드를 bmesh 로 직접 쌓았다. 아치는 반원을 8조각으로 나눈 스팬드럴 셀로 만들었고, 반원주·아틱 벽기둥·까치발은 메시를 공유하는 링크 복제로 배치했다. 관람석·기단·히포게움 둘레벽은 단면 프로필을 타원 둘레로 쓸어(sweep) 만들었고, 위층 관람석은 방사형 벽을 하나씩 세웠다. 붕괴 범위와 남은 교각 높이는 시드를 고정한 난수로 정했으며, 실제 폐허의 불규칙한 파단면은 계단식 단면으로 단순화했다. 회랑 볼트는 평평한 슬래브로, 기둥 주두와 코니스 몰딩은 단순한 상자와 원뿔대로 줄였다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 398줄.
# 콜로세움 (로마, 서기 80년 완공) — 코드로만 만든 폐허 모델# 평면은 장축 188m·단축 156m 타원. 바깥 타원의 호 길이를 따라 80개 베이를 고르게 나누고,# (호 길이 s, 법선 방향 오프셋 w, 높이 z) 좌표계에서 만든 육면체 셀을 타원 위로 휘어 붙여# 외벽·제2환·제3환 아케이드를 bmesh 로 직접 쌓았다. 외벽은 토스카나·이오니아·코린트 반원주가 붙은# 3층 아치와 창이 난 4층 아틱으로 이루어지고, 북쪽 32칸만 온전히 남기고 남쪽은 무너뜨려# 안쪽 고리가 드러나게 했다. 무너진 양 끝은 계단식 단면과 벽돌 버팀벽(스턴 버팀벽)으로 처리했다.# 관람석(카베아)은 단면 프로필을 타원 둘레로 쓸어(sweep) 만들고, 위쪽은 방사형 벽돌 벽만 남은# 폐허로, 경기장 바닥은 지하 통로(히포게움)가 드러난 채 동쪽 일부만 나무 바닥을 복원한 모습으로 표현했다.# 기둥·아틱 기둥·까치발은 같은 메시를 공유하는 링크 복제로 배치했다. random 은 시드를 고정했다.import bisectimport randomfrom atelier import *random.seed(80)SHOT = {"azimuth": 12, "elevation": 24, "lens": 45, "light": "day", "background": "#27303d"}# ── 재질 ──────────────────────────────────────────────M_TRAV = [mat('Travertine', '#cfb995', 0.85), mat('Travertine_Warm', '#c3ab88', 0.88), mat('Travertine_Weathered', '#ad9c80', 0.9)]M_TUFA = mat('Tufa_InnerRing', '#a8977a', 0.9)M_BRICK = mat('Roman_Brick', '#8c6250', 0.92)M_BRICK_DK = mat('Brick_Rubble', '#6e5446', 0.95)M_SEAT = mat('Marble_Seats', '#bdb19c', 0.8)M_WOOD = mat('Arena_Floor_Wood', '#7a5a3c', 0.85)M_EARTH = mat('Hypogeum_Earth', '#7c6c58', 0.95)M_PAVE = mat('Paving_Stone', '#7d766a', 0.9)# ── 타원과 호 길이 좌표계 ──────────────────────────────A, B = 94.0, 78.0 # 바깥 외벽 면의 반장축·반단축NB = 80 # 아치 수_N = 8000_TS = [2 * math.pi * k / _N for k in range(_N + 1)]_CUM = [0.0]for k in range(_N): t0, t1 = _TS[k], _TS[k + 1] _CUM.append(_CUM[-1] + math.hypot(A * (math.cos(t1) - math.cos(t0)), B * (math.sin(t1) - math.sin(t0))))PER = _CUM[-1]L = PER / NB # 베이 폭(바깥 면 기준, 약 6.8m)_FR = {}def frame(s): key = round(s % PER, 5) f = _FR.get(key) if f: return f나머지 350줄 펼치기접기
j = min(max(bisect.bisect_right(_CUM, key) - 1, 0), _N - 1) t = _TS[j] + (key - _CUM[j]) / (_CUM[j + 1] - _CUM[j]) * (_TS[j + 1] - _TS[j]) tx, ty = -A * math.sin(t), B * math.cos(t) ln = math.hypot(tx, ty) tx, ty = tx / ln, ty / ln f = (A * math.cos(t), B * math.sin(t), ty, -tx, tx, ty) # 위치, 바깥 법선, 접선 _FR[key] = f return fdef F(s, w, z): px, py, nx, ny, _, _ = frame(s) return (px + nx * w, py + ny * w, z)class Mesher: """(s, z) 사각형 × w 범위 육면체 셀을 타원 위로 휘어 하나의 메시에 모은다.""" def __init__(self, name, mats): self.name, self.mats, self.bm = name, mats, bmesh.new() def hexa(self, quad, w0, w1, m=0): bm = self.bm v = [bm.verts.new(F(s, w0, z)) for s, z in quad] + [bm.verts.new(F(s, w1, z)) for s, z in quad] for f in ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)): bm.faces.new([v[i] for i in f]).material_index = m def box(self, s0, s1, z0, z1, w0, w1, m=0, n=1): for k in range(n): a, b = s0 + (s1 - s0) * k / n, s0 + (s1 - s0) * (k + 1) / n self.hexa([(a, z0), (b, z0), (b, z1), (a, z1)], w0, w1, m) def spandrel(self, sc, hw, zs, ztop, w0, w1, m=0, n=8): """반원 아치 위 벽면(아치 안쪽 면 포함).""" for j in range(n): a0, a1 = math.pi * (1 - j / n), math.pi * (1 - (j + 1) / n) p0 = (sc + hw * math.cos(a0), zs + hw * math.sin(a0)) p1 = (sc + hw * math.cos(a1), zs + hw * math.sin(a1)) self.hexa([p0, p1, (p1[0], ztop), (p0[0], ztop)], w0, w1, m) def finish(self): bmesh.ops.recalc_face_normals(self.bm, faces=self.bm.faces) me = bpy.data.meshes.new(self.name) self.bm.to_mesh(me) self.bm.free() ob = bpy.data.objects.new(self.name, me) link(ob) for m in self.mats: me.materials.append(m) return obdef sweep(name, prof, material, s0=0.0, s1=None, nseg=400): """(w, z) 단면 프로필을 타원 둘레로 쓸어 닫힌 고리 솔리드를 만든다.""" closed = s1 is None s1 = PER if closed else s1 bm = bmesh.new() rows = [] for j in range(nseg if closed else nseg + 1): s = s0 + (s1 - s0) * j / nseg rows.append([bm.verts.new(F(s, w, z)) for w, z in prof]) n = len(prof) for j in range(len(rows) - (0 if closed else 1)): r0, r1 = rows[j], rows[(j + 1) % len(rows)] for k in range(n): bm.faces.new((r0[k], r0[(k + 1) % n], r1[(k + 1) % n], r1[k])) if not closed: bm.faces.new(rows[0]) bm.faces.new(rows[-1][::-1]) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) link(ob) return assign(ob, material)# ── 층 높이(절대 z, 기단 위 0.7m 부터) ─────────────────Z0 = 0.7LV = [Z0, 11.2, 23.1, 34.7, 49.2]T_OUT = 2.4 # 외벽 두께 (w: -2.4 ~ 0)R2 = (-10.2, -8.0) # 제2환R3 = (-16.0, -14.0) # 제3환HW = 2.1 # 외벽 아치 반폭INTACT = range(5, 37) # 4층이 온전히 남은 외벽 베이STEP_W = {37: 3, 38: 2, 39: 1} # 서쪽 끝 계단식 붕괴NORTH_R2 = set(range(3, 41)) # 제2환 3층까지 온전한 구간def outer_storeys(i): if i in INTACT: return 4 return STEP_W.get(i, 0)# ── 외벽 ─────────────────────────────────────────────outer = Mesher('Outer_Wall_Arcade', M_TRAV)pier_sets = [set(), set(), set(), set()]for i in range(NB): ns = outer_storeys(i) sc = i * L s0, s1 = sc - L / 2, sc + L / 2 W0 = -T_OUT for k in range(ns): m = random.choice((0, 0, 1, 2)) if k < 3 else random.choice((1, 2, 2)) zb, zt = LV[k], LV[k + 1] pier_sets[k].update((round(s0, 4), round(s1, 4))) if k == 0: pb, ent, zs = zb, 9.9, 6.0 elif k < 3: outer.box(s0, s1, zb, zb + 1.4, W0, 0.8, m, 3) # 기둥 받침띠 pb, ent, zs = zb + 1.4, (zt - 1.5 if k == 1 else zt - 1.4), (18.6 if k == 1 else 30.2) if k < 3: outer.box(s0, sc - HW, pb, ent, W0, 0, m) outer.box(sc + HW, s1, pb, ent, W0, 0, m) outer.spandrel(sc, HW, zs, ent, W0, 0, m) outer.box(s0, sc - HW, zs - 0.4, zs, 0.0, 0.2, m) # 아치 임포스트 outer.box(sc + HW, s1, zs - 0.4, zs, 0.0, 0.2, m) outer.box(s0, s1, ent, zt - 0.45, W0, 0.85, m, 3) # 아키트레이브·프리즈 outer.box(s0, s1, zt - 0.45, zt, W0, 1.2, m, 3) # 코니스 else: outer.box(s0, s1, zb, zb + 1.6, W0, 0.45, m, 3) top = 47.9 if i % 2 == 0: # 아틱의 작은 사각 창 outer.box(s0, sc - 1.0, zb + 1.6, top, W0, 0, m, 2) outer.box(sc + 1.0, s1, zb + 1.6, top, W0, 0, m, 2) outer.box(sc - 1.0, sc + 1.0, zb + 1.6, 38.6, W0, 0, m) outer.box(sc - 1.0, sc + 1.0, 41.2, top, W0, 0, m) else: outer.box(s0, s1, zb + 1.6, top, W0, 0, m, 3) outer.box(s0, s1, top, 48.5, W0, 0.35, m, 3) outer.box(s0, s1, 48.5, 49.2, W0, 0.8, m, 3) if ns == 0 and not (2 <= i <= 4): # 남쪽: 외벽은 무너지고 기초선과 교각 그루터기만 남았다 outer.box(s0, s1, Z0, Z0 + 0.45, W0 + 0.1, -0.1, 2, 3) if random.random() < 0.6: h = random.uniform(0.8, 4.5) outer.box(s1 - 1.25, s1 + 1.25, Z0, Z0 + h, W0, 0, random.choice((0, 1, 2)))outer = outer.finish()# 동쪽 끝: 스턴 버팀벽(벽돌 쐐기)stern = Mesher('Buttresses_Brick', [M_BRICK])sa, sb = 2.0 * L, 4.5 * LNS = 9zp = Z0for k in range(NS): # 계단처럼 층층이 쌓아 올린 벽돌 덩어리 a = sa + (sb - sa) * k / NS zt = Z0 + (49.2 - Z0) * ((k + 1) / NS) ** 1.2 stern.box(a, sb, zp, zt, -3.0, 0.9 - 0.06 * k, 0, 2) zp = zt# 서쪽 끝: 계단식 단면을 감싼 벽돌 보강(발라디에 버팀벽)for k, (zb, zt) in enumerate(((LV[3], LV[4]), (LV[2], LV[3]), (LV[1], LV[2]), (Z0, LV[1]))): sb_ = (36.5 + k) * L stern.box(sb_, sb_ + 1.4, zb, zt, -2.7, 0.95, 0, 2)stern = stern.finish()stern.name = 'Buttresses_Brick'# ── 반원주(토스카나·이오니아·코린트)와 아틱 기둥 ───────def column_mesh(name, order, h): parts = [] parts.append(cylinder(name + '_base', r=0.78, h=0.35, seg=20)) shaft = lathe(name + '_shaft', [(0.0, 0.35), (0.62, 0.35), (0.6, h * 0.5), (0.54, h - 0.9), (0.0, h - 0.9)], seg=20) parts.append(shaft) if order == 'doric': parts.append(cylinder(name + '_echinus', r=0.56, r2=0.74, h=0.6, loc=(0, 0, h - 0.9), seg=20)) parts.append(box(name + '_abacus', (1.6, 1.6, 0.3), loc=(0, 0, h - 0.3), base=True)) elif order == 'ionic': parts.append(cylinder(name + '_neck', r=0.56, h=0.65, loc=(0, 0, h - 0.9), seg=20)) for sy in (-1, 1): parts.append(cylinder(name + '_volute', r=0.27, h=0.9, loc=(0.0, sy * 0.62, h - 0.5), rot=(0, math.pi / 2, 0), base=False, seg=14)) parts.append(box(name + '_abacus', (1.4, 1.6, 0.25), loc=(0, 0, h - 0.25), base=True)) else: parts.append(cylinder(name + '_bell', r=0.55, r2=0.8, h=0.7, loc=(0, 0, h - 0.9), seg=20)) parts.append(box(name + '_abacus', (1.6, 1.7, 0.2), loc=(0, 0, h - 0.2), base=True)) ob = join(parts, name) return assign(ob, M_TRAV[0])ORDERS = [('Column_Tuscan', 'doric', LV[0], 9.9), ('Column_Ionic', 'ionic', LV[1] + 1.4, LV[2] - 1.5), ('Column_Corinthian', 'corinthian', LV[2] + 1.4, LV[3] - 1.4)]for k, (nm, order, zb, zt) in enumerate(ORDERS): proto = column_mesh(nm, order, zt - zb) first = True for s in sorted(pier_sets[k]): x, y, _ = F(s, 0, 0) nx, ny = frame(s)[2:4] ob = proto if first else duplicate(proto, name=nm) ob.location = (x, y, zb) ob.rotation_euler = (0, 0, math.atan2(ny, nx)) first = False# 아틱: 코린트식 벽기둥과 벨라리움 돛대를 받치던 까치발pil = join([box('Attic_Pilaster', (0.5, 1.3, 11.6), loc=(0, 0, 0), base=True), box('Attic_Pilaster_cap', (0.7, 1.55, 0.5), loc=(0, 0, 11.1), base=True)], 'Attic_Pilaster')assign(pil, M_TRAV[1])corbel = box('Attic_Corbel', (0.8, 0.6, 0.7), loc=(0.35, 0, 0), base=True, material=M_TRAV[2])first_p, first_c = True, Truefor s in sorted(pier_sets[3]): x, y, _ = F(s, 0, 0) nx, ny = frame(s)[2:4] ob = pil if first_p else duplicate(pil, name='Attic_Pilaster') ob.location, ob.rotation_euler = (x, y, LV[3] + 1.6), (0, 0, math.atan2(ny, nx)) first_p = Falsefor i in INTACT: for d in (-1.95, 0.0, 1.95): s = i * L + d x, y, _ = F(s, 0, 0) nx, ny = frame(s)[2:4] ob = corbel if first_c else duplicate(corbel, name='Attic_Corbel') ob.location, ob.rotation_euler = (x, y, 44.6), (0, 0, math.atan2(ny, nx)) first_c = False# ── 제2환·제3환 아케이드 ─────────────────────────────ring2 = Mesher('Inner_Ring2_Arcade', [M_TUFA, M_TRAV[2]])ring3 = Mesher('Inner_Ring3_Arcade', [M_TUFA, M_BRICK])pier_h = {}for i in range(NB): sc = i * L s0, s1 = sc - L / 2, sc + L / 2 w0, w1 = R2 hw = 2.0 for k, zs in ((0, 6.0), (1, 18.0)): zb, zt = LV[k], LV[k + 1] ring2.box(s0, sc - hw, zb, zt - 0.6, w0, w1) ring2.box(sc + hw, s1, zb, zt - 0.6, w0, w1) ring2.spandrel(sc, hw, zs, zt - 0.6, w0, w1) ring2.box(s0, s1, zt - 0.6, zt, w0, w1 + 0.25, 1, 2) zb, zt = LV[2], LV[3] if i in NORTH_R2 or random.random() < 0.3: ring2.box(s0, sc - hw, zb, zt - 0.6, w0, w1) ring2.box(sc + hw, s1, zb, zt - 0.6, w0, w1) ring2.spandrel(sc, hw, 29.5, zt - 0.6, w0, w1) ring2.box(s0, s1, zt - 0.6, zt, w0, w1 + 0.25, 1, 2) if i in NORTH_R2: ring2.box(s0, s1, zt, 36.0, w0, w1, 0, 2) full3 = True else: for ps, a, b in ((round(s0, 3), s0, sc - hw), (round(s1, 3), sc + hw, s1)): h = pier_h.setdefault(ps, random.uniform(24.0, 31.0)) ring2.box(a, b, zb, h, w0, w1) full3 = False # 제3환: 2층 아치 + 관람석 회랑 벽 w0, w1 = R3 for k, zs in ((0, 5.6), (1, 17.2)): zb, zt = LV[k], LV[k + 1] ring3.box(s0, sc - 1.8, zb, zt - 0.6, w0, w1) ring3.box(sc + 1.8, s1, zb, zt - 0.6, w0, w1) ring3.spandrel(sc, 1.8, zs, zt - 0.6, w0, w1) ring3.box(s0, s1, zt - 0.6, zt, w0, w1 + 0.25, 0, 2) ring3.box(s0, s1, LV[2], 30.5, w0, w1, 1, 2) # 회랑 천장(볼트) 슬래브 ns = outer_storeys(i) for k in range(1, 4): if k <= ns and (k < 3 or i in NORTH_R2): ring2.box(s0, s1, LV[k] - 1.1, LV[k], -7.75, -T_OUT, 0, 2) for z in (LV[1], LV[2]): ring2.box(s0, s1, z - 1.1, z, -13.75, R2[0], 0, 2) if full3: ring2.box(s0, s1, 29.4, 30.5, -13.75, R2[0], 0, 2)ring2 = ring2.finish()ring3 = ring3.finish()# ── 관람석(카베아) ────────────────────────────────────prof = [(-51.0, Z0), (-51.0, 4.0), (-48.5, 4.0)]z, w = 4.0, -48.5for r in range(12): # 아래층 대리석 좌석 단 z += 7.0 / 12 prof.append((w, z)) w += 8.5 / 12 prof.append((w, z))prof[-1] = (-40.0, prof[-1][1])prof += [(-40.0, 8.5), (-28.0, 17.0), (-28.0, 18.5), (-18.0, 26.0), (-18.0, 30.5), (-16.0, 30.5), (-16.0, Z0)]cavea = sweep('Cavea_Seating', prof, M_SEAT, nseg=480)# 위층: 좌석이 사라지고 남은 방사형·동심원 벽돌 벽sweep('Cavea_Balteus_Wall_1', [(-40.0, 8.2), (-38.8, 8.2), (-38.8, 12.5), (-40.0, 12.5)], M_BRICK, nseg=400)sweep('Cavea_Balteus_Wall_2', [(-28.6, 16.4), (-27.6, 16.4), (-27.6, 22.5), (-28.6, 22.5)], M_BRICK, nseg=400)radial = Mesher('Cavea_Radial_Walls', [M_BRICK, M_BRICK_DK])def seat_z(w): if w <= -28.0: return 12.5 + (w + 40.0) / 12.0 * 8.5 return 22.5 + (w + 28.0) / 10.0 * 7.5def floor_z(w): if w <= -28.0: return 8.5 + (w + 40.0) / 12.0 * 8.5 return 18.5 + (w + 28.0) / 10.0 * 7.5bmr = radial.bmfor i in range(NB): s = (i + 0.5) * L px, py, nx, ny, tx, ty = frame(s) loss = random.choice((0, 0, 0, 1.5, 3.0)) # 일부 벽은 윗부분이 더 깎여 나갔다 for wa, wb in ((-38.8, -28.6), (-27.6, -18.0)): pts = [(wa, floor_z(wa) - 0.3), (wb, floor_z(wb) - 0.3), (wb, max(seat_z(wb) - loss, floor_z(wb) + 0.5)), (wa, max(seat_z(wa) - loss, floor_z(wa) + 0.5))] vs = [] for side in (-0.6, 0.6): for ww, zz in pts: vs.append(bmr.verts.new((px + nx * ww + tx * side, py + ny * ww + ty * side, zz))) for f in ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)): bmr.faces.new([vs[k] for k in f]).material_index = 1 if loss else 0radial.finish()# ── 기단과 경기장 ─────────────────────────────────────sweep('Site_Base', [(-51.0, -3.2), (12.0, -3.2), (9.0, 0.0), (3.0, 0.0), (3.0, 0.35), (1.5, 0.35), (1.5, Z0), (-51.0, Z0)], M_PAVE, nseg=400)arena_pts = [F(PER * k / 240, -51.0, 0)[:2] for k in range(240)]prism('Arena_Pit_Floor', arena_pts, 0.4, loc=(0, 0, -2.8), material=M_EARTH)# 히포게움: 장축 방향으로 달리는 지하 통로 벽과 둘레 통로hyp = Mesher('Hypogeum_Walls', [M_BRICK_DK])sweep('Hypogeum_Ring_Wall', [(-54.5, -2.4), (-53.6, -2.4), (-53.6, 0.4), (-54.5, 0.4)], M_BRICK_DK, nseg=300)hb = hyp.bmfor y in (-1.6, 1.6, -5.0, 5.0, -8.4, 8.4, -11.8, 11.8, -15.2, 15.2, -18.6, 18.6): xe = 38.0 * math.sqrt(max(0.0, 1 - (y / 22.5) ** 2)) - 0.6 x = -xe while x < xe - 1.5: x1 = min(x + random.uniform(4.5, 7.5), xe) bmesh.ops.create_cube(hb, size=1.0, matrix=Matrix.Translation(((x + x1) / 2, y, -1.0)) @ Matrix.Diagonal((x1 - x, 0.8, 2.8, 1.0))) x = x1 + 1.4for x in range(-30, 31, 10): # 가로 칸막이 벽 ye = min(18.6, 22.5 * math.sqrt(1 - (x / 38.0) ** 2) - 1.0) bmesh.ops.create_cube(hb, size=1.0, matrix=Matrix.Translation((x, 0, -1.0)) @ Matrix.Diagonal((0.8, 2 * ye, 2.8, 1.0)))hyp.finish()def clip_x(poly, x0): out = [] for k in range(len(poly)): p, q = poly[k], poly[(k + 1) % len(poly)] if p[0] >= x0: out.append(p) if (p[0] >= x0) != (q[0] >= x0): t = (x0 - p[0]) / (q[0] - p[0]) out.append((x0, p[1] + t * (q[1] - p[1]))) return outprism('Arena_Floor_Reconstruction', clip_x(arena_pts, 14.0), 0.3, loc=(0, 0, 0.4), material=M_WOOD)






