CelShading卡通着色

Number of views 180

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在线地址

0 Answers