专栏目录
2173:【01】从零开始的卡通渲染-描边篇zhuanlan.zhihu.com序言:
接上一篇的描边篇,整理成一个专栏了。在本节中,我们开始讨论卡通渲染的一些光照计算方法。
如何让角色看起来卡通
思考一下,究竟是哪些因素,让我们觉得角色是卡通的呢。我觉得可以先从下面3点入手
1.减少色阶数量
2.冷暖色调分离
3.对明暗区域的手绘控制
减少色阶数量
冷暖色调分离
在美术上根据颜色区分为暖色调(红色,黄色)和冷色调(蓝色、紫色)。在偏真实的光照计算中,往往只计算一个明暗关系,然后由光和物体的颜色决定最终效果。而卡通渲染则会根据明暗关系,为明面和暗面分配不同色调的颜色。比如一个暖色调的明面,配合一个冷色调的暗面。将色调拉开以后,更进一步给人卡通感。相关链接 tone-based-shading
在《GUILTY GEAR Xrd》游戏中,绘制了一张称为SSS Texture的贴图,来对暗面的色调进行调整。
对明暗区域的手绘控制
在手绘动画中为了好的画面效果,往往其明暗的分布并不是完全正确的。最明显的,角色的脖子部分通常都出现明显的阴影。经典光照计算的结果是非常“正确”的,因而缺少卡通的手绘感。需要用其他方式对光照的计算结果进行调整。
下面介绍一下《GUILTY GEAR Xrd》中是如何对明暗区域进行手绘控制的
灯光方向控制:
卡通渲染的角色在部分灯光方向下,可以有最佳的画面表现。有时候这个灯光方向和场景灯光或者其他角色的灯光方向不一致。为了让每个角色都有最佳表现,最好每个角色有一盏自己的灯光方向。甚至当这个角色转向时,这个灯光也跟着角色做一定程度的转向,来让角色有一个更好的光影表现。
Threshold贴图控制:
《GUILTY GEAR Xrd》中将这张贴图称作ilmTexture。为了减少歧义,我们这里也这么称呼好了。
这张贴图有些类似于AO贴图,不过它是对光照计算的结果进行一些倾向性的修正。让一部分区域,比如角色脖子的部分更容易产生阴影。来达到手绘风格的阴影效果。
法线方向控制:
法线控制有两种方法,一种是直接编辑法线,达到想要的光照结果。一种是创建一个平滑的简单模型,然后将其法线传递到复杂物体上,达到优化阴影的效果。Maya自带法线传递的功能,3ds Max可以通过插件Noors Normal Thief实现法线传递的功能。
赛璐璐风格插画
赛璐璐片是一种塑料卡片,在早期日本动画制作流程中的,画师会在赛璐璐材质的塑料卡片上对原画进行上色。其特点为通常只有明暗2个色阶,明暗变化的交界非常明显。现在这种风格的卡通渲染比较流行。在本篇中,也将实现偏向这种风格的卡通渲染。
厚涂风格插画
厚涂风格相较赛璐璐风格,色阶更多,明暗交界变化会柔和很多。这个风格也有它的好处,因为3D场景比较难做成赛璐璐的。如何让赛璐璐风格的角色和非赛璐璐的场景融合是也许需要考虑的。厚涂风格的角色会更容易和场景进行融合。
双色阶的渲染实现
首先我们实现一个明暗边界分明的光照效果,并支持分别设置明暗区域的颜色,设置暗面颜色为冷色调,和明面的色调做出区分。
Shader "Unlit/CelRender"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_MainColor("Main Color", Color) = (1,1,1)
_ShadowColor ("Shadow Color", Color) = (0.7, 0.7, 0.8)
_ShadowRange ("Shadow Range", Range(0, 1)) = 0.5
_ShadowSmooth("Shadow Smooth", Range(0, 1)) = 0.2
[Space(10)]
_OutlineWidth ("Outline Width", Range(0.01, 2)) = 0.24
_OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
pass
{
Tags {"LightMode"="ForwardBase"}
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
half3 _MainColor;
half3 _ShadowColor;
half _ShadowRange;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
half4 frag(v2f i) : SV_TARGET
{
half4 col = 1;
half4 mainTex = tex2D(_MainTex, i.uv);
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
half3 worldNormal = normalize(i.worldNormal);
half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
half3 diffuse = halfLambert > _ShadowRange ? _MainColor : _ShadowColor;
diffuse *= mainTex;
col.rgb = _LightColor0 * diffuse;
return col;
}
ENDCG
}
Pass
{
//描边,参考上一篇
}
}
}
smoothstep柔化明暗边界
现在我们希望能够对明暗边界的变化做一些柔化,让风格往厚涂的风格靠一些,这样可以跟更容易地跟一些非赛璐璐风格的场景做融合。这里我们使用smoothstep函数实现这个效果。这个函数可以在根据输入数据,计算一个范围在0到1区间的平滑过渡曲线。通过这个函数的结果对明面和暗面的颜色进行插值,来实现明暗边界的软硬控制。wiki百科链接
对代码进行如下修改
half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
half ramp = smoothstep(0, _ShadowSmooth, halfLambert - _ShadowRange);
half3 diffuse = lerp(_ShadowColor, _MainColor, ramp);
Ramp贴图
还有一个做法是通过采样Ramp贴图来实现对色阶和明暗边界的控制。可以看成是用标准光照的结果为UV,采样一张用作颜色映射表的贴图,通过这张贴图控制光照计算的结果。制作如下图的ramp贴图,然后对代码进行修改。
half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
half ramp = tex2D(_rampTex, float2(saturate(halfLambert - _ShadowRange), 0.5)).r;
half3 diffuse = lerp(_ShadowColor,_MainColor, ramp);
Ramp贴图能够更容易的定义多个色阶,不过贴图需要自己制作。贴图制作起来并不复杂,也可以通过编写编辑器工具来生成。这里的贴图是使用Toony Colors Pro插件生成的。
在制作Ramp贴图的时候,最左边光照应该最弱的部分反而设置的比较亮。这个是为了制作出暗面的反光效果。在素描上面有个明暗五调子的知识,在物体边缘的部分会有一圈反光,所以物体的边缘不会是最暗的部分。链接
在图形学上,也有对应的概念,称为菲涅耳(fresnel)现象。我觉得这体现了一个非常有趣的观点,无论是图形学使用光照模型对现实世界的物理现象进行模拟,还是画家们通过观察现实世界总结出的美术理论,最终都是殊途同归的。一个使用公式进行绘图,一个使用画笔进行绘图罢了。
在《偶像大师》系列,也使用了Ramp贴图来实现色阶和色调的控制
因为本篇的篇幅有点太长了,有关边缘反光部分的讨论,放在下一篇再详细讨论。
ilmTexture贴图的实现
《GUILTY GEAR Xrd》中使用称为ilmTexture的贴图对角色明暗区域实现手绘风格的控制。其中绿通道控制漫反射的阴影阈值,红通道控制高光强度,蓝通道控制高光范围。这里跟据这个原理,完成一个最简单的实现。卡通渲染不像Lambert等光照模型有统一的公式,如果要更进一步的表现还需要根据画面需求做各种trick。比如专栏标题的展示图片还添加了阴影残留和阴影色调分离的效果,这方面就由大家自己发挥吧。
Shader "Unlit/CelRenderFull"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_IlmTex ("IlmTex", 2D) = "white" {}
[Space(20)]
_MainColor("Main Color", Color) = (1,1,1)
_ShadowColor ("Shadow Color", Color) = (0.7, 0.7, 0.7)
_ShadowSmooth("Shadow Smooth", Range(0, 0.03)) = 0.002
_ShadowRange ("Shadow Range", Range(0, 1)) = 0.6
[Space(20)]
_SpecularColor("Specular Color", Color) = (1,1,1)
_SpecularRange ("Specular Range", Range(0, 1)) = 0.9
_SpecularMulti ("Specular Multi", Range(0, 1)) = 0.4
_SpecularGloss("Sprecular Gloss", Range(0.001, 8)) = 4
[Space(20)]
_OutlineWidth ("Outline Width", Range(0, 1)) = 0.24
_OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1)
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _IlmTex;
float4 _IlmTex_ST;
half3 _MainColor;
half3 _ShadowColor;
half _ShadowSmooth;
half _ShadowRange;
half3 _SpecularColor;
half _SpecularRange;
half _SpecularMulti;
half _SpecularGloss;
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o = (v2f)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = 0;
half4 mainTex = tex2D (_MainTex, i.uv);
half4 ilmTex = tex2D (_IlmTex, i.uv);
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
half3 worldNormal = normalize(i.worldNormal);
half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
half3 diffuse = 0;
half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
half threshold = (halfLambert + ilmTex.g) * 0.5;
half ramp = saturate(_ShadowRange - threshold);
ramp = smoothstep(0, _ShadowSmooth, ramp);
diffuse = lerp(_MainColor, _ShadowColor, ramp);
diffuse *= mainTex.rgb;
half3 specular = 0;
half3 halfDir = normalize(worldLightDir + viewDir);
half NdotH = max(0, dot(worldNormal, halfDir));
half SpecularSize = pow(NdotH, _SpecularGloss);
half specularMask = ilmTex.b;
if (SpecularSize >= 1 - specularMask * _SpecularRange)
{
specular = _SpecularMulti * (ilmTex.r) * _SpecularColor;
}
col.rgb = (diffuse + specular) * _LightColor0.rgb;
return col;
}
ENDCG
}
Pass
{
//描边,参考上一篇
}
}
FallBack Off
}
总结
在本节中,介绍了卡通渲染和写实风格渲染的主要区别和一些实现方法。在下一个章节中,将会讨论卡通渲染的边缘光、后处理、以及和PBR进行融合的方向等。
分享
在写这篇文章的ramp贴图部分的时候,想起了以前在OpenGPU论坛看到的Trace大佬翻译的《偶像大师》相关文章。回去找的时候发现论坛已经没了,以前上面的很多文章也丢失了。觉得还是有点伤感的,我最早是看着OpenGPU上面的Trace翻译的科普文章,开始对渲染产生兴趣的。从这一点上来说,我非常感谢在网络上科普图形学知识的前辈们,给了我们非常好的学习途径。后来发现因为我一直有备份ppt的习惯,让我保留了这几篇文章的备份。虽然都很老了,但是觉得不应该消失在网络上面。所以我把这两篇《偶像大师》的文章上传到百度网盘里了,感兴趣的小伙伴可以去下载。 那么我们下一篇见。
百度网盘 提取码:2whi。
附:参考链接
【翻译】西川善司「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,前篇(1)
【翻译】西川善司「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,前篇(2)
【翻译】西川善司的「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,后篇