3D游戏引擎系列(六):卡通渲染
 Cel Shading是卡通渲染一种,卡通渲染尤其是在手机游戏中应用非常广泛。由于手机硬件的限制,美工为了优化资源,节约模型的面数,通常设计场景和角色时,一般都设计成卡通效果。程序员通过GPU编程对材质进行渲染,要实现卡通渲染首先要了解其实现原理,其核心思想主要是对要进行卡通渲染的对象进行边界绘制。卡通游戏种类很多,比如2头身比例,3头身比例等,3D卡通模型的渲染主要是通过材质表现,这样在模型面数相对减少的情况下并不影响其表现效果,所以非常适用于移动端游戏开发。卡通设计效果举例如下图:
实现方式:新建一个文本文件,将其扩展名字改成.fx。Shader完整内容如下:
| 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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | float4x4 matWorldViewPrj;  float4x4 matWorld;  float3 lightPos = float3(0,60,-60);  floatlightAttenuation = 0.01;    float3 eyePos = float3(0,200,400);    float4 mtlDiffuseColor = float4(1,1,1,1);    texture texCartoon;  texture texCartoonEdge;    //--  structVS_INPUT  {      float3 pos : POSITION;      float3 normal : NORMAL;  };    structVS_OUTPUT  {      float4 pos : POSITION;      float3 worldPos : TEXCOORD0;      float3 normal : TEXCOORD1;  };    VS_OUTPUT my_vs(VS_INPUT vert)  {      VS_OUTPUT vsout;            vsout.pos = mul(float4(vert.pos,1),matWorldViewPrj);      vsout.worldPos = mul(float4(vert.pos,1),matWorld);      vsout.normal = normalize(mul(vert.normal, matWorld));            returnvsout;  }    sampler CartoonSampler = sampler_state  {      Texture = ;      MinFilter = Point;      MagFilter = Point;      MipFilter = None;      AddressU  = Clamp;      AddressV  = Clamp;  };    sampler CartoonEdgeSampler = sampler_state  {      Texture = ;      MinFilter = Point;      MagFilter = Point;      MipFilter = None;      AddressU  = Clamp;      AddressV  = Clamp;  };    //--  float4 my_ps(float3 worldPos : TEXCOORD0,               float3 normal : TEXCOORD1) : COLOR  {      float4 color;            //-- ambient      floatlum = 0.2f;            //-- diffuse      normal = normalize(normal);            float3 L = lightPos - worldPos;      floatd = length(L);      L /= d; // normalize L            floatatten = 1/(lightAttenuation*d);      floatdiff = saturate(dot(normal, L));            lum += diff * atten;            float4 cartoonColorDiff = tex2D(CartoonSampler,lum);            color = cartoonColorDiff*mtlDiffuseColor;            //-- edge      float3 V = normalize(eyePos - worldPos);      floate = (dot(normal,V));      float4 edgeColor = tex2D(CartoonEdgeSampler,e);            color *= edgeColor;            returncolor;  }    //--  technique my_tech  {      pass p0      {          VertexShader = compile vs_1_1 my_vs();          PixelShader = compile ps_2_0 my_ps();      }  }   | 
针对上述代码解释一下,变量声明:
表示的是输入卡通化的纹理
表示的是输入的卡通边缘的纹理,其他变量声明上节已经介绍过了,这里就不重复了。
表示的是纹理采样,为了让大家更容易理解,结合着Unity的纹理设置。效果如下图:
下面是卡通纹理采样和纹理边缘取样,这两个是最重要的实现,它们调用的是Shader的接口函数tex2D实现的。
卡通纹理的像素着色器是在如下函数实现的:
下面的代码片段是用物体上的材质法线和灯光的照射方向进行点乘计算,从而得到物体上的颜色。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | float4 color;            //-- ambient      floatlum = 0.2f;            //-- diffuse      normal = normalize(normal);            float3 L = lightPos - worldPos;      floatd = length(L);      L /= d; // normalize L            floatatten = 1/(lightAttenuation*d);      floatdiff = saturate(dot(normal, L));            lum += diff * atten;            float4 cartoonColorDiff = tex2D(CartoonSampler,lum);            color = cartoonColorDiff*mtlDiffuseColor; | 
物体边缘渲染是通过法线计算得到的,最后使用纹理取样函数tex2D得到的最终的物体边缘颜色,代码片段如下所示:
| 1 2 3 4 5 | float3 V = normalize(eyePos - worldPos);      floate = (dot(normal,V));      float4 edgeColor = tex2D(CartoonEdgeSampler,e);            color *= edgeColor;   | 
这两张贴图在加载Shader文件之前都会被程序事先读取到内存里,Shader文件加载上节已经写过了,在这里就不重复了,直接上图片文件的加载,使用的是Direct3D的库函数代码如下:
文件加载使用的是Direct3D提供的接口:D3DXCreateTextureFromFile函数,接下来把Shader文件需要的参数通过C++代码传递给Shader文件,最后在GPU中执行。函数核心代码如下所示:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | floatt = timeGetTime()/4000.0f;      floatr = 150;      m_lightPos.x = sinf(t) * r;      m_lightPos.z = cosf(t) * r;      m_lightPos.y = sinf(t) * r;;        hr = m_pEffect->SetFloatArray("lightPos",(float*)&m_lightPos,3);      hr = m_pEffect->SetFloatArray("eyePos",(float*)&g_camera.getEyePos(),3);      hr = m_pEffect->SetTexture("texCartoon",m_pCartoonTex);      hr = m_pEffect->SetTexture("texCartoonEdge",m_pCartoonEdgeTex);            D3DXMATRIX matWorld;      D3DXMatrixIdentity(&matWorld);        D3DXMATRIX matWorldViewProj = matWorld * g_camera.getViewMat() * g_camera.getProjectMat();      hr = m_pEffect->SetMatrix("matWorldViewPrj",&matWorldViewProj);      hr = m_pEffect->SetMatrix("matWorld",&matWorld); | 
其中参数m_lightPos与上节的用法一样,表示灯光可以在场景中旋转运动。使用C++的接口SetFloatArray函数把Shader脚本的参数传递进来,再使用SetTexture函数把图片传递给Shader脚本文件用于纹理采样。这个与Unity中的纹理渲染一样,只是Unity编辑器提供了供用户直接拖放的接口,最后使用SetMatrix函数接口把3D世界空间中的矩阵传递给GPU去计算。实现的最终效果图:
在光线正面照射情况下,物体的材质颜色是明亮的,背面是比较暗的,这个是通过物体表面法线与灯光的夹角计算得到的。


