How do you achieve...?

A little thread for talking shop and asking for help :3

I’ll start! If anyone has experience with Shaderglass and/or Godot shaders, how do I achieve a… bitcrushing? colour indexing? effect like this:

This was a network corruption on a stream of Big Walk and I am enamored with the aesthetic. What if FMV but on the ZX Spectrum lol

3 Likes

[edit to clarify that I am very much an amateur with shaders, just in case that’s not immediately obvious lol]

Hey! There’s a bunch of ways to achieve this, and the right fit would depend on the project, but for a really simple effect you could just round each colour channel to reduce the maximum number of possible colours.

As an example, we’ll start with this image, imported into Godot:

We’ll apply a shader material to this, then add the following shader:

shader_type canvas_item;


void fragment()
{
	COLOR.r = floor(COLOR.r * 10.0) / 10.0;
	COLOR.g = floor(COLOR.g * 10.0) / 10.0;
	COLOR.b = floor(COLOR.b * 10.0) / 10.0;
}

To explain - we’re just limiting red, green and blue channels to a maximum of 10 possible values each here. You could naturally try different limits and see what outcomes you get!

The result for our super-simple shader comes out like this:

This is the most basic possible interpretation, and doesn’t take into account how brightness is actually perceived, will cut off maximum brightness, and doesn’t allow much room for the user to have creative control over the palette without processing the image beforehand. Still, it makes for a decent jumping off point!

You could also consider palette shaders for more creative control!

1 Like

I’d be very interested to hear if anyone has a take on how the original images seem to assign a different colour to different sections of the screen - that’s a really intriguing artifact!

Scottish Highlands, obliterated to max 4 colours per channel, goes from:

to:

With this simple shader though we’re missing a lot of the character of the original though. I’m fascinated by the way that in the original detail is retained in brightness, but colour appears to be determined at a much lower resolution - it’s like a separate, much larger set of colour pixels has been layered on top of the image. Would love to hear some takes on how it’s achieved!

A little more experimentation - separate pass for luminosity vs. colour at different resolutions:

Very much needs dialling in from here based on the image it’s given, but getting a little closer to the chunky lo-fi feel of the original I think?

Shader here:

shader_type canvas_item;

uniform float threshold;

void fragment()
{
	vec4 lum_base_col = texture(TEXTURE, floor(UV * 256.0) / 256.0);
	float lum = (0.299*lum_base_col.r + 0.587*lum_base_col.g + 0.114*lum_base_col.b);

	vec4 base_col = texture(TEXTURE, floor(UV * 128.0) / 128.0);
	COLOR.r = floor(base_col.r * 8.0) * .125;
	COLOR.g = floor(base_col.g * 8.0) * .125;
	COLOR.b = floor(base_col.b * 8.0) * .125;

	COLOR = mix(COLOR, vec4(.25, .25, .25, 1.0), .5 * step(lum, threshold));
}

Got work early in the morning, but will try and refine a little more tomorrow!

1 Like

Oh nice, thanks for explaining! I’ve never done anything with shaders, they always seemed really intimidating, but your explanations are really accessible :D

As you said, there’s still some unknowns about how to match the exact vibe, but it’s cool to see that playing around with it can be simpler than I imagined ^^

1 Like

In work at the moment but I’ve had an idea for a way to clean up the result! Will try a little more when I get in

3 Likes

A part of the effect def seems to be some kind of block boundary artifact from the compression. Tho I’m not sure the details of replicating that in a shader.

Might have some luck searching around for jpeg or block boundary type artifacting shaders for that specific color block mismatch look going on!

1 Like

think I’m getting closer! I’ll refine and make it legible and share a little later on :slight_smile:



Still missing some of the magic of the original - the scattered desaturated areas, the extreme blockyness - it’s a little closer than I was last night though!

I’d love to see some more takes on how to achieve this look if anyone’s up to it :slight_smile:

1 Like

ok, so honestly I’ve started to veer away from the original brief (sorry!), but I’m having a lot of fun with this so I’m not going to judge myself too harshly on that

First up, the shader in its entirety, as it currently stands:

shader_type canvas_item;

uniform vec2 resolution = vec2(1200.0, 900.0);

