TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

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

jplee 2023. 12. 1. 16:20
역자의 말.
일단 원본 링크 기억이 안납니다. -_-; 오래전에 노션에 한글로 정리 해 논 것만 남아 있네요. Zhihu 에서 발췌 된 내용입니다. 아무튼... 저번에 번역해서 공유 했던 원신 렌더링 리버스엔지니어링 기사에 연결 된 내용으로 함께 엮어 읽어보면 좋겠습니다.

이전 글 엮어서 보기. https://techartnomad.tistory.com/120

 

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

역자의 말. 오랫동안 위쳇에서 구독 하던 블로거의 글을 간단히 번역하여 공유 해 보기로 했습니다. 옛 넷이즈의 동료이자 10년 이상 렌더링 분야에 꾸준히 학습하며 공식 계정을 통해 기사를 올

techartnomad.tistory.com


예전 정리 했던 내용 시작.

그림 1 각 각도에 대한 음영 다이어그램 입력

주석 1: 그림 1에서 조명은 텍스처 오른편에 있으며, 왼쪽과 오른쪽 모두 영향을 미치지 않으며, 최종 셰이더는 양쪽 모두 호환됩니다.

주석 2: 0은 얼굴 전체가 흰색인 경우를 의미하며, 그림1 h.png과 대응됩니다.

주석 3: 180.png 파일은 전체가 검은색이면 안 되며, 최소한 하나의 흰색 픽셀이 있어야 합니다. 이는 시작점에 빛이 비추는 것을 나타냅니다.

9개의 이미지를 모두 SDF 텍스처로 변환합니다.

GitHub - mattdesl/image-sdf: generate a signed distance field from an image 를 사용하여 SDF를 생성합니다.

https://github.com/leegoonz/image-sdf

 

GitHub - leegoonz/image-sdf: generate a signed distance field from an image

generate a signed distance field from an image. Contribute to leegoonz/image-sdf development by creating an account on GitHub.

github.com

2048*2048,spread 512,downscale 7
원본 2048*2048, 스프레드 256, 다운스케일 4

매 두 인접한 SDF 텍스처에서 부드러운 이미지를 생성하십시오. 총 8 장입니다.

wow01.png를 예로 들면, PS에서 임계 값을 수정하여 미리보기를 조정할 수 있습니다. 결과는 다음과 같습니다.

wow01.png 임계값 수정하기

8장의 스무딩 이미지를 각 픽셀마다 합한 후 8로 나눈 것이 최종 얼굴 그림자 텍스처입니다.

최종 텍스처 wow.png
wow.png 임계값 수정

얼굴 그림자 맵 렌더링 그림자 코드:

float3 _Up= float3(0,1,0);//Direction on the character Pass in the code
float3 _Front= float3(0,0,-1);//Front direction of the figure Pass in the code
float3 Left= cross(_Up,_Front);
float3 Right=-Left;

//You can also take each direction directly from the model's world matrix
//This requires the model to be built with the correct orientation: X, Y, Z, right, top, front.
//float4 Front = mul(unity_ObjectToWorld,float4(0,0,1,0));
//float4 Right = mul(unity_ObjectToWorld,float4(1,0,0,0));
//float4 Up = mul(unity_ObjectToWorld,float4(0,1,0,0));

float FL=  dot(normalize(_Front.xz), normalize(L.xz));
float LL= dot(normalize(Left.xz), normalize(L.xz));
float RL= dot(normalize(Right.xz), normalize(L.xz));
float faceLight= faceLightMap.r+ _FaceLightmpOffset ;//Used to align the light and dark transitions with the body of the hair
float faceLightRamp= (FL> 0)* min((faceLight> LL),(1> faceLight+RL ) ) ;
float3 Diffuse= lerp( _ShadowColor*BaseColor,BaseColor,faceLightRamp);

보충 자료

그림 2에서 생성된 SDF 매핑을 사용하여 셰이더 시뮬레이션 4. 부드러운 매핑 전환 및 블렌딩

왼쪽: 시뮬레이션 임계값 오른쪽: 시뮬레이션 섀도 매핑 아래: 9 SDF 매핑

셰이더 코드입니다:

왼쪽: sdf_thread.shader

