TECHARTNOMAD TECHARTFLOW.IO

TECH.ART.FLOW.IO

[번역]원신 셰이더 렌더링 복원 해석

jplee 2023. 11. 22. 19:30
역자의 말.
오랫동안 위쳇에서 구독 하던 블로거의 글을 간단히 번역하여 공유 해 보기로 했습니다. 옛 넷이즈의 동료이자 10년 이상 렌더링 분야에 꾸준히 학습하며 공식 계정을 통해 기사를 올리는 GameDevLearning 이라는 체널에 올라왔던 글입니다. 2020년 부터 유독 서브컬처 장르 게임들이 대거 등장하기 시작했으니 우리도 이런 분석 기사를 통해 다시 유추 해 보고 구현 해 보는 계기가 되길 바랍니다. 

서문.
저는 오랫동안 원신 셰이더를 분석 해 왔습니다. 최근 이펙트가 어느 정도 만족스러워져서 오늘은 분석에 대한 제 개인적인 이해에 대해 몇 마디 말씀드리고자 합니다.

셰이더 분석
우선 셰이더 분석에 대해 이야기 해 봅시다. 저는 개인적으로 전체 셰이더에 여러 부분이 포함되어 있음을 이해합니다.

위 이미지 중의 자발광은 자체발광 입니다. Self illumination 을 중국어로 스스로 자 를 써서 자발광 그리고 중국 병음으로는 즈발광 이라고 읽어지구요.

위의 그림에서 볼 수 있듯이 원신의 셰이더 프로세스에 대한 개인적인 이해 요소는 너무 많으며, 여기서는 먼저 원신의 텍스처 몇 개를 포하고 하나씩 분석합니다!

텍스처 분석
여기서는 당분간 이 텍스처의 사용을 표현하지 않습니다. 우리는 나중에 그것에 대해 이야기 할 것입니다.

일반. 기본 색상
여기서 기본 색상은 보다 중심적인 것으로 간주됩니다. 이 원칙은 주로 DiffuseColor * RampColor를 사용하여 구현됩니다.

DiffuseColor
DiffuseColor는 사실 위의 Diffuse 맵이므로 따로 설명할 필요 없이 그냥 사용하면 됩니다.

RampColor
사용할 맵은 다음과 같습니다.

이것이 바로 DiffuseColor의 핵심입니다.

이 포스팅에는 낮과 밤에 해당하는 상위 5개와 하위 5개로 나뉜 10개의 램프가 있습니다( 제가 알기로는 원신의 캐릭터는 라이팅과 관련이 없으며, 모두 램프 매핑에 의존하여 변화를 구현합니다).

먼저 램프 범위를 분석해 보겠습니다.
이 맵은 원신의 원본 머티리얼맵 매핑의 알파 채널에 있으며, 선형으로 변환한 다음 다음과 같이 머티리얼 램프 사이의 그레이 스케일 1.0~0.0에서 컬러(사실 스노우 페더에서도 언급됨)를 흡수해야만 구분할 수 있습니다.

그레이 스케일 1.0: 피부 텍스처/헤어 텍스처(헤어 부분은 피부가 없음)
그레이 스케일 0.7: 실크/스타킹
그레이 스케일 0.5: 금속/금속 투영
그레이 스케일 0.3 : 부드러운 물체
그레이스케일 0.0 : 단단한 물체

위에서 언급한 방법은 맵을 10회 샘플링한 다음 다양한 회색조에 따라 텍스처를 차별화하는 것이지만, 사실 더 나은 해결책이 있습니다(실시간 샘플링이 5회밖에 되지 않는데도 굳이 10회 샘플링하는 것은 분명히 오버헤드라고 할수 있습니다).

일반적으로 다음과 같은 방식으로 샘플링한다는 것을 기억해 봅시다: 

tex2d(_RampMap,float2(HalfLambert,0.5));



