<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://renderer86.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://renderer86.github.io/" rel="alternate" type="text/html" /><updated>2026-04-02T17:32:32+00:00</updated><id>https://renderer86.github.io/feed.xml</id><title type="html">　</title><subtitle>Besisi Research</subtitle><entry><title type="html">내가 게임을 만드는 이유</title><link href="https://renderer86.github.io/game_was_a_small_society" rel="alternate" type="text/html" title="내가 게임을 만드는 이유" /><published>2026-03-14T00:00:00+00:00</published><updated>2026-03-14T00:00:00+00:00</updated><id>https://renderer86.github.io/game_was_a_small_society</id><content type="html" xml:base="https://renderer86.github.io/game_was_a_small_society"><![CDATA[<ul id="markdown-toc">
  <li><a href="#내가-게임을-만드는-이유" id="markdown-toc-내가-게임을-만드는-이유">내가 게임을 만드는 이유</a></li>
  <li><a href="#우리는-같은-편이었다" id="markdown-toc-우리는-같은-편이었다">우리는 같은 편이었다</a></li>
  <li><a href="#게임이-나에게-가르쳐-준-것" id="markdown-toc-게임이-나에게-가르쳐-준-것">게임이 나에게 가르쳐 준 것</a></li>
  <li><a href="#좋은-게임이란-무엇일까" id="markdown-toc-좋은-게임이란-무엇일까">좋은 게임이란 무엇일까</a></li>
  <li><a href="#그래서-나는-게임을-만든다" id="markdown-toc-그래서-나는-게임을-만든다">그래서 나는 게임을 만든다</a></li>
</ul>

<p><br /></p>

<h1 id="내가-게임을-만드는-이유">내가 게임을 만드는 이유</h1>

<p>나는 콘텐츠가 사람의 인생을 바꿀 수 있다고 믿는다.<br />
왜냐하면 내 인생이 그렇게 만들어졌기 때문이다.</p>

<p>드라마든 영화든 콘텐츠는 사람에게 영향을 준다.<br />
하지만 게임은 조금 다르다.</p>

<p>영상은 보는 것이지만<br />
게임은 <strong>직접 선택하고 행동하는 경험</strong>이기 때문이다.</p>

<p>버튼을 누르는 순간<br />
그 이야기는 누군가의 것이 아니라<br />
<strong>내가 살아본 기억</strong>이 된다.</p>

<p>나는 어린 시절 수많은 게임 속에서 자라났다.</p>

<p>동생과 함께 즐기던 <strong>킹오파</strong>,<br />
친구들과 차례를 기다리며 토론하던 <strong>삼국지</strong>,<br />
번갈아가며 플레이하던 <strong>슈퍼마리오</strong>.</p>

<p>돌이켜 보면<br />
내가 게임에서 가장 오래 기억하는 것은<br />
레벨도 아이템도 아니었다.</p>

<p><strong>사람들이었다.</strong></p>

<p><br /></p>

<h1 id="우리는-같은-편이었다">우리는 같은 편이었다</h1>

<p>동생과 했던 킹오파는 아직도 선명하게 기억난다.</p>

<p>내 동생은 항상 나보다 게임을 더 잘했다.<br />
그래서 나는 매번 지고, 매번 화가 났다.</p>

<p>하지만 어떤 날은<br />
내가 거의 지고 있는 순간에<br />
동생이 이겨주겠다고 스틱을 받아 들고 대신 플레이해 주었다.</p>

<p>그리고 우리는 결국 그 판을 짜릿하게 이기곤 했다.</p>

<p>그 순간 나는 처음으로 이런 느낌을 받았다.</p>

<p><strong>아, 우리는 같은 편이구나.</strong></p>

<p>동네 친구들과 번갈아 싸우며<br />
같은 편이 되어 상대를 이길 때마다<br />
이상하게도 너무 즐거웠고 행복했다.</p>

<p>그때의 기억을 떠올리면<br />
게임은 단순한 경쟁이 아니라<br />
<strong>사람과 사람 사이의 경험</strong>이었다.</p>

<p><br /></p>

<h1 id="게임이-나에게-가르쳐-준-것">게임이 나에게 가르쳐 준 것</h1>

<p>온라인 게임을 만나면서<br />
그 세계는 더 넓어졌다.</p>

<p><strong>바람의 나라와 와우</strong>를 하며<br />
나는 수많은 사람들과 모험을 했다.</p>

<p>어떤 날은 리더가 되었고<br />
어떤 날은 리더를 돕는 사람이 되었고<br />
어떤 날은 팀의 핵심 역할을 맡기도 했다.</p>

<p>그 과정 속에서 나는<br />
사람들과 함께 살아가는 방식을 배웠다.</p>

<p><strong>롤과 오버워치</strong>를 하면서는<br />
내가 어떻게 성장해야 하는지도 배웠다.</p>

<p>패배를 통해 배우고<br />
팀 속에서 나의 역할을 이해하고<br />
조금 더 나아지기 위해 스스로를 돌아보는 방법.</p>

<p>그래서 나는 이렇게 생각한다.</p>

<p><strong>게임은 나에게 작은 사회였다.</strong></p>

<p>그리고 나는<br />
<strong>그 사회를 다음 세대에게 만들어 주고 싶다.</strong></p>

<p><br /></p>

<h1 id="좋은-게임이란-무엇일까">좋은 게임이란 무엇일까</h1>

<p>나에게 좋은 게임은<br />
어릴 때 <strong>디즈니 만화동산을 보기 위해 아침 8시에 일어나던 설렘</strong>과 비슷하다.</p>

<p>그 세계로 들어가고 싶은 마음.<br />
그리고 그 안에서 완전히 몰입해 버리는 경험.</p>

<p>그리고 때때로<br />
그 안에서 예상하지 못했던 감정을 만난다.</p>

<p>예를 들어<br />
<strong>파이널 판타지 7의 장면에서 느꼈던 전율</strong> 같은 순간들.</p>

<p>또 어떤 게임은<br />
삶에 대한 질문을 던지기도 했다.</p>

<p><strong>서풍의 광시곡</strong>에서 시라노가 보여준 인간다움,<br />
<strong>어쌔신 크리드</strong>가 보여준 시간과 인간의 역사,<br />
<strong>디트로이트: 비컴 휴먼</strong>이 던진 인간성에 대한 질문.</p>

<p>그 순간들을 떠올리면<br />
나는 단순히 게임을 즐겼던 것이 아니라<br />
어떤 세계를 경험했던 것 같다.</p>

<p><br /></p>

<h1 id="그래서-나는-게임을-만든다">그래서 나는 게임을 만든다</h1>

<p>그래서 나는 믿는다.</p>

<p>게임은 단순한 오락이 아니라<br />
사람의 마음에 오래 남는 경험이 될 수 있다고.</p>

<p>내가 만드는 게임 속에서도<br />
누군가는 설렘을 느끼고<br />
누군가는 새로운 세계를 만나고<br />
누군가는 잠깐이라도 위로를 얻을 수 있기를 바란다.</p>

<p>그리고 무엇보다<br />
그 게임 속에서 <strong>사람을 만났으면 좋겠다.</strong></p>

<p>좋은 친구를 만나고<br />
좋은 사람들을 발견하고<br />
세상에는 생각보다 따뜻한 사람들이 많다는 것을<br />
잠깐이라도 느낄 수 있기를 바란다.</p>

<p>그리고 게임을 끝냈을 때<br />
이런 마음이 남았으면 좋겠다.</p>

<p><strong>조금 더 좋은 사람이 되어 살아가고 싶다.<br />
사람들과 함께 더 잘 살아가고 싶다.</strong></p>

<p>어린 시절 내가 게임 속에서 받았던 것처럼.</p>

<p>그래서 나는 게임을 만든다.</p>

<p>내가 받았던 그 경험을<br />
다음 세대에게 다시 전해주기 위해.</p>]]></content><author><name>renderer86</name></author><category term="Game" /><category term="game" /><category term="relationship" /><category term="life" /><category term="game_design" /><summary type="html"><![CDATA[게임은 나에게 작은 사회였다. 게임이 나에게 가르쳐 준 관계와 성장에 대한 이야기.]]></summary></entry><entry><title type="html">UE5 Nanite: 버추얼 지오메트리의 구조와 비용</title><link href="https://renderer86.github.io/nanite" rel="alternate" type="text/html" title="UE5 Nanite: 버추얼 지오메트리의 구조와 비용" /><published>2024-12-05T00:00:00+00:00</published><updated>2024-12-05T00:00:00+00:00</updated><id>https://renderer86.github.io/nanite</id><content type="html" xml:base="https://renderer86.github.io/nanite"><![CDATA[<blockquote>

  <p><strong>이런 분이 읽으면 좋습니다!</strong></p>

  <ul>
    <li>“Nanite가 뭐가 좋은 거야?”부터 궁금한 분 — 쉬운 설명에서 시작해 점점 깊어집니다</li>
    <li>UE5 Nanite가 내부에서 어떤 순서로 무엇을 하는지 궁금한 분</li>
    <li>CPU · GPU 양쪽에서 Nanite 비용이 어디서 발생하는지 알고 싶은 분</li>
  </ul>

  <p><strong>이 글로 알 수 있는 내용</strong></p>

  <ul>
    <li>기존 LOD 방식의 한계와 Nanite가 그것을 어떻게 해결하는지</li>
    <li>Nanite Cluster가 무엇이고, 파이프라인 전체에 어떻게 녹아 있는지</li>
    <li>사전 준비 단계에서 MaterialPass까지의 전체 흐름</li>
    <li>프로파일링 데이터 기준 비용이 가장 큰 패스와 그 이유</li>
  </ul>
</blockquote>

<p><br /></p>

<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet" />

