acecase 发表于 2022-5-21 18:45

如何在Unity里面再现原神的质感?



https://www.zhihu.com/video/1510224969603133441
<hr/>引


最近做了一个Demo,觉得效果还不错,正好开始尝试写写分享贴子。
原神的角色渲染有很多人都在做,我个人对以下几点进行了复刻:

[*]阈值NdotL的明暗分界 和 手绘贴图AO 混合
[*]Ramp图控制暗部的色调
[*]Matcap 金属高光
[*]NdotH 高光(贴图高光形状和强度)
[*]面部SDF图阴影
[*]材质ID区分,身体上最多五组可调参数
[*]背面贴图使用uv2,且常暗
[*]角色切换时菲涅尔光特效
[*]Dither Faded
[*]Sobel算子检测的边缘光
[*]使用SRP制作简单的延迟渲染管线
此外,以个人兴趣为由,在此基础上增加了个人的发挥部分:

[*]尝试按照相同标准制作模型
[*]使用Cascadeur制作动画
[*]角色控制器
[*]进行特效配合
<hr/>角色渲染分块

贴图各个通道解释

_BaseMap



这个没什么好说的吧...基础颜色

_LightMap.r



lightMap.r

用于区分材质类型。比如,对于身体和头发部分,值大于0.9使用Matcap金属高光。对于脸部分,值若大于0.5使用SDF图进行阴影控制。

_LightMap.g



lightMap.g

手绘AO

_LightMap.b



lightMap.b

高光形状和强度

_LightMap.a



lightMap.a

材质ID,将1分为五段0.2,五段分别表示不同的ID,但不是按顺序排。
        float materialType = 1;
        if ((_UseMaterial2 != 0) && (lightMap.a >= 0.8))
        {
                materialType = 2;
        }
        if ((_UseMaterial3 != 0) && (lightMap.a >= 0.4) && (lightMap.a < 0.6))
        {
                materialType = 3;
        }
        if ((_UseMaterial4 != 0) && (lightMap.a >= 0.2) && (lightMap.a < 0.4))
        {
                materialType = 4;
        }
        if ((_UseMaterial5 != 0) && (lightMap.a >= 0.6) && (lightMap.a < 0.8))
        {
                materialType = 5;
        }
阈值NdotL和手绘AO混合

float4 lightMap = SAMPLE_TEXTURE2D_BIAS(_LightMap, sampler_LightMap, uv, sampleTexBias);
#ifdef UseFaceMap
        float paintedAO = _UseLightMapColorAO ? lightMap.g : 1.0;
#else
        float paintedAO = _UseLightMapColorAO ? lightMap.g : 0.5;
#endif
float shadowIntensity = lerp(0, IN.normalAndNdotL.w, smoothstep(0.01, 0.5, paintedAO));



消除了锯齿,但是值域可能稍微偏高

这里IN.normalAndNdotL为顶点着色器传入的,xyz为归一化的NormalWS,w为世界空间的NdotL。
使用了smoothstep以防止计算精度出现色斑,原计算方式为
float shadowIntensity = (paintedAO + IN.normalAndNdotL.w) * 0.5;
if (paintedAO > 0.95)
{
        shadowIntensity = 1;
}
if (paintedAO < 0.050)
{
        shadowIntensity = 0;
}



这锯齿我有点受不了....

Ramp图控制暗部的色调

if (shadowArea)
{
        diffuseResult = baseMapColor.rgb * rampColor.rgb;
}
else
{
        diffuseResult = baseMapColor.rgb * _CharacterMainLightBrightness;
      //
}
_CharacterMainLightBrightness为一个0到1的值,这里不直接使用阴影图,而用computeShader对角色周围一个球体范围内进行阴影检测,求得平均阴影值。
rampColor.rgb就是ramp图。原神的角色ramp有多张,通过世界时间等进行ramp图之间的切换,满足各种环境情况下的lookdev。



不乘baseMapColor.rgb



乘baseMapColor.rgb

Matcap 金属高光

如果lightMap.r大于0.9,会启用Matcap金属高光。


float metalMatcap = SAMPLE_TEXTURE2D(_MatcapMap, sampler_MatcapMap, normalVSxy).r;
metalMatcap = clamp(metalMatcap * _MatcapBrightness, 0, 1);
float3 matcapColor = lerp(_MatcapDarkColor, _MatcapLightColor, metalMatcap) * baseMapColor.rgb;