위의 샘플링 방법에서 사용자 정의 값 0.5는 측면 샘플링을 위한 샘플링 지점에 대해 Y축 중앙의 램프 맵을 샘플링하고 싶다는 것을 의미하며, 실제로는 0.0 ~ 1.0의 값을 사용하여 점의 측면 좌표를 샘플링하고 싶다는 것을 나타내고, 다섯 개의 램프에서 맵을 샘플링하고 싶다는 것, 즉 (0.0, 0.25, 0.5, 0.5, 0.5, 0.25, 0.25, 0.5, 0.5, 0.5, 0.5, 0.25) 값을 취한다는 것을 의미합니다. 0.25,0.5,0.75,1.0)를 사용하여 아래에서 위로 5개의 램프맵을 샘플링할 수 있습니다.

주의 깊은 분들은 단지 범위의 값을 가져와야하며 위의 RampRange 매핑 값은 기본적으로 동일하므로 실제로 이 게시물을 사용하여 5 개의 램프를 한 번 샘플링 할 수 있다고 추론 할 수 있으며, 이는 조금 좋은 두뇌를 갖고 있어도 이해하지 못할 수도 있죠.

구체적인 공식은 다음과 같습니다: tex2d(_RampMap,HalfLambert,RampRangeMap).
이제 샘플링 방법을 알았으니 그의 경사로와 우리의 일반적인 경사로의 차이를 살펴보겠습니다.

이게 우리가 보통 사용하는 램프 매핑인데, 명암선이 중앙에 있습니다. 하프람베르트의 값 범위가 백라이트에서 라이트까지 0.0~1.0이기 때문에 보통 이 방법을 사용할 때는 명암선의 값을 0.5로 해서 명암선은 라이트에 수직인 선에, 라이트 선은 라이트의 중앙에 위치하도록 해야 합니다.
일반적인 내용은 끝났으니 이제 하라카미의 램프맵이 어떻게 생겼는지 살펴봅시다.

이 경사로에서 픽셀을 최대한 활용할 수 있는 이점이 있는 명암선이 오른쪽에 있다는 것을 발견했으며, 가장 오른쪽 픽셀을 넘어서는 부분은 딱딱한 가장자리를 만들어 만화적인 느낌이 나오도록 할 수 있습니다. 결국 빛에 노출된 쪽은 거의 한 가지 색상이므로 이전 솔루션은 실제로 잘못되었습니다. 그렇다면 이런 경사로를 샘플링하려면 어떻게 해야 할까요? 아주 간단합니다.

하프램버트 값을 0.0~1.0에서 0.0~0.5 -1.0으로 압축하면 됩니다. 여기서 0.5~1.0의 값은 1.0이 되고 0.0~0.5의 그라데이션만 유지한다는 의미는 다음과 같이 작성됩니다.

halfLambert = smoothstep(0.0,0.5,halfLambert);


이렇게 하면 0.5 위의 값은 1.0이 되어 그라데이션을 가질 수 있는 부분을 유지하고 1.0 부분은 경사로의 가장 오른쪽 픽셀을 균일하게 샘플링합니다.
그런 다음 주간에는 상위 5행만, 야간에는 하위 5행만 샘플링해야 하며, 이 경우 Y축의 샘플링 범위를 변경해야 합니다.

상반부 샘플링: 

tex2d(_RampMap,HalfLambert,RampRangeMap* 0.45 + 0.55);



아래쪽 절반 샘플링: 

tex2d(_RampMap,HalfLambert,RampRangeMap* 0.45);


이것은 Y축의 0.0~1.0을 각각 0.5~0.95와 0.0~0.45로 매핑합니다. 이는 픽셀 오버플로를 피하기 위해 샘플링할 때 램프의 중앙에 위치하도록 하기 위한 것입니다.
이것으로 램프 샘플링이 완료되었습니다.
사실 역광 표면과 그림자는 사실 같은 것으로, 둘 다 빛이 비추지 않는 곳이기 때문에 그림자를 직접 도입하는 것이 합리적입니다.
위의 텍스처를 보면 라이트맵 텍스처가 있는데, 실제로는 캐릭터에 고정된 그림자이기 때문에 그림자의 일부이므로 결합할 수 있습니다.

제가 한 방법은 먼저 실시간 그림자에 고정 그림자를 곱하여 모든 고정 그림자에 실시간 그림자가 생기도록 했습니다. 나중에 이 전체 그림자를 호출하기가 더 쉽고, 원본 라이트맵이 0.0~0.5이므로 곱한 후 0.0~1.0으로 *2를 곱해야 합니다.

