diff --git a/webgl/helpers.mjs b/webgl/helpers.mjs index 116554f..4f1748b 100644 --- a/webgl/helpers.mjs +++ b/webgl/helpers.mjs @@ -43,4 +43,22 @@ export function calcNormalMatrix(modelView) mat4.invert(normalMatrix, modelView); mat4.transpose(normalMatrix, normalMatrix); return normalMatrix; +} + +export function rgbaToArray(val) +{ + return [ + (val & 0xff) / 255.0, + (val >> 8 & 0xff) / 255.0, + (val >> 16 & 0xff) / 255.0, + (val >> 24 & 0xff) / 255.0, + ]; +} + +export function arrayToRGBA(arr) +{ + return Math.round(arr[0]) | + (Math.round(arr[1]) << 8) | + (Math.round(arr[2]) << 16) | + (Math.round(arr[3]) << 24); } \ No newline at end of file diff --git a/webgl/index.html b/webgl/index.html index 63dc35e..d020cb0 100644 --- a/webgl/index.html +++ b/webgl/index.html @@ -9,5 +9,9 @@ import {run} from './webgl.mjs'; run(); +

Click to add voxels

+

Hold alt and click to remove voxels

+

Scroll to zoom

+

Press middle mouse and drag to rotate

\ No newline at end of file diff --git a/webgl/input.mjs b/webgl/input.mjs new file mode 100644 index 0000000..45c63f7 --- /dev/null +++ b/webgl/input.mjs @@ -0,0 +1,56 @@ +class InputManager { + + static mouseX = 0; + static mouseY = 0; + static middleDown; + static initialize(canvas, addCallback, removeCallback, cameraCallback, zoomCallback) + { + InputManager.mouseX = 0; + InputManager.mouseY = 0; + canvas.addEventListener('mousemove', function(evt) { + var mousePos = InputManager.getMousePos(canvas, evt); + InputManager.mouseX = mousePos.x; + InputManager.mouseY = mousePos.y; + if(InputManager.middleDown) + { + cameraCallback(evt.movementX, evt.movementY); + } + //console.log(mousePos); + }); + canvas.addEventListener('mousedown', function(evt) { + if(evt.button == 0) + { + if(evt.altKey) + { + removeCallback(); + } + else + { + addCallback(); + } + } + else if(evt.button == 1){ + InputManager.middleDown = true; + } + }); + canvas.addEventListener('mouseup', function(evt) { + if(evt.button == 1) + { + InputManager.middleDown = false; + } + }); + canvas.addEventListener('wheel', function(evt) { + zoomCallback(evt.deltaY); + }) + } + + static getMousePos(canvas, evt) { + var rect = canvas.getBoundingClientRect(); + return { + x: evt.clientX - rect.left, + y: rect.height - (evt.clientY - rect.top) + }; + } +} + +export {InputManager} \ No newline at end of file diff --git a/webgl/shaders.mjs b/webgl/shaders.mjs index 4234ed8..84405cf 100644 --- a/webgl/shaders.mjs +++ b/webgl/shaders.mjs @@ -3,6 +3,7 @@ const vertShader = `#version 300 es in vec4 aVertexPosition; in vec3 aVertexNormal; in vec2 aTextureCoord; + in vec4 aFaceColor; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; @@ -10,6 +11,7 @@ const vertShader = `#version 300 es out highp vec3 vLighting; out highp vec2 vTextureCoord; + out highp vec4 vFaceColor; void main() { gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; @@ -23,6 +25,7 @@ const vertShader = `#version 300 es highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0); vLighting = ambientLight + (directionalLightColor * directional); vTextureCoord = aTextureCoord; + vFaceColor = aFaceColor; } `; @@ -31,6 +34,7 @@ const fragShader = `#version 300 es in highp vec3 vLighting; in highp vec2 vTextureCoord; + in highp vec4 vFaceColor; uniform sampler2D uSampler; @@ -39,7 +43,7 @@ const fragShader = `#version 300 es void main() { color = texture(uSampler, vTextureCoord) * vec4(vLighting, 1.0); - collision = texture(uSampler, vTextureCoord); + collision = vFaceColor; } `; @@ -73,6 +77,7 @@ export function initPrograms(gl) { vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'), vertexTextureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'), vertexNormal: gl.getAttribLocation(shaderProgram, 'aVertexNormal'), + faceColor: gl.getAttribLocation(shaderProgram, 'aFaceColor'), }, uniformLocations: { projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'), diff --git a/webgl/voxels.mjs b/webgl/voxels.mjs index 6767e7b..75d56a7 100644 --- a/webgl/voxels.mjs +++ b/webgl/voxels.mjs @@ -1,21 +1,78 @@ -import {bindFloatBuffer, makeColorTexture, calcModelView, calcNormalMatrix} from './helpers.mjs'; +import {bindFloatBuffer, makeColorTexture, + calcModelView, calcNormalMatrix, + rgbaToArray, arrayToRGBA} + from './helpers.mjs'; + +var voxelCount = 0; class Voxel { - constructor(gl, x, y, z, col) { + static voxelMap = {} + // maps to the block in front of the indexed face + // front, back, top, bottom, right, left + static faceMappings = [[0, 0, 1], [0, 0, -1], [0, 1, 0], [0, -1, 0], [1, 0, 0], [-1, 0, 0]]; + + // optionally be a ghost voxel, will stop the voxel from being put into the map for collision + constructor(gl, x, y, z, col, ghost=false) { this.pos = [x, y, z]; this.texture = makeColorTexture(gl, new Uint8Array(col)); + this.ghost = ghost; + if(!this.ghost) + { + this.voxelIndex = voxelCount; + Voxel.voxelMap[this.voxelIndex] = this; + voxelCount += 1; + + const baseCol = 1 + this.voxelIndex * 6; + const faceCols = [baseCol, baseCol + 1, baseCol + 2, baseCol + 3, baseCol + 4, baseCol + 5]; + var faceColArray = []; + for(var i = 0; i < faceCols.length; i++) + { + var val = rgbaToArray(faceCols[i]); + val[3] = 1.0; + faceColArray = faceColArray.concat(val, val, val, val); + } + } + else + { + this.voxelIndex = -1; + const baseCol = 1; + const faceCols = [baseCol, baseCol + 1, baseCol + 2, baseCol + 3, baseCol + 4, baseCol + 5]; + var faceColArray = []; + for(var i = 0; i < faceCols.length; i++) + { + var val = rgbaToArray(faceCols[i]); + val[3] = 0.0; + faceColArray = faceColArray.concat(val, val, val, val); + } + } + + + const faceColBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, faceColBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(faceColArray), gl.STATIC_DRAW); + this.faceColors = faceColBuffer; } drawVoxel(gl, programInfo) { - const modelViewMatrix = calcModelView(this.pos, [0, 0, 0]); + var modelViewMatrix; + // shrink ghost view a little so it doesn't clip + if(!this.ghost) + { + modelViewMatrix = calcModelView(this.pos, [0, 0, 0]); + } + else + { + modelViewMatrix = calcModelView(this.pos, [0, 0, 0], [0.9, 0.9, 0.9]); + } const normalMatrix = calcNormalMatrix(modelViewMatrix); gl.uniformMatrix4fv(programInfo.uniformLocations.modelViewMatrix, false, modelViewMatrix); gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, normalMatrix); gl.bindTexture(gl.TEXTURE_2D, this.texture); + bindFloatBuffer(gl, this.faceColors, programInfo.attribLocations.faceColor, 4); { const offset = 0; const vertexCount = 36; @@ -24,6 +81,12 @@ class Voxel { } } + + destroyVoxel() + { + delete Voxel.voxelMap[this.voxelIndex]; + } + static setupVoxelDrawing(gl, programInfo, projectionMatrix, buffers) { gl.useProgram(programInfo.program); @@ -161,6 +224,26 @@ class Voxel { textureCoord: textureBuffer, }; } + + static getVoxelByCol(rgba) + { + // zero out the alpha channel + rgba[3] = 0; + const val = arrayToRGBA(rgba) - 1; + if(val == -1) + { + return null; + } + const index = Math.trunc(val / 6); + const face = val % 6; + const voxel = Voxel.voxelMap[index]; + const targeted = Voxel.faceMappings[face]; + return { + voxel: voxel, + face: face, + targetedSpace: [voxel.pos[0] + targeted[0], voxel.pos[1] + targeted[1], voxel.pos[2] + targeted[2]], + } + } } export {Voxel} \ No newline at end of file diff --git a/webgl/webgl.mjs b/webgl/webgl.mjs index 70ca488..2ea39c5 100644 --- a/webgl/webgl.mjs +++ b/webgl/webgl.mjs @@ -1,6 +1,7 @@ import {initPrograms} from './shaders.mjs'; import {Voxel} from './voxels.mjs'; import {calcModelView, bindFloatBuffer} from './helpers.mjs'; +import {InputManager} from './input.mjs'; var mat4 = glMatrix.mat4; @@ -11,20 +12,28 @@ var buffers; var screenBuffers; var voxels; -var cameraPos = [0.0, 0.0, 6.0 ]; +var ghostVoxel; +var ghostVisible = false; +var targetedVoxel; +var newVoxelColor = [100, 100, 100, 255]; + +var cameraPos = [0.0, 0.0, 0.0 ]; var cameraRot = [0.0, 0.0, 0.0]; +var cameraDist = 10.0; var renderTextures; function main() { canvas = document.getElementById('glCanvas'); - gl = canvas.getContext('webgl2'); + gl = canvas.getContext('webgl2', {alpha: false}); if(gl === null) { const dContext = canvas.getContext('2d'); - dContext.fillText('Your browser no like webgl, use better browser', 10, 10); + dContext.fillText('Your browser no like webgl2, use better browser', 10, 10); } + InputManager.initialize(canvas, addVoxel, removeVoxel, rotateCamera, zoomCamera); + shaderInfo = initPrograms(gl); buffers = Voxel.initBuffers(gl); screenBuffers = initScreenBuffer(gl); @@ -35,6 +44,8 @@ function main() { new Voxel(gl, -1.0, -1.0, 0, [0, 0, 255, 255]), new Voxel(gl, -1.0, 0.0, 0, [255, 255, 255, 255]), ]; + + ghostVoxel = new Voxel(gl, 0, 0, 0, [255, 255, 255, 200], true); renderTextures = setupRenderTextures(gl); @@ -97,8 +108,13 @@ function setupRenderTextures(gl) { gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]); + const cfb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, cfb); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, collisionTexture, 0); + return { framebuffer: fb, + collisionOnlyBuffer: cfb, mainTexture: mainTexture, collisionTexture: collisionTexture, }; @@ -125,11 +141,12 @@ function render(now) { curTime = now; drawScene(gl, shaderInfo.objectInfo, buffers, dt); drawScreen(gl, shaderInfo.screenInfo, screenBuffers, renderTextures.mainTexture); - var test = gl.getError(); + checkMouseOver(gl); + /*var test = gl.getError(); if(test != 0) { console.log(test); - } + }*/ requestAnimationFrame(render); } @@ -140,6 +157,42 @@ function clearScreen(gl) { gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); +} + +function checkMouseOver(gl) +{ + gl.bindFramebuffer(gl.FRAMEBUFFER, renderTextures.collisionOnlyBuffer); + + var x = InputManager.mouseX; + var y = InputManager.mouseY; + var width = gl.canvas.clientWidth; + var height = gl.canvas.clientHeight; + var pixels = new Uint8Array(4); + // we are off the screeen + if (x < 0 || x >= width || y < 0 || y >= height) + { + + } + else + { + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + if(pixels[0] != 0 || pixels[1] != 0 || pixels[2] != 0) + { + var voxelInfo = Voxel.getVoxelByCol(pixels); + ghostVisible = true; + var newPos = voxelInfo.targetedSpace; + newPos = [newPos[0] + 0.05, newPos[1] + 0.05, newPos[2] + 0.05]; + ghostVoxel.pos = newPos; + targetedVoxel = voxelInfo.voxel; + } + else { + ghostVisible = false; + targetedVoxel = null; + } + } + } function getProjection(gl, fov) @@ -164,10 +217,14 @@ function getCameraMatrix(cameraPos, cameraRot) function drawScene(gl, programInfo, buffers, dt) { gl.bindFramebuffer(gl.FRAMEBUFFER, renderTextures.framebuffer); clearScreen(gl); - cameraRot[1] += dt * 180 / Math.PI; - const cameraDist = 10.0; - cameraPos[0] = cameraDist * Math.sin(cameraRot[1] * Math.PI / 180); - cameraPos[2] = cameraDist * Math.cos(cameraRot[1] * Math.PI / 180); + //cameraRot[1] = 30; + const phi = cameraRot[0] * Math.PI / 180; + const theta = cameraRot[1] * Math.PI / 180; + cameraPos[0] = cameraDist * Math.sin(theta) * Math.cos(phi); + cameraPos[1] = -cameraDist * Math.sin(phi); + cameraPos[2] = cameraDist * Math.cos(theta) * Math.cos(phi); + + //cameraPos[2] = cameraDist * Math.cos(cameraRot[1] * Math.PI / 180); //console.log(cameraPos); const projectionMatrix = getProjection(gl, 45); @@ -180,6 +237,10 @@ function drawScene(gl, programInfo, buffers, dt) { var v = voxels[i]; v.drawVoxel(gl, programInfo); } + if(ghostVisible) + { + ghostVoxel.drawVoxel(gl, programInfo); + } } function drawScreen(gl, programInfo, buffers, texture) @@ -194,6 +255,50 @@ function drawScreen(gl, programInfo, buffers, texture) gl.drawArrays(gl.TRIANGLES, 0, 6); } +function addVoxel() +{ + if(ghostVisible) + { + var toAdd = new Voxel(gl, ghostVoxel.pos[0] - 0.05, ghostVoxel.pos[1] - 0.05, ghostVoxel.pos[2] - 0.05, newVoxelColor); + voxels.push(toAdd); + } +} + +function removeVoxel() +{ + if(targetedVoxel != null && voxels.length > 1) + { + for(var i = 0; i < voxels.length; i++) + { + if(voxels[i].voxelIndex == targetedVoxel.voxelIndex) + { + voxels.splice(i, 1); + targetedVoxel.destroyVoxel(); + break; + } + } + } +} + +function rotateCamera(dx, dy) +{ + cameraRot[1] -= dx; + cameraRot[0] -= dy; +} + +function zoomCamera(delta) +{ + cameraDist += delta * 0.05; + if(cameraDist < 2) + { + cameraDist = 2; + } + if(cameraDist > 100) + { + cameraDist = 100; + } +} + export function run() { main();