Shader "sdf/thread"
{
    Properties
    {
        _MainTex0 ("Texture", 2D) = "white" {}
        _MainTex1 ("Texture", 2D) = "white" {}
        _MainTex2 ("Texture", 2D) = "white" {}
        _MainTex3 ("Texture", 2D) = "white" {}
        _MainTex4 ("Texture", 2D) = "white" {}
        _MainTex5 ("Texture", 2D) = "white" {}
        _MainTex6 ("Texture", 2D) = "white" {}
        _MainTex7 ("Texture", 2D) = "white" {}
        _MainTex8 ("Texture", 2D) = "white" {}
        _thread ("Thread", Range(0,1)) = 0.5
	_delta("delta", Range(0,0.05)) = 0.01

    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex0;
            sampler2D _MainTex1;
            sampler2D _MainTex2;
            sampler2D _MainTex3;
            sampler2D _MainTex4;
            sampler2D _MainTex5;
            sampler2D _MainTex6;
            sampler2D _MainTex7;
            sampler2D _MainTex8;
			float _thread;
			float _delta;

            fixed4 frag (v2f i) : SV_Target
            {
				fixed4 col0 = tex2D(_MainTex0, i.uv);
				fixed4 col1 = tex2D(_MainTex1, i.uv);
                fixed4 col2 = tex2D(_MainTex2, i.uv); 
                fixed4 col3 = tex2D(_MainTex3, i.uv); 
                fixed4 col4 = tex2D(_MainTex4, i.uv); 
                fixed4 col5 = tex2D(_MainTex5, i.uv); 
                fixed4 col6 = tex2D(_MainTex6, i.uv); 
                fixed4 col7 = tex2D(_MainTex7, i.uv); 
                fixed4 col8 = tex2D(_MainTex8, i.uv);

                float4 color = float4(0, 0, 0, 1);

                float cols[9];

                cols[0] = col0.a;
                cols[1] = col1.a;
                cols[2] = col2.a;
                cols[3] = col3.a;
                cols[4] = col4.a;
                cols[5] = col5.a;
                cols[6] = col6.a;
                cols[7] = col7.a;
                cols[8] = col8.a;

                 for (int i = 0; i < 8; i++) {
                     if (i/8.0 < _thread && _thread <= (i+1)/8.0) {
                         fixed r = lerp(cols[i], cols[i+1], _thread*8 - i);
                         r = smoothstep(0.5- _delta, 0.5+ _delta, r);
                         color = fixed4(r, r, r, 1);
                         return color;
                     }
                 }
                
                return color;
            }
            ENDCG
        }
    }
}

 

sdf_blend.shader

Shader "sdf/blend"
{
    Properties
    {
        _MainTex0 ("Texture", 2D) = "white" {}
        _MainTex1 ("Texture", 2D) = "white" {}
        _MainTex2 ("Texture", 2D) = "white" {}
        _MainTex3 ("Texture", 2D) = "white" {}
        _MainTex4 ("Texture", 2D) = "white" {}
        _MainTex5 ("Texture", 2D) = "white" {}
        _MainTex6 ("Texture", 2D) = "white" {}
        _MainTex7 ("Texture", 2D) = "white" {}
        _MainTex8 ("Texture", 2D) = "white" {}
	_delta ("delta", Range(0,0.05)) = 0.01

    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex0;
            sampler2D _MainTex1;
            sampler2D _MainTex2;
            sampler2D _MainTex3;
            sampler2D _MainTex4;
            sampler2D _MainTex5;
            sampler2D _MainTex6;
            sampler2D _MainTex7;
            sampler2D _MainTex8;
			float _delta;

            fixed4 frag (v2f i) : SV_Target
            {
				fixed4 col0 = tex2D(_MainTex0, i.uv);
				fixed4 col1 = tex2D(_MainTex1, i.uv);
                fixed4 col2 = tex2D(_MainTex2, i.uv); 
                fixed4 col3 = tex2D(_MainTex3, i.uv); 
                fixed4 col4 = tex2D(_MainTex4, i.uv); 
                fixed4 col5 = tex2D(_MainTex5, i.uv); 
                fixed4 col6 = tex2D(_MainTex6, i.uv); 
                fixed4 col7 = tex2D(_MainTex7, i.uv); 
                fixed4 col8 = tex2D(_MainTex8, i.uv);

                float4 color = float4(0, 0, 0, 1);

                float cols[9];

                cols[0] = col0.a;
                cols[1] = col1.a;
                cols[2] = col2.a;
                cols[3] = col3.a;
                cols[4] = col4.a;
                cols[5] = col5.a;
                cols[6] = col6.a;
                cols[7] = col7.a;
                cols[8] = col8.a;


                float4 color2 = float4(0, 0, 0, 1);
                for (float j=1; j <= 256.0; j++) {
                    float _thread2 = j / 256.0;

                    for (int i = 0; i < 8; i++) {
                        if (i/8.0 < _thread2 && _thread2 <= (i+1)/8.0) {
                            fixed r = lerp(cols[i], cols[i+1], _thread2*8 - i);
                            r = smoothstep(0.5- _delta, 0.5+ _delta, r);
                            fixed4 tmp_color = fixed4(r, r, r, 1);
                            color2 = ((j-1) * color2 + tmp_color) / j;
                            break;
                        }
                    }

                }

                color = color2;


                return color;
            }
            ENDCG
        }
    }
}

