Native Three.js
pnpm add crt-shader three@^0.186.0Live example · Click to inspect
Save as crt.js in your browser app.
import * as THREE from 'three';import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';import { CRTPass } from 'crt-shader/three';
export function mount(host) { const renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true }); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.NoToneMapping; renderer.setPixelRatio(1); renderer.setSize(640, 400); renderer.domElement.style.cssText = 'display:block;width:100%;height:auto'; renderer.domElement.setAttribute('aria-label', 'CRT torus knot'); host.append(renderer.domElement);
const scene = new THREE.Scene(); scene.background = new THREE.Color('black'); const camera = new THREE.PerspectiveCamera(40, 640 / 400, 0.1, 100); camera.position.z = 5; const geometry = new THREE.TorusKnotGeometry(0.8, 0.25, 128, 16); const material = new THREE.MeshNormalMaterial(); scene.add(new THREE.Mesh(geometry, material));
const composer = new EffectComposer(renderer); const render = new RenderPass(scene, camera); const output = new OutputPass(); const crt = new CRTPass({ preset: 'reference', inputResolution: 160 }); composer.addPass(render); composer.addPass(output); // Tone map and encode before native CRT. composer.addPass(crt); // Last: do not encode again. composer.render(); // For animation, call this in your render loop.
return { canvas: renderer.domElement, dispose() { crt.dispose(); output.dispose(); render.dispose(); composer.dispose(); geometry.dispose(); material.dispose(); renderer.dispose(); renderer.domElement.remove(); }, };}Then mount it from your entry point:
import { mount } from './crt.js';const demo = mount(document.body);Call demo.dispose() when removing the scene. Reuse your existing renderer if you have one.
Keep the color pipeline in order
Section titled “Keep the color pipeline in order”Native Three uses RenderPass, then OutputPass, then CRTPass from crt-shader/three. CRT expects display-encoded input and must stay last. Reuse your scene’s renderer; the pass does not create a second WebGL context.
For animation, call composer.render(delta) in your render loop. On resize, update the camera aspect and projection matrix, then resize both renderer and composer.
Input resolution is not effect strength
Section titled “Input resolution is not effect strength”inputResolution: 'auto' chooses a reduced input size. To retain native resolution, use a number at least as large as the incoming buffer’s longest edge and update it after resize or DPR changes. mode: 'pixelated' shows prepared input; mode: 'original' bypasses the effect without disabling the terminal pass.