【NPR】卡通渲染
来源:互联网 发布:linux mount 编辑:程序博客网 时间:2024/04/26 11:12
写在前面
我的博客讲过好几篇卡通渲染了,比如【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,这篇论文提出来的。作者提出,色调可以由混合两个颜色,冷调颜色
作者在论文中提到了使用蓝色
其中,
在NPR实验室项目中,场景ToneBasedShadingScene实现了上述的渲染方法:
当然,这里只是为了实现下论文中的方法。在实际渲染中,我们会直接使用一张渐变纹理进行采样,来得到漫反射颜色。
风格化的卡通高光
最后,我们来讲一下如何对卡通高光进行风格化的渲染。在之前的实现中,我们通过判断高光因子的范围来得到一块纯白的高光区域。但很多卡通动画中,高光区域是非常风格化的。2003年的Stylized highlights for cartoon rendering and animation论文中提到了一种算法来风格化高光区域,实现对高光区域的平移、旋转、缩放、分块和方块化。如下图所示:
它的主要思想是对Blinn-Phong模型中的半向量(half vector)进行一些修改操作,然后再判断半向量和法线方向点乘结果的范围,实现高光区域
因此,重点就是如何对半向量
定义平移操作
其中
// TranslationtangentHalfDir = tangentHalfDir + fixed3(_TranslationX, _TranslationY, 0);tangentHalfDir = normalize(tangentHalfDir);
其中_TranslationX和_TranslationY是用户可调参数,用于控制x和y方向的高光区域平移程度。
旋转操作
// Ratationfloat xRad = _RotationX * DegreeToRadian;float3x3 xRotation = float3x3(1, 0, 0, 0, cos(xRad), sin(xRad), 0, -sin(xRad), cos(xRad));float yRad = _RotationY * DegreeToRadian;float3x3 yRotation = float3x3(cos(yRad), 0, -sin(yRad), 0, 1, 0, sin(yRad), 0, cos(yRad));float zRad = _RotationZ * DegreeToRadian;float3x3 zRotation = float3x3(cos(zRad), sin(zRad), 0, -sin(zRad), cos(zRad), 0, 0, 0, 1); tangentHalfDir = mul(zRotation, mul(yRotation, mul(xRotation, tangentHalfDir)));
其中_RotationX、_RotationY和_RotationZ是用于控制半向量绕x轴、y轴和z轴的旋转角度。实际上,绕x轴和y轴的旋转和平移效果很类似,我们通常只需要调整z轴的旋转即可。
接着是缩放操作。例如,绕x轴的缩放为:
通过控制
// ScaletangentHalfDir = tangentHalfDir - _ScaleX * tangentHalfDir.x * fixed3(1, 0, 0);tangentHalfDir = normalize(tangentHalfDir);tangentHalfDir = tangentHalfDir - _ScaleY * tangentHalfDir.y * fixed3(0, 1, 0);tangentHalfDir = normalize(tangentHalfDir);
其中_ScaleX和_ScaleY用于控制高光区域在x方向和y方向上的缩放,范围都是0到1。
下面是分块操作(split)。分块操作会把一个高光区域分成两个分离的区域,它的公式其实就是缩放公式的修改版:
其中,
// Splitfixed signX = 1;if (tangentHalfDir.x < 0) { signX = -1;}fixed signY = 1;if (tangentHalfDir.y < 0) { signY = -1;}tangentHalfDir = tangentHalfDir - _SplitX * signX * fixed3(1, 0, 0) - _SplitY * signY * fixed3(0, 1, 0);tangentHalfDir = normalize(tangentHalfDir);
其中_SplitX和_SplitY用于控制高光区域在x方向和y方向上的分离程度。
最后一个操作是方块化操作(squaring)。这是最复杂的一个操作,公式如下:
不过按照上面的公式计算我总是无法调整得到希望的方块形……我稍微更改了下,不计算两个角度的最小值,而是同时使用两个角度。着色器代码如下:
// Squarefloat sqrThetaX = acos(tangentHalfDir.x);float sqrThetaY = acos(tangentHalfDir.y);fixed sqrnormX = sin(pow(2 * sqrThetaX, _SquareN));fixed sqrnormY = sin(pow(2 * sqrThetaY, _SquareN));tangentHalfDir = tangentHalfDir - _SquareScale * (sqrnormX * tangentHalfDir.x * fixed3(1, 0, 0) + sqrnormY * tangentHalfDir.y * fixed3(0, 1, 0));tangentHalfDir = normalize(tangentHalfDir);
其中_SquareScale控制方块的大小,_SquareN控制方块的尖锐程度。方块化的调整很tricky,一不小心就会出现些不好的效果。
在实现过程中,这些操作也是有顺序的,通常实现顺序是:缩放,旋转,平移,分块,方块化。
在NPR实验室项目中,场景StylizedHighlightsScene实现了上述的渲染方法:
写在最后
NPR实验室项目目前还包括了一些铅笔风格的实现,有时间再总结吧。希望大家有所收获,就这样~
- NPR-卡通渲染
- 【NPR】卡通渲染
- 非真实渲染技术(NPR)-1.卡通渲染
- NPR-Hatching素描渲染
- 【NPR】非真实感渲染实验室
- 【NPR】非真实感渲染实验室
- 【NPR】非真实感渲染实验室
- 卡通渲染最新进展汇报
- ShaderSimpler(3) : 卡通渲染
- 卡通渲染效果脚本
- 卡通渲染简报
- NPR技术(2)—Hatching 素描渲染
- 【NPR】漫谈轮廓线的渲染
- 【NPR】Unity3D非真实感渲染----铅笔画滤镜
- 卡通渲染的一点心得
- 卡通渲染进展(一)
- 卡通渲染效果shard-1
- 卡通渲染效果shard-2
- 李沐等:MXNet设计和实现简介
- 特征提取http://blog.csdn.net/passball/article/details/5204132
- Python脚本自动化编译RPM包
- hihocoder 1087 : Hamiltonian Cycle
- sql分组排序取top n
- 【NPR】卡通渲染
- 计算机网络 一个http请求的详细过程
- 无线路灯项目——STM32L151概述
- Android快速开发系列 10个常用工具类
- ccf 201509-3 模板生成系统
- hihocder 1088 : Right-click Context Menu
- 快速排序
- 用人需求分析
- CF 334 div.2-C/div.1-A/603A Alternative Thinking