import * as THREE from 'three'; export class RetroEffect { constructor(renderer, width, height) { this.renderer = renderer; this.width = width; this.height = height; this.renderTarget = new THREE.WebGLRenderTarget(width, height, { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, }); this.scene = new THREE.Scene(); this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); this.quad = new THREE.Mesh( new THREE.PlaneGeometry(2, 2), new THREE.ShaderMaterial({ uniforms: { tDiffuse: { value: null }, resolution: { value: new THREE.Vector2(width, height) }, time: { value: 0 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 0.0, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; uniform vec2 resolution; uniform float time; varying vec2 vUv; vec2 barrelDistortion(vec2 uv, float amount) { vec2 center = uv - 0.5; float dist = dot(center, center); return uv + center * dist * amount; } vec3 quantize(vec3 color, float levels) { return floor(color * levels + 0.5) / levels; } void main() { vec2 distortedUv = barrelDistortion(vUv, 0.25); // Clamp distorted UV distortedUv = clamp(distortedUv, 0.0, 1.0); // Chromatic aberration float aberration = 0.0015; vec2 dir = distortedUv - 0.5; float dist = length(dir); float r = texture2D(tDiffuse, distortedUv + dir * aberration).r; vec2 gbSample = distortedUv - dir * aberration * 0.5; float g = texture2D(tDiffuse, gbSample).g; float b = texture2D(tDiffuse, distortedUv - dir * aberration).b; vec3 color = vec3(r, g, b); // Scanline float scanline = sin(vUv.y * resolution.y * 3.14159) * 0.08; color -= scanline; // Quantize to EGA-like palette color = quantize(color, 16.0); // Vignette float vignette = 1.0 - dist * 0.6; vignette = smoothstep(0.0, 1.0, vignette); color *= vignette; // Subtle phosphor warmth color.r += 0.01; color.g += 0.005; // CRT curvature darkening at edges float edgeDarken = smoothstep(0.3, 1.2, dist); color *= 1.0 - edgeDarken * 0.3; gl_FragColor = vec4(color, 1.0); } `, }) ); this.scene.add(this.quad); } update(time) { this.quad.material.uniforms.time.value = time; } render(scene, camera) { // Render scene to offscreen target this.renderer.setRenderTarget(this.renderTarget); this.renderer.render(scene, camera); // Render post-processed quad to screen this.renderer.setRenderTarget(null); this.quad.material.uniforms.tDiffuse.value = this.renderTarget.texture; this.renderer.render(this.scene, this.camera); } }