今天专门为卡通渲染开一个分类,希望未来在卡通渲染上达到一定高度,因为卡通渲染风格才是我最喜欢的。
以前买过很多游戏机玩过很多游戏,当然pc网游也玩过不少,发现自己就是喜欢玩卡通风格的游戏,比如塞尔达传说从GBAsp的缩小帽/三角力量玩到wiiu的荒野之息、马里奥3D、路易鬼屋、星之卡比、黄金太阳等,还有pc上的跑跑卡丁车、dnf等,都是我玩过很长时间的游戏,当然其他很多3A大作也玩过,就是不喜欢,比如荒野大镖客、使命召唤、战地、孤岛危机、底特律变人,唯一喜欢的真实感渲染就是ps4的神秘海域4了,我针对我个人的情况查阅过资料,我是一个患有恐怖谷综合症的玩家(不清楚的百度恐怖谷效应),所以玩一些真实感渲染的游戏会心悸恐慌,跟喝了高浓度咖啡一样。
ps:我一般只看真实感渲染很好的游戏直播,跟看电影一样,而自己只玩卡通渲染游戏。
言归正传,说起卡通渲染,属于非真实感渲染的领域之一,学名NPR(Non Photorealistic Rendering),主要是为了渲染出一些特殊的画质风格,比如卡通、素描、油画等创作性艺术风格,这些风格可以让我创造特立独行的游戏、影视等作品。比如二次元动漫游戏、废土朋克、水墨油画等。当然NPR覆盖的范围广大无比。
卡通渲染的核心要素是两个:
1.具有非真实艺术性的光线算法渲染风格
2.能起到模型凸显效果的描边算法
而以上两个核心要素,就囊括万千了,我们就来研究学习下。
这里我做一个怀旧黑白渐变卡通风格的shader渲染,代码如下:
Shader "Custom/CartoonShader"
{
Properties
{
//渐变风格
_GradientWeight("Gradient Weight",Range(-1,1)) = 0.5
_BrightColor("Bright Color",Color) = (1,1,1,1)
_DarkColor("Dark Color",Color) = (0,0,0,1)
//描边
_Extsn("Extsn Value", Range(0,0.001)) = 0.001
_OutColor("Outline Color", Color) = (0,0,0,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
//正常渲染
//优先逐顶点光照
//测试逐片段光照
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 color : TEXCOORD1;
};
float _GradientWeight;
float4 _BrightColor;
float4 _DarkColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//计算顶点上的光照diffuse权重信息
float3 worldNormal = UnityObjectToWorldNormal(v.normal.xyz);
float3 worldp2s = normalize(WorldSpaceLightDir(v.vertex));
float diffuse = dot(worldNormal, worldp2s);
if (diffuse > _GradientWeight){
o.color = _BrightColor;
}
else {
o.color = _DarkColor;
}
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = i.color;
return col;
}
ENDCG
}
//扩展描边
Pass
{
Cull front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float _Extsn;
float4 _OutColor;
v2f vert(appdata v)
{
v2f o;
v.vertex += v.normal * _Extsn;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = _OutColor;
return col;
}
ENDCG
}
}
}
效果如下:
效果原理如下:黑白渐变渲染pass在顶点函数中模拟diffuse的世界空间中单位法向量和单位顶点朝向光源方向向量点积得到-1到1的权重值,然后根据权重值去赋值渐变色,描边pass则扩展法向量。
可以看得出来因为是逐顶点计算,所以在法向量与顶点到光源向量因旋转产生变化时,渐变非平滑很生硬,早期在游戏硬件设备不强的时候,使用的可编程管线都是逐顶点计算。同时因为顶点颜色之间会自动插值,所以产生渐变色,当然也可以在顶点函数中手动计算lerp插值color。
如果我们可以把计算挪到片段函数中,如下:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col;
float diffuse = dot(i.worldNormal, i.worldP2S);
if (diffuse > _GradientWeight) {
col = _BrightColor;
}
else {
col = _DarkColor;
}
return col;
}
效果如下:
可以看得出,因为是逐片段计算了,所以没有了自动颜色插值,如果只是单纯的大于小于等于判断就造成了无渐变的黑白两色。
以上两种做法是基于线性计算,因为diffuse = dot(normal,p2s)在函数图象上就是随着单位向量normal与p2s的夹角增长,diffuse从1到-1,所以达成的效果如上。
后面我来介绍一个非线性增长的光线计算,可以达到其他渲染效果,当然下次再聊。