export class Framebuffer { public readonly framebuffer: WebGLFramebuffer; public readonly texture: WebGLTexture; public readonly width: number; public readonly height: number; constructor(gl: WebGL2RenderingContext, width: number, height: number) { this.width = width; this.height = height; // Create texture for the framebuffer const texture = gl.createTexture(); if (!texture) { throw new Error('Failed to create framebuffer texture'); } this.texture = texture; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null ); // Nearest-neighbor filtering for pixel-perfect upscaling gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // Create framebuffer const framebuffer = gl.createFramebuffer(); if (!framebuffer) { throw new Error('Failed to create framebuffer'); } this.framebuffer = framebuffer; gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0 ); // Check framebuffer status const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { throw new Error(`Framebuffer not complete: ${status}`); } // Unbind gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); } bind(gl: WebGL2RenderingContext): void { gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.viewport(0, 0, this.width, this.height); } unbind(gl: WebGL2RenderingContext): void { gl.bindFramebuffer(gl.FRAMEBUFFER, null); } }