Black Souls – Grim Sketch
Fully transparent except the black lines, so use opaque monocolor/gradient below of the same mesh.
It has 3 layers: Layers are to start from 3, then 2, then 1. Think of them as LODs. Layer3 is when the model having this shader, is distant from the camera, e.g. combat. In my game, I have Layer3 on all the time.
The main reason for turning on layer1+2 exclusively when the camera is close, is because if you keep them enabled at all camera distances, there will be artifacts because of such thin lines (sub-pixels)
Incompetent devs fallback to anti-aliasing (e.g. TAA) to solve such problems. Here, just disable layers 1/2 as you zoom out the camera.
That said, when the camera gets a bit closer and the whole model is in view, layer2 is to be activated.
When very close (think face close-ups), enable layer1 too.
The idea is to plug a GDScript to this shader, and enable the layers based on distance, so as to not kill your GPU if you use this for every model in your scene, and don’t want to hurt your players’ eyes (sub-pixel artifacts/jittering)
Specifically, in my design at least, each model has a “BlackSoulsShaderManager” script, which stores references to all such shaders within a model/skeleton. After all, different values for each fabric/clothe part of a model, and none at skin, is beautiful. And based on the camera distance/events, it triggers that script which enables/disables the appropriate layers of all shader references.
This is how I would use this at least, do whatever you wish.
Default values shown in the video are for the images shown. Uniform stripes are the default in this code.
Shader code
shader_type spatial;
render_mode blend_mix, unshaded, cull_back, depth_prepass_alpha;
// General sketch settings
uniform float spacing_jitter = 0.3;
uniform float intensity_variation = 0.5;
uniform float wobble_strength = 0.02;
uniform float wobble_frequency = 20.0;
uniform float ink_variation_frequency = 15.0;
uniform float ink_variation_strength = 0.05;
uniform float thickness_variation = 0.08;
uniform float edge_fade = 0.15;
uniform float tip_stagger = 0.05; // max horizontal offset at line ends
// Layer 1
uniform bool layer1_enabled = true;
uniform float layer1_angle = 0.0;
uniform float layer1_density = 60.0;
uniform float layer1_thickness = 0.12;
// Layer 2
uniform bool layer2_enabled = true;
uniform float layer2_angle = 20.0;
uniform float layer2_density = 40.0;
uniform float layer2_thickness = 0.08;
// Layer 3
uniform bool layer3_enabled = true;
uniform float layer3_angle = -15.0;
uniform float layer3_density = 50.0;
uniform float layer3_thickness = 0.09;
// Hash / noise
float hash(float n){ return fract(sin(n) * 43758.5453123); }
float smooth_noise(float x){
float i = floor(x);
float f = fract(x);
float a = hash(i);
float b = hash(i + 1.0);
float t = f*f*(3.0-2.0*f);
return mix(a, b, t);
}
// Rotate UV
vec2 rotate_uv(vec2 uv, float angle_deg){
float angle = radians(angle_deg);
float s = sin(angle);
float c = cos(angle);
vec2 center = vec2(0.5, 0.5);
uv -= center;
uv = vec2(c*uv.x - s*uv.y, s*uv.x + c*uv.y);
uv += center;
return uv;
}
// Draw a single vertical sketch layer with staggered line tips
float draw_layer(vec2 uv_coord, float layer_angle, float density, float thickness_base){
vec2 uv = rotate_uv(uv_coord, layer_angle);
float x = uv.x * density;
float cell = floor(x);
// Base horizontal jitter
float offset = (hash(cell) - 0.5) * spacing_jitter;
// Micro-wobble along vertical
float wobble = (smooth_noise(uv_coord.y * wobble_frequency + cell) - 0.5) * 2.0 * wobble_strength;
// Tip stagger: more offset near top/bottom
float tip_factor = min(uv_coord.y, 1.0 - uv_coord.y) / edge_fade;
tip_factor = clamp(tip_factor, 0.0, 1.0);
float stagger = (hash(cell + 50.0) - 0.5) * tip_stagger * (1.0 - tip_factor);
float local = fract(x + offset + wobble + stagger);
// Thickness variation
float ink_var = (smooth_noise(uv_coord.y * ink_variation_frequency + cell) - 0.5) * 2.0 * ink_variation_strength;
float thickness = thickness_base + (hash(cell + 10.0) - 0.5) * thickness_variation + ink_var;
float line = step(local, thickness);
// Intensity per line
float intensity = 1.0 - hash(cell + 20.0) * intensity_variation;
// Soft top/bottom edges
float fade_top = smoothstep(0.0, edge_fade, uv_coord.y);
float fade_bottom = smoothstep(0.0, edge_fade, 1.0 - uv_coord.y);
float edge_fade_factor = min(fade_top, fade_bottom);
return line * intensity * edge_fade_factor;
}
void fragment(){
float line_mask = 0.0;
if(layer1_enabled)
line_mask = max(line_mask, draw_layer(UV, layer1_angle, layer1_density, layer1_thickness));
if(layer2_enabled)
line_mask = max(line_mask, draw_layer(UV, layer2_angle, layer2_density, layer2_thickness));
if(layer3_enabled)
line_mask = max(line_mask, draw_layer(UV, layer3_angle, layer3_density, layer3_thickness));
ALBEDO = vec3(0.0);
ALPHA = line_mask;
if(ALPHA < 0.01){
discard;
}
}



