성수-CELL
성수동 식물 복합문화시설 계획
| period | 2024.03–2024.06 · 후속 개발 2026.02 |
| location | 서울특별시 성동구 연무장길 89 |
| project | 개인 작업 · 4학년 건축설계 스튜디오 |
| status | 미시공 학업 설계안 · 후속 발전 모델 |
| goal | 3D 보로노이를 활용한 식물 복합문화시설의 공간 조직 |
| result | 기준점 배열 규칙과 최적화 모델, 3D 파워 다이어그램 컴포넌트 |
성수-CELL은 3D 보로노이로 식물 복합문화시설의 공간을 생성하고, 2026년 2월 파워 다이어그램을 구현해 공간과 구조의 위계를 다시 계획한 개인 작업이다. 2024년 원안에서는 기준점의 배열 규칙과 연간 일사량 탐색을 통해 계획안을 만들었다. 그러나 셀의 크기와 바닥을 프로그램에 맞게 조절하고, 복잡한 경계를 구조와 시공의 논리로 바꾸는 데에는 한계가 남았다. 후속 작업에서는 가중치로 셀의 규모를 조정하며 이 문제를 다시 다뤘다.


세포 조직과 세 겹의 프로그램
마늘 단면에서 관찰한 반복적인 분할은 최초의 시각적 출발점이었다. 공간 조직은 쌍떡잎식물의 줄기에서 세포의 배열과 조직별 역할, 그 사이를 지나는 양분 이동 경로가 서로 맞물리는 방식을 참고했다.
이를 세 겹의 프로그램으로 번역했다. 중심의 코어 셀은 수직 동선과 서비스를 담당하고, 중간의 식물원 셀은 건물의 주 공간을 이룬다. 외곽 셀은 표피 조직에 대응하며 관람 동선과 이벤트 공간을 수용한다. 식물 조직을 그대로 복제하기보다, 서로 다른 조직이 역할을 나누며 하나의 단면을 만드는 방식을 공간 구성의 원리로 삼았다.


건축 공간을 위한 기준점 배열
기준점을 자유롭게 배치하면 형태적으로 다양한 셀이 만들어지지만, 최소 바닥면적과 사용할 수 있는 바닥 기울기, 층고를 안정적으로 확보하기 어려웠다. 기준점 사이의 거리와 배열이 이 세 조건에 미치는 영향을 먼저 비교했다.




왼쪽은 기준점을 층별 높이에 맞춰 수직으로 배열한 단면 규칙이다. 기준점의 높이와 수직열을 고정해 규칙적인 층과 셀 높이를 확보했다. 오른쪽은 기준점을 세 겹의 범위 안에 배치한 방사형 규칙이다. 방사형 열의 간격으로 최소 바닥면적을 확보하고, 각 열에서 기준점이 움직일 수 있는 범위를 달리해 코어·식물원·외곽 셀의 상대적인 크기를 조절했다. 중앙의 모델은 두 규칙이 결합된 상태를 보여준다.
평면 위치가 층마다 달라지면 기준점의 수직열이 어긋나 바닥이 기울었다. 허용 기울기 안의 바닥만 남길 수 있었지만, 가능한 조합을 수동으로 조정하려면 시간이 오래 걸렸고 선택 기준도 부족했다. 이 조건을 이후 Galapagos의 탐색에 포함했다.
나선형 배열과 성능 탐색



왼쪽은 수직열과 방사형 배열만 적용한 기본 상태다. 중앙에서는 식물원 셀과 외곽 셀이 코어를 감싸며 두 층씩 상승하도록 배열했다. 램프와 계단으로 이어지는 관람·체험 동선의 기본 구조가 이 단계에서 만들어졌다. 오른쪽은 이 관계를 유지하면서 Galapagos로 기준점의 평면 위치를 미세 조정한 결과다.

셀 생성, 바닥면 판정과 성능 계산은 하나의 Grasshopper 정의 안에 구성했다. 기준점의 높이와 세 겹의 프로그램 관계, 나선형 배열은 고정하고, 각 기준점의 평면 위치만 방사형 범위 안에서 변수로 설정했다.
각 대안에서는 바닥면의 면적과 법선 벡터를 검사해 최소 면적과 허용 기울기를 만족하는 슬라브를 판정했다. 이 유효성 결과와 유효 바닥면의 연간 일사량을 하나의 fitness로 조합해 Galapagos가 최대화하도록 했다. 유효한 바닥과 일사 성능을 함께 확보하는 기준점 배열을 찾기 위한 탐색이었다.
132세대와 두 개의 봉우리



