用Unity实现Dota2角色Shader(以Ogre Magi为例)
写在前面:
本文章为个人学习笔记,方便以后自己复习,也希望能帮助到他人。
由于本人水平有限难免出现错误,还请评论区指出,多多指教。
部分图元和素材来源于网络,如有侵权请联系本人删除。
参考资料与链接会在文章末尾贴出。
=======================================================================
本文通过编写DOTA2角色shader来巩固一下之前所学光照模型的基础知识,因为V社有在官网放出DOTA2角色的模型和贴图,比较方便。
以下是DOTA2资源地址:
http://www.dota2.com/workshop/requirements
但是V社是没有放出过官方的Shader,只有一些贴图的相关用法(下面的网址),因此网上各种DOTA2 Shader都是各位大牛自己研究的,本文也是参考了很多大牛的分享与教学,在此是为了巩固自己所学并与大家分享,并非“唯一正确”。
https://help.steampowered.com/en/faqs/view/299C-D7F9-09A5-98B6
下载完资源包打开一看吓了一跳,一共80张贴图,分为base,arm,back,belt,head,weapon六个部分,每个部分也有十多张贴图,打开一看有一些是灰度图,或许可以把几张压在一张里(后面会提到)。
接下来浏览一下官网的页面看看每个贴图大概的用处是啥。
- Color贴图,透明贴图,法线贴图,自发光贴图
- Detail Mask与不同的角色特殊效果相关,Diffuse Mask极少使用
- Metalness Mask金属度贴图,后面会对金属部分做一些处理
- Rimlight边缘光效果
- Base Tint Mask高光染色遮罩,控制高光颜色与bask color颜色的混合程度(高光从base color拿了多少颜色?)
- Specular Exponent高光次幂
还有一些条状的Warp图,我们看看哪些贴图可以做一下资源优化:
- RGB:Color与A:opacity(图名为translucency)合并为一张
- R:SpecInt G:RimInt B:TintMask A:SpecPow合并为一张
- R:FresnelCol G:FresnelRim A:FresnelSpec合并为一张
我们先来看一下大概的光照模型和最终效果:
首先我们在片元着色器中准备后续计算用的向量:
解码法线贴图获取切线空间下的法线,再构建TBN矩阵计算出世界空间下的法线。
计算出世界空间下的视方向与光方向(平行光)的反射方向。
由于我们做了一些纹理资源优化,把可以合并的贴图合并起来,此时我们采样纹理的次数就能减少,避免更多性能消耗。
但是要记得每个通道中的贴图起的是什么作用。
比如这里,MaskTex的alpha通道是SpecExponent(SpecPow)高光次幂,我们用lerp后的结果作为采样cubemap的第四维,可以得到身体不同的部分有不同的光滑度。
纹理中越亮代表越光滑,反射内容则越清晰;反之,越暗代表越粗糙,反射内容越模糊。
如果只是单用mipmap等级采样cubemap则是全身同一程度的光滑度,效果比较死板。
采样过后把纹理结果整理好方便之后使用:
接下来是主光漫反射部分:
除了计算半兰伯特模型外还有两个额外操作。
Metalness Mask越亮金属度越高,暗部则是非金属。我们用金属度纹理做lerp,实际上是压暗了金属部分,意思是让金属部分的漫反射(diffuse)减弱(模拟出PBR的效果,能量守恒)。
用半兰伯特结果构建一个二维向量采样下面这张DiffuseWarpTex会得到类似3S的效果,暗紫色的皮肤非常符合怪物角色的设定:
同样用金属度做lerp,让金属部分的菲涅尔效果减弱:
然后除了计算phong模型外,还用到了specTint这张纹理:
此纹理亮部代表非金属,暗部代表金属。如此计算后会发现金属部分的高光会“染上”base color的颜色,在我们制作金子,黄铜,蓝钢,绿铜等金属时就会十分有效果;
同时在非金属的部分也会因为0.3的三维向量带有一点高光,这个值完全是自己觉得怎么好看怎么来,美术的经验值。
然后用spec跟fresnel做max,得到了比原先更亮的效果,全身更加油亮,也是很符合怪物的设定:
接下来部分就并没有什么特殊的地方:
环境漫反射用了简单的单色光,用球谐函数的效果会更好,但是考虑到moba类型游戏实际渲染出来只占到屏幕很小的地方,且环境变化不大,用单色光降低消耗是可以接受的。
轮廓光乘上了世界法线的g通道,模拟除了天光撒在了角色身上的效果。
最后再处理一下角色的透贴部分(披肩):
黑透白不透
要关闭背面剔除,否则效果出错:
最终效果:
以下是参考代码
1 Shader "Custom/Dota2" { 2 Properties 3 { 4 [Header(Texture)] 5 //注意某些图塞进了不同的纹理 6 _MainTex ("RGB:颜色 A:透贴", 2d) = "white"{} 7 _MaskTex ("R:高光强度 G:边缘光强度 B:高光染色 A:高光次幂", 2d) = "black"{} 8 _NormTex ("RGB:法线贴图", 2d) = "bump"{} 9 _MatelnessMask ("金属度遮罩", 2d) = "black"{} 10 _EmissionMask ("自发光遮罩", 2d) = "black"{} 11 _DiffWarpTex ("颜色Warp图", 2d) = "gray"{} 12 _FresWarpTex ("菲涅尔Warp图", 2d) = "gray"{} 13 _Cubemap ("环境球", cube) = "_Skybox"{} 14 15 [Header(DirDiff)] 16 _LightCol ("光颜色", color) = (1.0, 1.0, 1.0, 1.0) 17 [Header(DirSpec)] 18 _SpecPow ("高光次幂", range(0.0, 99.0)) = 5 19 _SpecInt ("高光强度", range(0.0, 10.0)) = 5 20 [Header(EnvDiff)] 21 _EnvCol ("环境光颜色", color) = (1.0, 1.0, 1.0, 1.0) 22 [Header(EnvSpec)] 23 _EnvSpecInt ("环境镜面反射强度", range(0.0, 30.0)) = 0.5 24 [Header(RimLight)] 25 [HDR]_RimCol ("轮廓光颜色", color) = (1.0, 1.0, 1.0, 1.0) 26 _RimInt ("轮廓光强度", range(0.0, 5.0)) = 1.0 27 [Header(Emission)] 28 _EmitInt ("自发光强度", range(0.0, 10.0)) = 1.0 29 [HideInInspector] 30 _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 31 [HideInInspector] 32 _Color ("Main Color", Color) = (1.0, 1.0, 1.0, 1.0) 33 } 34 SubShader 35 { 36 Tags 37 { 38 "RenderType"="Opaque" 39 } 40 Pass 41 { 42 Name "FORWARD" 43 Tags 44 { 45 "LightMode"="ForwardBase" 46 } 47 Cull off 48 49 CGPROGRAM 50 #pragma vertex vert 51 #pragma fragment frag 52 #include "UnityCG.cginc" 53 //追加unity投影CG 54 #include "AutoLight.cginc" 55 #include "Lighting.cginc" 56 57 #pragma multi_compile_fwdbase_fullshadows 58 #pragma target 3.0 59 //声明变量 60 uniform sampler2D _MainTex; 61 uniform sampler2D _MaskTex; 62 uniform sampler2D _NormTex; 63 uniform sampler2D _MatelnessMask; 64 uniform sampler2D _EmissionMask; 65 uniform sampler2D _DiffWarpTex; 66 uniform sampler2D _FresWarpTex; 67 uniform samplerCUBE _Cubemap; 68 // DirDiff 69 uniform float3 _LightCol; 70 // DirSpec 71 uniform half _SpecPow; 72 uniform half _SpecInt; 73 // EnvDiff 74 uniform half3 _EnvCol; 75 // EnvSpec 76 uniform half _EnvSpecInt; 77 // RimLight 78 uniform half3 _RimCol; 79 uniform half _RimInt; 80 // Emission 81 uniform half _EmitInt; 82 // Other 83 uniform half _Cutoff; 84 //输入结构 85 struct VertexInput 86 { 87 float4 vertex : POSITION; // 顶点信息 88 float2 uv0 : TEXCOORD0; // UV信息 89 float4 normal : NORMAL; // 法线信息 90 float4 tangent : TANGENT; // 切线信息 91 92 }; 93 //输出结构 94 struct VertexOutput 95 { 96 float4 pos : SV_POSITION; // 屏幕顶点位置 97 float2 uv0 : TEXCOORD0; // UV0 98 float4 posWS : TEXCOORD1; // 世界空间顶点位置 99 float3 nDirWS : TEXCOORD2; // 世界空间法线方向 100 float3 tDirWS : TEXCOORD3; // 世界空间切线方向 101 float3 bDirWS : TEXCOORD4; // 世界空间副切线方向 102 LIGHTING_COORDS(5,6) // 投影相关 103 }; 104 //顶点shader 105 VertexOutput vert (VertexInput v) 106 { 107 VertexOutput o = (VertexOutput)0; // 新建输出结构 108 o.pos = UnityObjectToClipPos( v.vertex ); // 顶点位置 OS>CS 109 o.uv0 = v.uv0; // 传递UV 110 o.posWS = mul(unity_ObjectToWorld, v.vertex); // 顶点位置 OS>WS 111 o.nDirWS = UnityObjectToWorldNormal(v.normal); // 法线方向 OS>WS 112 o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz); // 切线方向 OS>WS 113 o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w); // 副切线方向 114 TRANSFER_VERTEX_TO_FRAGMENT(o) // 投影相关 115 return o; // 返回输出结构 //将输出结构 输出 116 } 117 //片元着色器 118 float4 frag(VertexOutput i) : COLOR 119 { 120 // 向量准备 121 half3 nDirTS = UnpackNormal(tex2D(_NormTex, i.uv0)); 122 half3x3 TBN = half3x3(i.tDirWS, i.bDirWS, i.nDirWS); 123 half3 nDirWS = normalize(mul(nDirTS, TBN)); 124 half3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS); 125 half3 vrDirWS = reflect(-vDirWS, nDirWS); 126 half3 lDirWS = _WorldSpaceLightPos0.xyz; 127 half3 lrDirWS = reflect(-lDirWS, nDirWS); 128 // 中间量计算,为后续光照模型准备 129 half ndotl = dot(nDirWS, lDirWS); 130 half ndotv = dot(nDirWS, vDirWS); 131 half vdotr = dot(vDirWS, lrDirWS); 132 //采样纹理 133 half4 var_MainTex = tex2D(_MainTex, i.uv0); 134 half4 var_MaskTex = tex2D(_MaskTex, i.uv0); 135 half var_MatelnessMask = tex2D(_MatelnessMask, i.uv0).r; 136 half var_EmissionMask = tex2D(_EmissionMask, i.uv0).r; 137 half3 var_FresWarpTex = tex2D(_FresWarpTex, ndotv); 138 //cubemap用高光次幂做lerp,越亮的地方反射越清晰 139 half3 var_Cubemap = texCUBElod(_Cubemap, float4(vrDirWS, lerp(8.0, 0.0, var_MaskTex.a))).rgb; 140 //提取信息 141 half3 baseCol = var_MainTex.rgb; 142 half opacity = var_MainTex.a; 143 half specInt = var_MaskTex.r; 144 half rimInt = var_MaskTex.g; 145 half specTint = var_MaskTex.b; 146 half specPow = var_MaskTex.a; 147 half matellic = var_MatelnessMask; 148 half emitInt = var_EmissionMask; 149 half3 envCube = var_Cubemap; 150 half shadow = LIGHT_ATTENUATION(i); 151 // 光照模型 152 // 菲涅尔 153 half3 fresnel = lerp(var_FresWarpTex, 0.0, matellic); 154 half fresnelCol = fresnel.r; 155 half fresnelRim = fresnel.g; 156 half fresnelSpec = fresnel.b; 157 // 光源镜面反射 158 half3 specCol = lerp(baseCol, half3(0.3, 0.3, 0.3), specTint) ; 159 half phong = pow(max(0.0, vdotr), _SpecPow);//specPow * 160 half spec = phong * max(0.0, ndotl); 161 spec = max(spec, fresnelSpec); 162 spec = spec * _SpecInt; 163 half3 dirSpec = specCol * spec * _LightCol; 164 165 // 光源漫反射 166 half3 diffCol = lerp(baseCol, half3(0.0, 0.0, 0.0), matellic); 167 half halfLambert = ndotl * 0.5 + 0.5; 168 half3 var_DiffWarpTex = tex2D(_DiffWarpTex, half2(halfLambert, 0.2)); 169 half3 dirDiff = diffCol * var_DiffWarpTex * _LightCol; 170 171 // 环境漫反射 172 half3 envDiff = diffCol * _EnvCol; 173 // 环境镜面反射 174 half reflectInt = max(fresnelSpec, matellic) * specInt; 175 half3 envSpec = specCol * reflectInt * envCube * _EnvSpecInt; 176 // 轮廓光 177 half3 rimLight = _RimCol * fresnelRim * rimInt *_RimInt* max(0.0, nDirWS.g); 178 // 自发光 179 half3 emission = diffCol * emitInt * _EmitInt; 180 // 混合最终结果 181 half3 finalRGB = (dirDiff + dirSpec) * shadow + envDiff + envSpec + rimLight + emission; 182 183 //透明剪裁(披风部分) 184 clip(opacity-_Cutoff); 185 //返回值 186 return float4 (finalRGB,1.0); 187 } 188 ENDCG 189 } 190 } 191 //声明回退shader 192 FallBack "Legacy Shaders/Transparent/Cutout/VertexLit" 193 194 }