<style>
.re-post {
  --bg2: #f4f6fb;
  --bg3: #eef0f7;
  --surface: #f9fafd;
  --surface2: #eceef7;
  --border: rgba(60,80,180,0.10);
  --border2: rgba(60,80,180,0.22);
  --text: #1a1d2e;
  --text2: #464c6a;
  --text3: #8890aa;
  --accent: #3d63e0;
  --accent2: #7248d4;
  --gold: #b07d00;
  --teal: #0a8f62;
  --coral: #d63031;
  --orange: #c85a00;
}
.re-post .section-eyebrow {
  display: block;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: none;
  color: var(--accent);
  margin-bottom: 4px;
  margin-top: 56px;
}
.re-post .term-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
  margin: 28px 0;
}
.re-post .term-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 18px;
  position: relative;
  overflow: hidden;
}
.re-post .term-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
}
.re-post .term-card.blue::before   { background: var(--accent); }
.re-post .term-card.gold::before   { background: var(--gold); }
.re-post .term-card.teal::before   { background: var(--teal); }
.re-post .term-card.coral::before  { background: var(--coral); }
.re-post .term-card.purple::before { background: var(--accent2); }
.re-post .term-card.orange::before { background: var(--orange); }
.re-post .term-symbol {
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 6px;
}
.re-post .term-card.blue   .term-symbol { color: var(--accent); }
.re-post .term-card.gold   .term-symbol { color: var(--gold); }
.re-post .term-card.teal   .term-symbol { color: var(--teal); }
.re-post .term-card.coral  .term-symbol { color: var(--coral); }
.re-post .term-card.purple .term-symbol { color: var(--accent2); }
.re-post .term-card.orange .term-symbol { color: var(--orange); }
.re-post .term-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 4px;
}
.re-post .term-desc {
  font-size: 13px;
  color: var(--text2);
  line-height: 1.65;
  margin: 0;
}
.re-post .pipeline {
  display: flex;
  flex-direction: column;
  margin: 28px 0;
  position: relative;
}
.re-post .pipeline::before {
  content: '';
  position: absolute;
  left: 27px;
  top: 54px;
  bottom: 54px;
  width: 1px;
  background: linear-gradient(to bottom, var(--accent), var(--accent2));
  opacity: 0.25;
}
.re-post .pipe-item {
  display: grid;
  grid-template-columns: 54px 1fr;
  gap: 18px;
  padding: 20px 0;
  position: relative;
}
.re-post .pipe-num {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 1px solid var(--border2);
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}
.re-post .pipe-body h3 {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 6px;
}
.re-post .pipe-body p {
  font-size: 14px;
  color: var(--text2);
  line-height: 1.75;
  margin: 0;
}
.re-post .pipe-tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.re-post .pipe-tag {
  font-size: 11px;
  padding: 3px 10px;
  border-radius: 100px;
  font-weight: 600;
  letter-spacing: 0.04em;
}
.re-post .tag-cpu    { background: rgba(61,99,224,0.10); color: var(--accent); }
.re-post .tag-gpu    { background: rgba(10,143,98,0.10); color: var(--teal); }
.re-post .tag-cull   { background: rgba(114,72,212,0.10); color: var(--accent2); }
.re-post .tag-raster { background: rgba(176,125,0,0.10); color: var(--gold); }
.re-post .tag-shade  { background: rgba(200,90,0,0.10); color: var(--orange); }
.re-post .tag-stream { background: rgba(214,48,49,0.10); color: var(--coral); }
.re-post .step-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 4px;
  margin-bottom: 4px;
}
.re-post .badge-cpu    { background: rgba(61,99,224,0.12);  color: var(--accent); }
.re-post .badge-gpu    { background: rgba(10,143,98,0.12);  color: var(--teal); }
.re-post .badge-cull   { background: rgba(114,72,212,0.12); color: var(--accent2); }
.re-post .badge-raster { background: rgba(176,125,0,0.12);  color: var(--gold); }
.re-post .badge-shade  { background: rgba(200,90,0,0.12);   color: var(--orange); }
.re-post .badge-stream { background: rgba(214,48,49,0.12);  color: var(--coral); }
.re-post .callout {
  border-radius: 12px;
  padding: 18px 22px;
  margin: 24px 0;
  border: 1px solid;
  position: relative;
  overflow: hidden;
}
.re-post .callout::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
}
.re-post .callout-info { background: rgba(61,99,224,0.05); border-color: rgba(61,99,224,0.18); }
.re-post .callout-info::before { background: var(--accent); }
.re-post .callout-warn { background: rgba(176,125,0,0.05); border-color: rgba(176,125,0,0.20); }
.re-post .callout-warn::before { background: var(--gold); }
.re-post .callout-teal { background: rgba(10,143,98,0.05); border-color: rgba(10,143,98,0.20); }
.re-post .callout-teal::before { background: var(--teal); }
.re-post .callout-purple { background: rgba(114,72,212,0.05); border-color: rgba(114,72,212,0.20); }
.re-post .callout-purple::before { background: var(--accent2); }
.re-post .callout-title {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-bottom: 6px;
}
.re-post .callout-info .callout-title { color: var(--accent); }
.re-post .callout-warn .callout-title { color: var(--gold); }
.re-post .callout-teal .callout-title { color: var(--teal); }
.re-post .callout-purple .callout-title { color: var(--accent2); }
.re-post .callout p { margin: 0; font-size: 14px; color: var(--text2); line-height: 1.75; }
.re-post .callout p + p { margin-top: 8px; }
.re-post .mapping-table {
  width: 100%;
  border-collapse: collapse;
  margin: 28px 0;
  font-size: 14px;
}
.re-post .mapping-table th {
  background: var(--surface2);
  padding: 10px 14px;
  text-align: left;
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text3);
  border: 1px solid var(--border);
}
.re-post .mapping-table td {
  padding: 12px 14px;
  border: 1px solid var(--border);
  vertical-align: top;
  line-height: 1.6;
}
.re-post .mapping-table tr            { background: #ffffff; }
.re-post .mapping-table tr:nth-child(odd) { background: var(--surface); }
.re-post .mapping-table tr:hover      { background: var(--surface2); }
.re-post .mono-cell { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent); font-weight: 600; }
.re-post .num-cell  { font-family: 'JetBrains Mono', monospace; font-size: 13px; text-align: right; }
.re-post .excl-cell { color: var(--coral); font-weight: 700; }
.re-post .desc-cell { color: var(--text2); }
.re-post .bar-wrap { display: flex; align-items: center; gap: 8px; }
.re-post .bar-bg { flex: 1; height: 8px; background: var(--bg3); border-radius: 4px; overflow: hidden; }
.re-post .bar-fill { height: 100%; border-radius: 4px; }
.re-post .bar-val { font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 600; min-width: 52px; text-align: right; color: var(--text2); }
.re-post .summary-box {
  background: linear-gradient(135deg, rgba(61,99,224,0.06) 0%, rgba(114,72,212,0.06) 100%);
  border: 1px solid rgba(61,99,224,0.18);
  border-radius: 16px;
  padding: 36px;
  margin: 32px 0;
  text-align: center;
}
.re-post .summary-box h3 { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; color: var(--text); }
.re-post .summary-box p  { color: var(--text2); max-width: 600px; margin: 0 auto; font-size: 15px; line-height: 1.85; }
.re-post .two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 28px 0; }
@media (max-width: 640px) {
  .re-post .two-col { grid-template-columns: 1fr; }
  .re-post .term-grid { grid-template-columns: 1fr; }
}
.re-post .col-box { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
.re-post .col-box h4 { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.re-post .col-box ul { margin: 0; padding-left: 0; list-style: none; }
.re-post .col-box li { font-size: 13px; color: var(--text2); padding: 5px 0; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.re-post .col-box li:last-child { border-bottom: none; }
.re-post .col-box li .li-name { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--accent); font-weight: 600; flex: 1; }
.re-post .col-box li .li-val  { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text2); font-weight: 600; }
.re-post .highlight-row td { background: rgba(214,48,49,0.04) !important; }
.re-post .highlight-row .excl-cell { color: var(--coral); }
.re-post .cluster-diagram {
  background: var(--bg2);
  border: 1px solid var(--border2);
  border-radius: 14px;
  padding: 28px 32px;
  margin: 24px 0;
  font-family: 'JetBrains Mono', monospace;
}
.re-post .cluster-diagram .cd-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
  flex-wrap: wrap;
}
.re-post .cluster-diagram .cd-label {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text3);
  min-width: 90px;
}
.re-post .cd-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 600;
  padding: 4px 10px;
  border: 1px solid;
}
.re-post .cd-mesh  { background: rgba(61,99,224,0.08); border-color: rgba(61,99,224,0.25); color: var(--accent); }
.re-post .cd-cluster { background: rgba(10,143,98,0.08); border-color: rgba(10,143,98,0.25); color: var(--teal); }
.re-post .cd-tri   { background: rgba(176,125,0,0.08); border-color: rgba(176,125,0,0.25); color: var(--gold); }
.re-post .cd-page  { background: rgba(114,72,212,0.08); border-color: rgba(114,72,212,0.25); color: var(--accent2); }
.re-post .cd-arrow { color: var(--text3); font-size: 14px; }
.re-post .sub-section {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 22px 26px;
  margin: 20px 0;
}
.re-post .sub-section h4 {
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.re-post .sub-section p {
  font-size: 13px;
  color: var(--text2);
  line-height: 1.75;
  margin: 0;
}
.re-post .sub-section p + p { margin-top: 8px; }
</style>

<div class="re-post">

<span class="section-eyebrow" style="margin-top:0;">배경 — 기존 LOD의 한계</span>

<p style="color:var(--text2);line-height:1.85;margin-bottom:24px;">
  게임에서 시네마틱 품질의 바위나 조각상을 렌더링하려면, 원본 3D 에셋의 폴리곤 수가 수백만~수억 개에 달할 수 있다. 화면을 가득 채우는 오브젝트라면 몰라도, 멀리 있거나 일부만 보이는 오브젝트까지 전체 폴리곤을 계산하는 건 명백한 낭비다. 기존 게임 엔진은 이 문제를 <strong>LOD(Level of Detail)</strong>로 해결해왔다.
</p>

<div class="two-col" style="margin-bottom:32px;">
  <div class="col-box">
    <h4>기존 방식 — 수작업 LOD</h4>
    <ul>
      <li><span class="li-name">방식</span><span class="li-val" style="color:var(--text2);font-family:inherit;font-size:12px;">동일 메시를 여러 해상도로 수작업 제작</span></li>
      <li><span class="li-name">전환</span><span class="li-val" style="color:var(--text2);font-family:inherit;font-size:12px;">거리 기준으로 LOD0 → LOD1 → LOD2 교체</span></li>
      <li><span class="li-name">문제</span><span class="li-val" style="color:var(--coral);font-family:inherit;font-size:12px;">제작 비용 증가 + 팝핑 아티팩트</span></li>
    </ul>
    <p style="font-size:12px;color:var(--text2);margin-top:12px;line-height:1.65;">아티스트가 LOD를 직접 만들어야 하고, 전환 시점에 메시가 갑자기 바뀌는 <em>팝핑(popping)</em>이 발생한다. 에셋 수가 많을수록 관리 비용이 선형으로 증가한다.</p>
  </div>
  <div class="col-box">
    <h4>Nanite — 자동 가상화</h4>
    <ul>
      <li><span class="li-name">방식</span><span class="li-val" style="color:var(--text2);font-family:inherit;font-size:12px;">클러스터 계층으로 자동 LOD 대체</span></li>
      <li><span class="li-name">선택</span><span class="li-val" style="color:var(--text2);font-family:inherit;font-size:12px;">화면 픽셀 밀도 기반 런타임 선별</span></li>
      <li><span class="li-name">결과</span><span class="li-val" style="color:var(--teal);font-family:inherit;font-size:12px;">수억 폴리곤 → 실시간 렌더링</span></li>
    </ul>
    <p style="font-size:12px;color:var(--text2);margin-top:12px;line-height:1.65;">LOD를 수작업으로 만들 필요가 없다. 에셋을 그대로 임포트하면 Nanite가 화면에서 실제로 필요한 디테일만 선별해 렌더링한다.</p>
  </div>
</div>

<span class="section-eyebrow">핵심 아이디어 — 세 가지로 이해하기</span>

<p style="color:var(--text2);line-height:1.85;margin-bottom:20px;">
  Nanite가 이를 가능하게 하는 핵심 아이디어는 세 가지다. 기술적인 세부사항에 들어가기 전에 이 직관을 잡아두면, 이후 파이프라인 전체가 훨씬 자연스럽게 읽힌다.
</p>

<div class="pipeline" style="margin-bottom:8px;">
  <div class="pipe-item">
    <div class="pipe-num">A</div>
    <div class="pipe-body">
      <h3>메시를 작은 덩어리로 쪼개서 필요한 것만 선별한다</h3>
      <p>
        메시 전체를 한 번에 처리하는 대신, 최대 128개 삼각형의 덩어리(<strong>Cluster</strong>)로 분해한다. 이 클러스터들이 계층 구조(BVH)를 이루기 때문에, 카메라에서 멀수록 더 적은 클러스터를 사용하는 것이 자연스럽게 가능하다. 수억 폴리곤이라도 화면에 실제로 기여하는 클러스터만 처리한다.
      </p>
      <div class="pipe-tag-row">
        <span class="pipe-tag tag-cull">메시 → 클러스터 계층</span>
        <span class="pipe-tag tag-cull">BVH 순회로 필요한 것만 선별</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">B</div>
    <div class="pipe-body">
      <h3>"어디가 보이냐"를 싸게 먼저 기록하고, 그 다음 비싼 색칠을 한다</h3>
      <p>
        전통적인 렌더링은 삼각형을 처리하면서 바로 머티리얼(색상, 거칠기, 법선 등)을 계산한다. 문제는 나중에 다른 오브젝트에 가려져 보이지 않을 픽셀도 계산해버린다는 점이다.
      </p>
      <p style="margin-top:8px;">
        Nanite는 먼저 <strong>Visibility Buffer</strong>에 "이 픽셀은 어느 클러스터의 어느 삼각형"인지만 기록한다. 머티리얼 계산은 그 다음 별도 패스에서만 수행한다. 화면에 실제로 보이는 픽셀에 대해서만 머티리얼이 실행되므로 낭비가 없다.
      </p>
      <div class="pipe-tag-row">
        <span class="pipe-tag tag-raster">래스터 → VisibilityBuffer</span>
        <span class="pipe-tag tag-shade">별도 머티리얼 셰이딩 패스</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">C</div>
    <div class="pipe-body">
      <h3>이전 프레임으로 빠르게 컬링하고, 틀린 것을 현재 프레임으로 보정한다</h3>
      <p>
        컬링(안 보이는 것을 솎아내는 과정)의 이상적인 기준은 "현재 프레임에서 실제로 보이는지"다. 하지만 이를 정확히 알려면 렌더링을 해봐야 한다 — 닭이 먼저냐 달걀이 먼저냐의 문제다.
      </p>
      <p style="margin-top:8px;">
        Nanite는 이를 두 패스로 해결한다. <strong>Main Pass</strong>에서 이전 프레임 Depth 정보를 기준으로 빠르게 컬링·렌더링한다. 그 결과로 만든 현재 프레임 Depth를 이용해 <strong>Post Pass</strong>에서 Main Pass가 잘못 잘라낸 오브젝트를 보정한다.
      </p>
      <div class="pipe-tag-row">
        <span class="pipe-tag tag-cull">이전 프레임 HZB → Main Pass</span>
        <span class="pipe-tag tag-cull">현재 프레임 HZB → Post Pass 보정</span>
      </div>
    </div>
  </div>
</div>

<div class="callout callout-info" style="margin-bottom:40px;">
  <div class="callout-title">이 글의 나머지 구성</div>
  <p>이제 위 세 아이디어를 실제 코드 수준으로 파헤친다. Cluster 계층 구조 → 사전 준비 단계 → Two-Pass Occlusion Culling → Visibility Buffer → Base Pass → GPU/CPU 비용 → 각 패스의 상세 동작 순서로 진행된다. 앞의 직관을 떠올리며 읽으면 각 단계가 왜 존재하는지 자연스럽게 연결된다.</p>
</div>

<span class="section-eyebrow" style="margin-top:0;">구조 — Cluster가 모든 것의 기반</span>

<p style="color:var(--text2);line-height:1.85;margin-bottom:20px;">
  Nanite는 UE5의 <strong>가상화 지오메트리(Virtualized Geometry)</strong> 시스템이다. "가상화"라는 단어는 가상 메모리나 버추얼 텍스처에서 쓰는 개념과 같다. 실제로 필요한 데이터만 그때그때 메모리에 올리고, 나머지는 디스크에 두는 방식이다. Nanite는 이 개념을 지오메트리(폴리곤)에 적용한다. 수억 개의 폴리곤을 모두 메모리에 올리는 대신, <strong>화면에 보이는 부분만 런타임에 스트리밍</strong>해서 처리한다.
</p>

<div class="callout callout-info" style="margin-bottom:28px;">
  <div class="callout-title">클러스터(Cluster)란?</div>
  <p>클러스터는 메시 전체가 아니라 <strong>메시 표면의 일부분</strong>이다. 하나의 StaticMesh는 에디터에서 임포트할 때 자동으로 수백~수천 개의 클러스터로 분해된다. 각 클러스터는 최대 128개의 인접한 삼각형 묶음으로, 메시 표면을 이어붙인 조각이라고 생각하면 된다.</p>
  <p style="margin-top:8px;">"클러스터"라는 이름이 붙은 이유는 <strong>공간적으로 가까운 삼각형들을 한 묶음(cluster)으로 모았기 때문</strong>이다. 무작위로 나누는 게 아니라 위치가 인접한 삼각형끼리 그룹을 짓기 때문에, 클러스터 하나는 메시 표면의 연속된 한 영역을 나타낸다. 덕분에 클러스터 단위로 "이 영역이 화면에 보이는가"를 판단할 수 있다.</p>
</div>

<div class="sub-section" style="margin-bottom:28px;">
  <h4>에디터 빌드 시 클러스터는 어떻게 만들어지나</h4>
  <p>
    클러스터 생성은 런타임이 아니라 에디터에서 에셋을 임포트·빌드할 때 일어난다. 엔진 소스(<code>NaniteBuilder.cpp</code>)를 기준으로 흐름을 정리하면 다음과 같다.
  </p>

  <p style="margin-top:18px;font-weight:700;color:var(--text);font-size:15px;">① 인접 그래프 구성</p>
  <p>
    먼저 모든 삼각형의 엣지를 해싱해 <strong>어떤 삼각형끼리 엣지를 공유하는지</strong> 파악한다. 이렇게 하면 삼각형을 노드, 공유 엣지를 간선으로 하는 그래프가 만들어진다.
  </p>
  <p style="margin-top:8px;">
    이 그래프만 있으면 위상적으로 연결된 삼각형은 찾을 수 있다. 그런데 <strong>위상적으로는 떨어져 있지만 공간적으로는 가까운</strong> 경우도 있다. 예를 들어 입술 안쪽과 바깥쪽처럼, 실제 3D 위치는 가깝지만 메시 연결로는 아주 멀리 떨어진 삼각형들이다. 이런 경우도 같은 클러스터에 넣으면 바운딩 볼륨이 더 작아져 컬링 효율이 높아진다.
  </p>
  <p style="margin-top:8px;">
    그래서 <strong>Morton 코드</strong>를 사용한다. 각 삼각형의 중심점을 Morton 코드로 변환하면 3D 좌표가 공간적 근접도를 유지한 채 1D 숫자로 바뀐다. 이 숫자로 정렬하면 <strong>공간적으로 가까운 삼각형이 리스트에서도 가까이 모인다</strong>. O(N²) 비교 없이 O(N log N) 정렬만으로 공간적 이웃을 빠르게 찾을 수 있는 것이다. 이렇게 찾은 공간 근접 이웃 쌍에도 그래프 간선을 추가한다.
  </p>
  <p style="margin-top:8px;">
    최종 그래프는 두 종류의 간선을 갖는다. <strong>엣지 공유(위상 연결)는 강한 가중치</strong>, <strong>공간 근접은 약한 가중치</strong>. 이 가중치가 뒤에 나올 파티셔닝에서 "반드시 붙어 있어야 하는 삼각형 vs 가능하면 붙여줬으면 하는 삼각형"의 우선순위를 결정한다.
  </p>

  <p style="margin-top:18px;font-weight:700;color:var(--text);font-size:15px;">② METIS로 그래프 파티셔닝</p>
  <p>
    그래프를 N개의 파티션으로 나눠야 한다. 단순히 바운딩 박스로 공간을 자르거나 삼각형 인덱스 순서로 나누면, 파티션 경계가 메시 표면 위를 불규칙하게 가로지르게 된다. 경계가 많을수록 클러스터의 바운딩 볼륨이 커지고, 컬링이 부정확해진다.
  </p>
  <p style="margin-top:8px;">
    <strong>METIS</strong>는 이 문제를 위해 만들어진 그래프 파티셔닝 라이브러리다. METIS의 목표는 두 가지다.
  </p>
  <ul style="margin:8px 0 0 0;padding-left:20px;color:var(--text2);font-size:14px;line-height:1.8;">
    <li><strong>파티션 간 잘리는 엣지(간선) 수 최소화</strong> — 즉, 클러스터 경계를 최대한 메시의 자연스러운 경계(표면이 이어지지 않는 부분)에 맞춘다</li>
    <li><strong>각 파티션 크기 균등하게 유지</strong> — 특정 클러스터만 삼각형이 몰리지 않도록 128개 제한에 맞게 균형을 잡는다</li>
  </ul>
  <p style="margin-top:8px;">
    여기서 가중치가 의미를 갖는다. 강한 가중치(엣지 공유) 간선을 자르는 것은 큰 비용이 된다. METIS는 이 비용을 최소화하려 하기 때문에, 엣지를 공유하는 삼각형은 웬만하면 같은 파티션에 남는다. 약한 가중치(공간 근접) 간선은 "이 두 삼각형도 가능하면 같이 묶어 달라"는 힌트 역할을 한다.
  </p>
  <p style="margin-top:8px;">
    최종 결과물은 <strong>메시 표면이 연속적으로 이어진 조각들</strong>로, 각 조각이 하나의 클러스터(최대 128 삼각형)가 된다. 표면이 자연스럽게 모여 있으니 바운딩 볼륨도 빡빡하게 맞고, 런타임 컬링 정확도가 올라간다.
  </p>

  <p style="margin-top:18px;font-weight:700;color:var(--text);font-size:15px;">③ 계층 생성 — 이것이 곧 LOD다</p>
  <p>
    리프 클러스터(원본 메시에서 직접 분할된 것)가 완성되면, 이번에는 이 클러스터들을 8~32개씩 묶어 <strong>부모 클러스터</strong>를 만든다. 부모 클러스터는 자식들을 합친 뒤 폴리곤 수를 줄여야 한다. 이때 쓰는 것이 <strong>QEM(Quadric Error Metrics)</strong>이다.
  </p>
  <p style="margin-top:8px;">
    QEM은 엣지를 하나씩 붕괴(collapse)시켜 폴리곤을 줄이는 메시 단순화 알고리즘이다. 단순히 버텍스를 제거하는 게 아니라, <strong>각 버텍스가 인접한 평면들로부터 얼마나 벗어나는지를 행렬(Quadric)로 추적</strong>한다. 엣지를 붕괴할 때 이 오차가 가장 작은 엣지부터 제거하므로, 평평한 부분은 많이 줄이고 날카로운 실루엣은 최대한 보존한다. 부모 클러스터는 이렇게 단순화된 뒤 다시 128개 이하로 분할된다.
  </p>
  <p style="margin-top:8px;">
    이 과정을 반복하면 아래는 원본 해상도, 위로 갈수록 점점 단순화된 트리가 만들어진다. <strong>이것이 곧 Nanite의 LOD다</strong>. 전통적인 LOD처럼 메시 전체 단위로 교체되는 게 아니라, <strong>클러스터 단위로 독립적으로 선택</strong>된다. 같은 바위 메시라도 카메라에 가까운 쪽은 리프 클러스터(원본 해상도)를, 멀거나 가려진 쪽은 상위 클러스터(단순화된 버전)를 동시에 렌더링할 수 있다.
  </p>
  <p style="margin-top:8px;">
    각 클러스터는 빌드 시 QEM에서 계산한 <strong>LODError</strong> 값을 갖는다. 런타임에 이 값과 화면 픽셀 밀도를 비교해 "이 클러스터의 오차가 1픽셀 미만이면 충분히 정확하다"고 판단한다.
  </p>

  <p style="margin-top:18px;font-weight:700;color:var(--text);font-size:15px;">④ BVH 구성과 런타임 GPU 순회</p>
  <p>
    클러스터 계층이 완성되면 마지막으로 <strong>BVH(Bounding Volume Hierarchy)</strong>를 얹는다. BVH는 클러스터들을 공간적으로 묶은 트리 구조다. 각 노드가 자신의 하위 클러스터를 모두 감싸는 바운딩 스피어를 갖는다. 덕분에 루트 노드부터 "이 범위가 카메라에 안 보이면 하위 전체 스킵"이 가능해진다.
  </p>
  <p style="margin-top:8px;">
    BVH를 만들 때 어떻게 노드를 나눌지가 중요하다. 단순히 중간값으로 반으로 나누면 한쪽이 폴리곤이 몰리거나 바운딩 볼륨이 비효율적으로 커질 수 있다. Nanite는 <strong>SAH(Surface Area Heuristic)</strong>를 쓴다. SAH의 핵심 아이디어는 <em>"광선이 바운딩 볼륨에 맞을 확률은 그 표면적에 비례한다"</em>는 것이다. 분할 후 좌우 노드의 (표면적 × 자식 수)의 합이 최소가 되는 지점을 찾아 자르면, 컬링에서 탐색 비용이 통계적으로 최소화된다.
  </p>
  <p style="margin-top:8px;">
    런타임에 GPU가 이 BVH를 순회하는 것은 <strong>레이 트레이싱 전용 하드웨어(DXR/RTX)를 사용하지 않는다.</strong> 일반 <strong>Compute Shader</strong>로 작성된 코드다. 대신 "Persistent Thread" 방식을 쓴다. 고정된 수의 Compute Shader Workgroup이 계속 살아 있으면서 BVH 노드 큐에서 작업을 꺼내 처리한다. GPU Warp(wave)가 낭비 없이 계속 돌아가도록 설계된 방식으로, 일반 GPU 코드지만 GPU 병렬성을 최대한 활용한다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>클러스터 계층 구조 — 왜 계층이 필요한가</h4>
  <p>
    클러스터가 만들어지고 나면 끝이 아니다. 앞서 ③에서 설명했듯, 리프 클러스터들을 묶어 단순화한 부모 클러스터를 반복해서 만들면 트리 구조가 생긴다. <strong>이 트리 자체가 LOD 계층이다.</strong>
  </p>
  <p style="margin-top:8px;">
    아래를 보면 동일한 메시가 4단계로 표현된다. 리프(Level 0)는 원본 해상도, Level 1은 8~32개 리프를 병합·단순화한 버전, 위로 갈수록 더 단순해져서 루트(최상위)는 메시 전체를 아주 단순하게 표현한다.
  </p>

  <div class="cluster-diagram" style="margin-top:16px;">
    <div style="font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:var(--text3);margin-bottom:18px;">에디터 빌드 시 생성되는 구조 (에셋에 저장, 런타임 불변)</div>

    <div class="cd-row">
      <span class="cd-label" style="min-width:110px;">StaticMesh</span>
      <span class="cd-box cd-mesh">원본 메시 (N 삼각형)</span>
      <span class="cd-arrow">→ METIS 분할</span>
      <span class="cd-box cd-cluster">Leaf Cluster ×수백~수천</span>
      <span class="cd-arrow" style="font-size:11px;">각 ≤128 삼각형</span>
    </div>

    <div class="cd-row">
      <span class="cd-label" style="min-width:110px;">LOD 계층</span>
      <span class="cd-box cd-cluster">Level 0 (Leaf)</span>
      <span class="cd-arrow">원본 해상도</span>
      <span class="cd-box cd-cluster">Level 1</span>
      <span class="cd-arrow">단순화</span>
      <span class="cd-box cd-cluster">Level 2</span>
      <span class="cd-arrow">···</span>
      <span class="cd-box cd-cluster">Root</span>
    </div>

    <div class="cd-row">
      <span class="cd-label" style="min-width:110px;">BVH</span>
      <span class="cd-arrow" style="margin-left:0;">위 계층 전체를 감싸는 공간 트리 — 런타임 컬링용</span>
    </div>

    <div style="margin-top:16px;font-size:12px;color:var(--text3);line-height:1.8;">
      루트에 가까운 클러스터일수록 항상 메모리에 상주(Resident). 리프에 가까울수록 필요할 때만 스트리밍 로드.
    </div>
  </div>

  <p style="margin-top:16px;">
    런타임에 GPU는 이 BVH를 루트부터 순회한다. 각 노드에서 "이 클러스터의 LODError가 화면 픽셀 밀도 기준으로 1픽셀 미만인가?"를 확인한다. 충분히 작으면 이 클러스터를 그리고 자식 탐색을 멈춘다. 크면 자식으로 내려간다. 결과적으로 <strong>같은 메시의 서로 다른 영역이 다른 LOD 레벨의 클러스터로 동시에 렌더링</strong>된다.
  </p>

  <p style="margin-top:10px;">
    소스 기준으로 이 판단은 꽤 직설적이다. UE는 뷰마다 <code>LODScale</code>를 미리 계산해 두고(<code>NaniteShared.cpp</code>의 <code>FPackedView::UpdateLODScales</code>), 컬링 셰이더(<code>NaniteClusterCulling.usf</code>)에서 <strong>"투영된 클러스터/노드의 화면상 크기"</strong>와 <strong><code>LODError × LODScale</code></strong>를 비교한다. 대략적인 형태는 다음과 같다.
  </p>
  <p style="margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg2);padding:10px 14px;border-radius:8px;color:var(--accent);line-height:1.9;">
    LODScale = ViewToPixels / MaxPixelsPerEdge<br />
    <br />
    // 노드: 더 내려갈지 결정<br />
    should_visit_child = projected_error &lt;= MaxParentLODError * LODScale * UniformScale<br />
    <br />
    // 클러스터: 지금 이 클러스터를 그릴지 결정<br />
    small_enough_to_draw = projected_error &gt; LODError * LODScale * UniformScale
  </p>
  <p style="margin-top:10px;">
    의미를 풀면 이렇다. 화면에 크게 보이는 영역은 <code>projected_error</code>가 크므로 더 세밀한 자식 클러스터까지 내려가고, 멀거나 작게 보이는 영역은 상위 클러스터에서 멈춘다. 이때 기준이 되는 <code>MaxPixelsPerEdge</code>는 <code>r.Nanite.MaxPixelsPerEdge</code>와 뷰별 배율(업스케일링, 동적 해상도, 품질 스케일)의 영향을 받는다. 즉 Nanite의 런타임 LOD는 "거리" 하나로 자르는 전통적인 LOD가 아니라, <strong>현재 뷰에서 허용하는 픽셀 오차 한도</strong>를 만족할 때까지 BVH를 내려가는 방식이다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>클러스터 하나에 무엇이 들어 있는가</h4>
  <p>클러스터는 렌더링에 필요한 모든 정보를 스스로 갖고 있다.</p>

  <div class="two-col" style="margin-top:14px;">
    <div class="col-box">
      <h4>지오메트리 데이터</h4>
      <ul>
        <li><span class="li-name">삼각형</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">최대 128개 (인덱스 배열)</span></li>
        <li><span class="li-name">버텍스</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">최대 256개 (위치·법선·UV 등)</span></li>
        <li><span class="li-name">LODError</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">QEM 단순화 오차 — LOD 선택 기준</span></li>
        <li><span class="li-name">BoundingSphere</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">컬링용 바운딩 스피어</span></li>
      </ul>
    </div>
    <div class="col-box">
      <h4>머티리얼 정보</h4>
      <ul>
        <li><span class="li-name">RasterMaterial</span><span class="li-val" style="font-family:inherit;color:var(--teal);font-size:12px;">래스터라이즈 단계용 ID</span></li>
        <li><span class="li-name">ShadingMaterial</span><span class="li-val" style="font-family:inherit;color:var(--gold);font-size:12px;">셰이딩 단계용 ID</span></li>
        <li><span class="li-name">MaterialRanges</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">삼각형별 머티리얼 범위</span></li>
      </ul>
      <p style="font-size:12px;color:var(--text2);margin-top:10px;line-height:1.6;">하나의 클러스터가 여러 머티리얼 슬롯에 걸칠 수 있다. 최대 64개.</p>
    </div>
  </div>

  <p style="margin-top:14px;font-weight:600;color:var(--text);">RasterMaterial — 래스터라이즈 단계에서만 쓰는 머티리얼</p>
  <p>
    Visibility Buffer에 쓸 내용은 <code>Depth | ClusterIndex | TriangleIndex</code>뿐이다. 이걸 기록하는 데 실제 머티리얼(색상, 거칠기 등)은 전혀 필요 없다. 그래서 대부분의 클러스터는 래스터라이즈 시 아무 연산도 하지 않는 저렴한 <strong>Default 머티리얼</strong>을 쓴다.
  </p>
  <p style="margin-top:8px;">
    단, 머티리얼이 버텍스 위치 자체를 바꾼다면 얘기가 달라진다. <strong>WPO(World Position Offset)</strong>는 머티리얼 내에서 버텍스를 이동시킨다. Default 머티리얼로 래스터라이즈하면 버텍스가 원래 위치에 있는 채로 기록되므로 실루엣이 틀어진다. <strong>PDO(Pixel Depth Offset)</strong>, <strong>Masked</strong>도 마찬가지로 실제 머티리얼이 필요하다. 이런 클러스터만 RasterMaterial로 실제 머티리얼 ID를 갖고, 나머지는 Default를 가리킨다.
  </p>

  <p style="margin-top:14px;font-weight:600;color:var(--text);">ShadingMaterial — GBuffer를 채우는 실제 머티리얼</p>
  <p>
    Visibility Buffer가 완성된 뒤 BasePass에서 비로소 GBuffer를 채운다. 이 단계에서는 픽셀마다 실제 BaseColor, Normal, Roughness, Metallic 등을 계산해야 한다. <strong>ShadingMaterial은 항상 실제 머티리얼이다.</strong> Visibility Buffer에서 ClusterIndex와 TriangleIndex를 읽어 어떤 클러스터의 어떤 삼각형인지 알아낸 뒤, 해당 클러스터의 ShadingMaterial로 픽셀 셰이더를 실행해 GBuffer를 채운다.
  </p>
  <p style="margin-top:8px;">
    요약하면: 래스터라이즈 단계는 <em>"어디에 무엇이 있나"를 기록</em>하는 단계라 머티리얼이 최소한으로 필요하고, 셰이딩 단계는 <em>"그것의 생김새를 계산"</em>하는 단계라 항상 실제 머티리얼이 필요하다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Page 스트리밍 — 지오메트리도 가상화된다</h4>
  <p>
    클러스터 계층 전체를 처음부터 VRAM에 올려두는 건 불가능하다. 수억 폴리곤짜리 에셋이 씬에 수백 개 있을 수 있기 때문이다. Nanite는 이를 버추얼 텍스처와 동일한 방식으로 해결한다.
  </p>

  <p style="margin-top:12px;font-weight:600;color:var(--text);">버추얼 텍스처와의 비교</p>
  <div class="two-col" style="margin-top:8px;">
    <div class="col-box">
      <h4>Virtual Texture</h4>
      <ul>
        <li><span class="li-name">단위</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">Tile (예: 128×128 픽셀)</span></li>
        <li><span class="li-name">데이터</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">픽셀 색상 데이터</span></li>
        <li><span class="li-name">요청 시점</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">UV 샘플링 시 Feedback</span></li>
      </ul>
    </div>
    <div class="col-box">
      <h4>Nanite Streaming</h4>
      <ul>
        <li><span class="li-name">단위</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">Page (~수십 KB, 여러 클러스터 묶음)</span></li>
        <li><span class="li-name">데이터</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">버텍스 위치·법선·UV·인덱스 등</span></li>
        <li><span class="li-name">요청 시점</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">래스터라이즈 중 미로드 클러스터 접근 시</span></li>
      </ul>
    </div>
  </div>

  <p style="margin-top:14px;font-weight:600;color:var(--text);">런타임 스트리밍 흐름</p>
  <p>
    GPU가 래스터라이즈 중 아직 로드되지 않은 클러스터에 접근하면, 셰이더 내에서 해당 Page 번호를 <code>RequestPageRange</code> 버퍼에 기록한다. 프레임 마지막에 <strong>Readback 패스</strong>가 이 버퍼를 CPU로 읽어온다. CPU 스트리밍 매니저는 요청된 Page를 디스크에서 비동기 로드해 다음 프레임에 VRAM에 올린다.
  </p>
  <p style="margin-top:8px;">
    이번 프레임에는 해당 클러스터 대신 이미 메모리에 있는 <strong>상위 계층 클러스터(= 단순화된 버전)</strong>가 렌더링된다. 다음 프레임에 Page가 로드되면 자동으로 더 디테일한 클러스터로 전환된다.
  </p>
  <p style="margin-top:8px;">
    <strong>루트에 가까운 클러스터(=가장 단순화된 버전)는 항상 VRAM에 상주</strong>한다. 어떤 상황에서도 최소한의 메시 형태를 렌더링할 수 있도록 보장하기 위해서다.
  </p>
</div>

<p style="color:var(--text2);font-size:14px;line-height:1.8;">
  Cluster는 컬링·래스터라이즈·셰이딩 전 단계에서 기본 단위로 사용된다. <strong>컬링</strong>은 클러스터 계층을 순회해 가시 클러스터만 선별하고, <strong>래스터라이즈</strong>는 클러스터 단위로 SW/HW 방식을 결정하며, <strong>셰이딩</strong>은 클러스터에서 TriangleIndex·Barycentric을 복원해 GBuffer를 채운다. 아래 파이프라인 전체에 걸쳐 <em>Cluster</em>라는 단어가 반복되는 이유가 여기에 있다.
</p>

<div style="margin-top:28px;">
<div class="term-grid">
  <div class="term-card blue">
    <div class="term-symbol">Cluster-based Rendering</div>
    <div class="term-name">클러스터 단위 처리</div>
    <p class="term-desc">메시 단위가 아닌 최대 128 삼각형의 클러스터 단위로 컬링·래스터라이즈·셰이딩이 이루어진다. Mesh Shader와 개념적으로 유사하지만 UE5는 Compute Shader 기반 SW 래스터라이저와 기존 VS·PS 기반 HW 래스터라이저를 조합해 사용한다.</p>
  </div>
  <div class="term-card teal">
    <div class="term-symbol">Two-Pass Occlusion Culling</div>
    <div class="term-name">2패스 오클루전 컬링</div>
    <p class="term-desc">이전 프레임 HZB로 MainPass 컬링 후, MainPass 결과로 만든 현재 프레임 HZB를 사용해 PostPass에서 새로 보이게 된 클러스터를 보정한다. "프레임 간 변화가 작다"는 가정 하에 고효율 근사 컬링을 달성한다.</p>
  </div>
  <div class="term-card gold">
    <div class="term-symbol">Visibility Buffer Rendering</div>
    <div class="term-name">가시성 버퍼 렌더링</div>
    <p class="term-desc">래스터라이즈 단계에서 픽셀당 <code>Depth|ClusterIndex|TriangleIndex</code>만 기록한다. 머티리얼 셰이딩은 이후 별도 패스에서 머티리얼별 타일을 묶어 수행. 픽셀보다 작은 삼각형의 Quad Helper-Lane 낭비 문제를 해소한다.</p>
  </div>
</div>
</div>

<span class="section-eyebrow">준비 단계 — 머티리얼 슬롯과 실행 키를 미리 만든다</span>

</div>

<h1 id="준비-단계--머티리얼-슬롯과-실행-키를-미리-만든다">준비 단계 — 머티리얼 슬롯과 실행 키를 미리 만든다</h1>

<div class="re-post">

<p style="color:var(--text2);line-height:1.85;">
  여기서 먼저 <strong>bin</strong>이 무엇인지부터 잡고 가는 게 좋다. Nanite에서 bin은 "같은 실행 경로를 쓰는 작업 묶음" 정도로 이해하면 된다. 화면의 모든 삼각형과 픽셀을 하나씩 따로 처리하면 상태 전환과 셰이더 분기가 너무 많아지므로, <strong>비슷한 방식으로 처리될 것끼리 먼저 묶어 두고</strong> 나중에 한꺼번에 처리하려는 것이다.
</p>

<p style="color:var(--text2);line-height:1.85;margin-top:16px;">
  Nanite는 이 묶음을 둘로 나눈다. <strong>Raster Bin</strong>은 "이 삼각형을 Visibility Buffer에 쓸 때 어떤 래스터 경로를 써야 하는가"를 기준으로 한 묶음이고, <strong>Shader Bin</strong>은 "나중에 Base Pass에서 이 픽셀을 어떤 머티리얼 셰이더 경로로 칠해야 하는가"를 기준으로 한 묶음이다. 즉 Raster Bin은 래스터 단계의 실행 키, Shader Bin은 셰이딩 단계의 실행 키다.
</p>

<p style="color:var(--text2);line-height:1.85;margin-top:16px;">
  왜 이런 분리가 필요할까. Nanite의 래스터 단계는 모든 머티리얼을 똑같이 처리하지 않는다. 불투명하고 단순한 머티리얼은 거의 "주소만 기록하는" 가벼운 경로로 갈 수 있지만, <strong>Masked</strong>, <strong>WPO</strong>, <strong>PDO</strong>가 섞이면 픽셀별 clip 판단이나 depth 수정이 필요해져 <strong>programmable raster 경로</strong>를 타야 한다. 반대로 Base Pass에서는 BaseColor, Normal, Roughness 등을 실제로 계산해야 하므로, 머티리얼 인스턴스와 셰이더 permutation에 따라 <strong>material shading 경로</strong>가 달라진다. Nanite가 Raster Bin과 Shader Bin을 따로 두는 이유는, 래스터 단계와 셰이딩 단계가 같은 기준으로 묶이지 않기 때문이다.
</p>

<div class="callout callout-info">
  <div class="callout-title">Raster Bin과 Shader Bin을 왜 미리 정하나</div>
  <p>GPU가 런타임에 매 삼각형, 매 픽셀마다 "이건 어떤 래스터 경로지, 어떤 셰이딩 경로지"를 다시 추론하면 분기와 테이블 조회가 너무 많아진다. 그래서 CPU가 씬 등록 시점에 머티리얼 슬롯별로 bin ID를 미리 계산해 두고, GPU는 그 결과만 읽어 바로 분류와 실행에 들어간다.</p>
  <p>독자 입장에서는 이 단계를 "나중에 컬링, 래스터, Base Pass가 반복해서 참조할 실행 키를 미리 만들어 두는 준비 단계"라고 보면 된다.</p>
</div>

<p style="color:var(--text2);line-height:1.85;margin-top:16px;">
  Nanite는 런타임에 갑자기 머티리얼 bin을 계산하지 않는다. 프리미티브가 <code>FScene</code>에 등록될 때 미리 <strong>어떤 머티리얼 슬롯이 어떤 Raster Bin / Shader Bin에 속하는지</strong> 정리해 두고, 그 결과를 GPUScene 쪽 테이블로 넘긴다.
</p>

<p style="color:var(--text2);line-height:1.85;margin-top:16px;">
  엔진 코드 이름으로는 여기에 <code>MeshDrawCommand</code> 캐싱과 <code>FNaniteMaterialSlot</code> 준비가 함께 걸려 있다. 다만 이 글에서 중요한 건 이름 자체보다 역할이다. 런타임 셰이더가 매 픽셀마다 "이 삼각형은 어떤 programmable raster 경로를 타야 하지, 어떤 material shading 경로를 타야 하지"를 다시 추론하지 않도록, CPU가 장면 등록 시점에 <strong>bin ID와 머티리얼 슬롯 인덱스</strong>를 미리 정해 둔다.
</p>

<div class="callout callout-info">
  <div class="callout-title">왜 이 단계가 Two-Pass보다 앞에 있어야 하나</div>
  <p>Two-Pass Occlusion Culling은 프레임마다 다시 실행되는 GPU 컬링 단계지만, 머티리얼 슬롯과 bin ID 준비는 오브젝트 등록/변경 시점에만 갱신되는 사전 작업이다. 즉 독자 흐름으로도 "무엇을 그릴 준비가 되어 있나"가 먼저고, 그 다음이 "이번 프레임에 실제로 무엇이 보이나"다.</p>
  <p><code>FNaniteMaterialSlot</code>에는 RasterBin과 ShadingBin 정보가 함께 들어가며, GPU는 <code>PrimitiveIndex * MaxMaterials + MaterialIndex</code> 형태의 인덱스로 이 슬롯을 읽는다. 그래서 뒤쪽의 RasterBinning, ShadeBinning, BasePass는 이 준비된 키를 바로 소비할 수 있다.</p>
</div>

<p style="color:var(--text2);line-height:1.85;">
  프로파일러에서 보이는 <code>FPrimitiveSceneInfo_CacheNaniteMaterialBins</code> 비용은 바로 이 준비 단계의 흔적이다. 평상시에는 프레임마다 큰 비용이 들지 않고, 오브젝트 추가/삭제나 머티리얼 변경처럼 캐시가 무효화될 때만 다시 계산된다.
</p>

<span class="section-eyebrow">Two-Pass Occlusion Culling — 그릴 클러스터를 선별한다</span>

</div>

<h1 id="two-pass-occlusion-culling--그릴-클러스터를-선별한다">Two-Pass Occlusion Culling — 그릴 클러스터를 선별한다</h1>

<div class="re-post">

<p style="color:var(--text2);line-height:1.85;margin-bottom:20px;">
  래스터라이즈 전에 "어떤 클러스터를 그려야 하는가"를 결정하는 단계다. 씬에 수백만 개의 클러스터가 있어도 실제로 화면에 기여하는 것은 일부뿐이다. 컬링은 이 목록을 GPU에서 완전히 처리한다. CPU는 컬링 결과를 알지 못한 채 Indirect DrawCall만 발행한다.
</p>

<p style="color:var(--text2);line-height:1.85;margin-bottom:24px;">
  컬링은 세 단계로 내려간다. <strong>인스턴스 → BVH 노드 → 클러스터</strong>. 큰 단위에서 점점 작은 단위로 좁혀가며, 각 단계에서 살아남은 것만 다음 단계로 넘긴다.
</p>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>전체 흐름 — Main Pass · HZB 빌드 · Post Pass</h4>

  <div class="cluster-diagram" style="margin-top:14px;">
    <div style="font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:var(--text3);margin-bottom:16px;">Two-Pass 흐름</div>
    <div class="cd-row">
      <span class="cd-box cd-cluster" style="min-width:100px;">Main Pass</span>
      <span class="cd-arrow">이전 프레임 HZB로 컬링</span>
      <span class="cd-box cd-tri">살아남은 클러스터 래스터라이즈</span>
      <span class="cd-arrow">→</span>
      <span class="cd-box cd-page">VisibilityBuffer (부분)</span>
    </div>
    <div class="cd-row" style="margin-top:8px;">
      <span class="cd-box cd-cluster" style="min-width:100px;">HZB 빌드</span>
      <span class="cd-arrow">Main Pass Depth → 현재 프레임 HZB 생성</span>
    </div>
    <div class="cd-row" style="margin-top:8px;">
      <span class="cd-box cd-cluster" style="min-width:100px;">Post Pass</span>
      <span class="cd-arrow">현재 프레임 HZB로 재검사</span>
      <span class="cd-box cd-tri">새로 보이게 된 클러스터 추가 래스터라이즈</span>
      <span class="cd-arrow">→</span>
      <span class="cd-box cd-page">VisibilityBuffer (완성)</span>
    </div>
    <div style="margin-top:14px;font-size:12px;color:var(--text3);line-height:1.8;">
      Main Pass에서 <strong>Occluded로 판정된 인스턴스</strong>는 <code>OccludedInstances</code> 버퍼에 저장된다. Post Pass는 전체 씬이 아닌 이 버퍼만 재검사하므로 비용이 작다.
    </div>
  </div>

  <p style="margin-top:14px;">
    왜 두 패스인가. 이전 프레임 HZB는 카메라가 움직이거나 오브젝트가 새로 등장하면 틀릴 수 있다. Main Pass는 이 불완전한 HZB로 빠르게 컬링하고 래스터라이즈한다. 래스터라이즈 결과로 <em>현재</em> 프레임의 정확한 Depth를 얻을 수 있으니, Post Pass에서 이걸로 Main Pass가 틀리게 잘라낸 것들을 보정한다. 대부분의 프레임에서 "이전 프레임과 크게 다르지 않다"는 가정이 맞으므로 Post Pass에서 추가되는 클러스터는 소수다.
  </p>

  <div class="callout callout-purple" style="margin-top:18px;">
    <div class="callout-title">왜 InitViews가 아니라 여기서 실행되나</div>
    <p><code>InitViews</code>는 "무엇이 잠재적으로 보일 수 있는가"를 CPU 중심으로 준비하는 단계다. 여기서 뷰 uniform, GPUScene, primitive relevance, 그림자 준비, 인스턴스 컬링 입력 같은 <strong>렌더링 전제조건</strong>이 갖춰진다.</p>
    <p>실제 Nanite의 Two-Pass Occlusion Culling은 그 다음 단계인 <code>RenderPrepassAndVelocity → RenderNanite → Nanite::IRenderer::DrawGeometry → FRenderer::CullRasterize</code> 안에서 실행된다. 이유는 간단하다. Main Pass 뒤에는 곧바로 <strong>현재 프레임에서 방금 만들어진 Depth/VisBuffer</strong>가 필요하고, 그걸로 <code>BuildPreviousOccluderHZB</code>를 만든 다음 바로 Post Pass를 이어서 돌려야 하기 때문이다. 즉 이 로직은 단순한 가시성 준비가 아니라, <strong>실제 래스터 결과를 중간 산출물로 소비하는 렌더 패스</strong>라서 InitViews 내부에 둘 수 없다.</p>
  </div>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>1단계 — Instance Culling CS</h4>
  <p>
    WorkGroup당 64개 스레드, 각 스레드가 인스턴스 1개를 담당한다. 소스(<code>NaniteInstanceCulling.usf</code>)를 기준으로 순서대로 다음 테스트를 통과해야 살아남는다.
  </p>

  <div class="two-col" style="margin-top:14px;">
    <div class="col-box">
      <h4>컬링 테스트 순서</h4>
      <ul>
        <li><span class="li-name">① Visibility Flag</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">Hidden 여부, 게임/에디터/캡처 모드 구분</span></li>
        <li><span class="li-name">② Frustum</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">인스턴스 바운드 vs 뷰 프러스텀</span></li>
        <li><span class="li-name">③ Distance</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">Max Draw Distance, WPO 비활성화 거리</span></li>
        <li><span class="li-name">④ Global Clip Plane</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">추가 컬링 평면 (워터 리플렉션 등)</span></li>
        <li><span class="li-name">⑤ HZB</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">이전/현재 프레임 HZB로 오클루전 판정</span></li>
      </ul>
    </div>
    <div class="col-box">
      <h4>결과 분기</h4>
      <ul>
        <li><span class="li-name">Visible</span><span class="li-val" style="font-family:inherit;color:var(--teal);font-size:12px;">Root Node를 Node 큐에 등록</span></li>
        <li><span class="li-name">Occluded (Main)</span><span class="li-val" style="font-family:inherit;color:var(--coral);font-size:12px;">OccludedInstances 버퍼에 저장</span></li>
        <li><span class="li-name">Post Pass</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">Frustum/ClipPlane 테스트 생략 (Main에서 통과)</span></li>
      </ul>
      <p style="font-size:12px;color:var(--text2);margin-top:10px;line-height:1.6;">
        Wave Intrinsic(<code>WaveInterlockedAddScalar</code>)으로 64개 스레드가 Atomic 1회로 각자의 쓰기 오프셋을 받는다. 전역 Atomic 없이 큐에 동시 기록.
      </p>
    </div>
  </div>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>2단계 — Persistent Thread BVH 순회 (NodeAndClusterCull)</h4>
  <p>
    인스턴스가 큐에 등록되면 Persistent Thread 방식으로 BVH를 순회한다. "Persistent Thread"란 GPU Workgroup이 한 번 실행되고 끝나는 게 아니라, 큐가 빌 때까지 계속 작업을 꺼내 처리하는 방식이다. DX12 기준 1,440개 Workgroup이 스레드 풀처럼 동작한다.
  </p>
  <p style="margin-top:8px;">
    각 Workgroup은 매 루프마다 <strong>Node 큐</strong>와 <strong>Cluster 큐</strong>를 확인해 남은 작업을 처리한다.
  </p>
  <p style="margin-top:8px;">
    코드 위치로 보면 이 단계는 <code>InitViews</code> 안에 숨어 있는 것이 아니라 <code>NaniteCullRaster.cpp</code>의 <code>AddPass_InstanceHierarchyAndClusterCull</code>와 <code>AddPass_NodeAndClusterCull</code>에 있다. 그리고 이 함수는 <code>CullRasterize</code> 내부에서 <strong>MainPass용 한 번</strong>, 현재 프레임 HZB를 만든 뒤 <strong>PostPass용 한 번 더</strong> 호출된다. 즉 "2단계"는 설명용 번호일 뿐, 실제 엔진 구조에서는 <strong>Nanite::CullRasterize의 컬링 코어</strong>다.
  </p>
  <p style="margin-top:8px;">
    이 위치여야 하는 이유도 명확하다. Node/ClusterCull은 단순히 가시성만 고르는 게 아니라 <code>VisibleClustersSWHW</code>, <code>OccludedInstances</code>, <code>Main/PostPassRasterizeArgsSWHW</code> 같은 <strong>바로 다음 래스터 단계가 소비할 GPU 버퍼</strong>를 직접 채운다. 또한 Post Pass에서는 Main Pass가 만든 HZB와 Main Pass 클러스터 수를 기준으로 <code>ADD_CLUSTER_OFFSET</code>을 적용해 같은 Visibility Buffer / VisibleClusters 버퍼 뒤쪽에 이어 붙여야 한다. 그래서 이 단계는 준비 단계가 아니라, 래스터라이즈와 한 몸으로 붙은 DrawGeometry 내부에 있어야 한다.
  </p>

  <p style="margin-top:14px;font-weight:600;color:var(--text);">ProcessNodeBatch — BVH 노드 처리</p>
  <p>
    Workgroup(64 스레드) 중 앞 16개가 노드 16개를 로드한다. 각 노드는 최대 4개의 자식을 가지므로 16×4 = 64개 스레드 각각이 자식 1개의 가시성을 판단한다. 판단 기준은 다음과 같다.
  </p>
  <ul style="margin:8px 0 0 0;padding-left:20px;color:var(--text2);font-size:14px;line-height:1.8;">
    <li><strong>Frustum</strong> — 자식 노드의 바운딩 스피어가 뷰 프러스텀 안에 있는가</li>
    <li><strong>LOD</strong> — 화면상 픽셀 밀도 기준으로 이 노드의 LODError가 1픽셀 미만인가. 미만이면 더 내려갈 필요가 없다</li>
    <li><strong>HZB</strong> — 노드의 바운딩 볼륨이 HZB에서 완전히 가려지는가</li>
    <li><strong>Distance</strong> — 뷰 거리 초과 여부</li>
  </ul>
  <p style="margin-top:8px;">
    테스트 결과에 따라 자식을 Node 큐(더 내려갈 비리프 노드), Cluster 큐(리프 = 실제 클러스터), 또는 Post Pass 큐(Main Pass Occluded)에 등록한다.
  </p>

  <p style="margin-top:14px;font-weight:600;color:var(--text);">ProcessClusterBatch — 클러스터 최종 판정</p>
  <p>
    Cluster 큐에서 꺼낸 클러스터마다 최종 테스트를 수행한다. LOD·HZB·Distance 통과 시 <code>VisibleClustersSWHW</code> 버퍼에 등록한다. 이때 클러스터 크기에 따라 SW 경로 또는 HW 경로가 결정된다.
  </p>
  <p style="margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg2);padding:10px 14px;border-radius:8px;color:var(--accent);line-height:1.9;">
    // SW 클러스터 → 버퍼 앞에서 채움<br />
    SW: offset 0 → 1 → 2 ...<br />
    <br />
    // HW 클러스터 → 버퍼 뒤에서 채움<br />
    HW: offset MaxVisible-1 → MaxVisible-2 ...
  </p>
  <p style="margin-top:8px;">
    SW와 HW를 같은 버퍼에 앞뒤로 채우는 이유는 크기를 미리 알 수 없기 때문이다. 양 끝에서 채우면 절대로 겹치지 않는다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>HZB 오클루전 테스트 상세</h4>
  <p>
    HZB(Hierarchical Z-Buffer)는 Depth 버퍼의 Mip Chain이다. Mip 레벨 0은 원본 해상도, 레벨이 높아질수록 더 넓은 영역의 최솟값(가장 먼 Depth)을 저장한다.
  </p>
  <p style="margin-top:8px;">
    클러스터의 바운딩 볼륨을 화면에 투영해 픽셀 범위(Rect)를 구한다. 이 Rect를 덮기에 적당한 HZB Mip 레벨을 선택하고, 해당 레벨에서 최솟값을 샘플링한다. <strong>클러스터의 가장 가까운 Depth가 HZB 샘플 최솟값보다 멀면 완전히 가려진 것</strong>이므로 Occluded 처리한다.
  </p>

  <div class="two-col" style="margin-top:14px;">
    <div class="col-box">
      <h4>Main Pass HZB</h4>
      <ul>
        <li><span class="li-name">소스</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">이전 프레임 최종 Depth</span></li>
        <li><span class="li-name">정확도</span><span class="li-val" style="font-family:inherit;color:var(--coral);font-size:12px;">카메라 이동 시 부정확 가능</span></li>
        <li><span class="li-name">실패 처리</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">OccludedInstances에 저장 → Post Pass</span></li>
      </ul>
    </div>
    <div class="col-box">
      <h4>Post Pass HZB</h4>
      <ul>
        <li><span class="li-name">소스</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">현재 프레임 Main Pass 결과</span></li>
        <li><span class="li-name">정확도</span><span class="li-val" style="font-family:inherit;color:var(--teal);font-size:12px;">현재 시점 기준으로 정확</span></li>
        <li><span class="li-name">실패 처리</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">다음 프레임에서 보정</span></li>
      </ul>
    </div>
  </div>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>CalculateSafeRasterizerArgs — 버퍼 오버플로 방지</h4>
  <p>
    컬링이 끝나면 SW·HW 각각 몇 개의 클러스터가 살아남았는지 집계한다. 만약 SW 앞에서 채운 것과 HW 뒤에서 채운 것의 합이 버퍼 최대치를 초과하면 <strong>래스터라이즈 Dispatch가 버퍼를 넘어 잘못된 메모리를 읽는 문제</strong>가 생긴다.
  </p>
  <p style="margin-top:8px;">
    <code>CalculateSafeRasterizerArgs</code>는 이 값을 Clamp해서 안전한 Indirect DrawCall 인자를 만든다. 씬이 복잡하거나 컬링 정확도가 일시적으로 낮아질 때 이 안전망이 동작한다.
  </p>
</div>

<div class="callout callout-teal">
  <div class="callout-title">컬링 결과물 — VisibleClustersSWHW</div>
  <p>이 모든 과정의 최종 산출물은 <code>VisibleClustersSWHW</code> 버퍼다. 이번 프레임에 실제로 그려야 할 클러스터 목록이다. 래스터라이저는 이 버퍼를 순회하며 Visibility Buffer를 채운다. 앞서 Visibility Buffer 픽셀의 <code>VisibleClusterIndex</code>가 클러스터 직접 ID가 아닌 이 버퍼의 인덱스인 이유다.</p>
</div>

<span class="section-eyebrow">Visibility Buffer — 클러스터가 픽셀이 되는 과정</span>

</div>

<h1 id="visibility-buffer--클러스터가-픽셀이-되는-과정">Visibility Buffer — 클러스터가 픽셀이 되는 과정</h1>

<div class="re-post">

<p style="color:var(--text2);line-height:1.85;margin-bottom:20px;">
  클러스터 계층과 BVH가 준비되면, 런타임에 GPU는 매 프레임 이 클러스터들을 화면에 래스터라이즈한다. 이때 결과물이 기존의 GBuffer가 아닌 <strong>Visibility Buffer</strong>다. Visibility Buffer가 무엇인지, 클러스터에서 어떻게 만들어지는지를 소스 코드 기준으로 살펴본다.
</p>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Visibility Buffer가 뭔가 — 픽셀에 색이 아닌 "주소"를 저장한다</h4>
  <p>
    일반적인 Deferred 렌더링은 래스터라이즈를 하면서 GBuffer에 BaseColor, Normal, Roughness 등을 바로 기록한다. Visibility Buffer는 전혀 다르다. <strong>픽셀마다 색상이 아니라 "이 픽셀에 어떤 클러스터의 어떤 삼각형이 있는가"라는 주소만 기록한다.</strong>
  </p>
  <p style="margin-top:8px;">
    포맷은 픽셀당 64비트 unsigned integer(<code>R64_UINT</code> 또는 <code>R32G32_UINT</code> 폴백)다. 이 64비트가 다음과 같이 나뉜다.
  </p>

  <div class="cluster-diagram" style="margin-top:14px;">
    <div style="font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:var(--text3);margin-bottom:14px;">Visibility Buffer 픽셀 1개의 64비트 구성</div>
    <div class="cd-row">
      <span class="cd-box cd-tri" style="min-width:140px;">상위 32비트</span>
      <span class="cd-arrow">=</span>
      <span class="cd-box cd-tri">Depth (IEEE float → uint 변환)</span>
    </div>
    <div class="cd-row" style="margin-top:8px;">
      <span class="cd-box cd-cluster" style="min-width:140px;">하위 32비트</span>
      <span class="cd-arrow">=</span>
      <span class="cd-box cd-cluster">VisibleClusterIndex (24비트)</span>
      <span class="cd-arrow">|</span>
      <span class="cd-box cd-tri">TriangleIndex (7비트)</span>
      <span class="cd-arrow">|</span>
      <span class="cd-box cd-page">Reserved (1비트)</span>
    </div>
    <div style="margin-top:14px;font-size:12px;color:var(--text3);line-height:1.8;">
      <strong style="color:var(--teal);">VisibleClusterIndex</strong> — 클러스터 자체의 ID가 아니라 이번 프레임에 <em>살아남은 클러스터 목록</em>(VisibleClustersSWHW 버퍼)의 인덱스. +1 오프셋으로 0을 "빈 픽셀" 신호로 예약.<br />
      <strong style="color:var(--gold);">TriangleIndex</strong> — 해당 클러스터 내 삼각형 번호 (0~127). 7비트로 128개 표현.<br />
      <strong style="color:var(--accent);">Depth</strong> — DeviceZ를 float 비트 그대로 uint에 저장. IEEE 754 특성상 양수 float는 uint 비교와 대소가 일치하므로 Depth Test를 정수 비교로 처리 가능.
    </div>
  </div>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>프레임 시작 — 초기화</h4>
  <p>
    매 프레임 시작 시 <code>FRasterClearCS</code> Compute Shader가 Visibility Buffer 전체를 0으로 초기화한다. 0은 VisibleClusterIndex = 0xFFFFFFFF(언패킹 시 −1, 즉 "빈 픽셀")를 의미한다.
  </p>
  <p style="margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg2);padding:10px 14px;border-radius:8px;color:var(--accent);">
    OutVisBuffer64[PixelPos] = PackUlongType(uint2(0u, 0u));  // 빈 픽셀로 초기화
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Main Pass / Post Pass도 같은 Visibility Buffer를 쓴다</h4>
  <p>
    Two-Pass Occlusion Culling에서 중요한 점은 <strong>Main Pass용 Visibility Buffer와 Post Pass용 Visibility Buffer가 따로 있는 것이 아니라는 것</strong>이다. <code>CullRasterize</code>는 프레임 시작에 <code>InitRasterContext</code>로 VisBuffer를 한 번만 만들고 클리어한 뒤, Main Pass 래스터라이즈가 먼저 값을 쓴다.
  </p>
  <p style="margin-top:8px;">
    그 다음 엔진은 Main Pass 결과로 현재 프레임 HZB를 빌드하고, Post Pass에서 <code>OccludedInstances</code>만 다시 컬링한다. 여기서 살아남은 클러스터는 같은 <code>VisibleClustersSWHW</code>와 같은 <code>VisBuffer64</code>에 <strong>추가로</strong> 기록된다. Post Pass가 별도 버퍼를 만들지 않아도 되는 이유는 픽셀 기록 자체가 <code>InterlockedMax</code> 기반이라, Main Pass가 먼저 쓴 픽셀과 Post Pass가 나중에 쓴 픽셀이 경쟁하더라도 항상 더 가까운 값만 남기 때문이다.
  </p>
  <p style="margin-top:8px;">
    소스에서도 이 의도가 드러난다. Main Pass는 기본 오프셋으로 클러스터를 쓰고, Post Pass는 <code>NANITE_RENDER_FLAG_ADD_CLUSTER_OFFSET</code>를 켜서 Main Pass 뒤쪽 인덱스 영역을 사용한다. 즉 <strong>픽셀 저장소는 하나</strong>, <strong>클러스터 목록은 Main 뒤에 Post를 이어 붙이는 구조</strong>다. 그래서 Visibility Buffer 섹션에서 말한 <code>VisibleClusterIndex</code>는 "이번 프레임 전체(Main+Post)를 통과한 클러스터 목록"에 대한 인덱스라고 보는 편이 정확하다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>SW 래스터라이저 — Compute Shader로 직접 픽셀을 쓴다</h4>
  <p>
    작은 삼각형(픽셀 수 기준으로 일정 임계 이하)은 Compute Shader가 직접 처리한다. WorkGroup 하나가 클러스터 하나를 맡아, 클러스터 내 각 삼각형을 순회하며 커버하는 픽셀에 값을 쓴다.
  </p>
  <p style="margin-top:10px;">핵심은 쓰는 방법이다. 여러 삼각형이 같은 픽셀을 동시에 덮으려 경쟁할 수 있다. Nanite는 이를 <strong><code>InterlockedMax</code>(원자적 최대값 연산) 하나로 해결</strong>한다.</p>
  <p style="margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg2);padding:10px 14px;border-radius:8px;color:var(--accent);line-height:1.9;">
    uint PixelValue = (VisibleClusterIndex + 1) &lt;&lt; 7 | TriIndex;<br />
    uint64 WriteValue = PackUlongType(uint2(PixelValue, DepthInt));<br />
    <br />
    ImageInterlockedMaxUInt64(OutVisBuffer64, PixelPos, WriteValue);
  </p>
  <p style="margin-top:10px;">
    64비트 값의 <strong>상위 32비트가 Depth</strong>이므로, <code>InterlockedMax</code>는 자연스럽게 <strong>더 가까운(Depth 값이 큰) 픽셀을 선택</strong>한다. Depth Test와 값 기록이 원자적으로 한 번에 이루어지는 것이다. 락 없이 수천 개의 GPU 스레드가 동일 픽셀에 동시에 쓰더라도 항상 가장 가까운 삼각형이 남는다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>HW 래스터라이저 — 큰 삼각형은 하드웨어에게</h4>
  <p>
    충분히 큰 삼각형은 전통적인 VS→PS 파이프라인으로 처리한다. VS가 클러스터 데이터에서 버텍스를 읽어 Clip Space로 변환하고, PS에서 SW 래스터와 동일하게 <code>InterlockedMax</code>로 Visibility Buffer에 기록한다. 포맷과 원자 연산 방식은 SW 래스터와 완전히 동일하다.
  </p>
  <p style="margin-top:8px;">
    SW와 HW 래스터라이저가 같은 Visibility Buffer에 동시에 쓸 수 있는 것도 <code>InterlockedMax</code> 덕분이다. 어느 쪽이 먼저 쓰든 결과적으로 가장 가까운 값이 남는다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>래스터라이즈 이후 — VisibleClusterIndex로 실제 클러스터를 찾는다</h4>
  <p>
    Visibility Buffer에 저장된 VisibleClusterIndex는 클러스터의 직접 ID가 아니다. 이번 프레임 컬링을 통과한 클러스터들이 담긴 <strong><code>VisibleClustersSWHW</code> 버퍼의 인덱스</strong>다. 씬에 수백만 개의 클러스터가 있어도 프레임당 실제로 보이는 건 일부이므로, 이 목록은 훨씬 작다.
  </p>
  <p style="margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg2);padding:10px 14px;border-radius:8px;color:var(--accent);line-height:1.9;">
    // EmitSceneDepthPS — Visibility Buffer 를 읽어 실제 데이터로 변환<br />
    UnpackVisPixel(VisPixel, DepthInt, VisibleClusterIndex, TriIndex);<br />
    <br />
    FVisibleCluster VisCluster = GetVisibleCluster(VisibleClusterIndex);<br />
    // → VisCluster.PageIndex, VisCluster.ClusterIndex 획득<br />
    <br />
    FCluster Cluster = GetCluster(VisCluster.PageIndex, VisCluster.ClusterIndex);<br />
    // → 실제 클러스터 데이터(버텍스·삼각형·머티리얼) 로드<br />
    <br />
    uint ShadingBin = GetMaterialShadingBin(Cluster, PrimitiveId, TriIndex);<br />
    // → 이 픽셀에 어떤 머티리얼을 써야 하는지 결정
  </p>
  <p style="margin-top:10px;">
    이 단계(<code>EmitSceneDepthPS</code>)에서 비로소 SceneDepth, ShadingMask, MaterialDepth 버퍼가 생성된다. 이후 BasePass에서 이 정보를 이용해 머티리얼별로 GBuffer를 채운다.
  </p>
</div>

<div class="callout callout-teal">
  <div class="callout-title">Visibility Buffer 방식의 핵심 이점</div>
  <p>픽셀당 저장 크기가 <strong>8바이트</strong>(64비트)다. 기존 Deferred GBuffer(BaseColor·Normal·Roughness·Depth 등)는 픽셀당 32~128바이트에 달한다. Visibility Buffer는 색상 데이터가 없으므로 래스터라이즈 단계의 메모리 대역폭이 대폭 줄어든다. 또한 픽셀보다 작은 삼각형이 아무리 많아도 실제로 화면에 보이는 픽셀에 대해서만 머티리얼이 실행되므로, 마이크로폴리곤 환경에서 셰이딩 낭비가 없다.</p>
</div>

<span class="section-eyebrow">Base Pass 렌더링 — 이제 실제 머티리얼을 칠한다</span>

</div>

<h1 id="base-pass-렌더링--이제-실제-머티리얼을-칠한다">Base Pass 렌더링 — 이제 실제 머티리얼을 칠한다</h1>

<div class="re-post">

<p style="color:var(--text2);line-height:1.85;margin-bottom:20px;">
  Visibility Buffer가 완성되면 "어느 픽셀에 어떤 클러스터의 어떤 삼각형이 있는가"는 이미 정해졌다. 하지만 아직 색은 없다. 이제 해야 할 일은 <strong>같은 머티리얼끼리 픽셀을 묶어 실제 BaseColor · Normal · Roughness 등을 계산</strong>하는 것이다. Nanite는 이때 래스터 단계용 그룹인 <strong>Raster Bin</strong>과 셰이딩 단계용 그룹인 <strong>Shader Bin(=Shading Bin)</strong>을 따로 사용한다.
</p>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Bin이란 무엇인가 — 같은 작업을 하는 것끼리 묶은 실행 단위</h4>
  <p>
    여기서 <em>Bin</em>은 "같은 렌더링 상태를 공유하는 작업 묶음" 정도로 이해하면 된다. GPU는 완전히 랜덤한 머티리얼/셰이더를 픽셀마다 뒤섞어 처리할 때보다, <strong>같은 파이프라인 상태를 쓰는 삼각형이나 타일을 한 덩어리로 모아</strong> 처리할 때 훨씬 효율적이다.
  </p>
  <p style="margin-top:8px;">
    Nanite는 이 묶음을 두 번 만든다. 첫 번째는 <strong>래스터라이즈할 때 어떤 머티리얼 경로를 써야 하는가</strong>를 기준으로 묶는 Raster Bin이고, 두 번째는 <strong>BasePass에서 어떤 픽셀 셰이더/머티리얼 셋업을 실행해야 하는가</strong>를 기준으로 묶는 Shader Bin이다.
  </p>
</div>

<div class="two-col" style="margin-bottom:28px;">
  <div class="col-box">
    <h4>Raster Bin</h4>
    <ul>
      <li><span class="li-name">기준</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">래스터 단계 머티리얼 ID</span></li>
      <li><span class="li-name">목적</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">SW/HW 래스터 실행 묶음</span></li>
      <li><span class="li-name">산출물</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">RasterBinMeta / RasterBinArgs / RasterBinData</span></li>
      <li><span class="li-name">비용 민감도</span><span class="li-val" style="color:var(--gold);font-family:inherit;font-size:12px;">래스터화 비용 · draw dispatch 수</span></li>
    </ul>
  </div>
  <div class="col-box">
    <h4>Shader Bin</h4>
    <ul>
      <li><span class="li-name">기준</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">셰이딩 단계 머티리얼 ID</span></li>
      <li><span class="li-name">목적</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">BasePass 픽셀 셰이더 실행 묶음</span></li>
      <li><span class="li-name">산출물</span><span class="li-val" style="font-family:inherit;color:var(--text2);font-size:12px;">ShadingMask / MaterialTileRemap / MaterialIndirectArgs</span></li>
      <li><span class="li-name">비용 민감도</span><span class="li-val" style="color:var(--orange);font-family:inherit;font-size:12px;">픽셀 셰이더 비용 · material pass 수</span></li>
    </ul>
  </div>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>왜 Raster Bin이 필요한가</h4>
  <p>
    Nanite의 래스터라이저는 단순히 "클러스터를 하나씩" 그리는 것이 아니라, <strong>같은 래스터 머티리얼 경로를 쓰는 삼각형 범위끼리 묶어서</strong> SW/HW 래스터 패스를 발행한다. 그 이유는 같은 bin 안의 작업은 동일한 셰이더 permutation, 동일한 programmable raster 조건, 동일한 머티리얼 테이블 해석 규칙을 공유하기 때문이다.
  </p>
  <p style="margin-top:8px;">
    이렇게 묶어두면 각 래스터 패스는 "이 bin에 속한 클러스터/삼각형만 순회"하면 된다. GPU 입장에서는 파이프라인 상태를 덜 바꾸고, 간접 인수(Indirect Args)도 bin 단위로 한 번 계산하면 되며, 동일한 경로를 타는 삼각형을 연속으로 처리하므로 캐시 지역성과 wave 효율이 좋아진다. 그래서 Nanite는 <code>AddPass_Binning</code>으로 먼저 <code>RasterBinMeta</code>, <code>RasterBinArgsSWHW</code>, <code>RasterBinData</code>를 만든 뒤 래스터라이즈를 시작한다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Masked 머티리얼이 Raster Bin에 왜 부담이 되나</h4>
  <p>
    불투명 Default 경로만 쓰는 bin은 매우 싸다. Visibility Buffer에는 결국 <code>Depth | ClusterIndex | TriangleIndex</code>만 쓰면 되기 때문이다. 하지만 <strong>Masked</strong>, <strong>PDO</strong>, <strong>WPO</strong> 같은 programmable raster가 섞이면 상황이 달라진다. 이 경우 래스터 단계에서도 실제 머티리얼 평가가 필요하고, 픽셀별 clip/depth 수정 여부를 봐야 하므로 "그냥 주소만 쓰는" 경량 경로를 쓸 수 없다.
  </p>
  <p style="margin-top:8px;">
    즉 Masked 머티리얼이 많아질수록 Raster Bin이 늘어날 뿐 아니라, 각 bin이 더 비싼 래스터 경로를 타게 된다. 특히 잎사귀, 펜스처럼 알파 테스트가 많은 콘텐츠는 Nanite의 Visibility Buffer 장점을 일부 상쇄할 수 있다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Raster Bin이 많아지면 왜 GPU 래스터화 비용이 증가하나</h4>
  <p>
    Raster Bin 수가 많다는 것은 래스터라이즈 전에 작업을 더 잘게 쪼갰다는 뜻이다. 그러면 <strong>bin별 메타데이터 생성</strong>, <strong>bin별 Indirect Args 계산</strong>, <strong>bin별 SW/HW 래스터 디스패치</strong>가 모두 늘어난다. 같은 총 삼각형 수라도 bin이 많아지면 dispatch가 잘게 분산되고, 각 bin의 작업량이 작아져 GPU occupancy가 떨어지며, 파이프라인 전환과 인자 준비 오버헤드 비중이 커진다.
  </p>
  <p style="margin-top:8px;">
    쉽게 말해 "한 번에 크게 처리할 수 있던 래스터 작업"이 "잘게 쪼개진 여러 번의 래스터 작업"으로 바뀌는 것이다. 그래서 Raster Bin 증가는 곧 <strong>draw/dispatch 수 증가 + 래스터화 전처리 증가 + GPU 래스터화 비용 증가</strong>로 이어진다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Shader Bin은 무엇이고 Raster Bin과 어떻게 다른가</h4>
  <p>
    Shader Bin은 래스터용 분류가 아니라 <strong>셰이딩용 분류</strong>다. Visibility Buffer를 읽어 <code>EmitSceneDepthPS</code>가 각 픽셀의 <code>ShadingBin</code>을 구하고, 이후 <code>ShadeBinning</code>이 화면을 64×64 타일로 나눠 "이 타일에는 어떤 Shader Bin이 존재하는가"를 기록한다.
  </p>
  <p style="margin-top:8px;">
    이후 BasePass는 머티리얼별로 64×64 타일 quad를 그린다. VS는 타일 quad를 만들고, Pixel Shader는 <strong>DepthEqual + MaterialDepth</strong>로 자기 머티리얼 픽셀만 통과시켜 GBuffer를 채운다. 즉 Raster Bin이 "어떻게 Visibility Buffer를 만들까"의 묶음이라면, Shader Bin은 "어떻게 실제 픽셀 셰이더를 실행할까"의 묶음이다.
  </p>
</div>

<div class="sub-section" style="margin-bottom:24px;">
  <h4>Shader Bin이 많아지면 왜 픽셀 셰이더 비용이 증가하나</h4>
  <p>
    Shader Bin 수가 많아질수록 화면 타일마다 더 많은 머티리얼 패스가 필요해진다. 그러면 <code>MaterialIndirectArgs</code> 인스턴스 수가 늘고, BasePass에서 머티리얼별 Indirect DrawCall이 증가하며, 같은 타일을 여러 머티리얼이 반복해서 스캔하게 된다.
  </p>
  <p style="margin-top:8px;">
    이 비용은 래스터화보다 <strong>픽셀 셰이더</strong> 쪽으로 나타난다. 같은 화면을 여러 머티리얼이 분할 점유하면 GBuffer를 채우기 위한 패스 수가 늘고, CPU에서도 <code>FNaniteMaterialPassCommand</code> 빌딩 수가 증가한다. 그래서 글 후반의 프로파일링에서 BasePass가 병목으로 크게 보이는 것이다.
  </p>
</div>

<div class="callout callout-warn">
  <div class="callout-title">최적화 관점 핵심</div>
  <p><strong>Raster Bin</strong>이 많아지면 GPU 래스터화 비용이 증가하고, <strong>Shader Bin</strong>이 많아지면 픽셀 셰이더/BasePass 비용이 증가한다. 둘은 같은 "머티리얼 다양성"에서 나오지만 GPU에 주는 압력은 다르다.</p>
  <p>결국 Nanite 최적화는 "클러스터 수만 줄이기"가 아니라, <strong>래스터 단계에서 programmable path를 얼마나 줄일지</strong>, <strong>셰이딩 단계에서 유니크 머티리얼/타일 분화를 얼마나 줄일지</strong>를 균형 있게 보는 문제다. foliage처럼 Masked가 많은 콘텐츠는 Raster Bin이, modular asset처럼 머티리얼 인스턴스가 과도한 콘텐츠는 Shader Bin이 먼저 병목이 되기 쉽다.</p>
</div>

<span class="section-eyebrow">패스 흐름</span>

</div>

<h1 id="gpu--cpu-패스-흐름">GPU · CPU 패스 흐름</h1>

<div class="re-post">

<p style="color:var(--text2);line-height:1.85;">
  Nanite는 크게 <strong>컬링 + 래스터라이즈 (DrawGeometry)</strong>, <strong>Depth 추출 (EmitDepthTargets)</strong>, <strong>머티리얼 셰이딩 (BasePass)</strong> 세 단계로 구분된다. GPU와 CPU 각각의 실행 순서를 확인해보자.
</p>

<div class="two-col">
<div class="col-box">
<h4>🖥 GPU 실행 순서</h4>
<div class="pipeline" style="margin:0;">
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G1</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">InitArgs</div>
      <h3 style="font-size:13px;">QueueState · ArgBuffer 초기화</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G2</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">MainPass — InstanceCull</div>
      <h3 style="font-size:13px;">이전 프레임 HZB로 인스턴스 컬링</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G3</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">MainPass — NodeClusterCull</div>
      <h3 style="font-size:13px;">Persistent Thread로 BVH 순회 · Cluster 선별</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G4</div>
    <div class="pipe-body">
      <div class="step-badge badge-raster">MainPass — Rasterize</div>
      <h3 style="font-size:13px;">RasterBinning → SW/HW 래스터 → VisibilityBuffer</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G5</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">Build HZB (현재 프레임)</div>
      <h3 style="font-size:13px;">MainPass Depth → 현재 프레임 HZB</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G6</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">PostPass — Cull + Rasterize</div>
      <h3 style="font-size:13px;">현재 HZB로 MainPass Occluded 인스턴스 재확인</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G7</div>
    <div class="pipe-body">
      <div class="step-badge badge-gpu">EmitDepthTargets</div>
      <h3 style="font-size:13px;">VisBuffer → Depth · Stencil · MaterialDepth</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G8</div>
    <div class="pipe-body">
      <div class="step-badge badge-shade">ShadeBinning</div>
      <h3 style="font-size:13px;">64×64 타일 → 머티리얼 분류 (3 Compute)</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G9</div>
    <div class="pipe-body">
      <div class="step-badge badge-shade">BasePass</div>
      <h3 style="font-size:13px;">머티리얼별 Indirect DrawCall → GBuffer</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">G10</div>
    <div class="pipe-body">
      <div class="step-badge badge-stream">Readback</div>
      <h3 style="font-size:13px;">스트리밍 피드백 GPU→CPU</h3>
    </div>
  </div>
</div>
</div>

<div class="col-box">
<h4>⚙️ CPU 실행 순서 (Render Thread)</h4>
<div class="pipeline" style="margin:0;">
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">C1</div>
    <div class="pipe-body">
      <div class="step-badge badge-cpu">InitContext</div>
      <h3 style="font-size:13px;">NaniteView · 컬링 버퍼 할당</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">C2</div>
    <div class="pipe-body">
      <div class="step-badge badge-cpu">InitNaniteRaster</div>
      <h3 style="font-size:13px;">FRasterizerPass 생성 · 셰이더 바인딩 (캐싱)</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">C3</div>
    <div class="pipe-body">
      <div class="step-badge badge-cpu">DrawGeometry</div>
      <h3 style="font-size:13px;">컬링·래스터 전체 RDG 커맨드 레코딩</h3>
      <p style="font-size:11px;color:var(--text3);">VisBuffer 초기화 → MainPass → HZB 빌드 → PostPass</p>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">C4</div>
    <div class="pipe-body">
      <div class="step-badge badge-cpu">EmitDepthTargets</div>
      <h3 style="font-size:13px;">Depth 추출 3패스 RDG 등록</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">C5</div>
    <div class="pipe-body">
      <div class="step-badge badge-cpu">ShadeBinning</div>
      <h3 style="font-size:13px;">머티리얼 분류 3 Compute 패스 RDG 등록</h3>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">C6</div>
    <div class="pipe-body">
      <div class="step-badge badge-cpu">BasePass</div>
      <h3 style="font-size:13px;">FNaniteMaterialPassCommand 빌딩 · Indirect DrawCall 등록</h3>
      <p style="font-size:11px;color:var(--coral);">CPU 최대 비용 — 머티리얼 수에 비례</p>
    </div>
  </div>
  <div class="pipe-item" style="padding:10px 0;">
    <div class="pipe-num" style="width:40px;height:40px;font-size:11px;">C7</div>
    <div class="pipe-body">
      <div class="step-badge badge-stream">Readback · Streaming</div>
      <h3 style="font-size:13px;">스트리밍 요청 처리 (비동기)</h3>
    </div>
  </div>
</div>
</div>
</div>

<div class="callout callout-info">
  <div class="callout-title">💡 RDG 레코딩 vs GPU 실행</div>
  <p>CPU의 C3~C6는 실제 렌더링이 아니라 <strong>RDG 커맨드 레코딩</strong>이다. 실제 GPU 실행은 RDG ExecutePass 시점에 이루어진다. CPU 프로파일링에서 BasePass가 <strong>Nanite CPU 총비용의 약 15%</strong>를 차지하는 것은, 머티리얼 수만큼 드로콜을 빌딩하는 C6 작업이 무겁기 때문이다.</p>
</div>

<span class="section-eyebrow">GPU 비용</span>

</div>

<h1 id="gpu-비용">GPU 비용</h1>

<div class="re-post">

<p style="color:var(--text2);line-height:1.85;">
  이 표는 <strong>Nanite GPU 총비용을 100</strong>으로 놓고, 각 패스가 그중 몇 퍼센트를 <strong>직접</strong> 차지하는지 보여준다. 예를 들어 5%라면 "이 패스 하나가 Nanite GPU 총비용의 약 5%를 차지한다"는 뜻이다.
</p>

<table class="mapping-table">
  <thead>
    <tr><th>Pass</th><th style="text-align:right;">비중 %</th><th>설명</th></tr>
  </thead>
  <tbody>
    <tr><td class="mono-cell">Nanite::DrawGeometry</td><td class="num-cell">~4%</td><td class="desc-cell">컬링+래스터 전체. Nanite GPU 총비용 중 컬링 디스패치 오버헤드 비중</td></tr>
    <tr><td class="mono-cell">Nanite::VisBuffer</td><td class="num-cell">~0.1%</td><td class="desc-cell">래스터 페이즈 전체. 실제 비용은 자식 패스</td></tr>
    <tr><td class="mono-cell">NaniteBasePass</td><td class="num-cell">~0.2%</td><td class="desc-cell">머티리얼 셰이딩 페이즈 컨테이너</td></tr>
    <tr class="highlight-row"><td class="mono-cell">Nanite::BasePass</td><td class="num-cell excl-cell">~19%</td><td class="desc-cell"><strong>GPU 단일 패스 최대 비용.</strong> 머티리얼별 Indirect DrawCall → GBuffer</td></tr>
    <tr class="highlight-row"><td class="mono-cell">Nanite::Readback</td><td class="num-cell excl-cell">~8%</td><td class="desc-cell">GPU→CPU 스트리밍 피드백. 동기 읽기</td></tr>
    <tr class="highlight-row"><td class="mono-cell">Nanite::EmitDepthTargets</td><td class="num-cell excl-cell">~5%</td><td class="desc-cell">VisBuffer → Depth·Stencil·MaterialDepth (FullScreen ×3)</td></tr>
    <tr class="highlight-row"><td class="mono-cell">Nanite::ShadeBinning</td><td class="num-cell excl-cell">~4%</td><td class="desc-cell">머티리얼 타일 분류 3 Compute 패스</td></tr>
    <tr><td class="mono-cell">Nanite::RasterizeLumenCards</td><td class="num-cell">~0%</td><td class="desc-cell">Lumen 캡처용 래스터라이즈</td></tr>
    <tr><td class="mono-cell">Nanite::InitContext</td><td class="num-cell">~2%</td><td class="desc-cell">View·Buffer 초기화</td></tr>
  </tbody>
</table>
<p style="color:var(--text3);font-size:12px;margin-top:-16px;">* 기준: Nanite GPU 총비용 = 100. 표의 비중 값은 각 패스가 직접 차지하는 비용 비율이다.</p>

<div class="callout callout-warn">
  <div class="callout-title">⚡ GPU 병목 핵심</div>
  <p>DrawGeometry Incl(100%)은 자식 패스를 포함한 계층 합산이다. 순수 컬링 오버헤드는 <strong>Nanite GPU 총비용의 약 4%</strong>로 전체 대비 작다. <strong>실질 GPU 최대 병목은 BasePass 머티리얼 셰이딩으로, Nanite GPU 총비용의 약 19%</strong>를 차지하며 씬 내 유니크 Nanite 머티리얼 수에 비례한다.</p>
  <p>Bin 관점으로 보면 <strong>Raster Bin 수 증가는 RasterBinning과 SW/HW Rasterize 쪽 비용</strong>을 밀어 올리고, <strong>Shader Bin 수 증가는 ShadeBinning과 BasePass 픽셀 셰이더 비용</strong>을 밀어 올린다. 그래서 "삼각형 수를 줄였다"만으로는 설명이 끝나지 않고, 어떤 bin이 늘었는지를 같이 봐야 한다.</p>
</div>

<span class="section-eyebrow">CPU 비용</span>

</div>

<h1 id="cpu-비용">CPU 비용</h1>

<div class="re-post">

<p style="color:var(--text2);line-height:1.85;">
  이 표는 <strong>Nanite CPU 총비용을 100</strong>으로 놓고, 각 패스가 Render Thread 쪽에서 몇 퍼센트를 <strong>직접</strong> 차지하는지 보여준다. 예를 들어 1.2%라면 "이 패스 하나가 Nanite CPU 총비용의 약 1.2%를 차지한다"는 뜻이다.
</p>

<table class="mapping-table">
  <thead>
    <tr><th>Pass</th><th style="text-align:right;">비중 %</th><th>설명</th></tr>
  </thead>
  <tbody>
    <tr><td class="mono-cell">Nanite::DrawGeometry</td><td class="num-cell">~3%</td><td class="desc-cell">컬링 전체 RDG 레코딩 루트</td></tr>
    <tr><td class="mono-cell">Nanite::VisBuffer</td><td class="num-cell">~0.2%</td><td class="desc-cell">VisBuffer 페이즈 루트</td></tr>
    <tr><td class="mono-cell">NaniteBasePass</td><td class="num-cell">~0.4%</td><td class="desc-cell">머티리얼 셰이딩 페이즈 루트</td></tr>
    <tr class="highlight-row"><td class="mono-cell">Nanite::BasePass</td><td class="num-cell excl-cell">~15%</td><td class="desc-cell"><strong>CPU 최대 비용.</strong> FNaniteMaterialPassCommand 빌딩 + Indirect DrawCall 등록</td></tr>
    <tr><td class="mono-cell">InitNaniteRaster</td><td class="num-cell">~0.1%</td><td class="desc-cell">FRasterizerPass 생성 (캐싱됨)</td></tr>
    <tr><td class="mono-cell">Nanite::RasterizeLumenCards</td><td class="num-cell">~0.1%</td><td class="desc-cell">Lumen 래스터 레코딩</td></tr>
    <tr><td class="mono-cell">FVirtualShadowMapArray::<wbr />RenderVSMNanite</td><td class="num-cell">~0%</td><td class="desc-cell">VSM Nanite 렌더링 레코딩</td></tr>
    <tr><td class="mono-cell">Nanite::LumenMeshCapturePass</td><td class="num-cell">~0.6%</td><td class="desc-cell">Lumen 카드 캡처 레코딩</td></tr>
    <tr class="highlight-row"><td class="mono-cell">FPrimitiveSceneInfo_<wbr />CacheNaniteMaterialBins</td><td class="num-cell excl-cell">~0.2%</td><td class="desc-cell">씬 변경 시 MaterialBin 캐싱</td></tr>
    <tr class="highlight-row"><td class="mono-cell">Nanite::ShadeBinning</td><td class="num-cell excl-cell">~1.3%</td><td class="desc-cell">머티리얼 분류 Compute 레코딩</td></tr>
    <tr class="highlight-row"><td class="mono-cell">Nanite::EmitDepthTargets</td><td class="num-cell excl-cell">~1.2%</td><td class="desc-cell">Depth 추출 패스 레코딩</td></tr>
    <tr><td class="mono-cell">Nanite::InitContext</td><td class="num-cell">~0.7%</td><td class="desc-cell">버퍼 할당·View 초기화</td></tr>
    <tr><td class="mono-cell">NaniteMaterialListApply</td><td class="num-cell">~0.6%</td><td class="desc-cell">씬 변경 시 머티리얼 목록 적용</td></tr>
  </tbody>
</table>
<p style="color:var(--text3);font-size:12px;margin-top:-16px;">* 기준: Nanite CPU 총비용 = 100. 표의 비중 값은 각 패스가 직접 차지하는 비용 비율이다.</p>

<div class="callout callout-warn">
  <div class="callout-title">⚡ CPU 병목 핵심</div>
  <p><code>Nanite::BasePass</code>는 <strong>Nanite CPU 총비용의 약 15%</strong>를 차지하는 최대 비용 패스다. 씬의 <strong>유니크 Nanite 머티리얼 수에 비례</strong>하므로, 머티리얼 인스턴스를 과도하게 세분화하면 GPU·CPU 모두 선형 증가한다. 씬 변경 시에는 <code>CacheNaniteMaterialBins</code>도 추가로 발생한다.</p>
  <p>CPU 쪽에서도 본질은 비슷하다. <strong>Shader Bin과 유니크 머티리얼 수가 늘수록</strong> <code>FNaniteMaterialPassCommand</code> 빌딩과 indirect draw 등록 수가 같이 늘고, 준비 단계의 <code>CacheNaniteMaterialBins</code>도 변경 시점마다 다시 계산해야 한다. 즉 BasePass 병목은 GPU만의 문제가 아니라 명령 준비 비용까지 함께 끌고 온다.</p>
</div>

<span class="section-eyebrow">패스별 상세</span>

</div>

<h1 id="패스별-상세">패스별 상세</h1>

<div class="re-post">

<div class="pipeline">

  <div class="pipe-item">
    <div class="pipe-num">①</div>
    <div class="pipe-body">
      <div class="step-badge badge-gpu">VisibilityBuffer 초기화 — InitRasterContext</div>
      <h3>래스터라이즈 시작 전 VisibilityBuffer를 생성하고 클리어한다</h3>
      <p>
        Nanite 래스터라이즈의 결과물인 VisibilityBuffer는 매 프레임 <code>InitRasterContext</code>에서 생성·초기화된다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>포맷 결정</h4>
        <p>플랫폼이 64bit uint UAV를 지원하면 <code>PF_R64_UINT</code>를 사용한다. 지원하지 않으면 <code>PF_R32G32_UINT</code>로 폴백한다. 픽셀 하나에 다음이 패킹된다.</p>
        <p style="margin-top:8px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg2);padding:10px 14px;border-radius:8px;color:var(--accent);">
          uint64 = [ Depth(32bit) | ClusterIndex+1(22bit) | TriangleIndex(7bit) | Reserved(3bit) ]
        </p>
        <p style="margin-top:8px;">Depth를 최상위 비트에 배치해, <code>InterlockedMax</code> 한 번으로 Depth Test와 ClusterIndex/TriangleIndex 기록을 동시에 처리한다. ClusterIndex는 "0 = 비어있음"을 구분하기 위해 +1 오프셋을 사용한다.</p>
      </div>

      <div class="sub-section">
        <h4>DepthBuffer 생성</h4>
        <p>VisibilityBuffer와 함께 동일 해상도의 DepthBuffer(<code>PF_DepthStencil</code>)를 생성한다. SW 래스터라이저는 VisibilityBuffer의 uint64에 Depth를 내장하고, HW 래스터라이저는 하드웨어 DepthBuffer를 함께 사용한다.</p>
      </div>

      <div class="sub-section">
        <h4>RasterClear — VisibilityBuffer 초기화</h4>
        <p><code>AddClearVisBufferPass</code>에서 Compute Shader(<code>FRasterClearCS</code>)를 실행해 VisibilityBuffer를 <code>0</code>으로 초기화한다. 0은 "아무 클러스터도 래스터라이즈되지 않음"을 의미한다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-gpu">PF_R64_UINT</span>
        <span class="pipe-tag tag-gpu">InterlockedMax</span>
        <span class="pipe-tag tag-gpu">RasterClear CS</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">②</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">Main Pass vs Post Pass — Two-Pass Occlusion Culling 개요</div>
      <h3>이전 프레임 HZB로 빠르게 컬링하고, 현재 프레임으로 누락을 보정한다</h3>
      <p>
        카메라가 움직이거나 오브젝트가 등장하면, 이전 프레임 기준으로 컬링했을 때 실제로는 보여야 하는 오브젝트가 Occluded로 처리될 수 있다. Nanite는 이를 두 번의 패스로 해결한다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>Main Pass</h4>
        <p><strong>이전 프레임 HZB</strong>를 기준으로 인스턴스·노드·클러스터를 컬링한다. 컬링에서 살아남은 클러스터를 SW/HW 래스터라이즈해 VisibilityBuffer를 채운다.</p>
        <p style="margin-top:8px;">컬링 중 Occluded로 판정된 인스턴스는 <code>OccludedInstances</code> 버퍼에 기록해 둔다.</p>
      </div>

      <div class="sub-section">
        <h4>HZB 빌드</h4>
        <p>MainPass 래스터라이즈 결과로 생성된 DepthBuffer를 사용해 <strong>현재 프레임 HZB</strong>를 빌드한다. 이 HZB는 PostPass 컬링에서만 사용된다.</p>
      </div>

      <div class="sub-section">
        <h4>Post Pass</h4>
        <p>MainPass에서 Occluded로 기록된 인스턴스들을 대상으로 <strong>현재 프레임 HZB</strong>로 다시 컬링 테스트한다. 현재 시점에서 실제로 보이는 인스턴스가 있다면 래스터라이즈해 VisibilityBuffer에 추가한다.</p>
        <p style="margin-top:8px;">PostPass에서는 MainPass 결과를 유지한 채 새 클러스터를 VisibilityBuffer에 <em>추가</em>한다. PostPass의 VisibleClustersSWHW는 MainPass 데이터 뒤에 이어 붙여진다.</p>
      </div>

      <div class="callout callout-teal" style="margin-top:14px;">
        <div class="callout-title">연속 프레임 가정</div>
        <p>이 방식은 "프레임 간 시점 변화가 크지 않다"는 가정에 기반한다. 빠른 카메라 이동이나 급격한 오브젝트 변화 시 PostPass에서도 누락이 발생할 수 있으며, 이는 다음 프레임에서 보정된다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-cull">이전 HZB → MainPass</span>
        <span class="pipe-tag tag-cull">현재 HZB 빌드</span>
        <span class="pipe-tag tag-cull">현재 HZB → PostPass</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">③</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">TwoPassOcclusionCulling 상세 — 컬링 내부 동작</div>
      <h3>InstanceCulling · Persistent Thread NodeClusterCull · CalculateSafeArgs</h3>

      <div class="sub-section" style="margin-top:14px;">
        <h4>주요 버퍼 구조</h4>
        <p>컬링 전 단계에서 여러 공유 버퍼가 준비된다.</p>
        <p style="margin-top:8px;font-size:13px;color:var(--text2);">
          <strong style="color:var(--accent);">QueueState</strong> — Node·Cluster 큐의 읽기/쓰기 Offset과 총 개수를 추적. Main/Post 패스 각각 인덱스 0, 1번 슬롯 사용.<br />
          <strong style="color:var(--accent);">MainAndPostNodesAndClusterBatches</strong> — Node 정보와 ClusterBatch 개수를 저장하는 공유 버퍼. Main/Post 패스가 동일 버퍼를 구역을 나눠 사용.<br />
          <strong style="color:var(--accent);">MainAndPostCandidateClusters</strong> — 살아남은 Cluster를 FVisibleCluster로 패킹해 저장. MainPass는 앞에서, PostPass는 뒤에서 역방향으로 기록.<br />
          <strong style="color:var(--accent);">VisibleClustersSWHW</strong> — 최종 가시 Cluster. SW는 앞부터, HW는 뒤부터 기록.
        </p>
      </div>

      <div class="sub-section">
        <h4>InstanceCulling CS — 인스턴스 단위 컬링</h4>
        <p>WorkGroup당 64개 thread, 각 thread가 인스턴스 1개를 담당한다.</p>
        <p style="margin-top:8px;">① <strong>PrimitiveID·InstanceData</strong> 로드 → ② Distance / GlobalClipPlane / Frustum / HZB 컬링 순서로 테스트 → ③ Visible이면 Root Node를 <code>QueueState</code>에 기록 → ④ Occluded면 <code>OccludedInstances</code>에 기록(PostPass 대기).</p>
        <p style="margin-top:8px;">Wave Intrinsic(<code>WaveInterlockedAddScalar_</code>)을 사용해 64개 thread가 <strong>Atomic 연산 1회</strong>로 각자의 NodeWriteOffset을 받는다. 이는 Nanite 셰이더 전체에서 반복되는 최적화 패턴이다.</p>
      </div>

      <div class="sub-section">
        <h4>NodeAndClusterCull — Persistent Thread 계층 순회</h4>
        <p>DX12 기준 1,440개 WorkGroup이 thread pool처럼 동작한다. 각 WorkGroup은 Node 큐와 Cluster 큐가 모두 빌 때까지 루프를 반복한다.</p>

        <p style="margin-top:10px;font-weight:600;color:var(--text);">ProcessNodeBatch</p>
        <p>WorkGroup(64 thread) 중 앞 16개가 <code>MainAndPostNodesAndClusterBatches</code>에서 Node 16개를 로드한다. 각 Node는 최대 4개의 Child를 가지므로, 16 × 4 = 64개 thread가 각자 Child 1개의 가시성을 판단한다.</p>
        <p style="margin-top:6px;">
          · Child가 <strong>Leaf가 아니고 Visible</strong>: Child Node를 QueueState에 등록 (다음 루프에서 처리).<br />
          · Child가 <strong>Leaf이고 Visible</strong>: Cluster를 <code>CandidateClusters</code>에 등록 + ClusterBatch에 64개 묶음 정보 기록.<br />
          · Child가 <strong>Occluded (MainPass)</strong>: 현재 Node를 PostPass 큐에 등록해 재검사 예약.
        </p>

        <p style="margin-top:10px;font-weight:600;color:var(--text);">ProcessClusterBatch</p>
        <p>WorkGroup(64 thread) 각각이 Cluster 1개를 담당. Distance / GlobalClipPlane / HZB 컬링 후, <code>SmallEnoughToDraw</code>로 SW/HW 래스터라이즈 방식을 결정. Visible이면 <code>VisibleClustersSWHW</code>에 기록. MainPass에서 Occluded된 클러스터는 PostPass CandidateClusters에 역방향으로 추가.</p>
      </div>

      <div class="sub-section">
        <h4>CalculateSafeRasterizerArgs</h4>
        <p>컬링 결과(SW/HW 클러스터 수)를 기반으로 래스터라이즈 Indirect DrawCall argument를 생성한다. 최대 허용 클러스터 수를 초과하지 않도록 Clamp 처리 후 <code>SafeRasterizeArgsSWHW</code>에 기록한다.</p>
        <p style="margin-top:6px;">
          SW: <code>{ NumClustersSW/64, 1, 1 }</code> — Compute Dispatch 인수.<br />
          HW: <code>{ 128×3, NumClustersHW, 0, 0 }</code> — DrawInstanced 인수 (인스턴스=Cluster, 버텍스=최대 삼각형×3).
        </p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-cull">Wave Intrinsic</span>
        <span class="pipe-tag tag-cull">Persistent Thread</span>
        <span class="pipe-tag tag-cull">Node BVH 순회</span>
        <span class="pipe-tag tag-cull">Indirect Args</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">④</div>
    <div class="pipe-body">
      <div class="step-badge badge-raster">RasterBinning — 래스터라이즈 전처리</div>
      <h3>가시 Cluster와 머티리얼을 연결해 SW/HW 래스터 Indirect Arg를 만든다</h3>
      <p>
        앞 섹션에서 설명한 것처럼 Raster Bin은 "같은 래스터 경로를 공유하는 작업 묶음"이다. <code>AddPass_Binning</code>은 컬링 결과인 <code>VisibleClustersSWHW</code>를 바로 그리지 않고, 먼저 이를 <strong>RasterBin별 실행 리스트</strong>로 재정렬한다. 이렇게 해야 이후 SW/HW 래스터 패스가 bin 단위로 Indirect Dispatch/Draw를 발행할 수 있다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>RasterBinCount</h4>
        <p>각 가시 클러스터에서 사용하는 래스터 머티리얼 정보를 읽어 <code>RasterBinMeta[RasterBin].BinSWCount</code>, <code>BinHWCount</code>를 증가시킨다. 클러스터 내 삼각형이 최대 3개 머티리얼을 사용하면 FastPath, 그 이상이면 Page에서 직접 머티리얼 테이블을 로드한다.</p>
        <p style="margin-top:6px;">Indirect Dispatch 인수는 클러스터 수 / 64 (WorkGroup당 64 thread, thread당 Cluster 1개).</p>
      </div>

      <div class="sub-section">
        <h4>RasterBinReserve</h4>
        <p>각 RasterBin이 <code>RasterBinData</code> 버퍼에서 차지할 오프셋을 계산한다. <code>RangeAllocator</code>(uint32 atomic counter)에서 SW+HW 클러스터 총합만큼 영역을 할당받아 <code>RasterBinMeta[RasterBin].ClusterOffset</code>에 저장한다. 또한 각 RasterBin의 Indirect Draw/Dispatch 인자 초기값을 <code>RasterBinArgsSWHW</code>에 기록한다.</p>
      </div>

      <div class="sub-section">
        <h4>RasterBinScatter</h4>
        <p>RasterBinCount와 동일 로직으로 클러스터를 다시 순회하되, 이번에는 <code>RasterBinData[ClusterOffset]</code>에 <strong>ClusterIndex + 클러스터 내 삼각형 범위(StartTri, NumTri)</strong>를 기록한다. 동시에 <code>RasterBinArgsSWHW</code>의 해당 머티리얼 슬롯 클러스터 카운트를 1 증가시킨다.</p>
        <p style="margin-top:6px;">완료 후 각 RasterBin은 "어떤 클러스터를 어떤 삼각형 범위로 그릴지"를 정확히 알게 된다. 이후 SW/HW 래스터라이즈는 이 리스트를 그대로 소비한다. 즉 RasterBinning은 개념적으로는 분류 단계이고, 구현상으로는 <strong>bin별 실행 커맨드 버퍼를 만드는 단계</strong>다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-raster">RasterBinMeta</span>
        <span class="pipe-tag tag-raster">RasterBinData</span>
        <span class="pipe-tag tag-raster">Indirect Arg 생성</span>
        <span class="pipe-tag tag-raster">Wave Intrinsic</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">⑤</div>
    <div class="pipe-body">
      <div class="step-badge badge-raster">SW Rasterize — MicropolyRasterize</div>
      <h3>작은 삼각형을 Compute Shader로 직접 래스터라이즈한다</h3>
      <p>
        픽셀보다 작은 삼각형을 하드웨어 래스터라이저에 넘기면, PixelShader는 실제 Active lane 1개에 Helper lane 3개가 붙어 <strong>최대 4배 연산 낭비</strong>가 발생한다. SW 래스터라이저는 이를 Compute Shader로 직접 처리해 낭비를 제거한다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>MicropolyRasterize CS</h4>
        <p>RasterBin별로 Indirect Dispatch가 발행된다. WorkGroup당 32개 thread(NANITE_VERT_REUSE_BATCH 활성 시), 각 WorkGroup이 Cluster 1개를 담당한다.</p>
        <p style="margin-top:8px;"><strong>TSlidingWindowVertexCache</strong>: 32개 thread가 각자 삼각형 1개를 담당하면서, 자신의 삼각형에 필요한 버텍스가 이웃 thread(WindowSize=32 이내)에서 이미 변환되었다면 재사용한다. 버텍스 변환 중복 계산을 줄이기 위한 Compute Shader 고유 최적화다.</p>
      </div>

      <div class="sub-section">
        <h4>RasterizeTri_Adaptive</h4>
        <p>삼각형 크기에 따라 두 방식 중 하나를 선택한다.</p>
        <p style="margin-top:6px;">
          · <strong>Rect 방식</strong> (작은 삼각형): MinPixel~MaxPixel을 단순 반복하며 각 픽셀에 WritePixel 호출.<br />
          · <strong>Scanline 방식</strong> (큰 삼각형): 스캔라인 단위로 범위를 계산해 래스터라이즈.
        </p>
      </div>

      <div class="sub-section">
        <h4>WritePixel — VisibilityBuffer 기록</h4>
        <p>픽셀 좌표·PixelValue·Depth를 조합해 <code>InterlockedMax(VisibilityBuffer, Depth | ClusterIdx+1 | TriIdx)</code>를 호출한다. Depth가 최상위 비트이므로 InterlockedMax 한 번으로 Depth Test와 값 기록이 동시에 이루어진다. Masked 머티리얼의 경우 Clip 연산 후 조건부 기록한다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-raster">Compute Shader</span>
        <span class="pipe-tag tag-raster">TSlidingWindowVertexCache</span>
        <span class="pipe-tag tag-raster">InterlockedMax</span>
        <span class="pipe-tag tag-raster">Quad Overdraw 제거</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">⑥</div>
    <div class="pipe-body">
      <div class="step-badge badge-raster">HW Rasterize — FHWRasterizeVS · PS</div>
      <h3>충분히 큰 삼각형은 기존 VS·PS 파이프라인으로 래스터라이즈한다</h3>
      <p>
        <code>SmallEnoughToDraw</code> 판정에서 SW보다 HW가 효율적이라고 판단된 클러스터는 하드웨어 래스터라이저로 처리한다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>FHWRasterizeVS — 버텍스 셰이더</h4>
        <p>Indirect DrawInstanced 방식. 인스턴스 = Cluster, 버텍스 개수 = 128×3 = 384 (최대 삼각형 수 × 3).</p>
        <p style="margin-top:8px;">
          · <code>SV_InstanceID</code>로 ClusterIndex를 결정.<br />
          · <code>SV_VertexID</code>로 현재 삼각형과 버텍스 번호를 결정 (VertexID / 3 = LocalTriIndex, VertexID % 3 = VertexPos).<br />
          · 클러스터가 소유한 삼각형 범위를 초과하면 <code>Position = float4(0,0,0,1)</code>로 퇴화(Degenerate)시켜 래스터라이즈를 스킵한다.<br />
          · 유효 삼각형은 클러스터 데이터에서 버텍스를 읽어 World → Clip Space로 변환. <code>PixelValue = ClusterIndex+1 | TriangleIndex</code>를 PS로 넘긴다.
        </p>
      </div>

      <div class="sub-section">
        <h4>FHWRasterizePS — 픽셀 셰이더</h4>
        <p>VS에서 받은 <code>PixelValue</code>와 <code>SV_Position.z</code>(DeviceZ)를 조합해 SW 래스터와 동일하게 <code>InterlockedMax(VisibilityBuffer, Depth | PixelValue)</code>를 호출한다.</p>
        <p style="margin-top:6px;">Masked 머티리얼 또는 PDO(PixelDepthOffset)가 있는 경우 Barycentric을 보간해 머티리얼 셰이더를 실행하고 결과를 VisibilityBuffer에 반영한다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-raster">DrawInstanced Indirect</span>
        <span class="pipe-tag tag-raster">Degenerate Triangle</span>
        <span class="pipe-tag tag-raster">InterlockedMax</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">⑦</div>
    <div class="pipe-body">
      <div class="step-badge badge-cull">HZB 생성 — VisibilityBuffer 기반 Hierarchical Z-Buffer</div>
      <h3>MainPass 결과로 현재 프레임 HZB를 빌드해 PostPass 컬링에 사용한다</h3>
      <p>
        MainPass 래스터라이즈가 완료되면, 생성된 DepthBuffer를 기반으로 <code>BuildPreviousOccluderHZB</code>를 통해 현재 프레임 HZB를 생성한다. HZB는 원본 Depth의 Mip Chain으로 구성된다. Mip 레벨이 높을수록 더 큰 영역의 최소 Depth를 저장해 큰 오브젝트 컬링에 효율적이다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>PostPass 컬링 사용</h4>
        <p>생성된 현재 프레임 HZB는 <code>CullingParameters</code>에 바인딩되어 PostPass InstanceCulling과 NodeClusterCull에서 사용된다. MainPass에서 Occluded로 기록된 인스턴스를 이 HZB로 다시 검사해 "현재 시점에서도 Occluded인지" 확인한다.</p>
      </div>

      <div class="sub-section">
        <h4>최종 HZB</h4>
        <p>PostPass까지 완료된 후 <code>BeginOcclusionTests</code>에서 최종 HZB를 빌드한다. 이 HZB가 <strong>다음 프레임 MainPass</strong>에서 이전 프레임 HZB로 사용된다. 이렇게 프레임마다 HZB가 갱신되며 컬링 정확도가 유지된다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-cull">Depth Mip Chain</span>
        <span class="pipe-tag tag-cull">PostPass 컬링 입력</span>
        <span class="pipe-tag tag-cull">다음 프레임 MainPass 입력</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">⑧</div>
    <div class="pipe-body">
      <div class="step-badge badge-gpu">EmitDepthTargets — VisibilityBuffer에서 Depth 추출</div>
      <h3>Nanite GPU 총비용의 약 5% · FullScreen Quad 3회</h3>
      <p>
        VisibilityBuffer만으로는 기존 렌더링 파이프라인과 연동이 불가능하다. 세 개의 FullScreen Quad 패스로 필요한 버퍼를 추출한다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>EmitSceneDepthPS</h4>
        <p>VisibilityBuffer에서 Depth·ClusterIndex·TriangleIndex를 복원한다. ClusterIndex·TriangleIndex로 FCluster 정보를 읽어 Velocity와 <strong>ShadingMask</strong>를 출력한다.</p>
        <p style="margin-top:6px;"><code>ShadingMask = { NanitePixel 여부, ShadingBin(=LegacyShadingId), 라이팅 채널, DecalReceiver }</code>. ShadingBin은 이후 EmitMaterialDepthPS와 ShadeBinning·BasePass에서 반복 사용된다.</p>
      </div>

      <div class="sub-section">
        <h4>EmitSceneStencilPS</h4>
        <p>ShadingMask에서 <code>bIsDecalReceiver</code>가 설정된 픽셀을 찾아 Stencil에 132(0x84)를 기록한다. Decal 렌더링에서 해당 픽셀만 처리할 수 있도록 마스킹하는 용도다.</p>
      </div>

      <div class="sub-section">
        <h4>EmitMaterialDepthPS</h4>
        <p>ShadingMask에서 ShadingBin을 읽어 <code>MaterialDepthTable</code>에서 <code>MaterialDepthId</code>를 조회한다. 이 값을 Depth Buffer에 float으로 출력해 <strong>MaterialDepth 버퍼</strong>를 생성한다.</p>
        <p style="margin-top:6px;"><code>MaterialDepthId = float(StateBucketId + 1) / NANITE_MAX_STATE_BUCKET_ID</code>. 머티리얼마다 고유한 Depth 값이 부여된다. BasePass에서 DepthEqual 판별의 기준이 된다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-gpu">SceneDepth</span>
        <span class="pipe-tag tag-gpu">ShadingMask</span>
        <span class="pipe-tag tag-gpu">MaterialDepth</span>
        <span class="pipe-tag tag-gpu">Stencil</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">⑨</div>
    <div class="pipe-body">
      <div class="step-badge badge-shade">ShadeBinning — 머티리얼 타일 분류</div>
      <h3>Nanite GPU 총비용의 약 4% · 3 Compute 패스로 머티리얼별 Indirect DrawArg를 준비한다</h3>
      <p>
        앞에서 본 Shader Bin은 "같은 픽셀 셰이더/머티리얼 셋업을 공유하는 타일 묶음"이다. BasePass는 머티리얼별로 Indirect DrawCall을 발행하므로, 먼저 <strong>어떤 Shader Bin이 어느 64×64 타일에 존재하는지</strong>를 알아야 한다. 이 역할이 ShadeBinning이다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>InitializeMaterials</h4>
        <p>MaterialSlot 개수/64개 WorkGroup을 Dispatch. 각 MaterialSlot의 <code>MaterialIndirectArgs</code>(DrawCall argument)를 초기화하고 <code>MaterialTileRemap</code> 버퍼를 0으로 클리어한다. 이후 단계가 "머티리얼별로 몇 개 타일을 그려야 하는가"를 채워 넣을 빈 그릇을 준비하는 셈이다.</p>
      </div>

      <div class="sub-section">
        <h4>ClassifyMaterials</h4>
        <p>화면을 64×64 타일로 분할. WorkGroup 1개가 타일 1개를 담당. 16×16 thread가 타일의 64×64 픽셀을 4×4번 반복해 모두 처리한다.</p>
        <p style="margin-top:6px;">각 픽셀에서 ShadingMask를 읽어 <code>ShadingBin</code>을 추출. groupshared <code>TileMaterialBins</code>(비트마스크)에 해당 ShadingBin 위치에 bit를 set한다. Wave Intrinsic으로 같은 ShadingBin을 사용하는 thread들을 묶어 InterlockedOr 횟수를 최소화한다.</p>
        <p style="margin-top:6px;">루프 완료 후 <code>MaterialTileRemap[MaterialSlot][TileLinear]</code>를 비트마스크로 기록한다. 즉 "이 Shader Bin은 이 타일을 그려야 한다"는 맵이 만들어진다. 동시에 해당 MaterialSlot의 <code>MaterialIndirectArgs.InstanceCount</code>를 타일 수만큼 증가시킨다.</p>
      </div>

      <div class="sub-section">
        <h4>FinalizeMaterials</h4>
        <p>Compute Shader 방식(기본 설정에서는 미사용)을 위해 8×8 Micro Tile 기준 DispatchGroup을 계산해 <code>MaterialIndirectArgs</code>에 기록한다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-shade">64×64 Tile</span>
        <span class="pipe-tag tag-shade">MaterialTileRemap 비트마스크</span>
        <span class="pipe-tag tag-shade">Indirect Args 생성</span>
        <span class="pipe-tag tag-shade">Wave Intrinsic</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">⑩</div>
    <div class="pipe-body">
      <div class="step-badge badge-shade">BasePass — Material Pass와 GBuffer 생성</div>
      <h3>Nanite GPU 총비용의 약 19% (최대) · Nanite CPU 총비용의 약 15% (최대)</h3>
      <p>
        GPU·CPU 양쪽에서 모두 가장 비싼 패스다. Raster 단계가 "보이는 픽셀의 주소"를 만들었다면, BasePass는 이제 Shader Bin 단위로 실제 머티리얼 셰이더를 실행해 GBuffer를 채운다. 머티리얼 수와 타일 분화가 많을수록 Indirect DrawCall도 늘어난다.
      </p>

      <div class="sub-section" style="margin-top:14px;">
        <h4>BuildNaniteMaterialPassCommands — CPU 드로콜 빌딩</h4>
        <p>FScene에 등록된 모든 Nanite 머티리얼(FNaniteMaterialEntry)을 순회해 <code>FNaniteMaterialPassCommand</code>를 생성한다. 즉 CPU가 Shader Bin별 BasePass 실행 명령을 빌드하는 단계다. WPO 사용 여부로 두 개의 패스로 분리된다.</p>
        <p style="margin-top:6px;">
          · <strong>패스 1 (EmitGBuffer)</strong>: WPO 없는 머티리얼. GBuffer에 BaseColor·Normal·Roughness·Metallic 출력.<br />
          · <strong>패스 2 (EmitGBufferWithVelocity)</strong>: WPO 있는 머티리얼. GBuffer 출력 + Velocity Buffer도 함께 출력.
        </p>
        <p style="margin-top:6px;">각 Command에는 <code>MaterialDepth</code> 값이 기록된다. 이 값이 VS에서 설정하는 Depth이며 DepthEqual 판별 기준이 된다.</p>
      </div>

      <div class="sub-section">
        <h4>FNaniteIndirectMaterialVS — 타일 Quad 생성</h4>
        <p><code>SV_InstanceID</code>는 현재 머티리얼이 그릴 타일 순번(0, 1, 2...)이다. 선형 순번을 실제 타일 인덱스로 변환하기 위해 <code>MaterialTileRemap</code>에서 비트마스크를 읽어 TargetTileCount번째 bit가 1인 위치를 탐색한다. 다시 말해 ShadeBinning이 만든 "이 머티리얼이 처리해야 할 타일 목록"을 실제 드로우로 펼치는 단계다.</p>
        <p style="margin-top:6px;">타일 인덱스를 UV로 변환하고, UV를 NDC 좌표로 변환해 64×64 Quad를 생성한다. <strong>Z 값 = MaterialDepth</strong>로 설정해 DepthEqual이 올바르게 동작하도록 한다.</p>
      </div>

      <div class="sub-section">
        <h4>DepthEqual 판별</h4>
        <p>EmitMaterialDepthPS에서 생성한 MaterialDepth Buffer를 DepthStencil Target으로 바인딩. DepthTest를 <code>Equal</code>로 설정. VS에서 출력한 Z(= MaterialDepth)와 MaterialDepth Buffer의 값이 일치하는 픽셀만 Pixel Shader가 실행된다. 다른 머티리얼 픽셀은 자동으로 기각된다.</p>
      </div>

      <div class="sub-section">
        <h4>BasePassPixelShader — GBuffer 출력</h4>
        <p>VisibilityBuffer에서 ClusterIndex·TriangleIndex를 읽어 Barycentric 좌표를 보간하고, 머티리얼 셰이더를 실행해 GBuffer에 출력한다. 이 부분은 일반 비-Nanite BasePass 픽셀 셰이더와 동일한 코드를 사용한다.</p>
      </div>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-shade">Indirect DrawCall × 머티리얼 수</span>
        <span class="pipe-tag tag-shade">DepthEqual 판별</span>
        <span class="pipe-tag tag-shade">GBuffer</span>
        <span class="pipe-tag tag-shade">WPO Velocity</span>
      </div>
    </div>
  </div>

  <div class="pipe-item">
    <div class="pipe-num">⑪</div>
    <div class="pipe-body">
      <div class="step-badge badge-stream">Readback — 스트리밍 피드백</div>
      <h3>Nanite GPU 총비용의 약 8% · GPU→CPU 동기 읽기</h3>
      <p>
        Nanite는 Virtual Texture와 동일한 방식으로 필요한 클러스터 페이지만 런타임에 스트리밍 로드한다. 래스터라이즈 중 GPU의 <code>RequestPageRange</code>가 현재 화면에 필요하지만 아직 로드되지 않은 클러스터 페이지 요청을 누적한다.
      </p>
      <p style="margin-top:8px;">Readback 패스에서 이 요청 버퍼를 CPU로 읽어와 다음 프레임의 클러스터 스트리밍을 스케줄링한다. GPU→CPU 동기 읽기이므로 <strong>파이프라인 버블</strong>을 유발할 수 있다. 여기의 약 8%는 이 패스가 Nanite GPU 총비용 중 직접 차지하는 비율이며, 그 안에 대기 시간도 포함된다.</p>

      <div class="pipe-tag-row">
        <span class="pipe-tag tag-stream">Virtual-texture-style Streaming</span>
        <span class="pipe-tag tag-stream">GPU→CPU Readback</span>
        <span class="pipe-tag tag-stream">비동기 Async Load</span>
      </div>
    </div>
  </div>

</div>

<div class="summary-box">
  <h3>전체 흐름 요약</h3>
  <p>
    에셋 빌드 시 메시 → <strong>Cluster 계층 (최대 128 삼각형)</strong>. 씬 등록 시 <strong>사전 준비 단계</strong>에서 RasterBin/ShadingBin과 머티리얼 슬롯을 만든다. 런타임에는 <strong>Two-Pass Occlusion Culling</strong>으로 가시 Cluster 선별 → <strong>RasterBinning</strong>으로 Raster Bin별 실행 범위 구성 → <strong>SW/HW Rasterize</strong>로 VisibilityBuffer 작성 → <strong>EmitDepthTargets</strong>로 Depth·MaterialDepth 추출 → <strong>ShadeBinning</strong>으로 Shader Bin별 타일 분류 → <strong>BasePass</strong>에서 머티리얼별 Indirect DrawCall로 GBuffer 생성. GPU·CPU 최대 병목은 모두 <strong>유니크 머티리얼 수에 비례하는 BasePass</strong>다.
  </p>
</div>

</div>]]></content><author><name></name></author><category term="Rendering" /><category term="Rendering" /><category term="UnrealEngine" /><category term="Nanite" /><summary type="html"><![CDATA[Nanite]]></summary></entry><entry><title type="html">언리얼 엔진에서 렌더링 방정식 이해하기: 현실감을 더하는 기술적 접근</title><link href="https://renderer86.github.io/d8c73243c492ed7b5f44b70936cfe4521669ad34" rel="alternate" type="text/html" title="언리얼 엔진에서 렌더링 방정식 이해하기: 현실감을 더하는 기술적 접근" /><published>2024-11-04T00:00:00+00:00</published><updated>2024-11-04T00:00:00+00:00</updated><id>https://renderer86.github.io/Rendering_Equation</id><content type="html" xml:base="https://renderer86.github.io/d8c73243c492ed7b5f44b70936cfe4521669ad34"><![CDATA[<p><strong>이런 분이 읽으면 좋습니다!</strong></p>