탐색의 목적은 하나의 최고점으로 빠르게 수렴하는 데에만 있지 않았다. 서로 다른 봉우리에서 나타나는 공간적 가능성을 비교하기 위해 입력값의 해상도를 낮춰 탐색 간격을 넓혔다. fitness에는 로그 변환과 자릿수 축약을 적용해 고득점 구간의 미세한 차이가 탐색을 지배하지 않도록 했다.
Galapagos의 Inbreeding 설정도 유전자 값이 가까운 개체끼리만 교배되지 않도록 조정했다. 서로 다른 유전자 조합이 교배에 참여할 가능성을 남겨, 한 계열의 해에 가려질 수 있는 지역 최적해를 함께 관찰하기 위한 선택이었다. 다양한 해를 보장하는 설정은 아니었지만, 성능 순위 밖의 형상을 설계 대안으로 검토할 수 있었다.
132세대의 기록에서는 두 개의 뚜렷한 봉우리가 나타났다. 한 지역 최적해는 15세대에 등장했다가 도태된 뒤 50세대에 다시 나타났다. 다른 계열은 25세대 이후 점수를 높였고, 73세대부터 탐색에서 가장 높은 fitness를 기록했다.



최고점에 수렴한 해들은 유효한 바닥과 일사 성능을 고르게 확보했다. 다른 봉우리의 지역 최적해는 점수는 낮았지만 중층부의 돌출과 공간 위계가 더 선명했다. 최종안은 후자의 형상을 바탕으로 발전시켰다.


생성된 셀과 건축의 간극
원안은 보로노이의 불규칙성을 배열 규칙과 성능 조건 안에서 다뤘지만, 프로그램별 셀 규모를 독립적으로 조절하지 못했다. 셀의 크기는 기준점 사이의 거리에서 간접적으로 결정됐고, 평면 위치의 변화는 바닥 기울기의 변화로 이어졌다.
보로노이의 엣지 역시 공간의 경계를 만들 뿐, 안정적인 하중 경로나 반복 가능한 접합을 보장하지 않았다. 복잡하게 분기되는 프레임을 합리적인 부재와 연결 방식으로 바꾸지 못한 채 원안은 스튜디오 결과로 마무리됐다.
2026년 후속 작업은 셀의 규모를 별도의 변수로 다루고, 공간 위계와 구조 위계를 연결하는 데서 시작했다.
가중 셀을 위한 3차원 파워 다이어그램
2026년 2월, 기준점마다 서로 다른 크기 매개변수를 부여할 수 있는 3차원 파워 다이어그램을 Grasshopper에서 사용하기 위해 Python으로 직접 구현했다.




일반 보로노이의 이웃 관계는 기준점의 위치만을 사용하는 Delaunay Triangulation으로 정해진다. 파워 다이어그램은 위치와 가중치를 함께 반영하는 Regular Triangulation, 즉 가중 Delaunay 분할을 사용한다. 같은 기준점 배열에서도 크기 매개변수에 따라 셀의 상대적 규모와 인접 관계를 조정할 수 있다.
컴포넌트는 입력한 크기 매개변수 R의 제곱을 파워 가중치로 사용한다. 각 기준점을 x²+y²+z²−R² 값을 더한 4차원 점으로 변환하고, 아래쪽 볼록껍질에서 Regular Triangulation의 사면체 연결을 구했다. 각 사면체의 파워 센터를 셀의 꼭짓점으로 사용한 뒤, 이를 지정된 경계 안에서 3차원 셀 메쉬로 구성했다.
입출력 구조는 기존 3D 보로노이 컴포넌트의 사용 방식을 유지하고 크기 매개변수 목록을 추가했다. 기존 Grasshopper 정의를 다시 만들지 않고 후속 탐색에 연결하기 위한 구성이다.
가중치가 만든 공간의 위계



