본문으로 바로가기
반응형

DX11 Graphics pipeline

 

MS hlsl guide  https://learn.microsoft.com/ko-kr/windows/win32/direct3dhlsl/dx-graphics-hlsl

hlsl의 정의

Microsoft가 2002년 Direct X 9.0을 공개하면서 도입된 GPU 프로그래밍 언어이며, 고급 셰이딩 언어(High Level Shading Language)라고도 불리며 줄여서 HLSL이라고도 불린다. Fixed function이라 불리는 고정 기능 파이프라인에서 DirectX 8.0에서 셰이더 개념이 도입되고 프로그래머블 셰이더 제작이 가능했으나 어셈블리와 유사한 낮은 수준의 코드로 작성해야 했으며, 복잡한 그래픽스 표현이 힘들어 HLSL의 도입으로 점차 대체되어갔다.

 

 

  • 초기 (DirectX 8, OpenGL 1.4)
    • 어셈블리 언어 기반의 셰이더 도입.
    • 정점(Vertex) 셰이더와 픽셀(Pixel) 셰이더로 시작.
  • HLSL과 GLSL의 등장 (DirectX 9, OpenGL 2.0)
    • 고급 언어(HLSL, GLSL)로 셰이더 작성이 가능해짐.
    • 셰이더 모델 2.0과 3.0에서 더 복잡한 그래픽 효과 지원.
  • DirectX 10 / OpenGL 3.0 (2007~)
    • 지오메트리 셰이더 추가.
    • GPU의 성능과 유연성 증가.
  • DirectX 11 / OpenGL 4.0 (2009~)
    • 테셀레이션 셰이더컴퓨트 셰이더 추가.
    • 실시간 물리 효과와 고품질 그래픽 가능.
  • DirectX 12 / Vulkan
    • 저수준 API 제공으로 GPU를 더욱 효율적으로 제어.
    • RayTracing / Mesh Shaders / Sampler Feedback Streaming / Variable Rate Shading / HW Accelarated Machine Learning.

 

 

특징

HLSL의 프로그램 형식에는 3가지가 있다. 버텍스 셰이더, 지오메트리 셰이더, 그리고 픽셀 셰이더이다. 다이렉트3D 10 인터페이스에서는 지오메트리 셰이더가 버텍스 셰이더와 픽셀 셰이더 사이의 파이프라인 가운데 새로 추가되었다. 버텍스 셰이더는 응용 프로그램이 제공하는 정점에 각각 붙어 실행되어 주로 이하의 처리를 담당한다. 객체 공간으로부터 시 공간에의 정점 변환이나 텍스처 좌표의 생성, 또 정점의 접선이나 종법선이나 법선 벡터와 같은 광선의 계 계산 등에 쓰인다. 버텍스 셰이더를 통해 정점의 그룹이 입력되었을 때, 출력 좌표는 그 영역내에서의 픽셀을 결정하기 위해서 보간 되게 된다. 이러한 작업은 레스터라이제이션으로 더 잘 알려져 있다. 이러한 픽셀이 각각이 픽셀 셰이더를 통과하는 것으로, 결과적으로 화면상의 색이 계산된다.

또, 다이렉트3D 10 인터페이스 또는 다이렉트3D 10 하드웨어를 사용하는 응용 프로그램은 지오메트리 셰이더를 직접 지정할 수도 있다. 이 셰이더는 삼각형의 3개의 정점을 입력받아 추가의 삼각형을 생성해 틈새 없게 채운 후, 레스터라이저에 보내게 된다.

shader는 하드웨어에서 지원하는 shader model에 따라 기능수준이 결정된다. 자세한 내용은 아래 링크 참고

주요 셰이더 모델

  • Shader Model 2.0: 기본적인 픽셀 및 버텍스 셰이더 지원.
  • Shader Model 3.0: 더 많은 명령어 세트와 동적 브랜칭 지원.
  • Shader Model 4.0 (DX10): 지오메트리 셰이더 도입.
  • Shader Model 5.0 (DX11): 컴퓨트 셰이더 도입, 더 복잡한 효과 구현 가능.
  • Shader Model 6.x (DX12): 최신 GPU 기능 지원 (예: 레이 트레이싱, FP16, 웨이브 셰이딩 등).

Unity Shaderlab shader model features : https://illu.tistory.com/1248
Mobile Graphics API 및 Shader Model feature : https://illu.tistory.com/1209

Vertex Shader

