import * as THREE from 'three'; import { VERTEX_SHADER } from './shader-contract.js'; /** * Thin wrapper over WebGLRenderer that provides the two primitives everything * else is built from: allocate a render target, and run a fullscreen shader pass * into one. Keeping this small matters — preview and export share it exactly, * and any state that leaks between frames here would break determinism. */ export class Renderer { constructor({ width = 1280, height = 720, canvas = null } = {}) { this.gl = new THREE.WebGLRenderer({ canvas: canvas || undefined, antialias: false, // we render through targets; MSAA here buys nothing preserveDrawingBuffer: true, // required to read pixels back for hashing/export powerPreference: 'high-performance', }); this.gl.autoClear = false; this.gl.setPixelRatio(1); // never device-dependent: output size is explicit this.gl.setSize(width, height, false); this.width = width; this.height = height; // Fullscreen quad rig, reused for every pass. this.quadScene = new THREE.Scene(); this.quadCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); this.quadGeometry = new THREE.PlaneGeometry(2, 2); this.quadMesh = new THREE.Mesh(this.quadGeometry, new THREE.MeshBasicMaterial()); this.quadMesh.frustumCulled = false; this.quadScene.add(this.quadMesh); this._readBuffer = null; } get canvas() { return this.gl.domElement; } setSize(width, height) { if (width === this.width && height === this.height) return; this.width = width; this.height = height; this.gl.setSize(width, height, false); this._readBuffer = null; } createTarget(width = this.width, height = this.height, options = {}) { const target = new THREE.WebGLRenderTarget(width, height, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, type: options.float ? THREE.HalfFloatType : THREE.UnsignedByteType, depthBuffer: options.depth === true, stencilBuffer: false, generateMipmaps: false, }); target.texture.wrapS = THREE.ClampToEdgeWrapping; target.texture.wrapT = THREE.ClampToEdgeWrapping; // Deterministic initial contents: never inherit whatever was in GPU memory. this.clear(target); return target; } clear(target = null, r = 0, g = 0, b = 0, a = 1) { const prev = this.gl.getClearColor(new THREE.Color()); const prevAlpha = this.gl.getClearAlpha(); this.gl.setRenderTarget(target); this.gl.setClearColor(new THREE.Color(r, g, b), a); this.gl.clear(true, true, true); this.gl.setClearColor(prev, prevAlpha); this.gl.setRenderTarget(null); } /** Run a fullscreen shader pass. target === null renders to the canvas. */ blit(material, target = null) { this.quadMesh.material = material; this.gl.setRenderTarget(target); this.gl.clear(true, false, false); this.gl.render(this.quadScene, this.quadCamera); this.gl.setRenderTarget(null); } /** Render a real three.js scene (used by 3D layers). */ renderScene(scene, camera, target = null, clear = true) { this.gl.setRenderTarget(target); if (clear) this.gl.clear(true, true, true); this.gl.render(scene, camera); this.gl.setRenderTarget(null); } readPixels(target) { const w = target ? target.width : this.width; const h = target ? target.height : this.height; const needed = w * h * 4; if (!this._readBuffer || this._readBuffer.length !== needed) { this._readBuffer = new Uint8Array(needed); } this.gl.readRenderTargetPixels(target, 0, 0, w, h, this._readBuffer); return this._readBuffer; } dispose() { this.quadGeometry.dispose(); this.gl.dispose(); } } /** Convenience for building the shader materials used by passes. */ export function makePassMaterial(fragmentShader, uniforms) { return new THREE.ShaderMaterial({ vertexShader: VERTEX_SHADER, fragmentShader, uniforms, depthTest: false, depthWrite: false, transparent: true, }); }