import { Shader } from "./Shader"; import { mat4 } from "gl-matrix"; import { Cube } from "./cube"; import vertexSource from "./vertices.glsl"; import fragmentSource from "./fragments.glsl"; const ROTATION_SPEED = 0.0005; class RendererError extends Error {} class Renderer { private canvas: HTMLCanvasElement; private analyser: AnalyserNode; private analyserData: Uint8Array; private time: number; private buffers: { indices?: WebGLBuffer; positions?: WebGLBuffer; normals?: WebGLBuffer; fft?: WebGLBuffer; }; constructor( context: AudioContext, node: AudioNode, canvas: HTMLCanvasElement ) { const analyser = context.createAnalyser(); analyser.fftSize = 2048; analyser.smoothingTimeConstant = 0.8; node.connect(analyser); this.canvas = canvas; this.analyser = analyser; this.analyserData = new Uint8Array(analyser.frequencyBinCount); this.time = 0; this.buffers = {}; } resize() { this.canvas.width = this.canvas.parentElement!.clientWidth; this.canvas.height = this.canvas.parentElement!.clientHeight; } initializeScene() { this.resize(); const gl = this.canvas.getContext("webgl2"); if (gl === null) { throw new RendererError("WebGL (2) is unsupported on this browser"); } const shader = Shader.builder(gl) .addShader(vertexSource, gl.VERTEX_SHADER) .addShader(fragmentSource, gl.FRAGMENT_SHADER) .addAttribute("aVertexPosition") .addAttribute("aVertexNormal") .addAttribute("aHeight") .addUniforms("uProjectionMatrix") .addUniforms("uModelViewMatrix") .addUniforms("uNormalMatrix") .build(); this.initGL(gl, shader); this.initBuffers(gl); this.drawScene(gl, shader); } initBuffers(gl: WebGLRenderingContext) { // Scale down the unit cube before we use it Cube.vertices = Cube.vertices.map( (num: number) => num / this.analyser.frequencyBinCount ); // Position buffer const positionBuffer = gl.createBuffer(); if (positionBuffer === null) { throw new Error("could not initialize position buffer"); } gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, Cube.vertices, gl.STATIC_DRAW); this.buffers.positions = positionBuffer; // Index buffer const indexBuffer = gl.createBuffer(); if (indexBuffer === null) { throw new Error("could not initialize index buffer"); } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Cube.indices, gl.STATIC_DRAW); this.buffers.indices = indexBuffer; // Surface normal buffer const normalBuffer = gl.createBuffer(); if (normalBuffer === null) { throw new Error("could not initialize normal buffer"); } gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); gl.bufferData(gl.ARRAY_BUFFER, Cube.normals, gl.STATIC_DRAW); this.buffers.normals = normalBuffer; // fft data buffer const fftBuffer = gl.createBuffer(); if (fftBuffer === null) { throw new Error("could not initialize fft buffer"); } // No need to initialize this buffer here since we will be // updating it as soon as we start rendering anyway. this.buffers.fft = fftBuffer; } initGL(gl: WebGLRenderingContext, shader: Shader) { gl.useProgram(shader.program); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LESS); } updateMatrices(gl: WebGLRenderingContext, shader: Shader) { const projectionMatrix = mat4.create(); mat4.perspective( projectionMatrix, (45 * Math.PI) / 180, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.1, 100.0 ); gl.uniformMatrix4fv( shader.getUniform("uProjectionMatrix"), false, projectionMatrix ); const modelViewMatrix = mat4.create(); mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -1.5]); mat4.rotateX(modelViewMatrix, modelViewMatrix, Math.PI / 6); mat4.rotateY( modelViewMatrix, modelViewMatrix, this.time * ROTATION_SPEED ); mat4.translate(modelViewMatrix, modelViewMatrix, [-1.0, 0.0, 0.0]); gl.uniformMatrix4fv( shader.getUniform("uModelViewMatrix"), false, modelViewMatrix ); const normalMatrix = mat4.create(); mat4.invert(normalMatrix, modelViewMatrix); mat4.transpose(normalMatrix, normalMatrix); gl.uniformMatrix4fv( shader.getUniform("uNormalMatrix"), false, normalMatrix ); } drawScene(gl: WebGL2RenderingContext, shader: Shader) { if ( this.buffers.indices === undefined || this.buffers.positions === undefined || this.buffers.normals === undefined || this.buffers.fft === undefined ) { throw new Error("failed to create buffers before rendering"); } this.updateMatrices(gl, shader); // Update cube buffers gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.positions); gl.vertexAttribPointer( shader.getAttribute("aVertexPosition"), 3, gl.FLOAT, false, 0, 0 ); gl.enableVertexAttribArray(shader.getAttribute("aVertexPosition")); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffers.indices); gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.normals); gl.vertexAttribPointer( shader.getAttribute("aVertexNormal"), 3, gl.FLOAT, false, 0, 0 ); gl.enableVertexAttribArray(shader.getAttribute("aVertexNormal")); // Update fft this.analyser.getByteFrequencyData(this.analyserData); gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.fft); gl.bufferData(gl.ARRAY_BUFFER, this.analyserData, gl.STREAM_DRAW); gl.vertexAttribPointer( shader.getAttribute("aHeight"), 1, gl.UNSIGNED_BYTE, false, 0, 0 ); gl.vertexAttribDivisor(shader.getAttribute("aHeight"), 1); gl.enableVertexAttribArray(shader.getAttribute("aHeight")); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.drawElementsInstanced( gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0, this.analyser.frequencyBinCount ); requestAnimationFrame((time) => { this.time = time; this.drawScene(gl, shader); }); } } export { Renderer, RendererError };