Unity3D手游项目的总结和思考(2) - 角色渲染
上一篇介绍了一下场景的渲染,这一篇准备分享一下角色的渲染.
项目一开始的时候,考虑到我们是多人同屏的游戏,沿用了以前项目最简单的角色制作标准,一张512贴图,1500面,随着手机硬件的发展和各种牛逼重度手游的出现,角色就需要新的shader来提升效果了.对于大量同屏的怪物,我们采用传统光照模型,对于重要的NPC和玩家,可以使用PBR.除了这些基本的,角色渲染还需要各种效果,比如流光,残影,X-Ray透视,溶解,受击闪光等等.
传统光照模型的角色渲染,大部分和场景的差不多,也有一些技巧,会让效果更好,比如用边缘光来模拟背光.
// back light diffuse
nl = dot(fixedNormal, _GlobalBackLightDir.xyz);
finalColor.rgb += mainColor.rgb * _GlobalBackLightColor.rgb * saturate(nl);
// back light rim
half fresnel = 1 - saturate(dot(viewDir, fixedNormal));
nl = nl * 0.5 + 0.5; // [0, 1]
half rim = saturate(step(_RimSharp, fresnel) * fresnel - _RimSharp); // fixed rim = pow(fresnel, _RimSharp);
fixed3 rimColor = _GlobalBackLightColor.rgb * nl * _RimIntensity;
finalColor.rgb += rimColor * rim;
只有背光的diffuse:
加上边缘光后:
对边缘光做过滤,只留下背光方向的边缘光:
然后背光的光照效果就特别明显了.
接下来主要介绍下PBR.Unity5支持PBR技术,所以我们也直接跟进了,由于我们的角色有很多其他效果,所以只能把PBR技术整合进自己的shader中.
PBR(Physically Based Rendering)翻译过来就是指基于物理的渲染,为什么叫物理的渲染,其实就是他用的光照模型的理论和物理原理更加符合,效果也更加真实.由于它与物理性质非常接近,因此我们(尤其是美术师们)可以直接以物理参数为依据来编写表面材质,而不必依靠粗劣的修改与调整来让光照效果看上去正常。使用基于物理参数的方法来编写材质还有一个更大的好处,就是不论光照条件如何,这些材质看上去都会是正确的,而在非PBR的渲染管线当中有些东西就不会那么真实了。
PBR物理原理的近似,体现在三个方面:微表面;能量守恒;基于物理的BRDF。
微表面:简单说,就是在微观尺度下,没有任何平面是完全光滑的,这个光滑或者粗糙的程度,我们用粗糙度贴图来表示.
能量守恒:出射光线的能量永远不能超过入射光线的能量(发光面除外)。当一束光线碰撞到一个表面的时候,它就会分离成一个折射部分和一个反射部分。反射部分就是会直接反射开来而不会进入平面的那部分光线,这就是我们所说的镜面光照(高光)。而折射部分就是余下的会进入表面并被吸收的那部分光线,这也就是我们所说的漫反射光照。能量守恒公式:漫反射+镜面反射= 1
BRDF:Cook-Torrance BRDF光照模型兼有漫反射和镜面反射两个部分:
漫反射用Lambert,镜面反射比较复杂,包含3个函数,
1.D(法线分布函数)
用统计学来估算在受到表面粗糙度的影响下,微观法线方向与宏观法线一致的微平面的数量。对于美术来说,主要受粗糙贴图的影响,它决定了高光的大小强度和形状。
2.V(自阴影遮挡函数)
描述了微平面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而减少表面所反射的光线。如果没有遮挡函数,就可能造成能量不守恒。
3.F(菲涅尔方程)
菲涅尔方程描述的是在不同的表面角下表面所反射的光线所占的比率。
以上三项,每一项都会有一些变种算法,比如法线分布函数,常见的是BlinnPhong和GGX,由于移动平台的性能限制,我们的PBR采用的算法是:
D: BlinnPhong
V: Modified Kelemen and Szirmay-Kalos
F: Schlick Fresnel
CharacterStandard_PBR.shader:
Shader "Luoyinan/Character/CharacterStandard_PBR"
{
Properties
{
_MainTex("Main Texture", 2D) = "white" {}
_Color ("Main Color", Color) = (1, 1, 1, 1)
_NormalTex("Normal Texture", 2D) = "bump" {}
_GlossTex ("Gloss Texture (R:gloss) (G:roughness) B(:flowlight)", 2D) = "white" {}
_SpecularColor ("Specular Color", Color) = (1, 1, 1, 0.5)
_SpecularColor2 ("Specular Color 2", Color) = (1, 1, 1, 1)
_Roughness ("Roughness", Range (0, 1)) = 0
_RoughnessBias ("Roughness Bias", Float) = 0
_RefectionTex("Refection Texture (Cubemap)", Cube) = "" {}
_RefectionColor ("Refection Color", Color) = (1, 1, 1, 1)
_DissolveTex ("Dissolve Texture", 2D) = "white" {}
_DissolveColor ("Dissolve Color", Color) = (1, 0, 0, 1)
_DissolveCutoff ("Dissolve Cutoff", Range (0, 1)) = 0
_FlowLightTex ("Flow Light Texture", 2D) = "white" {}
_FlowLightColor ("Flow Light Color", Color) = (1, 0, 0, 1)
_FlowDirectionX("Flow Speed&Direction X", Float) = 0
_FlowDirectionY("Flow Speed&Direction Y", Float) = 8
_FlowDistortionIntensity("Flow Distortion Intensity", Range (0, 0.5)) = 0.2
_FlashColor ("Flash Color", Color) = (1, 1, 1, 1)
[HideInInspector] _FlashIntensity("", Range (0, 1)) = 0
[HideInInspector] _DoubleSided("", Float) = 2.0
[HideInInspector] _CutOff("", Float) = 0
[HideInInspector] _UseRoughness("", Float) = 0
}
SubShader
{
Tags
{
"Queue" = "Geometry"
"RenderType" = "Opaque" // 支持渲染到_CameraDepthNormalsTexture
}
Pass
{
Lighting Off
Cull[_DoubleSided] // CullMode: Off(0) Front(1) Back(2)
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
#include "TerrainEngine.cginc"
#include "UnityStandardBRDF.cginc"
#include "UnityStandardUtils.cginc"
#pragma shader_feature _ALPHA_TEST_ON
#pragma shader_feature _REFLECTION_ON
#pragma shader_feature _NORMAL_MAP
#pragma shader_feature _DISSOLVE_ON
#pragma shader_feature _FLOW_LIGHT_ON
#pragma shader_feature _USE_ROUGHNESS
#pragma multi_compile __ _ORIGIN_ALPHA
#pragma multi_compile __ _POINT_LIGHT
#pragma multi_compile __ _FANCY_STUFF
struct appdata_custom
{
half4 vertex : POSITION;
half2 texcoord : TEXCOORD0;
#if _FANCY_STUFF
half3 normal : NORMAL;
#if _NORMAL_MAP
half4 tangent : TANGENT;
#endif
#endif
};
// SM2.0的texture interpolator只有8个,要合理规划.
struct v2f
{
half4 pos : SV_POSITION;
half2 uv0 : TEXCOORD0;
#if _FLOW_LIGHT_ON
half2 uv1 : TEXCOORD1;
#endif
UNITY_FOG_COORDS(2)
#if _FANCY_STUFF
float3 posWorld : TEXCOORD3;
half3 normalWorld : TEXCOORD4;
#if _NORMAL_MAP
half3 tangentWorld : TEXCOORD5;
half3 binormalWorld : TEXCOORD6;
#endif
#endif
};
sampler2D _MainTex;
half4 _MainTex_ST;
fixed4 _Color;
fixed4 _FlashColor;
fixed _FlashIntensity;
sampler2D _GlossTex;
fixed4 _SpecularColor;
fixed4 _SpecularColor2;
#if _DISSOLVE_ON
sampler2D _DissolveTex;
fixed4 _DissolveColor;
half _DissolveCutoff;
#endif
#if _FLOW_LIGHT_ON
sampler2D _FlowLightTex;
half4 _FlowLightTex_ST;
fixed4 _FlowLightColor;
fixed _FlowDirectionX;
fixed _FlowDirectionY;
fixed _FlowDistortionIntensity;
#endif
#if _FANCY_STUFF
fixed4 _GlobalAmbientColor;
half4 _GlobalMainLightDir;
fixed4 _GlobalMainLightColor;
half4 _GlobalBackLightDir;
fixed4 _GlobalBackLightColor;
#if _POINT_LIGHT
float4 _GlobalPointLightPos;
fixed4 _GlobalPointLightColor;
fixed _GlobalPointLightRange;
#endif
fixed _Roughness;
fixed _RoughnessBias;
#if _REFLECTION_ON
uniform samplerCUBE _RefectionTex;
fixed4 _RefectionColor;
#endif
#if _NORMAL_MAP
uniform sampler2D _NormalTex;
#endif
#endif
v2f vert(appdata_custom i)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
o.uv0 = TRANSFORM_TEX(i.texcoord, _MainTex);
#if _FLOW_LIGHT_ON
o.uv1 = TRANSFORM_TEX(i.texcoord, _FlowLightTex);
#endif
#if _FANCY_STUFF
o.posWorld = mul(unity_ObjectToWorld, i.vertex).xyz;
o.normalWorld = UnityObjectToWorldNormal(i.normal);
#if _NORMAL_MAP
o.tangentWorld = UnityObjectToWorldDir(i.tangent);
o.binormalWorld = cross(o.normalWorld, o.tangentWorld) * i.tangent.w;
#endif
#endif
UNITY_TRANSFER_FOG(o, o.pos);
return o;
}
#if _ALPHA_TEST_ON
uniform fixed _CutOff;
#endif
fixed4 frag(v2f i) : COLOR
{
// main color
#if _FANCY_STUFF
fixed4 mainColor = tex2Dbias(_MainTex, fixed4(i.uv0, 0, -1.5)) * _Color; // 通过TextureImporter来设置mipmapbias对mobile平台无效
#else
fixed4 mainColor = tex2D(_MainTex, i.uv0) * _Color;
#endif
fixed alpha = mainColor.a;
// alpha test
#if _ALPHA_TEST_ON
clip(alpha - _CutOff);
#endif
// dissolve
#if _DISSOLVE_ON
if (_DissolveCutoff > 0) // performance
{
fixed dissolve = tex2D(_DissolveTex, i.uv0).r;
fixed clipValue = dissolve - _DissolveCutoff;
clip(clipValue);
// edge range [0, 0.1)
if (clipValue < 0.1)
{
fixed3 edgeColor = fixed3(_DissolveColor.x, clipValue / 0.1, _DissolveColor.z);
fixed colorTotal = edgeColor.x + edgeColor.y + edgeColor.z;
mainColor.rgb = (mainColor.rgb * edgeColor * colorTotal * colorTotal) * 3;
}
}
#endif
// gloss
fixed4 glossTex = tex2D(_GlossTex, i.uv0);
if (glossTex.r > _SpecularColor.a)
mainColor.rgb *= _SpecularColor.rgb;
if (glossTex.a > _SpecularColor2.a)
mainColor.rgb *= _SpecularColor2.rgb;
fixed4 finalColor = mainColor;
#if _FANCY_STUFF
// normalmap
#if _NORMAL_MAP
fixed3x3 tangentToWorld = fixed3x3(i.tangentWorld, i.binormalWorld, i.normalWorld);
half3 normalMap = UnpackNormal(tex2D(_NormalTex, i.uv0));
half3 fixedNormal = normalize(mul(normalMap, tangentToWorld));
#else
half3 fixedNormal = normalize(i.normalWorld);
#endif
// common PBR params
#if _USE_ROUGHNESS
fixed roughness = _Roughness;
#else
fixed roughness = saturate(glossTex.g + _RoughnessBias);
#endif
fixed oneMinusRoughness = 1 - roughness;
half specularPower = RoughnessToSpecPower(roughness);
// specular workflow
fixed oneMinusReflectivity;
fixed3 specColor = glossTex.r * mainColor.rgb; // monochrome specular -> color specular
fixed3 diffColor = EnergyConservationBetweenDiffuseAndSpecular (mainColor.rgb, specColor, /*out*/ oneMinusReflectivity);
// metallic workflow
//fixed metallic = glossTex.r;
//fixed3 specColor;
//fixed oneMinusReflectivity;
//fixed3 diffColor = DiffuseAndSpecularFromMetallic(mainColor.rgb, metallic, /*out*/ specColor, /*out*/ oneMinusReflectivity);
fixed reflectivity = 1 - oneMinusReflectivity;
half3 viewDir = normalize(_WorldSpaceCameraPos - i.posWorld);
half nv = DotClamped(fixedNormal, viewDir);
alpha *= reflectivity;
// main light PBR
half3 halfDir = Unity_SafeNormalize(_GlobalMainLightDir + viewDir);
half nl = DotClamped(fixedNormal, _GlobalMainLightDir);
half nh = DotClamped(fixedNormal, halfDir);
half lh = DotClamped(_GlobalMainLightDir, halfDir);
half invV = lh * lh * oneMinusRoughness + roughness * roughness;
half invF = lh;
half specular = ((specularPower + 1) * pow (nh, specularPower)) / (8 * invV * invF + 1e-4h);
if (IsGammaSpace())
specular = sqrt(max(1e-4h, specular));
specular = clamp(specular, 0.0, 100.0); // Prevent FP16 overflow on mobiles
finalColor.rgb = _GlobalAmbientColor * diffColor + (diffColor + specular * specColor) * _GlobalMainLightColor * nl;
// env reflection
#if _REFLECTION_ON
half3 reflUVW = normalize(reflect(-viewDir, fixedNormal));
fixed3 envColor = texCUBE(_RefectionTex, reflUVW) * _RefectionColor.rgb;
half realRoughness = roughness * roughness;
half surfaceReduction = IsGammaSpace() ? 0.28 : (0.6 - 0.08*roughness);
surfaceReduction = 1.0 - realRoughness * roughness * surfaceReduction;
half grazingTerm = saturate(oneMinusRoughness + reflectivity);
finalColor.rgb += surfaceReduction * envColor * FresnelLerpFast(specColor, grazingTerm, nv);
#endif
// back light PBR
halfDir = Unity_SafeNormalize(_GlobalBackLightDir + viewDir);
nl = DotClamped(fixedNormal, _GlobalBackLightDir);
nh = DotClamped(fixedNormal, halfDir);
lh = DotClamped(_GlobalBackLightDir, halfDir);
invV = lh * lh * oneMinusRoughness + roughness * roughness;
invF = lh;
specular = ((specularPower + 1) * pow (nh, specularPower)) / (8 * invV * invF + 1e-4h);
if (IsGammaSpace())
specular = sqrt(max(1e-4h, specular));
specular = clamp(specular, 0.0, 100.0); // Prevent FP16 overflow on mobiles
finalColor.rgb += (diffColor + specular * specColor) * _GlobalBackLightColor * nl;
// point light PBR
#if _POINT_LIGHT
half3 toLight = _GlobalPointLightPos.xyz - i.posWorld ;
half ratio = saturate(length(toLight) / _GlobalPointLightRange);
//half attenuation = 1 - ratio; // linear attenuation
ratio *= ratio;
half attenuation = 1.0 / (1.0 + 0.01 * ratio) * (1 - ratio); // quadratic attenuation
if (attenuation > 0) // performance
{
halfDir = Unity_SafeNormalize(toLight + viewDir);
nl = DotClamped(fixedNormal, toLight);
nh = DotClamped(fixedNormal, halfDir);
lh = DotClamped(toLight, halfDir);
invV = lh * lh * oneMinusRoughness + roughness * roughness;
invF = lh;
specular = ((specularPower + 1) * pow (nh, specularPower)) / (8 * invV * invF + 1e-4h);
if (IsGammaSpace())
specular = sqrt(max(1e-4h, specular));
specular = clamp(specular, 0.0, 100.0); // Prevent FP16 overflow on mobiles
finalColor.rgb += (diffColor + specular * specColor) * _GlobalPointLightColor * nl * attenuation;
}
#endif
#else
finalColor.rgb *= 1.4;
#endif
// flow light
#if _FLOW_LIGHT_ON
fixed3 flow = tex2D(_FlowLightTex, frac(i.uv1 + _Time.xx * half2(_FlowDirectionX, _FlowDirectionY)) + mainColor.xy * _FlowDistortionIntensity).rgb;
finalColor.rgb += flow * _FlowLightColor.rgb *_FlowLightColor.a * 2 * glossTex.b;
#endif
// flash
finalColor.rgb += _FlashIntensity * _FlashColor;
// fog
UNITY_APPLY_FOG(i.fogCoord, finalColor);
// alpha
#if _ORIGIN_ALPHA
finalColor.a = mainColor.a;
#else
finalColor.a = alpha;
#endif
return finalColor;
}
ENDCG
}
// 没用Unity自带的阴影,只是用来来渲染_CameraDepthsTexture.
Pass
{
Tags{ "LightMode" = "ShadowCaster" }
Fog { Mode Off }
ZWrite On
Offset 1, 1
Cull[_DoubleSided]
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#pragma fragmentoption ARB_precision_hint_fastest
#pragma shader_feature _ALPHA_TEST_ON
#include "UnityCG.cginc"
struct v2f
{
V2F_SHADOW_CASTER;
#if _ALPHA_TEST_ON
fixed2 uv0 : TEXCOORD1;
#endif
};
#if _ALPHA_TEST_ON
sampler2D _MainTex;
half4 _MainTex_ST;
uniform fixed _Cutoff;
#endif
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER(o)
#if _ALPHA_TEST_ON
o.uv0 = TRANSFORM_TEX(v.texcoord, _MainTex);
#endif
return o;
}
fixed4 frag(v2f i) : COLOR
{
#if _ALPHA_TEST_ON
fixed4 col = tex2D(_MainTex, i.uv0);
clip(col.a - _Cutoff);
#endif
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
Fallback off
CustomEditor "CharacterStandard_PBR_ShaderGUI"
}
这个shader除了PBR以外,还实现了以下几个功能:
1.mipmap偏移
为什么纹理采样要做mipmap偏移呢,因为对于斜45度角的游戏来说,角色一般用的都不是最清晰的mip0级的贴图,而是mip2或者mip3.就会导致角色看起来模糊.所以要适当偏移,让角色清晰.
2.dissolve溶解
3.flow light流光
最简单的流光就是UV移动,为了效果好,还需要几个改进,一是增加流光掩码,只在固定的区域有流光,比如金属,二是增加扰动,这样的流光就更加漂亮,三是对贴图的UV展开要尽量连续,这样流光流动的路径才连贯.
4.flash受击闪光
被打就闪一下,用指数曲线来表现渐变效果,比线性曲线好.
5.装备换色
衣服颜色和金属颜色都可以改变.Specular Color一个用调节金属颜色,一个用来调节衣服颜色.Gloss Texture:R通道表示单色高光,G通道表示粗糙度,B通道表示流光掩码,A通道表示衣服掩码.是的,就是这么多掩码...
设置面板:
新的角色制作标准,头部要和身体分开制作贴图的,头发需要特殊处理高光,但是历史原因很多老的模型只有一张贴图.
我们的PBR实现和网易的镇魔曲类似,可以实现装备*换色.高光颜色通过从单色高光组合成彩色高光.
fixed3 specColor = glossTex.r * mainColor.rgb; // monochrome specular -> color specular
**镇魔曲的模型(只是研究用),用我们的shader渲染,效果也差不太多,金属质感效果还不错.
如下图: