Stylized 3D Outline 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.