Company
교육 철학

Material(메테리얼), InputLayout(정점 데이타 정보)

개요

Material(머티리얼)Input Layout(입력 레이아웃)은 DirectX 11 렌더링 파이프라인에서 각각 독립적이지만 상호보완적인 역할을 수행합니다. Material은 오브젝트의 시각적 외관을 정의하고, Input Layout은 GPU가 정점 데이터를 올바르게 해석하도록 돕습니다.

Part 1: Material (머티리얼)

Material의 개념

Material(머티리얼)이란 메시에 적용하여 씬의 시각적인 모양을 조절할 수 있는 애셋입니다. 넓게 보면, 머티리얼이란 오브젝트에 적용할 수 있는 "페인트"라 보면 됩니다. 하지만 그것도 약간 오해의 소지가 있는데, 머티리얼이란 말 그대로 오브젝트가 어떤 재질로 만들어졌는지를 정의하는 것이기 때문입니다. 그 색이나 광택, 투과성 등을 정의할 수 있습니다.
보다 기술적인 용어로 풀자면, 씬에서의 빛이 표면에 닿으면, 머티리얼을 사용하여 그 빛과 표면의 상호작용을 계산합니다. 이러한 계산은 여러가지 이미지(텍스처)에서 온 데이터를 수학 표현식과 아울러 머티리얼 자체에 상속된 여러가지 프로퍼티 세팅을 통해 이루어집니다.

Material이 정의하는 것들

시각적 속성
색상(Color): 오브젝트의 기본 색상
광택(Glossiness): 표면이 얼마나 반짝이는지
거칠기(Roughness): 표면의 미세한 질감
투과성(Transparency): 빛이 얼마나 통과하는지
반사율(Reflectivity): 주변 환경을 얼마나 반사하는지
물리적 속성
금속성(Metallic): 금속인지 비금속인지
굴절률(Index of Refraction): 투명 재질의 빛 굴절 정도
자체 발광(Emission): 스스로 빛을 내는지
렌더링 동작
Shader: 어떤 셰이더를 사용할지
Rendering Mode: 불투명, 투명, 가산 블렌딩 등
Culling Mode: 앞면/뒷면 컬링
Depth Testing: 깊이 테스트 방식

Material 클래스 구조

#pragma once #include "yaResource.h" #include "yaTexture.h" #include "yaShader.h" namespace ya { class Material : public Resource { public: struct Data { std::wstring albedo; //diffuse }; Material(); virtual ~Material(); virtual HRESULT Save(const std::wstring& path) override; virtual HRESULT Load(const std::wstring& path) override; void Bind(); void SetShader(graphics::Shader* shader) { mShader = shader; } private: graphics::eRenderingMode mMode; Material::Data mData; //Texture* mTexture; graphics::Shader* mShader; }; }
C++
복사

클래스 구조 분석

Resource 상속
Material은 Resource 클래스를 상속받아 리소스 관리 시스템에 통합됩니다
Save()Load() 메서드를 통해 파일로 저장/로드 가능
리소스 매니저를 통한 중앙 집중식 관리
Material::Data 구조체
Material의 실제 데이터를 담는 구조체
현재는 albedo (Diffuse 텍스처 경로)만 포함
향후 Normal Map, Roughness Map 등 추가 가능
Rendering Mode
mMode: 렌더링 방식을 결정하는 열거형
불투명(Opaque), 투명(Transparent), 가산(Additive) 등
블렌딩 및 깊이 테스트 설정에 영향
Shader 참조
mShader: 이 Material이 사용할 셰이더
Material마다 다른 셰이더 사용 가능
PBR Shader, Toon Shader, Unlit Shader 등

Material의 역할

