TECH.ART.FLOW.IO

[번역] 피부 렌더링 연구 기록

jplee 2024. 4. 19. 13:51

토픽 저자 : 판스칭 ( 范世青 )

효과 분해

  1. 듀얼 로브 PBR
  2. 간접 조명, AO 보정
  3. 사전 통합된 SSS 기반 스킨, 일반 + 카메라 공간
  4. 피부 위의 소프트 쉐이딩은 주로 모바일 SSSM이 켜지지 않은 상태에서 ShadowMap을 기반으로 한 간편한 PCF 쉐이딩
  5. 옵션, 테두리
  6. 옵션, 측면 조명
  7. 옵션, 보조 조명
  8. ToneMapping
  9. 화면 공간의 표면 아래 산란 효과에 기반한 SSSSS 계산.

1 더블베이스 PBR

기본 BRDF 강조 공식, 쿡-토런스, UE 소스 코드에서 파생된 코드

// GGX / Trowbridge-Reitz
// [Walter et al. 2007, "Microfacet models for refraction through rough surfaces"]
float D_GGX( float a2, float NoH )
{
    float d = ( NoH * a2 - NoH ) * NoH + 1; // 2 mad
    return a2 / ( UNITY_PI*d*d );                   // 4 mul, 1 rcp
}
 
// Smith term for GGX
// [Smith 1967, "Geometrical shadowing of a random rough surface"]
float Vis_Smith( float a2, float NoV, float NoL )
{
    float Vis_SmithV = NoV + sqrt( NoV * (NoV - NoV * a2) + a2 );
    float Vis_SmithL = NoL + sqrt( NoL * (NoL - NoL * a2) + a2 );
    return rcp( Vis_SmithV * Vis_SmithL );
}
 
// [Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"]
float3 F_Schlick( float3 SpecularColor, float VoH )
{
    float Fc = Pow5( 1 - VoH );                 // 1 sub, 3 mul
    return Fc + (1 - Fc) * SpecularColor;       // 1 add, 3 mad
     
    // Anything less than 2% is physically impossible and is instead considered to be shadowing
    //return saturate( 50.0 * SpecularColor.g ) * Fc + (1 - Fc) * SpecularColor;
}
 
half3 DirectBRDFSpecularSmith(float roughness, float3 specularColor, float3 normalWS, half3 lightDirectionWS, float3 viewDirectionWS)
{
    float3 lightDirectionWSFloat3 = float3(lightDirectionWS);
    float3 halfDir = SafeNormalize(lightDirectionWSFloat3 + viewDirectionWS);
 
    float NoH = saturate(dot(float3(normalWS), halfDir));
    half LoH = half(saturate(dot(lightDirectionWSFloat3, halfDir)));
    half NoL = saturate(dot(normalWS, lightDirectionWS));
    half NoV = saturate(dot(normalWS, viewDirectionWS));
 
    float D = D_GGX(roughness * roughness * roughness * roughness, NoH);
    float V = Vis_Smith(roughness * roughness, NoV, NoL);
    float3 F = F_Schlick(specularColor, LoH);   //LoH == VoH
 
    float3 result = D * V * F * NoL;
    return result;
}

피부 본체 구조의 첫 번째 레이어, PBR 효과가 한 번 계산됨
 

두 번째 피부 매끄러움 층이 상쇄되고 PBR 효과가 한 번 더 계산됩니다.

두 개의 레이어가 겹쳐져 피부의 디테일한 하이라이트를 시뮬레이션하고 두 번째 레이어는 그림자와 고유 색상을 오버레이한 후 하이라이트 색상입니다.

위의 두 결과가 중첩되어 있습니다.

그림자 및 고유 색상 오버레이 후 색상 강조하기

2 간접광 계산

사용자 지정 SH + 보정을 사용한 AO 계산, AO 색상이 빨간색으로 전환됨

float3 aoColor = lerp(_SkinAOColor, 1, ao);
aoColor = saturate(lerp(1, aoColor, _SSSSpecParam4));
 
