456 lines
15 KiB
TypeScript
456 lines
15 KiB
TypeScript
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 = <WebGL2RenderingContext>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<id> */
|
|
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(<WebGLProgram>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(<WebGLProgram>defaultProgram, "view");
|
|
gl.uniformMatrix4fv(view_loc, false, view);
|
|
let model_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "model");
|
|
gl.uniformMatrix4fv(model_loc, false, model);
|
|
let projection_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "projection");
|
|
gl.uniformMatrix4fv(projection_loc, false, projection);
|
|
let eye_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "eyePos");
|
|
gl.uniform3fv(eye_loc, camera.pos);
|
|
//let uTime_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "uTime");
|
|
//gl.uniform1f(uTime_loc, timeSpent);
|
|
let displacementMap_loc = gl.getUniformLocation(<WebGLProgram>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 = <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(); |