# Curve Smoothing 전략 (Phase 0 — 분석 + 1차 패치) 작성: pyvista-renderer subagent 배경: 사용자 피드백 #2 — "곡선은 float 형태이므로, 매끄럽지 않게 보일 수 밖에 없으니, 매끄럽고 우수한 품질을 위한 고민 필요" 문제: ogee 프로파일·radial gate 스킨·실린더 등 모든 곡선이 polyline/저해상도 디스크리션으로 빌드되어 줌인 시 직선 분절(faceting)로 보임. --- ## 1. 거친 곡선 카탈로그 | 부위 | 파일:라인 | 현재 점/단편 개수 | 시각 품질 (1-5) | 우선순위 | |---|---|---|---|---| | **Spillway ogee 프로파일** | `gate_3d_builder.py:78` (`_build_spillway_body`) | DXF 추출 5~50점, 기본값 8점 | **2** (정점에서 꺾임 명확) | **P1** ← 1차 패치 | | **Radial gate skin (Tainter)** | `gate_3d_builder.py:428` (`_make_radial_skin`, `n_circ=16`) | 17 ribs × 2 = 34점 | 3 (옆면 폴리곤 가시) | P2 | | **Pier nose (삼각 물가르기)** | `gate_3d_builder.py:307` (`_make_pier_nose`) | 평면 삼각형 1개 | 4 (의도된 sharp edge) | P4 (변경 불필요) | | **Gate arm (tube)** | `gate_3d_builder.py:480` (`tube(n_sides=8)`) | 8각형 단면 | 3 | P3 | | **Intake-tower jack/crane 실린더** | `intake_tower_3d_builder.py:191,302` (`Cylinder(resolution=16)`) | 16각형 | 3 | P3 | | **Valve/handle/flange 실린더** | `valve_chamber_3d_builder.py:200,221,251,265,302,365,378` (`resolution=8~20`) | 8~20각형 | 2~3 (handle 16, flange 16, weep 12, anchor 8 — anchor가 특히 거침) | P2 | | **Anchor bar (옹벽)** | `retaining_wall_3d_builder.py:212` (`Cylinder(resolution=8)`) | 8각형 (수십~수백 개) | 2 (다수 객체라 누적 거침) | P2 | | **Weep hole** | `retaining_wall_3d_builder.py:302` (`resolution=12`) | 12각형 | 3 | P3 | | **Backfill 경사면 (옹벽)** | `retaining_wall_3d_builder.py:_build_backfill` | 8 vertex 박스 | 4 (경사 자체 OK, normal 이슈 없음) | P4 | | **Polygon reconstructor 외곽선** | `polygon_reconstructor.py` 전반 | 원본 LINE 그대로 | 3 (사용처에 따라 다름) | P3 | **핵심 인사이트**: - "곡선이 거침"은 **두 종류**로 나뉜다: 1. **Sweep 단면 곡선** (ogee 프로파일) — 점 개수가 적어 extrude 시 단면 자체가 각짐 → **Spline densify**가 정공법. 2. **회전체 다각형화** (Cylinder, tube) — `resolution=N` 인자로 단순 증가 가능 → **저비용 win**. - `mesh.smooth_taubin()`은 사후 적용으로 normal을 부드럽게 만들지만, **각진 면 자체를 점 추가로 늘려주지는 않음** (정점 수 동일). 이미 직선인 polyline에는 효과 제한. - `mesh.subdivide(nsub=2)`는 cell이 4배 증가 — 단순한 box face들에 적용하면 GPU 낭비. **곡면 의심 영역에만 선택 적용**해야. --- ## 2. 권장 smoothing 전략 매트릭스 | 부위 | 전략 | 새 점 개수 | 예상 cell 증가 | 성능 영향 | 1차 패치 포함? | |---|---|---|---|---|---| | **Ogee profile** | `scipy.interpolate.CubicSpline` (parametric, x→z 단조 아니므로 arc-length 매개화) | 8~50 → ×4 (32~200) | sweep cell ×4 ≈ +수백 | 무시 가능 | **✓ 1차 적용** | | Radial gate skin | `n_circ` 16 → 32 또는 CubicSpline ang | 34 → 66 | +30 cell | 무시 | P2 | | Cylinder (jack, valve body) | `resolution` 16 → 32 | +16 cells/cyl | 객체당 무시 | P3 | | Anchor bar | `resolution` 8 → 16 (개수 많음 주의) | +8 cells × N개 | 격자 200개면 +1.6K cells | 60 FPS 영향 가능 — **격자 cap 200 유지 필수** | P3 | | 임의 곡면 메시 (사후) | `mesh.smooth_taubin(n_iter=10, pass_band=0.1)` | 정점 수 유지 | +0 | 무시 (CPU 1회) | P3 | | Box face (subdivide 불필요) | — | — | — | — | 적용 안 함 | | 폴리곤 외곽 (chamber, pier) | Chaikin / B-spline 보간 | ×2~×4 | side cells ×2~4 | 가능 (큰 다각형은 100K cell 위험) | P2 — **선택 적용** | --- ## 3. 1차 패치 — Ogee profile spline (gate_3d_builder.py) ### 변경 위치 `_build_spillway_body()` 내부, `closed_pts_2d` 생성 전에 ogee profile (x, z)를 **CubicSpline로 4× 보간**. ### 핵심 함수 추가 ```python def _densify_profile(profile_2d, n_factor=4, n_min=4): """(x, z) 프로파일 점을 arc-length parametric CubicSpline로 보간. 이유: ogee는 단조 함수가 아닐 수 있고(상류 옹벽 수직부에서 x=동일이 여러 z), parametric (s, x), (s, z) 곡선이 안전하다. n_factor: 출력 점 개수 = max(n_factor * len(profile), n_min) """ import numpy as np from scipy.interpolate import CubicSpline if profile_2d is None or len(profile_2d) < 4: return list(profile_2d) if profile_2d else [] pts = np.asarray(profile_2d, dtype=float) # arc-length 누적 거리(매개변수 s) diffs = np.diff(pts, axis=0) seg_len = np.sqrt((diffs ** 2).sum(axis=1)) s = np.concatenate([[0.0], np.cumsum(seg_len)]) if s[-1] <= 1e-9: return list(profile_2d) # 정규화 [0, 1] s_norm = s / s[-1] cs_x = CubicSpline(s_norm, pts[:, 0], bc_type='natural') cs_z = CubicSpline(s_norm, pts[:, 1], bc_type='natural') n_out = max(n_factor * len(profile_2d), n_min) s_new = np.linspace(0.0, 1.0, n_out) return list(zip(cs_x(s_new).tolist(), cs_z(s_new).tolist())) ``` ### `_build_spillway_body()` 본문 수정 변경 전 `profile = p.ogee_profile` 직후에: ```python profile = self._densify_profile(p.ogee_profile, n_factor=4) ``` ### Diff 형식 ```diff @@ class GateBuilder ... _build_spillway_body def _build_spillway_body(self): """Ogee 프로파일을 span 방향으로 extrude하여 본체 생성.""" p = self.params - profile = p.ogee_profile + # 매끄러운 곡면을 위한 CubicSpline 보간 (사용자 피드백 #2 대응) + profile = self._densify_profile(p.ogee_profile, n_factor=4) if len(profile) < 3: return @@ + @staticmethod + def _densify_profile(profile_2d, n_factor: int = 4, n_min: int = 4): + """(x, z) 프로파일 점을 arc-length parametric CubicSpline로 보간. + ogee는 단조 함수가 아닐 수 있어 parametric (s, x), (s, z) 곡선 사용. + """ + if profile_2d is None or len(profile_2d) < 4: + return list(profile_2d) if profile_2d else [] + try: + from scipy.interpolate import CubicSpline + except Exception: + return list(profile_2d) + pts = np.asarray(profile_2d, dtype=float) + diffs = np.diff(pts, axis=0) + seg_len = np.sqrt((diffs ** 2).sum(axis=1)) + s = np.concatenate([[0.0], np.cumsum(seg_len)]) + if s[-1] <= 1e-9: + return list(profile_2d) + s_norm = s / s[-1] + cs_x = CubicSpline(s_norm, pts[:, 0], bc_type='natural') + cs_z = CubicSpline(s_norm, pts[:, 1], bc_type='natural') + n_out = max(n_factor * len(profile_2d), n_min) + s_new = np.linspace(0.0, 1.0, n_out) + return list(zip(cs_x(s_new).tolist(), cs_z(s_new).tolist())) ``` ### Cell 수 영향 추정 - 기본 ogee: 8점 → 보간 후 32점 → closed_pts_2d = 34점 (앞·뒤 바닥 각 1개 추가) - prism cells = 측면 strip 34×2 + 양 끝 fan (32+32) = 68 + 64 = 132 cells - 변경 전: 8점 → 10점 → 20 + 16 = 36 cells - **+96 cells 증가**, 단일 메시 100K 임계 대비 미미함. DXF에서 50점 추출된 경우: 200점 보간 → cells ≈ 800 — 여전히 안전. --- ## 4. 적용 후 검증 방법 1. **Compile + Import 테스트** ```sh python -m py_compile gate_3d_builder.py python -c "from gate_3d_builder import GateBuilder; print('ok')" ``` 2. **메쉬 빌드 smoke test** (옵션) ```sh python gate_3d_builder.py # __main__ block에 이미 있음 ``` `meshes[0]` (spillway body)의 `n_points`, `n_cells`가 변경 전 대비 약 4배 증가 확인. 3. **시각 비교** (사용자 측에서) - `pl.add_mesh(spillway_body, show_edges=True)` 로 wireframe 비교. - Ogee 정점 부근(crest)에서 직선 분절이 곡선으로 부드럽게 변경되었는지 확인. - capture_image PNG로 before/after 저장 → AI 입력에서도 차이 확인. 4. **60 FPS 게이트** (performance-guardian 확인) - 단일 spillway body cells < 1K 유지: P1 OK. - 전체 scene 실시간 회전 시 FPS 측정 (PyVista plotter `pl.add_text(f"FPS: {pl.iren.get_event_observer_count()}")`). --- ## 5. P2/P3 후보 (다음 라운드) ### P2 — Radial gate skin (`_make_radial_skin`) - `n_circ = 16 → 32` (1줄 변경) - Cell 수 +32 — 무시. - 또는 CubicSpline로 ang_sill ↔ ang_top 사이 36점 균등. ### P2 — Polygon reconstructor 결과 (chamber, pier polygon) - `polygon_reconstructor.py`에서 반환된 polygon points를 빌더가 사용하기 전에 **Chaikin 알고리즘** 1~2회 적용해 corner를 라운딩. - 단, sanity check (`_validate_pier_polys`)와 호환 유지 (bbox 기반이므로 OK). - **주의**: 직각 corner를 의도한 pier에는 적용 부적절. layer 이름이나 geometry hint로 곡면 의심 폴리곤만 선별. ### P3 — Cylinder resolution 일괄 상향 - `gate_3d_builder.py:tube(n_sides=8)` → 16 - `intake_tower_3d_builder.py:Cylinder(resolution=16)` → 24 - `valve_chamber_3d_builder.py:Cylinder(resolution=8~20)` → 16~32 - `retaining_wall_3d_builder.py:Cylinder(resolution=8)` → 12 (격자 200개라 cell 폭증 우려; 8 → 12면 +800 cells, 안전) ### P3 — `pl.add_mesh(..., smooth_shading=True)` - VTK side에서 normal averaging만 켜면 무료로 면이 매끄러워 보임. 정점 수 변화 없음. - 단, 박스(직각 corner)에 적용 시 corner가 둥글어 보여 이상해짐 → **곡면 mesh에만 선별 적용**. ### P3 — `mesh.smooth_taubin(n_iter=10, pass_band=0.1)` post-build - ogee/radial skin 등에 추가 적용 가능. CPU에서 1회 — 인터랙션 무관. ### P4 — NURBS-level (장기) - 수문 trunnion radial sweep을 진짜 parametric surface로 정의 → VTK `vtkParametricSpline`. - 효과 크지만 빌더 구조 변경 큼. Phase 1+에서 검토. --- ## Appendix: 왜 ogee를 1차 적용 대상으로? - 사용자 줌인 시 **가장 먼저 보는 부분이 spillway 본체 단면** (ogee 곡선이 spillway의 시그니처 형상). - DXF 파서가 추출하는 점 수 자체가 적은 케이스(8~20점)가 흔함 → **직선 분절이 가장 명확히 보임**. - 위험 낮음: extrude 함수(`_extrude_2d_profile`, `_triangulate_prism`) 그대로, 입력 점 수만 늘어남. - 효과 즉각: 다음 빌드 결과부터 곡면이 부드러워짐.