本教程涵盖了卡通着色(也叫做卡通渲染)作为非真实感渲染技术的一个实例。
奶牛图标。本节中的所有图片都由Mariana Ruiz提供。
这是几个关于光照教程中的其中之一,这个光照超出了Phong反射模型的范围。但是,它是基于用章节“光滑镜面高光”中描述的Phong反射模型实现的逐像素光照。如果你还没有阅读过那章,建议你最好先读一下它。
非真实感渲染是计算机图形学中的一个非常宽泛的术语,它涵盖了所有的渲染技术,以及明显不同于实物照片外观的视觉风格。例子包括:孵化、概述、线性透视扭曲、粗抖动、粗颜色量化等。
卡通着色(也叫卡通渲染)是任何被用来实现一个卡通或手绘外观三维模型的非真实感绘制技术的子集。
一个指定视觉的着色器
皮克斯的John Lasseter曾在接受采访时说:“艺术挑战技术,技术激发艺术。”传统上用于描述三维对象的许多视觉风格和绘图技术实际上很难在着色器中实现。但是,我们根本没有理由不去尝试它。
当为指定视觉风格实现一个或多个着色器时,首先应该确定风格的特性哪些必须被实现。这是精确分析视觉风格例子的主要任务。没有这些例子,通常很难确认风格的特征。即使掌握某种风格的艺术家也往往无法恰当地描述这些特征;举个例子,因为他们不再意识到某些特征,或者认为某些特征是不必要的缺陷,不值得一提。
对于每一个特性,它应该被确定是否以及如何准确地实现它们。有些特性很容易实现,有些则很难通过程序员来实现或通过GPU来计算。因此,以John Lasseter的名言为精神的着色器程序员和(技术)艺术家之间的讨论通常是非常值得的,以决定哪些特性包括以及如何准确地重现它们。
非写实的镜面高光
跟在章节“光滑镜面高光”中实现的Phong反射模型相比较,本节图像中的镜面高光显然是白色的,没有任何其它颜色的添加。此外,它们还有一个非常锐化的边界。
通过计算Phong着色模型的高光项,以及设置片元颜色为镜面反射颜色乘以光源颜色(无衰减),如果镜面反射项大于某一阈值,比如最大强度的一半,我们可以实现这种程序式的镜面高光。
但是如果没有任何高光呢?通常,用户会为这种情况指定一个黑色镜面反射颜色;但是,用我们的方法会导致黑色的高光。解决这个问题的一个方法是考虑镜面反射的颜色,并且根据镜面颜色的不透明度将高光颜色和其它颜色“混合”。Alpha混合作为片元着色器的操作已经在章节“透明度”中描述过了。但是,如果所有的颜色都在片元着色器中已知,那么它也可以在片元着色器中计算。
在以下的代码片段中,假定fragmentColor
已经分配了一个颜色,比如基于漫射照明。镜面颜色_SpecColor
乘以光源颜色_LightColor0
,然后在镜面颜色的不透明度_SpecColor.a
上混合fragmentColor
:
if (dot(normalDirection, lightDirection) > 0.0
// light source on the right side?
&& attenuation * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess) > 0.5)
// more than half highlight intensity?
{
fragmentColor = _SpecColor.a
* _LightColor0.rgb * _SpecColor.rgb
+ (1.0 - _SpecColor.a) * fragmentColor;
}
这就足够了吗?如果你仔细观察上图中公牛的眼睛,你将会看到一对镜面高光,即有一个以上的光源导致了镜面高光。在大多数教程中,我们通过有加性混合的第二个渲染通道把额外的光源考虑进来。但是,如果镜面高光的颜色不应该被添加到其它颜色上的话,加性混合就不应该被使用。相反,有镜面高光不透明颜色(通常)和其它片元的透明片元的的alpha混合将是一个可行的解决方案。(查看章节“透明度”中关于alpha混合的描述。)
非写实漫射照明
上面公牛图像中的漫射光照只包含了两种颜色:照亮毛皮的浅棕色和无光毛皮的深棕色。公牛其它部分的颜色与灯光无关。
实现这个的一种方法就是不管Phong反射模型的漫反射项是否达到了某个阈值都使用完整的漫反射颜色,比如大于0以及第二个颜色小于0。对于公牛的毛皮,这两种颜色将会是不一样的;对于其它部分,它们将会是一样的,这样在有光和无光区域就没有视觉差别了。对于阈值_DiffuseThreshold
从深色_UnlitColor
转换到浅色_Color
(乘以光源颜色_LightColor0
)的实现可能看起来像这样:
float3 fragmentColor = _UnlitColor.rgb;
if (attenuation * max(0.0, dot(normalDirection, lightDirection))>= _DiffuseThreshold)
{
fragmentColor = _LightColor0.rgb * _Color.rgb;
}
这就是上面图像中关于非写实照明的所有内容吗?仔细看一下就会发现在深棕色和浅棕色之间有一条浅的不规则的线。实际上,情况甚至会更复杂,深棕色有时并不包括上述技术所覆盖的区域,并且有时它包括了更多甚至超越了黑色轮廓。它在视觉风格上添加了丰富的细节并创建了手工绘制的外观。另一方面,在着色器中很难重现这一点。
轮廓
很多卡通着色器的特征之一是沿着模型的轮廓以一种指定的颜色勾勒(通常是黑色的,但也会是其它颜色,可以看看上图中的牛)。
在着色器中有不同的技术来达到这种效果。Unity 3.3在标准资源中附带了这样一个着色器,它通过在轮廓的颜色渲染放大模型的背面来呈现这些轮廓(通过移动表面法向量上的顶点坐标来放大),然后在上面绘制正面。这里我们用另一基于章节“轮廓增强”中技术:如果一个片元被确定为接近轮廓(原文为silhouette),它就被设置为轮廓线(原文为outline)的颜色。这个只对光滑表面有用,并且它会生成不同厚度的轮廓线(略厚或略薄取决于视觉风格)。但是,至少轮廓的总厚度应该由着色器的属性来控制。
我们做完了吗?如果你仔细看一下上图中的驴,你会看到在它腹部和耳朵的轮廓线比其它轮廓要厚得多。这样表示无光的区域;但是,厚度上的改变是连续的。模拟这种效果的一个方法是让用户指定两种总的轮廓线厚度:一个用于光照充足的区域,一个用作无光的区域(根据Phong反射模型中的漫反射项)。在这两个极端之间,厚度的参数应该被插值(也是根据Phong反射模型中的漫反射项)。但是,这样会使得轮廓线依赖于指定的光源;因此,以下的着色器只渲染第一个光源的轮廓线和漫射光照,这个光源通常是光源中最重要的一个。其它所有的光源只是渲染镜面高光。
以下的实现必须在_UnlitOutlineThickness
(如果漫反射项的点乘结果小于等于0)和_LitOutlineThickness
(如果点乘结果为1)之间进行插值。对于用0到1之间的参数x把值a线性变换到值b,Cg提供了内置函数lerp(a, b, x)。被插值的值随后会被用作阈值以决定一个点是否足够接近轮廓。如果它接近轮廓,它的片元颜色就被设置为轮廓线的颜色_OutlineColor
:
if (dot(viewDirection, normalDirection)
< lerp(_UnlitOutlineThickness, _LitOutlineThickness,
max(0.0, dot(normalDirection, lightDirection))))
{
fragmentColor = _LightColor0.rgb * _OutlineColor.rgb;
}
完整的着色器代码
目前为止应该很清楚的是,即使是上面的一些图像也会给忠实实现带来非常大的困难。因此,以下的着色器只实现了上面描述的一些特性而忽略了许多其它特性。注意不同颜色的贡献值(漫射照明、轮廓线、高光)是根据互相之间的遮挡来指定不同的优先级。你也可以把这些优先级考虑为不同的层放在彼此之上。
Shader "Cg shader for toon shading" {
Properties {
_Color ("Diffuse Color", Color) = (1,1,1,1)
_UnlitColor ("Unlit Diffuse Color", Color) = (0.5,0.5,0.5,1)
_DiffuseThreshold ("Threshold for Diffuse Colors", Range(0,1))
= 0.1
_OutlineColor ("Outline Color", Color) = (0,0,0,1)
_LitOutlineThickness ("Lit Outline Thickness", Range(0,1)) = 0.1
_UnlitOutlineThickness ("Unlit Outline Thickness", Range(0,1))
= 0.4
_SpecColor ("Specular Color", Color) = (1,1,1,1)
_Shininess ("Shininess", Float) = 10
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for ambient light and first light source
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _UnlitColor;
uniform float _DiffuseThreshold;
uniform float4 _OutlineColor;
uniform float _LitOutlineThickness;
uniform float _UnlitOutlineThickness;
uniform float4 _SpecColor;
uniform float _Shininess;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.xyz);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
// default: unlit
float3 fragmentColor = _UnlitColor.rgb;
// low priority: diffuse illumination
if (attenuation
* max(0.0, dot(normalDirection, lightDirection))
>= _DiffuseThreshold)
{
fragmentColor = _LightColor0.rgb * _Color.rgb;
}
// higher priority: outline
if (dot(viewDirection, normalDirection)
< lerp(_UnlitOutlineThickness, _LitOutlineThickness,
max(0.0, dot(normalDirection, lightDirection))))
{
fragmentColor = _LightColor0.rgb * _OutlineColor.rgb;
}
// highest priority: highlights
if (dot(normalDirection, lightDirection) > 0.0
// light source on the right side?
&& attenuation * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess) > 0.5)
// more than half highlight intensity?
{
fragmentColor = _SpecColor.a
* _LightColor0.rgb * _SpecColor.rgb
+ (1.0 - _SpecColor.a) * fragmentColor;
}
return float4(fragmentColor, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend SrcAlpha OneMinusSrcAlpha
// blend specular highlights over framebuffer
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _UnlitColor;
uniform float _DiffuseThreshold;
uniform float4 _OutlineColor;
uniform float _LitOutlineThickness;
uniform float _UnlitOutlineThickness;
uniform float4 _SpecColor;
uniform float _Shininess;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).rgb);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.rgb);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float4 fragmentColor = float4(0.0, 0.0, 0.0, 0.0);
if (dot(normalDirection, lightDirection) > 0.0
// light source on the right side?
&& attenuation * pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess) > 0.5)
// more than half highlight intensity?
{
fragmentColor =
float4(_LightColor0.rgb, 1.0) * _SpecColor;
}
return fragmentColor;
}
ENDCG
}
}
Fallback "Specular"
}
总结
恭喜,你完成了本章的学习。我们看到了:
- 什么是卡通着色,卡通渲染,以及非真实感渲染。
- 非真实感渲染中的一些技术是如何被使用到卡通着色中去的。
- 如何在着色器中实现这些技术。