DirectX

[DirectX11] Normal의 이해와 활용

유니얼 2024. 3. 6. 00:03
728x90

DirectX 11로 게임 엔진 아키텍처 만들기

3D 그래픽에서, Normal 벡터는 단순히 한 점이 아니라, 그 점이 속한 표면이나 면이 어느 방향을 향하고 있는지를 나타내는 방향성을 가진 벡터입니다. 이러한 Normal 벡터는 3D 모델의 렌더링, 특히 조명과 음영, 그리고 물리적 상호작용을 계산하는 데 있어 필수적인 요소입니다. DirectX11을 사용하는 게임 엔진 아키텍처를 설계하면서, Normal 벡터의 이해와 적절한 활용은 사실적이고 다이나믹한 3D 환경을 구현하는 데 있어 핵심적인 역할을 합니다.

 

result

참고강의 링크:

https://www.inflearn.com/course/directx11-%EA%B2%8C%EC%9E%84%EA%B0%9C%EB%B0%9C-%EB%8F%84%EC%95%BD%EB%B0%98/dashboard

 

[게임 프로그래머 도약반] DirectX11 입문 강의 - 인프런

게임 프로그래머 공부에 있어서 필수적인 DirectX 11 지식을 초보자들의 눈높이에 맞춰 설명하는 강의입니다., [사진][사진] [사진] 게임 개발자는 Unreal, Unity만 사용할 줄 알면 되는 거 아닌가요? 엔

www.inflearn.com

Normal 벡터란?

Normal 벡터는 표면이나 경계의 방향을 나타내는 단위 벡터입니다. 3D 그래픽스에서, 각 정점(Vertex)에 대한 Normal 벡터는 그 정점이 속한 면의 방향을 가리킵니다. 이는 빛의 반사, 음영 처리 등을 계산할 때 필수적인 정보로 사용됩니다.

Normal의 중요성

  1. 조명 및 음영 처리: 라이팅 계산에서 Normal 벡터는 빛의 방향과 면의 방향 사이의 관계를 결정하는 데 사용됩니다. 이를 통해 물체의 밝은 부분과 그림자가 지는 부분을 정확히 계산할 수 있습니다.
  2. 시각적 디테일의 향상: Normal 맵핑 기술을 사용하여, 고해상도 디테일을 저해상도 모델에 적용함으로써, 성능을 크게 저하시키지 않고 시각적 디테일을 향상시킬 수 있습니다.
  3. 물리적 상호작용: 충돌 검출, 반사, 굴절 등 물리적 상호작용을 시뮬레이션할 때 Normal 벡터는 충돌하는 표면의 방향 정보를 제공합니다.

DirectX11에서의 Normal 활용

DirectX11과 같은 고급 그래픽 API를 사용하면, 셰이더 프로그래밍을 통해 Normal 데이터를 효율적으로 활용할 수 있습니다. 예를 들어, Pixel Shader에서 Normal 맵을 사용하여 표면의 디테일을 향상시키거나, Vertex Shader에서 Normal 벡터를 조정하여 특정 조명 효과를 구현할 수 있습니다.

실습: Normal 맵핑

Normal 맵핑은 표면의 작은 요철을 표현하기 위해 사용되는 기법으로, 모델에 더 많은 디테일을 추가하려 할 때 유용합니다. 다음은 DirectX11을 사용하여 Normal 맵을 적용하는 간단한 예제입니다.

Normal.fx

// 변환 행렬과 텍스처, 조명 방향을 정의합니다.
matrix World;
matrix View;
matrix Projection;
Texture2D Texture0;
float3 LightDir;

// 버텍스 쉐이더로 넘어오는 입력 데이터 구조체입니다.
// 각 버텍스의 위치, 텍스처 좌표, 법선 벡터를 포함합니다.
struct VertexInput
{
	float4 position : POSITION;
	float2 uv : TEXCOORD;
	float3 normal : NORMAL;
};

// 버텍스 쉐이더의 출력 데이터 구조체입니다.
// 처리된 버텍스 위치, 텍스처 좌표, 법선 벡터를 포함합니다.
struct VertexOutput
{
	float4 position : SV_POSITION;
	float2 uv : TEXCOORD;
	float3 normal : NORMAL;
};

// 버텍스 쉐이더 함수입니다.
VertexOutput VS(VertexInput input)
{
	VertexOutput output;
	// 입력된 버텍스 위치를 월드, 뷰, 프로젝션 행렬을 사용하여 변환합니다.
	output.position = mul(input.position, World);
	output.position = mul(output.position, View);
	output.position = mul(output.position, Projection);

	// 입력된 텍스처 좌표와 법선 벡터를 출력 데이터에 복사합니다.
	// 법선 벡터는 월드 행렬로 변환하여 방향을 조정합니다.
	output.uv = input.uv;
	output.normal = mul(input.normal, (float3x3)World);

	return output;
}

// 샘플러 상태를 정의합니다.
SamplerState Sampler0;

// 픽셀 쉐이더 함수입니다.
float4 PS(VertexOutput input) : SV_TARGET
{
	// 정규화된 법선 벡터와 조명 방향을 계산합니다.
	float3 normal = normalize(input.normal);
	float3 light = -LightDir;

	// 텍스처 샘플링 결과와 조명 방향과의 내적을 곱하여 최종 색상을 계산합니다.
	// 이는 간단한 확산 조명 효과를 생성합니다.
	return Texture0.Sample(Sampler0, input.uv) * dot(light, normal);
}