버텍스 셰이더(Vertex Shader)는 그래픽스 파이프라인에서 각 정점(Vertex)의 위치를 처리하는 GPU 프로그램으로, 주로 모델의 변환(예: 회전, 이동, 스케일)을 계산하고, 정점 정보를 변형하여 월드 좌표계에서 화면 좌표계로 변환하는 역할을 하게 된다.

  • 변환(Matrix Transformations) : 오브젝트의 위치, 회전, 스케일을 처리하여, 모델 좌표계에서 뷰, 클립 좌표계로 변환한다. 이는 주로 모델-뷰-프로젝션 행렬을 곱하는 과정에서 이루어진다.
  • 정점 속성 전달 : 각 정점의 위치뿐 아니라, 정점에 대한 추가적인 속성(예 : 색상, 텍스처 좌표, 노멀 벡터 등)을 픽셀 셰이더로 전달하게 된다. 이를 통해 픽셀 셰이더는 각 픽셀에 필요한 정보를 기반으로 최종 색을 계산할 수 있다.
  • 조명 계산 : 버텍스 셰이더에서 조명 계산을 일부 수행할 수 있다. 예를 들어, 고전적인 램버트 조명 모델이나 퐁 조명 모델 등 기본적인 조명 효과는 버텍스 셰이더에서 계산할 수 있다(다만, 버텍스 기반 계산이므로 버텍스가 충분히 많지 않을때 엣지가 두드러지게 보이는 증상이 있다)
  • 정점 변형 : GPU는 버텍스 셰이더를 사용해 정점들을 변형할 수 있는데 이를 활용해 정점의 움직임을 제어해 다양한 움직임을 만들수 있다.
  • 텍스처 좌표 계산 : 버텍스 셰이더는 텍스처 좌표를 계산하고, 이를 픽셀 셰이더로 전달한다. 예를 들어, 정점의 위치에 따라 텍스처 맵핑이 다르게 적용될 수 있다. 또한, 이를 활용해 텍스처 UV 정보값을 왜곡해 다양한 효과를 만들어 낼 수도 있다

Vertex Shader의 동작 과정

  • 입력(Input) : CPU에서 정점 데이터(예: 위치, 색상, 텍스처 좌표 등)를 GPU로 전달한다. 이 데이터는 버텍스 셰이더에서 변환되게 된다(Input Assembly)
  • 계산(Processing) : 정점의 변환, 조명, 텍스처 좌표 등의 계산을 수행.
  • 출력(Output) : 최종적으로 변환된 정점 데이터를 그래픽스 파이프라인의 다음 단계로 보간기(Interpolator)를 통해 전달한다(주로 픽셀 셰이더)

 

주요 함수 및 구성

  • float4 : 위치, 색상 등의 데이터형으로 주로 4D 벡터로 나타내는 자료형.
  • mul : 행렬 변환을 위해 많이 사용되는 함수로, 버텍스 위치에 변환 행렬을 곱하는 데 사용.

struct VertexInput 
 {
    float3 pos : POSITION;
    float4 color : COLOR;
 };

struct VertexOutput 
  {
    float4 pos : SV_POSITION;
    float4 color : COLOR;
  };

cbuffer MatrixBuffer 
  {
    matrix world;
    matrix view;
    matrix projection;
  };

VertexOutput main(VertexInput input) 
  {
    VertexOutput output;
    float4 worldPos = mul(float4(input.pos, 1.0f), world);
    float4 viewPos = mul(worldPos, view);
    output.pos = mul(viewPos, projection);
    output.color = input.color;
    return output;
  }

위 예시에서 VertexInput은 정점의 입력 데이터(위치, 색상 등)를 나타내며, MatrixBuffer는 변환을 위한 행렬들을 담고 있다. 이 행렬들을 이용해 정점의 위치를 변환하고, 결과값을 출력하게 된다.
버텍스 셰이더는 그래픽스 파이프라인의 첫 번째 단계에서 매우 중요한 역할을 하며, 복잡한 3D 장면에서 정점 데이터를 효율적으로 처리하는 데 사용된다.

 

Pixel Shader

픽셀 셰이더(Pixel Shader)는 그래픽스 파이프라인에서 각 픽셀의 색상, 명도, 텍스처 정보 등을 계산하는 GPU 프로그램으로, 주로 화면에 렌더링될 각 픽셀에 대한 최종 처리를 담당하게 된다.여기에 각 오브젝트의 픽셀당 조명 및 음영 계산(lighting & ambient color), 텍스처 맵핑, 포스트 프로세싱 등이 해당된다


struct PS_Input
  {
    float4 pos : SV_POSITION;
    float2 tex : TEXCOORD;
  };

sampler2D texSampler;
float4 main(PS_Input input) : SV_Target 
  {
    return tex2D(texSampler, input.tex);
  }