parameter.b *= smoothstep(0.1,0.5,shadow) * 2;

 
실시간 그림자에서 0.5 이상의 값을 필터링하여 그림자 오류(라이트맵이 parameter.b에 넣는 값)를 방지했습니다.
그러면 전체 램프 컬러 샘플링 프로세스는 다음과 같습니다.

float3 NPR_Base_Ramp (float NdotL,float Night,float4 parameter)
    {
        float halfLambert = smoothstep(0.0,0.5,NdotL) * parameter.b;
        
        /* 
        Skin = 1.0
        Silk = 0.7
        Metal = 0.5
        Soft = 0.3
        Hand = 0.0
        */
            if (Night > 0.0)
            {
                return SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(halfLambert, parameter.a * 0.45 + 0.55)).rgb;//레이어드 머티리얼 맵은 0-1의 단일 맵이므로 샘플링된 UV의 Y축으로 바로 사용할 수 있습니다. 
                //그리고 낮 동안 램프 맵의 상단 절반만 샘플링해야 하므로 범위(범위 0.55 - 1.0)를 제한하기 위해 * 0.45 + 0.55를 설정했습니다.
            }
            else
            {
                return SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(halfLambert, parameter.a * 0.45)).rgb;//야간에 램프 맵의 상단 절반만 샘플링해야 하므로 범위를 제한하기 위해 * 0.45로 설정했습니다(0.5를 샘플링하면 위의 픽셀에 영향을 받습니다).
            }

    }

그런 다음 램프색 * 확산을 사용하여 전체 알베도 색상을 얻습니다.



일반. 하이라이트
하이라이트는 하이라이트와 메탈의 두 부분으로 구성됩니다.

여기서는 이 두 가지 맵을 주로 사용합니다. 광택은 주로 하이라이트의 범위를 제어하는 역할을 합니다. 스페큘러는 주로 하이라이트의 모양을 제어하는 역할을 하며, 메탈 맵에 대해서는 나중에 설명하겠습니다.

SpecularColor
하이라이트와 관련하여 여기에 사용된 주요 방법은 블린퐁의 하이라이트 계산 방법이며, 기회가 된다면 그의 소스 코드를 살펴볼 수도 있습니다.

float3 NPR_Base_Specular(float NdotL,float NdotH ,float3 normalDir,float3 baseColor,float4 parameter)//최적화된 어웨이 메탈 매핑
{
        float Ks = 0.04;
        float  SpecularPow = exp2(0.5 * parameter.r * 11.0 + 2.0);//여기에 0.5를 곱하여 하이라이트 범위를 확장합니다.
        float  SpecularNorm = (SpecularPow+8.0) / 8.0;
        float3 SpecularColor = baseColor * parameter.g;
        float SpecularContrib = baseColor * (SpecularNorm * pow(NdotH, SpecularPow));

        //하라카미의 메탈 매핑(여기서는 일종의 피팅 커브를 사용하여 시뮬레이션했습니다.)
        float MetalDir = normalize(mul(UNITY_MATRIX_V,normalDir));
        float MetalRadius = saturate(1 - MetalDir) * saturate(1 + MetalDir);
        float MetalFactor = saturate(step(0.5,MetalRadius)+0.25) * 0.5 * saturate(step(0.15,MetalRadius) + 0.25) * lerp(_MetalIntensity * 5,_MetalIntensity * 10,parameter.b);
        
        float3 MetalColor = MetalFactor * baseColor * step(0.95,parameter.r);
        return SpecularColor * (SpecularContrib  * NdotL* Ks * parameter.b + MetalColor);
    }

위 코드에서 parameter.g는 실제로 스페큘러 맵이므로 코드를 주의 깊게 분석할 수 있습니다.



MetalColor
여기서 초점은 금속 부분에 있습니다.
제가 알기로는 원래의 해결책은 광택도 맵에서 값이 1.0인 부분이 금속으로 분류되는 것이므로 여기서는 다음과 같이 했습니다.

parameter.r은 광택도 맵입니다.

float3 MetalColor = MetalFactor * baseColour * step(0.95,parameter.r);