렌더링 파이프라인 설정
1.
Shader 바인딩: 어떤 셰이더로 렌더링할지 결정
2.
Texture 바인딩: 필요한 텍스처들을 GPU에 전달
3.
State 설정: Blend State, Depth-Stencil State 등 설정
4.
Constant Buffer: Material 파라미터를 상수 버퍼로 전달
Bind() 메서드의 역할
void Material::Bind() { // 1. Shader 바인딩 if (mShader != nullptr) { mShader->Bind(); } // 2. Rendering Mode에 따른 State 설정 // (Blend State, Depth-Stencil State 등) // 3. Texture 바인딩 // (Albedo, Normal, Roughness 등) // 4. Material 파라미터를 상수 버퍼로 전달 }
C++
복사

Material 사용 예시

Material 생성 및 설정
// Material 생성 Material* woodMaterial = new Material(); // Shader 설정 Shader* basicShader = Resources::Find<Shader>(L"BasicShader"); woodMaterial->SetShader(basicShader); // 리소스 매니저에 등록 Resources::Insert(L"WoodMaterial", woodMaterial);
C++
복사
Material 사용
// 렌더링 시 woodMaterial->Bind(); // Material 설정 적용 mesh->Render(); // 메시 렌더링
C++
복사

Part 2: Input Layout (입력 레이아웃)

Input Assembler Stage

입력 어셈블러 스테이지(Input Assembler, 이하 IA)는 렌더링 파이프라인의 제일 처음 스테이지이자 애플리케이션으로부터 넘겨받은 버텍스 버퍼나 인덱스 버퍼의 데이터를 읽어들여 [점], [선], [삼각형] 등의 프리미티브를 조합하여 파이프라인의 다음 스테이지로 데이터를 흘려보내는 역할을 합니다.
그 밖에 셰이더에서의 처리에 필요한 [시스템 생성값]을 추가할 수도 있습니다.
시스템 생성값은 다른 입출력 Element와 마찬가지로 [시멘틱스]를 붙여 식별하며, 입력 어셈블러가 생성하는 시스템 생성값에는 [버텍스 ID], [프리미티브 ID], [인스턴스 ID] 등이 있습니다.
위 그림은 입력 어셈블러 스테이지에서의 데이터들의 관계도를 나타냅니다.

Input Assembler의 주요 역할

데이터 읽기
Vertex Buffer: 정점 데이터 (위치, 색상, 텍스처 좌표 등)
Index Buffer: 정점 인덱스로 메모리 절약
Primitive Topology: 정점들을 어떻게 연결할지 (삼각형, 선, 점 등)
프리미티브 조립
정점들을 연결하여 기하학적 도형 생성
Triangle List: 3개 정점마다 삼각형 하나
Triangle Strip: 연속된 정점으로 삼각형 띠 생성
Line List, Point List 등
시스템 값 생성
Vertex ID (SV_VertexID): 현재 정점의 인덱스
Instance ID (SV_InstanceID): 인스턴싱 시 인스턴스 번호
Primitive ID: 프리미티브의 고유 번호

Input Layout의 필요성

문제점
DirectX의 버텍스 버퍼에는 애플리케이션 측에서 임의의 정보를 기록할 수 있습니다. 그 때문에 [어떤 버텍스 버퍼의 어느 데이터가 포함되어 있고, 셰이더에 어떤 식으로 넘길 것인가]를 IA에 알려줄 필요가 있습니다.
해결책: Input Layout
이 때문에 필요한 것이 [Input Layout]입니다. Input Layout은 다음을 정의합니다:
정점 데이터에 어떤 정보가 들어있는가? (위치, 색상, UV 등)
각 데이터의 타입과 크기는? (float3, float4, float2 등)
데이터가 메모리에서 어떻게 배치되어 있는가? (오프셋)
Shader의 어떤 입력 변수에 연결되는가? (시맨틱)

Input Layout 객체

입력 레이아웃 객체는 ID3D11InputLayout 인터페이스를 사용하며, 이 인터페이스 자체는 딱히 기능이 있는 것은 아닙니다. 단지 정점 데이터의 구조 정보를 담고 있을 뿐입니다.
입력 레이아웃 객체를 만들려면 입력 Element의 배열을 D3D11_INPUT_ELEMENT_DESC 구조체의 배열 형태로 준비합니다.

D3D11_INPUT_ELEMENT_DESC 구조체

typedef struct D3D11_INPUT_ELEMENT_DESC { LPCSTR SemanticName; // 시맨틱 이름 ("POSITION", "COLOR" 등) UINT SemanticIndex; // 시맨틱 인덱스 (0, 1, 2...) DXGI_FORMAT Format; // 데이터 포맷 (float3, float4 등) UINT InputSlot; // 입력 슬롯 번호 (0~15) UINT AlignedByteOffset; // 정점 구조체 내 오프셋 D3D11_INPUT_CLASSIFICATION InputSlotClass; // 데이터 분류 UINT InstanceDataStepRate; // 인스턴싱 스텝 레이트 } D3D11_INPUT_ELEMENT_DESC;
C++
복사
각 필드의 의미
SemanticName
HLSL Shader의 입력 변수 시맨틱과 매칭
"POSITION", "COLOR", "TEXCOORD", "NORMAL" 등
대소문자 구분하지 않음
SemanticIndex
같은 시맨틱이 여러 개일 때 구분용
TEXCOORD0, TEXCOORD1처럼 인덱스로 구분
하나만 있으면 0
Format
데이터의 타입과 크기를 정의
DXGI_FORMAT_R32G32B32_FLOAT: float3 (12 bytes)
DXGI_FORMAT_R32G32B32A32_FLOAT: float4 (16 bytes)
DXGI_FORMAT_R32G32_FLOAT: float2 (8 bytes)
InputSlot
정점 버퍼가 바인딩될 슬롯 번호 (0~15)
여러 버퍼를 동시에 사용 가능
일반적으로 0번 슬롯 사용
AlignedByteOffset
정점 구조체 시작부터의 바이트 오프셋
메모리 상에서의 위치를 나타냄
예: Position(12 bytes) 다음에 Color가 있으면 오프셋 12
InputSlotClass
D3D11_INPUT_PER_VERTEX_DATA: 정점마다 데이터 (일반적)
D3D11_INPUT_PER_INSTANCE_DATA: 인스턴스마다 데이터
InstanceDataStepRate
인스턴싱 사용 시 설정
일반 렌더링에서는 0

Input Layout 예제 1: Position + Color

아래는 버텍스 데이터 1개에 대한 정의를 선언하는 코드입니다.
D3D11_INPUT_ELEMENT_DESC inputLayoutDesces[2] = {}; inputLayoutDesces[0].AlignedByteOffset = 0; inputLayoutDesces[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; inputLayoutDesces[0].InputSlot = 0; inputLayoutDesces[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; inputLayoutDesces[0].SemanticName = "POSITION"; inputLayoutDesces[0].SemanticIndex = 0; inputLayoutDesces[1].AlignedByteOffset = 12; inputLayoutDesces[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; inputLayoutDesces[1].InputSlot = 0; inputLayoutDesces[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; inputLayoutDesces[1].SemanticName = "COLOR"; inputLayoutDesces[1].SemanticIndex = 0; graphics::Shader* triangleShader = Resources::Find<graphics::Shader>(L"TriangleShader"); mesh->SetVertexBufferParams(2, inputLayoutDesces, triangleShader->GetVSBlob()->GetBufferPointer(), triangleShader->GetVSBlob()->GetBufferSize());
C++
복사
이 코드의 의미
정점 구조
struct Vertex { float3 position; // 12 bytes (offset 0) float4 color; // 16 bytes (offset 12) // 총 28 bytes };
C++
복사
Element 0: POSITION
오프셋 0에서 시작
float3 타입 (R32G32B32_FLOAT)
POSITION 시맨틱으로 Shader에 전달
Element 1: COLOR
오프셋 12에서 시작 (Position 다음)
float4 타입 (R32G32B32A32_FLOAT)
COLOR 시맨틱으로 Shader에 전달

Input Layout 예제 2: Position + Color + UV

D3D11_INPUT_ELEMENT_DESC inputLayoutDesces[3] = {}; inputLayoutDesces[0].AlignedByteOffset = 0; inputLayoutDesces[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; inputLayoutDesces[0].InputSlot = 0; inputLayoutDesces[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; inputLayoutDesces[0].SemanticName = "POSITION"; inputLayoutDesces[0].SemanticIndex = 0; inputLayoutDesces[1].AlignedByteOffset = 12; inputLayoutDesces[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; inputLayoutDesces[1].InputSlot = 0; inputLayoutDesces[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; inputLayoutDesces[1].SemanticName = "COLOR"; inputLayoutDesces[1].SemanticIndex = 0; inputLayoutDesces[2].AlignedByteOffset = 28; inputLayoutDesces[2].Format = DXGI_FORMAT_R32G32_FLOAT; inputLayoutDesces[2].InputSlot = 0; inputLayoutDesces[2].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; inputLayoutDesces[2].SemanticName = "TEXCOORD"; inputLayoutDesces[2].SemanticIndex = 0; graphics::Shader* spriteShader = Resources::Find<graphics::Shader>(L"SpriteShader"); mesh->SetVertexBufferParams(3, inputLayoutDesces, spriteShader->GetVSBlob()->GetBufferPointer(), spriteShader->GetVSBlob()->GetBufferSize());
C++
복사
이 코드의 의미
정점 구조
struct VertexColorUV { float3 position; // 12 bytes (offset 0) float4 color; // 16 bytes (offset 12) float2 uv; // 8 bytes (offset 28) // 총 36 bytes };
C++
복사
Element 2: TEXCOORD
오프셋 28에서 시작 (Position + Color 다음)
float2 타입 (R32G32_FLOAT)
TEXCOORD 시맨틱으로 Shader에 전달
텍스처 샘플링을 위한 UV 좌표

시맨틱과 포맷 설명

SemanticName
해당 HLSL에서 시맨틱스 이름이며, 같은 시맨틱스 명이 여러 개 있을 경우에는 다음 인자값(인덱스 값)으로 구별을 하며 필요 없을 때는 0을 설정합니다.
Format
포맷은 DXGI_FORMAT enum형을 사용하며, 데이터의 타입과 크기를 정의합니다.
일반적인 Format 종류
DXGI_FORMAT_R32G32B32_FLOAT: 3D 벡터 (float3)
DXGI_FORMAT_R32G32B32A32_FLOAT: 4D 벡터 (float4)
DXGI_FORMAT_R32G32_FLOAT: 2D 벡터 (float2)
DXGI_FORMAT_R8G8B8A8_UNORM: 정규화된 바이트 4개 (색상)
데이터 위치 지정
데이터 위치로써 데이터가 있는 버텍스 버퍼를 등록할 입력 슬롯 번호와 버텍스 버퍼 내에 있는 버텍스 데이터의 처음에서 데이터까지의 오프셋 값을 지정합니다.
오프셋 계산 예시

InputSlotClass와 인스턴싱

각 Element의 입력 슬롯 번호와 오프셋은 다음과 같이 됩니다.
그 다음 버텍스 버퍼에 넘겨진 3D 오브젝트를 단순히 한 번 렌더링할 경우에는 D3D11_INPUT_PER_VERTEX_DATA와 0을 인자값으로 주면 되며, 인스턴싱을 할 경우에는 D3D11_INPUT_PER_INSTANCE_DATA와 그릴 횟수를 설정합니다. (추후 다시 설명)
일반 렌더링
inputLayoutDesces[i].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; inputLayoutDesces[i].InstanceDataStepRate = 0;
C++
복사
인스턴싱
inputLayoutDesces[i].InputSlotClass = D3D11_INPUT_PER_INSTANCE_DATA; inputLayoutDesces[i].InstanceDataStepRate = 1; // 인스턴스마다 한 번
C++
복사

Part 3: Input Layout 생성

InputLayout 클래스의 CreateInputLayout 메서드

입력 레이아웃 오브젝트 생성:
void InputLayout::CreateInputLayout(UINT vertexCount, D3D11_INPUT_ELEMENT_DESC* layout , const void* pShaderBytecodeWithInputSignature, SIZE_T BytecodeLength) { if (!(GetDevice()->CreateInputLayout(layout, vertexCount , pShaderBytecodeWithInputSignature , BytecodeLength , mInputLayout.GetAddressOf()))) assert(NULL && "Create input layout failed!"); }
C++
복사
위의 설정을 가지고 ID3D11Device::CreateInputLayout 함수를 호출하여 입력 레이아웃 오브젝트를 생성합니다.

성능 최적화와 검증

DirectX 11에서는 성능을 향상시키기 위해 파이프라인의 설정에 관한 타당성 검증을 렌더링 시가 아닌 가능한 한 오브젝트를 생성할 때에 수행합니다. 이것이 IA 레이아웃이 필요한 이유입니다.
렌더링 시점 검증의 문제점
매 프레임마다 검증하면 성능 저하
실시간 렌더링에서는 치명적
생성 시점 검증의 장점
초기화 때 한 번만 검증
런타임 성능 향상
에러를 미리 발견 가능

Input Signature의 필요성

렌더링 파이프라인이 올바르게 동작하려면 [IA에 입력될 데이터의 구조]와 [버텍스 셰이더 함수에의 입력 데이터의 구조]가 일치해야 합니다.
대응하는 HLSL Vertex Shader
Input Layout과 Shader의 대응

Input Signature 획득

그 때문에 입력 레이아웃 오브젝트를 생성할 때 위의 2개가 일치하고 있는지를 확인하기 위한 버텍스 셰이더의 [입력 시그니처]가 필요하게 됩니다.
버텍스 셰이더의 [입력 시그니처]는 컴파일된 버텍스 셰이더의 바이트코드에 포함되어 있기에 입력 레이아웃 오브젝트 생성 시에는 버텍스 셰이더의 바이트 코드가 필요합니다.
바이트코드 획득
바이트코드는 지난 포스팅에서 다루었던 셰이더 오브젝트를 만들 때의 얻을 수 있는 ID3DBlob 인터페이스가 가지고 있습니다. 우리 엔진에서는 해당 바이트코드는 셰이더 클래스가 들고 있습니다.
graphics::Shader* triangleShader = Resources::Find<graphics::Shader>(L"TriangleShader"); mesh->SetVertexBufferParams(2, inputLayoutDesces, triangleShader->GetVSBlob()->GetBufferPointer(), triangleShader->GetVSBlob()->GetBufferSize());
C++
복사
코드 분석
1.
Resources::Find(): 리소스 매니저에서 Shader 가져오기
2.
GetVSBlob(): Vertex Shader 바이트코드 Blob 획득
3.
GetBufferPointer(): 바이트코드 메모리 포인터
4.
GetBufferSize(): 바이트코드 크기
5.
SetVertexBufferParams(): Input Layout 생성 및 설정

Part 4: Material과 Input Layout의 관계

렌더링 파이프라인에서의 역할

Material의 역할
무엇을 어떻게 그릴 것인가 (What & How)
Shader, Texture, State 등을 설정
시각적 결과물을 결정
Input Layout의 역할
데이터를 어떻게 해석할 것인가 (Data Structure)
정점 버퍼의 구조를 GPU에 알림
Shader가 데이터를 올바르게 받도록 보장

통합 렌더링 흐름

코드 예시
void GameObject::Render() { // 1. Transform 설정 UpdateTransform(); // 2. Input Layout 바인딩 mesh->BindInputLayout(); // 3. Vertex/Index Buffer 바인딩 mesh->BindBuffers(); // 4. Material 바인딩 material->Bind(); // 5. Draw Call mesh->Render(); }
C++
복사

Material과 Shader의 호환성

중요한 점
Material이 사용하는 Shader는 특정 Input Layout을 요구합니다. 따라서:
Shader의 입력 구조 (VS_Input)
Input Layout의 정의 (D3D11_INPUT_ELEMENT_DESC)
정점 버퍼의 실제 데이터 구조
이 세 가지가 모두 일치해야 올바른 렌더링이 가능합니다.
불일치 시 발생하는 문제
Input Layout 생성 실패
데이터 왜곡 (잘못된 해석)
런타임 크래시
이상한 시각적 결과

결론

핵심 요약

Material
오브젝트의 시각적 외관을 정의
Shader, Texture, Rendering Mode 등을 포함
빛과의 상호작용 방식을 결정
Bind() 메서드로 렌더링 파이프라인에 적용
Input Layout
정점 데이터의 구조를 GPU에 알림
Vertex Shader 입력과 정점 버퍼를 연결
생성 시점에 유효성 검증으로 성능 향상
Input Signature를 통한 Shader와의 호환성 보장

실전 적용

설계 시 고려사항
1.
Material과 Shader의 일대일 또는 일대다 관계 설계
2.
Input Layout은 Shader와 정점 구조에 의존
3.
리소스 매니저를 통한 효율적인 관리
4.
검증을 초기화 시점에 수행하여 성능 확보
디버깅 팁
Input Layout 생성 실패 시 Shader와 정점 구조 확인
Material 바인딩 후 렌더링 결과 이상 시 Texture와 State 점검
성능 문제 시 불필요한 State 변경 최소화
Material과 Input Layout은 각각 독립적이지만, 함께 작동하여 3D 오브젝트가 화면에 올바르게 렌더링되도록 합니다. 이 두 개념의 깊은 이해는 고급 렌더링 기법 구현과 성능 최적화의 핵심입니다.