이 예시는 간단히 텍스처 좌표를 바탕으로 픽셀의 색상을 계산하는 기본적인 픽셀 셰이더로써, 복잡한 시각 효과(예: 반사, 그림자, 물리 기반 조명 등)를 구현할 수 있다.

포워드와 디퍼드의 셰이더 구조가 다른데 디퍼드의 경우 아래와 같이 해당 픽셀정보를 순차적으로 계산하는것이 아닌 Gbuffer에 저장하고 이를 최종 이미지에 사용하는 차이가 있다.

//First pass, 이 셰이더는 첫 번째 패스에서 각 픽셀의 알베도(색상), 노멀 벡터, 그리고 위치를 G-buffer에 저장

struct PS_Input 
  {
    float4 position : SV_POSITION;
    float3 normal : NORMAL;
    float2 uv : TEXCOORD0;
  };

struct GBufferOutput 
  {
    float4 albedo : SV_Target0;  // 색상 정보
    float4 normal : SV_Target1;  // 노멀 벡터 정보
    float4 position : SV_Target2; // 위치 정보
  };

GBufferOutput main(PS_Input input) {
    GBufferOutput output;
    output.albedo = float4(1.0, 0.0, 0.0, 1.0);  // 알베도 값 (빨간색)
    output.normal = float4(normalize(input.normal), 0.0);  // 노멀 벡터
    output.position = input.position;  // 월드 위치
    return output;
}

 

//second pass, 이 두 번째 패스의 픽셀 셰이더는 G-buffer에서 각 픽셀의 데이터를 가져와 조명 효과를 계산하고, 최종적으로 렌더링할 픽셀 값을 생성

struct PS_Input 
  {
    float2 uv : TEXCOORD0;
  };

sampler2D gAlbedo;   // G-buffer에서 가져온 알베도 값
sampler2D gNormal;   // G-buffer에서 가져온 노멀 값
sampler2D gPosition; // G-buffer에서 가져온 위치 값

float4 main(PS_Input input) : SV_Target0 {
    float4 albedo = tex2D(gAlbedo, input.uv);      // 알베도 값 가져오기
    float3 normal = normalize(tex2D(gNormal, input.uv).xyz);  // 노멀 값 가져오기
    float3 position = tex2D(gPosition, input.uv).xyz;  // 월드 공간의 위치

    // 간단한 조명 계산 (디렉셔널 라이트)
    float3 lightDir = normalize(float3(0.5, 0.5, -0.5));  // 라이트 방향
    float NdotL = max(dot(normal, lightDir), 0.0);  // 노멀과 라이트 벡터의 내적
    float3 lighting = NdotL * float3(1.0, 1.0, 1.0); // 디렉셔널 라이트 색상 (흰색)

    return float4(albedo.rgb * lighting, 1.0);  // 조명이 적용된 알베도 값 반환
}

 

Geometry Shader

그래픽스 파이프라인에서 버텍스 셰이더와 래스터라이저 사이에 위치하며, 3D 모델의 정점(버텍스) 데이터를 입력받아 추가적인 정점을 생성하거나, 정점의 데이터를 수정하는 역할을 한다. 즉, 입력된 정점 데이터를 이용해 새로운 도형을 생성하거나, 정점을 늘리고 줄이는 등의 다양한 조작이 가능하다.
https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-geometry-shader

 

Geometry-Shader Object - Win32 apps

A geometry-shader object processes entire primitives. Use the following syntax to declare a geometry-shader object.

learn.microsoft.com

