dev
SR Team Project(모작)


Software Rendering Team Project

모작 대상: Project Warlock

bookmark

작업 인원: 4명

작업 기간: 2025-02-27 ~ 2025-04-17

기술 스택

  1. C++
  2. DirectX9(DX9)

사용한 협업 툴

  1. Notion
  2. GitHub

외부 라이브러리

  1. FMOD Studio
  2. IMGUI
  3. ImGizmo
  4. json

맡은 역할

  1. 렌더링 + 쉐이더
  2. 이펙트, 파티클
  3. 사운드

개요

4명의 인원으로 2025-02-27부터 2025-04-17까지 진행한 팀 프로젝트로 상용 엔진 없이 작업했으며, 맡은 파트는 프로젝트 전반적인 렌더링 요소들과, 게임에 사용될 이펙트와 파티클을 구현하는 역할을 맡았다. 그 외로 사운드 라이브러리를 준비하고 컴포넌트를 작성했다.

프레임워크

Entity-Component-System(ECS) 에 가까운 프레임워크를 작성하여 사용했으며 필요한 시스템들을 싱글턴 으로 선언했고, Engine DLL을 따로 작성하여 두개의 프로젝트로 관리했다. 대부분의 기초 컴포넌트와 시스템들을 엔진에서 작성후 동적 라이브러리화 해서 클라이언트에서 접근 가능하게 했으며 모든 시스템을 가지고 있는 싱글턴 인스턴스 하나만 접근 가능하게 했다. 고정 렌더링 파이프 라인과 쉐이더를 혼용해서 사용했으며, 필요한 객체에 쉐이더 컴포넌트를 추가해서 쉐이더에게 렌더링 과정을 넘겼다.

렌더

렌더 대상의 그룹을 나누어서 순서를 분리해 각기 다른 렌더 상태를 이용하여 화면을 구성한다.

대략적인 그룹과 순서는 다음과 같다.

  1. Priority
  2. Non_Blend
  3. Blend
  4. UI

1. Priority

이 프로젝트에서는 하늘과 같이 항상 위치하는 배경을 그리기 위해 사용했다.

사용한 옵션은

RenderState Z Enable = false;
RenderState Z Write = false;

Z값을 사용하지 않고, 작성하지 않아서 다른 객체들의 렌더링에 영향을 주지 않는다.

2. Non_Blend

RS_LIGHTING = false;
RS_ZEnable = true;
RS_ZWrite = true;

대부분의 게임 오브젝트를 렌더링 하는데 사용한 그룹이며 알파 블렌드를 진행하지 않는 객체들의 집합이다.

빛 연산은 전반적으로 꺼두고, 필요한 객체에서 따로 켜서 사용한다.

Z값을 쓰고 읽어 들여, 객체들이 다른 객체에 가려질 수 있게 한다.

3. Blend

RS_LIGHTING = FALSE;
RS_CULLMODE = D3DCULL_NONE;
RS_ALPHATESTENABLE = TRUE;
RS_ALPHAFUNC = D3DCMP_GREATER; // 알파 값이 기준보다 크면 픽셀 렌더링
RS_ALPHAREF = 200; // 기준값 설정 (0~255)

이펙트와 파티클을 카메라에서 먼 객체부터 렌더링하기 위해 거리의 제곱을 기준으로 정렬했습니다. 위 렌더 상태 예시는 알파 값이 기준 이하인 픽셀을 버리는 알파 테스트 설정입니다. 알파 테스트는 반투명 색을 혼합하는 알파 블렌딩과 구분됩니다.

m_RenderObjects[RG_BLEND].sort([cameraTransform](CGameObject* a, CGameObject* b) -> bool {
	// 입력 유효성 검사 (선택 사항이지만 권장)
	if (!a || !b) return false; // 혹은 다른 규칙 적용 (예: nullptr을 뒤로 보내기)
	CTransform* transformA = static_cast<CTransform*>(a->Get_Component(L"Com_Transform"));
	CTransform* transformB = static_cast<CTransform*>(b->Get_Component(L"Com_Transform"));
	if (!transformA || !transformB) return false; // 위와 동일

	// 각 게임 오브젝트의 월드 위치 가져오기
	_float3 posA = transformA->Get_State(CTransform::STATE_POSITION);
	_float3 posB = transformB->Get_State(CTransform::STATE_POSITION);
	_float3 posC = cameraTransform->Get_State(CTransform::STATE_POSITION);

	// 카메라로부터 각 오브젝트까지의 거리 '제곱' 계산
	// 제곱 거리를 사용하는 이유: sqrt() 함수 호출보다 훨씬 빠르며, 크기 비교 목적으론 충분함
	// YourVectorType은 D3DXVECTOR3 또는 사용하는 벡터 라이브러리에 맞춰 변경

	_float3 CtoA = posC - posA;
	_float3 CtoB = posC - posB;

	_float distSqA = D3DXVec3LengthSq(&CtoA); // 아래 Helper 함수 참조
	_float distSqB = D3DXVec3LengthSq(&CtoB); // 아래 Helper 함수 참조

	// back-to-front 정렬: 거리 제곱이 큰 쪽(더 먼 쪽)이 먼저 오도록 함
	return distSqA > distSqB;
	// front-to-back 정렬 (가까운 순서)을 원하면: return distSqA < distSqB;
	});
}

4. UI