이렇게 하면 금속 부분의 색상만 얻을 수 있습니다.
그런 다음 하이라이트에 금속 색상을 추가하여 최종 하이라이트 색상을 얻습니다.
원신의 금속 부분의 원본 맵은 이와 같으며, 매트캡 샘플링 방법을 사용하여 샘플링해야 하는 일종의 맵입니다.

위 코드의 MetalFactor 값에 디퓨즈를 곱하여 금속 색상을 얻습니다.

마찬가지로 위의 하이라이트는 디퓨즈에 곱해 하이라이트 색상을 얻는데, 환경과 반응하지 않으므로 _LightColor를 하이라이트 색상으로 사용할 수 없습니다.

그러다 다시 생각해보니 이 포스팅은 너무 간단해서 코드를 사용하면 어떨까 해서 위 코드를 작성했습니다.

float MetalDir = normalize(mul(UNITY_MATRIX_V,normalDir));

float MetalRadius = saturate(1 - MetalDir) * saturate(1 + MetalDir);

float MetalFactor = saturate(step(0.5,MetalRadius)+0.25) * 0.5 * saturate(step(0.15,MetalRadius) + 0.25);



이 부분은이 게시물을 시뮬레이션하는 것입니다. 원칙은 UV가 스텝의 왼쪽과 오른쪽 부분이 될 때 매트 캡 샘플링을 사용하고 일부 값의 0.0 양쪽에 중간 1.0이있을 때 매트 캡 샘플링을 사용한 다음 메탈 맵 복원의 효과를 극대화하기 위해 그레이 스케일 처리를 수행하는 것입니다.

후자의 * lerp(_MetalIntensity * 5,_MetalIntensity * 10,parameter.b); 는 하이라이트 응답을 향상시키기 위해 (매개변수가 적을수록 좋다는 규칙의 정신에 따라) 제가 만든 타협점입니다.

Ks도 SD 코드에 있으므로 알고리즘의 용도를 알 수 없습니다.
일반. 림라이팅.
림 라이팅에 대해서는 자세히 설명하지 않겠습니다만, 하라 카미의 원래 림 라이팅 방식은 뎁스 림 라이팅입니다. 
자세한 내용은 유키하루 님이 올려 주신 링크를 참조해 주시고, 여기에도 게재하겠습니다.

그럼 곧바로 프레넬의 림라이팅 방법으로 넘어가겠습니다.

float3 NPR_Base_RimLight(float NdotV,float NdotL,float3 baseColor)
    {
        return (1 - smoothstep(_RimRadius,_RimRadius + 0.03,NdotV)) * _RimIntensity * (1 - (NdotL * 0.5 + 0.5 )) * baseColor;
    }

 
일반. 자체 조명
자체 조명은 자체 조명 마스크에 Diffuse를 곱하고 강도를 부여하는 간단한 문제입니다.
그리고 원신의 캐릭터에는 깜박이는 자체 발광 효과가 있어서 그냥 추가했습니다.

float3 NPR_Emission(float4 baseColor)
{
    return baseColor.a * baseColor * _EmissionIntensity * abs((frac(_Time.y * 0.5) - 0.5) * 2);
}

 
최종합성
위 가이드의 일반 부분에 대한 조건을 모았으므로 최종 출력은 다음과 같습니다.

사실 위의 모든 조건을 더하기만 하면 됩니다. 하지만 블린퐁의 보존을 준수하기 위해 금속성이 강할수록 디퓨즈가 어두워지므로 여기서 특별한 방법으로 처리해야 합니다(아래 코드를 참조하세요).

float3 NPR_Function_Base (float NdotL,float NdotH,float NdotV,float3 normalDir,float4 baseColor,float4 parameter,Light light,float Night)
{
    float3 RampColor = NPR_Base_Ramp (NdotL,Night,parameter);
    float3 Albedo = baseColor * RampColor;
    float3 Specular = NPR_Base_Specular(NdotL,NdotH,normalDir,baseColor,parameter);
    float3 RimLight = NPR_Base_RimLight(NdotV,NdotL,baseColor) * parameter.b;
    float3 Emission = NPR_Emission(baseColor);
    float3 finalRGB = Albedo* (1 - step(0.95,parameter.r)) + Specular + RimLight + Emission;
    return finalRGB;
}

