1. 다중 카메라 렌더링 시스템 구현
다중 카메라를 사용하는 주요 이유
1. 게임 뷰 + 에디터 뷰
•
•
•
서로 다른 시점에서 씬을 동시에 렌더링해야 할 경우 (Unity 씬뷰처럼)
2. UI와 3D 월드 분리
•
UI는 깊이/광원/투명도 등과 상관없이 항상 앞에 떠야 함
•
그래서 보통 UI 전용 카메라에서 orthographic + depth off 로 처리
[World Camera] → G-Buffer / Lighting 처리
[UI Camera] → Overlay / No Depth
Plain Text
복사
3. 미니맵 / CCTV / 거울 / 보안카메라
•
화면 구석에 다른 위치나 시점을 보여줘야 할 때
•
각각의 미니맵, CCTV가 자기 전용 카메라로 씬을 렌더링
4. Split Screen 멀티플레이
•
2명 이상이 같은 씬을 다른 시점에서 동시에 보는 경우
•
각 플레이어마다 독립된 카메라 필요 → 분할 화면 구성
5. 후처리 효과 분리
•
특정 카메라는 Bloom, DOF 적용
•
다른 카메라는 그냥 렌더링 → 예: Portal, XR, Virtual Camera 시스템
6. 렌더 타겟에 별도로 그리고 싶을 때
•
예: Game View는 화면에, Scene View는 Texture로 그려서 저장하거나 디버깅
•
이때 **카메라별로 Render Target (RTV)**를 따로 갖게 됨
1. 핵심 메인 루프 (Scene::Render)
for (Camera* camera : mCameras)
C++
복사
•
씬에 등록된 모든 카메라에 대해 반복 수행
•
각 카메라의 뷰에서 오브젝트를 렌더링
•
UI 카메라, 게임 카메라 등 분리 가능
2. 카메라별 렌더링 처리 흐름
viewMatrix, projectionMatrix, cameraPos
void Scene::Render()
{
for (Camera* camera : mCameras)
{
if (camera == nullptr)
continue;
Matrix viewMatrix = camera->GetViewMatrix();
Matrix projectionMatrix = camera->GetProjectionMatrix();
Vector3 cameraPos = camera->GetOwner()->GetComponent<Transform>()->GetPosition();
std::vector<GameObject*> opaqueList = {};
std::vector<GameObject*> cutoutList = {};
std::vector<GameObject*> transparentList = {};
// collect randerables(game objects)
CollectRenderables(opaqueList, cutoutList, transparentList);
// soring renderables by distance (between camera and game object)
SortByDistance(opaqueList, cameraPos, true);
SortByDistance(cutoutList, cameraPos, true);
SortByDistance(transparentList, cameraPos, false);
// render game objects
RenderRenderables(opaqueList, viewMatrix, projectionMatrix);
RenderRenderables(cutoutList, viewMatrix, projectionMatrix);
RenderRenderables(transparentList, viewMatrix, projectionMatrix);
}
}
C++
복사
•
현재 카메라 기준으로 시점 및 좌표 변환 준비
렌더 리스트 초기화
std::vector<GameObject*> opaqueList = {};
std::vector<GameObject*> cutoutList = {};
std::vector<GameObject*> transparentList = {};
C++
복사
•
재질 타입에 따라 리스트 분류 (불투명, 컷아웃, 반투명)
3. 오브젝트 수집 - CollectRenderables
void Scene::CollectRenderables(std::vector<GameObject*>& opaqueList, std::vector<GameObject*>& cutoutList
, std::vector<GameObject*>& transparentList) const
{
for (Layer* layer : mLayers)
{
if (layer == nullptr)
continue;
std::vector<GameObject*>& gameObjects = layer->GetGameObjects();
for (GameObject* gameObj : gameObjects)
{
if (gameObj == nullptr)
continue;
// to do : renderer 상속구조 만들기
SpriteRenderer* renderer = gameObj->GetComponent<SpriteRenderer>();
if (renderer == nullptr)
continue;
switch (renderer->GetMaterial()->GetRenderingMode())
{
case graphics::eRenderingMode::Opaque:
opaqueList.push_back(gameObj);
break;
case graphics::eRenderingMode::CutOut:
cutoutList.push_back(gameObj);
break;
case graphics::eRenderingMode::Transparent:
transparentList.push_back(gameObj);
break;
}
}
}
}
C++
복사
•
Opaque - 디폴트입니다. 투명한 영역이 없는 일반 솔리드 오브젝트에 적합합니다.
•
Cutout - 투명 영역과 불투명 영역 사이에 하드 에지가 있는 투명 효과를 만들 수 있습니다. 이 모드에서는 반투명 영역이 없고 텍스처가 100% 불투명이거나 보이지 않습니다. 이 모드는 나뭇잎이나 구멍과 찢어진 부분이 있는 옷감 같은 머티리얼의 형상을 만들기 위해 투명도를 사용할 때 유용합니다.
•
Transparent - 투명한 플라스틱이나 유리처럼 투명한 머티리얼을 사실적으로 렌더링하는 데 적합합니다. 이 모드에서는 머티리얼 자체에 (텍스처의 알파 채널과 틴트 컬러의 알파에 기반한)투명도 값이 있지만, 반사 및 조명 하이라이트는 실제 투명 머티리얼과 마찬가지로 계속 완전히 투명하게 보입니다.
각 게임 오브젝트에서 SpriteRenderer 컴포넌트를 찾아 렌더링 타입 분류:
•
Opaque: 깊이 테스트 + Z 쓰기
•
CutOut: 깊이 테스트 + 알파 테스트
•
Transparent: 깊이 정렬 필요
4. 거리 정렬 - SortByDistance
void Scene::SortByDistance(std::vector<GameObject*>& renderList, const Vector3& cameraPos, bool bAscending) const
{
// opaqueList and cutoutList are sorted in ascending order
// trasparentList is sorted in descending order
auto comparator = [cameraPos, bAscending](GameObject* a, GameObject* b)
{
float distA = Vector3::Distance(a->GetComponent<Transform>()->GetPosition(), cameraPos);
float distB = Vector3::Distance(b->GetComponent<Transform>()->GetPosition(), cameraPos);
return bAscending ? (distA < distB) : (distA > distB);
};
std::ranges::sort(renderList, comparator);
}
C++
복사
•
거리 기준 정렬:
◦
불투명 / 컷아웃: 가까운 순 (앞 → 뒤)
◦
반투명: 먼 순 (뒤 → 앞) → 블렌딩용
렌더링 타입별 깊이 테스트 변경
깊이 버퍼(Depth Buffer)란?
•
GPU가 "화면에서 가장 앞에 있는 픽셀만 출력" 하기 위해 사용하는 버퍼입니다.
•
모든 픽셀은 그려지기 전에 Z값(깊이값)을 기존 값과 비교해서 그릴지 말지를 결정합니다.
Depth Test 함수 종류 요약
함수 이름 | 의미 | 예시 |
Less | 현재 픽셀이 더 가까우면 출력 | 기본값 |
Equal | 깊이가 같을 때만 출력 | 스텐실 등 |
Always | 항상 출력 (Z비교 안 함) | 투명 객체에 자주 사용 |
LessEqual | 더 가깝거나 같으면 출력 | 이중 패스 등 |
Opaque vs Transparent 의 깊이 테스트 차이
D3D11_DEPTH_STENCIL_DESC dsDesc = {};
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS_EQUAL;
dsDesc.StencilEnable = false;
GetDevice()->CreateDepthStencilState(
&dsDesc, depthStencilStates[static_cast<UINT>(eDepthStencilState::LessEqual)].GetAddressOf());
dsDesc = {};
dsDesc.DepthEnable = false;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
dsDesc.DepthFunc = D3D11_COMPARISON_NEVER;
dsDesc.StencilEnable = false;
GetDevice()->CreateDepthStencilState(
&dsDesc, depthStencilStates[static_cast<UINT>(eDepthStencilState::DepthNone)].GetAddressOf());
dsDesc = {};
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
dsDesc.DepthFunc = D3D11_COMPARISON_ALWAYS;
dsDesc.StencilEnable = false;
GetDevice()->CreateDepthStencilState(
&dsDesc, depthStencilStates[static_cast<UINT>(eDepthStencilState::Always)].GetAddressOf());
C++
복사
Opaque, Cutout (불투명)
•
깊이 비교 활성화 (ZTest: LessEqual)
•
픽셀이 더 앞에 있으면 그려짐, 뒤에 있으면 무시됨
•
→ 가장 앞쪽의 불투명 픽셀만 화면에 나옴
•
*Z Write(깊이 쓰기)**도 켜져 있음 → 다른 객체는 이 값을 기준으로 비교
Transparent (투명)
•
깊이 비교를 아예 하지 않음
•
ZWrite도 보통 꺼져 있음 (ZWrite: Off)
•
→ 순서대로 그려야 함 (Back to Front 정렬 필수)
ZTest를 하면 뒤에 있는 픽셀이 잘려버려 투명 블렌딩이 깨짐
왜 Always?
•
투명 렌더링은 GPU에게 “깊이로 거르지 마! 걍 그려봐!” 라고 말하는 것과 같음
•
대신 개발자가 순서를 보장해야 함
◦
보통 카메라로부터 거리 정렬 → 먼 것부터 그리기 (Back to Front)
주의: 투명 객체는 Z Write도 꺼져야
깊이 값을 쓰면 뒤에 있는 투명한 것도 앞에 있는 투명 오브젝트가 가려버림
→ 투명끼리의 겹침도 깨짐
그래서 Unity Standard Shader도 이런 식으로 동작:
shader
복사편집
Tags { "Queue"="Transparent" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Plain Text
복사
결론 요약
설정 항목 | Opaque | Transparent |
DepthFunc | LessEqual | Always |
ZWrite | On | Off |
정렬 필요? | X | |
이유 | 깊이 테스트로 오버드로우 방지 | 겹쳐서 그려야 투명하게 보임 |
5. 오브젝트 렌더링 - RenderRenderables
D3D11_BLEND_DESC bsDesc = {};
bsDesc.RenderTarget[0].BlendEnable = FALSE;
bsDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
GetDevice()->CreateBlendState(&bsDesc, blendStates[static_cast<UINT>(eBlendState::Opaque)].GetAddressOf());
bsDesc = {};
bsDesc.RenderTarget[0].BlendEnable = FALSE;
bsDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
GetDevice()->CreateBlendState(&bsDesc, blendStates[static_cast<UINT>(eBlendState::Cutout)].GetAddressOf());
bsDesc = {};
bsDesc.RenderTarget[0].BlendEnable = TRUE;
bsDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
bsDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
bsDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
bsDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
bsDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
bsDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
bsDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
GetDevice()->CreateBlendState(&bsDesc, blendStates[static_cast<UINT>(eBlendState::Transparent)].GetAddressOf());
C++
복사
•
정렬된 오브젝트 리스트를 순회하며 렌더링 호출
렌더링 모드별 거리 정렬시 사용할 블렌딩 모드
렌더 모드 | BlendEnable | SrcBlend | DestBlend | 설명 |
Opaque | FALSE | - | - | 깊이 테스트만 사용, 가장 빠름 |
Cutout | FALSE | - | - | 알파테스트(discard) 처리 |
Transparent | TRUE | SRC_ALPHA | INV_SRC_ALPHA | 알파 블렌딩 처리 |
6. 카메라 추가/제거
void Scene::AddCamera(Camera* camera);
void Scene::RemoveCamera(Camera* camera);
C++
복사
•
mCameras 벡터에 카메라를 추가/삭제
•
씬에 존재하는 카메라 수를 유동적으로 제어 가능
•
UI 카메라, 월드 카메라 등을 구분하여 등록 가능
2. BaseRenderer 도입
BaseRenderer 도입 이유 정리
렌더링 계층의 다형성 확보
BaseRenderer* renderer = gameObj->GetComponent<BaseRenderer>();
renderer->Render(view, proj);
C++
복사
•
여러 종류의 렌더러를 하나의 리스트에 넣고 순회하면서 렌더링 가능
•
GameObject의 컴포넌트가 SpriteRenderer이든 MeshRenderer이든 상관없이 공통 렌더링 인터페이스 제공
•
yaBaseRenderer라는 공통 렌더링 인터페이스를 정의하여, SpriteRenderer 등 구현체들이 이를 상속 가능하게 구조화.
•
추후 다양한 렌더러 추가 확장을 고려한 구조 개선.
구조 예시
// Base
class BaseRenderer : public Component {
public:
virtual void Render(const Matrix& view, const Matrix& proj) = 0;
};
// 구현
class SpriteRenderer : public BaseRenderer {
public:
void Render(const Matrix& view, const Matrix& proj) override;
};
class MeshRenderer : public BaseRenderer {
public:
void Render(const Matrix& view, const Matrix& proj) override;
};
C++
복사
항목 | 효과 |
코드 일관성 | Renderer마다 개별 처리할 필요 없음 |
유지보수성 | 공통 속성은 상위에서 처리 |
다형성 활용 | 모든 렌더러를 같은 리스트로 처리 가능 |
렌더링 최적화 | 정렬, 컬링, Batch 처리 가능 |
구조 확장 | 신규 렌더러 도입이 쉬움 |
3. DirectXTex 외부 프로젝트 추가
•
Vendor/DirectXTex 디렉토리 추가.
•
다양한 이미지 포맷 지원과 압축 해제, GPU 연산 기반 텍스처 전처리 가능.
•
.vcxproj, .filters 파일 포함으로 Visual Studio 통합 완료.
•
라이브러리 프로젝트를 솔루션에 추가하여 보관하면 내부 함수 디버깅이 가능해진다.
외부 프로젝트를 직접 추가하는 방식의 장점
1. 내부 함수 디버깅 가능
•
가장 큰 장점!
•
F11 또는 Call Stack으로 외부 라이브러리 내부까지 소스 추적 디버깅 가능
2. 라이브러리 빌드 설정을 직접 조정 가능
•
디버그/릴리즈 설정, 정적/동적 링킹, 런타임 설정 등을 직접 커스터마이징 가능
•
예: DirectXTex에서 특정 압축 포맷만 제거하거나, SSE 최적화 꺼버릴 수 있음
3. 의존성 자동 해결 없이 바로 사용 가능
•
시스템에 vcpkg 설치되어 있지 않아도 프로젝트 복제 후 바로 빌드 가능
•
팀원이나 협업자 입장에서 빌드 환경 독립성 확보
4. 패키징 또는 정적 링크 없이 배포 가능
•
빌드 아티팩트가 .lib 등 외부 파일로 흩어지지 않고, 솔루션 안에 묶여 있음
•
릴리즈 배포 시 번들 관리가 쉬움
5. Visual Studio 필터, 그룹 관리 쉬움
•
.filters 포함 시 솔루션 탐색기에서 파일 정리 + 수정 편리
단점도 명확히 존재합니다
1. 업데이트가 불편
•
외부 프로젝트에 버그 수정이나 새 기능이 생겨도,
Git Submodule이 아니면 수동으로 교체/머지해야 함
2. 의존성 충돌 위험
•
여러 외부 프로젝트가 STL 버전, CRT 설정, Warning Level 등 충돌할 수 있음
•
특히 Unity Build나 C++ 전처리기 확장 사용 시 컴파일 충돌 가능성 있음
3. 빌드 시간 증가
•
빌드 대상이 .lib가 아니라 .cpp 전체이므로 전체 빌드 시 시간 증가
4. 빌드 플랫폼에 따라 설정 추가 필요
•
예: x64 Debug와 Win32 Release 빌드 옵션을 모두 직접 맞춰야 함
•
경로/플랫폼별 설정 자동화가 어려움 (vcpkg는 자동 처리)
5. 모듈화를 깨뜨릴 가능성
•
프로젝트 내부로 끌어오면 의존성이 흐려지고,
“직접 고치기 시작하면” → 향후 유지보수 포인트가 외부까지 퍼짐
정리: 직접 추가 vs 패키지 설치
항목 | 직접 프로젝트 추가 (소스 포함) | vcpkg / Conan 등 설치 방식 |
디버깅 | ||
커스터마이징 | ||
설정 관리 | ||
빌드 시간 | ||
협업 일관성 | ||
유지보수 |
추천 전략
상황 | 전략 |
디버깅이 중요한 이미지/수학/툴킷 | 직접 추가 (예: DirectXTex, ImGuizmo 등) |
안정된 릴리즈 버전 사용만 필요 | vcpkg 설치 (예: GLFW, stb, imgui 등) |
패키지화해서 릴리즈할 경우 | 정적 링크해서 .lib만 포함 |