|
本文为《Unity Shader入门精要》第十四章《非真实感渲染》的第一节内容《卡通风格的渲染》。
本文相关代码,详见:
原书代码,详见原作者github:
<hr/>非真实感渲染(Non-Photorealistic Rendering,NPR)的主要目标:使用一些渲染方法,使得画面达到和某些特殊的绘画风格相似的效果,例如:卡通、水彩风格等。
本章将学习两种常见的非真实感渲染方法:卡通风格的渲染、实时素描效果的渲染。本次笔记将学习卡通风格的渲染。
1. 概念原理
卡通风格的渲染共有的特点:黑色的线性描边、分明的明暗变化等。
实现卡通渲染的方法有很多,其中之一是:使用基于色调的着色技术(tone-based shading)。
- 漫反射部分:使用漫反射系数对一张一维纹理进行采样,以控制漫反射的色调;
- 高光部分:是一块块分界明显的纯色区域(与以往渐变过渡的高光不同);
- 额外:在物体边缘绘制轮廓(基于模型的描边方法)。
1.1 渲染轮廓线
《Real Time Rendering》第三版中,作者把这些方法分成5类:
使用视角方向和表面法线的点乘结果得到轮廓线的信息。
优点:简单快速,一个Pass即可得到渲染结果;
缺点:描边效果不尽如人意。
2个Pass渲染,第一个Pass渲染背面的面片,通过默写技术让轮廓可见,第二个Pass正常渲染背面的面片。
优点:快速有效,适用于表面平滑的模型;
缺点:不适合平整的模型。
基于图像后处理,在所有物体渲染后,通过边缘检测判定轮廓。
优点:适用于任何种类的模型;
缺点:深度和法线变换很小的轮廓无法被检测出来,例如:桌面上的纸张。
通过判定两个相邻三角面片,是否一个朝正面,一个朝反面来判定是否为轮廓边。
优点:1. 前几种渲染最大的问题——无法控制轮廓线的风格渲染,但是这种方法可以;2. 精准;
缺点:1. 实现相对复杂;2. 由于是逐帧单独提取轮廓,帧与帧之间会出现跳跃性。
例如:先找到精确的轮廓边,把模型和轮廓边渲染到纹理中,再使用图像处理的方法识别出轮廓线,并在图像空间下进行风格化渲染。
本节将会在Unity中使用过程式几何轮廓渲染的方法进行轮廓描边,在第一个Pass中使用轮廓线颜色渲染整个背面的面片,并在视角空间下把模型顶点沿着法线方向向外扩展一段距离,以此让背面轮廓线可见:
viewPos = viewPos + viewNormal * _Outline;
但对于内凹的模型,会发生背面面片遮挡正面面片的情况。降低这种情况发生的可能,在扩张背面顶点之前,首先对顶点法线的z分量进行处理,使其等于一个定值,然后把法线归一化后再对顶点进行扩张,这样扩张后的背面更加扁平化:
viewNormal.z = -0.5;
viewNomal = normalize(viewNormal);
viewPos = viewPos + viewNormal * _Outline;
1.2 添加高光
卡通风格中的高光是模型上一块块分界明显的纯色区域。因此不能在使用之前学习的光照模型,回顾一下之前实现的Blinn-Phong模型:
// 使用法线点乘光照方向以及视角方向和的一半,再和另一个参数进行指数运算得到高光发射
float specular = pow(max(0, dot(normal, halfDir)), _Gloss);
为了得到分界明显的效果,我们同样需要计算normal和halfDir的点乘结果,但不同的是通过这个结果和阈值进行比较,是最终返回结果是0或1:
float specular = dot(worldNormal, worldHalfDir);
// step函数:CG内置函数
// 如果spec比threshold大,则返回1;否则返回0
spec = step(threshold, spec);
但这种粗暴的判断方法会在高光区域的边界产生锯齿,因为高光边缘不是平滑渐变的,而是0到1突变的,因此需要在边界处很小的一块区域进行平滑处理:
float spec = dot(worldNormal, worldHalfDir);
// smoothstep函数:CG内置函数
// w是很小的值,当spec - threshold小于-w时,返回0;当大于w时,返回1;否则在0~1之间差值
// 这样可以让高光区域在阈值上下很小的范围实现渐变
spec = lerp(0, 1, smoothstep(-w, w, spec - threshold);
w会被设为很小的值,本次案例中w被设置为邻域像素之间的近似导数值,通过CG的fwidth函数来得到。
fwidth(c) ==> abs(ddx(c)) + abs(ddy(c))
当代GPU在像素化的时候一般是以2x2像素为基本单位,那么在这个2x2像素块当中,右侧的像素对应的fragment的x坐标减去左侧的像素对应的fragment的x坐标就是ddx;下侧像素对应的fragment的坐标y减去上侧像素对应的fragment的坐标y就是ddy,ddx和ddy代表了相邻两个像素在设备坐标系当中的距离。
摘自知乎:https://www.zhihu.com/question/329521044/answer/717906456 从上面的引用可看出,fwidth就是临近像素级大小的宽度,本案例使用这个方法,就是为了让w被设置为一个足够小的值。
卡通渲染中高光往往有更多个性化的需要,更多非真实渲染的效果可前往原书作者的CSDN博客:https://blog.csdn.net/candycat1992/article/details/47284289。
2. 案例:卡通化渲染
2.1 效果预览
2.2 准备工作
完成如下准备工作:
- 新建名为Scene_14_1的场景,并去掉天空盒;
- 往场景中拖入一个Suzanne模型,可从文章开头的工程中找;
- 新建名为ToonShadingMat的材质,并赋给上一步的模型;
- 新建名为Chapter14-ToonShading的Unity Shader,并赋给上一步的材质;
- 保存场景。
2.3 编写Shader:Chapter14-ToonShading
编写代码如下:
Shader &#34;Unity Shaders Book/Chapter 14/Toon Shading&#34;
{
Properties
{
_Color (&#34;Color Tint&#34;, Color) = (1.0, 1.0, 1.0, 1.0)
_MainTex (&#34;Main Tex&#34;, 2D) = &#34;white&#34; {}
_Ramp (&#34;Ramp Texture&#34;, 2D) = &#34;white&#34; {}
_Outline (&#34;Outline&#34;, Range(0.0, 1.0)) = 0.1
_OutlineColor (&#34;Outline Color&#34;, Color) = (0.0, 0.0, 0.0, 1.0)
_Specular (&#34;Specular&#34;, Color) = (1.0, 1.0, 1.0, 1.0)
_SpecularScale (&#34;Specular Scale&#34;, Range(0.0, 0.1)) = 0.01
}
SubShader
{
// 背面渲染Pass
Pass
{
// 为Pass命名,方便后续复用
NAME &#34;OUTLINE&#34;
// 只渲染背面面片
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include &#34;UnityCG.cginc&#34;
// 控制轮廓线宽度
float _Outline;
// 控制轮廓线颜色
fixed4 _OutlineColor;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert (a2v v)
{
v2f o;
float4 worldPos = mul(UNITY_MATRIX_MV, v.vertex);
float3 worldNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
// 将z分量设置为某个固定值,降低内凹模型的背面面片挡住正面面片的可能性
worldNormal.z = -0.5;
// 顶点位置沿法线方向偏移一定距离
worldPos = worldPos + float4(normalize(worldNormal), 0) * _Outline;
o.pos = mul(UNITY_MATRIX_P, worldPos);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(_OutlineColor.rgb, 1);
}
ENDCG
}
// 正面渲染Pass
Pass
{
Tags
{
&#34;LightMode&#34; = &#34;ForwardBase&#34;
}
// 只渲染正面
Cull Back
CGPROGRAM
#include &#34;UnityCG.cginc&#34;
#include &#34;AutoLight.cginc&#34;
#include &#34;Lighting.cginc&#34;
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
// 控制漫反射色调的渐变纹理
sampler2D _Ramp;
// 高光颜色
fixed4 _Specular;
// 控制高光反射时使用的阈值
float _SpecularScale;
struct a2v
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
// 世界空间的法线方向
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
// 世界空间的顶点位置
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
// 世界空间的光照方向
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// 世界空间的视角方向
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 上面两个方向的中间方向,用于计算半兰伯特漫反射系数
fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
// 主纹理采样
fixed4 c = tex2D(_MainTex, i.uv);
// 颜色叠加
fixed3 albedo = c.rgb * _Color.rgb;
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
// 光照衰减
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 法线、光照方向点乘,计算漫反射系数
fixed diff = dot(worldNormal, worldLightDir);
diff = (diff * 0.5 + 0.5) * atten;
// 漫反射:光照颜色 * 主纹理颜色 * 漫反射色调控制
fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
// 高光
fixed spec = dot(worldNormal, worldHalfDir);
// 通过fwidth函数,让w被设置为一个很小的值
fixed w = fwidth(spec) * 2.0;
// 阈值控制高光边缘渐变
float stepFactor = smoothstep(-w, w, spec + _SpecularScale - 1);
// 最后使用step函数,为了在_SpecularScale为0时,可以完全消除高光反射的光照
fixed3 specular = _Specular.rgb * lerp(0, 1, stepFactor) * step(0.0001, _SpecularScale);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback &#34;Diffuse&#34;
}
2.4 配置材质
配置好如下材质,即可得到最终的效果:
以上是本次笔记的所有内容,下一篇笔记我们将学习《素描风格的渲染》的相关知识。
<hr/>
写在最后
本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder) |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|