【NPR】卡通渲染

写在前面我的博客讲过好几篇卡通渲染了,比如【Unity Shader实战】卡通风格的Shader(一)、【Unity Shader实战】卡通风格的Shader(二)、【NPR】漫谈轮廓线的渲染、【Shader拓展】Illustrative Rendering in Team Fortress 2。后来,我搞了个所谓的NPR实验室,来实现一些论文里或者网络博客里讲到的NPR渲染算法,这里面包含了一些卡
摘要由CSDN通过智能技术生成

写在前面

我的博客讲过好几篇卡通渲染了,比如【Unity Shader实战】卡通风格的Shader(一)【Unity Shader实战】卡通风格的Shader(二)【NPR】漫谈轮廓线的渲染【Shader拓展】Illustrative Rendering in Team Fortress 2。后来,我搞了个所谓的NPR实验室,来实现一些论文里或者网络博客里讲到的NPR渲染算法,这里面包含了一些卡通风格的渲染。这篇文章主要想介绍一下这个项目里的一些卡通渲染的方法。包括:

  • 一个最常见的包含了卡通风格的漫反射+高光的场景。
  • 基于色调的卡通渲染。
  • 一种风格化的卡通高光的计算方法。

以下所有图片和代码均出自github上的NPR Labs项目,使用Unity 5.x进行实现,如果你有兴趣的话可以贡献或下载。下面如果出现代码的话均是相关的着色器代码(通常是片元着色器)。

最常见的卡通渲染

卡通渲染的特点通常有三个:一般物体轮廓处有黑色描边;漫反射呈现明显的色块,而不是渐变;高光区域通常是一块突变的白色亮块。

描边

卡通渲染的一个特点是描边。关于描边的方法可以参见【NPR】漫谈轮廓线的渲染一文,在后面的实现中,我们主要选择过程式几何轮廓渲染的方法,即使用两个Pass,第一个Pass只渲染背面,把法线扁平化后再沿着法线方向扩张顶点,使得背部区域可见,再把这部分区域输出成轮廓线颜色即可。主要代码如下:

v2f vert (a2v v) {
    v2f o;

    float4 pos = mul(UNITY_MATRIX_MV, v.vertex); 
    float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
    normal.z = -0.5;
    pos = pos + float4(normalize(normal), 0) * _Outline;
    o.pos = mul(UNITY_MATRIX_P, pos);

    return o;
}

float4 frag(v2f i) : SV_Target { 
    return float4(_OutlineColor.rgb, 1);               
}

第二个Pass即可以进行正常的渲染流程。这种方法简单而且对大部分模型都有比较好的健壮性,缺点是不适用于正方体这样扁平的表面和模型。

漫反射和高光反射

之前讲过,卡通渲染的漫反射呈现明显的色块,而不是渐变。这可以通过对法线和光源方向的点乘结果进行范围判断,使结果划分到固定的几个值(一般取三或四个值,模拟三层或四层渐变)。例如我们可以在片元着色器中这样写:

fixed diff = dot(worldNormal, worldLightDir);
diff = diff * 0.5 + 0.5;
if (diff < _DiffuseSegment.x) {
    diff = _DiffuseSegment.x;
} else if (diff < _DiffuseSegment.y) {
    diff = _DiffuseSegment.y;
} else if (diff < _DiffuseSegment.z) {
    diff = _DiffuseSegment.z;
} else {
    diff = _DiffuseSegment.w;
}

其中_DiffuseSegment为(0.1, 0.3, 0.6, 1.0)。在上面的代码中,我们首先计算半兰伯特值diff,然后判断它的范围并进行修改,最后,整个模型表面的diff值实际只有4个不同的值,对应了_DiffuseSegment。然后,我们再根据这个diff值进行漫反射颜色的计算即可。

fixed3 texColor = tex2D(_MainTex, i.uv).rgb;
fixed3 diffuse = diff * _LightColor0.rgb * _DiffuseColor.rgb * texColor;

上面做法的问题在于,在分段的边界处会有明显的锯齿,这是因为从值_DiffuseSegment.x到_DiffuseSegment.y这样的变化是突变的。为了进行抗锯齿,我们可以使用fwidth函数:

fixed w = fwidth(diff) * 2.0;
if (diff < _DiffuseSegment.x + w) {
    diff = lerp(_DiffuseSegment.x, _DiffuseSegment.y, smoothstep(_DiffuseSegment.x - w, _DiffuseSegment.x + w, diff));
//  diff = lerp(_DiffuseSegment.x, _DiffuseSegment.y, clamp(0.5 * (diff - _DiffuseSegment.x) / w, 0, 1));
} else if (diff < _DiffuseSegment.y + w) {
    diff = lerp(_DiffuseSegment.y, _DiffuseSegment.z, smoothstep(_DiffuseSegment.y - w, _DiffuseSegment.y + w, diff));
//  diff = lerp(_DiffuseSegment.y, _DiffuseSegment.z, clamp(0.5 * (diff - _DiffuseSegment.y) / w, 0, 1));
} else if (diff < _DiffuseSegment.z + w) {
    diff = lerp(_DiffuseSegment.z, _DiffuseSegment.w, smoothstep(_DiffuseSegment.z - w, _DiffuseSegment.z + w, diff));
//  diff = lerp(_DiffuseSegment.z, _DiffuseSegment.w, clamp(0.5 * (diff - _DiffuseSegment.z) / w, 0, 1));
} else {
    diff = _DiffuseSegment.w;
}

在上面的代码中,我们首先使用fwidth函数计算了邻域内diff的梯度值w,我们将据此在分段的边界处的+-w范围内进行渐变混合,这个混合值既可以使用smoothstep函数也可以通过clamp函数计算而得。

对高光区域的渲染也是类似的。我们首先计算得到高光反射因子,再判断它的范围,如果超过了某个值,就把值直接设为1,对应了高光区域,否则值为0,没有任何高光。同样,为了进行抗锯齿,我们通过需要使用fwidth进行边界混合。主要代码如下:

fixed spec = max(0, dot(worldNormal, worldHalfDir));
spec = pow(spec, _Shininess);
w = fwidth(spec);
if (spec < _SpecularSegment + w) {
    spec = lerp(0, 1, smoothstep(_SpecularSegment - w, _SpecularSegment + w, spec));
} else {
    spec = 1;
}

fixed3 specular = spec * _LightColor0.rgb * _SpecularColor.rgb;

至此,我们就完成了一个最简单(或者说是原始)的卡通渲染。在NPR实验室项目中,这对应的场景是AntialiasedCelShadingScene:

这里写图片描述


基于色调的卡通渲染

在上面的实现中,我们是在着色器中判断漫反射因子的范围来实现大色块的渲染的。在实际的游戏制作中,我们一般是不会使用这种方法的,一方面是性能比较耗,更重要的是可控性比较弱。更实用的方法是用一张色调图(渐变图)来模拟漫反射的渐变。这种理论其实是由1998年的A Non-Photorealistic Lighting Model for Automatic Technical Illustration,这篇论文提出来的。作者提出,色调可以由混合两个颜色,冷调颜色 kcool 和暖调颜色 kwarm 来得到,公式是:

I=(1+ln2)kcool+(11+ln2)kwarm

作者在论文中提到了使用蓝色 kblue=(0,0,b),b[0,1]

  • 23
    点赞
  • 117
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值