half3 indirectDiffuse = ShadeSH9_New(half4(normalWorld1, 1));
indirectDiffuse = indirectDiffuse * aoColor;

 

3 선적분 SSS 스킨

원리 참고:
手机端皮肤渲染(4) - 知乎 (zhihu.com)

安全验证 - 知乎

知乎,中文互联网高质量的问答社区和创作者聚集的原创内容平台,于 2011 年 1 月正式上线,以「让人们更好的分享知识、经验和见解,找到自己的解答」为品牌使命。知乎凭借认真、专业、友

www.zhihu.com

NVIDIAGameWorks/FaceWorks:用于高质量皮肤和眼睛渲染的中间件库和示例应用程序 (github.com)

跳转中...

link.zhihu.com

FaceWorks/doc/slides/FaceWorks-Overview-GTC14.pdf at master · NVIDIAGameWorks/FaceWorks (github.com)

跳转中...

link.zhihu.com

핵심 알고리즘, 기본 사항
 

float NoL = dot(mainLight.dir, normalWorld1);
float saturateNoL = saturate(NoL);
 
float ModelNoL = dot(mainLight.dir, normal);
 
float preintegratedUVX = clamp(NoL * _SSSGameDiffParam14 + _SSSCurveParam, 0.01, 0.99);
float preintegratedUVY = clamp(1 - paramTex.x * _SSSIntensity, 0.01, 0.99);
float3 preintegratedValue = tex2D(_PreintegratedTex, float2(preintegratedUVX, preintegratedUVY));
 
float normalSmoothFactor = _NormalSmoothFactor * 0.7 + 0.3;
float3 sssNormal1 = lerp(normalWorld1, normal, normalSmoothFactor);
sssNormal1 = normalize(sssNormal1);
 
float3 sssNormal2 = lerp(normalWorld1, normal, _NormalSmoothFactor);
sssNormal2 = normalize(sssNormal2);
 
float sssNormal1oL = saturate(dot(sssNormal1, mainLight.dir));
float sssNorma21oL = saturate(dot(sssNormal2, mainLight.dir));
float modelNoL = saturate(ModelNoL);
 
float _CurvatureTex_LUT_UVX = ModelNoL * 0.5 + 0.5;
float _CurvatureTex_LUT_UVY = paramTex2.x;
float3 curvatureValue = tex2D(_CurvatureTex_LUT, float2(_CurvatureTex_LUT_UVX, _CurvatureTex_LUT_UVY));
 
float3 tempCurvatureValue = saturate(curvatureValue * 0.5 + float3(modelNoL, sssNormal1oL, sssNorma21oL) - 0.25);
 
float3 mainSSSColor = lerp(tempCurvatureValue * preintegratedValue * _SSSGameDiffParam12, NoL, modelNoL);

정광

측광
코어 알고리즘, 화면 공간

float viewYOffset = viewDir.y + _SkinCameraLightingAngle;
float3 viewOffset = float3(viewDir.x, viewYOffset, viewDir.z);
float NoOffsetV = saturate(dot(viewOffset, normalWorld1));
 
float preintegrated2ndUVX = clamp((NoOffsetV - 0.5) * _SkinCameraLightingRange + _SkinCameraLightingOffset + 0.5, 0.01, 0.99);
float preintegrated2ndUVY = 0.5;
float3 preintegratedValueInCameraLight = tex2D(_PreintegratedTex, float2(preintegrated2ndUVX, preintegrated2ndUVY)).xyz;
 
float3 skinCameraSSSColor = ao * _SkinCameraLightColor.xyz * preintegratedValueInCameraLight;
 
float NoOffsetVPow = exp2(log2(NoOffsetV) * _SkinCameraLightingPow);
 
skinCameraSSSColor = skinCameraSSSColor * NoOffsetVPow;

효과: 측면 조명과 유사하게 모든 각도에서 카메라 공간을 기준으로 조명이 있는지 확인합니다.

 

모든 SSS 효과가 중첩된 후

4 부드러운 그림자

고정 파이프라인용 Unity 소프트 섀도

PC 또는 에디터, 유니티의 자체 스크린 스페이스 섀도 사용

