ChuanXin 发表于 2022-11-6 15:25

卡通渲染总结-原神达达利亚


gif渣画质警告。7.多m的gif点这个链接
https://upload-images.jianshu.io/upload_images/28340786-c561331b1ebcd621.gif?imageMogr2/auto-orient/strip

惯例先上参考:

【Unity技术美术】 原神Shader渲染还原解析

原神角色渲染Shader分析还原

【01】Unity URP 卡通渲染 原神角色渲染记录-Diffuse: Ramp + AO + Double Shadow

【JTRP】屏幕空间深度边缘光 Screen Space Depth Rimlight

原神角色渲染Shader分析还原

我这程度挑战卡渲,还是太有勇气了……

时隔良久才来写文章,会简略很多。
贴图分析


当时漫天找资源,求爷爷告奶奶地要贴图,现在给像我一样懵逼不知道这些贴图哪来的小萌新指条明路,使用截帧软件NinjaRipper自己去游戏里拿。。

问题又出现了,大佬们的文章告诉我每个通道应该长得像最下面一排那样,但我找到的某些图是透明的。最后知道png图片自带透明度,而且不存在a通道里,贴图格式最好为tga(希望没说错)。

放到模型上

ramp


关于ramp每条功能,当时一直在凑参数,画了个图,现在快看不懂了……