// 래스터라이저 상태를 정의합니다. 여기서는 와이어프레임 모드를 설정합니다.
RasterizerState FillModeWireFrame
{
	FillMode = Wireframe;
};

// 렌더링 기법을 정의합니다. 이 기법은 두 개의 패스를 사용합니다.
technique11 T0
{
	pass P0
	{
		// 첫 번째 패스에서는 버텍스 쉐이더와 픽셀 쉐이더를 설정합니다.
		SetVertexShader(CompileShader(vs_5_0, VS()));
		SetPixelShader(CompileShader(ps_5_0, PS()));
	}

	pass P1
	{
		// 두 번째 패스에서는 래스터라이저 상태를 와이어프레임 모드로 설정하고, 
		// 동일한 버텍스 쉐이더와 픽셀 쉐이더를 재사용합니다.
		SetRasterizerState(FillModeWireFrame);

		SetVertexShader(CompileShader(vs_5_0, VS()));
		SetPixelShader(CompileShader(ps_5_0, PS()));
	}
};

프로젝트 호출

NormalDemo.h

#pragma once
#include "IExecute.h"
#include "Geometry.h"

class NormalDemo : public IExecute
{
public:
	void Init() override;
	void Update() override;
	void Render() override;

	shared_ptr<Shader> _shader;

	// Object
	shared_ptr<Geometry<VertexTextureNormalData>> _geometry;
	shared_ptr<VertexBuffer> _vertexBuffer;
	shared_ptr<IndexBuffer> _indexBuffer;
	Matrix _world = Matrix::Identity;

	// Camera
	shared_ptr<GameObject> _camera;
	shared_ptr<Texture> _texture;

	Vec3 _lightDir = Vec3(0.f, -1.f, 0.f);
};

NormalDemo.cpp

#include "pch.h" // 프리컴파일 헤더 파일
#include "08. NormalDemo.h" // 이 클래스의 헤더 파일
#include "GeometryHelper.h" // 지오메트리 생성 도우미 함수가 있는 헤더 파일
#include "Camera.h" // 카메라 클래스 헤더 파일
#include "GameObject.h" // 게임 오브젝트 관리를 위한 클래스 헤더 파일
#include "CameraScript.h" // 카메라 스크립트(카메라 제어) 헤더 파일

void NormalDemo::Init()
{
	// 쉐이더 로드
	_shader = make_shared<Shader>(L"07. Normal.fx");

	// 지오메트리 생성: 여기서는 구(Sphere)를 생성함
	_geometry = make_shared<Geometry<VertexTextureNormalData>>();
	GeometryHelper::CreateSphere(_geometry); // 구를 생성하는 도우미 함수 호출

	// 버텍스 버퍼 생성 및 지오메트리의 버텍스 데이터로 초기화
	_vertexBuffer = make_shared<VertexBuffer>();
	_vertexBuffer->Create(_geometry->GetVertices());

	// 인덱스 버퍼 생성 및 지오메트리의 인덱스 데이터로 초기화
	_indexBuffer = make_shared<IndexBuffer>();
	_indexBuffer->Create(_geometry->GetIndices());

	// 카메라 설정
	_camera = make_shared<GameObject>(); // 카메라를 위한 게임 오브젝트 생성
	_camera->GetOrAddTransform(); // 변환 컴포넌트 추가 혹은 가져오기
	_camera->AddComponent(make_shared<Camera>()); // 카메라 컴포넌트 추가
	_camera->AddComponent(make_shared<CameraScript>()); // 카메라 스크립트(제어) 추가

	// 텍스처 로드
	_texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
}

void NormalDemo::Update()
{
	_camera->Update(); // 카메라 업데이트(예: 위치, 회전 등의 변화 반영)
}

void NormalDemo::Render()
{
	// 쉐이더에 행렬 및 텍스처 리소스 바인딩
	_shader->GetMatrix("World")->SetMatrix((float*)&_world);
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());
	_shader->GetVector("LightDir")->SetFloatVector((float*)&_lightDir); // 조명 방향 설정

	// 버텍스 및 인덱스 버퍼를 입력 어셈블러 스테이지에 바인딩
	uint32 stride = _vertexBuffer->GetStride();
	uint32 offset = _vertexBuffer->GetOffset();
	DC->IASetVertexBuffers(0, 1, _vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
	DC->IASetIndexBuffer(_indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

	// 쉐이더를 사용하여 지오메트리 렌더링
	_shader->DrawIndexed(0, 0, _indexBuffer->GetCount(), 0, 0);
}

결론

Normal 벡터는 3D 그래픽스에서 모델의 형태와 조명을 정확하게 표현하는 데 필수적인 요소입니다. Normal 정보를 활용하여 보다 사실적이고 디테일한 3D 씬을 구현할 수 있습니다. Normal 맵핑과 같은 기술을 통해 성능 저하 없이 시각적 품질을 크게 향상시킬 수 있으므로, 이러한 기법을 적극 활용하는 것이 좋습니다.

반응형