# ================================================================== Ⅱ. 시뮬레이션 엔진 아키텍처 divider(slides[6], "시뮬레이션 엔진 아키텍처", "Vulkan 기반 시뮬레이션 엔진의 전체 구조, 한 프레임의 렌더 그래프, 시뮬레이션 루프, Python 과의 데이터 인터페이스를 설명한다.") def module(sl, x, y, w, h, title, desc, fill=WHITE, line=LIGHT, tcolor=NAVY, dcolor=GRAY, size=10): box(sl, x, y, w, h, "", fill=fill, line=line) tb(sl, x + 0.06, y + 0.04, w - 0.12, 0.26, title, size=size + 1, bold=True, color=tcolor) tb(sl, x + 0.06, y + 0.3, w - 0.12, h - 0.32, desc, size=size, color=dcolor) def figure(sl, path, y, w=11.8, h=4.4, x=0.85): im = Image.open(path) s_ = min(w / im.width, h / im.height) ww, hh = im.width * s_, im.height * s_ return sl.shapes.add_picture(path, Inches(x + (w - ww) / 2), Inches(y), Inches(ww), Inches(hh)) def down_arrow(sl, x, y, w=0.26, h=0.26, color=BLUE): s_ = sl.shapes.add_shape(MSO_SHAPE.DOWN_ARROW, Inches(x), Inches(y), Inches(w), Inches(h)) s_.fill.solid() s_.fill.fore_color.rgb = color s_.line.fill.background() s_.shadow.inherit = False # ------------------------------------------------------------------ 8 엔진 전체 구조도 s = slides[7] y = body(s, "엔진 구조", "시뮬레이션 엔진 전체 구조도", [ "(a) CPU 시뮬레이션 루프가 매 프레임 제어 · 자차 동역학 · 충돌 · 구간 스트리밍 · 센서 · 기록을 처리하고, (b) GPU 렌더 그래프가 가우시안 → 정렬 → 래스터 → 후처리로 영상을 만듦 (그림의 각 단계는 scene-0402 실제 데이터)", ]) figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_engine_pipeline.png", y + 0.02, h=6.98 - (y + 0.02)) notes(s, "참고한 도식 형식: 3DGS (Kerbl 2023) 최적화 파이프라인, HUGSIM · OmniRe 개요도 — 단계마다 실제 중간 결과를 보여 준다. (a) 의 장애물 지도 · 구간 · 라이다 · 속도 기록, (b) 의 가우시안 · 화면 타원 · 정렬 순서 · 하늘 · 색 · 거리는 모두 scene-0402 구간 1 모델과 실제 주행 로그에서 만든 그림이다. 엔진은 하나의 실행 파일(vkgs_viewer)이고, 창 모드(시뮬레이터)와 오프스크린 모드(데이터 렌더 · 증강)가 같은 렌더러를 쓴다.") # ------------------------------------------------------------------ 9 렌더 그래프 파이프라인 s = slides[8] y = body(s, "엔진 구조", "렌더 그래프 — 한 프레임의 패스 파이프라인", [ "프레임 구성은 configs/render_graph.json 이 정함 — 패스마다 쓰는 자원과 용도 (uses) 를 선언하면 그래프가 배리어 · 레이아웃 전환을 자동으로 삽입함", "가우시안 SSBO → preprocess (투영 · SH · 보이는 것만 압축) → GPU radix 정렬 → 하늘 배경 → 스플랫 래스터 (RGBA16F 색 + r32f 거리) → 후처리 — 800×450 장당 약 6.8 ms", ]) figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_engine_pipeline_b.png", y + 0.1, h=6.95 - (y + 0.1)) notes(s, "실제 패스 순서: clear_draw_args → preprocess → sort → readback_count → background → scene → precip → weather → fxaa → fxaa_resolve → to_present (camera_isp 꺼짐). 측정(800×450): preprocess 2.02 ms, sort 0.44 ms (이전 bitonic 31~35 ms 대비 13배), scene 4.38 ms. preprocess 가 200만 개 중 보이는 약 25~46만 개만 압축해 정렬 대상을 줄인다. RGBA16F 블렌딩은 32비트 대비 64 dB, 거리 누적은 센서 · 날씨 · 반사에 쓴다.") # ------------------------------------------------------------------ 10 시뮬레이션 루프 s = slides[9] y = body(s, "엔진 구조", "시뮬레이션 루프 — 한 프레임에 일어나는 일", [ "제어 입력 → 자차 적분 · 충돌 → 구간 선택 → 센서 → 렌더 → 기록의 순서로 매 프레임을 처리하며, 스크립트 제어는 50 ms 고정 스텝이라 재현 가능함", "VLA 모델은 '제어 입력' 자리에 들어와 6캠 (+ 센서) 관측을 받고 조향 · 가속을 돌려주는 구조 — 기록된 제어는 drive_log.csv 로 남음 (오른쪽: 실제 시험 주행의 속도)", ]) pic = figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_engine_pipeline_a.png", y + 0.05, h=2.75) yy = y + 0.2 + pic.height / 914400 table(s, 0.85, yy, 11.8, [["단계", "핵심 처리"], ["③ 충돌", "차체 샘플 0.1 m 격자 · 깊이 = 닿은 점 − 스침 (2) · 같은 방향 0.3 m 앞이 더 깊지 않으면 허용 (갇힘 방지)"], ["④ 구간", "담당 궤적이 가장 가까운 구간 · 3 m 히스테리시스 · 가까운 3구간 비동기 로드 · 교체 0.19초"], ["⑤ 센서", "네 바퀴 자리 지면 높이로 z · pitch · roll → 라이다 32 × 1,091 광선 DDA 4 ms · 레이더 5대 도플러"]], [0.1, 0.9], size=10, rowh=0.29) notes(s, "③ 장애물 지도의 외톨이 칸이나 화단 가장자리 한 칸에 차체 옆이 스치기만 해도 막혀 갇히던 문제를 스침 허용과 탈출 판정으로 해결했다. ⑤ 기울어진 길을 무시하면 낮은 빔의 노면 거리가 20% 넘게 짧아져 노면에 앉힌다.") # ------------------------------------------------------------------ 11 데이터 인터페이스 s = slides[10] y = body(s, "엔진 구조", "Python ↔ 엔진 데이터 인터페이스", [ "엔진과 Python 은 사람이 읽을 수 있는 파일로만 연결됨 — 각 파일은 무엇으로 만들었는지 export.json 에 남기고, 모델 · 알고리즘이 바뀌면 자동으로 다시 만듦", ]) table(s, 0.85, y + 0.15, 11.8, [ ["파일", "만드는 곳 (Python)", "내용", "엔진에서 쓰는 곳"], ["point_cloud.ply (구간마다)", "recon train", "가우시안 + 원점 주석 (nusim_origin)", "렌더러 · SegmentStreamer"], ["segments.txt", "render/segmap.py", "구간 원점 차 · 담당 궤적 점", "SegmentMap (구간 선택)"], ["rig.txt", "render/export.py", "프레임별 6캠 w2c · 자차 포즈 · 객체 박스", "재생 · 운전 카메라 · 증강"], ["map.txt · drivable.txt", "render/export.py", "맵 선 · 주행가능영역 폴리곤", "오버레이 · 연석 판정"], ["obstacles.pgm", "sim/obstacles.py", "0.2 m 장애물 격자 (정적 라이다)", "충돌 판정"], ["world.vox", "sim/voxels.py + vkgs_voxcarve", "0.2 m 복셀 + 0.5 m 지면 높이 지도", "라이다 · 레이더 · 노면 정착"], ["sky.ktx2 · IBL", "recon/sky.py · render/ibl.py", "학습 하늘 큐브맵 · 환경광 (BC6H)", "background · glTF 조명"], ["sim.json · render_graph.json", "configs/", "날씨 · 센서 · 충돌 · 카메라 · 패스 구성", "엔진 전체 설정"], ], [0.22, 0.24, 0.32, 0.22], size=10, rowh=0.42) notes(s, "하드코딩을 줄이기 위해 조정값은 모두 configs/ 에 있다. 좌표계는 nusim/data/rig.py 의 docstring 이 유일한 출처다. 모든 파일은 그 모델의 원점 기준 좌표를 쓴다.") # ================================================================== Ⅲ. 멀티뷰 시점 변환 모델 아키텍처 divider(slides[12], "멀티뷰 시점 변환 모델 아키텍처", "6캠 멀티뷰 영상을 새로운 카메라 배치로 변환하는 모델 — 구간별 3DGS 재구성 모델의 구조, 모듈, 손실 함수, 리그 변환을 설명한다.") # ------------------------------------------------------------------ 14 모델 전체 구조도 s = slides[13] y = body(s, "모델 구조", "멀티뷰 시점 변환 모델 전체 구조도", [ "구간마다 멀티뷰 사진 · 라이다로 3DGS 장면을 학습: 라이다 초기화 → 3D 가우시안 → 미분 래스터라이저 → 렌더 (색 · 깊이) → 사진 · 라이다 · 마스크 감독 → 역전파", "학습된 장면을 리그 변형 T 를 준 6캠으로 다시 렌더링해 새 시점 데이터를 생성 (그림의 각 단계는 scene-0402 실제 데이터)", ]) figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_viewpoint_model_a.png", y + 0.15, h=6.9 - (y + 0.15)) notes(s, "참고한 도식 형식: 3DGS (Kerbl 2023) Fig. 2 최적화 파이프라인 — 초기 점 → 3D 가우시안 → 투영 · 타일 래스터 → 이미지, 그리고 gradient 되돌림. 여기서는 SfM 점 대신 라이다 누적으로 초기화하고, 사진 손실에 라이다 깊이 · 하늘 · 정규화를 더했다. (b) 는 시점 변환이 학습이 아니라 '새 카메라 배치로 다시 찍기' 임을 보인다. 오른쪽 9장은 같은 순간의 앞 카메라 변형 9종이다.") # ------------------------------------------------------------------ 15 장면 표현 · 미분 렌더러 s = slides[14] y = body(s, "모델 구조", "모듈 ① 장면 표현 및 미분 래스터라이저", [ "장면 모델은 신경망이 아닌 명시적 가우시안 집합이며, 라이다로 초기화하고 3DGS-MCMC 로 개수 (최대 200만) 를 유지하며 위치를 재배치함", "미분 래스터라이저 (gsplat 1.4.0) 가 색 · 불투명도 · 깊이를 함께 렌더링하고, 그 gradient 로 모든 가우시안 파라미터를 갱신함", ]) box(s, 0.85, y + 0.15, 5.75, 3.55, "", fill=PALE, line=BLUE) tb(s, 0.95, y + 0.2, 5.5, 0.3, "가우시안 장면 G", size=13, bold=True, color=NAVY) table(s, 1.0, y + 0.58, 5.45, [["항목", "설정"], ["초기화", "정적 라이다 누적 (약 400 sweep) → 복셀 → 사진 투영 색"], ["파라미터", "μ · 스케일 · 쿼터니언 · α · SH (3차, 1,000 스텝마다 차수 증가)"], ["밀도 제어", "MCMC: 상한 200만, 낮은 α 재배치, 위치 잡음 (lr 5e5)"], ["정규화", "불투명도 · 스케일 평균 각 0.01 (보이는 것만 옵션)"], ["가림", "차량 · 사람 3D 박스 → 사진 · 라이다에서 제외"]], [0.25, 0.75], size=10, rowh=0.44) box(s, 6.9, y + 0.15, 5.75, 3.55, "", fill=PALE, line=BLUE) tb(s, 7.0, y + 0.2, 5.5, 0.3, "미분 래스터라이저 (gsplat)", size=13, bold=True, color=NAVY) rs = [("투영", "Σ′ = J W Σ Wᵀ Jᵀ, 보는 방향으로 SH 색 계산"), ("타일 정렬", "16×16 타일마다 겹치는 가우시안을 깊이순 정렬"), ("알파 블렌딩", "C = Σ cᵢ αᵢ Π(1 − αⱼ), α 누적 · 기대 깊이 D"), ("역전파", "μ · Σ · α · SH 로 gradient (packed 모드)"), ("카메라 행렬", "view gradient 끔 (2.2배 느려짐) → Δ 는 별도 모듈")] for k, (t, d) in enumerate(rs): box(s, 7.05, y + 0.6 + k * 0.6, 1.5, 0.5, t, fill=NAVY, color=WHITE, size=10, bold=True) tb(s, 8.62, y + 0.62 + k * 0.6, 3.95, 0.48, d, size=10, color=GRAY, anchor=MSO_ANCHOR.MIDDLE) notes(s, "Vulkan 렌더러는 이 래스터라이저와 같은 수식(gs_common.glsl)을 쓰고, 학습기 렌더와 PSNR 56 dB 로 일치한다. 학습은 2배 다운스케일(800×450)에서 한다.") # ------------------------------------------------------------------ 16 보정 · 하늘 s = slides[15] y = body(s, "모델 구조", "모듈 ② 카메라 보정 및 하늘 모델", [ "6대 카메라의 색 · 장착 오차와 하늘을 별도 모듈로 분리하여, 가우시안이 카메라 차이나 하늘을 흡수해 생기는 floater 를 막음", ]) cards = [("색 보정 (ColorCorrection)", "카메라별 3×4 아핀 + 프레임별 RGB log 게인\n\naffine_c = [I|0] + d_c − mean(d)\nlog_gain_v = g_v − mean(g)\n\n중심화 → 보정 없이 그린 ply 가 곧\n평균 카메라 색 (증강 영상의 색 기준)"), ("외부 파라미터 보정 (CameraAdjust)", "카메라별 SE(3) Δ (회전벡터 3 + 이동 3)\n\nw2c′ = Δ · w2c\ncam→ego′ = cam→ego · Δ⁻¹\n\nlr 1e-4, 4 스텝마다, 3만 스텝까지\n사전 정규화 5e-4\n결과: 회전 0.1~0.4°, 이동 약 1 cm"), ("하늘 모델 (SkyCube)", "학습 큐브맵 256² (lr 0.01)\n\nÎ_final = Î + (1 − α) · S(광선 방향)\n\nBCE: 하늘 픽셀 α → 0,\n라이다 표면 픽셀 α → 1 (λ 0.05)\n\n저장: sky.ktx2 (RGBA16F 선형)\n→ Vulkan background 패스")] for k, (t, d) in enumerate(cards): box(s, 0.85 + k * 4.0, y + 0.2, 3.8, 2.45, d, title=t, fill=PALE, line=BLUE, size=11, align=PP_ALIGN.LEFT) why = [("분리하지 않으면", "카메라마다 다른 색 · 노출을 가우시안 색이 흡수 → 새 시점에서 얼룩"), ("분리하지 않으면", "장착 오차 (0.1~0.4°) 만큼 같은 물체가 겹쳐 흐려짐"), ("분리하지 않으면", "하늘이 9 m 거리의 밝은 가우시안 덩어리로 구워져 floater 가 됨")] for k, (t, d) in enumerate(why): x = 0.85 + k * 4.0 down_arrow(s, x + 1.77, y + 2.72, h=0.24, color=ORANGE) box(s, x, y + 3.02, 3.8, 0.85, d, title=t, fill=WHITE, line=ORANGE, size=10) notes(s, "하늘을 분리하지 않으면 하늘 일부가 가까운 불투명 가우시안으로 구워져(9 m 거리의 밝은 덩어리) 새 시점에서 떠다닌다. '하늘 아님' 전체에 α→1 을 걸면 나뭇잎 사이로 floater 가 생겨 라이다가 표면을 확인한 픽셀에만 건다.") # ------------------------------------------------------------------ 17 손실 · 학습 설정 s = slides[16] y = body(s, "모델 구조", "손실 함수 및 학습 설정", [ "사진 재구성 손실에 라이다 깊이 · 하늘 분리 · MCMC 정규화 · 카메라 보정 사전을 더해 학습함 — 사진 손실은 차량 · 사람 마스크 밖 픽셀에서만 계산", ]) box(s, 0.85, y + 0.15, 11.8, 0.62, "L = 0.8 · L1 + 0.2 · (1 − SSIM) + 0.1 · L_depth + 0.05 · (L_sky + L_surf) + 0.01 · mean(α) + 0.01 · mean(s) + 5·10⁻⁴ · R(Δ)", fill=NAVY, color=WHITE, size=14, bold=True) table(s, 0.85, y + 0.95, 6.3, [["항", "정의"], ["L1 · D-SSIM", "렌더 Î 와 실제 사진, 객체 마스크 밖 픽셀 (SSIM λ 0.2)"], ["L_depth", "|D − D_lidar| / D_lidar, 1/4 해상도, 차량 픽셀 제외"], ["L_sky · L_surf", "하늘 픽셀 −log(1−α), 라이다 표면 픽셀 −log α"], ["mean(α) · mean(s)", "MCMC 불투명도 · 스케일 정규화"], ["R(Δ)", "카메라 외부 파라미터 보정의 크기 사전"]], [0.3, 0.7], size=10, rowh=0.44) table(s, 7.4, y + 0.95, 5.25, [["학습 설정 (day_map)", "값"], ["스텝 / 구간", "15,000 (약 20분)"], ["해상도", "800×450 (2배 축소)"], ["가우시안 상한", "2,000,000"], ["lr 위치 / 스케일", "1.6e-4 (→ ×0.01) / 5e-3"], ["lr 회전 / 불투명도 / 색", "1e-3 / 5e-2 / 1e-3"], ["재배치 구간", "500 ~ 15,000 스텝"]], [0.55, 0.45], size=10, rowh=0.37) notes(s, "L_depth 는 같은 정적 라이다를 그 카메라에 투영한 깊이 지도와의 상대 오차다. 차량 픽셀의 라이다 깊이는 차 뒤 배경이라 뺀다. GPU 텐서 값을 파이썬 if 로 검사하면 매 스텝 동기화가 걸려 학습 루프에서 피한다.") # ------------------------------------------------------------------ 18 리그 변환 s = slides[17] y = body(s, "모델 구조", "모듈 ③ 멀티뷰 리그 변환 및 렌더링", [ "6대 카메라 전체에 같은 변형 T 를 주어 리그 기하를 유지한 채 '다른 차에 단 카메라' 를 만듦 — 자차 궤적 · 시각은 원본 그대로", "프레임마다 그 시각을 맡은 구간 모델로 렌더링하여 구간 경계에서도 색 · 기하 이음매 없이 이어짐 (scene-0402: 구간 5개)", ]) pic = figure(s, "/home/mm/Downloads/vkgs-sim/docs/presentation/figures/out/fig_viewpoint_model_b.png", y + 0.1, h=2.9) yy = y + 0.2 + pic.height / 914400 table(s, 0.85, yy, 11.8, [["단계", "내용"], ["1 · 변형 정의", "shift [x 앞, y 왼쪽, z 위] m · rot [roll, pitch, yaw]° — configs/aug/rig_variants.json (9종)"], ["2 · 캘리브 변환", "R′ = Rz(yaw) · R · Rx(−pitch) · Rz(roll), t′ = t + shift → w2c′ = (ego → world · cam → ego′)⁻¹ · Δ"], ["3 · 담당 구간", "CAM_FRONT 시각 ∈ [own0, own1) 인 구간 모델 (겹침 10 m 의 가운데에서 자름)"], ["4 · 렌더 · 출력", "구간마다 rig.txt → 오프스크린 6캠 (FXAA 끔, 하늘 큐브맵) → //*.jpg + meta.json"]], [0.16, 0.84], size=10, rowh=0.31) notes(s, "변환 공식은 nusim/aug/rig_aug.py transform_calib 그대로다. pitch + 는 카메라가 아래로 숙이는 방향, yaw + 는 왼쪽으로 돌리는 방향. 리그 전체에 같은 T 를 주므로 6캠 사이의 상대 기하가 실제 차량처럼 일관된다.")