Unity에서 GPU 렌더링 SDF를 추가로 구현하면 SDF 스펙 생성이 1초도 걸리지 않습니다.

참고1: Texture는 입력 텍스처이며 Rt는 채우지 않아도 됩니다. sdfgenerate 쉐이더는 이 글의 뒷부분에 있습니다.

참고2: UP 컴퓨터에서 Unity 1024 * 1024 * 약 350 * 350 크기가 되면 GPU가 충돌합니다. 크기 제한이 있습니다.

출력 크기는 1024 * 1024입니다.

350 * 350 픽셀을 무차별 대상으로 하면 충돌합니다.

물론 아래 그림의 512 * 512 * 256 * 256 크기는 충돌 값보다 훨씬 작습니다.

모노 비헤이비어 컴포넌트로 사용하면 Rt를 채우지 않아도 되며, gen을 클릭하십시오.

1초도 안 걸렸네요, 정말 빠르네요

 

sdfgenerate.shader

Shader "ShaderMan/sdfgenerate"
    {

    Properties{
        _MainTex ("MainTex", 2D) = "white" {}
        _range ("range", Range(16, 256)) = 16
    }

    SubShader
    {
        Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }

        Pass
        {
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"

        struct VertexInput {
            fixed4 vertex : POSITION;
            fixed2 uv:TEXCOORD0;
            fixed4 tangent : TANGENT;
            fixed3 normal : NORMAL;
           //VertexInput
        };


        struct VertexOutput {
            fixed4 pos : SV_POSITION;
            fixed2 uv:TEXCOORD0;
            //VertexOutput
        };

        //Variables
        sampler2D _MainTex;
        uniform float4 _MainTex_TexelSize;
        float _range;

    
        bool isIn(fixed2 uv) {
            fixed4 texColor = tex2D(_MainTex, uv);
            return texColor.r > 0.5;
        }
        

        float squaredDistanceBetween(fixed2 uv1, fixed2 uv2)
        {
            fixed2 delta = uv1 - uv2;
            float dist = (delta.x * delta.x) + (delta.y * delta.y);
            return dist;
        }


        VertexOutput vert (VertexInput v)
        {
           VertexOutput o;
           o.pos = UnityObjectToClipPos (v.vertex);
           o.uv = v.uv;
           //VertexFactory
           return o;
        }
        fixed4 frag(VertexOutput i) : SV_Target
        {

            fixed2 uv = i.uv;

            const float range = _range;
            const int iRange = int(range);
            float halfRange = range / 2.0;
            fixed2 startPosition = fixed2(i.uv.x - halfRange * _MainTex_TexelSize.x, i.uv.y - halfRange * _MainTex_TexelSize.y);

            bool fragIsIn = isIn(uv);
            float squaredDistanceToEdge = (halfRange* _MainTex_TexelSize.x*halfRange*_MainTex_TexelSize.y)*2.0;

            // [unroll(100)]
            for (int dx = 0; dx < iRange; dx++) {
                // [unroll(100)]  
                for (int dy = 0; dy < iRange; dy++) {
                    fixed2 scanPositionUV = startPosition + float2(dx * _MainTex_TexelSize.x, dy* _MainTex_TexelSize.y);

                    bool scanIsIn = isIn(scanPositionUV / 1);
                    if (scanIsIn != fragIsIn) {
                        float scanDistance = squaredDistanceBetween(i.uv, scanPositionUV);
                        if (scanDistance < squaredDistanceToEdge) {
                            squaredDistanceToEdge = scanDistance;
                        }
                    }
                }
            }

            float normalised = squaredDistanceToEdge / ((halfRange * _MainTex_TexelSize.x*halfRange * _MainTex_TexelSize.y)*2.0);
            float distanceToEdge = sqrt(normalised);
            if (fragIsIn)
                distanceToEdge = -distanceToEdge;
            normalised = 0.5 - distanceToEdge;


            return fixed4(normalised, normalised, normalised, 1.0);
        }
        ENDCG

        }
    }
}

 

