CellShading也叫ToonShading,它是一种能够使3D游戏场景中产生动漫效果的渲染技术。
这是维基百科对于CelShading的解释。
CelShading也叫卡通渲染(ToonShading)是一种非真实感绘制(NPR),旨在使电脑生成的图像呈现出手绘般的效果。为了使图像可以与漫画或者卡通达到相似的效果,专业人员通常使用卡通渲染着色器进行处理。卡通渲染是在大约21世纪初期,作为计算机图形学的副产物出现的新技术,并且主要应用于电子游戏中;然而,它可以呈现出如手绘动画一样简洁明了的效果。
CelShading又称赛璐珞渲染(Celluloid Rendering),Cel一词取自Celluloid前三个英文字母,来自常用于传统动画的材料赛璐珞,想深入了解传统动画可访问:
在 2D 渲染中,通常通过应用平滑渐变使2D物体看起来像3D对象。相反的,在3D渲染中可以使用 CelShading,使平滑渐变拆解为较为突然的过渡。当与轮廓相结合时,CelShading可以实现卡通渲染的外观。
以Lambert光照模型为例,以下为Lambert光照模型代码:
float diffuseIntensity = max(dot(normal, lightDir), 0.0);
为了拆解平滑渐变为突然的过度,我们可以将diffuseIntensity判定取值范围,如大于0.1时diffuseIntensity为1,否则为0:
diffuseIntensity = step(0.1, diffuseIntensity);
示意图如下:
上述光强度的变换如果觉得太单一,还可以对不同阶段求取不同的取值范围,如下所示:
// ...
if (diffuseIntensity >= 0.8) { diffuseIntensity = 1.0; }
else if (diffuseIntensity >= 0.6) { diffuseIntensity = 0.6; }
else if (diffuseIntensity >= 0.3) { diffuseIntensity = 0.3; }
else { diffuseIntensity = 0.0; }
// ...
示意图如下:
除了通过数学取值的方式求取不同阶段的范围外,还可以通过给定的纹理来取到不同阶段的图像颜色梯度,如下图所示:
通过下面代码采样上述图像:
// ...
// 使用diffuseIntensity作为U坐标,通过该方式进行采样。
diffuseIntensity = texture(steps, vec2(diffuseIntensity, 0.0)).r;
// ...
同样的,Specular之类的计算方式也可以通过上述方式进行计算。
为了限制特定区域显示特定颜色,也可以自定义一些简单的算法,代码如下:
#extension GL_OES_standard_derivatives : enable
#define MAX_STEPS 200
#define MAX_DEPTH 100.
#define HIT_VAL 0.01
#define Cel_STEP_WIDTH 0.3
#define Cel_STEP_AMOUNT 3.
float sdSphere(vec3 p) {
return length(p) - 1.;
}
float sdTorus(vec3 p, vec2 t)
{
vec2 q = vec2(length(p.xz)-t.x,p.y);
return length(q)-t.y;
}
float getSDF(vec3 pos) {
return min(sdTorus(pos, vec2(2., .8)), sdSphere(pos - vec3(0, 1.8, 0)));
}
vec3 getNormal(vec3 pos) {
float curDis = getSDF(pos);
vec2 offset = vec2(-1., 0.);
return normalize(curDis - vec3(
getSDF(pos + offset.xyy),
getSDF(pos + offset.yxy),
getSDF(pos + offset.yyx)
));
}
float rayMarch(vec3 origin, vec3 dir) {
float depth = 0.;
for(int i = 0; i < MAX_STEPS; i++) {
vec3 pos = origin + dir * depth;
float curDis = getSDF(pos);
if(depth > MAX_DEPTH || curDis < HIT_VAL) {
break;
}
depth += curDis;
}
return depth;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
uv = uv * 2. - 1.;
uv.x *= iResolution.x/iResolution.y;
// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
// Output to screen
vec4 sceneColor = vec4(col,1.0);
vec3 origin = vec3(0, sin(iTime) * 5., 5.);
vec3 lookAt = vec3(0,0,0);
vec3 forward = normalize(lookAt - origin);
vec3 up = vec3(0, 1, 0);
vec3 right = normalize(cross(up, forward));
up = normalize(cross(forward, right));
vec3 dir = normalize(right * uv.x + up * uv.y + forward);
float depth = rayMarch(origin, dir);
vec3 pos = origin + dir * depth;
if(getSDF(pos) < HIT_VAL) {
vec3 lightDir = vec3(0., 5., 5.);
vec3 normal = getNormal(pos);
vec3 lightNor = normalize(lightDir);
float diffuseF = dot(lightNor, normal);
// 看图示1
diffuseF = diffuseF / Cel_STEP_WIDTH;
float intense = ceil(diffuseF);
// 看图示2
intense = intense / Cel_STEP_AMOUNT;
// 看图示3
intense = clamp(0., 1., intense);
float diffuseFChange = fwidth(intense);
sceneColor = vec4(intense * vec4(0.933,0.906,0.125,1));
}
fragColor = sceneColor;
}
因为diffuseF是0~1之间的数,所以ceil(diffuseF / 0.3)的示意图如下:
intense / Cel_STEP_AMOUNT的示意图如下,可看到由之前快速变化的值区间到更平缓的值域过渡,且在0~1内有三个梯度:
clamp(0.,1., intense)限制了梯度在0~1之间。
最终效果图输出如下:
这是Demo在线地址。