지오메트리 셰이더는 Shader Model 4.0 이상 부터 지원하며, 모바일에서는 OpenGL es 3.1+ AEP(Android Extension Pack) 이상 부터 지원.(https://docs.unity3d.com/2021.3/Documentation/Manual/SL-ShaderCompileTargets.html)

//수정할 최대 버텍스 수를 정의
[maxvertexcount(3)]

// primitive type 정의 및 받아오는 data type 및 stream output 정의
void main(triangle InputType input[3], inout TriangleStream<OutputType> outputStream)
   {
      for (int i = 0; i < 3; ++i) {
        OutputType output;
        output.pos = input[i].pos;  // 정점 위치
        output.color = input[i].color;  // 정점 색상
        outputStream.Append(output);  // 출력 스트림에 추가
    }
}

stream output : https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-so-type

활용 사례

  • 그림자 볼륨 생성
  • 파티클 시스템에서 스프라이트 생성
  • 와이어프레임 렌더링에서 추가 선분 생성

장점

  • 추가적인 정점 생성으로 3D 모델의 디테일을 동적으로 조정 가능.
  • 한 번의 호출로 여러 정점이나 프리미티브를 생성할 수 있어 효율적.

단점

  • 연산 비용이 크기 때문에 성능에 영향을 미칠 수 있음.
  • 모든 GPU에서 효율적으로 지원되지 않을 수 있음.
 

Hull Shader

Tessellation 파이프라인에서 사용되는 셰이더로, 테셀레이션의 첫 단계에서 실행된다. 이 셰이더는 입력된 패치(정점 집합-삼각형, 사각형 또는 선)에 대한 세분화의 정도를 정의하고, 패치의 변형이나 추가 정보를 계산하게 된다. 패치당 한번 호출되며, 하위 표면을 정의하는 입력 제어점(Control point)을 패치를 만드는 제어점으로 변환(Patch Constant Data)하기도 한다. 또한 TS(Tessellation Stage) 및 DS(Domain Shader)에 데이터를 제공하기 위한 일부 패치별 계산도 수행하게 된다.


// 패치 상수 함수 정의
[patchconstantfunc("PatchConstantFunction")]
HS_CONSTANT_DATA_OUTPUT PatchConstantFunction(InputPatch<VertexInputType, 3> patch) 
   {
    HS_CONSTANT_DATA_OUTPUT output;
    output.edgeTessellationFactors[0] = 4.0f; // 엣지의 테셀레이션 레벨 설정
    output.insideTessellationFactor = 3.0f;   // 내부 테셀레이션 레벨 설정
    return output;
   }


// Hull Shader 함수 정의
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("PatchConstantFunction")]

HS_OUTPUT main(InputPatch<VertexInputType, 3> patch, uint id : SV_OutputControlPointID) 
   {
    HS_OUTPUT output;
    output.position = patch[id].position;
    return output;
   }
  • PatchConstantFunction 함수는 패치의 세분화 수준을 설정.
  • domain("tri")는 삼각형 패치로 정의
  • partitioning("fractional_odd")는 세분화를 홀수 분할로 설정하여 부드러운 전환을 제공
  • outputcontrolpoints(3)는 출력 제어점의 개수를 3개로 설정
  • main 함수에서 각 제어점의 위치를 반환

 

 

Domain Shader

Hull Shader에서 정의된 세분화 수준에 따라 생성된 정점의 최종 위치를 계산하는 역할을 한다. 이 셰이더는 주로 Hull Shader와 함께 사용되며, 세분화된 패치의 각 정점 위치를 정의하여 최종적으로 세분화된 표면의 모양을 결정하게 된다


[domain("quad")]
OutputType domainShader(HullOutputType patchData, float2 uv : DOMAIN_LOCATION) 
   {
    OutputType output;

    // 세분화된 정점의 위치 계산
    float3 pos = lerp(
        lerp(patchData.controlPoints[0].position, patchData.controlPoints[1].position, uv.x),
        lerp(patchData.controlPoints[3].position, patchData.controlPoints[2].position, uv.x),
        uv.y);

    output.position = float4(pos, 1.0f);
    return output;
   }

  • [domain("quad")]는 이 Domain Shader가 사각형 패치(quad)를 대상으로 작동하도록 설정하며, patchData는 Hull Shader에서 전달된 제어점 데이터를 포함하게 된다.
  • uv는 Domain Shader에 입력되는 세분화된 정점의 좌표로, 이를 통해 최종 정점의 위치를 계산하며, lerp 함수를 사용해 두 점 사이를 선형 보간하여 새로운 정점의 위치를 결정하게 된다.

 

쿼드 기반 테셀레이션과 삼각형 기반 테셀레이션은 테셀레이션 패치의 기본 형상에 따라 처리 프로세스에 차이

쿼드 기반 테셀레이션

  • 기본 단위 : 사각형(쿼드) 패치를 사용.
  • 처리 프로세스 : 각 쿼드는 정사각형 또는 직사각형 형태로 세분화되며, control point로 4개의 제어점이 사용되게 된다.  사각형 구조이기 때문에 UV 매핑이 쉽고, 정사각형 패치에서는 부드러운 변형이 용이하여 평평하거나 규칙적인 표면을 세분화할 때 유리하다.

삼각형 기반 테셀레이션

  • 기본 단위: 삼각형 패치를 사용.
  • 처리 프로세스 : 각 삼각형 패치는 중심에서 각 변으로 세분화되어 삼각형 단위로 세분화가 진행되며, 삼각형은 비정형 표면이나 복잡한 기하학에 유연하게 대응 가능하여 곡선 형태를 잘 처리할 수 있다. 곡선이 많고 비정형적인 지형이나 표면을 세분화할 때 유리하다.