왼쪽은 후속 작업에서 기준점을 다시 배열한 뒤 모든 셀을 같은 조건으로 생성한 기준 모델이다. 중앙은 프로그램과 층에 따라 가중치를 적용한 결과이며, 오른쪽은 공간과 구조의 위계를 함께 구분해 보여준다.
같은 배열과 층에 속한 셀은 동일한 가중치 범위를 공유하고, 코어·식물원·외곽 셀에는 서로 다른 범위를 부여했다. 기준점의 높이는 고정하고 이 가중치만 Galapagos의 변수로 설정했다. 어떤 조합에서도 세 겹의 프로그램 위계가 유지되도록 범위를 정한 뒤, 연간 일사량의 fitness가 가장 높은 결과를 발전 모델로 사용했다.
기준점의 평면 위치를 움직이지 않고 셀의 크기를 조절하면서 바닥을 전반적으로 수평에 가깝게 유지할 수 있었다. 식물원과 외곽 공간이 코어를 감싸며 상승하는 원안의 나선형 구성도 유지했다. 코어와 외곽의 셀은 구조와 서비스·이벤트 공간의 골격을 만들고, 그 사이의 큰 셀은 식물원을 수용한다. 식물 줄기의 조직에서 참고한 세 겹의 관계가 프로그램과 공간의 위계로 구체화됐다.

세 겹의 공간



코어 셀은 각 층의 엘리베이터 홀과 서비스 동선을 수용한다. 중간의 식물원 셀은 두 층 높이의 주 공간이며, 바닥을 주변 동선보다 낮춰 식재에 필요한 토심을 확보했다. 외곽 셀은 한 층 높이의 관람·체험 동선과 이벤트 공간으로 구성했다.
코어 셀은 매 층에 놓이고, 식물원과 외곽 공간은 두 층 단위로 연결된다. 1층에서 시작한 나선형 구간은 3층에서 다음 구간과 만나고, 다시 5층으로 이어진다. 램프와 계단이 각 구간을 연결한다. 중간 층의 코어 셀과 외곽 셀은 복층 식물원에 접한 메자닌으로 작동하며, 서로 다른 높이에서 식물원으로 진입하거나 내려다볼 수 있는 동선을 만든다.


공간 위계에 따른 구조 계획



