import { vec3, mat4, vec4 } from 'gl-matrix' var gl: WebGL2RenderingContext; var viewportWidth = 0; var viewportHeight = 0; /** A camera that always looks at the world origin. Can have an offset and be rotated. */ class Camera { pos: vec3; target: vec3; up: vec3; xRot: number; yRot: number; offset: number; constructor() { this.pos = vec3.create(); vec3.set(this.pos, 0.0, 0.0, 0.0); this.target = vec3.create(); vec3.set(this.target, 0.0, 0.0, 0.0); this.up = vec3.create(); vec3.set(this.up, 0.0, 1.0, 0.0); this.xRot = 0.0; this.yRot = 0.0; this.offset = 0.0; } setRotationX(rotX: number): void { this.xRot = rotX; this.updatePos(); } setRotationY(rotY: number): void { this.yRot = rotY; this.updatePos(); } /** Sets the offset to world origin. */ setOffset(off: number): void { this.offset = off; this.updatePos(); } /** Reculcates the position according to xy-rotation and offset. */ private updatePos(): void { var transformation: mat4 = mat4.create(); mat4.identity(transformation); //2. xy-Rotation mat4.rotateX(transformation, transformation, this.xRot); mat4.rotateY(transformation, transformation, this.yRot); //1. Translation var translation = vec3.create(); vec3.set(translation, 0.0, 0.0, this.offset); mat4.translate(transformation, transformation, translation); var temp: vec4 = vec4.create(); vec4.set(temp, 0.0, 0.0, 0.0, 1.0); vec4.transformMat4(temp, temp, transformation); vec3.set(this.pos, temp[0], temp[1], temp[2]); } getViewMatrix(): mat4 { var ret: mat4; ret = mat4.create(); mat4.lookAt(ret, this.pos, this.target, this.up); return ret; } } /** Init OpenGL and gets the viewport/canvas sizes */ function initGL(canvas: HTMLCanvasElement) { var gltemp; try { gltemp = canvas.getContext("webgl2"); if (!gltemp) gltemp = canvas.getContext("experimental-webgl2"); if (gltemp != null) { viewportWidth = canvas.width; viewportHeight = canvas.height; } } catch (e) { } // Not the best error detection logic. // Redirect to http://get.webgl.org in failure case. if (gltemp == null) { alert("Unable to initialize WebGL2. Your browser or machine may not support it."); } gl = gltemp; //WebGL2 supports floating point textures by default but it does not support filtering them or rendering to them by default. Note: 16bit filtering is included 32bit not if (!gl.getExtension('EXT_color_buffer_float')) { alert("32Bit/16Bit single Color render Buffers not available."); } //allow 16bit texture as framebuffer target gl.enable(gl.DEPTH_TEST); } /** Grid for the watersurface */ var gridIndices: number[] = []; var gridVertices: number[] = []; function generateGrid(N: number) { for (let j = 0; j <= N; ++j) { for (let i = 0; i <= N; ++i) { //Generate Vertices let x = i / N; let y = j / N; let z = 0; gridVertices.push(x); gridVertices.push(y); gridVertices.push(z); if (i < N && j < N) //Skip edges { let row1 = j * (N + 1); let row2 = (j + 1) * (N + 1); // triangle 1 gridIndices.push(row1 + i); gridIndices.push(row1 + i + 1); gridIndices.push(row2 + i + 1); // triangle 2 gridIndices.push(row1 + i); gridIndices.push(row2 + i + 1); gridIndices.push(row2 + i); } } } } var gridVAO: WebGLVertexArrayObject | null = null; function initGridVAO() { generateGrid(128); gridVAO = gl.createVertexArray(); gl.bindVertexArray(gridVAO); let vboGrid: WebGLBuffer | null = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vboGrid); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(gridVertices), gl.STATIC_DRAW); let iboGrid: WebGLBuffer | null = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iboGrid); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(gridIndices), gl.STATIC_DRAW); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0); gl.enableVertexAttribArray(0); gl.bindVertexArray(null); } /** Init Geomtry for an Triangle */ var VBO: WebGLBuffer | null = null; function initGeometry() { VBO = gl.createBuffer(); //Vertex data represent fullscreen quad in NDC-Space // X, Y, Z, U, V let vertexData = [-1.0, -1.0, 0.0, /*BOTTOM LEFT*/ 0.0, 0.0, 1.0, -1.0, 0.0, /*BOTTOM RIGHT*/ 1.0, 0.0, -1.0, 1.0, 0.0, /*TOP LEFT */ 0.0, 1.0, 1.0, -1.0, 0.0, /*BOTTOM RIGHT */ 1.0, 0.0, -1.0, 1.0, 0.0, /*TOP LEFT */ 0.0, 1.0, 1.0, 1.0, 0.0, /*TOP RIGHT */ 1.0, 1.0 ]; gl.bindBuffer(gl.ARRAY_BUFFER, VBO); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW); } /** Get shader source by HTML-Element */ function getShader(id: string) { var script: any = document.getElementById(id); if (!script) { console.log("Coudln't get shader source from HTML!"); return null; } var shader: WebGLShader | null; if (script.type == "x-shader/x-fragment") { shader = gl.createShader(gl.FRAGMENT_SHADER); } else if (script.type == "x-shader/x-vertex") { shader = gl.createShader(gl.VERTEX_SHADER); } else { console.log("Script type is wrong!"); return null; } if (shader == null) { console.log("Cannot create Shaders!") return null; } gl.shaderSource(shader, script.text); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert(gl.getShaderInfoLog(shader)); return null; } return shader; } /** Init all Shaders that are needed */ var perlinNoiseProgram: WebGLProgram | null; var defaultProgram: WebGLProgram | null; var textureProgram: WebGLProgram | null; function initShaders() { { //Get perlinNoiseProgram let vertexShader = getShader("ndc-vs"); let fragmentShader = getShader("noise-fs"); if (vertexShader == null || fragmentShader == null) { console.log("Didn't find shader sources!") return null; } perlinNoiseProgram = gl.createProgram(); if (perlinNoiseProgram == null) { console.log("Program not created!") return null } gl.attachShader(perlinNoiseProgram, vertexShader); gl.attachShader(perlinNoiseProgram, fragmentShader); gl.linkProgram(perlinNoiseProgram); if (!gl.getProgramParameter(perlinNoiseProgram, gl.LINK_STATUS)) { alert("Could not initialise shaders"); } } { //Get defaultShaders let vertexShader = getShader("default-vs"); let fragmentShader = getShader("default-fs"); if (vertexShader == null || fragmentShader == null) { console.log("Didn't find shader sources!") return null; } defaultProgram = gl.createProgram(); if (defaultProgram == null) { console.log("Program not created!") return null } gl.attachShader(defaultProgram, vertexShader); gl.attachShader(defaultProgram, fragmentShader); gl.linkProgram(defaultProgram); if (!gl.getProgramParameter(defaultProgram, gl.LINK_STATUS)) { alert("Could not initialise shaders"); } } { //Get textureShader let vertexShader = getShader("texture-vs"); let fragmentShader = getShader("texture-fs"); if (vertexShader == null || fragmentShader == null) { console.log("Didn't find shader sources!") return null; } textureProgram = gl.createProgram(); if (textureProgram == null) { console.log("Program not created!") return null } gl.attachShader(textureProgram, vertexShader); gl.attachShader(textureProgram, fragmentShader); gl.linkProgram(textureProgram); if (!gl.getProgramParameter(textureProgram, gl.LINK_STATUS)) { alert("Could not initialise shaders"); } } } /** Init an FBO used for the first render pass / perlin noise */ var perlinNoiseFBO: WebGLFramebuffer | null = null; var textureFBO: WebGLTexture | null = null; var perlinNoiseFBOWidth = 256; var ferlinNoiseFBOHeight = 256; function initFBO() { perlinNoiseFBO = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, perlinNoiseFBO); // Add attachments textureFBO = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, textureFBO); //last 3 parameter not intertesting becuase we are not supplying data gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, perlinNoiseFBOWidth, ferlinNoiseFBOHeight, 0, gl.RED, gl.HALF_FLOAT, null); // set the filtering so we don't need mips gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 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); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureFBO, 0); if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) { console.log("Framebuffer creation failed."); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); //Reset to default framebuffer } /** Update/Draw function.*/ /** Framerate measurement variables */ var timeSpent = 0.0; var lastTime = new Date().getTime(); var counter = 0.0; var fps = 0; /** Input states*/ var mouseXVel = 0; var mouseYVel = 0; /** Objects and states*/ var camera: Camera; var curRotX = 1.1; var curRotY = 0; function drawScene() { fps++; let now = new Date(); let delta = now.getTime() - lastTime; timeSpent += delta; if ((counter += delta) >= 1000.) { counter = 0; console.log("FPS: " + fps); fps = 0; } lastTime = now.getTime(); // Two Rendering passes. The first one generates a perlin noise // texture. Second one uses the textur for vertex displacement // of a grid representing the water surface. //--- First render pass -> Perlin Noise (it updates the perlin noise texture) { gl.bindFramebuffer(gl.FRAMEBUFFER, perlinNoiseFBO); gl.viewport(0, 0, perlinNoiseFBOWidth, ferlinNoiseFBOHeight); //Clear buffer content gl.clearColor(1.0, 1.0, 1.0, 1); gl.clear(gl.COLOR_BUFFER_BIT); //No depth buffer //draw a fullscreen quad gl.bindBuffer(gl.ARRAY_BUFFER, VBO); // There are 7 floating-point values per vertex let stride = 5 * Float32Array.BYTES_PER_ELEMENT; // Set up position stream gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, 3 * Float32Array.BYTES_PER_ELEMENT); gl.enableVertexAttribArray(1); gl.useProgram(perlinNoiseProgram); let uTime = gl.getUniformLocation(perlinNoiseProgram, "uTime"); gl.uniform1f(uTime, timeSpent); gl.drawArrays(gl.TRIANGLES, 0, 6); // Draw fullscreen quad } //--- Second render pass -> Geomtry with displacement by perlin noise texture --- { gl.bindFramebuffer(gl.FRAMEBUFFER, null); //Bind default framebuffer gl.viewport(0, 0, viewportWidth, viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.activeTexture(gl.TEXTURE0); //Binds the texture to 0 gl.bindTexture(gl.TEXTURE_2D, textureFBO); var projection = mat4.create(); mat4.identity(projection); mat4.perspective(projection, 1.0, viewportWidth / viewportHeight, 0.1, 1000.); //projection mode should actually be camera specific camera.setOffset(1.2); camera.setRotationX((curRotX += mouseYVel * 0.001)); camera.setRotationY((curRotY += mouseXVel * 0.001)); var view = camera.getViewMatrix(); var model = mat4.create(); mat4.identity(model); let translationCentering = vec3.create(); vec3.set(translationCentering, -0.5, -0.5, 0.0); mat4.translate(model, model, translationCentering); //1. First Center the Surface in the origin. gl.useProgram(defaultProgram); let view_loc = gl.getUniformLocation(defaultProgram, "view"); gl.uniformMatrix4fv(view_loc, false, view); let model_loc = gl.getUniformLocation(defaultProgram, "model"); gl.uniformMatrix4fv(model_loc, false, model); let projection_loc = gl.getUniformLocation(defaultProgram, "projection"); gl.uniformMatrix4fv(projection_loc, false, projection); let eye_loc = gl.getUniformLocation(defaultProgram, "eyePos"); gl.uniform3fv(eye_loc, camera.pos); //let uTime_loc = gl.getUniformLocation(defaultProgram, "uTime"); //gl.uniform1f(uTime_loc, timeSpent); let displacementMap_loc = gl.getUniformLocation(defaultProgram, "displace_map"); gl.uniform1i(displacementMap_loc, 0); //Get texture from slot 0 gl.bindVertexArray(gridVAO); gl.drawElements(gl.TRIANGLES, gridIndices.length, gl.UNSIGNED_INT, 0); gl.bindVertexArray(null); } requestAnimationFrame(drawScene); } function main() { const canvas: HTMLCanvasElement = document.getElementById("window"); initGL(canvas); var drag = false; var previousPosX: number | null; var previousPosY: number | null; canvas.addEventListener('mousedown', function (evt) { drag = true; }, false); canvas.addEventListener('mousemove', function (evt) { if (drag) { if (previousPosX == null || previousPosY == null) { previousPosX = evt.x; previousPosY = evt.y; } var mousePosX = evt.x; var mousePosY = evt.y; mouseXVel = (mousePosX - previousPosX); mouseYVel = (mousePosY - previousPosY); previousPosX = mousePosX; previousPosY = mousePosY; } }, false); var deactivateMouseMovement = function () { previousPosX = null; previousPosY = null; mouseXVel = 0.0; mouseYVel = 0.0; drag = false; } canvas.addEventListener('mouseup', deactivateMouseMovement, false); canvas.addEventListener('mouseleave', deactivateMouseMovement, false); initShaders(); initGeometry(); initFBO(); initGridVAO(); camera = new Camera(); //Check if any errors apeared during init. if (gl.getError() != gl.NO_ERROR) { console.log("OpenGL Error!: "); } drawScene(); } main();