이제 바디 셰이더를 완성했습니다.


헤어 하이라이트.
그렇다면 왜 머리카락 부분을 따로 떼어낸 건가요?
헤어 하이라이트는 별도로 처리해야 하기 때문입니다.
헤어 매핑에는 머리카락뿐만 아니라 금속 및 기타 헤어 밴드 등이 포함되어 있기 때문에 실제로는 위의 일반 셰이더 부분에 적용되는 것이 일반적이기 때문에 헤어 부분에 하이라이트를 별도로 추가해야 합니다.

이것은 머리카락의 광택도 맵입니다. 사실 검은색 부분이 PS에 의해 검게 처리되었는지 아니면 검은색인지 잊어버렸기 때문에 그냥 검은색이라고 가정하겠습니다.

검은색 부분은 실제로는 모두 머리카락이므로 그 부분만 골라서 따로 강조하면 됩니다.

float HariSpecRadius = 0.25;//여기에서 머리카락의 반사 범위를 제어할 수 있습니다.
float HariSpecDir = normalize(mul(UNITY_MATRIX_V,normalDir)) * 0.5 + 0.5;
float3 HariSpecular = smoothstep(HariSpecRadius,HariSpecRadius + 0.1,1 - HariSpecDir) * smoothstep(HariSpecRadius,HariSpecRadius + 0.1,HariSpecDir) *NdotL;//화면 공간 노멀 활용

헤어 하이라이트의 전체 알고리즘은 다음과 같습니다.

화면 공간 노멀을 사용한 다음 왼쪽과 오른쪽 끝을 스무스 스텝으로 처리하여 왼쪽과 오른쪽은 검정색, 가운데는 흰색으로 만들어 중간 부분을 따로 강조하여 따로 강조하려는 목표를 달성할 수 있도록 했습니다.

NdotH를 사용하여 계산하지 않는 이유는 주로 머리의 NdotH가 원으로되어 있기 때문에 머리카락을 내려다 보면 하이라이트가 잘린 것을 알 수 있는데 이는 분명히 내가 원하는 것이 아니므로 여기서는 다른 접근 방식을 사용하여 하이라이트가 항상 우리를 향하도록하여 최상의 시각적 효과를 보여줍니다.

그래서 헤어 파트는 헤어 하이라이트 + 일반입니다. 하이라이트를 사용하여 최종 하이라이트를 얻으므로 최종 코드는 다음과 같습니다.

float3 NPR_Function_Hair (float NdotL,float NdotH,float NdotV,float3 normalDir,float3 viewDir,float3 baseColor,float4 parameter,Light light,float Night)
{

    float3 RampColor = NPR_Base_Ramp (NdotL,Night,parameter);
    float3 Albedo = baseColor * RampColor;

    float HariSpecRadius = 0.25;//여기에서 머리카락의 반사 범위를 제어할 수 있습니다.
    float HariSpecDir = normalize(mul(UNITY_MATRIX_V,normalDir)) * 0.5 + 0.5;
    float3 HariSpecular = smoothstep(HariSpecRadius,HariSpecRadius + 0.1,1 - HariSpecDir) * smoothstep(HariSpecRadius,HariSpecRadius + 0.1,HariSpecDir) *NdotL;//화면 공간 노멀 활용 


    float3 Specular = NPR_Base_Specular(NdotL,NdotH,normalDir,baseColor,parameter) + HariSpecular * _HairSpecularIntensity * 10 * parameter.g * step(parameter.r,0.1);
    // float3 Metal =  NPR_Base_Metal(normalDir,parameter,baseColor);
    float3 RimLight = NPR_Base_RimLight(NdotV,NdotL,baseColor);
    float3 finalRGB = Albedo* (1 - parameter.r) + Specular  + RimLight;
    return finalRGB;
}

사실 위의 메탈 매핑 핏 커브와 같은 방식으로 헤어 범위를 제어하도록 코드를 변경할 수 있지만, 이 글을 쓰는 시점에서는 변경하지 않았으므로 다음과 같이 시작하겠습니다.

얼굴.
얼굴의 그림자를 제어하는 방법은 다른 포스트에서 설명한 후 여기서 다시 설명하겠습니다.