uniform float shadow_scale;
uniform float shadow_threshold;
uniform float shadow_depth;

uniform float color_depth;
uniform float color_scale;
uniform int blur_depth : hint_range(2, 15) = 2;

uniform float brightness_compensation : hint_range(.9, 2.0) = 1.0;


vec4 blur(sampler2D blur_texture, vec2 uv)
{
	vec4 color = vec4(.0);

	float start_coord = .0 - (float(blur_size) * .5);
	float max_coord = (float(blur_size) * .5) + 1.0;

	for (float blur_x = start_coord; blur_x < max_coord; blur_x++)
	{
		for (float blur_y = start_coord; blur_y < max_coord; blur_y++)
		{
			vec2 offset = vec2(blur_x, blur_y) / resolution;
			color += texture(blur_texture, uv + offset);
		}
	}

	return color / pow(float(blur_size) + 1.0, 2.0);
}


void fragment()
{
	vec2 color_res = resolution / color_scale;
	vec4 base_col = blur(TEXTURE, floor(UV * color_res) / color_res);

	COLOR.r = log(floor(exp(base_col.r) * color_depth) / color_depth);
	COLOR.g = log(floor(exp(base_col.g) * color_depth) / color_depth);
	COLOR.b = log(floor(exp(base_col.b) * color_depth) / color_depth);

	vec2 shadow_res = resolution / shadow_scale;
	vec4 lum_base_col = texture(TEXTURE, floor(UV * shadow_res) / shadow_res);
	float lum = (0.299*lum_base_col.r + 0.587*lum_base_col.g + 0.114*lum_base_col.b);

	COLOR = COLOR * (1.0 - (shadow_depth * step(lum, shadow_threshold)));
	COLOR *= brightness_compensation;
}

This shader consists of two main parts: firstly, it will pixelate and reduce the colour count of the colour component of the image.

Secondly, it will look at the perceived brightness of the image at each pixel, and where the brightness falls below a user-defined threshold it will apply a shadow. This will also be pixelated, but at a scale the user can define separately from the colour component of the image.

There’s some fun quirks to it, which bear a little explaining. I’ve included a simple blur function, which I found plays really nicely with the pixelation - higher blur values will result in much larger blocks of single colours, especially at higher colour scales and lower colour counts. For comparison, let’s fuck up a beautiful photo of the coast:

So pretty! Here’s the same photo, with the colour count set to 5, scale of 12 and a blur depth of 2:

We have created a horrible ugly mess of colours. Terrible work! But what if we up the blur depth to 15?

Much cleaner! The pixels are still chunky, but at least now we can parse the image more clearly.

1 Like

Oh nice! I do think the blur makes it a bit too vaseline like, maybe that’s just because it’s relatively high. Glad you’re having fun though!

Also I forgot to link the stream for anyone who wants more than two screencaps (an live in aesthetic Big Walk for a few hours):

(it starts off normal, this corruption was a way to keep the stream from stuttering. Corruption starts about 12 mins in)

1 Like

Next, I decided to fix the issue where colours were indexed linearly - particularly at lower colour counts I really didn’t like how this was perceived.

My original code was pretty simple, just getting the floor of each channel multiplied by the color depth, then dividing it back by colour depth:

COLOR.r = floor(base_col.r * color_depth) / color_depth;
COLOR.g = floor(base_col.g * color_depth) / color_depth;
COLOR.b = floor(base_col.b * color_depth) / color_depth;

I replaced this with a version that used exp and log to get something a little closer to how colour is actually perceived:

COLOR.r = log(floor(exp(base_col.r) * color_depth) / color_depth);
COLOR.g = log(floor(exp(base_col.g) * color_depth) / color_depth);
COLOR.b = log(floor(exp(base_col.b) * color_depth) / color_depth);

For an example of the difference this makes, here’s a nice autumnal woodland, processed first with the original shader and then with the new version:

Both versions have identical blur, scale and colour depth, but the second version has much more definition in lighter areas of the picture :slight_smile:

1 Like

Trying it out for some more subtle effects:

and some more extreme:

2 Likes

quick correction to my shader as the original implementation of shadows was broken - I was also multiplying the alpha channel when applying darkening, which would result in transparency where there should have been shadows! I had wondered why high values for shadow depth were rendering incorrectly lol

