Unity3D教程:如何实现水墨渲染效果
在一些中国风的游戏项目中,需要做水墨风格,那么那种水墨渲染的效果要如何才能实现,下面就给大家介绍下在Unity3D中水墨渲染效果的实现方法,一起来看看吧。
一、前言
本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。
以下部分着色器代码参考了网上和一些书籍里的着色器技术。
二、概述
首先让我们来看一下我们要实现的最终效果:
三、原理
那么在这里,我是通过类似于卡通着色的技术来实现水墨的渲染效果。先考虑卡通着色技术,我们先实现描边的功能,描边的方法有很多种,各有优缺点,很难找出一个完美的适用任何情景的技术,在这里我们通过沿法线放大模型,绘制一层背影图,然后再在其上叠加未放大的正常缩放的模型来实现描边功能。
在水墨风格中,光线和阴影的关系并不严格遵守物理规则,甚至可以说明暗浓淡的着色完全跟绘画者的构图有关系,绘制者想要表现某种层次感,可能说近处的物体着墨更多,更浓,细节更突出,而远处则寥寥几笔带过(当然这些是我的个人理解^^)。所以我在这里并未选用Lambert或者Phong光照模型。使用来计算物体表面的光暗颜色,其中N表示顶点的法线量,V是指顶点指向视点的方向向量。完成计算,得到diffuse后,通过预定义的亮度表(一张纹理图)来从中取出对应颜色值。
在完成了上述两个步骤后,我们把输入的漫反射贴图转换成黑白色,通过使用公式diff=r*0.33+g*0.33+b*0.33来完成计算。当然你可以修改其中的权重来达到不同的效果。上述公式通过点乘来简化。
四、实现
首先用一个Pass来实现描边效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | Pass{ Tags{ "RenderType" = "Opaque" } cull front lighting off ZWrite on CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float _outline; struct appdata { float4 vertex:POSITION; float3 normal:NORMAL; }; struct v2f { float4 pos:POSITION; }; v2f vert(appdata v) { v2f o; float4 pos = mul(UNITY_MATRIX_MV, v.vertex); float3 normal = mul(UNITY_MATRIX_IT_MV, v.normal); normal.z = -0.5f; float depth = -pos.z/pos.w*0.01f; _outline = clamp(depth, 0, _outline); pos = pos + float4(normalize(normal), 0)*_outline; o.pos = mul(UNITY_MATRIX_P, pos); return o; } float4 frag(v2f i):COLOR { return float4(0,0,0,1); } ENDCG } |
有以下几点需要注意:
1、我们是在相机空间进行缩放顶点的操作,那么在将法线normal转换到相机空间时,需要使用UNITY_MATRIX_IT_MV矩阵,而不是UNITY_MATRIX_MV矩阵。因为法线通过UNITY_MATRIX_MV矩阵转换到相机空间后不会再与顶点的切向量垂直了,更详细的推导过程请大家自行查看相关书籍。
2、_outline是一个可变参数,用于调节描边的粗细,这里加入了一个深度值来用于防止_outline太大而视点太靠近模型而出现的不理想渲染结果。
然后我们考虑使用第二个Pass实现水墨渲染效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | Pass { Tags { "RenderType" = "Opaque" } cull back lighting on CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; sampler2D _black; sampler2D _gray; sampler2D _depth; float4 _Color; float _ink; struct appdata { float4 vertex : POSITION; fixed3 normal : NORMAL; half2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : POSITION; half2 uv : TEXCOORD0; float vdotn : TEXCOORD1; float2 depth: TEXCOORD2; }; v2f vert (appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = v.texcoord; float3 viewDir = normalize( mul(_World2Object, float4(_WorldSpaceCameraPos.xyz, 1)).xyz - v.vertex); o.vdotn = dot(normalize(viewDir),v.normal); o.depth = o.vertex.zw; return o; } fixed4 frag (v2f i) : COLOR { fixed4 diff = tex2D(_MainTex, i.uv)*_Color; diff.xyz = dot(diff.xyz, float3(0.33f, 0.33f, 0.33f)); float f = pow(i.vdotn,_ink); fixed4 inkCol = fixed4(1,1,1,1); float depth = Linear01Depth(i.depth.x/i.depth.y); float depthCol = tex2D(_depth, float2(depth, 0.5f)); if (f<0.25) { float2 findColor = float2(f*2.5f, i.uv.x*0.5f+i.uv.y*0.5f); //float2 findColor = float2(f,0.5f); inkCol = tex2D(_black, findColor); } else { float2 findColor = float2(f,0.5f); //if(depthCol < 1) inkCol = tex2D(_gray, findColor); } inkCol *= depthCol; return diff*inkCol; } ENDCG } |
以上代码在优化方面可能有待改进,我们来关注几点要点:
1、_black贴图映射的颜色值更黑,_gray贴图映射的颜色值较淡,之所以用两张,而不是在一张中完成映射操作,是因为这两个映射时计算UV的方式不一样,这会导致不同的效果,_black贴图的映射方式取出的颜色值变化更匀称。
2、这里我们仍然使用到了深度值,这里是想实现一种效果:随着物体越来越远,直到其超出某种范围后,我们就将其颜色退化为偏白色,以表示远处的风景,使之随着深度的变化有层次感。Linear01Depth函数是将经过透视投影变换的Z值还原到[0, 1]区间,0表示近裁剪面,1表示远裁剪面。