본문 바로가기

C++ & etc

3dsmax 에서 HLSL 커스텀 셰이더 사용하기 (with Unreal)

언리얼 Material Editor에서 미리 짜여진 머터리얼을 HLSL로 맥스로 옮기는 과정에서 알게 된 내용들입니다.

 

목표

언리얼 엔진 출력 결과물과 최대한 비슷한 룩을 맥스에서 만들어, 아티스트가 엔진에 작업물을 올리기 전에 맥스 뷰포트에서 미리 작업 결과를 확인 및 수정이 용이하도록 하였다.

엔진에서는 머터리얼에서 그려지는 셰이딩 이외에도 루멘, 톤맵핑과 같은 여러 가지 처리가 들어가기 때문에, 완전히 같은 룩을 재현하기는 어렵다고 보았다. 아래 요소들은 맥스상에서 구현할 요소에서 제외하고, 결과물을 비교할 때는 엔진상에서 미리 꺼두었다.

  • Lumen Global Illumination
  • Lumen Reflection (프로젝트 특성상 Reflection 은 사용하지 않고 Matcap으로 대체)
  • Dynamic Shadow
  • Ambient Occlusion

 

3dsMax에서 미리 셋팅해 주어야 할 것

1. Normal Space 셋팅

언리얼 엔진은 Mikk 라는 노멀 스페이스를 사용하고 있다.

https://yeonggyoon.tistory.com/35

맥스 뷰포트에서도 이것을 사용하도록 지정해 주어야 한다. 그렇지 않으면 탄젠트와 바이탄젠트를 이용해 TBN 행렬을 만들어서 노멀맵 텍스처에 곱할 때, 언리얼과 동일한 결과가 나오지 않는다..

Normal Bump 모드를 MikkT로 바꾸어 준다. 바꾼 후에는 맥스를 재시작해야 한다.

2. Gamma Correction 셋팅

톤맵핑이 진행되는 HDR 이미지는 감마 코렉션이 적용되지 않은 Linear 색상 값이어야 하므로, 아래와 같이 감마 설정을 꺼 주었다. 최종 색상 외에도 HLSL에서 읽어들이는 텍스처들 자체에 영향을 준다.

이 설정은 맥스를 재시작하지 않아도 바로 적용된다.

감마 코렉션은 톤맵핑 이후 HLSL 에서 수동으로 진행해 주었다.

HLSL에서 샘플링한 모든 텍스처들은 Linear 로 취급되며, Albedo 텍스처 등 sRGB 공간에서 사용하고 싶은 텍스처들은 사용하기 전에 감마 코렉션을 해 주어야 했다.

 

더보기

언리얼에서의 Gamma Correction

언리얼에서 HDR 화면을 감마 코렉션 할 때는 Output device에 따라 다른 공식을 사용하는데, 각 Output Device 번호는 아래와 같다.

r.HDR.Display.OutputDevice ?

OutputDevice == 0일때는 다음과 같은 공식으로 화면을 감마코렉션 한다. (PostProcessCombineLUTs.ush)

	// sRGB, user specified gamut
	if( GetOutputDevice() == 0 )
	{		
		// Convert from sRGB to specified output gamut	
		//float3 OutputGamutColor = mul( AP1_2_Output, mul( sRGB_2_AP1, FilmColor ) );

		// FIXME: Workaround for UE-29935, pushing all colors with a 0 component to black output
		// Default parameters seem to cancel out (sRGB->XYZ->AP1->XYZ->sRGB), so should be okay for a temp fix
		float3 OutputGamutColor = FilmColor;

		// Apply conversion to sRGB (this must be an exact sRGB conversion else darks are bad).
		OutDeviceColor = LinearToSrgb( OutputGamutColor );
	}

GammaCorrectionCommon.ush

half3 LinearToSrgbBranchless(half3 lin) 
{
	lin = max(6.10352e-5, lin); // minimum positive non-denormal (fixes black problem on DX11 AMD and NV)
	return min(lin * 12.92, pow(max(lin, 0.00313067), 1.0/2.4) * 1.055 - 0.055);
	// Possible that mobile GPUs might have native pow() function?
	//return min(lin * 12.92, exp2(log2(max(lin, 0.00313067)) * (1.0/2.4) + log2(1.055)) - 0.055);
}

half LinearToSrgbBranchingChannel(half lin) 
{
	if(lin < 0.00313067) return lin * 12.92;
	return pow(lin, (1.0/2.4)) * 1.055 - 0.055;
}

half3 LinearToSrgbBranching(half3 lin) 
{
	return half3(
		LinearToSrgbBranchingChannel(lin.r),
		LinearToSrgbBranchingChannel(lin.g),
		LinearToSrgbBranchingChannel(lin.b));
}

half3 LinearToSrgb(half3 lin) 
{
#if FEATURE_LEVEL > FEATURE_LEVEL_ES3_1
	// Branching is faster than branchless on AMD on PC.
	return LinearToSrgbBranching(lin);
#else
	// Adreno devices(Nexus5) with Android 4.4.2 do not handle branching version well, so always use branchless on Mobile
	return LinearToSrgbBranchless(lin);
#endif
}

