HLSL - 개요
파이프라인과 쉐이더
DirectX의 그래픽 파이프라인에서
많은 스테이지는 고정적이다
이는 GPU 드라이버나 하드웨어가
‘정해진 방식’대로 동작하며 프로그래머도 이를 따르기에
자체적인 알고리즘을 바꾸기 보단, 주어지는 State 설정 구조체를 통해
설정값을 조정한다
그렇기에 프로그래머는
Shader Stage에서 쉐이더 프로그래밍을 통해
작성한 ‘사용자 정의 프로그램’을 실행시켜
그래픽 파이프라인에 개입할 수 있다
HLSL(High-Level Shading Language)?
DirectX용을 마이크로스프트가 만든 GPU 프로그래밍 언어
(C언어와 비슷)
(Windows, Xbox에서 사용)
다양한 Shader Stage에서 실행되는 프로그램을 작성 가능
ex)
- Vertex Shader : 정점을 이동시키거나 애니메이션의 변형
- Pixel Shader : 픽셀 색을 결정하거나 라이팅/텍스쳐 샘플링
-
Compute Shader : 파티클 시뮬레이션, 물리 계산
-
- GLSL
- OpenGL/ Vulkan 계열
- GLSL
-
- Metal Shading Language(MSL)
- MacOS / iOS 계열
- Metal Shading Language(MSL)
HLSL의 문법들
데이터 타입
float2,3,4 는 vector2,3,4 와 동일하다 생각하자
float 4x4 은 matrix와 연동된다
(matrix가 의도된거라면 그냥 통일해주는게 더 좋긴하다)
타입 | 의미 | 예시 |
---|---|---|
float , int , uint , bool |
스칼라(단일 값) | float a = 1.0; |
float2 , float3 , float4 |
벡터 타입 (2,3,4차원) | float3 pos = float3(1,2,3); |
floatNxM |
행렬 (N×M, 열 우선 저장) | float4x4 mat; |
half , double |
정밀도 다른 실수형 (제한적 사용) | half x; double y; |
Texture2D , SamplerState |
GPU 리소스 핸들 | Texture2D tex; |
StructuredBuffer<T> |
구조체 배열 버퍼 | StructuredBuffer<float4> buf; |
-
- Swizzling
- 벡터의 요소를 재배치하거나 선택하여 새로운 벡터나 스칼라를 만드는 기능
- Swizzling
float4 pos = float4(0,0,2,1);
float2 f_2D;
f_2D = pos.xy; // f_2D = 0,0 이렇게 두 요소를 읽을 수 있음
f_2D = pos.zz; // f_2D = 1,1 중복도 가능함
반대로
float4 f_4D;
f_4D.xx = pos.xy; // 이렇게는 안됨
Swizzle 이름 체계
그룹 | 요소 이름 | 주 용도 |
---|---|---|
좌표 | .x .y .z .w |
일반 좌표계 |
색상 | .r .g .b .a |
색상 채널 |
텍스처 좌표 | .s .t .p .q |
UV/3D 텍스처 좌표 |
인덱스 | [0] [1] … |
배열 접근식 |
상수 버퍼
CPU -> GPU 데이터 전달을 위한 저장소
(16Byte 단위 패킹 규칙이 존재)
cbuffer MyCB : register(b0)
{
float4x4 WorldViewProj; // 64 bytes
float4 Color; // 16 bytes
};
-
cbuffer 안의 변수는 16바이트 슬롯 단위 정렬할 것
(부족하면 패딩을 하거나 변수 순서를 변경) -
VSSetConstantBuffers, PSSetConstantBuffers 등으로 바인딩
(CPU 측에서 보내주는 ‘데이터 구조’가 동일해야 함)
VS.hlsl
cbuffer ModelViewProjectionConstantBuffer : register(b0) {
matrix model; // matrix 대신에 float4x4를 사용할 수도 있습니다.
matrix view;
matrix projection;
};
---
ExampleApp.h
struct ModelViewProjectionConstantBuffer {
Matrix model;
Matrix view;
Matrix projection;
};
Register (레지스터 바인딩)
리소스를 GPU 슬롯에 매핑하는 키워드
레지스터 종류 | 접두어 | 예시 |
---|---|---|
Constant Buffer | b# |
register(b0) |
Texture (SRV) | t# |
register(t0) |
Sampler | s# |
register(s0) |
UAV | u# |
register(u0) |
cbuffer Matrices : register(b0) { float4x4 World; };
Texture2D tex : register(t0);
SamplerState samp : register(s0);
Semantics (시맨틱)
입출력 변수가 파이프라인에서 어떤 역할을 GPU에게 알려주는 태그
실제 Ms 문서의 시맨틱 모음들
struct VertexShaderInput {
float3 pos : POSITION;
float3 color : COLOR0;
float2 texcoord : TEXCOORD0;
};
- pos : GPU에게 이 변수는 POSITION(정점 위치)을 의미한다고 알려준다
(보통 이러한 position들은 ‘변환’전의 값이 들어옴) - color : GPU에게 이 변수는 COLOR 중 하나(인덱스 번호 : 0)라고 알려준다
일반 시맨틱
시맨틱 | 의미 | 예시 |
---|---|---|
POSITION |
정점 위치 | float3 pos : POSITION; |
NORMAL |
법선 벡터 | float3 n : NORMAL; |
TEXCOORDn |
텍스처 좌표 | float2 uv : TEXCOORD0; |
COLORn |
색상 | float4 c : COLOR0; |
시스템 시맨틱
시맨틱 | 의미 |
---|---|
SV_POSITION |
정점 셰이더 출력 / 픽셀 셰이더 입력 위치 |
SV_Target |
픽셀 셰이더 출력 색상 |
SV_VertexID |
정점 ID |
SV_InstanceID |
인스턴스 번호 |
SV_DispatchThreadID |
컴퓨트 셰이더 스레드 ID |
- SV는 System Value의 약자
- 보통 ‘파이프라인’이 ‘알아야 할’ 시스템 값들을 설정하며
‘파이프라인’ 제어에 필수적인 데이터들을 뜻함
동작을 보장하기 위해 필요한 값으로 인식하자
(이게 안 붙은것은 그냥 사용자가 정의한 것들)
(SV_Position : 필수적인 정점 정보 데이터로
이게 없으면 ‘정점 정보’가 ‘RS’가 ‘어떤 정보’가 클립 공간 위치인지 모름)
Intrinsic Functions (내장 함수)
GPU에서 직접 지원하는 수학/벡터/텍스쳐 연산용 함수들
(성능 최적화된 연산)
수학
함수 | 설명 | 예시 |
---|---|---|
abs(x) |
절댓값 | abs(-3.0) → 3 |
sqrt(x) |
제곱근 | sqrt(9) → 3 |
rsqrt(x) |
역제곱근 | rsqrt(4) → 0.5 |
sin(x), cos(x), tan(x) |
삼각 함수 | sin(PI/2) → 1 |
pow(x,y) |
거듭제곱 | pow(2,3) → 8 |
min(a,b), max(a,b) |
최소/최대 | max(3,5) → 5 |
clamp(x,a,b) |
구간 제한 | clamp(2.5,0,1) → 1 |
lerp(a,b,t) |
선형 보간 | lerp(0,10,0.3) → 3 |
벡터/행렬
함수 | 설명 | 예시 |
---|---|---|
dot(a,b) |
내적 | dot(float3(1,0,0), float3(0,1,0)) = 0 |
cross(a,b) |
외적 | cross(x,y) |
length(v) |
벡터 길이 | length(float3(3,4,0)) = 5 |
normalize(v) |
단위 벡터화 | normalize(float3(3,0,0)) = (1,0,0) |
유틸리티
함수 | 설명 | 예시 |
---|---|---|
saturate(x) |
0~1로 clamp | saturate(1.5) = 1.0 |
step(edge,x) |
x<edge=0, x≥edge=1 | step(0.5,0.7)=1 |
smoothstep(a,b,x) |
부드러운 보간 | smoothstep(0,1,0.5)=0.5 |
그 외의 몇가지 규칙
Vertex Shader에 들어오는 데이터(VertexShaderInput)는
사실상 InputLayout에서 맞추는 것과 동일한 구조체를 만들어야 함
또한 VS에서 나가는 데이터는
PS에서 ‘입력’받는 타입의 데이터와 동일해야 한다
(물론 내부 값은 GPU가 NDC 좌표로 만들었지만)
-
Pixel Shader에 들어오는 녀석들은 전부 ‘픽셀’에 해당하는 존재들
따라서 PixelShader의 main이 ‘각 픽셀’에 적용된다
(vertex의 다양한 pos,color 등은 Barycentric coordinates 을 통해
삼각형 내부의 각 점들이 세 꼭짓점의 가중치 값들을 이용하여 구해진다)
(이러한 값들이 각종 변형 행렬 과, 원근 투영 을 모두 거쳐 이곳으로 들어온다) -
SV_Position이 float4이지만
Position은 float3(다만 어차피 poinr 값이기에 뒤에 1붙여도 상관이 없음) -
기본적으로 쉐이더 코드는 매우 빠른편이다
- 애초부터 Model, View, Projection을 CPU에서 곱하고 전달도 가능함
다만, CPU가 ‘딱히 할 이유’가 없음
- 정점 데이터가 많아질수록 GPU에게 보내고 CPU는 다른 일 하는게 이득
- 애초에 모든 정점이 같은 계산을 하므로 GPU의 SIMD 연산에 적합
- 각 행렬들의 값이 프레임마다 바뀌는 경우도 CPU가 다시 일해야 함
-
Normal, Light, Texture 간의 계산도 고려해야 하기에
차라리 VS에서 처리하는 것이 더 자연스러움 -
- GPU SIMD(Single Instruction, Multiple Data)?
- SIMD는 하나의 명령으로 여러 데이터를 동시에 처리하는 구조를 뜻한다
GPU는 많은 SIMD 유닛이 수십~수백 개의 정점을 동시에 계산이 가능함
(CPU보다 이러한 연산에 최적)
(그렇기에 Compute Shading(범용 계산)도 GPU에서 한 후, CPU로 다시 받음)
- GPU SIMD(Single Instruction, Multiple Data)?
- 정점 데이터가 많아질수록 GPU에게 보내고 CPU는 다른 일 하는게 이득
- 픽셀 쉐이더의 SV_Target[n]은 n번째 RenderTarget에 결과가 적용된다는 뜻
(안써있으면 0이 선택되는듯하다)
Shader 예제
주어진 예제에서
Pixel Shader를 통하여 위와 같이
특정 x 좌표를 통해 ‘빨간색’으로 바꾸는 것이다
구현 사항
- Vertex Shader에 텍스쳐 좌표 기능 추가
// 이 예제에서 사용하는 Vertex 정의 struct Vertex { Vector3 position; Vector3 color; // TODO: texture coordinates 추가 Vector2 textureCoord; };
- 픽셀 쉐이더로 보낼 ConstantBuffer 와 그 Data
// ExampleApp.h
// TODO: 픽셀 쉐이더로 보낼 ConstantBuffer
struct PixelShaderConstantBuffer
{
float xSplit;
Vector3 Buffer;
};
...
ComPtr<ID3D11Buffer> m_pixelUsedCBuffer;
PixelShaderConstantBuffer m_pixelShaderConstantBufferData;
---
// ExampleApp.cpp
MakeBox()
{
..
texcoords.push_back(Vector2(0, 0));
texcoords.push_back(Vector2(1, 0));
texcoords.push_back(Vector2(1, 1));
texcoords.push_back(Vector2(0, 1));
..
}
Initalize()
{
...
m_pixelShaderConstantBufferData.xSplit = 0.0f;
m_pixelShaderConstantBufferData.Buffer = Vector3();
// TODO: 픽셀쉐이더로 보낼 ConstantBuffer 만들기
AppBase::CreateConstantBuffer(m_pixelShaderConstantBufferData, m_pixelUsedCBuffer);
...
vector<D3D11_INPUT_ELEMENT_DESC> inputElements = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 4 * 3,
D3D11_INPUT_PER_VERTEX_DATA, 0},
// TODO: 텍스춰 좌표를 버텍스 쉐이더로 보내겠다!
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 4 * 6,
D3D11_INPUT_PER_VERTEX_DATA, 0},
};
}
Update() : AppBase::UpdateBuffer(m_pixelShaderConstantBufferData, m_pixelUsedCBuffer);
Render() : m_context->PSSetConstantBuffers(0, 1, m_pixelUsedCBuffer.GetAddressOf());
UpdateGUI() : ImGui::SliderFloat("xSplit", &m_pixelShaderConstantBufferData.xSplit, 0.0f,
1.0f);
-
Vec3 는 패딩용
-
CreateBuffer 쪽의
D3D11_BUFFER_DESC 요소 중
ByteWidth 가 16의 배수여야 한다
(그렇기에 최소값을 16으로 세팅하던가
구조체에 패딩을 줘서 16으로 맞춰야 한다)
- Vertex Shader는 텍스쳐 좌표를 넘겨주기만 한다
// Data Types (HLSL)
// https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-data-types
// Shader Constants (HLSL)
// https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-constants
// Register
// https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-variable-register
// float4, matrix
// https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-per-component-math
cbuffer ModelViewProjectionConstantBuffer : register(b0) {
matrix model; // matrix 대신에 float4x4를 사용할 수도 있습니다.
matrix view;
matrix projection;
};
// Semantics
// https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
struct VertexShaderInput {
float3 pos : POSITION;
float3 color : COLOR0;
// TODO: 텍스춰 좌표 추가!
float2 texcoord : TEXCOORD0;
};
struct PixelShaderInput {
float4 pos : SV_POSITION;
float3 color : COLOR0;
// TODO: 텍스춰 좌표 추가!
float2 texcoord : TEXCOORD0;
};
// Intrinsic Functions
// https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-intrinsic-functions
PixelShaderInput main(VertexShaderInput input) {
PixelShaderInput output;
float4 pos = float4(input.pos, 1.0f);
pos = mul(pos, model);
pos = mul(pos, view);
pos = mul(pos, projection);
output.pos = pos;
output.color = input.color;
// TODO: 텍스춰 좌표 추가!
output.texcoord = input.texcoord;
return output;
}
- Pixel Shader에 적용부분
(xSplit 과 픽셀 좌표를 비교하여 return하는 최종색상을 결정한다)
// TODO: 받아올 constant 선언
cbuffer pixelUsedConstantBuffer : register(b0)
{
float xSplit;
float3 buff;
};
struct PixelShaderInput {
float4 pos : SV_POSITION;
float3 color : COLOR;
// TODO: 버텍스 쉐이더와 맞춰주기 (텍스춰 좌표 추가)
float2 texcoord : TEXCOORD;
};
float4 main(PixelShaderInput input) : SV_TARGET {
// TODO: 텍스춰 좌표를 이용해서 색 결정
if (input.texcoord.x < xSplit)
return float4(1, 0, 0, 0);
// Use the interpolated vertex color
return float4(input.color, 1.0);
}
트러블 슈팅 - x3000 에러
ColorPixelShader.hlsl 에서 해당 에러가 발생하여
진행이 어려운 상황이 발생하였다
// TODO: 받아올 constant 선언
cbuffer pixelUsedConstantBuffer : register(b0)
{
float xSplit;
};
struct PixelShaderInput {
float4 pos : SV_POSITION;
float3 color : COLOR0;
// TODO: 버텍스 쉐이더와 맞춰주기 (텍스춰 좌표 추가)
float2 texCoord : TEXCOORD0;
};
float4 main(PixelShaderInput input) : SV_TARGET {
// TODO: 텍스춰 좌표를 이용해서 색 결정
// Use the interpolated vertex color
return float4(input.color, 1.0);
}
일반적으로 x3000 이 발생한 경우
(그것도 1행에서)
인코딩 문제가 가깝다 하였으나
나는 인코딩을 여러번 바꾸어 시도하여도 문제가 해결되지 않았다
그렇다고 주석을 지운다던가, 다양한 시도를 해보았지만 이상하게 해결되지 않았다
- 기존 저장했던 ‘서명 있는’을 ‘서명 없는’으로 바꾸었는데도 동일한 문제가 지속
알고 보니
이전에 Git에 자동저장을 위하여 만들어 놓은 인코딩이 문제였다
해당 부분에서 ‘서명 있는’으로 계속 저장하고 있었기에…
-
vs쪽도 서명 없는 UTF-8로 등록하였는데 해결할 수 있었다
-
결국 인코딩 문제가 맞았다…
댓글남기기