직교 투영으로 뷰스페이스에서 고정된 위치에 표현될 UI들을 나누기 위한 그룹이다.

RS_ALPHABLENDENABLE = TRUE;
RS_SRCBLEND = D3DBLEND_SRCALPHA;
RS_DESTBLEND = D3DBLEND_INVSRCALPHA;

쉐이더

셰이더가 필요한 객체에 셰이더 컴포넌트를 추가하고, 해당 객체의 렌더링을 셰이더에서 처리하도록 구성했습니다. 고정 기능 파이프라인과 셰이더를 함께 사용했으며, 다음 요소들을 다뤘습니다.

  1. VS(Vertex Shader): 정점 데이터를 처리하는 단계
  2. PS(Pixel Shader): 픽셀의 최종 색을 계산하는 단계
  3. Technique / Pass: 셰이더의 실행 단위와 렌더링 패스 구성
  4. Double-sided Rect: 양면 사각형 표현
  5. Fog: 안개 표현

CRenderer::Set_Lights에서 카메라와 광원의 거리 제곱을 비교해 가까운 광원부터 정렬하고, 셰이더에 보낼 광원 구조체 배열에는 최대 8개를 채웁니다. 이는 해당 프로젝트의 셰이더 전달 경로이며, DirectX9의 모든 광원 처리 경로를 동일한 방식으로 제어한 것으로 일반화하지 않았습니다. 미니맵은 당시 프로젝트 기록상 맵 오브젝트를 같은 위치 관계로 다시 그리는 멀티 패스 방식으로 표현했습니다.

이펙트와 파티클

시연 영상에서 이펙트와 파티클 확인하기

이펙트는 형태마다 각기 다른 로직을 사용하여 작성했으며 작업한 이펙트는 8개가 있다.

대부분의 이펙트는 빌보드를 적용하였다.

이펙트는 시간이 지나면 사라지게 했다.

파티클

파티클을 컴포넌트로 작성하여, 게임 오브젝트에 추가 할 수 있게 만들었다.

공통 CParticle_System은 포인트 스프라이트용 동적 정점 버퍼를 배치 단위로 채워 DrawPrimitive(D3DPT_POINTLIST)로 그립니다. 버퍼 오프셋을 순환시키면서 첫 구간에는 D3DLOCK_DISCARD, 이어 쓰는 구간에는 D3DLOCK_NOOVERWRITE를 사용했습니다. 파티클마다 별도 Draw 호출을 만드는 대신 여러 포인트를 묶어 전달하는 방식입니다.

모든 파티클이 같은 렌더 경로를 쓰는 것은 아닙니다. 탄피 시스템은 별도 Quad를 구성해 Triangle Strip으로 그리며, 업데이트에서는 중력을 적용하고 월드 위치와 바닥 높이를 비교해 낙하를 처리합니다. 따라서 전체 이펙트를 GPU 인스턴싱으로 처리했다고 설명하지 않았습니다.

  1. Snow
  2. Hit

BloodStain

맞은 적의 위치에서 바닥의 높이를 구해서 텍스쳐를 가진 이펙트를 생성했다. 약간의 범위를 지정해서 랜덤으로 오프셋을 지정해주고 변수로 넣은 만큼 반복해서 생성해줬다.

Hit

총알이 투사체가 아니기때문에, 눈으로 볼수가 없어, 벽에 맞았을 경우 맞았다고 판단된 벽 위에 이펙트를 생성하고 애니메이션을 재생했다.

Staff Bullet

스태프로 쏘는 투사체에는 잔상을 재현하기 위해 꼬리가 달려있었는데 이를 표현하기 위해서 투사체의 진행 방향의 뒷 쪽으로 항상 따라다니는 로컬 좌표의 점을 하나 찍어서 진행 방향과 업 벡터를 외적하여 양옆의 점을 구하고 이를 통하여 Quad를 그려서 표현했습니다.

  1. BulletShell
  2. Blood
  3. Gib
  4. Snow
  5. Projectile

사운드

FMOD Studio를 사용해 3D 사운드 환경을 구축했습니다.

Sound Manager가 이벤트 생성을 담당하고, Sound Source가 2D·3D 이벤트 목록을 구분해 관리하도록 구성했습니다. 3D 이벤트에는 객체의 Transform을 전달해 FMOD 공간 속성을 설정하며, Sound Listener는 청취자의 Transform을 Manager로 전달합니다. 팀원들이 FMOD 자료형을 직접 다루는 대신 사운드 컴포넌트와 이벤트 래퍼를 통해 재생·정지·파라미터를 제어하도록 했습니다.

성능 문제 해결

프레임 드랍이 발생했을 때 Visual Studio Debugger로 CPU 병목을 추적했습니다. 생성자와 소멸자가 호출되는 부분을 확인하고, 해당 처리를 참조 포인터를 사용하는 방식으로 변경해 프레임 드랍을 해결했습니다.

코드 근거와 분석 범위

GitHub 저장소의 main 커밋 a043a16 기준으로 렌더링·이펙트·사운드 담당 범위를 정리했습니다.

코드 정적 확인과 당시 회고를 구분해 작성했습니다. 성능 문제 해결 경험은 당시 기록이며, 해당 개선의 전후 수치나 동일 환경 재현 결과를 새로 측정한 것은 아닙니다.

더 해봤으면 좋을 점

  1. 인스턴싱(파티클 최적화)
  2. 쉐이더 후처리(블룸효과, Deferred Shading)