From 49e814d1bc0143ebfb0fefd47571e63683db204f Mon Sep 17 00:00:00 2001 From: Paul Mathieu Date: Wed, 8 Dec 2021 09:24:19 -0800 Subject: [PATCH] A few things - webgl boilerplate - shader ambiant + diffuse lighting - fractal perlin noise chunk generation - super primitive world generation (stone, dirt, grass blocks) - WASD controls + mouse pointer capture On the menu: - collisions - gravity - mine & place - more stuff --- .gitignore | 1 + geometry.js | 127 ++++++++++++++++ gl.js | 49 ++++++ index.html | 13 ++ index.js | 420 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 12 ++ se3.js | 102 +++++++++++++ texture.png | Bin 0 -> 39727 bytes yarn.lock | 111 ++++++++++++++ 9 files changed, 835 insertions(+) create mode 100644 .gitignore create mode 100644 geometry.js create mode 100644 gl.js create mode 100644 index.html create mode 100644 index.js create mode 100644 package.json create mode 100644 se3.js create mode 100644 texture.png create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/geometry.js b/geometry.js new file mode 100644 index 0000000..ac1d8de --- /dev/null +++ b/geometry.js @@ -0,0 +1,127 @@ +import * as se3 from './se3'; + +/** Makes a square with constant z = 0. + * + * Face is oriented towards z+. + */ +function makeSquare() { + const vertices = [ + // triangle 0 + 0.5, 0.5 , 0.0, + -0.5, 0.5, 0.0, + -0.5, -0.5, 0.0, + + // triangle 1 + 0.5, 0.5, 0.0, + -0.5, -0.5, 0.0, + 0.5, -0.5, 0.0, + ]; + + // normals toward z+ + const normals = [].concat(...Array(6).fill([0.0, 0.0, 1.0])); + + const textures = [ + 0.99, 0.99, + 0.01, 0.99, + 0.01, 0.01, + + 0.99, 0.99, + 0.01, 0.01, + 0.99, 0.01, + ]; + + return { + vertices, + normals, + textures, + }; +} + +function makeZFace(normal, texture, transform) { + const textMul = 0.0625; + const textOff = texture.map(x => x * textMul); + + const vertices = []; + const textures = []; + + const sq = makeSquare(); + + for (let i = 0; i < 6; i++) { + vertices.push( + se3.product( + transform, + sq.vertices.slice(3 * i, 3 * (i + 1)).concat([1.0]) + ).slice(0, 3)); + textures.push(sq.textures.slice(2 * i, 2 * (i + 1)).map((v, j) => textMul * v + textOff[j])); + } + + const normals = [].concat(...Array(6).fill(normal)); + + return { + vertices: [].concat(...vertices), + normals, + textures: [].concat(...textures), + }; + +} + +export function makeFace(which, texture, centerPos) { + const rot = { + '-x': se3.roty(-Math.PI / 2), + '+x': se3.roty(Math.PI / 2), + '-y': se3.rotx(Math.PI / 2), + '+y': se3.rotx(-Math.PI / 2), + '-z': se3.roty(Math.PI), + '+z': se3.identity(), + }[which]; + + const normal = { + '+x': [1.0, 0.0, 0.0], + '-x': [-1.0, 0.0, 0.0], + '+y': [0.0, 1.0, 0.0], + '-y': [0.0, -1.0, 0.0], + '+z': [0.0, 0.0, 1.0], + '-z': [0.0, 0.0, -1.0], + }[which]; + + const tf = se3.product(se3.translation(...centerPos), rot); + return makeZFace(normal, texture, tf); +} + +export function makeBuffersFromFraces(gl, faces) { + vertices = [].concat(...faces.map(f => f.vertices)); + normals = [].concat(...faces.map(f => f.normals)); + textures = [].concat(...faces.map(f => f.textures)); + + const vertexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + + const normalBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); + + const textureBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textures), gl.STATIC_DRAW); + + return { + vertexBuffer, + normalBuffer, + textureBuffer, + numVertices: vertices.length / 3, + }; +} + +export function makeGrassCube(gl) { + faces = [ + makeFace('+y', [0, 15], [0.0, 0.5, 0.0]), + makeFace('-y', [2, 15], [0.0, -0.5, 0.0]), + makeFace('-x', [1, 15], [-0.5, 0.0, 0.0]), + makeFace('+x', [1, 15], [0.5, 0.0, 0.0]), + makeFace('-z', [1, 15], [0.0, 0.0, -0.5]), + makeFace('+z', [1, 15], [0.0, 0.0, 0.5]), + ]; + + return makeBuffersFromFraces(gl, faces); +} \ No newline at end of file diff --git a/gl.js b/gl.js new file mode 100644 index 0000000..e09dcba --- /dev/null +++ b/gl.js @@ -0,0 +1,49 @@ +function makeShader(gl, type, source) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw Error(`Shader compiler error: ${gl.getShaderInfoLog(shader)}`); + } + + return shader; +} + +export function makeProgram(gl, vertexShader, fragmentShader) { + const vshader = makeShader(gl, gl.VERTEX_SHADER, vertexShader); + const fshader = makeShader(gl, gl.FRAGMENT_SHADER, fragmentShader); + + const shaderProg = gl.createProgram(); + gl.attachShader(shaderProg, vshader); + gl.attachShader(shaderProg, fshader); + gl.linkProgram(shaderProg); + + if (!gl.getProgramParameter(shaderProg, gl.LINK_STATUS)) { + throw Error(`Shader linker error: ${gl.getProgramInfoLog(shaderProg)}`); + } + + return shaderProg; +} + +function getImage(url) { + return new Promise(resolve => { + const image = new Image(); + image.onload = () => resolve(image); + image.src = url; + }); +} + +export async function loadTexture(gl, url) { + const image = await getImage(url); + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); + 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); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + gl.generateMipmap(gl.TEXTURE_2D); + + return texture; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..a104c76 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + WebMinecraft + + + + + +
+ + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..cad921b --- /dev/null +++ b/index.js @@ -0,0 +1,420 @@ +import { makeBuffersFromFraces, makeFace, makeGrassCube } from "./geometry"; +import { loadTexture, makeProgram } from "./gl"; +import * as se3 from './se3'; + +const TEST_VSHADER = ` +attribute vec3 aPosition; +attribute vec3 aNormal; +attribute vec2 aTextureCoord; + +uniform mat4 uProjection; +uniform mat4 uModel; +uniform mat4 uView; + +varying highp vec2 vTextureCoord; +varying lowp vec3 vLighting; +varying lowp float vDistance; + +void main() { + highp mat4 modelview = uView * uModel; + + gl_Position = uProjection * modelview * vec4(aPosition, 1.0); + + lowp vec3 normal = mat3(modelview) * aNormal; + lowp vec3 lightDirection = - normalize(mat3(uView) * vec3(1.0, -0.3, -0.8)); + lowp float diffuseAmount = max(dot(lightDirection, normal), 0.0); + lowp vec3 ambiant = vec3(0.7, 0.7, 0.2); + vLighting = ambiant + vec3(1.0, 1.0, 1.0) * diffuseAmount; + + vTextureCoord = aTextureCoord; + + vDistance = length(modelview * vec4(aPosition, 1.0)); +} +`; + +const TEST_FSHADER = ` +uniform sampler2D uSampler; +uniform lowp vec3 uFogColor; + +varying highp vec2 vTextureCoord; +varying lowp vec3 vLighting; +varying lowp float vDistance; + +void main() { + highp vec4 color = texture2D(uSampler, vTextureCoord); + lowp float fogamount = smoothstep(30.0, 90.0, vDistance); + gl_FragColor = vec4(mix(vLighting * color.rgb, uFogColor, fogamount), color.a); +} +`; + +/** Draw. + * + * @param {WebGLRenderingContext} gl + */ +function draw(gl, params, objects) { + gl.clearColor(0.6, 0.8, 1.0, 1.0); + gl.clearDepth(1.0); + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + + gl.enable(gl.CULL_FACE); + gl.cullFace(gl.BACK); + + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + const camrot = params.camera.orientation; + const campos = params.camera.position; + const viewMat = se3.product( + se3.rotxyz(-camrot[0], -camrot[1], -camrot[2]), + se3.translation(-campos[0], -campos[1], -campos[2]) + ); + + for (const {program, texture, position, orientation, geometry} of objects) { + const viewLoc = gl.getUniformLocation(program, 'uView'); + const modelLoc = gl.getUniformLocation(program, 'uModel'); + const projLoc = gl.getUniformLocation(program, 'uProjection'); + const samplerLoc = gl.getUniformLocation(program, 'uSampler'); + const fogColorLoc = gl.getUniformLocation(program, 'uFogColor'); + + const positionLoc = gl.getAttribLocation(program, 'aPosition'); + const normalLoc = gl.getAttribLocation(program, 'aNormal'); + const textureLoc = gl.getAttribLocation(program, 'aTextureCoord'); + + const mvMatrix = se3.product(se3.translation(...position), se3.rotxyz(...orientation)); + + gl.useProgram(program); + + gl.uniformMatrix4fv(viewLoc, false, new Float32Array(viewMat)); + gl.uniformMatrix4fv(modelLoc, false, new Float32Array(mvMatrix)); + gl.uniformMatrix4fv(projLoc, false, new Float32Array(params.projMatrix)); + gl.uniform3f(fogColorLoc, 0.6, 0.8, 1.0); + + gl.bindBuffer(gl.ARRAY_BUFFER, geometry.vertexBuffer); + gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(positionLoc); + + gl.bindBuffer(gl.ARRAY_BUFFER, geometry.normalBuffer); + gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(normalLoc); + + gl.bindBuffer(gl.ARRAY_BUFFER, geometry.textureBuffer); + gl.vertexAttribPointer(textureLoc, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(textureLoc); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i(samplerLoc, 0); + + gl.drawArrays(gl.TRIANGLES, 0, geometry.numVertices); + } +} + +const stuff = { + lastFrameTime: 0, +}; + +function handleKeys(params) { + const move = (forward, right) => { + const dir = [right, 0, -forward, 1.0]; + const camori = params.camera.orientation; + const ori = se3.rotxyz(...camori); + const tf = se3.apply(ori, dir); + + params.camera.position[0] += tf[0]; + params.camera.position[2] += tf[2]; + }; + + params.keys.forEach(key => { + switch (key) { + case 'KeyW': + move(0.1, 0.0); + return; + case 'KeyA': + move(0.0, -0.1); + return; + case 'KeyS': + move(-0.1, 0.0); + return; + case 'KeyD': + move(0.0, 0.1); + return; + + case 'Space': + params.camera.position[1] += 0.1; + return; + case 'ControlLeft': + params.camera.position[1] -= 0.1; + return; + } + }); +} + +function tick(time, gl, params, objects) { + draw(gl, params, objects); + + handleKeys(params); + + const dt = (time - stuff.lastFrameTime) * 0.001; + stuff.lastFrameTime = time; + + document.querySelector('#fps').textContent = `${1.0 / dt} fps`; + + requestAnimationFrame(time => tick(time, gl, params, objects)); +} + +const BlockType = { + AIR: 1, + DIRT: 2, + GRASS: 3, + STONE: 4, +}; + +function ghettoPerlinNoise(seed, x, y, gridSize = 16) { + const dot = (vx, vy) => vx[0] * vy[0] + vx[1] * vy[1]; + // super ghetto random + const xorshift = (x) => { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + return x; + }; + + const randGrad = (x0, y0) => { + const rand = xorshift(1337 * x0 + seed + 80085 * y0); + return [Math.sin(rand), Math.cos(rand)]; + }; + + const interpol = (a, b, x) => a + (x*x*(3 - 2*x)) * (b - a); + + const x0 = Math.floor(x / gridSize); + const y0 = Math.floor(y / gridSize); + const sx = x / gridSize - x0; + const sy = y / gridSize - y0; + + const n0 = dot(randGrad(x0, y0), [sx, sy]); + const n1 = dot(randGrad(x0 + 1, y0), [sx - 1, sy]); + const n2 = dot(randGrad(x0, y0 + 1), [sx, sy - 1]); + const n3 = dot(randGrad(x0 + 1, y0 + 1), [sx - 1, sy - 1]); + + return interpol(interpol(n0, n1, sx), interpol(n2, n3, sx), sy); +} + +function makeTerrain(x, y) { + const seed = 1337; + + const fractalNoise = (x, y) => ( + ghettoPerlinNoise(seed, x, y, 8) * 0.06 + + ghettoPerlinNoise(seed, x, y, 16) * 0.125 + + ghettoPerlinNoise(seed, x, y, 32) * 0.25 + + ghettoPerlinNoise(seed, x, y, 64) * 0.5 + ); + + const terrain = Array(16); + + for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + terrain[i * 16 + j] = fractalNoise(x + i, y + j); + } + } + + return terrain; +} + +function makeChunk(x, y) { + const terrain = makeTerrain(x, y); + + const chunk = new Uint8Array(16 * 16 * 256); + + for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + const height = Math.floor(64 + 64 * terrain[i * 16 + j]); + // everything above is air + // that block is grass + // everything below is dirt + const offset = i * (16 * 256) + j * 256; + const stoneHeight = Math.min(62, height); + chunk.set(Array(stoneHeight).fill(BlockType.STONE), offset); + if (stoneHeight < height) { + chunk.set(Array(height - 1 - stoneHeight).fill(BlockType.DIRT), offset + stoneHeight); + chunk[offset + height - 1] = BlockType.GRASS; + } + chunk.set(Array(256 - height).fill(BlockType.AIR), offset + height); + } + } + + return chunk; +} + +function makeChunkMesh(chunk, z0, x0) { + const blockPos = i => { + const x = Math.floor(i / (16 * 256)); + const y = Math.floor((i - 16 * 258 * x) / 256); + const z = i - 256 * (16*x + y); + + return [x, y, z]; + }; + + const blockIndex = (i, j, k) => 256 * (i*16 +j) + k; + + const sideNeighbors = (i, j) => { + return [ + {pos: [i - 1, j], dir: '-z'}, + {pos: [i + 1, j], dir: '+z'}, + {pos: [i, j - 1], dir: '-x'}, + {pos: [i, j + 1], dir: '+x'}, + ].filter(v => v.pos.every(i => i >= 0 && i < 16)); + }; + + const faceDir = (dir, x, y, z) => { + return { + '-x': [x - 0.5, y, z], + '+x': [x + 0.5, y, z], + '-z': [x, y, z - 0.5], + '+z': [x, y, z + 0.5], + }[dir]; + }; + + const faces = []; + + for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + for (let k = 0; k < 256; k++) { + const bi = blockIndex(i, j, k); + + const upTexture = (() => { + switch (chunk[bi - 1]) { + case BlockType.GRASS: return [0, 15]; + case BlockType.DIRT: return [2, 15]; + case BlockType.STONE: return [3, 15]; + } + })(); + + if (chunk[bi] == BlockType.AIR) { + faces.push(makeFace('+y', upTexture, [x0 + j, k - 0.5, z0 + i])); + break; + } + + const sideTexture = (() => { + switch (chunk[bi]) { + case BlockType.GRASS: return [1, 15]; + case BlockType.DIRT: return [2, 15]; + case BlockType.STONE: return [3, 15]; + } + })(); + + for ({ pos, dir } of sideNeighbors(i, j)) { + if (chunk[blockIndex(...pos, k)] == BlockType.AIR) { + faces.push(makeFace(dir, sideTexture, faceDir(dir, x0 + j, k, z0 + i))); + } + } + if (i == 0) { + faces.push(makeFace('-z', sideTexture, faceDir('-z', x0 + j, k, z0 + i))); + } + if (i == 15) { + faces.push(makeFace('+z', sideTexture, faceDir('+z', x0 + j, k, z0 + i))); + } + if (j == 0) { + faces.push(makeFace('-x', sideTexture, faceDir('-x', x0 + j, k, z0 + i))); + } + if (j == 15) { + faces.push(makeFace('+x', sideTexture, faceDir('+x', x0 + j, k, z0 + i))); + } + } + } + } + + return faces; +} + +function makeWorldBuffers(gl) { + const chunkFaces = [].concat( + makeChunkMesh(makeChunk(0, -16), 0, -16), + makeChunkMesh(makeChunk(16, -16), 16, -16), + makeChunkMesh(makeChunk(0, 0), 0, 0), + makeChunkMesh(makeChunk(16, 0), 16, 0), + makeChunkMesh(makeChunk(16, 16), 16, 16), + makeChunkMesh(makeChunk(0, 16), 0, 16), + ); + + + return makeBuffersFromFraces(gl, chunkFaces); +} + +// Stuff I need to do: +// [x] a skybox +// [x] a movable camera +// [ ] some kind of gravity +// [ ] collision detection +// [x] more blocks +// [ ] ability to mine & place +// [ ] generating & loading of more chunks +// [x] distance fog +// [ ] different biomes (with different noise stats) +// [ ] non-flowy water +// [ ] flowy water +// [ ] ALIGN CHUNK WITH WORLD COORDS +// [x] better controls + +async function main() { + const canvas = document.querySelector('#game'); + const gl = canvas.getContext('webgl'); + + if (gl === null) { + console.error('webgl not available') + return; + } + + const program = makeProgram(gl, TEST_VSHADER, TEST_FSHADER); + + const params = { + projMatrix: se3.perspective(Math.PI / 3, canvas.clientWidth / canvas.clientHeight, 0.1, 100.0), + camera: { + position: [0.0, 70.5, 0.0], + orientation: [0.0, Math.PI, 0.0], + }, + keys: new Set() + } + + const texture = await loadTexture(gl, 'texture.png'); + + const objects = [ + { + program, + position: [0.0, 0.0, 0.0], + orientation: [0.0, 0.0, 0.0], + geometry: makeWorldBuffers(gl), + texture, + }, + ]; + + canvas.onclick = e => { + canvas.requestPointerLock(); + const keyListener = e => { + if (e.type === 'keydown') { + params.keys.add(e.code); + } else { + params.keys.delete(e.code); + } + }; + const moveListener = e => { + params.camera.orientation[1] -= e.movementX / 500; + params.camera.orientation[0] -= e.movementY / 500; + }; + const changeListener = () => { + if (document.pointerLockElement === canvas) { + return; + } + document.removeEventListener('pointerlockchange', changeListener); + document.removeEventListener('pointermove', moveListener); + document.removeEventListener('keydown', keyListener); + document.removeEventListener('keyup', keyListener); + }; + document.addEventListener('pointerlockchange', changeListener); + document.addEventListener('pointermove', moveListener); + document.addEventListener('keydown', keyListener); + document.addEventListener('keyup', keyListener); + }; + requestAnimationFrame(time => tick(time, gl, params, objects)); +} + +window.onload = main; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..694e5d6 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "wmc", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "esbuild": "^0.14.2" + }, + "scripts": { + "serve": "esbuild --outfile=app.js index.js --bundle --servedir=." + } +} diff --git a/se3.js b/se3.js new file mode 100644 index 0000000..3b88fae --- /dev/null +++ b/se3.js @@ -0,0 +1,102 @@ +// all column-major, *sigh* +export function identity() { + return [ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 + ]; +} + +export function rotx(rad) { + const s = Math.sin(rad); + const c = Math.cos(rad); + + return [ + 1.0, 0.0, 0.0, 0.0, + 0.0, c, s, 0.0, + 0.0, -s, c, 0.0, + 0.0, 0.0, 0.0, 1.0, + ]; +} + +export function roty(rad) { + const s = Math.sin(rad); + const c = Math.cos(rad); + + return [ + c, 0.0, -s, 0.0, + 0.0, 1.0, 0.0, 0.0, + s, 0.0, c, 0.0, + 0.0, 0.0, 0.0, 1.0, + ]; +} + +export function rotz(rad) { + const s = Math.sin(rad); + const c = Math.cos(rad); + + return [ + c, s, 0.0, 0.0, + -s, c, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ]; +} + +export function apply(mat, vect) { + const indices = [0, 1, 2, 3]; + const sum = v => v.reduce((a, b) => a + b); + return [ + sum(indices.map(i => mat[4*i + 0] * vect[i])), + sum(indices.map(i => mat[4*i + 1] * vect[i])), + sum(indices.map(i => mat[4*i + 2] * vect[i])), + sum(indices.map(i => mat[4*i + 3] * vect[i])), + ]; +} + +export function rotxyz(x, y, z) { + return [rotx(x), roty(y), rotz(z)].reduce(product); +} + +export function translation(x, y, z) { + return [ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + x, y, z, 1.0, + ]; +} + +export function product(a, b) { + const c = (i, j) => ( + a[4 * 0 + i] * b[4 * j + 0] + + a[4 * 1 + i] * b[4 * j + 1] + + a[4 * 2 + i] * b[4 * j + 2] + + a[4 * 3 + i] * b[4 * j + 3] + ); + return [ + c(0, 0), c(1, 0), c(2, 0), c(3, 0), + c(0, 1), c(1, 1), c(2, 1), c(3, 1), + c(0, 2), c(1, 2), c(2, 2), c(3, 2), + c(0, 3), c(1, 3), c(2, 3), c(3, 3), + ]; +} + +export function ortho(aspectRatio) { + const out = identity(); + out[0] = 1 / aspectRatio; + return out; +} + +export function perspective(fov, aspectRatio, near, far) { + const f = 1 / Math.tan(fov / 2); + const rangeInv = 1 / (near - far); + + return [ + f / aspectRatio, 0.0, 0.0, 0.0, + 0.0, f, 0.0, 0.0, + 0.0, 0.0, (near + far) * rangeInv, -1, + 0.0, 0.0, near * far * rangeInv * 2, 0, + ]; +}; \ No newline at end of file diff --git a/texture.png b/texture.png new file mode 100644 index 0000000000000000000000000000000000000000..aab2d7b211a08283a85eaa01206410c6e39aac69 GIT binary patch literal 39727 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