Three.js retro arcade effect using post-processing

Using the power of post-processing, any scene can be given a retro arcade look with very minimal overhead.

The code snippets below make use of the postprocessing library, which is an abstraction on top three.js' post-processing, providing effect merging, performance optimisations and some built in common effects like SMAA.

Before
After

Setting up and rendering through the EffectComposer

To achieve an optimal post-processing workflow and result the following WebGL attributes should be used.

const renderer = new WebGLRenderer({
	powerPreference: "high-performance",
	antialias: false,
	stencil: false,
	depth: false
});

Let's set up the EffectComposer with a RenderPass and empty EffectPass for now.

const composer = new EffectComposer(renderer);

const renderPass = new RenderPass(scene, camera);
const effectPass = new EffectPass(camera);

composer.addPass(renderPass);
composer.addPass(effectPass);

const animate = () => {
	requestAnimationFrame(animate);

	/**
	 * When working with an effect composer, the composer will take over the task to render the scene.
	 * So instead of letting the renderer render the scene with renderer.render(scene, camera)
	 * the effectcomposer will be used to render instead like so effectComposer.render()
	 */
	effectComposer.render();
};

animate();

Now the scene should be rendered to the canvas with the same visual results as before.
Note that if you previously used the anti aliasing built into the WebGLRenderer, you may notice a jagged effect along straight edges. To fix this you could create a separate EffectPass that handles anti aliasing using SMAAEffect. Read Effect Mergin on the posprocessing github page for more info on why to do this in a seperate EffectPass.

Remapping the scene's colors to a retro color palette using a LUT

To really sell the retro effect we need to limit the color palette used to render our scene. We can do this by providing a LUT (Look-Up-Table) which has a retro looking color palette.

To apply the LUD to our scene we will create a LUT1DEffect which is based on postprocessing's LUT1DEffect, adding an extra property that gives us control over mixing the original colors with the resulting colors. Allowing us to smoothly transition between both.

/**
 * Based on https://github.com/pmndrs/postprocessing/blob/main/src/effects/LUT1DEffect.js
 * We add a mixValue property which is passed as a Uniform to our fragment shader,
 * so we can smoothly blend into the effect later on.
 */ 
export class LUT1DEffect extends Effect {
	constructor(lut: Texture, {blendFunction = BlendFunction.SRC} = {}) {
		super('LUT1DEffect', fragmentShader, {
			blendFunction,
			uniforms: new Map([
				['uLut', new Uniform(null)],
				['uMixValue', new Uniform(null)]
			])
		});

		this.lut = lut;
		this.mixValue = 1;
	}

	get lut() {
		return this.uniforms.get('uLut')?.value;
	}

	set lut(value) {
		const uLut = this.uniforms.get('uLut');
		if (uLut) uLut.value = value;

		if (value !== null && (value.type === FloatType || value.type === HalfFloatType)) {
			this.defines.set('LUT_PRECISION_HIGH', '1');
		}
	}

	get mixValue() {
		return this.mixValue;
	}

	set mixValue(value) {
		const uMixValue = this.uniforms.get('uMixValue');
		if (uMixValue) uMixValue.value = value;
	}
}

Now we use this class to create our effect, using a LUT. Add the effect to your EffectPass to see the result.

// Create a LUT effect using a retro LUT
const retroColorGradingEffect = new LUT1DEffect(
	new TextureLoader().load('/lut/8bit.png')
);

// Include the effect in the EffectPass
const effectPass = new EffectPass(..., retroColorGradingEffect);

We used the following LUT, but feel free to use any LUT that achieves your desired color palette.

Pixelating the scene

Now we've got the colors going, but our render is still way more detailed than what you'd see on a retro arcade screen. We can easily fix this by making use of the PixelationEffect provided by postprocessing.

// Create a pixelation effect with a granularity of 5
const pixelationEffect = new PixelationEffect(5);

// Include the effect in the EffectPass
const effectPass = new EffectPass(..., pixelationEffect);

Your scene should now look like it was rendered on a retro arcade display! Many more effects could be used instead of pixelation, like dithering for example. This however requires some more experience in working with fragment shaders but could lead to some stunning results.

Interested?

Would you like to know more, have some great ideas or need help building an awesome product? Reach out at info@appfoundry.be

Contacteer Ons

Heeft u een uitdaging die u graag met ons zou willen bespreken? Contacteer ons gerust via onderstaande knop. Wij helpen u graag verder!

Email