소프트섀도우를 켜고 Unity의 기본 화면 공간 그림자의 블러 효과를 바로 사용하세요.

모바일의 경우 일반 섀도맵을 사용하세요.

소프트섀도우를 켜고, 섀도우맵 샘플이 이중선형 샘플인지 확인합니다.

셰이더에서 나만의 3x3 PCF 샘플 만들기

#define _ShadowMapTexelOffset 0.00049       //1 / 2048
#define _ShadowKernelOffset 3
 
float CalculateShadowMap(v2f i, float3 posWorld)
{
    float atten = 1;
    #if defined (SHADOWS_SCREEN) && defined(UNITY_NO_SCREENSPACE_SHADOWS)
    int totalWeight = 0;
    float totalAtten = 0;
    float4 shadowCoord = mul(unity_WorldToShadow[0], unityShadowCoord4(i.posWorld, 1));
    float end = (_ShadowKernelOffset - 1) * 0.5;
    float start = -end;
    for(int a = start; a <= end; a++)
    {
        for(int b = start; b <= end; b++)
        {
            float4 shadowCoordTemp = shadowCoord;
            shadowCoordTemp.x += _ShadowMapTexelOffset * a;
            shadowCoordTemp.y += _ShadowMapTexelOffset * b;
            totalAtten += unitySampleShadowNew(shadowCoordTemp);
            totalWeight += 1;
        }
    }
 
    atten = totalAtten / totalWeight;
    atten = smoothstep(0, 1, atten);    //효과에는 큰 차이가 없습니다.
     
    #endif
     
    return atten;
}

기본 모바일 그림자와 PCF를 추가한 그림자 효과의 차이를 비교합니다.

 

3x3 소프트 섀도우

기본 그림자

5 옵션, 림 효과

float NoV = saturate(dot(viewDir, normalWorld1));
 
float rimValue = saturate((1 - _RimPowerGame - NoV) / (1 - _RimPowerGame));
float3 rimColor = saturate(smoothstep(0, 1, rimValue) * _RimColorGame.xyz);
rimColor = rimColor * shadowmap;

6 옵션, 화면 측면 조명

float3 normal1InView = normalize(mul(UNITY_MATRIX_V, normalWorld1));
 
float3 characterRimLightDir = normalize(float3(_CharacterRimLightDirection.xy, 1));
 
float viewNormaloRimLightDir = 1 - (dot(normal1InView, characterRimLightDir) * 0.5 + 0.5);
float minBorder = _CharacterRimLightBorderOffset.w + 0.5 - _CharacterRimLightBorderOffset.y;
float maxBorder = _CharacterRimLightBorderOffset.w + 0.5 + _CharacterRimLightBorderOffset.y;
 
float rimTempValue = (viewNormaloRimLightDir - minBorder) / (maxBorder - minBorder);
float3 rimColor2nd = smoothstep(0, 1, rimTempValue) * _CharacterRimLightColorSkin.xyz * shadowmap;

효과:

 

측면 조명

7 두 번째 빛.

고정 파이프라인, URP의 멀티 라이팅 구현 참조, 캐릭터 스킨 셰이더 스페셜은 이 라이트를 개별적으로 칠 수 있습니다.

C# 코드에서 _SecondLightColorSkin 색상을 동적으로 전달하기

bool secondLightEnable = 0.0 < _SecondLightColor.w;
 
if(secondLightEnable)
{
    float3 specularTerm2nd1 = DirectBRDFSpecularSmith(roughness1, _SkinSpecular.xyz, normalWorld1, _ForwardCompensateLightDirection.xyz, viewDir);
    specularTerm2nd1 = saturate(specularTerm2nd1 * paramTex2.z);
     
    float3 specularTerm2nd2 = DirectBRDFSpecularSmith(roughness2, skinSpecularAdd.xyz, normalWorld1, _ForwardCompensateLightDirection.xyz, viewDir);
    specularTerm2nd2 = saturate(specularTerm2nd2 * paramTex2.z);
     
    float No2ndL = saturate(dot(normalWorld1, _ForwardCompensateLightDirection.xyz));
    extraColor = _SecondLightColorSkin * No2ndL + extraColor;
     
    directSpecular = min(_SecondLightColorSkin .xyz, 3) * (specularTerm2nd1 + specularTerm2nd2) + directSpecular;
}