차이점의 이유

이 차이는 기본 형상의 특성에서 비롯된다. 쿼드는 직교 구조로, 정사각형과 같은 규칙적인 형태에 적합하지만 곡면 처리에 제한적이며, 삼각형은 임의의 형태와 곡선을 자연스럽게 표현할 수 있어 비정형 모델에 적합하다.

 

소프트웨어 테셀레이션

GPU가 아닌 CPU에서 tri를 분할해 처리하는 소프트웨어 방식의 테셀레이션도 가능하다(하드웨어 방식이 hull, domain shader를 사용)


void TessellateTriangle(Vertex v0, Vertex v1, Vertex v2, int levels)

 {
    for (int i = 0; i <= levels; ++i)
      {
        for (int j = 0; j <= i; ++j) 
            {
            // 새로운 정점 계산 (예: 선형 보간)
            Vertex vA = Lerp(v0, v1, (float)j / i);
            Vertex vB = Lerp(v0, v2, (float)(i - j) / i);

            // 세분화된 삼각형 추가
            AddTriangle(vA, vB, ...);
        }
    }
}

 

아래는 unity C# 코드 예제이다.


using UnityEngine;

public class SoftwareTessellation : MonoBehaviour
{
    public MeshFilter meshFilter;
    public int tessellationLevel = 2;

    void Start()
    {
        Mesh originalMesh = meshFilter.mesh;
        Mesh tessellatedMesh = TessellateMesh(originalMesh, tessellationLevel);
        meshFilter.mesh = tessellatedMesh;
    }

    Mesh TessellateMesh(Mesh originalMesh, int level)
    {
        Vector3[] originalVertices = originalMesh.vertices;
        int[] originalTriangles = originalMesh.triangles;

        // 새 정점 및 삼각형 리스트
        var newVertices = new System.Collections.Generic.List<Vector3>();
        var newTriangles = new System.Collections.Generic.List<int>();

        // 각 삼각형에 대해 세분화 수행
        for (int i = 0; i < originalTriangles.Length; i += 3)
        {
            Vector3 v0 = originalVertices[originalTriangles[i]];
            Vector3 v1 = originalVertices[originalTriangles[i + 1]];
            Vector3 v2 = originalVertices[originalTriangles[i + 2]];

            SubdivideTriangle(v0, v1, v2, level, newVertices, newTriangles);
        }

        // 새로운 메쉬 생성
        Mesh tessellatedMesh = new Mesh();
        tessellatedMesh.vertices = newVertices.ToArray();
        tessellatedMesh.triangles = newTriangles.ToArray();
        tessellatedMesh.RecalculateNormals();
        return tessellatedMesh;
    }

    void SubdivideTriangle(Vector3 v0, Vector3 v1, Vector3 v2, int level, System.Collections.Generic.List<Vector3> vertices, System.Collections.Generic.List<int> triangles)
    {
        if (level == 0)
        {
            int startIndex = vertices.Count;
            vertices.Add(v0);
            vertices.Add(v1);
            vertices.Add(v2);
            triangles.Add(startIndex);
            triangles.Add(startIndex + 1);
            triangles.Add(startIndex + 2);
        }

        else
        {
            Vector3 mid01 = (v0 + v1) * 0.5f;
            Vector3 mid12 = (v1 + v2) * 0.5f;
            Vector3 mid20 = (v2 + v0) * 0.5f;

            SubdivideTriangle(v0, mid01, mid20, level - 1, vertices, triangles);
            SubdivideTriangle(mid01, v1, mid12, level - 1, vertices, triangles);
            SubdivideTriangle(mid12, v2, mid20, level - 1, vertices, triangles);
            SubdivideTriangle(mid01, mid12, mid20, level - 1, vertices, triangles);
        }
    }
}

 

차이 요약

처리 위치 CPU GPU (전용 테셀레이션 유닛)
코드 구조 CPU 루프, 선형 보간 Hull Shader, Domain Shader 사용
성능 CPU 부하로 속도 느림 병렬 처리로 빠르고 효율적
유연성 세부 제어 가능 제어가 제한적

 

반응형

'Technical Report > Unity Shader' 카테고리의 다른 글

Autodesk Media & Entertainment 2012  (4) 2012.07.23
lighting 관련 몇가지 용어  (0) 2012.07.15
Cg Shader Programming  (0) 2012.07.12
120710 아트 세미나 Photoshop Tool and Tip  (0) 2012.07.08
binormal, tangent normal  (5) 2012.05.24