<ul>
  <li>언리얼 엔진5를 사용하면서 빛이 어떻게 계산되는지 궁금했던 분</li>
  <li>PBR, Lumen, Nanite 같은 용어가 수학적으로 어떤 의미인지 알고 싶은 분</li>
</ul>

<p><strong>이 글로 알 수 있는 내용</strong></p>

<ul>
  <li>카지야 렌더링 방정식의 각 항이 무엇을 의미하는지</li>
  <li>UE5의 Nanite, Lumen, VSM이 방정식의 어느 부분을 담당하는지</li>
  <li>UE5 PBR 재질(GGX BRDF)이 수학적으로 어떻게 작동하는지</li>
</ul>

<p><br /></p>

<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet" />

<style> .re-post { --bg2: #f4f6fb; --bg3: #eef0f7; --surface: #f9fafd; --surface2: #eceef7; --border: rgba(60,80,180,0.10); --border2: rgba(60,80,180,0.22); --text: #1a1d2e; --text2: #464c6a; --text3: #8890aa; --accent: #3d63e0; --accent2: #7248d4; --gold: #b07d00; --teal: #0a8f62; --coral: #d63031; --orange: #c85a00; } .re-post .eq-block { position: relative; background: var(--bg2); border: 1px solid var(--border2); border-radius: 16px; padding: 32px 40px; margin: 24px 0 40px; overflow-x: auto; overflow-y: hidden; font-family: 'JetBrains Mono', monospace; } .re-post .eq-block::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, transparent, var(--accent), transparent); } .re-post .eq-label { font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--text3); margin-bottom: 16px; } .re-post .eq-main { font-size: clamp(13px, 2.5vw, 16px); color: var(--text); line-height: 2.2; white-space: nowrap; word-break: normal; min-width: max-content; } .re-post .eq-term { color: var(--accent); font-weight: 600; } .re-post .eq-op { color: var(--text3); } .re-post .eq-fn { color: var(--teal); font-weight: 600; } .re-post .eq-int { color: var(--gold); font-size: 1.3em; } .re-post .section-eyebrow { display: block; font-size: 12px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; color: var(--accent); margin-bottom: 4px; margin-top: 56px; } .re-post .term-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin: 28px 0; } .re-post .term-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px; position: relative; overflow: hidden; transition: border-color 0.2s, box-shadow 0.2s; } .re-post .term-card:hover { border-color: var(--border2); box-shadow: 0 2px 12px rgba(60,80,180,0.07); } .re-post .term-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; } .re-post .term-card.blue::before { background: var(--accent); } .re-post .term-card.gold::before { background: var(--gold); } .re-post .term-card.teal::before { background: var(--teal); } .re-post .term-card.coral::before { background: var(--coral); } .re-post .term-card.purple::before { background: var(--accent2); } .re-post .term-card.orange::before { background: var(--orange); } .re-post .term-symbol { font-family: 'JetBrains Mono', monospace; font-size: 17px; font-weight: 600; margin-bottom: 6px; } .re-post .term-card.blue .term-symbol { color: var(--accent); } .re-post .term-card.gold .term-symbol { color: var(--gold); } .re-post .term-card.teal .term-symbol { color: var(--teal); } .re-post .term-card.coral .term-symbol { color: var(--coral); } .re-post .term-card.purple .term-symbol { color: var(--accent2); } .re-post .term-card.orange .term-symbol { color: var(--orange); } .re-post .term-name { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; } .re-post .term-desc { font-size: 13px; color: var(--text2); line-height: 1.65; margin: 0; } .re-post .pipeline { display: flex; flex-direction: column; margin: 28px 0; position: relative; } .re-post .pipeline::before { content: ''; position: absolute; left: 27px; top: 54px; bottom: 54px; width: 1px; background: linear-gradient(to bottom, var(--accent), var(--accent2)); opacity: 0.25; } .re-post .pipe-item { display: grid; grid-template-columns: 54px 1fr; gap: 18px; padding: 20px 0; position: relative; } .re-post .pipe-num { width: 54px; height: 54px; border-radius: 50%; border: 1px solid var(--border2); background: var(--surface); display: flex; align-items: center; justify-content: center; font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 600; color: var(--accent); flex-shrink: 0; position: relative; z-index: 1; } .re-post .pipe-body h3 { font-size: 1rem; font-weight: 700; color: var(--text); margin-bottom: 6px; } .re-post .pipe-body p { font-size: 14px; color: var(--text2); line-height: 1.75; margin: 0; } .re-post .pipe-tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } .re-post .pipe-tag { font-size: 11px; padding: 3px 10px; border-radius: 100px; font-weight: 600; letter-spacing: 0.04em; } .re-post .tag-geo { background: rgba(61,99,224,0.10); color: var(--accent); } .re-post .tag-light { background: rgba(176,125,0,0.10); color: var(--gold); } .re-post .tag-gi { background: rgba(10,143,98,0.10); color: var(--teal); } .re-post .tag-shadow { background: rgba(114,72,212,0.10); color: var(--accent2); } .re-post .tag-post { background: rgba(200,90,0,0.10); color: var(--orange); } .re-post .step-badge { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; padding: 2px 8px; border-radius: 4px; margin-bottom: 4px; } .re-post .badge-approx { background: rgba(200,90,0,0.12); color: var(--orange); } .re-post .badge-exact { background: rgba(10,143,98,0.12); color: var(--teal); } .re-post .badge-hybrid { background: rgba(61,99,224,0.12); color: var(--accent); } .re-post .mapping-table { width: 100%; border-collapse: collapse; margin: 28px 0; font-size: 14px; } .re-post .mapping-table th { background: var(--surface2); padding: 10px 14px; text-align: left; font-weight: 700; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text3); border: 1px solid var(--border); } .re-post .mapping-table td { padding: 12px 14px; border: 1px solid var(--border); vertical-align: top; line-height: 1.6; } .re-post .mapping-table tr { background: #ffffff; } .re-post .mapping-table tr:nth-child(odd) { background: var(--surface); } .re-post .mapping-table tr:hover { background: var(--surface2); } .re-post .math-cell { font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--accent); font-weight: 600; } .re-post .ue5-cell { color: var(--teal); font-weight: 600; } .re-post .desc-cell { color: var(--text2); } .re-post .callout { border-radius: 12px; padding: 18px 22px; margin: 24px 0; border: 1px solid; position: relative; overflow: hidden; } .re-post .callout::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; } .re-post .callout-info { background: rgba(61,99,224,0.05); border-color: rgba(61,99,224,0.18); } .re-post .callout-info::before { background: var(--accent); } .re-post .callout-warn { background: rgba(176,125,0,0.05); border-color: rgba(176,125,0,0.20); } .re-post .callout-warn::before { background: var(--gold); } .re-post .callout-title { font-size: 12px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 6px; } .re-post .callout-info .callout-title { color: var(--accent); } .re-post .callout-warn .callout-title { color: var(--gold); } .re-post .callout p { margin: 0; font-size: 14px; color: var(--text2); line-height: 1.75; } .re-post .code-block { background: #1e2230; border: 1px solid rgba(120,140,200,0.15); border-radius: 12px; padding: 22px; font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.8; overflow-x: auto; margin: 20px 0; position: relative; white-space: pre; color: #c8d0ea; } .re-post .code-block .kw { color: #a78bfa; } .re-post .code-block .fn { color: #34d399; } .re-post .code-block .cm { color: #525a78; font-style: italic; } .re-post .code-block .num { color: #fb923c; } .re-post .code-lang { position: absolute; top: 10px; right: 14px; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: #525a78; } .re-post .brdf-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 24px 0; } @media (max-width: 640px) { .re-post .brdf-grid { grid-template-columns: 1fr; } .re-post .term-grid { grid-template-columns: 1fr; } } .re-post .brdf-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; text-align: center; } .re-post .brdf-card .icon { font-size: 26px; margin-bottom: 8px; display: block; } .re-post .brdf-card h4 { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 4px; } .re-post .brdf-card p { font-size: 12px; color: var(--text2); margin: 0; line-height: 1.55; } .re-post .summary-box { background: linear-gradient(135deg, rgba(61,99,224,0.06) 0%, rgba(114,72,212,0.06) 100%); border: 1px solid rgba(61,99,224,0.18); border-radius: 16px; padding: 36px; margin: 32px 0; text-align: center; } .re-post .summary-box h3 { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; color: var(--text); } .re-post .summary-box p { width: 100%; max-width: none; margin: 0; font-size: 15px; line-height: 1.85; color: var(--text2); text-align: left; } </style>

<div class="re-post">
<div class="eq-block">
<div class="eq-label">Kajiya's Rendering Equation (1986)</div>
<div class="eq-main"><span class="eq-term">L<sub>o</sub>(x, ω<sub>o</sub>)</span> <span class="eq-op">=</span> <span class="eq-term">L<sub>e</sub>(x, ω<sub>o</sub>)</span> <span class="eq-op">+</span> <span class="eq-int">∫</span><sub>Ω</sub> <span class="eq-fn">f<sub>r</sub></span><span class="eq-op">(x, ω<sub>i</sub>, ω<sub>o</sub>)</span> · <span class="eq-term">L<sub>i</sub>(x, ω<sub>i</sub>)</span> · <span class="eq-fn">cos θ<sub>i</sub></span> <span class="eq-op">dω<sub>i</sub></span></div>
</div>
<p style="color:var(--text2);line-height:1.85;margin-bottom:20px;"> 1986년 James Kajiya가 발표한 렌더링 방정식은 빛의 전파를 수학적으로 정의한다. 이 방정식이 실시간 엔진에서 어떻게 근사되고 구현되는지 살펴본다. </p>
<span class="section-eyebrow">00 — 그래픽스 파이프라인</span>
</div>

<h1 id="그래픽스-파이프라인-개요">그래픽스 파이프라인 개요</h1>

<div class="re-post">
카지야 방정식을 실제로 계산하기 전에, UE5가 매 프레임 거치는 그래픽스 파이프라인 전체 흐름을 먼저 보자. 방정식의 각 항은 파이프라인의 여러 단계에 걸쳐 대체로 대응하며, 최종 픽셀 값은 각 패스의 결과를 합성해 얻어진다.
<div style="overflow-x:auto;margin:28px 0;">
<div style="display:flex;flex-direction:column;gap:0;min-width:560px;">
<div style="display:grid;grid-template-columns:180px 1fr;gap:0;border:1px solid rgba(60,80,180,0.12);border-radius:12px 12px 0 0;overflow:hidden;">
<div style="background:rgba(61,99,224,0.06);border-right:1px solid rgba(60,80,180,0.12);padding:20px 18px;">
<div style="font-size:10px;letter-spacing:0.15em;text-transform:uppercase;color:var(--accent);margin-bottom:6px;">CPU Stage</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:600;color:var(--text);margin-bottom:4px;">InitViews</div>
<div style="font-size:12px;color:var(--text3);">Culling · 분류 · DrawCmd</div>
</div>
<div style="padding:20px 22px;display:flex;flex-direction:column;gap:6px;">
<div style="font-size:13px;font-weight:700;color:var(--text);">x 후보 결정 — 계산할 픽셀을 솎아낸다</div>
<div style="font-size:13px;color:var(--text2);">Frustum/Occlusion/Distance Culling으로 화면 밖 오브젝트 제거. View Relevance로 패스 분류. MeshPassProcessor로 셰이더(f<sub>r</sub>) 선택.</div>
<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(61,99,224,0.10);color:var(--accent);font-weight:600;">방정식 전처리</span>
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(136,144,170,0.12);color:var(--text3);font-weight:600;">L_o 계산 불필요한 x 제거</span>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:center;height:28px;border-left:1px solid rgba(60,80,180,0.12);border-right:1px solid rgba(60,80,180,0.12);">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 2v10M4 8l4 4 4-4" fill="none" stroke="#8890aa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
</div>
<div style="display:grid;grid-template-columns:180px 1fr;gap:0;border:1px solid rgba(60,80,180,0.12);border-top:none;overflow:hidden;">
<div style="background:rgba(114,72,212,0.06);border-right:1px solid rgba(60,80,180,0.12);padding:20px 18px;">
<div style="font-size:10px;letter-spacing:0.15em;text-transform:uppercase;color:var(--accent2);margin-bottom:6px;">GPU Stage 1</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:600;color:var(--text);margin-bottom:4px;">Rasterization</div>
<div style="font-size:12px;color:var(--text3);">삼각형 → 픽셀 변환</div>
</div>
<div style="padding:20px 22px;display:flex;flex-direction:column;gap:6px;">
<div style="font-size:13px;font-weight:700;color:var(--text);">x 확정 — 픽셀마다 월드 좌표·법선·UV 결정</div>
<div style="font-size:13px;color:var(--text2);">버텍스 셰이더 후 삼각형을 픽셀로 분할. 픽셀마다 보간된 월드 포지션(x), 노멀, 텍스처 좌표가 결정된다. Nanite는 이 단계를 Cluster-level 소프트웨어 래스터로 대체.</div>
<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(114,72,212,0.10);color:var(--accent2);font-weight:600;">x 확정</span>
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(136,144,170,0.12);color:var(--text3);font-weight:600;">방정식 입력값 생성</span>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:center;height:28px;border-left:1px solid rgba(60,80,180,0.12);border-right:1px solid rgba(60,80,180,0.12);">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 2v10M4 8l4 4 4-4" fill="none" stroke="#8890aa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
</div>
<div style="display:grid;grid-template-columns:180px 1fr;gap:0;border:1px solid rgba(60,80,180,0.12);border-top:none;overflow:hidden;position:relative;">
<div style="position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#0a8f62,transparent);"></div>
<div style="background:rgba(10,143,98,0.06);border-right:1px solid rgba(60,80,180,0.12);padding:20px 18px;">
<div style="font-size:10px;letter-spacing:0.15em;text-transform:uppercase;color:var(--teal);margin-bottom:6px;">GPU Stage 2 ★</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:600;color:var(--text);margin-bottom:4px;">Shading</div>
<div style="font-size:12px;color:var(--text3);">Base Pass · Lumen</div>
</div>
<div style="padding:20px 22px;display:flex;flex-direction:column;gap:6px;">
<div style="font-size:13px;font-weight:700;color:var(--text);">방정식 계산 — f<sub>r</sub> · L<sub>i</sub> · cosθ 적분</div>
<div style="font-size:13px;color:var(--text2);">픽셀 셰이더에서 PBR BRDF(GGX) 평가. 직접광은 해석적 계산, 간접광은 Lumen이 surface cache와 tracing으로 근사. 방정식의 핵심 항들(f<sub>r</sub>, L<sub>i</sub>, cosθ)이 주로 이 단계에서 평가되지만, 최종 결과는 Shadow, GI, Reflection 등 여러 패스의 기여가 합산된다.</div>
<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(10,143,98,0.12);color:var(--teal);font-weight:600;">방정식 실제 계산</span>
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(10,143,98,0.08);color:var(--teal);font-weight:600;">f_r · L_i · cosθ dω</span>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:center;height:28px;border-left:1px solid rgba(60,80,180,0.12);border-right:1px solid rgba(60,80,180,0.12);">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M8 2v10M4 8l4 4 4-4" fill="none" stroke="#8890aa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
</div>
<div style="display:grid;grid-template-columns:180px 1fr;gap:0;border:1px solid rgba(60,80,180,0.12);border-top:none;border-radius:0 0 12px 12px;overflow:hidden;">
<div style="background:rgba(200,90,0,0.06);border-right:1px solid rgba(60,80,180,0.12);padding:20px 18px;">
<div style="font-size:10px;letter-spacing:0.15em;text-transform:uppercase;color:var(--orange);margin-bottom:6px;">GPU Stage 3</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:600;color:var(--text);margin-bottom:4px;">Output Merger</div>
<div style="font-size:12px;color:var(--text3);">ROP · Depth · Blend</div>
</div>
<div style="padding:20px 22px;display:flex;flex-direction:column;gap:6px;">
<div style="font-size:13px;font-weight:700;color:var(--text);">조명 결과 기록 — 셰이딩 결과를 render target에 쓴다</div>
<div style="font-size:13px;color:var(--text2);">Depth Test(가시성 최종 확인), Stencil Test, Alpha Blending(반투명 합산). 각 패스에서 계산된 조명 결과가 render target에 누적되고, 이후 Tone Mapping·Exposure·Post Process를 거쳐 최종 디스플레이 색으로 변환된다.</div>
<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(200,90,0,0.10);color:var(--orange);font-weight:600;">render target 기록</span>
<span style="font-size:11px;padding:2px 9px;border-radius:100px;background:rgba(136,144,170,0.12);color:var(--text3);font-weight:600;">Depth · Blend · ROP</span>
</div>
</div>
</div>
</div>
</div>
<div class="callout callout-info">
<div class="callout-title">💡 방정식과 파이프라인 대응 요약</div>
<p>InitViews는 "계산할 x를 고른다", Rasterization은 "x 좌표를 확정한다", Shading은 "방정식의 핵심 항들을 주로 평가한다", Output Merger는 "각 패스의 조명 결과를 render target에 누적한다". 방정식의 항들은 Base Pass, Shadow, GI, Reflection 등 여러 패스에 걸쳐 계산되고, 그 결과가 합산되어 최종 픽셀 색이 만들어진다.</p>
</div>
<span class="section-eyebrow">01 — 방정식 이해</span>
</div>

<h1 id="각-항이-의미하는-것">각 항이 의미하는 것</h1>

<div class="re-post">
렌더링 방정식은 "어떤 점 **x**에서 방향 ω<sub>o</sub>로 나가는 빛의 양"을 정의한다. 이 값을 알면 픽셀의 색을 결정할 수 있다.
<div class="term-grid">
<div class="term-card blue">
<div class="term-symbol">L<sub>o</sub>(x, ω<sub>o</sub>)</div>
<div class="term-name">나가는 복사 휘도 (Outgoing Radiance)</div>
<p class="term-desc">점 x에서 방향 ω<sub>o</sub>(카메라 방향)로 나가는 빛의 총량. 최종적으로 픽셀에 기록되는 값.</p>
</div>
<div class="term-card gold">
<div class="term-symbol">L<sub>e</sub>(x, ω<sub>o</sub>)</div>
<div class="term-name">방출 휘도 (Emitted Radiance)</div>
<p class="term-desc">재질 자체가 발광하는 경우 추가되는 항. 모니터, 네온사인, 불꽃 같은 발광 오브젝트가 해당.</p>
</div>
<div class="term-card teal">
<div class="term-symbol">f<sub>r</sub>(x, ω<sub>i</sub>, ω<sub>o</sub>)</div>
<div class="term-name">BRDF</div>
<p class="term-desc">양방향 반사 분포 함수. "들어온 빛 중 얼마나 반사되어 나가는가"를 결정하는 재질의 핵심.</p>
</div>
<div class="term-card coral">
<div class="term-symbol">L<sub>i</sub>(x, ω<sub>i</sub>)</div>
<div class="term-name">들어오는 복사 휘도 (Incoming Radiance)</div>
<p class="term-desc">방향 ω<sub>i</sub>에서 점 x로 들어오는 빛의 양. 이 값 자체도 재귀적으로 렌더링 방정식을 풀어야 한다.</p>
</div>
<div class="term-card purple">
<div class="term-symbol">cos θ<sub>i</sub></div>
<div class="term-name">Lambert 코사인 항</div>
<p class="term-desc">빛이 표면에 비스듬히 입사할수록 에너지가 넓게 퍼지는 물리 현상. 법선과 입사 방향의 내적.</p>
</div>
<div class="term-card orange">
<div class="term-symbol">∫<sub>Ω</sub> dω<sub>i</sub></div>
<div class="term-name">반구 적분</div>
<p class="term-desc">표면 법선을 기준으로 모든 방향에서 들어오는 빛을 다 더한다. 이 적분이 실시간 렌더링의 핵심 난제.</p>
</div>
</div>
<div class="callout callout-warn">
<div class="callout-title">⚡ 왜 어려운가</div>
<p>L<sub>i</sub>(x, ω<sub>i</sub>)를 구하려면 다시 렌더링 방정식을 풀어야 한다. 즉 빛은 재귀적으로 튕기고, 그 모든 경로를 추적하면 무한 연산이 필요하다. 실시간 엔진은 이 무한 재귀를 영리하게 <strong>근사</strong>한다.</p>
</div>
<span class="section-eyebrow">02 — UE5 렌더링 파이프라인</span>
</div>

<h1 id="언리얼-엔진5가-방정식을-푸는-방법">언리얼 엔진5가 방정식을 푸는 방법</h1>

<div class="re-post">
UE5는 렌더링 방정식의 각 항을 서로 다른 시스템이 나누어 계산한다. 완벽한 해가 아니라 시각적으로 그럴듯한 근사치를 실시간으로 만들어내는 것이 목표다.
<div class="pipeline">
<div class="pipe-item">
<div class="pipe-num">01</div>
<div class="pipe-body">
<div class="step-badge badge-exact">Geometry Pass</div>
<h3>기하 처리 및 재질 입력 생성 — Nanite + Base Pass</h3>
<p>Nanite가 픽셀 단위 정밀도로 가시 기하를 결정하고, 이어지는 Base Pass에서 재질 셰이더가 실행되어 위치(x), 법선, BaseColor, Roughness, Metallic 등이 G-Buffer에 저장된다. Nanite는 rasterization과 visibility를 담당하고, G-Buffer 저장은 deferred base pass의 결과다. 이후 모든 조명 계산의 입력값이 여기서 만들어진다.</p>
<div class="pipe-tag-row">
<span class="pipe-tag tag-geo">Nanite</span>
<span class="pipe-tag tag-geo">Deferred Shading</span>
<span class="pipe-tag tag-geo">G-Buffer</span>
</div>
</div>
</div>
<div class="pipe-item">
<div class="pipe-num">02</div>
<div class="pipe-body">
<div class="step-badge badge-exact">Direct Light</div>
<h3>직접광 — L<sub>i</sub> × f<sub>r</sub> × cosθ 직접 계산</h3>
<p>태양, 포인트 라이트, 스팟 라이트 등 명시적 광원에서 오는 빛은 방정식을 해석적으로 적용한다. 광원 방향이 정해져 있으므로 반구 적분이 단순화된다. UE5는 Disney식 metallic/roughness 워크플로의 영향을 받은 실시간 PBR 모델을 사용하며, GGX 기반 specular BRDF와 Lambert 계열의 diffuse 근사를 조합한다.</p>
<div class="pipe-tag-row">
<span class="pipe-tag tag-light">Direct Lighting</span>
<span class="pipe-tag tag-light">PBR / GGX BRDF</span>
<span class="pipe-tag tag-shadow">Shadow Map</span>
</div>
</div>
</div>
<div class="pipe-item">
<div class="pipe-num">03</div>
<div class="pipe-body">
<div class="step-badge badge-approx">GI Approximation</div>
<h3>Lumen — 간접광 (GI) 근사</h3>
<p>방정식에서 가장 어려운 부분: 다른 표면에서 한 번 이상 튕겨온 빛. Lumen은 Surface Cache(Mesh Cards), Screen Probe Gather, Radiance Cache, Software Ray Tracing(SDF), Hardware RT fallback을 계층적으로 조합해 scene-space에서 간접광을 근사한다. 단순한 SDF 레이마칭이 아니라 여러 기법의 하이브리드 시스템이다.</p>
<div class="pipe-tag-row">
<span class="pipe-tag tag-gi">Lumen GI</span>
<span class="pipe-tag tag-gi">SDF Ray Marching</span>
<span class="pipe-tag tag-gi">Radiance Cache</span>
<span class="pipe-tag tag-gi">SSGI</span>
</div>
</div>
</div>
<div class="pipe-item">
<div class="pipe-num">04</div>
<div class="pipe-body">
<div class="step-badge badge-approx">Reflection</div>
<h3>반사 — 거울 방향 L<sub>i</sub> 추적</h3>
<p>광택 있는 표면에서의 반사는 특정 방향 ω<sub>i</sub>에 집중된 샘플링으로 근사한다. 매끈한 표면일수록 반사 방향성이 강해져 더 높은 정확도의 추적이 필요하며, Lumen은 screen trace, surface cache 조회, hardware ray tracing 등을 플랫폼·설정·hit 여부에 따라 상황에 맞게 조합해 사용한다.</p>
<div class="pipe-tag-row">
<span class="pipe-tag tag-gi">Lumen Reflection</span>
<span class="pipe-tag tag-gi">Screen Space Reflection</span>
<span class="pipe-tag tag-gi">Hardware RT</span>
</div>
</div>
</div>
<div class="pipe-item">
<div class="pipe-num">05</div>
<div class="pipe-body">
<div class="step-badge badge-approx">Shadow</div>
<h3>그림자 — 가시성 함수 V(x, ω<sub>i</sub>)</h3>
<p>빛이 점 x에 실제로 도달하는지 여부를 결정하는 가시성 함수. UE5에서는 이를 Virtual Shadow Maps, screen-space visibility, distance field 기반 occlusion, ray tracing, Lumen 내부의 tracing hit/miss 등 여러 시스템이 나누어 담당하며 상황에 따라 혼합해 사용한다. Nanite VSM은 그 중 주요한 하나로 픽셀 단위 정밀도의 그림자를 메모리 효율적으로 제공한다.</p>
<div class="pipe-tag-row">
<span class="pipe-tag tag-shadow">Virtual Shadow Map</span>
<span class="pipe-tag tag-shadow">Screen-space Visibility</span>
<span class="pipe-tag tag-shadow">Distance Field Occlusion</span>
<span class="pipe-tag tag-shadow">Ray Traced Shadow</span>
</div>
</div>
</div>
<div class="pipe-item">
<div class="pipe-num">06</div>
<div class="pipe-body">
<div class="step-badge badge-hybrid">Post Process</div>
<h3>포스트 프로세스 — 최종 L<sub>o</sub> 보정</h3>
<p>계산된 Radiance 값을 실제 디스플레이에 맞게 변환한다. Tone Mapping(HDR → LDR 변환), Exposure, Bloom(발광 오브젝트 L<sub>e</sub> 항 강조), Temporal Anti-Aliasing이 여기서 처리된다. TSR(Temporal Super Resolution)은 낮은 해상도로 렌더링 후 업스케일해 성능을 확보한다.</p>
<div class="pipe-tag-row">
<span class="pipe-tag tag-post">Tone Mapping</span>
<span class="pipe-tag tag-post">Bloom</span>
<span class="pipe-tag tag-post">TSR</span>
<span class="pipe-tag tag-post">TAA</span>
</div>
</div>
</div>
</div>
<span class="section-eyebrow">03 — 1:1 대응</span>
</div>

<h1 id="방정식-항--ue5-시스템-매핑">방정식 항 ↔ UE5 시스템 매핑</h1>

<div class="re-post">
<table class="mapping-table">
<thead>
<tr>
<th>방정식 항</th>
<th>물리적 의미</th>
<th>UE5 구현</th>
<th>방식</th>
</tr>
</thead>
<tbody>
<tr>
<td class="math-cell">L<sub>o</sub>(x, ω<sub>o</sub>)</td>
<td class="desc-cell">최종 픽셀 색</td>
<td class="ue5-cell">Final Color Buffer</td>
<td class="desc-cell">합산 결과</td>
</tr>
<tr>
<td class="math-cell">L<sub>e</sub>(x, ω<sub>o</sub>)</td>
<td class="desc-cell">재질 자체 발광</td>
<td class="ue5-cell">Emissive Channel + Bloom</td>
<td class="desc-cell">직접 추가</td>
</tr>
<tr>
<td class="math-cell">f<sub>r</sub>(x, ω<sub>i</sub>, ω<sub>o</sub>)</td>
<td class="desc-cell">BRDF (재질 반응)</td>
<td class="ue5-cell">PBR Material (GGX + Lambert)</td>
<td class="desc-cell">Disney 근사</td>
</tr>
<tr>
<td class="math-cell">L<sub>i</sub> (직접광)</td>
<td class="desc-cell">광원에서 직접 오는 빛</td>
<td class="ue5-cell">Directional / Point / Spot Light</td>
<td class="desc-cell">해석적 계산</td>
</tr>
<tr>
<td class="math-cell">L<sub>i</sub> (간접광)</td>
<td class="desc-cell">다른 표면에서 반사된 빛</td>
<td class="ue5-cell">Lumen GI (Surface Cache · Screen Probe · Radiance Cache · Tracing)</td>
<td class="desc-cell">하이브리드 근사</td>
</tr>
<tr>
<td class="math-cell">L<sub>i</sub> (환경광)</td>
<td class="desc-cell">Sky / IBL</td>
<td class="ue5-cell">Sky Atmosphere + SkyLight</td>
<td class="desc-cell">큐브맵 컨볼루션</td>
</tr>
<tr>
<td class="math-cell">cos θ<sub>i</sub></td>
<td class="desc-cell">Lambert 코사인 항</td>
<td class="ue5-cell">N · L (Shader 내적 연산)</td>
<td class="desc-cell">셰이더 내적 계산</td>
</tr>
<tr>
<td class="math-cell">V(x, ω<sub>i</sub>)</td>
<td class="desc-cell">가시성 (그림자)</td>
<td class="ue5-cell">VSM · DFAO · RT Occlusion 등</td>
<td class="desc-cell">복수 시스템 조합</td>
</tr>
<tr>
<td class="math-cell">∫<sub>Ω</sub> dω<sub>i</sub></td>
<td class="desc-cell">반구 적분</td>
<td class="ue5-cell">분석적 조명 · 환경맵 사전적분 · Lumen probe · 시간적 누적</td>
<td class="desc-cell">복수 기법 조합</td>
</tr>
</tbody>
</table>
<span class="section-eyebrow">04 — BRDF 상세</span>
</div>

<h1 id="ue5의-pbr-재질">UE5의 PBR 재질</h1>

<div class="re-post">
UE5의 기본 재질 모델은 Disney식 metallic/roughness 워크플로의 영향을 받은 실시간 PBR 모델이다. Disney 원 논문을 그대로 구현한 것은 아니며, Epic이 SIGGRAPH 2013에서 발표한 실시간 근사를 기반으로 한다. BRDF는 Diffuse 항과 Specular 항으로 분리된다.
<div class="code-block"><div class="code-lang">HLSL (UE5 Shader)</div><span style="color:#525a78;font-style:italic">// UE5 PBR BRDF 구조 (단순화)</span>
<span class="kw">float3</span>
<span class="fn">BRDF</span>(MaterialInputs mat, <span class="kw">float3</span> L, <span class="kw">float3</span> V, <span class="kw">float3</span> N) { <span class="kw">float3</span> H = normalize(L + V); <span style="color:#525a78;font-style:italic">// Halfway vector</span>
<span class="kw">float</span> NdotL = saturate(dot(N, L)); <span style="color:#525a78;font-style:italic">// cosθ — 방정식의 cosθᵢ</span>
<span class="kw">float</span> NdotV = saturate(dot(N, V)); <span class="kw">float</span> NdotH = saturate(dot(N, H)); <span class="kw">float</span> roughness = mat.Roughness; <span style="color:#525a78;font-style:italic">// Specular BRDF: D × G × F / (4 × NdotL × NdotV)</span>
<span class="kw">float</span> D = <span class="fn">GGX_Distribution</span>(NdotH, roughness); <span style="color:#525a78;font-style:italic">// 노멀 분포 함수</span>
<span class="kw">float</span> G = <span class="fn">Smith_Schlick_GGX</span>(NdotL, NdotV, roughness); <span style="color:#525a78;font-style:italic">// 기하 감쇠</span>
<span class="kw">float3</span> F = <span class="fn">Fresnel_Schlick</span>(mat.F0, NdotV); <span style="color:#525a78;font-style:italic">// 프레넬 반사율</span>
<span class="kw">float3</span> Specular = (D * G * F) / (<span class="num">4.0</span> * NdotL * NdotV + <span class="num">0.001</span>); <span style="color:#525a78;font-style:italic">// Diffuse BRDF: Lambertian (에너지 보존 위해 Specular 뺌)</span>
<span class="kw">float3</span> kD = (<span class="num">1.0</span> - F) * (<span class="num">1.0</span> - mat.Metallic); <span class="kw">float3</span> Diffuse = kD * mat.BaseColor / PI; <span class="kw">return</span> (Diffuse + Specular) * NdotL; <span style="color:#525a78;font-style:italic">// × NdotL = cosθ 항</span> }</div>
<div class="brdf-grid">
<div class="brdf-card">
<span class="icon">🔵</span>
<h4>D — 노멀 분포 함수</h4>
<p>GGX/Trowbridge-Reitz 모델. Roughness에 따라 반사 로브의 날카로움을 결정.</p>
</div>
<div class="brdf-card">
<span class="icon">🟡</span>
<h4>G — 기하 감쇠 함수</h4>
<p>Smith's Schlick-GGX. 미세면이 서로 가리거나 반사광을 막는 효과(Shadowing/Masking).</p>
</div>
<div class="brdf-card">
<span class="icon">🟢</span>
<h4>F — 프레넬 반사율</h4>
<p>Schlick 근사. 빛이 표면에 비스듬히 입사할수록 반사율이 증가하는 프레넬 효과.</p>
</div>
</div>
<div class="callout callout-info">
<div class="callout-title">💡 Metallic/Roughness 파라미터</div>
<p>Metallic과 BaseColor가 함께 specular reflectance(F0)를 결정한다. 비금속의 경우 F0는 약 0.04로 고정되고, Metallic=1에 가까울수록 BaseColor가 specular reflectance를 직접 구성하며 diffuse 기여가 줄어든다. Roughness는 GGX 분포의 α값을 제어해 반사 로브의 날카로움을 결정한다. 이 두 파라미터로 BRDF를 실용적인 수준에서 제어한다.</p>
</div>
<span class="section-eyebrow">05 — 간접광의 핵심</span>
</div>

<h1 id="lumen이-gi를-근사하는-방법">Lumen이 GI를 근사하는 방법</h1>

<div class="re-post">
렌더링 방정식에서 가장 비싼 부분인 간접광(∫ L<sub>i</sub> dω<sub>i</sub>)을 Lumen은 여러 기법을 계층적으로 조합해 처리한다.
<div class="pipe-item" style="padding:0 0 20px">
<div class="pipe-num" style="background:var(--bg3)">→</div>
<div class="pipe-body">
<h3>Surface Cache &amp; Mesh Card</h3>
<p>씬의 모든 메시에 대해 Mesh Card(텍스처 형태의 표면 캐시)를 생성한다. 각 카드에는 해당 표면의 Radiance가 저장되며, 빛이 바뀌면 점진적으로 갱신된다. Screen Probe Gather가 이 Surface Cache를 scene-space에서 샘플링해 간접광을 누적한다. 인접 표면 간 다중 바운스 간접광도 이 캐시를 통해 반복적으로 근사한다.</p>
</div>
</div>
<div class="pipe-item" style="padding:0 0 20px">
<div class="pipe-num" style="background:var(--bg3)">→</div>
<div class="pipe-body">
<h3>Software Ray Marching (SDF)</h3>
<p>Signed Distance Field를 활용해 레이를 빠르게 전진시킨다. 정확한 삼각형 교차 검사 없이 "얼마나 가까운 표면이 있는가"만 확인해 레이를 진행시키므로 GPU에서 효율적으로 작동한다. 먼 거리의 GI에 주로 사용된다.</p>
</div>
</div>
<div class="pipe-item" style="padding:0 0 0">
<div class="pipe-num" style="background:var(--bg3)">→</div>
<div class="pipe-body">
<h3>Hardware Ray Tracing (선택적)</h3>
<p>DXR/Vulkan RT 지원 GPU에서는 실제 레이트레이싱으로 근거리 GI 및 반사를 계산한다. Lumen은 Software RT(원거리)와 Hardware RT(근거리)를 혼합해 품질과 성능을 동시에 달성한다.</p>
</div>
</div>
<div class="callout callout-info">
<div class="callout-title">📐 반구 적분의 실시간 근사</div>
<p>실시간 엔진은 반구 적분을 직접 계산하지 않는다. UE5는 분석적 직접광 계산, 환경맵 사전적분(IBL convolution), Screen Probe 기반 중요도 샘플링, Radiance Cache를 통한 공간 재사용, Temporal Accumulation을 통한 시간적 누적을 조합해 적분을 근사한다. 각 기법은 적분의 서로 다른 주파수 영역을 담당한다 — 저주파 영역(diffuse GI, irradiance)은 probe와 캐시로, 고주파 영역(glossy reflection, sharp visibility)은 tracing과 screen-space 기법으로 처리하는 방식이다.</p>
</div>
</div>

<h1 id="마치며">마치며</h1>

<div class="re-post">
<div class="summary-box">
<h3>렌더링 방정식은 "이상"이고, UE5는 "현실"이다</h3>
<p> 카지야의 방정식은 빛의 완벽한 물리적 거동을 기술하지만, 완전히 푸는 것은 오프라인 레이트레이싱에서도 수 시간이 걸린다. UE5는 Nanite, Lumen, VSM, TSR 등의 시스템으로 각 항을 지능적으로 근사해 초당 60프레임의 실시간 렌더링으로 구현해낸다. 이것이 "Physically Based Rendering"의 본질이다 — 물리를 흉내 내되, 영리하게. </p>
</div>
</div>]]></content><author><name></name></author><category term="Rendering" /><category term="Rendering" /><category term="UnrealEngine" /><summary type="html"><![CDATA[Rendering Equation]]></summary></entry></feed>