두 번째 조명 BRDF 효과

 

두 번째 조명에 대한 하이라이트

8 ToneMapping

몇 가지 방법을 제안합니다:

1 오브젝트를 구분하기 위해 톤 매핑이 필요한지 여부에 따라 템플릿 테스트를 사용하여 화면 포스트 프로세싱과 함께 톤 매핑을 수행할 수 있습니다.

2 스킨 셰이더의 끝에서 직접 톤 매핑을 수행합니다.

여기서 두 번째 방법은 프로젝트의 실제 상황에 따라 사용됩니다.

핵심 코드, 해당 구현은 URP에서 찾을 수 있습니다.

    float3 NeutralTonemap(float3 x)
    {
        // Tonemap
        float a = 0.2;
        float b = 0.29;
        float c = 0.24;
        float d = 0.272;
        float e = 0.02;
        float f = 0.3;
        float whiteLevel = 5.3;
        float whiteClip = 1.0;
 
        float3 whiteScale = (1.0).xxx / NeutralCurve(whiteLevel, a, b, c, d, e, f);
        x = NeutralCurve(x * whiteScale, a, b, c, d, e, f);
        x *= whiteScale;
 
        // Post-curve white point adjustment
        x /= whiteClip.xxx;
 
        return x;
    }

효과: 톤 매핑 스위치의 피부 성능 비교

 

Tonempping 없음.

Tonemapping 있음.

9 화면 공간의 표면 아래 산란 효과에 기반한 SSSSS 계산

단계:

1 스텐실 테스트로 피부 부위를 표시하는 것으로 시작하세요.

2 이 영역의 컨볼루션

CS 코드

public class Skin5SHelper
{
    //5S Shader Property 2 ID
    internal static readonly int _5STempBuffer = Shader.PropertyToID("_5STempBuffer");
    internal static readonly int _5STempBuffer1 = Shader.PropertyToID("_5STempBuffer1");
    internal static readonly int _5SCameraParamsID = Shader.PropertyToID("_5SCameraParams");
    internal static readonly int _5SKernelParamsID = Shader.PropertyToID("_5SKernelParams");
    internal static readonly int _5SKernelID = Shader.PropertyToID("_5SKernel");
     
    private static readonly Vector4 _5SCameraParams = new Vector4(7.59575f, 1f, 0f, 0f);
    private static readonly Vector4 _5SKernelParams = new Vector4(1.0f, 0f, 7f, 3.2f);
 
    private static readonly List<Vector4> _5SKernel = new List<Vector4>()
    {
        new Vector4( 0.72651f, 0.90199f, 0.94555f, 0.00f    ),
        new Vector4( 0.00368f, 0.00035f, 0.00019f, -2.00f   ),
        new Vector4( 0.02459f, 0.00834f, 0.00463f, -0.88889f),
        new Vector4( 0.10848f, 0.04032f, 0.0224f, -0.22222f ),
        new Vector4( 0.10848f, 0.04032f, 0.0224f, 0.22222f  ),
        new Vector4( 0.02459f, 0.00834f, 0.00463f, 0.88889f ),
        new Vector4( 0.00368f, 0.00035f, 0.00019f, 2.00f    ),
    };
 