가중치는 공간의 크기뿐 아니라 셀의 방향성을 조절하는 데에도 사용했다. 외곽 셀의 가중치는 줄이고 인접 셀의 가중치는 높여, 외곽 셀의 수평 폭을 좁히면서 수직 길이를 유지했다. 그 결과 외곽에는 수직 방향의 이방성을 갖는 길쭉한 셀이 형성됐다. 수직으로 긴 셀은 하중 전달 경로를 단순화하고, 식물 줄기 표피세포의 배열을 구조 형상으로 옮긴다.
구조는 세 단계로 나눴다. 코어와 외곽 셀의 수직축에는 주요 수직 부재를 두었다. 두 층 높이의 식물원 셀에는 보조 수직 부재를 배치해 식재 하중의 전달 경로를 보완했다. 수평 연결재는 수직 부재를 묶어 횡변형과 비틀림을 제어하고, 하중을 코어와 외곽의 주요 부재로 전달하도록 계획했다.
외곽 프레임은 별도의 Galapagos 탐색으로 단순화했다. 허용거리 안의 근접 정점과 엣지를 병합한 뒤, 외곽 셀 기준점의 높이와 가중치를 변수로 설정했다. 바닥 기울기의 허용 범위를 유지하면서 남는 엣지 수를 최소화해 부재의 수를 줄이고 반복성을 높이려 했다.
발전 모델과 남은 과제
가중 셀은 2024년 원안에서 해결하지 못했던 공간 규모와 수평 바닥의 조절 문제를 보완했다. 프로그램에 따라 셀의 위계를 설정하고, 공간과 프레임을 하나의 모델 안에서 함께 조직할 수 있게 된 것이 현재의 성과다.
구조 해석과 부재 계산은 아직 진행하지 않았으며, 부재의 단면과 접합 방식, 식재 공간의 배수·설비 통합도 남아 있다. 보로노이로 생성한 형상을 시공 가능한 건축으로 바꾸는 문제는 여전히 성수-CELL의 다음 작업이다.
Power Diagram - Grasshopper python component Script
1#r: numpy
2#r: scipy
3import numpy as np
4from scipy.spatial import ConvexHull
5import Rhino.Geometry as rg
6import System
7import System.Threading.Tasks as tasks
8
9'''
10= 3D Weighted Voronoi Algorithm - Power Diagram =
11- Rhino8 Grasshopper, Python 3 Script Component -
12
13[Component Input]
14- P (List Access, Point3d): Voronoi Seed Points
15- R (List Access, float): Weights of Seed Points
16- B (Item Access, Brep): Voronoi Boundary
17
18[Component Output]
19- M (Mesh): Voronoi Cells
20- D (Curve): Regular Triangulation
21
22Last edit by. HelixHo 02/16/2026
23'''
24
25### Global Tolerance
26tol = 0.001
27
28### Parallel Operations ProcessorCount
29options = tasks.ParallelOptions()
30options.MaxDegreeOfParallelism = System.Environment.ProcessorCount - 2
31
32###### Calculate simplex 3D Volume (simplex validation)
33def get_simplex_volume_3d(pts_4d):
34 # pts_4d: [[x,y,z,w], [x,y,z,w], [x,y,z,w], [x,y,z,w]] (4x4 array)
35 pts_3d = pts_4d[:, :3]
36 v1 = pts_3d[1] - pts_3d[0]
37 v2 = pts_3d[2] - pts_3d[0]
38 v3 = pts_3d[3] - pts_3d[0]
39 matrix = np.array([v1, v2, v3])
40
41 return abs(np.linalg.det(matrix)) / 6.0
42
43###### Calculate Power Center (vertices of Weighted Voronoi)
44# ∣∣X-P_i∣∣^2 − w_i = ∣∣X-P_j∣∣^2 − w_j
45# (x-x_i)^2 + (y-y_i)^2 + (z-z_i)^2 - w_i = (x-x_j)^2 + (y-y_j)^2 + (z-z_j)^2 - w_j
46# 2x(x_j-x_i) + 2y(y_j-y_i) + 2z(z_j-z_i) = (x_j^2 + y_j^2 + z_j^2) - (x_i^2 + y_i^2 + z_i^2) - (w_j-w_i)
47# 2X⋅(P_i-P_j) = ∣∣P_i∣∣^2 − ∣∣P_j∣∣^2 − (w_i−w_j)
48def solve_power_center(pts, weights):
49 A, B = [], []
50 p0, w0 = pts[0], weights[0]
51
52 for i in range(1, 4):
53 pi, wi = pts[i], weights[i]
54 A.append([2 * (pi[j] - p0[j]) for j in range(3)])
55 B.append(np.sum(pi**2) - np.sum(p0**2) - (wi - w0))
56
57 try:
58 res = np.linalg.solve(np.array(A, dtype=float), np.array(B, dtype=float))
59 return rg.Point3d(float(res[0]), float(res[1]), float(res[2]))
60 except:
61 return None
62
63###### Parallel Operations Section: Individual Task Functions
64# Generate Voronoi Cell (polyhedron)
65def construct_single_cell(i, simplices, p_centers, B_mesh, move_back, tol):
66 try:
67 # i: Index of Seed Point (from. Input: P)
68 # Vertex extraction
69 sim_indices = [idx for idx, sim in enumerate(simplices) if i in sim]
70 if len(sim_indices) < 4: return None
71 v_pts = [p_centers[idx] for idx in sim_indices if p_centers[idx] is not None]
72 if len(v_pts) < 4: return None
73
74 # Vertex integration stabilization (rounding to 7th digit)
75 snapped = [rg.Point3d(round(v.X, 7), round(v.Y, 7), round(v.Z, 7)) for v in v_pts]
76 unique_v = list(dict.fromkeys(snapped))
77 if len(unique_v) < 4: return None
78
79 # Create Meshes
80 temp_mesh = rg.Mesh.CreateConvexHull3D(unique_v, tol, tol)
81 if not temp_mesh: return None
82 cell_mesh = temp_mesh[0]
83
84 # Trim with Voronoi border (from. Input: B)
85 intersections = rg.Mesh.CreateBooleanIntersection([cell_mesh], [B_mesh])
86
87 if intersections and len(intersections) > 0:
88 res_mesh = intersections[0]
89 res_mesh.Transform(move_back)
90 return res_mesh
91
92 except Exception as e:
93 return None
94 return None
95
96###### Generate Bounding Seed Points
97def get_26_anchors(bbox, scale=5.0):
98 big_box = bbox
99 big_box.Inflate(bbox.Diagonal.Length * scale)
100
101 corners = []
102 for x in [0, 0.5, 1]:
103 for y in [0, 0.5, 1]:
104 for z in [0, 0.5, 1]:
105 if x == 0.5 and y == 0.5 and z == 0.5: continue
106 corners.append(big_box.PointAt(x, y, z))
107 return corners
108
109############################################################
110### MAIN ### 3D Weighted Voronoi (POWER DIAGRAM) Algorithm
111############################################################
112def main():
113 if not P or not R or not B:
114 return [], None
115
116 ### --- 1. Data Set
117 W = [r**2 for r in R]
118
119 anchor_pts = get_26_anchors(rg.BoundingBox(P), 2.0)
120 total_pts = P + anchor_pts
121 total_ws = W + [0.0] * len(anchor_pts)
122
123 ## Move closer to World Origin
124 pts_center = rg.BoundingBox(total_pts).Center
125 move_back = rg.Transform.Translation(rg.Vector3d(pts_center))
126 P_local = [p - pts_center for p in total_pts]
127
128 move_matrix = rg.Transform.Translation(-rg.Vector3d(pts_center))
129 B_local = B.DuplicateBrep()
130 B_local.Transform(move_matrix)
131
132 ## Convert to numpy.array
133 np_pts = np.array([[p.X, p.Y, p.Z] for p in P_local], dtype=float)
134 np_ws = np.array(total_ws, dtype=float)
135
136 ## Normalize Input Data
137 max_coord = np.max(np.abs(np_pts))
138 if max_coord == 0:
139 max_coord = 1.0
140 np_pts_norm = np_pts / max_coord
141 np_ws_norm = np_ws / (max_coord**2)
142
143 ### --- 2. 4D Lifting & Convex Hull
144 # 4th Coord = x^2+y^2+z+2-w, w=r^2
145 lifted_coord = np.sum(np_pts_norm**2, axis=1) - np_ws_norm
146 lifted = np.column_stack((np_pts_norm, lifted_coord))
147
148 hull = ConvexHull(lifted, qhull_options='QJ')
149
150 ## Select Lower Hull & Simplex Validation
151 # hull.equations: numpy.list(A B C D E) of Ax+By+Cz+Dw+E = 0
152 lower_hull_indices = []
153 for i, eq in enumerate(hull.equations):
154 if eq[3] < -1e-7:
155 pts_4d = lifted[hull.simplices[i]]
156 vol = get_simplex_volume_3d(pts_4d)
157 if abs(vol) > 1e-7:
158 lower_hull_indices.append(i)
159
160 # hull.simplices: tetrahedron on 4D
161 # simplices: 3D Regular Triangulation (tetrahedron)
162 simplices = [hull.simplices[idx] for idx in lower_hull_indices]
163
164 ### --- 3. Get Power Center of each simplices
165 p_centers = []
166 for sim in simplices:
167 center_norm = solve_power_center(np_pts_norm[sim], np_ws_norm[sim])
168 # Return to original coord
169 if center_norm is not None:
170 p_centers.append(center_norm * max_coord)
171 else:
172 p_centers.append(None)
173
174 ### --- 4. Generate Voronoi Cell
175 ## Convert Voronoi border(Brep) to Mesh (from. Input: B)
176 mp = rg.MeshingParameters.Default
177 raw_meshes = rg.Mesh.CreateFromBrep(B_local, mp)
178 final_bd_mesh = rg.Mesh()
179 for m in raw_meshes:
180 final_bd_mesh.Append(m)
181 final_bd_mesh.Vertices.CombineIdentical(True, True)
182 final_bd_mesh.Compact()
183 final_bd_mesh.Normals.ComputeNormals()
184
185 ## Parallel Operations (Generate Voronoi Cell)
186 num_pts = len(P)
187 result_cells = [None] * num_pts
188
189 def parallel_body(i):
190 result_cells[i] = construct_single_cell(i, simplices, p_centers, final_bd_mesh, move_back, tol)
191
192 tasks.Parallel.For(0, num_pts, options, parallel_body)
193
194 ### --- 5. Extract 3D Regular Triangulation (WireFrame)
195 d_lines = []
196 edges = set()
197
198 num_input_pts = len(P)
199
200 ## Possible edge combinations from a tetrahedron (6 total)
201 # (0,1), (0,2), (0,3), (1,2), (1,3), (2,3)
202 for sim in simplices:
203 combinations = [
204 (sim[0], sim[1]), (sim[0], sim[2]), (sim[0], sim[3]),
205 (sim[1], sim[2]), (sim[1], sim[3]), (sim[2], sim[3])
206 ]
207
208 for start, end in combinations:
209 s_idx = int(start)
210 e_idx = int(end)
211 if s_idx < num_input_pts and e_idx < num_input_pts:
212 edge = tuple(sorted((s_idx, e_idx)))
213 edges.add(edge)
214
215 ## Convert Edge to Rhino Line
216 for start_idx, end_idx in edges:
217 line = rg.Line(total_pts[start_idx], total_pts[end_idx])
218 d_lines.append(line)
219
220 return result_cells, d_lines
221
222############################################################
223### EXECUTE ###
224############################################################
225
226M, D = main()