https://www.3dgep.com/forward-plus/
Introduction
Forward rendering은 씬(scene)의 각 기하학적 개체(geometry object)를 래스터화(rasterizing)하여 작동한다. 셰이딩 처리중 씬의 조명리스트가 반복되어 기하학적 개체가 어떻게 조명되어야 하는지를 결정하게 되며, 이것은 모든 기하학적 개체가 씬의 모든 조명을 고려해야 한다는 것을 의미한다. 물론 가려지거나 카메라의 절두체에 나타나지 않는 기하학적 객체를 버려 이를 최적화할 수 있으며, 카메라의 시야 절두체내에 있지 않은 조명을 버려 이 기술을 더욱 최적화할 수 있다. 조명의 범위를 알고 있으면 씬 형상을 렌더링하기 전에 조명 볼륨에 대해 절두체 컬링을 수행할 수 있으며, 오브젝트 컬링 및 라이트 볼륨 컬링은 이 기술에 대해 제한된 최적화를 제공하며 포워드 렌더링 파이프라인을 사용할 때 라이트 컬링은 종종 실행되지 않는다. 씬의 개체에 영향을 줄 수 있는 조명의 수를 단순히 제한하는 것이 더 일반적이다. 예를 들어, 일부 그래픽 엔진은 가장 가까운 두 개 또는 세 개의 조명으로 픽셀당(per-pixel) 조명을 수행하고 다음 가까운 조명 중 세 개 또는 네 개에서 정점당 조명을 수행하기도 한다. OpenGL 및 DirectX가 제공하는 기존의 고정 기능 렌더링 파이프라인에서 씬에 활성화되는 동적 조명의 수는 보통 약 8개로 제한한다. 최신 그래픽 하드웨어를 사용하더라도 포워드 렌더링 파이프라인은 눈에 띄는 프레임 속도 문제가 나타나기 전에 약 100개의 동적 씬 조명으로 제한한다.(역주 : 넓은 씬에서의 사용에 대한 제한을 의미하는것이고 밀접된 공간에 100개의 동적라이팅을 배치하는건 매우 비효율적인 방식이다.)
Deferred Shading은 반대로 모든 씬의 기하학적 개채 정보를 이후 패스에서 라이팅 정보 계산을 위해 일련의 2D 이미지 버퍼로 래스터화하여 저장한다. 2D 이미지 버퍼에 저장되는 정보는 다음과 같다.
- screen space depth
- surface normals
- diffuse color
- specular color and specular power
이러한 2D 이미지 버퍼의 조합을 기하학적 버퍼(Geometry Buffer)(또는 G-버퍼)라고 한다.
나중에 수행될 조명 계산에 필요한 경우 다른 정보도 이미지 버퍼에 저장할 수 있지만 각 G-버퍼 텍스처에는 F HD(1080p) 및 픽셀당 32비트에서 최소 8.29MB의 텍스처 메모리가 필요하게 된다.
G 버퍼가 생성된 후 기하학적 정보를 사용하여 조명 패스의 조명 정보를 계산할 수 있다. 조명 패스는 각 광원을 씬의 기하학적 개체로 렌더링하여 수행되게 된다. 조명의 기하학적 표현이 닿는 각 픽셀은 원하는 조명 방정식을 사용하여 셰이딩 처리되게 된다.
포워드 렌더링과 비교하여 디퍼드 셰이딩 기술의 명백한 이점은 값비싼 조명 계산이 커버된 픽셀당 라이팅을 한번만 계산된다는 것이다. 최신 하드웨어에서 디퍼드 셰이딩은 불투명(opaque) 씬 개체만 렌더링할 때 프레임 속도 문제가 나타나기 전에 FHD 해상도(1080p)에서 약 2,500개의 동적 장면 라이팅을 처리할 수 있다.
디퍼드 셰이딩을 사용할 때의 단점 중 하나는 불투명한 개체(opaque object)만 G 버퍼로 래스터화할 수 있다는 것이다. 그 이유는 여러 투명 개체(Transparent object)가 동일한 화면 픽셀을 덮을 수 있지만 G 버퍼에는 픽셀당 단일 값만 저장할 수 있기 때문이다. 조명 패스에서 depth value, surface normal, diffuse 및 specular 색상이 조명되고 있는 현재 화면 픽셀에 대해 샘플링 되게 된다. 각 G 버퍼의 단일 값만 샘플링되므로 조명 패스에서 투명한 개체를 지원할 수 없게된다. 이 문제를 피하려면 씬의 투명 지오메트리나 씬의 동적 조명 수를 제한하는 표준 포워드 렌더링 기술을 사용하여 투명 지오메트리를 렌더링해야 합니다. 불투명한 개체로만 구성된 장면은 프레임 속도 문제가 나타나기 전에 약 2000개의 동적 조명을 처리할 수 있다.(역자주 : 포워드와 디퍼드 모두 opaque와 transparent object는 분리해서 각 패스별로 처리하지만 디퍼드는 반투명 메시의 경우 포워드로 처리 한다.)
디퍼드 셰이딩의 또 다른 단점은 조명 패스에서 단일 조명 모델만 시뮬레이션할 수 있다는 것이다. 이는 라이트 지오메트리를 렌더링할 때 단일 픽셀 셰이더만 바인딩할 수 있기 때문이다. 단일 픽셀 셰이더로 렌더링하는 것이 표준이므로 ubershader를 사용하는 파이프라인에서는 일반적으로 문제가 되지 않지만, 렌더링 파이프라인이 다양한 픽셀 셰이더에서 구현된 여러 다른 조명 모델을 활용하는 경우 렌더링을 전환하는 것이 문제가 되게된다.
Forward+ (Tiled Forward Shading이라고도 함)는 포워드 렌더링과 타일링된 조명 컬링을 결합하여 셰이딩 처리 중에 고려해야 하는 조명 수를 줄이는 렌더링 기술이다. Forward+는 주로 두 단계로 구성된다.
- Light culling
- Forward rendering
Forward+ 렌더링 기술의 첫 번째 단계는 화면 공간에서 균일한 타일 그리드를 사용하여 조명을 타일별 목록으로 분할한다.
두 번째 패스는 표준 포워드 렌더링 패스를 사용해 씬의 개체를 셰이딩 처리하지만, 씬의 모든 동적 조명을 반복해 계산하는 대신 현재 픽셀의 화면 공간 위치를 사용하여 이전패스에서 계산된 그리드의 조명 목록을 조회한다. 이 라이트 컬링은 픽셀을 올바르게 조명하기 위해 반복해야 하는 중복 조명의 수를 크게 줄이므로 표준 포워드 렌더링 기술에 비해 상당한 성능 향상을 제공하게된다. Forward+에서는 여러 매터리얼과 조명 모델은 기본으로 불투명 및 투명 지오메트리 모두 성능의 상당한 손실 없이 유사한 방식으로 처리할 수 있다. Forward+는 표준 포워드 렌더링 파이프라인을 기술에 통합하기 때문에 Forward+는 처음에 포워드 렌더링을 사용하여 구축된 기존 그래픽 엔진에 통합될 수 있다. Forward+는 G-buffer를 사용하지 않으며 deferred shading의 한계를 겪지 않는다. 불투명 및 투명 지오메트리 모두 Forward+를 사용하여 렌더링할 수 있다. 최신 그래픽 하드웨어를 사용하여 5,000~6,000개의 동적 조명으로 구성된 장면을 풀 HD 해상도(1080p)에서 실시간으로 렌더링할 수 있게된다. 이 포스팅의 나머지 부분에서는 다음 세 가지 기술의 구현에 대해 설명한다.
- Forward Rendering
- Deferred Shading
- Forward+ (Tiled Forward Rendering)
또한 이 기술이 다른 기술보다 더 잘 수행되는 조건을 결정하기 위해 다양한 상황에서의 성능 통계를 보여줄 예정이다.
Definitions
이 포스팅의 맥락에서 나머지 부분을 더 쉽게 이해할 수 있도록 몇 가지 용어를 정의하는 것이 중요하다. 그래픽 프로그래밍에 사용되는 기본 용어에 익숙하다면 이 섹션을 건너뛰어도 괜찮다.
Scene은 렌더링할 수 있는 개체의 중첩 계층을 나타낸다. 예를 들어, 렌더링할 수 있는 모든 정적 개체는 씬으로 그룹화된다. 각 개별 렌더링 가능한 개체는 씬노드를 사용하여 씬에서 참조된다. 각 씬노드(scene node)는 단일 렌더링 가능한 개체(예: 메쉬)를 참조하고 전체 장면은 루트 노드(root node)라고 하는 장면의 최상위 노드를 사용하여 참조할 수 있다. 씬내 씬노드의 연결을 씬그래프(Scene Graph)라고도 한다. 루트 노드는 씬노드이기도 하므로 씬을 중첩하여 정적 및 동적 개체를 모두 사용하여 더 복잡한 씬그래프를 생성할 수 있다.
Pass는 렌더링 기술의 한 단계를 수행하는 단일 작업을 나타낸다. 예를 들어 불투명 패스(opaque pass)는 씬의 모든 개체를 반복하고 불투명 개체만 렌더링하는 패스이다. 투명 패스(Transparent pass)는 씬의 모든 개체를 반복하지만 투명 개체만 렌더링한다. 패스는 또한 GPU 리소스 복사 또는 컴퓨팅 셰이더 디스패치와 같은 보다 일반적인 작업에도 사용할 수 있다.
기술(Technique)은 렌더링 알고리즘을 구현하기 위해 특정 순서로 실행되어야 하는 여러 패스의 조합이다.
파이프라인 상태(Pipeline state)는 개체가 렌더링되기전의 렌더링 파이프라인 구성을 나타낸다. 파이프라인 상태 개체는 다음 렌더링 상태를 압축한다.
- Shaders (vertex, tessellation, geometry, and pixel)
- Rasterizer state (polygon fill mode, culling mode, scissor culling, viewports)
- Blend state
- Depth/Stencil state
- Render target
DirectX 12는 파이프라인 상태 개체를 도입하지만 파이프라인 상태에 대한 내 정의(my definition)는 DirectX 12의 정의와 약간 다르다.
포워드 렌더링은 전통적으로 두 가지 패스만 있는 렌더링 기술을 나타낸다.
Opaque Pass
Transparent Pass
불투명 패스는 오버드로(overdraw)를 최소화하기 위해(카메라를 기준으로-relative to the camera) 앞에서 뒤로 이상적으로 정렬된 장면의 모든 불투명 개체를 렌더링한다. 불투명 패스 중에는 블렌딩을 수행할 필요가 없다(역주 : Transparent 계산에서 Blend operation).
투명 패스는 정확한 블렌딩을 지원하기 위해 씬의 모든 투명 개체를 이상적으로 뒤로(카메라를 기준으로) 정렬하여 렌더링한다. 투명 패스중에 반투명 매터리얼이 렌더 대상의 색상 버퍼에 이미 렌더링된 픽셀과 올바르게 혼합될 수 있도록 알파 블렌딩을 활성화해야 한다.
포워드 렌더링 동안 모든 조명은 픽셀 셰이더에서 수행되며 다른 모든 머티리얼 셰이딩 명령과 함께 수행된다.
디퍼드 셰이딩은 아래 세 가지 기본 패스로 구성된 렌더링 기술을 나타낸다.
- Geometry Pass
- Lighting Pass
- Transparent Pass
첫 번째 패스는 불투명 객체만 이 패스에서 렌더링되기 때문에 정방향 렌더링 기술의 불투명 패스와 유사한 지오메트리 패스이다. 차이점은 지오메트리 패스는 조명 계산을 수행하지 않고 도입부에서 설명한 G버퍼에 지오메트리 및 매터리얼 데이터만 출력한다는 것이다.
조명 패스에서 조명을 나타내는 기하학적 볼륨이 씬에 렌더링되고 G-버퍼에 저장된 재료 정보를 사용하여 래스터화된 픽셀의 조명을 계산한다.
마지막 패스는 투명 패스다. 이 패스는 포워드 렌더링 기술의 투명 패스와 동일하다. 디퍼드 셰이딩은 투명 머티리얼을 기본적으로 지원하지 않기 때문에 투명 오브젝트는 표준 포워드 렌더링 방법을 사용하여 조명을 수행하는 별도의 패스에서 렌더링되어야 한다.
Forward+(타일드 포워드 렌더링이라고도 함)는 세 가지 기본 패스로 구성된 렌더링 기술이다.
- Light Culling Pass
- Opaque Pass
- Transparent Pass
소개에서 언급했듯이 라이트 컬링 패스는 장면의 다이내믹 라이트를 스크린 공간 타일로 분류하는 역할을 한다. 라이트 인덱스 목록은 어떤 라이트 인덱스(전체 라이트 목록에서)가 각 화면 타일과 겹치는지 나타내는 데 사용된다. 라이트 컬링 패스에서 두 세트의 라이트 인덱스 목록이 생성된다.
- Opaque light index list
- Transparent light index list
opaque light index list는 opaque geometry를 렌더링할 때 사용되며 transparent light index list는 투명 geometry를 렌더링할 때 사용된다.
Forward+ 렌더링 기술의 불투명과 투명 패스는 표준 포워드 렌더링 기술과 동일하지만 씬의 모든 동적 조명을 반복하는 대신 현재 조각의 화면 공간 타일에 있는 조명만 고려하면 된다.
조명은 다음 유형의 조명 중 하나를 나타냅니다.
- Point light
- Spot light
- Directional light
이 포스팅에서 설명하는 모든 렌더링 기술은 이 세 가지 조명 유형을 지원한다. Area light는 지원되지 않는다. 포인트 라이트와 스포트 라이트는 단일 원점에서 발산하는 것으로 시뮬레이션되는 반면 디렉셔널 라이트는 무한히 멀리 떨어진 점에서 발산하여 동일한 방향으로 모든 곳에서 빛을 방출하는 것으로 간주된다. 포인트 라이트와 스포트 라이트는 범위가 제한되어 있으며 그 이후에는 강도가 0으로 떨어지게된다. 빛의 강도가 줄어드는 것은 감쇠(attenuation)라고 한다. 포인트 라이트는 기하학적으로 구체로, 스폿 라이트는 원뿔로, 방향 라이트는 전체 화면 쿼드로 표시된다.
먼저 표준 포워드 렌더링 기술에 대해 자세히 살펴 본다.
Forward Rendering
포워드 렌더링은 세 가지 조명 기술 중 가장 단순하며 게임에서 그래픽을 렌더링하는 데 사용되는 가장 일반적인 기술이다. 또한 조명을 계산하는 데 가장 계산 비용이 많이 드는 기술이며 이러한 이유로 씬에서 많은 수의 동적 조명을 사용할 수 없다.
포워드 렌더링을 사용하는 대부분의 그래픽 엔진은 다양한 기술을 활용하여 씬의 많은 조명을 시뮬레이션 한다. 예를 들어 라이트 매핑 및 라이트 프로브는 장면에 배치된 정적 조명의 조명 기여도를 미리 계산하고 런타임에 로드되는 텍스처에 이러한 조명 기여도(contribution)를 저장하는 데 사용되는 방법이다. 불행히도 라이트맵을 생성하는 데 사용된 라이트는 런타임에 종종 폐기(discard)되기 때문에 라이트매핑 및 라이트 프로브를 사용하여 장면의 동적 라이트를 시뮬레이션할 수 없다.
이 실험에서는 다른 두 렌더링 기술을 비교하기 위해 포워드 렌더링을 ground truth로 사용한다. 포워드 렌더링 기술은 또한 다른 렌더링 기술의 성능을 비교하는 데 사용할 수 있는 성능 기준을 설정하는 데 사용된다.
포워드 렌더링 기술의 많은 기능은 디퍼드 및 포워드+ 렌더링 기술에서 재사용된다. 예를 들어 포워드 렌더링에 사용되는 버텍스 셰이더는 디퍼드 셰이딩과 포워드+ 렌더링에도 모두 사용된다. 또한 최종 조명 및 매터리얼 셰이딩을 계산하는 방법은 모든 렌더링 기술에서 재사용됩니다.
다음 섹션에서는 포워드 렌더링 기술의 구현에 대해 설명합니다.
Vertex Shader
버텍스 셰이더는 모든 렌더링 기술에 공통적이다. 이 실험에서는 정적 지오메트리만 지원되며 다른 버텍스 셰이더가 필요한 스켈레탈 애니메이션이나 터레인은 없다. 버텍스 셰이더는 노멀 매핑과 같이 픽셀 셰이더에서 필요한 기능을 지원하는 것처럼 간단하다.
버텍스 셰이더 코드를 보여주기 전에 버텍스 셰이더에서 사용하는 데이터 구조에 대해 설명한다.
CommonInclude.hlsl |
struct AppData
{
float3 position : POSITION;
float3 tangent : TANGENT;
float3 binormal : BINORMAL;
float3 normal : NORMAL;
float2 texCoord : TEXCOORD0;
};
|
AppData 구조는 응용 프로그램 코드에서 보낼 것으로 예상되는 데이터를 정의한다(응용 프로그램에서 버텍스 셰이더로 데이터를 전달하는 방법에 대한 자습서는 DirectX 11 소개라는 제목의 이전 기사 참조). 노멀 매핑의 경우 노멀 벡터 외에 탄젠트 벡터도 보내야 하며 선택적으로 바이노멀(또는 바이탄젠트) 벡터도 보내야 한다. 탄젠트 및 바이노멀 벡터는 모델이 생성될 때 3D 아티스트가 생성하거나 모델 임포터에서 생성할 수 있다. 제 경우에는 3D아티스트가 아직 생성하지 않은 탄젠트 및 바이탄젠트를 생성하기 위해 Open Asset Import Library에 의존한다.
버텍스 셰이더에서 우리는 또한 응용 프로그램에서 보낸 객체 공간 벡터를 픽셀 셰이더에 필요한 뷰 공간으로 변환하는 방법을 알아야 한다. 이렇게 하려면 월드, 뷰 및 투영 행렬을 버텍스 셰이더로 보내야 한다(이 기사에서 사용된 다양한 공간에 대한 검토는 좌표 시스템이라는 제목의 이전 기사를 참조). 이 행렬을 저장하기 위해 버텍스 셰이더에 필요한 개체별 변수를 저장할 상수 버퍼를 생성한다.
CommonInclude.hlsl
|
cbuffer PerObject : register( b0 )
{
float4x4 ModelViewProjection;
float4x4 ModelView;
}
|
월드 행렬을 별도로 저장할 필요가 없기 때문에 응용 프로그램에서 결합된 모델, 뷰 및 결합된 모델, 뷰 및 투영 행렬을 미리 계산하고 이러한 행렬을 단일 상수 버퍼에서 정점 셰이더로 보낸다.
버텍스 셰이더의 출력(결과적으로 픽셀 셰이더에 대한 입력)은 다음과 같다.
CommonInclude.hlsl
|
struct VertexShaderOutput
{
float3 positionVS : TEXCOORD0; // View space position.
float2 texCoord : TEXCOORD1; // Texture coordinate
float3 tangentVS : TANGENT; // View space tangent.
float3 binormalVS : BINORMAL; // View space binormal.
float3 normalVS : NORMAL; // View space normal.
float4 position : SV_POSITION; // Clip space position.
};
|
VertexShaderOutput 구조는 변환된 버텍스 속성을 픽셀 셰이더에 전달하는 데 사용된다. VS 접미사로 명명된 멤버는 벡터가 뷰 공간에서 표현됨을 나타낸다. 나는 모든 조명을 월드 스페이스가 아닌 뷰 스페이스에서 하기로 선택했다. 디퍼드 셰이딩과 포워드+ 렌더링 기술을 구현할 때 뷰 공간 좌표에서 작업하는 것이 더 쉽기 때문이다.
버텍스 셰이더는 매우 간단하고 최소한으로, 유일한 목적은 응용 프로그램에서 전달한 개체 공간 벡터를 픽셀 셰이더에서 사용할 보기 공간으로 변환하는 것이다.
버텍스 셰이더는 래스터라이저가 사용하는 클립 공간 위치도 계산해야 한다. SV_POSITION 의미-시맨틱(semantic) 체계는 버텍스 셰이더의 출력 값에 적용되어 값이 클립 공간 위치로 사용됨을 지정하지만 이 의미 체계는 픽셀 셰이더의 입력 변수에도 적용될 수 있다. SV_POSITION이 픽셀 셰이더에 대한 입력 의미론으로 사용될 때 값은 화면 공간에서 픽셀의 위치이다. deferred shading과 forward+ shader 모두에서 이 의미 체계를 사용하여 현재 픽셀의 화면 공간 위치를 얻게된다.
ForwardRendering.hlsl |
VertexShaderOutput VS_main( AppData IN )
{
VertexShaderOutput OUT;
OUT.position = mul( ModelViewProjection, float4( IN.position, 1.0f ) );
OUT.positionVS = mul( ModelView, float4( IN.position, 1.0f ) ).xyz;
OUT.tangentVS = mul( ( float3x3 )ModelView, IN.tangent );
OUT.binormalVS = mul( ( float3x3 )ModelView, IN.binormal );
OUT.normalVS = mul( ( float3x3 )ModelView, IN.normal );
OUT.texCoord = IN.texCoord;
return OUT;
}
|
입력 벡터에 행렬을 미리 곱하고 있음을 알 수 있다. 이는 행렬이 기본적으로 열 우선 순위로 저장됨을 나타낸다. DirectX 10 이전에는 HLSL의 행렬이 행 우선 순위로 로드되고 입력 벡터에 행렬이 후배되었다. DirectX 10부터 행렬은 기본적으로 열 우선 순위로 로드된다. 행렬 변수 선언에 row_major 유형 수정자를 지정하여 기본 순서를 변경할 수 있다.
Pixel Shader
픽셀 셰이더는 단일 화면 픽셀의 최종 색상을 결정하는 데 사용되는 모든 조명 및 음영을 계산한다. 이 픽셀 셰이더에 사용된 조명 방정식의 조명 모델에 익숙하지 않은 경우 DirectX 11의 텍스처링 및 조명이라는 제목의 이전 기사에 설명되어 있으므로 계속하기 전에 해당 기사를 먼저 읽어야 한다.
픽셀 셰이더는 작업을 수행하기 위해 여러 구조를 사용한다. Material 구조체는 셰이딩 처리되는 오브젝트의 표면 재질을 설명하는 모든 정보를 저장하고 Light 구조체에는 장면에 배치되는 조명을 설명하는 데 필요한 모든 매개변수가 포함되게 된다.
Material
Material 구조체는 현재 셰이딩 처리되는 개체의 표면을 설명하는 데 필요한 모든 속성을 정의한다. 일부 매터리얼 속성(Material Properties)은 연관된 텍스처(예: 디퓨즈 텍스처, 스페큘러 텍스처 또는 노멀 텍스처)를 가질 수도 있으므로 해당 텍스처가 개체에 있는지 여부를 나타내는 데도 해당 매터리얼을 사용한다.
CommonInclude.hlsl |
struct Material
{
float4 GlobalAmbient;
//-------------------------- ( 16 bytes )
float4 AmbientColor;
//-------------------------- ( 16 bytes )
float4 EmissiveColor;
//-------------------------- ( 16 bytes )
float4 DiffuseColor;
//-------------------------- ( 16 bytes )
float4 SpecularColor;
//-------------------------- ( 16 bytes )
// Reflective value. float4 Reflectance; //-------------------------- ( 16 bytes )
float Opacity;
float SpecularPower;
// For transparent materials, IOR > 0. float IndexOfRefraction;
bool HasAmbientTexture;
//-------------------------- ( 16 bytes )
bool HasEmissiveTexture;
bool HasDiffuseTexture;
bool HasSpecularTexture;
bool HasSpecularPowerTexture;
//-------------------------- ( 16 bytes )
bool HasNormalTexture;
bool HasBumpTexture;
bool HasOpacityTexture;
float BumpIntensity;
//-------------------------- ( 16 bytes )
float SpecularScale;
float AlphaThreshold;
float2 Padding;
//--------------------------- ( 16 bytes )
};
//--------------------------- ( 16 * 10 = 160 bytes ) |
GlobalAmbient 용어(term)는 장면의 모든 개체에 전역적으로 적용되는 주변 효과를 설명하는 데 사용된다. 기술적으로 이 변수는 전역 변수여야 하지만(단일 개체에 국한되지 않음) 픽셀 셰이더에는 한 번에 하나의 매터리얼만 있으므로 두기에 좋은 위치라고 생각했다.
Ambient, emissive, diffuse 및 specular 색상 값은 DirectX 11의 텍스처링 및 조명이라는 제목의 이전 포스팅에서와 동일한 의미를 가지므로 여기에서 자세히 설명하지 않는다.
반사율(Reflectance) 구성요소는 디퓨즈 색상과 혼합되어야 하는 반사된 색상의 양을 나타내는 데 사용할 수 있다. 이를 위해서는 이 실험에서 수행하지 않는 환경 매핑을 구현해야 하므로 이 값은 여기에서 사용되지 않는다.
Opacity 값은 개체의 전체 불투명도를 결정하는 데 사용된다. 이 값은 개체를 투명하게 표시하는 데 사용할 수 있다. 이 속성은 투명 패스에서 반투명 개체를 렌더링하는 데 사용된다. 불투명도 값이 1보다 작으면(1은 완전히 불투명하고 0은 완전히 투명) 개체는 투명한 것으로 간주되어 불투명 패스 대신 투명 패스에서 렌더링된다.(역주 : 유니티의 URP의 경우 셰이더에서 선언된 렌더큐에 따른 순서에 따라 opaque와 Transparent 순서가 결정되며, HLSL의 경우 Blend operation이 정의되어야만 알파 블렌딩이 연산된다)
SpecularPower 변수는 개체가 얼마나 빛나는지 결정하는 데 사용된다다. 반사광은 DirectX 11의 텍스처링 및 조명이라는 제목의 이전 기사에서 설명했으므로 여기서 반복하지 않는다.
IndexOfRefraction 변수는 빛을 굴절시켜야 하는 객체에 적용할 수 있다. 굴절에는 이 실험에서 구현되지 않은 환경 매핑 기술이 필요하므로 이 변수는 여기에서 사용되지 않는다.
HasTexture 변수는 렌더링되는 객체에 해당 속성에 대한 관련 텍스처가 있는지 여부를 나타낸다. 매개변수가 true이면 해당 텍스처가 샘플링되고 텍셀이 해당 재질 색상 값과 혼합된다.
BumpIntensity 변수는 범프 맵의 높이 값을 스케일링하는 데 사용된다(스케일링할 필요가 없는 노멀 매핑과 혼동하지 말 것). 오브젝트 표면의 겉보기 울퉁불퉁함을 부드럽게 하거나 강조하기 위해 사용된다. 대부분의 경우 모델은 높은 테셀레이션 없이 오브젝트 표면에 세부 정보를 추가하기 위해 노멀 맵을 사용하지만 동일한 작업을 수행하기 위해 하이트맵을 사용할 수도 있다. 모델에 범프 맵이 있는 경우 재질의 HasBumpTexture 속성이 true로 설정되고 이 경우 모델은 노멀 매핑 대신 범프 매핑된다.
SpecularScale 변수는 반사광 텍스처(Specular texture)에서 읽은 반사광 값의 크기를 조정하는 데 사용된다. 텍스처는 일반적으로 값을 부호 없는 정규화된 값으로 저장하기 때문에 텍스처에서 샘플링할 때 값은 [0..1] 범위의 부동 소수점 값으로 읽는다. 1.0의 스페큘러 파워는 별로 의미가 없으므로 텍스처에서 읽은 스페큘러 파워 값은 최종 조명 계산에 사용되기 전에 SpecularScale에 의해 조정된다.
AlphaThreshold 변수는 픽셀 셰이더에서 "discard" 명령을 사용하여 불투명도가 특정 값 미만인 픽셀을 버리는 데 사용할 수 있다.(AlphaTest) 이것은 오브젝트를 알파 블렌딩할 필요가 없지만 오브젝트에 구멍이 있어야 하는 "Cutout" 재료와 함께 사용할 수 있다(예: 체인, 링크, 울타리).
Padding 변수는 8바이트의 패딩을 매터리얼 구조체에 명시적으로 추가하는 데 사용된다. HLSL은 구조체의 크기가 16바이트의 배수인지 확인하기 위해 이 패딩을 이 구조체에 암시적으로 추가하지만 명시적으로 패딩을 추가하면 이 구조체의 크기와 정렬이 C++ 대응과 동일하다는 것이 분명해진다.
머티리얼 속성은 상수 버퍼(constant buffer)를 사용하여 픽셀 셰이더에 전달된다.
CommonInclude.hlsl |
cbuffer Material : register( b2 )
{
Material Mat;
};
|
이 상수 버퍼 및 버퍼 레지스터 슬롯 할당은 이 기사에서 설명하는 모든 픽셀 셰이더에 사용된다.
Textures
The materials have support for eight different textures.(8가지 다른 텍스쳐를 지원한다)
- Ambient
- Emissive
- Diffuse
- Specular
- SpecularPower
- Normals
- Bump
- Opacity
모든 씬의 개체가 모든 텍스처 슬롯을 사용하는 것은 아니다(노멀맵과 범프맵은 상호 배타적이므로 동일한 텍스처 슬롯 할당해 재사용할 수 있다). 씬의 모델이 사용할 텍스처를 결정하는 것은 3D 아티스트의 몫이다. 응용 프로그램은 매터리얼과 연결된 텍스처를 로드한다. 텍스처 매개변수 및 관련 텍스처 슬롯 할당은 이러한 각 매터리얼 속성에 대해 선언된다.
commonInclude.hlsl |
Texture2D AmbientTexture : register( t0 ); Texture2D EmissiveTexture : register( t1 );
Texture2D DiffuseTexture : register( t2 );
Texture2D SpecularTexture : register( t3 );
Texture2D SpecularPowerTexture : register( t4 );
Texture2D NormalTexture : register( t5 );
Texture2D BumpTexture : register( t6 );
Texture2D OpacityTexture : register( t7 );
|
이 기사에서 설명하는 모든 픽셀 셰이더에서 텍스처 슬롯 0-7은 이러한 텍스처용으로 예약되어 있다.
LIGHTS
Light 구조체는 씬에서 라이팅을 정의하는 데 필요한 모든 정보를 저장한다. 스포트라이트, 포인트 라이트, 디렉셔널 라이트는 서로 다른 구조체로 분리되지 않으며 이러한 라이트 유형을 정의하는 데 필요한 모든 속성은 단일 구조체에 저장되게 된다.
commonInclude.hlsl |
struct Light
{
/**
* Position for point and spot lights (World space).
*/
float4 PositionWS;
//--------------------------------------------------------------( 16 bytes )
/**
* Direction for spot and directional lights (World space).
*/
float4 DirectionWS;
//--------------------------------------------------------------( 16 bytes )
/**
* Position for point and spot lights (View space).
*/
float4 PositionVS;
//--------------------------------------------------------------( 16 bytes )
/**
* Direction for spot and directional lights (View space).
*/
float4 DirectionVS;
//--------------------------------------------------------------( 16 bytes )
/**
* Color of the light. Diffuse and specular colors are not seperated.
*/
float4 Color;
//--------------------------------------------------------------( 16 bytes )
/**
* The half angle of the spotlight cone.
*/
float SpotlightAngle;
/**
* The range of the light.
*/
float Range;
/**
* The intensity of the light.
*/
float Intensity;
/**
* Disable or enable the light.
*/
bool Enabled;
//--------------------------------------------------------------( 16 bytes )
/**
* Is the light selected in the editor?
*/
bool Selected;
/**
* The type of the light.
*/
uint Type;
float2 Padding;
//--------------------------------------------------------------( 16 bytes )
//--------------------------------------------------------------( 16 * 7 = 112 bytes )
};
|
Position 및 Direction 속성은 월드 공간(WS 접미사 포함)과 뷰 공간(VS 접미사 포함) 모두에 저장된다. 물론 Position 변수는 포인트 라이트와 스폿 라이트에만 적용되는 반면 디렉셔널 변수는 스폿 라이트와 디렉셔널 라이트에만 적용된다. 여기서는 월드공간과 뷰공간 위치 및 방향 벡터를 모두 저장한다. 그 이유는 애플리케이션의 월드공간에서 작업한 다음 광원 배열을 GPU에 업로드하기 전에 월드 공간 벡터를 뷰 공간으로 변환하는 것이 더 쉽기 때문이다. 이렇게 하면 GPU에 필요한 추가 공간을 희생하면서 여러 조명 목록을 유지할 필요가 없다. 그러나 10,000개의 조명이라도 GPU에서 1.12MB만 필요로 하므로 이것이 합리적인 희생이라고 생각했으나 라이트 구조체의 크기를 최소화하면 GPU의 캐싱에 긍정적인 영향을 미치고 렌더링 성능이 향상될 수 있다. 이에 대해서는 이 기사의 끝 부분에 있는 향후 고려 사항 섹션에서 자세히 설명한다.
일부 조명 모델에서는 디퓨즈 및 스페큘러 라이팅 기여도가 분리된다. 나는 이러한 값이 다른 경우가 드물기 때문에 디퓨즈 및 스페큘러 컬러 기여를 분리하지 않기로 결정했다. 대신 Color이라는 단일 변수에 디퓨즈 및 반사 조명 기여도를 모두 저장하기로 했다.
SpotlightAngle은 도 단위로 표시되는 스포트라이트 원뿔의 반각이다. 각도로 작업하는 것이 라디안으로 작업하는 것보다 더 직관적인 것 같다. 물론 스포트라이트의 코사인 각도와 조명 벡터를 계산해야 할 때 스포트라이트 각도는 셰이더에서 라디안으로 변환된다.
Range 변수는 빛이 도달하는 거리와 표면에 여전히 빛을 제공하는 거리를 결정한다. 물리적으로 완전히 정확하지는 않지만(실제 조명은 실제로 0에 도달하지 않는 감쇠를 가짐) 디퍼드 셰이딩 및 forward+ 렌더링 기술을 구현하려면 조명이 유한한 범위를 가져야 한다. 이 범위의 단위는 씬에 따라 다르지만 일반적으로 1단위는 1미터 사양을 준수하려고 한다. 포인트 라이트의 경우 범위는 라이트를 나타내는 구의 반경이고 스포트라이트의 경우 범위는 라이트를 나타내는 원뿔의 길이다. 디렉셔널 조명은 모든 곳에서 동일한 방향을 가리키는 무한히 멀리 있는 것으로 간주되기 때문에 범위를 사용하지 않는다.
Intensity 변수는 계산된 조명 기여도를 조절하는 데 사용된다. 기본적으로 이 값은 1이지만 일부 조명을 다른 조명보다 더 밝거나 미묘하게 만드는 데 사용할 수 있다.
장면의 조명은 Enabled 플래그로 켜고 끌 수 있다. Enabled 플래그가 false인 조명은 셰이더에서 건너뛴다.
이 데모에서 조명을 편집할 수 있다. 데모 응용 프로그램에서 조명을 클릭하여 선택할 수 있으며 속성을 수정할 수 있다. 조명이 현재 선택되어 있음을 나타내기 위해 Selected 플래그가 true로 설정된다. 씬에서 조명을 선택하면 시각적 표현이 더 어둡게(덜 투명하게) 나타나 현재 선택되었음을 나타낸다.
Type 변수는 이것이 어떤 유형의 조명인지 나타내는 데 사용된다. 다음 값 중 하나를 가질 수 있다.
commonInclude.hlsl |
#define POINT_LIGHT 0
#define SPOT_LIGHT 1
#define DIRECTIONAL_LIGHT 2
|
다시 한 번 Light 구조체는 8바이트로 명시적으로 채워져 C++의 구조체 레이아웃과 일치하고 구조체가 HLSL에 필요한 16바이트에 명시적으로 정렬되도록 한다.
조명 배열은 StructuredBuffer를 통해 액세스된다. 대부분의 조명 셰이더 구현은 상수 버퍼를 사용하여 조명 배열을 저장하지만 상수 버퍼는 크기가 64KB로 제한된다. 즉, GPU의 상수 메모리가 부족하기 전에 약 570개의 조명으로 제한되게 된다. 구조화된 버퍼는 GPU에서 사용 가능한 텍스처 메모리의 양으로 제한되는 텍스처 메모리에 저장된다(일반적으로 데스크탑 GPU의 경우 GB 범위). 텍스처 메모리는 대부분의 GPU에서 매우 빠르므로 구조화된 버퍼에 조명을 저장해도 성능에 영향을 미치지 않는다. 사실, 특정 GPU(NVIDIA GeForce GTX 680)에서 조명 어레이를 구조 버퍼로 옮겼을 때 상당한 성능 향상을 발견했다.
commonInclude.hlsl |
StructuredBuffer<Light> Lights : register( t8 );
|
Pixel Shader
먼저 매터리얼의 매터리얼 속성(Material Properties)을 수집해야 한다. 매터리얼에 다양한 컴포넌트들과 연결된 텍스처가 있는 경우 조명이 계산되기 전에 텍스처가 샘플링된다. 매터리얼 속성이 초기화된 후 씬의 모든 조명이 반복되고 조명 기여도가 누적되고 매터리얼 속성으로 변조되어 최종 픽셀 색상을 생성하게 된다.
ForwardRendering.hlsl |
[earlydepthstencil]
float4 PS_main( VertexShaderOutput IN ) : SV_TARGET
{
// Everything is in view space.
float4 eyePos = { 0, 0, 0, 1 };
Material mat = Mat;
|
함수 앞의 [earlydepthstencil] 속성은 GPU가 초기 깊이 및 스텐실 컬링을 활용해야 함을 나타낸다. 이로 인해 픽셀 셰이더가 실행되기 전에 깊이/스텐실 테스트가 수행된다. 이 속성은 SV_Depth 시멘틱을 사용하여 값을 출력하므로 픽셀의 깊이 값을 수정하는 셰이더에서는 사용할 수 없다. 이 픽셀 셰이더는 SV_TARGET 시멘틱을 사용하여 색상 값만 출력하므로 초기 깊이/스텐실 테스트를 활용하여 픽셀이 거부될 때 성능 향상을 제공할 수 있다. 대부분의 GPU는 이 속성이 없어도 초기 깊이/스텐실 테스트를 수행할 것이며 이 속성을 픽셀 셰이더에 추가해도 성능에 눈에 띄는 영향은 없었지만 어쨌든 속성을 유지하기로 결정했다.
모든 조명 계산은 뷰 공간에서 수행되므로 눈의 위치(카메라 위치)는 항상 (0, 0, 0)이다. 이것은 뷰 공간에서 작업하는 좋은 부작용이다. 카메라의 눈 위치는 셰이더에 추가 매개변수로 전달할 필요가 없다.
24행에서 매터리얼 속성에 대한 관련 텍스처가 있는 경우 해당 속성이 셰이더에서 수정되기 때문에 재료의 임시 복사본이 생성된다. 매터리얼 속성은 상수 버퍼에 저장되기 때문에 상수 버퍼 균일 변수(constant buffer uniform variable)에서 매터리얼 속성을 직접 업데이트할 수 없으므로 local temporary를 사용해야 한다.
Diffuse
우리가 읽을 첫 번째 매터리얼 속성은 디퓨즈 색상이다.
ForwardRendering.hlsl |
float4 diffuse = mat.DiffuseColor;
if ( mat.HasDiffuseTexture )
{
float4 diffuseTex = DiffuseTexture.Sample( LinearRepeatSampler, IN.texCoord );
if ( any( diffuse.rgb ) )
{
diffuse *= diffuseTex;
}
else
{
diffuse = diffuseTex;
}
}
|
Opacity
픽셀의 알파값은 다음에 의해 결정된다.
ForwardRendering.hlsl |
float alpha = diffuse.a;
if ( mat.HasOpacityTexture )
{
// If the material has an opacity texture, use that to override the diffuse alpha.
alpha = OpacityTexture.Sample( LinearRepeatSampler, IN.texCoord ).r;
}
|
기본적으로 픽셀의 투명도 값은 디퓨즈 색상의 알파 구성 요소에 의해 결정된다. 매터리얼에 불투명 텍스처가 연결되어 있으면 불투명 텍스처의 빨간색 구성요소가 알파 값으로 사용되어 디퓨즈 텍스처의 알파 값을 재정의한다. 대부분의 경우 불투명 텍스처는 Sample 메서드에서 반환된 색상의 첫 번째 구성 요소에 단일 채널만 저장하게 된다. 단일 채널 텍스처에서 읽으려면 알파 채널이 아닌 빨간색 채널에서 읽어야 한다. 단일 채널 텍스처의 알파 채널은 항상 1이므로 불투명도 맵(단일 채널 텍스처일 가능성이 높음)에서 알파 채널을 읽으면 필요한 값을 제공하지 않는다.
Ambient and Emissive
앰비언트 및 emissive color는 diffuse color과 유사한 방식으로 읽힌다. GlobalAmbient는 매터리얼의 GlobalAmbient 변수 값과도 결합된다.
ForwardRendering.hlsl |
float4 ambient = mat.AmbientColor;
if ( mat.HasAmbientTexture ) { float4 ambientTex = AmbientTexture.Sample( LinearRepeatSampler, IN.texCoord ); if ( any( ambient.rgb ) ) { ambient *= ambientTex; } else { ambient = ambientTex; } } // Combine the global ambient term. ambient *= mat.GlobalAmbient; float4 emissive = mat.EmissiveColor; if ( mat.HasEmissiveTexture ) { float4 emissiveTex = EmissiveTexture.Sample( LinearRepeatSampler, IN.texCoord ); if ( any( emissive.rgb ) ) { emissive *= emissiveTex; } else { emissive = emissiveTex; } } |
Specular Power
ForwardRendering.hlsl |
if ( mat.HasSpecularPowerTexture ) { mat.SpecularPower = SpecularPowerTexture.Sample( LinearRepeatSampler, IN.texCoord ).r \ * mat.SpecularScale; } |
Normals
매터리얼에 연결된 노멀맵이나 범프맵이 있는 경우 노멀 벡터를 계산하기 위해 노멀매핑 또는 범프매핑이 수행 된다. 노멀 맵이나 범프 맵 텍스처가 매터리얼과 연결되지 않은 경우 있는 노멀을 그대로 사용하게 된다.
재질에 연결된 법선 맵이나 범프 맵이 있는 경우 법선 벡터를 계산하기 위해 법선 매핑 또는 범프 매핑이 수행됩니다. 법선 맵이나 범프 맵 텍스처가 재질과 연결되지 않은 경우 입력 법선이 있는 그대로 사용됩니다.
ForwardRendering.hlsl |
// Normal mapping if ( mat.HasNormalTexture ) { // For scenes with normal mapping, I don't have to invert the binormal. float3x3 TBN = float3x3( normalize( IN.tangentVS ), normalize( IN.binormalVS ), normalize( IN.normalVS ) ); N = DoNormalMapping( TBN, NormalTexture, LinearRepeatSampler, IN.texCoord ); } // Bump mapping else if ( mat.HasBumpTexture ) { // For most scenes using bump mapping, I have to invert the binormal. float3x3 TBN = float3x3( normalize( IN.tangentVS ), normalize( -IN.binormalVS ), normalize( IN.normalVS ) ); N = DoBumpMapping( TBN, BumpTexture, LinearRepeatSampler, IN.texCoord, mat.BumpIntensity ); } // Just use the normal from the model. else { N = normalize( float4( IN.normalVS, 0 ) ); } |
Normal Mappings
DoNormalMapping 함수는 TBN(Tangent, Bitangent/Binormal, Normal) 행렬과 노멀맵에서 노멀 매핑을 수행한다.
ForwardRendering.hlsl |
float3 ExpandNormal( float3 n ) { return n * 2.0f - 1.0f; } float4 DoNormalMapping( float3x3 TBN, Texture2D tex, sampler s, float2 uv ) { float3 normal = tex.Sample( s, uv ).xyz; normal = ExpandNormal( normal ); // Transform normal from tangent space to view space. normal = mul( normal, TBN ); return normalize( float4( normal, 0 ) ); } |
노멀 매핑은 매우 간단하며 이전 기사 Normal Mapping에서 자세히 설명했기 때문에 여기서는 자세히 설명하지 않는다. 기본적으로 우리는 노멀 맵에서 노멀을 샘플링하고 노멀을 [-1..1] 범위로 확장하고 TBN 매트릭스를 사후 곱하여 탄젠트 공간에서 뷰공간으로 변환하기만 하면 된다.
Bump Mapping
범프 매핑은 노멀을 텍스처에 직접 저장하는 대신 범프맵 텍스처가 [0..1] 범위의 높이 값을 저장한다는 점을 제외하면 비슷한 방식으로 작동한다. U 및 V 텍스처 좌표 방향 모두에서 높이 값의 기울기를 계산하여 하이트맵에서 노멀을 생성할 수 있다. 각 방향에서 그라디언트의 외적을 취하면 텍스처 공간의 노멀이 제공되게 된다. 노멀에 TBN 행렬을 이후에 곱하면 뷰 공간에서 노멀이 제공된다. 범프 맵에서 읽은 높이 값의 크기를 조정하여 울퉁불퉁함을 더 많이(또는 덜) 강조할 수 있게된다.
ForwardRendering.hlsl |
float4 DoBumpMapping( float3x3 TBN, Texture2D tex, sampler s, float2 uv, float bumpScale ) { // Sample the heightmap at the current texture coordinate. float height = tex.Sample( s, uv ).r * bumpScale; // Sample the heightmap in the U texture coordinate direction. float heightU = tex.Sample( s, uv, int2( 1, 0 ) ).r * bumpScale; // Sample the heightmap in the V texture coordinate direction. float heightV = tex.Sample( s, uv, int2( 0, 1 ) ).r * bumpScale; float3 p = { 0, 0, height }; float3 pU = { 1, 0, heightU }; float3 pV = { 0, 1, heightV }; // normal = tangent x bitangent float3 normal = cross( normalize(pU - p), normalize(pV - p) ); // Transform normal from tangent space to view space. normal = mul( normal, TBN ); return float4( normal, 0 ); } |
100% 완전한 방법은 아님. 주의 필요 |
매터리얼에 연결된 노멀맵이나 범프 맵이 없는 경우 버텍스 셰이더 출력의 노멀 벡터가 직접 사용되게 된다. 이제 조명을 계산하는 데 필요한 모든 데이터가 모였다.
- Crytek.com, Crytek3 Downloads&rsquo;, 2015.Online. Available: http://www.crytek.com/cryengine/cryengine3/downloads. Accessed: (12- Aug- 2015) [본문으로]
- Crytek.com, Crytek3 Downloads’, 2015.Online. Available: http://www.crytek.com/cryengine/cryengine3/downloads. Accessed: (12- Aug- 2015) [본문으로]
- Graphics.cs.williams.edu, Computer Graphics Data; Meshes, 2015. [Online]. Available: http://graphics.cs.williams.edu/data/meshes.xml . [Accessed: 12- Aug- 2015]. [본문으로]
- Graphics.cs.williams.edu, Computer Graphics Data; Meshes, 2015. [Online]. Available: http://graphics.cs.williams.edu/data/meshes.xml . [Accessed: 12- Aug- 2015]. [본문으로]
'Technical Report > Graphics Tech Reports' 카테고리의 다른 글
[번역]Disney Principled BSDF (0) | 2022.03.02 |
---|---|
[번역]Terrain in Battlefield 3:A modern, complete and scalable system. GDC 2012 - 진행중 (0) | 2022.02.25 |
Mapping between HLSL and GLSL (0) | 2022.02.07 |
[번역]Occluders : Blocking the player (0) | 2022.01.12 |
[번역]Unity(및 기타 프로그램)를 위한 완벽한 노멀 맵 생성 (0) | 2022.01.07 |