    public static void ExecutePass(CommandBuffer cmd, Material mat, int width, int height, int _5SIterations, RenderTargetIdentifier sourceRT, RenderTargetIdentifier dstDepthRT, RenderTextureFormat format)
    {
        cmd.BeginSample("5SSkin");
                     
        cmd.GetTemporaryRT(_5STempBuffer, width, height, 0, FilterMode.Bilinear, format);
                     
        cmd.SetGlobalVector(_5SCameraParamsID, _5SCameraParams);
        cmd.SetGlobalVector(_5SKernelParamsID, _5SKernelParams);
        cmd.SetGlobalVectorArray(_5SKernelID, _5SKernel);
 
        if (_5SIterations > 0)
        {
            cmd.GetTemporaryRT(_5STempBuffer1, width, height, 0, FilterMode.Bilinear, format);
        }
                     
        cmd.BlitFullscreenTriangle(sourceRT, _5STempBuffer, dstDepthRT, mat, 0, true);
        for (int i = 0; i < _5SIterations - 1; i++)
        {
            cmd.BlitFullscreenTriangle(_5STempBuffer, _5STempBuffer1, dstDepthRT, mat, 1, true);
            cmd.BlitFullscreenTriangle(_5STempBuffer1,_5STempBuffer, dstDepthRT, mat, 0, true);
        }
                     
        cmd.BlitFullscreenTriangle(_5STempBuffer, sourceRT, dstDepthRT, mat, 1);
        cmd.EndSample("5SSkin");
    }
}

셰이더 코드

 

float4 Frag5S1(VaryingsDefault i) : SV_Target
{
    float4 offset = 0;
    offset.xy = _5SKernelParams.w * _5SCameraParams.y * _MainTex_TexelSize.xy * float2(1, 0);   //改为float(0,1)做纵向卷积
    offset.xz = offset.xy * _5SCameraParams.x;
     
 
    float2 depthUV = clamp(i.texcoordStereo.xy, 0, 1);
    float linearEyeDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_OpaqueDepthTexture, depthUV));
    float depth = _5SKernelParams.x * linearEyeDepth + _5SKernelParams.y;
    depth = max(depth, linearEyeDepth);
    offset.xz = offset.xz / depth;
 
    float tempValue = _5SCameraParams.x * 300.0 * offset.x;
     
    half4 mainColor = tex2D(_MainTex, i.texcoordStereo.xy);
 
    float3 tempColor = mainColor.xyz * _5SKernel[0].xyz;
 
    for(int index = 1; index < _5SKernelParams.z; index++)
    {
        float2 offsetUV = _5SKernel[index].ww * offset.xz + i.texcoordStereo.xy;
 
        half3 offsetMainColor = tex2D(_MainTex, offsetUV).xyz;
        bool pixelInvalid = dot(offsetMainColor, offsetMainColor) <= 0.01;
         
        float linearEyeDepth1 = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_OpaqueDepthTexture, offsetUV));
         
        float depthOffset = (linearEyeDepth1 - linearEyeDepth);
 
        float weight = saturate(dot(float2(tempValue, tempValue), abs(float2(depthOffset, depthOffset))));
 
        half3 colorOffset = offsetMainColor - mainColor.xyz;
        float colorWeight = dot(colorOffset, colorOffset);
        colorWeight = (colorWeight - 0.2) * 1.666666;
        colorWeight = smoothstep(0, 1, colorWeight);
 
        float colorWeightTemp = pixelInvalid ? (1 - colorWeight) : 0;
 
        colorWeight = colorWeight + colorWeightTemp;
 
        colorWeight = -weight * colorWeight + weight + colorWeight;
 
        half3 loopColor = lerp(offsetMainColor, mainColor.xyz, colorWeight);
        loopColor = loopColor * _5SKernel[index].xyz;
        tempColor += loopColor;
    }
     
    return float4(tempColor, mainColor.w);
}

효과의 차이

 

SSSSS 미적용

SSSSS 적용

10 최종 결과

역자의 말.
구현 접근은 인상적인데 스크린 스페이스 서브 서피스 스케터링 효과는 제한적이며 화면 공간에서 만들어내는 RT를 함께 계산 할 때 또 다른 마스크가 필요하다는 점도 있고 딱히 추천하지는 않습니다.

원문
https://zhuanlan.zhihu.com/p/691142348

安全验证 - 知乎

知乎,中文互联网高质量的问答社区和创作者聚集的原创内容平台,于 2011 年 1 月正式上线,以「让人们更好的分享知识、经验和见解,找到自己的解答」为品牌使命。知乎凭借认真、专业、友

www.zhihu.com