整体效果展示:主要方案是对下面几张图做不同的处理
身体
基础颜色光照:主要贴图+卡通贴图+ramp图+法线图+光照图
金属度与高光,头发部分高光:光照图,头发部分用高光black图
深度边缘光:用额外pass DepthNormals 获取深度信息
描边:额外pass对描边进行处理,用光照图的a通道对不同区域做颜色区分
脸
方案:主要贴图+卡通贴图+ramp图+SDF+描边
需要用到的贴图
将光照图,ramp图的sRGB关闭,法线图Type改成法线
妮露基础图的a通道储存的是神之眼灰度信息
光照图的rba存储材质信息,r通道储存金属强度,g通道储存AO信息,b通道储存高光形状与细节
ramp图上五行是白天暗部颜色,下五行是夜晚暗部颜色
卡通图用来做底部光照体积
准备阶段
//数据准备
Properties{[Space][Space][Space][Space]_AmbientColor ("基础颜色", Color) = (0.667,0.667,0.667,1)_DiffuseColor ("漫反射颜色", Color) = (0.906,0.906,0.906,1)_ShadowColor ("阴影颜色", Color) = (0.737,0.737,0.737,1)[Space][Space][Space][Space]_DiffuseTexFac("漫反射Fac",Range(0,1))=1_DiffuseTex ("漫反射贴图", 2D) = "white" {}[Space][Space][Space][Space]_ToonTexFac("卡通Fac",Range(0,1))=1_ToonTex ("卡通贴图", 2D) = "white" {}[Space][Space][Space][Space]_SphereTexHair ("头发高光", 2D) = "black" {}_SphereHair("头发高光强度",Range(0,128))=68[Space][Space][Space][Space]_DoubleSided("双面",Range(0,1))=0_Alpha("透明值",Range(0,1))=1[Space][Space][Space][Space]_MetalTex("金属贴图", 2D) = "black" {}_SpecExpon("高光强度",Range(1,128))=58_KsNonMetallic("非金属",Range(0,3))=1_KsMetallic("金属度",Range(0,3))=1[Space][Space][Space][Space]_NormalMap("法线贴图", 2D) = "bump" {}[Space][Space][Space][Space]_ILM("光照贴图",2D)="black"{}[Space][Space][Space][Space]_RampTex("Ramp贴图", 2D) = "white" {}_RampMapRow0("Ramp0",Range(1,5))=1_RampMapRow1("Ramp1",Range(1,5))=4_RampMapRow2("Ramp2",Range(1,5))=3_RampMapRow3("Ramp3",Range(1,5))=5_RampMapRow4("Ramp4",Range(1,5))=2[Space][Space][Space][Space]_OutlineOffset("描边值",Float)=0.0007_OutlineMapColor0("描边颜色0",Color)=(0,0,0,0)_OutlineMapColor1("描边颜色1",Color)=(0,0,0,0)_OutlineMapColor2("描边颜色2",Color)=(0,0,0,0)_OutlineMapColor3("描边颜色3",Color)=(0,0,0,0)_OutlineMapColor4("描边颜色4",Color)=(0,0,0,0)[Space][Space][Space][Space]_rimStrength("边缘光宽度",Range(0,10))=0.8_rimMax("边缘光强度",Range(0,20))=0.8}
//准备pass,阴影和深度信息
Pass{Name"ShadowCaster"Tags{"LightMode"="ShadowCaster"}ZWrite OnZTest LEqualColorMask 0Cull offHLSLPROGRAM#pragma exclude_renderers gles gles3 glcore#pragma target 4.5//材质关键字#pragma shader_feature_local_fragment _ALPHATEST_ON#pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A//显卡实例化#pragma multi_compile_instancing#pragma multi_compile _ DOTS_INSTANCING_ON//管线配置关键字#pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW#pragma vertex ShadowPassVertex#pragma fragment ShadowPassFragment#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//Shaders/LitInput.hlsl"#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//Shaders/ShadowCasterPass.hlsl"ENDHLSL}Pass{Name"DepthNormals"Tags{"LightMode"="DepthNormals"}ZWrite OnCull offHLSLPROGRAM#pragma exclude_renderers gles gles3 glcore#pragma target 4.5#pragma vertex DepthNormalsVertex#pragma fragment DepthNormalsFragment//材质关键字#pragma shader_feature_local _NORMALMAP#pragma shader_feature_local _PARALLAXMAP#pragma shader_feature_local _ _DETAIL_MULX2 _DETAIL_SCALED#pragma shader_feature_local_fragment _ALPHATEST_ON#pragma shader_feature_local_fragment _SMODTHNESS_TEXTURE_ALBEDO_CHANNEL_A//显卡实例化#pragma multi_compile_instancing#pragma multi_compile _ DOTS_INSTANCING_ON#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//Shaders/LitInput.hlsl"#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//Shaders/LitDepthNormalsPass.hlsl"ENDHLSL}
开始主题,准备好变体与顶点数据
Tags{"RenderPipeline"="UniversalPipeline""RenderType"="Opaque""LightMode"="UniversalForward"}Cull offHLSLPROGRAM#pragma multi_compile _MAIN_LIGHT_SHADOWS#pragma multi_compile _MAIN_LIGHT_SHADOWS_CASCADE#pragma multi_compile _SHADOWS_SOFT#pragma vertex vert#pragma fragment frag#pragma multi_compile_fog#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//ShaderLibrary/Core.hlsl"#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//ShaderLibrary/Lighting.hlsl"#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//ShaderLibrary/DeclareDepthTexture.hlsl"#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//ShaderLibrary/Shadows.hlsl"struct appdata{float4 vertex:POSITION ;half3 normal:NORMAL ;half4 tangent:TANGENT ;float2 uv:TEXCOORD0 ;half4 color:COLOR0 ;};struct v2f{float4 positionCS:SV_POSITION ;float2 uv:TEXCOORD0 ;float3 positionWS:TEXCOORD1;float3 positionVS:TEXCOORD2;float4 positionNDC:TEXCOORD3;float3 normalWS:TEXCOORD4 ;float3 tangentWS:TEXCOORD5 ;float3 bitangentWS:TEXCOORD6 ;float fogCooed:TEXCOORD7 ;float4 shadowCooed:TEXCOORD8 ;};
v2f vert(appdata v){v2f o;VertexPositionInputs vertexInput = GetVertexPositionInputs(v.vertex.xyz);o.uv = TRANSFORM_TEX(v.uv,_DiffuseTex);o.positionWS=vertexInput.positionWS;o.positionVS=vertexInput.positionVS;o.positionCS=vertexInput.positionCS;o.positionNDC=vertexInput.positionNDC;VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(v.normal,v.tangent);o.tangentWS = vertexNormalInput.tangentWS;o.bitangentWS=vertexNormalInput.bitangentWS;o.normalWS = vertexNormalInput.normalWS;o.fogCooed=ComputeFogFactor(vertexInput.positionCS.z);o.shadowCooed=TransformWorldToShadowCoord(vertexInput.positionWS);return o;}
URP中 非常便利的将需要的模型数据封装到了VertexPositionInputs中,可直接取出使用。 VertexPositionInputs vertexInput = GetVertexPositionInputs(v.vertex.xyz); 注意雾和阴影的计算
接下来开始写片元着色器
准备好需要用到的函数
Light light =GetMainLight(i.shadowCooed);float4 normalMap=tex2D(_NormalMap,i.uv);float3 normalTS=float3(normalMap.ag*2-1,0);normalTS.z=sqrt(1-dot(normalTS.xy,normalTS.xy));float3 N=normalize(mul(normalTS,float3x3(i.tangentWS,i.bitangentWS,i.normalWS)));float3 V=normalize(mul((float3x3)UNITY_MATRIX_I_V,i.positionVS*(-1)));float3 L=normalize(light.direction);float3 H=normalize(L+V);float NoL=dot(N,L);float NoH=dot(N,H);float NoV=dot(N,V);
法线的xy分量储存在法线图的ag通道,将其映射到-1—1得到xy分量,z分量通过勾股定理算出,再乘以TBN矩阵将其转换到世界空间
视向量是将视线的反方向乘以视图矩阵的逆矩阵将其变换到世界空间(V矩阵是将世界坐标转换到观察坐标)
float3 normalVS=normalize(mul((float3x3)UNITY_MATRIX_V,N));float2 matcapUV=normalVS.xy*0.5+0.5;float4 baseTex=tex2D(_DiffuseTex,i.uv);float4 toonTex=tex2D(_ToonTex,matcapUV); float3 baseColor=_AmbientColor.rgb;baseColor=saturate(lerp(baseColor,baseColor+_DiffuseColor.rgb,0.6));baseColor=lerp(baseColor,baseColor*baseTex.rgb,_DiffuseTexFac);baseColor=lerp(baseColor,baseColor*toonTex.rgb,_ToonTexFac);
再做法线matcap对toon图采样(matcap:将法线转换到视线空间,并计算到适合采样UV纹理的区间,进行采样。 问题:非物理,固定性无法与环境交互,倾向用于补光)
tex2D(sampler2D tex, float2 s)
函数,这是CG程序中用来在一张贴图中对一个点进行采样的方法
至此基础颜色部分完成,可以打开unity写写看。
光照部分
处理ramp图
光照图的a通道储存材质枚举,与ramp图的行对应
黑到白分别为—0,0.3,0.5,0.7,1
定义灰阶枚举,用step函数与光照图a通道做判断,再用lerp函数取ramp的行
以左上角为原点,除以10减去0.05得到v坐标
0的部分不需要额外计算 可删减
float matEnum1=0.3;float matEnum2=0.5;float matEnum3=0.7;float matEnum4=1.0;float ramp0=_RampMapRow0/10.0-0.05;float ramp1=_RampMapRow1/10.0-0.05;float ramp2=_RampMapRow2/10.0-0.05;float ramp3=_RampMapRow3/10.0-0.05;float ramp4=_RampMapRow4/10.0-0.05;float dayRampV=lerp(ramp4,ramp3,step(ilm.a,matEnum4));dayRampV=lerp(dayRampV,ramp2,step(ilm.a,matEnum3));dayRampV=lerp(dayRampV,ramp1,step(ilm.a,matEnum2));dayRampV=lerp(dayRampV,ramp0,step(ilm.a,matEnum1));float nightRampv=dayRampV+0.5;
下一步 加入兰伯特光照模型(半兰伯特让暗部更通透)
把半兰伯特当作ramp图的u坐标,平滑半兰伯特的过度部分,并且将其钳制在设定值内,防止错误采样(clamp函数:x,a,b 小于a返回a,大于b返回b ab之间返回x)
uv坐标得到后,用光照方向的y值来选择采样ramp图
用半兰伯特平滑混合基础颜色和暗部颜色(smoothstep 平滑 a,b,x x小于a 返回0,x大于b返回1,在ab之间就平滑)
将g通道乘以2 用saturate函数将超过1的部分去掉,混合常暗区域(AO),将g通道减0.5乘2 用saturate函数将小于0的部分去掉,混合常亮部分(眼睛)(saturate函数:x大于1返回1,x小于0返回0)
float lambert=saturate(NoL);float halflambert=pow(lambert*0.5+0.5,2);float lanbertStep=smoothstep(0.423,0.460,halflambert);float rampClampMin=0.003;float rampClampMax=0.997;float rampGrayU=clamp(smoothstep(0.2,0.4,halflambert),rampClampMin,rampClampMax);float2 rampGrayDayUV=float2(rampGrayU,1-dayRampV);float2 rampGrayNightUV=float2(rampGrayU,1-nightRampv);float rampDarkU=rampGrayU;float2 rampDarkDayUV=float2(rampDarkU,1-dayRampV);float2 rampDarkNightUV=float2(rampDarkU,1-nightRampv);float isDay=(L.y + 1)/2;float3 rampGrayColor=lerp(tex2D(_RampTex,rampGrayNightUV).rgb,tex2D(_RampTex,rampGrayDayUV).rgb,isDay);float3 rampDarkColor=lerp(tex2D(_RampTex,rampDarkNightUV).rgb,tex2D(_RampTex,rampDarkDayUV).rgb,isDay);float3 grayShadowColor=baseColor*rampGrayColor*_ShadowColor.rgb;float3 darkShadowColor=baseColor*rampDarkColor*_ShadowColor.rgb;float3 diffuse;diffuse=lerp(grayShadowColor,baseColor,lanbertStep);diffuse=lerp(darkShadowColor,diffuse,saturate(ilm.g*2));//diffuse=lerp(darkShadowColor,diffuse,light.shadowAttenuation);//自阴影diffuse=lerp(diffuse,baseColor,saturate(ilm.g-0.5)*2);
漫反射部分完成
下一步,高光部分
前面说到光照图r通道储存高光强度,b通道储存高光形状与细节,这里主要对这两个通道做计算
非金属高光
布林冯取反用step取值乘高光强度,可以加上头发高光,头发高光乘以基础颜色感觉更自然
金属高光
用光照图的r通道乘以半兰伯特做强度变化,乘以基础颜色得到金属质感
r通道为1的是金属,小于1的为非金属。用step做判断,混合金属和非金属高光得到最终高光
金属还有反射细节,这里用金属贴图简单实现,用法线采样金属图,用isMetal混合
将漫反射 高光 金属度加起来,到此光照部分完成。
float blinnPhong=step(0,NoL)*pow(max(0,NoH),_SpecExpon);float3 specularT=(tex2D(_SphereTexHair,i.uv))*blinnPhong*_SphereHair*baseColor;float3 nonMetallicSpec=step(1.04-blinnPhong,ilm.b)*ilm.r*_KsNonMetallic+specularT;float3 metallicSpec=blinnPhong*ilm.b*(lanbertStep*3.5)*baseColor*_KsMetallic;//*(tex2D(_SphereTextf,i.uv)*20)float isMetal=step(0.95,ilm.r);float3 specular=lerp(nonMetallicSpec,metallicSpec,isMetal);float3 metallic=lerp(0,tex2D(_MetalTex,matcapUV).r*baseColor,isMetal)*2;float3 albedo=diffuse+specular+metallic;
屏幕空间边缘光部分
用NDC算出物体屏幕uv位置,用SampleSceneDepth方法采样深度图,用LinearDepth方法把深度纹理的采样结果转换到视角空间下的线性深度值
用观察空间法线x坐标做内偏移,再重新采样偏移后的深度值,相减得到边缘
再与菲涅尔计算,产生边缘光变化,得到最终边缘光效果。
加上双面渲染摄制,混合雾效,得到最终效果。
float2 screenUV=i.positionNDC.xy/i.positionNDC.w;float rawDepth=SampleSceneDepth(screenUV);//float linearDepth=LinearEyeDepth(rawDepth,_ZBufferParams);//负责把深度纹理的采样结果转换到视角view空间下的线性深度值float2 screenOffset=float2(lerp(-1,1,step(0,normalVS.x))*rimOffset/_ScreenParams.x/max(1,pow(linearDepth,2)),0);float offsetDepth=SampleSceneDepth(screenUV+screenOffset);float offsetLinearDepth=LinearEyeDepth(offsetDepth,_ZBufferParams);float rim=saturate(offsetLinearDepth-linearDepth);rim=step(rimThreshold,rim)*clamp(rim*_rimStrength,0,_rimMax);float fresnelPower=6;float fresnelClamp=0.8;float fresnel=1-saturate(NoV);fresnel=pow(fresnel,fresnelPower);fresnel=fresnel*fresnelClamp+(1-fresnelClamp);albedo=1-(1-rim*fresnel)*(1-albedo);float alpha=_Alpha*_DiffuseColor.a*toonTex.a*sphereTex.a;alpha=saturate(min(max(IsFacing,_DoubleSided),alpha));float4 col=float4(albedo,alpha);clip(col.a-0.5);col.rgb=MixFog(col.rgb,i.fogCooed);return col;
至此,身体部分的片元着色器完成。
下一步,描边效果pass
剔除正面
和ramp一样,用光照图a通道枚举来定义不同材质的描边颜色。
Pass{Name"Outline"Tags{"Renderpipeline"="UniversalPipeline""RenderType"="Opaque"}Cull FrontHLSLPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_fog#include "Library/PackageCache/com.unity.render-pipelines.universal@12.1.7//ShaderLibrary/Core.hlsl"struct appdata{float4 vertex:POSITION ;float2 uv:TEXCOORD0 ;float3 normal:NORMAL ;float4 tangent:TANGENT ;float4 color:COLOR0 ;};struct v2f{float2 uv:TEXCOORD0 ;float4 positionCS:SV_POSITION ;float fogcoord:TEXCOORD1 ;};CBUFFER_START(UnituPerMaterial)sampler2D _DiffuseTex;float4 _DiffuseTex_ST;sampler2D _ILM;float4 _OutlineMapColor0;float4 _OutlineMapColor1;float4 _OutlineMapColor2;float4 _OutlineMapColor3;float4 _OutlineMapColor4;float _OutlineOffset;CBUFFER_ENDv2f vert(appdata v){v2f o;//VertexPositionInputs vertexInput=GetVertexPositionInputs(v.vertex.xyz+v.normal.xyz*_OutlineOffset);VertexPositionInputs vertexInput=GetVertexPositionInputs(v.vertex.xyz+v.tangent.xyz*_OutlineOffset);o.uv=TRANSFORM_TEX(v.uv,_DiffuseTex);o.positionCS=vertexInput.positionCS;o.fogcoord=ComputeFogFactor(vertexInput.positionCS.z);return o;}float4 frag(v2f i,bool isFacing:SV_IsFrontFace):SV_Target{float4 ilm =tex2D(_ILM,i.uv);//float matEnum0=0.0;float matEnum1=0.3;float matEnum2=0.5;float matEnum3=0.7;float matEnum4=1.0;float4 color =lerp(_OutlineMapColor4,_OutlineMapColor3,step(ilm.a,matEnum4));color=lerp(color,_OutlineMapColor2,step(ilm.a,matEnum3));color=lerp(color,_OutlineMapColor1,step(ilm.a,matEnum2));color=lerp(color,_OutlineMapColor0,step(ilm.a,matEnum1));float3 albedo =color.rgb;float4 col =float4(albedo,1);col.rgb=MixFog(col.rgb,i.fogcoord);return col;}ENDHLSL}
法线偏移会使过渡剧烈的地方出现断边,所以我们采用切线偏移方法。
用脚本对顶点做平滑,将同位置的顶点法线相加,结果存到切线里,在shader里改成向切线方向偏移。
c#脚本如下
public class Normal : MonoBehaviour
{private void Awake(){Mesh mesh = GetComponent<SkinnedMeshRenderer>().sharedMesh;IEnumerable<IEnumerable<KeyValuePair<Vector3, int>>> groups = mesh.vertices.Select((vertex, index) => new KeyValuePair<Vector3, int>(vertex, index)).GroupBy(pair => pair.Key);Vector3[] normals = mesh.normals;Vector4[] smoothNormals =normals.Select((normal, index) => new Vector4(normal.x, normal.y, normal.z,0f)).ToArray();foreach (IEnumerable<KeyValuePair<Vector3,int>> group in groups){if (group.Count()==1){continue;}Vector3 smoothNormal = Vector3.zero;foreach (KeyValuePair<Vector3,int> pair in group){smoothNormal += normals[pair.Value];}smoothNormal.Normalize();foreach (KeyValuePair<Vector3,int> pair in group){smoothNormals[pair.Value] = new Vector4(smoothNormal.x, smoothNormal.y, smoothNormal.z);}}mesh.tangents = smoothNormals;}
}
至此,描边部分完成。
下一步脸部综合效果
复制身体shader,修改一下,把不需要的去掉
脸部需要的参数,脸部的描边和ramp只需要一个参数就行
_ShadowTex("阴影图", 2D) = "black" {}_SDF("SDF",2D)="black"{}_ForwardVector("_ForwardVector",Vector)=(0,0,1,0)_RightVector("_RightVector",Vector)=(1,0,0,0)_RampTex ("Ramp贴图", 2D) = "white" {}_RampRow("Ramp排",Range(1,5))=3_OutlineOffset("描边值",Float)=0.00007_OutlineColor("描边颜色0",Color)=(0,0,0,0)
重点:SDF采样,SDF朝向
求出光照向量在上向量上的投影向量,即光向量乘以光向量与上向量夹角的余弦值,再乘以上向量的单位向量,就得到了光向量在上向量的投影向量。
将灯光向量与投影向量相减,得到新的灯光向量参与SDF计算
将投影向量与右向量点乘反余弦得到夹角,除以Π映射到0-1,也就是说0-0.5光线是照在脸的右边,0.5-1是左边。
用step函数来区分脸左右
将左右脸重新映射,都映射到0-1,平方让接近边缘时更加平滑,混合起来
采样左右脸的SDF,混合
判断,SDF灰阶阈值大于光照映射区域为1,小于为0
再判断朝向,防止后脑勺采样SDF
加上阴影颜色控制,得到脸部SDF最终效果
float3 upVector=cross(forwardVec,rightVec);float3 LpU=dot(L,upVector)/pow(length(upVector),2)*upVector;float3 LpHeadHorizon=L-LpU;float pi=3.141592654;float value=acos(dot(normalize(LpHeadHorizon),normalize(rightVec)))/pi;float exposeRight=step(value,0.5);float valueR=pow(1-value*2,3);float valueL=pow(value*2-1,3);float mixValue=lerp(valueL,valueR,exposeRight);float sdfRembrandL=tex2D(_SDF,float2(1-i.uv.x,i.uv.y)).r;float sdfRembrandR=tex2D(_SDF,i.uv).r;float mixSdf=lerp(sdfRembrandR,sdfRembrandL,exposeRight);float sdf=step(mixValue,mixSdf);sdf=lerp(0,sdf,step(0,dot(normalize(LpHeadHorizon),normalize(forwardVec))));float3 shadowColor=baseColor*rampColor*_ShadowColor.rgb;float3 diffuse =lerp(shadowColor,baseColor,sdf);//diffuse=lerp(shadowColor,diffuse,light.shadowAttenuation);float3 albedo =diffuse;
SDF朝向问题,当角色倒过来时,SDF会反向,所以需要C#脚本控制,方法如下
在角色头上绑定两个空物体,取名,一个放在头的前面,一个放在头的右边,方向不用调整
在C#中找到对象和材质球,将位置传到shader里,大功告成!
public class Face : MonoBehaviour
{private Transform headTransform;private Transform headForward;private Transform headRight;private Material[] faceMaterials;private void Start(){headTransform = transform.Find("頭").GetComponent<Transform>();headForward = transform.Find("頭/HeadForward").GetComponent<Transform>();headRight = transform.Find("頭/HeadRight").GetComponent<Transform>();SkinnedMeshRenderer render = transform.Find("角色_mesh").GetComponent<SkinnedMeshRenderer>();Material[] allMaterials = render.materials;faceMaterials = new Material[1];faceMaterials[0] = allMaterials[脸的材质球序号];Update();}private void Update(){Vector3 forwardVector = headForward.position - headTransform.position;Vector3 rightVector = headRight.position - headTransform.position;forwardVector = forwardVector.normalized;rightVector = rightVector.normalized;Vector4 forwardVector4 = new Vector4(forwardVector.x, forwardVector.y, forwardVector.z);Vector4 rightVector4 = new Vector4(rightVector.x, rightVector.y, rightVector.z);for (int i = 0; i < faceMaterials.Length; i++){Material material = faceMaterials[i];material.SetVector("_ForwardVector", forwardVector4);material.SetVector("_RightVector", rightVector4);}}}