half3 Cel_Ramp(half NdotL, half4 parameter)      {parameter.g = smoothstep(0.0,0.1,parameter.g) ;            half3 fixShadow = saturate(_fixShadowColor.rgb + parameter.g);         half halfLambert =smoothstep(0.1,0.7,NdotL*0.2+0.3) * parameter.g * 2;//当没有分界线的时候,就是g不要乘2,要用step限制/*            Skin = 1.0 white            Silk = 0.7            Metal = 0.5            Soft = 0.3            Hard = 0.0 black coat            */            #if _SHADERENUM_HAIR                half v = -0.5 * _inNight + 0.95;            #elif _SHADERENUM_BASE                half v = parameter.a * 0.45 + -0.5 * _inNight + 0.55;            #else                half v = 0;            #endifreturn SAMPLE_TEXTURE2D(_rampMap, sampler_rampMap, half2(halfLambert, v)).rgb * fixShadow;      }
问题:亮面有ramp阴影,暗面没有,有时候双面都有

解决:clamp!!

问题:远处模糊方块近处没有

解决:mipmap关闭!!

lightmap,也就是g通道在0-0.5,但乘以2映射到0-1的做法是错的,怎么都取不出固定阴影。还有世界佬那个,用实时阴影与固定阴影相乘的,我得到的实时阴影很奇怪,干脆舍弃。

于是使用smoothstep函数,将0-0.1黑色部分留下,其余全部取1即取白。

固定阴影等于g通道乘以外部给出的固定阴影颜色,再截取到0-1,只影响黑色0-0.1的部分,给g里的阴影加上颜色。

接下来我的lambert很奇怪,是凑出来的结果,不要学。

为了肤色与脸色对上,为肤色单独创建一个材质,将这个颜色设为白色。

整体ramp预览


高光


对于头发的高光抄了最简单的,能用就行。
float3Cel_Specular(float3 NdotH,float3 NdotL,float3 baseColor,float4 var_ParamTex){#if _SHADERENUM_HAIRfloatspecularRadius=pow(NdotH,var_ParamTex.b*50);float3specularColor=var_ParamTex.b*baseColor;return_enableSpecular*smoothstep(0.3,0.4,specularRadius)*specularColor*lerp(_hairSpecularIntensity,1,step(0.9,var_ParamTex.b));#elif _SHADERENUM_BASEfloatspec=pow(saturate(NdotH),_shininess);floatw=0.4f;spec=lerp(0,1,smoothstep(-w,w,spec-1.1f+var_ParamTex.b));float3specularColor=_enableSpecular*_lightSpecColor*_specMulti*var_ParamTex.r*spec;//multi越小越黑return specularColor;#elsereturn0;#endif}
还花了两天把变体那块的文档看完了,必须为每个变体安排上,否则他就不编译。
高光-step函数

half spec = pow(saturate(dot(input.normalWS, halfViewLightWS)), _Shininess);spec = step(1.0f - LightMapColor.b, spec);half4 specularColor = _EnableSpecular * _LightSpecColor * _SpecMulti * LightMapColor.r * spec;from 雪羽

spec经step后,各像素有无(1 0)高光就知道了。作为高光控制参数。
金属度

float3 Cel_Metal(float3 nDir, float4 var_ParamTex, float3 baseColor)      {float2 mcCoord = float2(dot(normalize(UNITY_MATRIX_V.xyz), normalize(nDir)), dot(normalize(UNITY_MATRIX_V.xyz), normalize(nDir)));            float var_MatCap = SAMPLE_TEXTURE2D(_metalMap, sampler_metalMap, mcCoord * 0.5 + 0.5) * 2 + 0.3f;//金属图想要变白,增大值。漫反射色不变甚至变黑,减小值            return _enableMetalSpecular * var_MatCap * baseColor *var_ParamTex.r;//取0.95到1的部分,即glossiness图白色金属部分            //金属色乘漫反射色得到高光色,要与光源做反应的话用光源色      }金属度笔记


r通道glossiness告诉你哪里是金属、高光强的地方,b通道specular告诉你金属长啥样

整体金属高光预览


参数调不好

神之眼自发光

half3 Cel_Emission(half4 baseColor, half4 var_ParamTex)      {            half bloomMask= baseColor.a;            half3 emission = _enableEmission * bloomMask * _emissionIntensity * abs(_SinTime.w);//abs((frac(_Time.y * 0.5) - 0.5) * 2)            emission *= baseColor;            return emission;      }自发光预览


BaseColor汇合

half3 Cel_Base(half NdotL, half NdotH, half NdotV, half3 nDir, half4 baseColor, half4 var_ParamTex)      {            //baseColor = step(0.95, var_ParamTex.a) * baseColor * _skinWhite + (1-step(0.95, var_ParamTex.a))*baseColor;            //提亮肤色            //baseColor = lerp(baseColor, baseColor * _skinWhite, step(0.95, var_ParamTex.a));            half3 rampColor = Cel_Ramp(NdotL, var_ParamTex);                        //float3 albedo = baseColor * rampColor;            half3 albedo = lerp(baseColor, baseColor * rampColor, _rampDarkness);            half3 specular = Cel_Specular(NdotH, NdotL, baseColor, var_ParamTex);            half3 metal = Cel_Metal(nDir, var_ParamTex, baseColor);            half3 emission = Cel_Emission(baseColor, var_ParamTex);            half3 finalColor = albedo+ specular + metal ;//金属色与高光色相加            #if _SHADERENUM_BASE            returnfinalColor + emission;            #else            return finalColor;            #endif      }SDF的脸

half3 Cel_Face(half3 baseColor, Light mainLight, float4 vuv){//https://zhuanlan.zhihu.com/p/552097741//左右阴影图   Sample flipped face light map    float2 rightFaceUV = float2(-vuv.z, vuv.w);    float4 faceShadowR = SAMPLE_TEXTURE2D(_paramTex, sampler_paramTex, rightFaceUV);    float4 faceShadowL = SAMPLE_TEXTURE2D(_paramTex, sampler_paramTex, float2(vuv.z, vuv.w)); //人物朝向 Get character orientation    float3 up = float3(0,1,0);      float3 front=TransformObjectToWorldDir(float4(0.0,0.0,1.0,1.0));   float3 right = cross(up, front);   float2 realLDir = normalize(mainLight.direction.xz);    float realFDotL = dot(normalize(front.xz), realLDir);//FaceShadowRange   float realRDotL = dot(normalize(right.xz), realLDir); //通过RdotL决定用哪张阴影图   float shadowTex = realRDotL < 0? faceShadowL: faceShadowR;//FaceShadow //获取当前像素的阴影阈值    float shadowMargin = shadowTex.r;//FaceShadow //判断是否在阴影中    float inShadow = -0.5 * realFDotL + 0.5 < shadowMargin;//小于marging,在阴影中    float2 shadowUV = float2(inShadow, -0.5 * _inNight + 1);//1day,0.5night    half3 faceShadowColor = SAMPLE_TEXTURE2D(_rampMap, sampler_rampMap, shadowUV) * baseColor;    faceShadowColor = lerp(baseColor, baseColor * faceShadowColor, _rampDarkness);    return faceShadowColor;}
先分别采样左右的脸,然后定义上右前方向,文章说灯光朝向和灯光vector不一致,逆时针转90,投影后要归一化,不然长度会小于1,我不乘那个矩阵才是对的……接下来注释都有。采了ramp里的肤色。由于与肤色的u值并不完全相同,脸色与肤色还是有一定差距,不过已经相当可以了。到时候写个脚本把几个材质的是否夜晚选项统一起来。

怎么样是对的呢,光照在0度左右,鼻子上的阴影左右动。

脸部预览


外描边


描边及修正,不知道抄的哪位的,实际上精要里的描边也够了,总之都会断裂。实际游戏里看剧情,镜头拉近的时候也看到了断裂的边,瞬间心理平衡了哈哈哈。

唯一看懂的真正实现描边的函数,由于我实在不会绘制顶点色(在3d max与blender都试过,无效。另外max导入文件一定不能直接拖进去,否则直接放大一百倍),又在草地里学了在顶点着色器采样贴图的方法,现在实践一下对脸部绘制遮罩贴图来去除嘴角、眼眶的描边。

未处理的脸部描边:


在脸的a通道画遮罩。


反复调整区域达到完美。




游戏截图:


上代码。
float3 TransformPositionWSToOutlinePositionWS(float FaceMask, float3 positionWS, float positionVS_Z, float3 normalWS)      {            float texAlpha;            #if !_SHADERENUM_FACE            texAlpha = 1;            #else            texAlpha = FaceMask;            #endif            float outlineExpandAmount = texAlpha * _outlineWidth * GetOutlineCameraFovAndDistanceFixMultiplier(positionVS_Z);            return positionWS + normalWS * outlineExpandAmount;      }      v2f OutlinePassVertex(a2v a)      {             v2f v = (v2f)0;             ZERO_INITIALIZE(v2f, v);                VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(a.normalOS, a.tangentOS);v.normWS = vertexNormalInput.normalWS;//原始法线                            v.uv.xy = TRANSFORM_TEX(a.texcoord, _diffTex).xy;            v.posWS = TransformObjectToWorld(a.vertex);            v.posVS = TransformWorldToView(v.posWS);            float FaceMask = SAMPLE_TEXTURE2D_LOD(_diffTex, sampler_diffTex,v.uv.xy, 0).a;            v.posWS = TransformPositionWSToOutlinePositionWS(FaceMask, v.posWS, v.posVS.z, v.normWS);            v.posCS = TransformWorldToHClip(v.posWS);            //冯乐乐            // float4 posVS =mul(UNITY_MATRIX_MV, a.vertex);            // float3 normalVS= mul((float3x3)UNITY_MATRIX_IT_MV, a.normalOS);//a.tangentOS            // normalVS.z = -0.5 ;            // posVS = posVS + float4(normalize(normalVS) , 0) * _outlineWidth ;            // v.posCS = mul(UNITY_MATRIX_P , posVS) ;            v.shadowCoord = TransformWorldToShadowCoord(v.posWS); // jave.lin : shadow recieve 将 世界坐标 转到 灯光坐标(阴影坐标)            return v;      }描边预览


边缘光

//================               //边缘光Rim: 屏幕空间深度边缘光               //如果我们把每个点的位置向着法线方向位移一段距离,再根据位移后的坐标采样相机深度图,把深度和自己的深度做对比,如果差值大于一个阈值,那么就是边缘。               //https://zhuanlan.zhihu.com/p/365339160               //一定要注意一点,要保持z分量不变,因为Homogeneous Clip Space中的w分量就是负的ViewSpace中的z分量,必须保持一致才能转换至正确的ViewPort(视口)坐标,否则采样的结果不对。                float3 offsetPosVS = float3(v.posVS.xy + v.normVS.xy * _rimWidth, v.posVS.z);                float4 offsetPosCS = TransformWViewToHClip(offsetPosVS);//在观察空间偏移后转裁剪空间                float4 offsetPosVP = TransformHClipToViewPortPos(offsetPosCS);//从裁剪空间转view port space,视口空间(也就是屏幕UV坐标空间),获得位移后视图                //float3 nonHomogeneousCoord = v.posCS.xyz / v.posCS.w;//裁剪空间进行齐次除法得NDC                //float2 screenUV = nonHomogeneousCoord.xy;                float2 screenUV = v.scrPos.xy/v.scrPos.w;                float depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV).r; //获得原深度,链接文章中depth获取存疑                float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams); // 离相机越近越大(小???//解析深度为线性深度                float offsetDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, offsetPosVP).r;//获得位移后视图的深度值                float linearEyeOffsetDepth = LinearEyeDepth(offsetDepth, _ZBufferParams);                float depthDiff = linearEyeOffsetDepth - linearEyeDepth;                float rimMask = smoothstep(0, _rimThreshold, depthDiff);                //float rimMask = step(_rimThreshold, depthDiff);                float3 rim = _enableRim * saturate(rimMask - _rimRange);//_RimColor shadowColor step(0.1, lightMap.g)               //==================边缘光预览


此外还有阴影ShadowCaster Pass,描边片元着色器,获取深度图的pass,urp自带后处理,就不放了。欣赏最终效果:

晚上调参后


白天

下次再调,要先把夜晚参数记下
页: [1]
查看完整版本: 卡通渲染总结-原神达达利亚