Scene View 시스템: 에디터 뷰포트 구현
게임 엔진 에디터의 핵심은 Scene View입니다. Scene View는 개발자가 게임 월드를 시각적으로 편집할 수 있는 창으로, Unity와 Unreal Engine 같은 상용 엔진의 필수 기능입니다. 이 문서에서는 DirectX 11과 ImGui를 활용하여 완전한 Scene View 시스템을 구현하는 방법을 상세히 다룹니다.
Scene View의 필요성
Scene View는 단순히 게임 화면을 보여주는 것을 넘어, 에디터의 중심 작업 공간입니다.
Scene View의 핵심 기능
시각적 편집 환경
개발자는 코드를 작성하지 않고도 오브젝트를 배치하고 조작할 수 있습니다.
•
드래그 앤 드롭: 프로젝트 브라우저에서 에셋을 Scene View로 드래그하여 즉시 배치
•
기즈모 조작: 오브젝트를 마우스로 직접 이동, 회전, 크기 조절
•
카메라 내비게이션: WASD 키와 마우스로 씬을 자유롭게 탐색
실시간 프리뷰
게임이 실제로 어떻게 보일지 즉시 확인할 수 있습니다.
•
렌더링 결과: 조명, 그림자, 포스트 프로세싱이 적용된 최종 화면
•
즉각적인 피드백: 변경 사항이 실시간으로 반영되어 작업 효율성 향상
•
여러 뷰 모드: 와이어프레임, 라이팅만, 텍스처만 등 다양한 시각화 모드
독립적인 렌더 타겟
Game View와 분리된 별도의 렌더링 파이프라인을 제공합니다.
•
에디터 전용 렌더링: 그리드, 기즈모, 선택 아웃라인 등 에디터 UI 요소 표시
•
다중 뷰포트: 여러 각도에서 동시에 씬을 관찰
•
해상도 독립성: 에디터 창 크기와 게임 해상도를 독립적으로 설정
Game View vs Scene View
측면 | Scene View (에디터 뷰) | Game View (게임 뷰) |
목적 | 개발 및 편집 작업 공간 | 실제 플레이어가 보는 화면 |
카메라 제어 | 개발자가 자유롭게 조작 (WASD, 마우스) | 게임 로직에 의해 제어 (플레이어 추적 등) |
렌더링 요소 | 에디터 기즈모, 그리드, 아웃라인, 디버그 정보 | 게임 오브젝트, UI만 표시 |
입력 처리 | 에디터 단축키, 오브젝트 선택, 기즈모 조작 | 게임 입력 (플레이어 이동, 공격 등) |
해상도 | 에디터 창 크기에 따라 동적 변경 | 게임 설정에 따라 고정 (예: 1920x1080) |
RenderTarget 클래스: 오프스크린 렌더링의 핵심
Scene View를 구현하려면 먼저 스왑체인의 백버퍼가 아닌 별도의 텍스처에 렌더링해야 합니다. RenderTarget 클래스는 이러한 오프스크린 렌더링을 캡슐화합니다.
RenderTarget이 필요한 이유
스왑체인의 제약
DirectX의 스왑체인은 화면 출력용으로 설계되어 있어 ImGui 창 내부에 직접 렌더링할 수 없습니다.
// 문제: 스왑체인 백버퍼는 전체 화면에만 렌더링 가능
// ImGui 창 내부에는 렌더링 불가능
// 해결: 별도의 텍스처에 렌더링 후 ImGui에 표시
RenderTarget* sceneViewTarget = RenderTarget::Create(spec);
sceneViewTarget->Bind();
// 씬 렌더링...
sceneViewTarget->Unbind();
// ImGui 창에서 텍스처 표시
ImGui::Image(sceneViewTarget->GetTexture(), size);
C++
복사
다중 렌더 타겟 (MRT)
Deferred Rendering이나 고급 효과를 위해 여러 렌더 타겟에 동시에 렌더링해야 합니다.
포스트 프로세싱 체인
여러 단계의 후처리 효과를 적용하려면 중간 결과를 저장할 렌더 타겟이 필요합니다.
렌더 타겟 포맷 열거형
enum class eRenderTargetFormat
{
None = 0,
RGBA8, // 8비트 RGBA 색상
RED_INTEGER, // 정수형 RED 채널 (오브젝트 ID 저장)
DEPTH24STENCIL8, // 24비트 깊이 + 8비트 스텐실
SHADER_RESOURCE, // 셰이더에서 읽을 수 있는 범용 포맷
Depth = DEPTH24STENCIL8
};
C++
복사
각 포맷의 용도
RGBA8
•
일반적인 색상 렌더링
•
각 채널 8비트 (0-255)
•
메모리 효율적
•
용도: 알베도, 디퓨즈 색상
RED_INTEGER
•
정수 데이터 저장 (부동소수점 아님)
•
오브젝트 ID나 마스크 저장
•
용도: 마우스 픽킹, 오브젝트 선택
// 오브젝트 ID를 RED_INTEGER 포맷 렌더 타겟에 저장
pixelShaderOutput.objectID = entityID; // 1, 2, 3, ...
// 마우스 클릭 시 오브젝트 ID 읽기
int clickedObjectID = renderTarget->ReadPixel(0, mouseX, mouseY);
C++
복사
DEPTH24STENCIL8
•
24비트 깊이 값 (0.0 ~ 1.0을 24비트로 표현)
•
8비트 스텐실 값 (0 ~ 255)
•
용도: 깊이 테스트, 스텐실 마스킹, 섀도우 맵
SHADER_RESOURCE
•
셰이더에서 텍스처로 샘플링 가능
•
포스트 프로세싱에 사용
•
용도: Bloom, Blur, Reflection 등
렌더 타겟 사양 구조체
struct RenderTargetTextureSpecification
{
eRenderTargetFormat TextureFormat = eRenderTargetFormat::None;
// 향후 확장 가능: 필터링, 래핑 모드 등
};
struct RenderTargetAttachmentSpecification
{
std::vector<RenderTargetTextureSpecification> Attachments;
};
struct RenderTargetSpecification
{
UINT Width = 0, Height = 0;
RenderTargetAttachmentSpecification Attachments;
UINT Samples = 1; // MSAA 샘플 수
bool SwapChainTarget = false; // 스왑체인 백버퍼 사용 여부
};
C++
복사
사용 예시
// Scene View용 렌더 타겟 생성
RenderTargetSpecification sceneSpec;
sceneSpec.Width = 1280;
sceneSpec.Height = 720;
sceneSpec.Attachments.Attachments =
{
{ eRenderTargetFormat::RGBA8 }, // 색상
{ eRenderTargetFormat::RED_INTEGER }, // 오브젝트 ID
{ eRenderTargetFormat::DEPTH24STENCIL8} // 깊이
};
RenderTarget* sceneViewRT = RenderTarget::Create(sceneSpec);
C++
복사
RenderTarget 클래스 구현
RenderTarget 클래스는 복잡한 DirectX API를 캡슐화하여 간단한 인터페이스를 제공합니다.
클래스 선언
class RenderTarget
{
public:
RenderTarget(const RenderTargetSpecification& spec);
virtual ~RenderTarget();
// 정적 생성 함수
static RenderTarget* Create(const RenderTargetSpecification& spec);
// 렌더 타겟 재생성
void Invalidate();
// 바인딩 (렌더링 대상으로 설정)
void Bind();
void Unbind();
// 크기 조정
void Resize(UINT width, UINT height);
// 픽셀 데이터 읽기
int ReadPixel(uint32_t attachmentIndex, int x, int y);
// 첨부물 초기화
void ClearAttachment(UINT index, const void* value);
// 텍스처 접근
Texture* GetAttachmentTexture(UINT index);
// 사양 접근
RenderTargetSpecification& GetSpecification() { return mSpecification; }
private:
RenderTargetSpecification mSpecification;
// 색상 첨부물
std::vector<RenderTargetTextureSpecification> mColorAttachmentSpecifications;
std::vector<Texture*> mColorAttachments;
// 깊이 첨부물
RenderTargetTextureSpecification mDepthAttachmentSpecification;
Texture* mDepthAttachment = nullptr;
};
C++
복사
Invalidate: 렌더 타겟 초기화
Invalidate()는 실제 DirectX 리소스를 생성하는 핵심 함수입니다.
void RenderTarget::Invalidate()
{
// 1. 기존 리소스 해제
for (auto* attachment : mColorAttachments)
{
if (attachment)
delete attachment;
}
mColorAttachments.clear();
if (mDepthAttachment)
{
delete mDepthAttachment;
mDepthAttachment = nullptr;
}
// 2. 색상 첨부물 생성
for (const auto& spec : mColorAttachmentSpecifications)
{
Texture* colorTexture = CreateColorTexture(
mSpecification.Width,
mSpecification.Height,
spec.TextureFormat
);
mColorAttachments.push_back(colorTexture);
}
// 3. 깊이 첨부물 생성
if (mDepthAttachmentSpecification.TextureFormat != eRenderTargetFormat::None)
{
mDepthAttachment = CreateDepthTexture(
mSpecification.Width,
mSpecification.Height,
mDepthAttachmentSpecification.TextureFormat
);
}
}
C++
복사
색상 텍스처 생성 상세
Texture* RenderTarget::CreateColorTexture(
UINT width,
UINT height,
eRenderTargetFormat format)
{
// DirectX 텍스처 설명
D3D11_TEXTURE2D_DESC textureDesc = {};
textureDesc.Width = width;
textureDesc.Height = height;
textureDesc.MipLevels = 1;
textureDesc.ArraySize = 1;
textureDesc.SampleDesc.Count = mSpecification.Samples;
textureDesc.SampleDesc.Quality = 0;
textureDesc.Usage = D3D11_USAGE_DEFAULT;
textureDesc.CPUAccessFlags = 0;
// 포맷 설정
switch (format)
{
case eRenderTargetFormat::RGBA8:
textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
textureDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
break;
case eRenderTargetFormat::RED_INTEGER:
textureDesc.Format = DXGI_FORMAT_R32_SINT;
textureDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
break;
// ... 기타 포맷
}
// 텍스처 생성
ID3D11Texture2D* d3dTexture = nullptr;
HRESULT hr = device->CreateTexture2D(&textureDesc, nullptr, &d3dTexture);
// RTV (Render Target View) 생성
ID3D11RenderTargetView* rtv = nullptr;
hr = device->CreateRenderTargetView(d3dTexture, nullptr, &rtv);
// SRV (Shader Resource View) 생성
ID3D11ShaderResourceView* srv = nullptr;
hr = device->CreateShaderResourceView(d3dTexture, nullptr, &srv);
// Texture 래퍼 객체 생성
Texture* texture = new Texture();
texture->SetD3DTexture(d3dTexture);
texture->SetRTV(rtv);
texture->SetSRV(srv);
return texture;
}
C++
복사
깊이 텍스처 생성
Texture* RenderTarget::CreateDepthTexture(
UINT width,
UINT height,
eRenderTargetFormat format)
{
D3D11_TEXTURE2D_DESC depthDesc = {};
depthDesc.Width = width;
depthDesc.Height = height;
depthDesc.MipLevels = 1;
depthDesc.ArraySize = 1;
depthDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthDesc.SampleDesc.Count = mSpecification.Samples;
depthDesc.SampleDesc.Quality = 0;
depthDesc.Usage = D3D11_USAGE_DEFAULT;
depthDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthDesc.CPUAccessFlags = 0;
ID3D11Texture2D* depthTexture = nullptr;
device->CreateTexture2D(&depthDesc, nullptr, &depthTexture);
// DSV (Depth Stencil View) 생성
ID3D11DepthStencilView* dsv = nullptr;
device->CreateDepthStencilView(depthTexture, nullptr, &dsv);
Texture* texture = new Texture();
texture->SetD3DTexture(depthTexture);
texture->SetDSV(dsv);
return texture;
}
C++
복사
Bind: 렌더 타겟 활성화
void RenderTarget::Bind()
{
// 1. RTV 배열 준비
std::vector<ID3D11RenderTargetView*> rtvs;
for (auto* attachment : mColorAttachments)
{
rtvs.push_back(attachment->GetRTV().Get());
}
// 2. DSV 가져오기
ID3D11DepthStencilView* dsv = mDepthAttachment
? mDepthAttachment->GetDSV().Get()
: nullptr;
// 3. 렌더 타겟 설정
deviceContext->OMSetRenderTargets(
static_cast<UINT>(rtvs.size()),
rtvs.data(),
dsv
);
// 4. 뷰포트 설정
D3D11_VIEWPORT viewport = {};
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.Width = static_cast<float>(mSpecification.Width);
viewport.Height = static_cast<float>(mSpecification.Height);
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
deviceContext->RSSetViewports(1, &viewport);
}
C++
복사
Resize: 동적 크기 조정
void RenderTarget::Resize(UINT width, UINT height)
{
// 크기가 변경되지 않았으면 무시
if (mSpecification.Width == width && mSpecification.Height == height)
return;
// 사양 업데이트
mSpecification.Width = width;
mSpecification.Height = height;
// 리소스 재생성
Invalidate();
}
C++
복사
ReadPixel: 픽셀 데이터 읽기
마우스 픽킹을 위해 특정 픽셀의 값을 읽어옵니다.
int RenderTarget::ReadPixel(uint32_t attachmentIndex, int x, int y)
{
assert(attachmentIndex < mColorAttachments.size());
Texture* texture = mColorAttachments[attachmentIndex];
// 1. Staging 텍스처 생성 (CPU에서 읽을 수 있는 텍스처)
D3D11_TEXTURE2D_DESC stagingDesc = {};
stagingDesc.Width = 1;
stagingDesc.Height = 1;
stagingDesc.MipLevels = 1;
stagingDesc.ArraySize = 1;
stagingDesc.Format = DXGI_FORMAT_R32_SINT; // RED_INTEGER 포맷
stagingDesc.SampleDesc.Count = 1;
stagingDesc.Usage = D3D11_USAGE_STAGING;
stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
ID3D11Texture2D* stagingTexture = nullptr;
device->CreateTexture2D(&stagingDesc, nullptr, &stagingTexture);
// 2. 픽셀 복사
D3D11_BOX sourceRegion = {};
sourceRegion.left = x;
sourceRegion.right = x + 1;
sourceRegion.top = y;
sourceRegion.bottom = y + 1;
sourceRegion.front = 0;
sourceRegion.back = 1;
deviceContext->CopySubresourceRegion(
stagingTexture,
0,
0, 0, 0,
texture->GetD3DTexture(),
0,
&sourceRegion
);
// 3. CPU에서 데이터 읽기
D3D11_MAPPED_SUBRESOURCE mappedResource = {};
deviceContext->Map(stagingTexture, 0, D3D11_MAP_READ, 0, &mappedResource);
int pixelValue = *static_cast<int*>(mappedResource.pData);
deviceContext->Unmap(stagingTexture, 0);
stagingTexture->Release();
return pixelValue;
}
C++
복사
ClearAttachment: 첨부물 초기화
void RenderTarget::ClearAttachment(UINT index, const void* value)
{
assert(index < mColorAttachments.size());
Texture* texture = mColorAttachments[index];
// 정수형 포맷인 경우
if (mColorAttachmentSpecifications[index].TextureFormat == eRenderTargetFormat::RED_INTEGER)
{
UINT clearValue[4] = { *static_cast<const int*>(value), 0, 0, 0 };
deviceContext->ClearRenderTargetView(texture->GetRTV().Get(), reinterpret_cast<const float*>(clearValue));
}
// 실수형 포맷인 경우
else
{
float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
if (value)
memcpy(clearColor, value, sizeof(float) * 4);
deviceContext->ClearRenderTargetView(texture->GetRTV().Get(), clearColor);
}
}
C++
복사
윈도우 크기 변경 처리
에디터 창 크기가 변경될 때 렌더 타겟과 뷰포트를 동기화해야 합니다.
ResizeGraphicDevice 구현
void Application::ResizeGraphicDevice()
{
// 그래픽 디바이스가 없으면 무시
if (mGraphicDevice == nullptr)
return;
// 1. 윈도우 클라이언트 영역 크기 가져오기
RECT winRect;
::GetClientRect(mHwnd, &winRect);
// 2. 뷰포트 설정
D3D11_VIEWPORT viewport = {};
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.Width = static_cast<float>(winRect.right - winRect.left);
viewport.Height = static_cast<float>(winRect.bottom - winRect.top);
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
// 3. 애플리케이션 크기 저장
mWidth = static_cast<UINT>(viewport.Width);
mHeight = static_cast<UINT>(viewport.Height);
// 4. 그래픽 디바이스 리소스 조정
mGraphicDevice->Resize(viewport);
// 5. 프레임 버퍼 크기 조정
if (renderer::FrameBuffer)
{
renderer::FrameBuffer->Resize(mWidth, mHeight);
}
}
C++
복사
GraphicDevice::Resize 상세
void GraphicDevice_DX11::Resize(const D3D11_VIEWPORT& viewport)
{
// 1. 기존 RTV와 DSV 해제
mContext->OMSetRenderTargets(0, nullptr, nullptr);
if (mRenderTargetView)
{
mRenderTargetView->Release();
mRenderTargetView = nullptr;
}
if (mDepthStencilView)
{
mDepthStencilView->Release();
mDepthStencilView = nullptr;
}
// 2. 스왑체인 버퍼 크기 조정
HRESULT hr = mSwapChain->ResizeBuffers(
0, // 버퍼 수 유지
static_cast<UINT>(viewport.Width),
static_cast<UINT>(viewport.Height),
DXGI_FORMAT_UNKNOWN, // 포맷 유지
0
);
if (FAILED(hr))
{
LOG_ERROR("Failed to resize swap chain buffers");
return;
}
// 3. 새 백버퍼로 RTV 재생성
ID3D11Texture2D* backBuffer = nullptr;
mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer);
mDevice->CreateRenderTargetView(backBuffer, nullptr, &mRenderTargetView);
backBuffer->Release();
// 4. 새 깊이 스텐실 버퍼 생성
D3D11_TEXTURE2D_DESC depthDesc = {};
depthDesc.Width = static_cast<UINT>(viewport.Width);
depthDesc.Height = static_cast<UINT>(viewport.Height);
depthDesc.MipLevels = 1;
depthDesc.ArraySize = 1;
depthDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthDesc.SampleDesc.Count = 1;
depthDesc.Usage = D3D11_USAGE_DEFAULT;
depthDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
ID3D11Texture2D* depthStencilBuffer = nullptr;
mDevice->CreateTexture2D(&depthDesc, nullptr, &depthStencilBuffer);
mDevice->CreateDepthStencilView(depthStencilBuffer, nullptr, &mDepthStencilView);
depthStencilBuffer->Release();
// 5. 뷰포트 설정
mViewport = viewport;
mContext->RSSetViewports(1, &mViewport);
}
C++
복사
윈도우 메시지 처리
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_SIZE:
{
// 최소화 상태는 무시
if (wParam == SIZE_MINIMIZED)
break;
// 크기 조정
application.ResizeGraphicDevice();
break;
}
case WM_EXITSIZEMOVE:
{
// 크기 조정 완료 시 최종 업데이트
application.ResizeGraphicDevice();
break;
}
// ... 기타 메시지 처리
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
C++
복사
WM_SIZE vs WM_EXITSIZEMOVE
•
WM_SIZE: 크기가 변경될 때마다 발생 (드래그 중 계속 발생)
•
WM_EXITSIZEMOVE: 크기 조정이 완료된 후 한 번만 발생
성능을 위해 WM_EXITSIZEMOVE에서만 처리하거나, WM_SIZE에서는 플래그만 설정하고 다음 프레임에서 처리하는 방식을 사용할 수 있습니다.
// 성능 최적화 버전
bool resizePending = false;
case WM_SIZE:
resizePending = true;
break;
// 메인 루프에서
if (resizePending)
{
application.ResizeGraphicDevice();
resizePending = false;
}
C++
복사
Scene View ImGui 통합
ImGui를 사용하여 Scene View를 에디터 창으로 구현합니다.
Scene View 렌더링 전체 흐름
void EditorLayer::OnImGuiRender()
{
// 1. 스타일 설정 (패딩 제거)
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ 0, 0 });
// 2. Scene View 창 시작
ImGui::Begin("Scene");
// 3. 뷰포트 좌표 및 크기 계산
CalculateViewportBounds();
// 4. 뷰포트 상태 확인
UpdateViewportState();
// 5. 렌더 타겟 크기 조정
ResizeViewportIfNeeded();
// 6. 씬을 렌더 타겟에 렌더링
RenderSceneToTarget();
// 7. 렌더 타겟 텍스처를 ImGui에 표시
DisplayRenderTarget();
// 8. 드래그 앤 드롭 처리
HandleDragAndDrop();
// 9. 마우스 픽킹 처리
HandleMousePicking();
// 10. 기즈모 렌더링
RenderGizmo();
// 11. Scene View 창 종료
ImGui::End();
// 12. 스타일 복구
ImGui::PopStyleVar();
}
C++
복사
뷰포트 좌표 계산
void EditorLayer::CalculateViewportBounds()
{
// 창 내부의 콘텐츠 영역 가져오기
ImVec2 viewportMinRegion = ImGui::GetWindowContentRegionMin();
ImVec2 viewportMaxRegion = ImGui::GetWindowContentRegionMax();
// 창의 화면 상 위치
ImVec2 viewportOffset = ImGui::GetWindowPos();
// 절대 좌표 계산
mViewportBounds[0] = {
viewportMinRegion.x + viewportOffset.x,
viewportMinRegion.y + viewportOffset.y
};
mViewportBounds[1] = {
viewportMaxRegion.x + viewportOffset.x,
viewportMaxRegion.y + viewportOffset.y
};
}
C++
복사
좌표 시스템
뷰포트 상태 업데이트
void EditorLayer::UpdateViewportState()
{
// 포커스 확인: 키보드 입력 받을 수 있는지
mViewportFocused = ImGui::IsWindowFocused();
// 호버 확인: 마우스가 창 위에 있는지
mViewportHovered = ImGui::IsWindowHovered();
// 입력 블로킹 설정
Application::Get().GetImGuiLayer()->BlockEvents(!mViewportFocused && !mViewportHovered);
}
C++
복사
입력 블로킹의 중요성
// Scene View 창 밖에서 클릭했을 때
if (!mViewportHovered)
{
// ImGui가 이벤트를 먼저 처리
// → 다른 패널 클릭, 버튼 클릭 등
}
else
{
// Scene View에서 마우스 클릭
// → 오브젝트 선택, 카메라 이동 등
}
C++
복사
렌더 타겟 크기 조정
void EditorLayer::ResizeViewportIfNeeded()
{
// 현재 사용 가능한 콘텐츠 영역 크기
ImVec2 viewportPanelSize = ImGui::GetContentRegionAvail();
// 크기가 변경되었는지 확인
if (mViewportSize.x != viewportPanelSize.x ||
mViewportSize.y != viewportPanelSize.y)
{
// 유효한 크기인지 확인 (0은 무시)
if (viewportPanelSize.x > 0 && viewportPanelSize.y > 0)
{
mViewportSize = { viewportPanelSize.x, viewportPanelSize.y };
// 렌더 타겟 크기 조정
mFrameBuffer->Resize(
static_cast<UINT>(mViewportSize.x),
static_cast<UINT>(mViewportSize.y)
);
// 카메라 종횡비 업데이트
mEditorCamera.SetViewportSize(mViewportSize.x, mViewportSize.y);
}
}
}
C++
복사
씬 렌더링
void EditorLayer::RenderSceneToTarget()
{
// 1. 렌더 타겟 바인딩
mFrameBuffer->Bind();
// 2. 렌더 타겟 초기화
float clearColor[4] = { 0.1f, 0.1f, 0.1f, 1.0f };
mFrameBuffer->ClearAttachment(0, clearColor);
// 오브젝트 ID 버퍼 초기화 (-1로)
int clearID = -1;
mFrameBuffer->ClearAttachment(1, &clearID);
// 3. 카메라 설정
mEditorCamera.OnUpdate(deltaTime);
// 4. 씬 렌더링
Renderer::BeginScene(mEditorCamera);
// 모든 엔티티 렌더링
auto view = mActiveScene->GetAllEntitiesWith<TransformComponent, MeshComponent>();
for (auto entity : view)
{
auto [transform, mesh] = view.get<TransformComponent, MeshComponent>(entity);
// 오브젝트 ID를 셰이더에 전달
Renderer::SetEntityID(static_cast<int>(entity));
// 메쉬 렌더링
Renderer::DrawMesh(transform.GetTransform(), mesh);
}
Renderer::EndScene();
// 5. 렌더 타겟 언바인딩
mFrameBuffer->Unbind();
}
C++
복사
렌더 타겟 텍스처 표시
void EditorLayer::DisplayRenderTarget()
{
// 색상 첨부물 가져오기
Texture* colorTexture = mFrameBuffer->GetAttachmentTexture(0);
// ImGui 이미지로 표시
ImGui::Image(
(ImTextureID)colorTexture->GetSRV().Get(), // 텍스처
ImVec2{ mViewportSize.x, mViewportSize.y }, // 크기
ImVec2{ 0, 0 }, // UV 시작
ImVec2{ 1, 1 } // UV 끝
);
}
C++
복사
UV 좌표 설명
드래그 앤 드롭 처리
void EditorLayer::HandleDragAndDrop()
{
// 드롭 타겟으로 설정
if (ImGui::BeginDragDropTarget())
{
// "PROJECT_ITEM" 페이로드 수락
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("PROJECT_ITEM"))
{
// 파일 경로 가져오기
const wchar_t* path = (const wchar_t*)payload->Data;
// 파일 확장자 확인
std::wstring pathStr(path);
if (pathStr.ends_with(L".scene"))
{
// 씬 파일 열기
OpenScene(path);
}
else if (pathStr.ends_with(L".prefab"))
{
// 프리팹 인스턴스 생성
InstantiatePrefab(path);
}
else if (pathStr.ends_with(L".fbx") || pathStr.ends_with(L".obj"))
{
// 3D 모델 임포트
ImportModel(path);
}
}
ImGui::EndDragDropTarget();
}
}
C++
복사
마우스 픽킹 구현
void EditorLayer::HandleMousePicking()
{
// Scene View에 포커스되어 있고 마우스가 위에 있을 때만
if (!mViewportHovered || !ImGui::IsMouseClicked(ImGuiMouseButton_Left))
return;
// 1. 마우스 위치 가져오기
ImVec2 mousePos = ImGui::GetMousePos();
// 2. 뷰포트 상대 좌표로 변환
float mouseX = mousePos.x - mViewportBounds[0].x;
float mouseY = mousePos.y - mViewportBounds[0].y;
// 3. 범위 체크
if (mouseX >= 0 && mouseX < mViewportSize.x &&
mouseY >= 0 && mouseY < mViewportSize.y)
{
// 4. 오브젝트 ID 읽기
int entityID = mFrameBuffer->ReadPixel(1, (int)mouseX, (int)mouseY);
// 5. 엔티티 선택
if (entityID >= 0)
{
SelectEntity(entityID);
}
else
{
// 빈 공간 클릭 시 선택 해제
DeselectEntity();
}
}
}
C++
복사
픽킹 원리
성능 최적화
렌더 타겟 크기 조정 최적화
void EditorLayer::ResizeViewportIfNeeded()
{
ImVec2 viewportPanelSize = ImGui::GetContentRegionAvail();
// 작은 변화는 무시 (1픽셀 차이 무시)
float widthDiff = abs(mViewportSize.x - viewportPanelSize.x);
float heightDiff = abs(mViewportSize.y - viewportPanelSize.y);
if (widthDiff < 1.0f && heightDiff < 1.0f)
return;
// 일정 크기 이상만 처리
if (viewportPanelSize.x < 32 || viewportPanelSize.y < 32)
return;
// 크기 조정
mViewportSize = { viewportPanelSize.x, viewportPanelSize.y };
mFrameBuffer->Resize(
static_cast<UINT>(mViewportSize.x),
static_cast<UINT>(mViewportSize.y)
);
}
C++
복사
렌더링 스킵
void EditorLayer::OnUpdate(float deltaTime)
{
// Scene View가 보이지 않으면 렌더링 스킵
if (!mSceneViewVisible || mViewportSize.x == 0 || mViewportSize.y == 0)
return;
// 씬 렌더링...
}
C++
복사
픽셀 읽기 최적화
// 매 프레임 읽지 않고 마우스 클릭 시에만 읽기
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
{
int entityID = mFrameBuffer->ReadPixel(1, mouseX, mouseY);
// ...
}
// 또는 비동기로 읽기 (다음 프레임에서 결과 받기)
RequestPixelRead(1, mouseX, mouseY, [this](int entityID) {
SelectEntity(entityID);
});
C++
복사
디버깅 팁
렌더 타겟 시각화
void EditorLayer::DebugRenderTargets()
{
ImGui::Begin("Debug Render Targets");
// 각 첨부물 표시
for (int i = 0; i < mFrameBuffer->GetAttachmentCount(); i++)
{
Texture* texture = mFrameBuffer->GetAttachmentTexture(i);
ImGui::Text("Attachment %d", i);
ImGui::Image(
(ImTextureID)texture->GetSRV().Get(),
ImVec2{ 256, 256 }
);
}
ImGui::End();
}
C++
복사
뷰포트 정보 표시
void EditorLayer::DisplayViewportInfo()
{
ImGui::Begin("Viewport Info");
ImGui::Text("Size: %.0f x %.0f", mViewportSize.x, mViewportSize.y);
ImGui::Text("Bounds: (%.0f, %.0f) to (%.0f, %.0f)",
mViewportBounds[0].x, mViewportBounds[0].y,
mViewportBounds[1].x, mViewportBounds[1].y);
ImGui::Text("Focused: %s", mViewportFocused ? "Yes" : "No");
ImGui::Text("Hovered: %s", mViewportHovered ? "Yes" : "No");
ImGui::End();
}
C++
복사
요약
Scene View 시스템은 다음과 같은 핵심 구성 요소로 이루어집니다.
구성 요소 | 역할 |
RenderTarget | 오프스크린 렌더링을 위한 텍스처와 뷰를 캡슐화합니다. 다중 첨부물을 지원하여 Deferred Rendering과 고급 효과를 구현할 수 있습니다. |
크기 조정 시스템 | 윈도우나 에디터 패널 크기가 변경될 때 렌더 타겟, 뷰포트, 스왑체인을 동기화합니다. WM_SIZE 메시지를 처리하여 동적 크기 조정을 지원합니다. |
ImGui 통합 | 렌더 타겟의 텍스처를 ImGui 창 내부에 표시합니다. 뷰포트 좌표를 계산하고 포커스 상태를 관리하여 올바른 입력 처리를 보장합니다. |
드래그 앤 드롭 | 프로젝트 브라우저에서 에셋을 Scene View로 드래그하여 씬에 추가할 수 있습니다. 파일 형식에 따라 적절한 처리를 수행합니다. |
마우스 픽킹 | 별도의 렌더 타겟에 오브젝트 ID를 렌더링하고, 마우스 클릭 시 픽셀 값을 읽어 오브젝트를 선택합니다. |
이 시스템은 Unity와 Unreal Engine의 에디터와 유사한 작업 환경을 제공하며, 확장 가능한 구조로 설계되어 새로운 기능을 쉽게 추가할 수 있습니다.