NdotH 高光(贴图高光形状和强度)



float powResult = pow(max(dot(normalWS, h), 0.001), shininess);
bool specTerm = (1 - lightMap.b) < powResult;

if (specTerm)
{
        specularResult = specMult * _SpecularColor * lightMap.r;
}
else
{
        specularResult = 0;
}
                       

面部SDF图阴影


float DoSDF(float2 uv, float3 mainLightDir, float ctrl)
{
        // sample the texture
        float col = SAMPLE_TEXTURE2D(_FaceMapTex, sampler_FaceMapTex, float2(uv.x, uv.y)).w;
        // sample the texture
        float mirrorCol = SAMPLE_TEXTURE2D(_FaceMapTex, sampler_FaceMapTex, float2(1-uv.x, uv.y)).w;

        if (ctrl >= 0.98)//if back to light
        {
                return 0;
        }
        return ctrl> 0 ? col : mirrorCol;
}



float faceShadowArea = DoSDF(IN.faceMap.xy, mainLigtDirWS, IN.faceMap.z);
其中,IN.faceMap来自顶点着色器
OUT.faceMap.xy = IN.uv.xy;//uv
float3 Front = _FaceForwardDirection;        //forwardWS
float3 Left = -_FaceRightDirection;        //rightWS
float ctrl = 1 - (dot(Front, normalize(float3(mainLightDir.x, 0, mainLightDir.z))) * 0.5 + 0.5);
float faceShadowSDF = dot(mainLightDir, Left) > 0 ? 1 : 0;
OUT.faceMap.z = ctrl;

材质ID区分,身体上最多五组可调参数

上面提到过,材质ID,将1分为五段0.2,五段分别表示不同的ID,但不是按顺序排。
        float materialType = 1;
        if ((_UseMaterial2 != 0) && (lightMap.a >= 0.8))
        {
                materialType = 2;
        }
        if ((_UseMaterial3 != 0) && (lightMap.a >= 0.4) && (lightMap.a < 0.6))
        {
                materialType = 3;
        }
        if ((_UseMaterial4 != 0) && (lightMap.a >= 0.2) && (lightMap.a < 0.4))
        {
                materialType = 4;
        }
        if ((_UseMaterial5 != 0) && (lightMap.a >= 0.6) && (lightMap.a < 0.8))
        {
                materialType = 5;
        }
得到materialType之后,就可以分别去调参数了,比如自发光颜色:
        float3 emission = baseMapColor.rgb * _EmissionColor * _EmissionScaler;
        if (materialType == 5)
        {
                emission *= _EmissionScaler5;
        }
        if (materialType == 4)
        {
                emission *= _EmissionScaler4;
        }
        if (materialType == 3)
        {
                emission *= _EmissionScaler3;
        }
        if (materialType == 2)
        {
                emission *= _EmissionScaler2;
        }
        if (materialType == 1)
        {
                emission *= _EmissionScaler1;
        }

背面贴图使用uv2,且常暗

(需要先设置双面显示)
struct v2f {
//...
};

struct v2f_CullFace {
        v2f v2f;
        FRONT_FACE_TYPE cullFace : FRONT_FACE_SEMANTIC;
};

