Graphics Programming
렌더링 일지 - 머티리얼 셰이더 어셈블러 본문
토이 렌더링 엔진의 머티리얼 시스템을 개편했다. 최근에 리팩토링보다는 렌더링 퀄리티에 집중한다고 결심했지만, 기술 부채가 계속 쌓이면 감당이 안 될 것 같아서 이걸 먼저 처리했다.
기존에는 머티리얼 타입마다 완전한 셰이더 파일(그 자체로 온전히 컴파일 가능한 코드)이 있었고, 그에 상응하는 셰이더 프로그램, 또 그걸 래핑하는 렌더 패스 클래스가 있었다. 예를 들면 albedo, normal, roughness, metallic 등의 PBR 텍스처들을 샘플링하는 머티리얼을 위해 다음 요소들이 필요했다.
- Vertex shader "deferred_pack_pbr_texture_vs.glsl"
- Fragment shader "deferred_pack_pbr_texture_fs.glsl"
- class PBRTextureMaterial : public Material { ... };
- class Deferred_Pack_PBR_Texture : public Deferred_Pack_Base { ... };
이러니 머티리얼 타입을 추가할 때마다 유니폼 버퍼 셋업 등에서 중복되는 보일러플레이트 코드가 많아지고 새로운 머티리얼을 만드는게 점점 힘들어졌다.
그래서 파라미터, 셰이딩 모델, G버퍼 출력값 등 정말 필수적인 요소들만 정의해놓으면 머티리얼 셰이더 코드를 알아서 생성하는 시스템을 만들기로 했다.
아이러니하게도 내가 아주 예전에 최초에 만들었던 머티리얼 시스템이 바로 이런 자동 생성 방식이었다. self-contained 셰이더 파일은 없고 내가 필요한 파라미터들을 머티리얼 클래스를 통해 기술하면 이에 기반해 셰이더 코드를 조립한다.
src << "#version 430 core" << std::endl;
if (usePosition)
src << "layout (location = " << positionLocation << ") in vec3 position;" << std::endl;
if (useNormal)
src << "layout (location = " << normalLocation << ") in vec3 normal;" << std::endl;
if (useUV)
src << "layout (location = " << uvLocation << ") in vec2 uv;" << std::endl;
if (useTangent)
src << "layout (location = " << tangentLocation << ") in vec3 tangent;" << std::endl;
if (useBitangent)
src << "layout (location = " << bitangentLocation << ") in vec3 bitangent;" << std::endl;
if (useVarying() || outVars.size() > 0) {
src << "out VS_OUT {" << std::endl;
for (auto it = outVars.begin(); it != outVars.end(); it++) {
src << " " << it->getDecl() << ";\n";
}
src << "} vs_out;" << std::endl;
}
src << "uniform mat4 modelTransform;" << std::endl;
src << "uniform mat4 mvpTransform;" << std::endl;
for (auto it = uniforms.begin(); it != uniforms.end(); it++) {
src << "uniform " << it->getDecl() << ";\n";
}
최초에 만들었던 머티리얼 시스템의 흔적. 우욱...
예전에 만든 것은 짬밥이 모자라서 구현이 지저분하고 유지보수하기 어려웠지만, 머티리얼 저작에 한해서는 유연하기는 했었다. 하지만 셰이더 종류는 계속 늘어가는데 문자열 조립으로 구현된 이 시스템을 확장하는게 셰이더 코드를 직접 작성하는 것보다 너무 번거로워서 항상 완전한 셰이더 파일을 만드는 방향으로 선회했었다.
// #note: Should match with definitions in direct_lighting.glsl.
enum class ELightSourceType : uint32 {
Directional = 0,
Point = 1,
Rect = 2
};
template<typename LightProxy>
struct UBO_DirectLighting {
static_assert(
std::is_same<LightProxy, DirectionalLightProxy>::value
|| std::is_same<LightProxy, PointLightProxy>::value
|| std::is_same<LightProxy, RectLightProxy>::value,
"Not supported LightProxy type");
static const uint32 BINDING_SLOT = 1;
uint32 enableShadowing;
uint32 haveShadowMap;
uint32 omniShadowMapIndex;
uint32 _padding0;
LightProxy lightParameters;
};
class DirectLightingVS : public ShaderStage {
public:
DirectLightingVS() : ShaderStage(GL_VERTEX_SHADER, "DirectLightingVS")
{
setFilepath("fullscreen_quad.glsl");
}
};
template<ELightSourceType LightSourceType>
class DirectLightingFS : public ShaderStage {
public:
DirectLightingFS() : ShaderStage(GL_FRAGMENT_SHADER, "DirectLightingFS")
{
addDefine("LIGHT_SOURCE_TYPE", (uint32)LightSourceType);
setFilepath("direct_lighting.glsl");
}
};
DEFINE_SHADER_PROGRAM2(Program_DirectLighting_Directional, DirectLightingVS, DirectLightingFS<ELightSourceType::Directional>);
DEFINE_SHADER_PROGRAM2(Program_DirectLighting_Point, DirectLightingVS, DirectLightingFS<ELightSourceType::Point>);
DEFINE_SHADER_PROGRAM2(Program_DirectLighting_Rect, DirectLightingVS, DirectLightingFS<ELightSourceType::Rect>);
언리얼 엔진의 글로벌 셰이더 정의하는 포맷을 참고하여 구현은 내 맘대로 한, 지금 쓰고 있는 셰이더 시스템.
이번에 리팩토링하기 전에는 머티리얼 셰이더들도 이런 식으로 정의되어 있었다.
이 포맷은 앞으로도 풀스크린 패스 등 자동 생성할 필요가 없는 셰이더들에 계속 사용할 것이다.
코드베이스가 비대해지다보니 그것도 귀찮아서 또다시 머티리얼 시스템을 갈아엎게 되었다. 예전에 처음 기획한 방향으로 ...
이번에 만든 것은 언리얼 엔진의 머티리얼 셰이더 저작 시스템에서 머티리얼 에디터 부분을 셰이더 파일 하드 코딩으로 대체한 버전에 가깝다. 언리얼 엔진에는 고정된 머티리얼 템플릿이 있고, 에디터에서 노드 기반 시스템을 이용해 머티리얼을 정의하면 그걸 셰이더 코드로 조립하여 템플릿 내 플레이스홀더들을 치환해 머티리얼 셰이더 코드를 생성한다. 내가 그런 에디터를 위한 GUI와 노드 기반 시스템까지 만들 시간도 없고 그런 것에 흥미도 없어서 -_-;; 에디터 파트는 머티리얼 파일을 직접 작성하는 것으로 대체했다.
새 머티리얼 셰이더 시스템에서 머티리얼 템플릿은 일종의 우버 셰이더로서 기능한다. 빠진 부분들만 채워넣으면 그 자체로 완전한 셰이더 파일이 되어서, 셰이더 컴파일러가 컴파일할 수 있는 형태가 된다. 머티리얼 셰이더 어셈블러는 머티리얼 파일을 읽어서 빠진 부분들에 해당하는 요소를 채워넣는다. 다음 코드는 머티리얼 템플릿을 요약한 것이다.
#version 460 core
// --------------------------------------------------------
// Common
#include "common.glsl"
#include "deferred_common.glsl"
#define UBO_BINDING_OBJECT 1
#define UBO_BINDING_MATERIAL 2
#define UBO_BINDING_LIGHTINFO 3
// VERTEX_SHADER or FRAGMENT_SHADER
$NEED SHADERSTAGE
// Should be one of MATERIAL_SHADINGMODEL_XXX in common.glsl.
$NEED SHADINGMODEL
#define FORWARD_SHADING (SHADINGMODEL == MATERIAL_SHADINGMODEL_TRANSLUCENT)
// #todo: Remove unnecessary members
layout (std140, binding = UBO_BINDING_OBJECT) uniform UBO_PerObject {
// 64 bytes (4 * 16)
mat4 modelTransform;
// 64 bytes (4 * 16)
mat4 mvTransform;
// 48 bytes (3 * 16)
mat3 mvTransform3x3;
} uboPerObject;
// The assembler will generate a UBO from PARAMETER_CONSTANT definitions.
$NEED UBO_Material
#if FORWARD_SHADING
#define MAX_DIRECTIONAL_LIGHTS 2
#define MAX_POINT_LIGHTS 8
layout (std140, binding = UBO_BINDING_LIGHTINFO) uniform UBO_LightInfo {
ivec4 numLightSources; // (directional, point, ?, ?)
DirectionalLight directionalLights[MAX_DIRECTIONAL_LIGHTS];
PointLight pointLights[MAX_POINT_LIGHTS];
} uboLight;
#endif
// Texture parameters
$NEED TEXTURE_PARAMETERS
struct MaterialAttributes_Unlit { ... };
struct MaterialAttributes_DefaultLit { ... };
struct MaterialAttributes_Translucent { ... };
#if SHADINGMODEL == MATERIAL_SHADINGMODEL_UNLIT
#define MaterialAttributes MaterialAttributes_Unlit
#elif SHADINGMODEL == MATERIAL_SHADINGMODEL_DEFAULTLIT
#define MaterialAttributes MaterialAttributes_DefaultLit
#elif SHADINGMODEL == MATERIAL_SHADINGMODEL_TRANSLUCENT
#define MaterialAttributes MaterialAttributes_Translucent
#else
#error "Invalid SHADINGMODEL. See common.glsl"
#endif
// Interpolants (VS to PS)
#if VERTEX_SHADER
#define INTERPOLANTS_QUALIFIER out
#elif FRAGMENT_SHADER
#define INTERPOLANTS_QUALIFIER in
#endif
INTERPOLANTS_QUALIFIER Interpolants {
// #todo-material-assembler: Optimize memory bandwidth
vec3 positionVS; // view space
vec3 position; // local space
vec3 normal; // local space
// ...
} interpolants;
#if FORWARD_SHADING
#include "brdf.glsl"
#endif
// Controls world position offset.
$NEED getVertexPositionOffset
// Most important output of material shaders.
$NEED getMaterialAttributes
// Forward shading only.
$NEED getSceneColor
// --------------------------------------------------------
// Vertex shader
#if VERTEX_SHADER
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec2 inTexcoord;
layout (location = 2) in vec3 inNormal;
// ...
void main() {
// ...
interpolants.positionVS = positionVS.xyz;
interpolants.position = inPosition;
interpolants.normal = inNormal;
interpolants.tangent = inTangent;
interpolants.bitangent = inBitangent;
interpolants.texcoord = inTexcoord;
gl_Position = proj * positionVS;
}
#endif // VERTEX_SHADER
// --------------------------------------------------------
// Fragment shader
#if FRAGMENT_SHADER
#if !FORWARD_SHADING
#include "deferred_common_fs.glsl"
#else
// #todo: Wrap with forward_common.glsl?
layout (location = 0) out vec4 outSceneColor;
#endif
void main() {
MaterialAttributes attr = getMaterialAttributes();
#if SHADINGMODEL == MATERIAL_SHADINGMODEL_UNLIT
packGBuffer(
attr.color, // Unlit color into albedo
vec3(0.0),
SHADINGMODEL,
interpolants.positionVS,
0.0,
0.0,
1.0,
vec3(0.0));
#endif
#if SHADINGMODEL == MATERIAL_SHADINGMODEL_DEFAULTLIT
packGBuffer(
attr.albedo,
detailNormal,
SHADINGMODEL,
interpolants.positionVS,
attr.metallic,
attr.roughness,
attr.localAO,
attr.emissive);
#endif
#if SHADINGMODEL == MATERIAL_SHADINGMODEL_TRANSLUCENT
outSceneColor = getSceneColor(attr);
#endif
}
#endif // FRAGMENT_SHADER
그리고 pbr_texture 머티리얼은 이렇게 정의된다.
// Simple DefaultLit material that samples PBR textures for each material attribute.
// Also supports fallback constant parametes for absent textures.
#define SHADINGMODEL MATERIAL_SHADINGMODEL_DEFAULTLIT
PARAMETER_CONSTANT(vec3, albedoOverride)
PARAMETER_CONSTANT(vec3, normalOverride)
PARAMETER_CONSTANT(float, metallicOverride)
PARAMETER_CONSTANT(float, roughnessOverride)
PARAMETER_CONSTANT(float, localAOOverride)
PARAMETER_CONSTANT(bool, bOverrideAlbedo)
PARAMETER_CONSTANT(bool, bOverrideNormal)
PARAMETER_CONSTANT(bool, bOverrideMetallic)
PARAMETER_CONSTANT(bool, bOverrideRoughness)
PARAMETER_CONSTANT(bool, bOverrideLocalAO)
PARAMETER_CONSTANT(bool, bHasOpacity)
PARAMETER_CONSTANT(vec3, emissiveConstant)
PARAMETER_TEXTURE(0, sampler2D, albedo)
PARAMETER_TEXTURE(1, sampler2D, normal)
PARAMETER_TEXTURE(2, sampler2D, metallic)
PARAMETER_TEXTURE(3, sampler2D, roughness)
PARAMETER_TEXTURE(4, sampler2D, localAO)
VPO_BEGIN
vec3 getVertexPositionOffset(VertexShaderInput vsi) {
return vec3(0.0);
}
VPO_END
ATTR_BEGIN
MaterialAttributes getMaterialAttributes() {
MaterialAttributes_DefaultLit attr;
vec2 uv = interpolants.texcoord;
if (uboMaterial.bOverrideAlbedo) {
attr.albedo = uboMaterial.albedoOverride;
} else {
attr.albedo = texture(albedo, uv).rgb;
}
// Deal with normal, metallic, roughness, localAO, and emissive
// ...
return attr;
}
ATTR_END
어셈블러는 머티리얼 파일의 구성요소들을 파싱하여 템플릿에 채워넣음으로써 온전한 셰이더 코드를 생성한다.
- SHADINGMODEL 정의는 템플릿의 $NEED SHADINGMODEL을 대체한다.
- PARAMETER_CONSTANT들은 어셈블러가 파싱 후 16바이트 정렬되도록 조립하여 템플릿의 $NEED UBO_Material을 대체한다.
- PARAMETER_TEXTURE들은 texture unit으로 번역되어 템플릿의 $NEED TEXTURE_PARAMETERS를 대체한다.
- 나머지 함수들도 템플릿에서 그에 상응하는 $NEED 토큰을 대체한다. 예를 들어 getMatrialAttributes()는 g버퍼 출력값을 정의한다.
머티리얼 파일 자체는 완전한 머티리얼 셰이더를 일일이 작성하는 것보다 몸집이 크게 줄었다. 그리고 엔진 시작 시 머티리얼 파일들을 모두 긁어모아서 셰이더 프로그램들을 생성하기 때문에 Material 서브클래스와 머티리얼 타입 별 렌더 패스 클래스를 모두 지워버릴 수 있었다.
특히나 고무적인 것은 유지보수하기 힘들어서 날려버렸던 포워드 셰이딩 파이프라인을 복구할 가능성이 열린 것이다. 머티리얼 템플릿은 이미 디퍼드/포워드를 모두 지원하도록 정의되어 있고, 단지 라이팅 계산 코드를 아직 정리하지 못해서 디퍼드 라이팅은 g버퍼를 읽어서 라이팅 계산하는 렌더 패스들에, 포워드 라이팅은 유일한 반투명 머티리얼인 translucent_color에 하드코딩되어 있다. 셰이더 코드를 좀 더 정리하고 나면 불투명 머티리얼들도 포워드 셰이딩 버전으로 컴파일할 수 있게 될 것이다.
P.S. 이걸 구현할 때 Ryzen 6800U 노트북을 활용했는데 (정확히 말하면 iGPU가 RX 680M인) 알 수 없는 셰이더 버그를 두어 번 겪었다. 셰이더 컴파일 단계에서 에러도 없고, RenderDoc으로 캡처해봐도 에러 리포트가 없는데 셰이더 실행 결과가 이상했다. 한참 씨름하다가 데스크탑에서 엔비디아 그래픽카드로 돌려보면 정상 실행 -_- 리사 수님 그래픽 드라이버 팀 좀 닦달해주세요...