卡通风格渲染,或者叫非真实感渲染(NPR),是游戏中常用的渲染方法,本文主要参考Unity入门精要和一些网上资料对这块内容进行初步了解和学习


卡通风格着色

相较于传统的渲染方式,卡通风格的渲染的色差会更为明显,在Shader中这一改变主要是通过diffuse的结果进行处理,将NDotL的结果进行离散化并通过Smoothstep将边缘进行柔化

通过控制SmoothStep的阈值和区间大小,我们可以控制光影的过渡效果

此外,我们常会用到一张RampTexture实现漫反射的颜色梯度变化

1
2
3
fixed halfLambert = 0.5* dot(lightDir, worldNormal) + 0.5;

fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb;


卡通风格高光

对于卡通风格的高光,我们需要和漫反射进行相似的处理将NDotH进行离散化处理

fixed spec = dot(worldNormal, halfDir);
fixed w = fwidth(spec) * 2.0;
//这边Smoothstep的渐变区域大小由邻近域像素的近似导数值决定,让高光边缘部分更平滑
fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1))
* step(0.0001, _SpecularScale);

可以看到高光边缘部分是比较平滑的


边缘光

在卡通渲染中,许多元素都需要加上边缘光的效果,需要基于视线和物体表面法线来实现

边缘光的表现为视线与物体表面垂直时,边缘光强度高,平行时强度低,并且在暗面不会产生,此外我们也进行边缘的平滑我的可以得到如下代码

1
2
3
4
5
6
float NDL = saturate(dot(worldNormal, worldLightDir));
NDL = NDL > 0 ? 1 : 0;
half4 lightIntensity = NDL * _LightColor0;
float rimVal = 1- saturate(dot(viewDir ,worldNormal));
rimVal = smoothstep(_RimThreshold - _RimSmooth, _RimThreshold + _RimSmooth, rimVal);
half4 rim = rimVal * _RimColor * NDL;


渲染轮廓线

​ 在卡通渲染中,我们需要在物体边缘部分绘制轮廓,绘制轮廓的方法的大致可以分为三种

  1. 基于视角方向的描边:思路主要是将视角方向和表面法线点乘来判断是否是边缘,然后对边缘进行轮廓线绘制,该方法只需要一个Pass,效果如下

    可以看到效果不佳,并不能体现完整的轮廓线效果

  2. 基于过程几何方法的描边:思路主要是渲染两层Pass,一层是本体,一层进行处理过的本体的背面,而这层渲染背面的Pass用于描边。这部分处理可以分为两种,一种是在调节物体观察空间的Z值,让物体更接近相机,然后渲染背面效果如下

    可以看到效果还可以,但是不同位置的物体会有不一样的轮廓线表现

    ​ 第二种是让物体沿着法线向外扩张来描边,效果如下

    ​ 该方法有相机拉近后描边会变粗的问题,要解决这一问题需要将扩张转移到NDC空间进行解决,代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    float4 pos = UnityObjectToClipPos(v.vertex);
    float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz);
    //这边乘以pos.w是为了抵消光栅化后偏移量会除以pos.w,使得计算结果保持不变
    float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w;
    //为了让描边均匀,需要按照屏幕宽高比进行调整,这里通过近裁面右上坐标(PS:在Reverse-Z模式下,近裁面z为1,坐标为1,1,1)相机空间的位置来获得宽高比
    float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));
    float aspect = abs(nearUpperRight.y / nearUpperRight.x);
    float2 offset = float2(ndcNormal.x * aspect, ndcNormal.y);
    o.pos = pos;
    //可以通过顶点颜色R通道来控制轮廓线粗细
    o.pos.xy += offset * v.color.r *_Outline * _OUTLINE_WIDTH_BASE;
    //在实际项目中会通过顶点颜色G通道来把轮廓线往远离相机方向推,以此解决背面面片遮挡正面面片的问题
    //ShoveOutlineDepth(o.pos.z, v.color.g);
  3. 通过边缘检测实现描边:思路是屏幕后处理中计算得到的边缘来作为轮廓线,这边也有两种方式一种是卷积,一种是通过相机深度图