#ifdef UseBackFace
PixelOutput frag(v2f_CullFace INPUT){
        v2f IN = INPUT.v2f;
        bool isFront = IS_FRONT_VFACE(INPUT.cullFace, true, false);
#else
PixelOutput frag(v2f IN){
#endif
如此得到isFront变量后,对面片的正面和背面分别处理。
float2 uv = IN.uvs.xy;
float shadowArea = shadowIntensity < _LightArea;
#ifdef UseBackFace
        if (isFront == false && _UseBackFaceUV2) {
                uv = IN.uvs.zw;
                shadowArea = 1;
        }
      
#endif
shadowArea上面有提到过,如果为1表示暗面,使用Ramp进行色调调整。



背面使用UV2,并常暗

角色切换时菲涅尔光特效

float dissolveRim = dot(normalize(normalWS), normalize(IN.cameraDirWS.xyz));
dissolveRim = max(1 - dissolveRim, 0.0001);
dissolveRim = pow(dissolveRim, saturate(_DissolveValue - 0.7 * float(-3.3333) + float(1.0000)) * 5);
float3 dissolveRimColor = dissolveRim * _DissolveColor;


改变_DissolveValue 即可。

Dither Faded

这个其实没啥说的,知乎上实现这个的大佬很多,使用一组特殊的数字以屏幕空间坐标将像素不同程度地clip掉就好了。



Sobel算子检测的边缘光

这个是在延迟光照阶段做的。
                        float GetDepth(float2 uv)
                        {
                                return _DepthTex.Load(uint3(uv * _ScaledScreenParams.xy, 0));
                        }

                        static float2 sobelSamplePoints =
                        {
                                float2(-1,1), float2(0,1), float2(1,1),
                                float2(-1,0), float2(0,0), float2(1,0),
                                float2(-1,-1), float2(0,-1), float2(1,-1)
                        };
                        static float sobelXMatrix =
                        {
                                1,0,-1,
                                2,0,-2,
                                1,0,-1
                        };
                        static float sobelYMatrix =
                        {
                                -1,-2,-1,
                                0,0,0,
                                1,2,1
                        };

                        float GetEdgeSobelHighlight(float2 originUV)
                        {
                                float2 sobel = 0;
                                float thickness = _Global_EdgeSobelHighlightThickness;
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                sobel += GetDepth(originUV + sobelSamplePoints * thickness) * float2(sobelXMatrix, sobelYMatrix);
                                return length(sobel) * _Global_EdgeSobelHighlightIntensity;
                        }
使用GetEdgeSobelHighlight函数得到边缘光之后,可以使用diffuse乘上去,或者做一个lerp。


值得一提的是,这里_Global_EdgeSobelHighlightThickness最好跟屏幕大小有关,我这里1920*1080使用的值是0.004。
使用SRP制作简单的延迟渲染管线

由于这个Demo的目标只是做一个天空球环境,所以只打了个框架,光照部分还没仔细写。
传入的GBuffer:

[*]SpecDiffuse:使用高光和阴影之后的画面
[*]Diffuse:无光照的贴图画面
[*]Depth:深度图
执行顺序为:
参数设置Pass、天空球Pass、GBufferPass、延迟光照Pass、半透明物体Pass、后处理Pass
其中,半透明Pass的深度需要从GBufferPass中的深度结果复制过来。

<hr/>其他Demo内容

使用Cascadeur制作动画

Cascadeur是我朋友推荐来的一个动作编辑软件,最明显的特定是有AI辅助重心补帧功能。


https://www.zhihu.com/video/1510253493080768512
对于我这种完全没K过帧的人来讲,即使没有很细,使用这个软件出的效果确实已经达到我想要的程度了。

角色控制器


[*]使用Unity的AnimationRigging进行头部和躯干的目标跟随,并且用代码控制当目标物在背后时淡出。
[*]衣摆没有使用动画(太难K了),使用DynamicBone进行骨骼设置,根据骨骼根路径长度有数值的lerp。
<hr/>关于动画的一些琐碎事

一开始并没有想法去自己K动画,使用一些动画拼,但套动作实在是不满意。
自己做了一个工具把先是把所有动作换算到了自己的角色模型骨骼上,生成一个个脱离fbx的新的独立的.anim文件,然后在Cascadeur里面K好3次攻击部分的动作,最后在Unity里的Animation窗口逐帧调整动画曲线,并加入了特效相关的一些曲线。
在Animation里面做特效真的很方便,不用去关心动作和动作之间特效的值有没有跳变,只要状态机有过渡会自动把这些特效相关的参数也进行一个过渡。
<hr/>结语

这次Demo的尝试是我参与部分最多的一次尝试,对于我个人来讲整个游戏的美术流程比较完整的走了一遍,确实地体悟到美术人才的重要性。
希望我之后也能做出同等或者超越其品质的作品。
将这些内容分享出来,欢迎大家跟我讨论。

七彩极 发表于 2022-5-21 18:49

给涛涛来个沙发

FeastSC 发表于 2022-5-21 18:58

来一个雅座

RedZero9 发表于 2022-5-21 19:02

给涛涛来个地板
页: [1]
查看完整版本: 如何在Unity里面再现原神的质感?