기기에 따라 감마코렉션 공식이 다르다.

언리얼에서 텍스처에 대해 이루어지는 감마 코렉션(sRGB = On 인 상태의 텍스처에 행해지는)은 에디터에서 오버라이드 할 수 있다.

텍스처 에디터에서 보이는&nbsp; Encoding Override 셋팅

여기에서 Gamma 2.2를 선택하면, 텍스처 샘플링에 pow(2.2)를 해 준 것과 같다. 또는, 아래에 보이는 Use Legacy Gamma 옵션을 사용할 수 있다.

HLSL에서 텍스처에 수동으로 감마 코렉션을 해 주어야 할 때, 언리얼에서도 같은 공식으로 맞추어 줄 수 있을 것 같다.

또는, 이미 감마 코렉션 된 상태인 영상 등을 텍스처로 출력해야 하는 경우에도 필요한 옵션인 것 같다..

 

HLSL 작성

https://headerfile.tistory.com/12

 

셰이더 공부 - 법선 매핑

셰이더를 진행하기 전에 알아야할 몇가지 수학 지식이 있습니다. 셰이더에서 mul 함수를 써서 행렬과 벡터를 곱할 때 mul(행렬, 벡터) - 행기준 행렬mul(벡터, 행렬) - 열기준 행렬 DirectX는 행우선 행

headerfile.tistory.com

널리 알려진 방법대로, Tangent / BiTangent / Normal을 버텍스 셰이더에서 각각 월드행렬을 곱해 월드공간으로 변환한 후, 픽셀 셰이더로 넘겨준다.

app에서 받아온 Tanget, BiTangent 벡터

v2f std_VS(appdata v)
{
//...
	o.T = mul(v.T, (float3x3)World).xyz;
	o.B = mul(v.B, (float3x3)World).xyz;
	return o;
}

월드행렬을 Tangent, BiTangent에 곱해 Tangent(WS), BiTangent(WS) 를 계산한다. 이것을 fragment shader로 넘긴다.

 

넘겨받은 3가지 벡터로 float3x3 행렬을 구성한다. 이것이 TBN 행렬이고, 이것의 역행렬(TBN 행렬은 단위행렬이므로, 행렬을 transpose 해서 역행렬을 구한다)

이후 픽셀 셰이더에서 샘플링한 노멀 텍스처를 [-1 ~ 1] 범위로 확장하고, transpose(TBN) 행렬에 곱해주면, 탄젠트 스페이스에 있던 노멀 텍스처를 월드 공간으로 가져올 수 있다.

 

float3 MF_Normal(float2 UV, float3 Normal, float3 TangentWS, float3 BiNormalWS)
{
	float3 Result = Normal;
	float3 TangentNormal = g_NormalTexture.Sample(g_NormalSampler, UV).xyz;
	
	TangentNormal = (TangentNormal * 2) - 1;

	float3x3 TBN = float3x3(TangentWS, BiNormalWS, Normal);
	TBN = transpose(TBN);
    
	Result = normalize(mul(TBN, TangentNormal));

	return Result; 
}
  • 검색하다가 알게 된 것 : 노멀과 라이트 방향을 탄젠트 공간으로 가져와서 노멀 맵과의 반응을 버텍스 셰이더에서 미리 계산해서 오는 방법도 있는 것 같다. 효율 측면에서 더 낫다고 하는데, 나의 경우는 픽셀 셰이더에서 월드 공간으로 변환된 노멀 텍스처를 월드 공간에 있는 라이트 각도. 카메라 각도와 계산해야 하는 Diffuse 계산, RIm 효과 등에도 활용해야 했기 때문에 이 방법을 사용할 수 없었다.
    https://bbtarzan12.github.io/Noraml-Mapping/
 

Normal Mapping 과 TBN 행렬

World 공간에서 Tangent 공간으로!

bbtarzan12.github.io

3dsmax에서 지원하는 HLSL 시맨틱과 기타 문법은 아래 문서에 잘 정리되어 있다.

https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=Max_Developer_Help_3ds_max_sdk_features_rendering_programming_hardware_shaders_shader_semantics_and_annotations_supported_hlsl_shader_semantics_html 

 

Help

 

help.autodesk.com

https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=Max_Developer_Help_3ds_max_sdk_features_rendering_programming_hardware_shaders_shader_semantics_and_annotations_supported_hlsl_shader_annotation_html 

 

Help

 

help.autodesk.com

C:\Program Files\Autodesk\3ds Max 2021\maps\fx 에서 아주 기본적인 구성의 Fx파일 프리셋들을 살펴볼 수 있다.

테스트용 혹은 연습용으로 작성할 때는 여기서부터 출발해도 좋을 것 같다.

 

 

'C++ & etc' 카테고리의 다른 글

Maxscript / DirectX Shader for 3dsMax  (2) 2023.10.08
VS Code에서 C++ 컴파일 환경 만들기  (1) 2023.10.08