shader_type canvas_item;

uniform vec2 resolution = vec2(1200.0, 900.0);

uniform float shadow_scale;
uniform float shadow_threshold;
uniform float shadow_depth;

uniform float color_depth;
uniform float color_scale;
uniform int blur_size : hint_range(2, 15) = 2;

uniform float brightness_compensation : hint_range(.9, 2.0) = 1.0;


vec4 blur(sampler2D blur_texture, vec2 uv)
{
	vec4 color = vec4(.0);

	float start_coord = .0 - (float(blur_size) * .5);
	float max_coord = (float(blur_size) * .5) + 1.0;

	for (float blur_x = start_coord; blur_x < max_coord; blur_x++)
	{
		for (float blur_y = start_coord; blur_y < max_coord; blur_y++)
		{
			vec2 offset = vec2(blur_x, blur_y) / resolution;
			color += texture(blur_texture, uv + offset);
		}
	}

	return color / pow(float(blur_size) + 1.0, 2.0);
}


void fragment()
{
	vec2 color_res = resolution / color_scale;
	vec4 base_col = blur(TEXTURE, floor(UV * color_res) / color_res);

	COLOR.r = log(floor(exp(base_col.r) * color_depth) / color_depth);
	COLOR.g = log(floor(exp(base_col.g) * color_depth) / color_depth);
	COLOR.b = log(floor(exp(base_col.b) * color_depth) / color_depth);

	vec2 shadow_res = resolution / shadow_scale;
	vec4 lum_base_col = texture(TEXTURE, floor(UV * shadow_res) / shadow_res);
	float lum = (0.299*lum_base_col.r + 0.587*lum_base_col.g + 0.114*lum_base_col.b);

	COLOR.rgb = COLOR.rgb * (1.0 - (shadow_depth * step(lum, shadow_threshold)));
	COLOR *= brightness_compensation;
}

ALTERNATIVE VERSION

I’ve also been trying out an alternate implementation - this allows for for more complex shadows with the user defining the number of layers of shadow, contrast, depth and pixel scale, can get real weird with it if you want




Shader could use a clean up and optimisation, but is as follows:

shader_type canvas_item;

uniform vec2 resolution = vec2(1200.0, 900.0);

uniform float shadow_scale;
uniform float shadow_depth;
uniform int shadow_layers = 1;
uniform float shadow_contrast;

uniform float color_depth;
uniform float color_scale;
uniform int blur_size : hint_range(2, 15) = 2;

uniform float brightness_compensation : hint_range(.9, 2.0) = 1.0;


vec4 blur(sampler2D blur_texture, vec2 uv)
{
	vec4 color = vec4(.0);

	float start_coord = .0 - (float(blur_size) * .5);
	float max_coord = (float(blur_size) * .5) + 1.0;

	for (float blur_x = start_coord; blur_x < max_coord; blur_x++)
	{
		for (float blur_y = start_coord; blur_y < max_coord; blur_y++)
		{
			vec2 offset = vec2(blur_x, blur_y) / resolution;
			color += texture(blur_texture, uv + offset);
		}
	}

	return color / pow(float(blur_size) + 1.0, 2.0);
}


void fragment()
{
	vec2 color_res = resolution / color_scale;
	vec4 base_col = blur(TEXTURE, floor(UV * color_res) / color_res);

	COLOR.r = log(floor(exp(base_col.r) * color_depth) / color_depth);
	COLOR.g = log(floor(exp(base_col.g) * color_depth) / color_depth);
	COLOR.b = log(floor(exp(base_col.b) * color_depth) / color_depth);

	vec2 shadow_res = resolution / shadow_scale;
	vec4 lum_base_col = texture(TEXTURE, floor(UV * shadow_res) / shadow_res);
	float lum = (0.299*lum_base_col.r + 0.587*lum_base_col.g + 0.114*lum_base_col.b);

	float shadow_strength = ceil(lum * float(shadow_layers)) / float(shadow_layers);
	shadow_strength = pow(shadow_strength, -shadow_contrast);
	COLOR.rgb *= (1.0 - (shadow_strength * shadow_depth));
	COLOR *= brightness_compensation;
}