월드: 유니티 셰이더 "원신" 얼굴 부드러운 그림자 솔루션 아이디어.

엮인 글은 이곳에... https://techartnomad.tistory.com/124

 

[번역/정리]만화 얼굴 그림자 매핑 생성 렌더링 원리

역자의 말. 일단 원본 링크 기억이 안납니다. -_-; 오래전에 노션에 한글로 정리 해 논 것만 남아 있네요. Zhihu 에서 발췌 된 내용입니다. 아무튼... 저번에 번역해서 공유 했던 원신 렌더링 리버스

techartnomad.tistory.com

요점은 다음과 같습니다. 얼굴은 그림자이기 때문에 위와 동일하므로 램프 맵을 샘플링하는 데 사용하고 얼굴을 약간 조정 한 다음 R 및 G 채널의 얼굴 그림자 맵을 좌우로 뒤집어 잘못된 그림자의 검은 색 위치에 나타나는 얼굴 왼쪽의 빛의 방향을 해결하기 위해 다음과 같이 사용할 것입니다.

코드는 다음과 같습니다.

float3 NPR_Function_face (float NdotL,float4 baseColor,float4 parameter,Light light,float Night)
{
    float3 Up = float3(0.0,1.0,0.0);
    float3 Front = unity_ObjectToWorld._12_22_32;
    float3 Right = cross(Up,Front);
    float switchShadow  = dot(normalize(Right.xz), normalize(light.direction.xz)) < 0;
    float FaceShadow = lerp(1 - parameter.g,1 - parameter.r,switchShadow.r); //밝게 만들어야 하므로 여기서 듀얼 채널로 섀도 매핑을 반전시키는 것이 중요합니다.
    float FaceShadowRange = dot(normalize(Front.xz), normalize(light.direction.xz));
    float lightAttenuation = 1 - smoothstep(FaceShadowRange - 0.05,FaceShadowRange + 0.05,FaceShadow);


    float3 rampColor = NPR_Base_Ramp(lightAttenuation * light.shadowAttenuation,Night,parameter);//여기서 얼굴 파라미터 매핑의 알파는 1이어야 합니다.
    return baseColor.rgb * rampColor ;
}

여기서 중요한 점은 얼굴이 피부여야 하므로 RampRange 값이 1.0이어야 한다는 것입니다.
얼굴에는 고정된 그림자가 없으며 실제로 그림자를 직접 그렸기 때문에 여분의 그림자를 곱했습니다.
자, 이제 상황에 따라 베이스, 얼굴, 하리의 세 가지 최종 출력을 사용하는 매크로를 작성할 수 있습니다.

스트로크.
스트로크에 대해서는 아무 말도 하지 않겠습니다. 일반적인 더블 패스 스트로크일 뿐이며 그보다 더 좋은 것은 없으며 모든 사람에게 동일합니다.

도구
램프 맵을 생성하는 또 다른 방법은 지식에 대한 게시물에서 "Little Genius"와 함께 램프 생성기 툴을 공동 개발한 것입니다.
리틀 지니어스: Unity 툴 - 오프라인으로 램프 맵 만들기
그리고 얼굴 SDF 생성 툴인 오렌지 캣의 툴을 포스팅하겠습니다.
오렌지 캣: 하이브리드 카툰 라이트맵을 빠르게 생성하는 방법

비디오 효과 미리보기
테크니컬 아트 데모 - 오리지널 갓 캐릭터 렌더링(최적화된 리마스터 버전)_beili_bilibili

 

技术美术Demo - 原神角色渲染(优化重制版)_哔哩哔哩_bilibili

相比于之前的版本,优化了Ramp的色彩,后效,以及角色的高光反应(视频内的高光被我刻意调曝了) 大事件!!! 解析来了!! https://zhuanlan.zhihu.com/p/435005339

www.bilibili.com

이것으로 모두 끝입니다, 더 많은 좋아요와 댓글로 응원해주시길 바랍니다 매일 주말 잘 보내세요 !!!!!


면책 조항
이 기사의 리소스와 스티커는 모두 네트워크에서 제공 한 것이며, 침해가 있으면 즉시 삭제하십시오! 학습 교환 용도로만 사용하고 상업적으로 사용하지 마십시오!