Stylized 3D Outline Shader

Stylized 3D Outline Shader

· 227 字 · 2 分鐘 reading time bevy shader

很喜歡Cat and soup 裡面的貓咪,手繪風的和流暢的動作非常療癒,所以花了一些時間研究,也做了一個小demo。

Depth-based

可以想像你的手在身體前方,遮擋住的輪廓。

source code
// -----------------------
// Depth Detection -------
// -----------------------

fn prepass_depth(uv: vec2f) -> f32 {
# ifdef MULTISAMPLED
    let pixel_coord = vec2i(uv * texture_size);
    let depth = textureLoad(depth_prepass_texture, pixel_coord, sample_index_i);
# else
    let depth = textureSample(depth_prepass_texture, texture_sampler, uv);
# endif
    return depth;
}

fn prepass_view_z(uv: vec2f) -> f32 {
    let depth = prepass_depth(uv);
    return depth_ndc_to_view_z(depth);
}

fn view_z_gradient_x(uv: vec2f, y: f32, thickness: f32) -> f32 {
    let l_coord = uv + texel_size *vec2f(-thickness, y);    // left  coordinate
    let r_coord = uv + texel_size* vec2f( thickness, y);    // right coordinate

    return prepass_view_z(r_coord) - prepass_view_z(l_coord); 
}

fn view_z_gradient_y(uv: vec2f, x: f32, thickness: f32) -> f32 {
    let d_coord = uv + texel_size *vec2f(x, -thickness);    // down coordinate
    let t_coord = uv + texel_size* vec2f(x,  thickness);    // top  coordinate

    return prepass_view_z(t_coord) - prepass_view_z(d_coord);
}

fn detect_edge_depth(uv: vec2f, thickness: f32, fresnel: f32) -> f32 {
    let deri_x =
        view_z_gradient_x(uv, thickness, thickness) +
        2.0 * view_z_gradient_x(uv, 0.0, thickness) +
        view_z_gradient_x(uv, -thickness, thickness);

    let deri_y =
        view_z_gradient_y(uv, thickness, thickness) +
        2.0 * view_z_gradient_y(uv, 0.0, thickness) +
        view_z_gradient_y(uv, -thickness, thickness);

    // why not `let grad = sqrt(deri_x * deri_x + deri_y * deri_y);`?
    //
    // Because ·deri_x· or ·deri_y· might be too large,
    // causing overflow in the calculation and resulting in incorrect results.
    let grad = max(abs(deri_x), abs(deri_y));

    let view_z = abs(prepass_view_z(uv));

    let steep_angle_adjustment = 
        smoothstep(ed_uniform.steep_angle_threshold, 1.0, fresnel) * ed_uniform.steep_angle_multiplier * view_z;

    return f32(grad > ed_uniform.depth_threshold * (1.0 + steep_angle_adjustment));
}

Normal-based

平滑表面法線變化很小,鄰近像素法線有了非常大的高度落差。

source code
// -----------------------
// Normal Detection ------
// -----------------------

fn prepass_normal_unpack(uv: vec2f) -> vec3f {
    let normal_packed = prepass_normal(uv);
    return normalize(normal_packed.xyz * 2.0 - vec3(1.0));
}

fn prepass_normal(uv: vec2f) -> vec3f {
#ifdef MULTISAMPLED
    let pixel_coord = vec2i(uv * texture_size);
    let normal = textureLoad(normal_prepass_texture, pixel_coord, sample_index_i);
#else
    let normal = textureSample(normal_prepass_texture, texture_sampler, uv);
#endif
    return normal.xyz;
}

fn normal_gradient_x(uv: vec2f, y: f32, thickness: f32) -> vec3f {
    let l_coord = uv + texel_size * vec2f(-thickness, y);    // left  coordinate
    let r_coord = uv + texel_size * vec2f( thickness, y);    // right coordinate

    return prepass_normal(r_coord) - prepass_normal(l_coord);
}

fn normal_gradient_y(uv: vec2f, x: f32, thickness: f32) -> vec3f {
    let d_coord = uv + texel_size * vec2f(x, -thickness);    // down coordinate
    let t_coord = uv + texel_size * vec2f(x,  thickness);    // top  coordinate

    return prepass_normal(t_coord) - prepass_normal(d_coord);
}

fn detect_edge_normal(uv: vec2f, thickness: f32) -> f32 {
    let deri_x = abs(
        normal_gradient_x(uv,  thickness, thickness) +
        2.0 * normal_gradient_x(uv,  0.0, thickness) +
        normal_gradient_x(uv, -thickness, thickness));

    let deri_y = abs(
        normal_gradient_y(uv, thickness, thickness) +
        2.0 * normal_gradient_y(uv, 0.0, thickness) +
        normal_gradient_y(uv, -thickness, thickness));

    let x_max = max(deri_x.x, max(deri_x.y, deri_x.z));
    let y_max = max(deri_y.x, max(deri_y.y, deri_y.z));
    
    let grad = max(x_max, y_max);

    return f32(grad > ed_uniform.normal_threshold);
}

UV Distortion

使用雜訊擾動UV座標,讓線不那麼筆直,像是手繪一樣。

source code
@fragment
fn fragment(
#ifdef MULTISAMPLED
    @builtin(sample_index) sample_index: u32,
#endif
    in: FullscreenVertexOutput
) -> @location(0) vec4f {
#ifdef MULTISAMPLED
    sample_index_i = i32(sample_index);
#endif

    texture_size = vec2f(textureDimensions(screen_texture));
    texel_size = 1.0 / texture_size;

    let near_ndc_pos = vec3f(uv_to_ndc(in.uv), 1.0);
    let near_world_pos = position_ndc_to_world(near_ndc_pos);

    let view_direction = calculate_view(near_world_pos);
    
    let normal = prepass_normal_unpack(in.uv);
    let fresnel = 1.0 - saturate(dot(normal, view_direction));;

    let sample_uv = in.position.xy * min(texel_size.x, texel_size.y);
    let noise = textureSample(noise_texture, noise_sampler, sample_uv * ed_uniform.uv_distortion.xy);
    let uv = in.uv + noise.xy * ed_uniform.uv_distortion.zw;

    var edge = 0.0;

Slides

Demo (WebGPU)

Credits & Assets

Character Models: Chonky Cat Trio by [Kanna-Nakajima] via Sketchfab.

Environment Props: Restaurant Bits by [Kay Lousberg] via itch.io.