SdfGenerate.cs

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(SdfGenerate))]
public class SdfGenerateInsp : Editor
{
    public override void OnInspectorGUI() {
        base.DrawDefaultInspector();

        var sdfGenerate = target as SdfGenerate;
        if (GUILayout.Button("gen")) {
            sdfGenerate.gen();

        }
    }
}
public class SdfGenerate : MonoBehaviour
{
    public RenderTexture rt;
    public Texture texture;
    public Shader shader;
    public int spread = 16;

    public string savePath;

    public Vector2 size = new Vector2(1024, 1024);

    public void gen() {
        var sdfGenerate = this;
        if (sdfGenerate.texture == null) {
            Debug.LogErrorFormat("texture is null ");
            return;
        }

        if (sdfGenerate.shader == null) {
            Debug.LogErrorFormat("shader is null ");
            return;
        }

        if (sdfGenerate.rt != null) {
            sdfGenerate.rt.Release();
            sdfGenerate.rt = null;
        }

        System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
        watch.Start();
        //init();计算耗时的方法


        sdfGenerate.rt = new RenderTexture((int)sdfGenerate.size.x, (int)sdfGenerate.size.y, 32, RenderTextureFormat.ARGB32);

        Material mat = new Material(sdfGenerate.shader);
        mat.hideFlags = HideFlags.DontSave;
        mat.SetFloat("_range", sdfGenerate.spread);

        var input_rt = RenderTexture.GetTemporary(new RenderTextureDescriptor(sdfGenerate.rt.width, sdfGenerate.rt.height, sdfGenerate.rt.format));

        Graphics.Blit(texture, input_rt);

        Graphics.Blit(input_rt, sdfGenerate.rt, mat);


        RenderTexture.ReleaseTemporary(input_rt);

        var rt = sdfGenerate.rt;
        Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false);
        tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
        tex.Apply();

        var directory = Path.GetDirectoryName(sdfGenerate.savePath);
        var fileName = Path.GetFileName(sdfGenerate.savePath);

        //Debug.LogErrorFormat("path: {0}", sdfGenerate.savePath);
        //Debug.LogErrorFormat("directory: {0}", directory);
        //Debug.LogErrorFormat("fileName: {0}", fileName);

        if (!string.IsNullOrEmpty(directory)) {
            if (!Directory.Exists(directory)) {
                Directory.CreateDirectory(directory);
            }
        }
        else {
            Debug.LogErrorFormat("savePath directory no exist {0}", sdfGenerate.savePath);
            return;
        }



        File.WriteAllBytes(sdfGenerate.savePath, tex.EncodeToPNG());

        Debug.LogFormat("save png: {0}", sdfGenerate.savePath);

        watch.Stop();
        var mSeconds = watch.ElapsedMilliseconds / 1000.0;
        Debug.LogErrorFormat("耗时:{0}秒", mSeconds.ToString());
    }
}

참고 문서: https://zhuanlan.zhihu.com/p/337944099

 

Signed Distance Field

signed distance field最近UE4.26上线了,离UE5又近了一点。UE的各种渲染大量运用了一种名为Signed Distance Field的技术,前段时间刷屏的《黑神话·悟空》的主程,在一次分享会上也介绍说《悟空》项目中

zhuanlan.zhihu.com

 

 

卡通渲染之基于SDF生成面部阴影贴图的效果实现(URP)

看了很多大佬的教程: 黑魔姬:神作面部阴影渲染还原 这篇讲了图的制作流程(大概思路) Signed Distance Fields 这篇讲了SDF的算法 橘子猫:如何快速生成混合卡通光照图 这是之前一个群里的大佬

zhuanlan.zhihu.com

 

 

【03】Unity URP 卡通渲染 原神角色渲染记录-Function-Based Light and Shadow: Emission + SDF脸部阴影

点击下面链接,B站上传了实际Game窗口效果,视频有压缩,实际运行效果更好些~ 【1080P高码率】Unity URP管线,仿原神渲染,第6弹,人物展示场景更新_哔哩哔哩_bilibili系列一共5篇: 【01】Unity URP

zhuanlan.zhihu.com

 

 

Unity着色器《原神》面部平滑阴影解决思路

免责声明:此文章仅供学习交流一、解决思路《原神》脸部的平滑阴影过度实际上是画了一张面部的阴影过渡图,依靠这张图我们设置一个阈值来形成黑白交界,这样就达成了一个平滑的阴影过

zhuanlan.zhihu.com