├── gpucraft.png ├── resources ├── sky2.jpg ├── clouds.png ├── BlockAtlas.png └── flashlight.png ├── README.md ├── js ├── gpu │ ├── sampler.js │ ├── mesh.js │ ├── cube_mesh.js │ ├── texture.js │ └── texture_util.js ├── voxel_mod.js ├── gpucraft.js ├── scene_object.js ├── block_type.js ├── globals.js ├── chunk_coord.js ├── voxel_map.js ├── math │ ├── random.js │ ├── noise.js │ ├── vector2.js │ ├── vector4.js │ ├── math.js │ ├── vector3.js │ └── matrix4.js ├── lighting.js ├── biome_attributes.js ├── voxel_data.js ├── camera.js ├── transform.js ├── world_data.js ├── chunk_data.js ├── engine.js ├── skybox.js ├── chunk.js ├── voxel_material.js ├── input.js ├── player.js └── world.js ├── style.css ├── index.html └── LICENSE /gpucraft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendan-duncan/gpucraft/HEAD/gpucraft.png -------------------------------------------------------------------------------- /resources/sky2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendan-duncan/gpucraft/HEAD/resources/sky2.jpg -------------------------------------------------------------------------------- /resources/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendan-duncan/gpucraft/HEAD/resources/clouds.png -------------------------------------------------------------------------------- /resources/BlockAtlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendan-duncan/gpucraft/HEAD/resources/BlockAtlas.png -------------------------------------------------------------------------------- /resources/flashlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendan-duncan/gpucraft/HEAD/resources/flashlight.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gpucraft 2 | 3 | Simple WebGPU minecraft clone. 4 | ![GPUCraft Screenshot](gpucraft.png) 5 | 6 | ## Try It Out 7 | 8 | https://brendan-duncan.github.io/gpucraft 9 | -------------------------------------------------------------------------------- /js/gpu/sampler.js: -------------------------------------------------------------------------------- 1 | export class Sampler { 2 | constructor(device, options) { 3 | this.device = device; 4 | this.gpu = device.createSampler(options); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /js/voxel_mod.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "./math/vector3.js"; 2 | 3 | export class VoxelMod { 4 | constructor(p, id) { 5 | this.position = p || new Vector3(); 6 | this.id = id || 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /js/gpucraft.js: -------------------------------------------------------------------------------- 1 | import { Engine } from "./engine.js"; 2 | 3 | function main() { 4 | const canvas = document.getElementById("gpucraft"); 5 | const engine = new Engine(); 6 | engine.run(canvas, { autoResizeCanvas: true }); 7 | } 8 | window.addEventListener('load', main); 9 | -------------------------------------------------------------------------------- /js/scene_object.js: -------------------------------------------------------------------------------- 1 | import { Transform } from "./transform.js"; 2 | 3 | export class SceneObject extends Transform { 4 | constructor(name, parent) { 5 | super(parent); 6 | 7 | this.name = name || ""; 8 | this.active = true; 9 | this.mesh = null; 10 | this.meshData = null; 11 | this.material = null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /js/block_type.js: -------------------------------------------------------------------------------- 1 | export class BlockType { 2 | constructor(options) { 3 | options = options || {}; 4 | this.name = options.name || ""; 5 | this.isSolid = options.isSolid || false; 6 | this.renderNeighborFaces = options.renderNeighborFaces || false; 7 | this.opacity = options.opacity || 0; 8 | 9 | this.textures = options.textures || [0, 0, 0, 0, 0, 0]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0 6 | } 7 | 8 | body { 9 | background-color: #222; 10 | overflow: hidden; 11 | color: #666; 12 | font: 14px "Tahoma"; 13 | width: 100%; 14 | height: 100%; 15 | margin: 0; 16 | padding: 0 17 | } 18 | 19 | #gpucraft { 20 | position: absolute; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | -------------------------------------------------------------------------------- /js/globals.js: -------------------------------------------------------------------------------- 1 | export const Globals = { 2 | engine: null, 3 | world: null, 4 | canvas: null, 5 | input: null, 6 | player: null, 7 | camera: null, 8 | time: 0, 9 | deltaTime: 1.0 / 60.0, 10 | maxDeltaTime: 0, 11 | fixedDeltaTime: 1.0 / 60.0 12 | }; 13 | 14 | if (typeof(performance) != "undefined") { 15 | Globals.now = performance.now.bind(performance); 16 | } else { 17 | Globals.now = Date.now.bind(Date); 18 | } 19 | -------------------------------------------------------------------------------- /js/chunk_coord.js: -------------------------------------------------------------------------------- 1 | export class ChunkCoord extends Int32Array { 2 | constructor(x = 0, z = 0) { 3 | super(2); 4 | this[0] = x; 5 | this[1] = z; 6 | } 7 | 8 | get x() { return this[0]; } 9 | 10 | set x(v) { this[0] = v; } 11 | 12 | get z() { return this[1]; } 13 | 14 | set z(v) { this[1] = v; } 15 | 16 | equals(other) { 17 | return !other ? false : this.x == other.x && this.z == other.z; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /js/voxel_map.js: -------------------------------------------------------------------------------- 1 | import { VoxelData } from "./voxel_data.js"; 2 | 3 | export class VoxelMap extends Uint8Array { 4 | constructor() { 5 | super(VoxelData.ChunkWidth * VoxelData.ChunkHeight * VoxelData.ChunkWidth); 6 | } 7 | 8 | get(x, y, z) { 9 | return this[z * VoxelData.ChunkWidthHeight + y * VoxelData.ChunkWidth + x]; 10 | } 11 | 12 | set(x, y, z, v) { 13 | this[z * VoxelData.ChunkWidthHeight + y * VoxelData.ChunkWidth + x] = v; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GPUCraft 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /js/gpu/mesh.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | export class Mesh { 3 | constructor(device, attributes) { 4 | this.device = device; 5 | this.buffers = {}; 6 | 7 | for (const a in attributes) { 8 | const attr = attributes[a]; 9 | const data = a === "triangles" ? new Uint16Array(attr) : new Float32Array(attr); 10 | const buffer = device.createBuffer({ 11 | size: data.byteLength, 12 | usage: a === "triangles" ? GPUBufferUsage.INDEX : GPUBufferUsage.VERTEX, 13 | mappedAtCreation: true 14 | }); 15 | 16 | if (a === "triangles") { 17 | new Int16Array(buffer.getMappedRange()).set(data); 18 | this.indexCount = data.length; 19 | } else { 20 | new Float32Array(buffer.getMappedRange()).set(data); 21 | } 22 | buffer.unmap(); 23 | 24 | this.buffers[a] = buffer; 25 | } 26 | } 27 | 28 | destroy() { 29 | for (const i in this.buffers) { 30 | this.buffers[i].destroy(); 31 | this.buffers[i] = null; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /js/math/random.js: -------------------------------------------------------------------------------- 1 | 2 | /// Psuedo Random Number Generator using the Xorshift128 algorithm 3 | /// (https://en.wikipedia.org/wiki/Xorshift). 4 | export class Random extends Uint32Array { 5 | constructor(seed) { 6 | super(6); 7 | this.seed = seed || new Date().getTime(); 8 | } 9 | 10 | get seed() { return this[0]; } 11 | 12 | set seed(seed) { 13 | this[0] = seed; 14 | this[1] = this[0] * 1812433253 + 1; 15 | this[2] = this[1] * 1812433253 + 1; 16 | this[3] = this[2] * 1812433253 + 1; 17 | } 18 | 19 | // Generates a random number between [0,0xffffffff] 20 | randomUint32() { 21 | // Xorwow scrambling 22 | let t = this[3]; 23 | const s = this[0]; 24 | this[3] = this[2]; 25 | this[2] = this[1]; 26 | this[1] = s; 27 | t ^= t >> 2; 28 | t ^= t << 1; 29 | t ^= s ^ (s << 4); 30 | this[0] = t; 31 | this[4] += 362437; 32 | this[5] = (t + this[4])|0; 33 | return this[5]; 34 | } 35 | 36 | /// Generates a random number between [0,1] 37 | randomFloat() { 38 | const value = this.randomUint32(); 39 | return (value & 0x007fffff) * (1.0 / 8388607.0); 40 | } 41 | 42 | /// Generates a random number between [0,1) with 53-bit resolution 43 | randomDouble() { 44 | const a = this.randomUint32() >>> 5; 45 | const b = this.randomUint32() >>> 6; 46 | return (a * 67108864 + b) * (1.0 / 9007199254740992); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /js/lighting.js: -------------------------------------------------------------------------------- 1 | import { VoxelData } from "./voxel_data.js"; 2 | import { Globals } from "./globals.js"; 3 | 4 | export class Lighting { 5 | static recalculateNaturalLight(chunkData) { 6 | for (let z = 0; z < VoxelData.ChunkWidth; ++z) { 7 | for (let x = 0; x < VoxelData.ChunkWidth; ++x) { 8 | Lighting.castNaturalLight(chunkData, x, z, VoxelData.ChunkHeight - 1); 9 | } 10 | } 11 | } 12 | 13 | // Propogates natural light straight down from at the given x,z coords starting from the 14 | // startY value. 15 | static castNaturalLight(chunkData, x, z, startY) { 16 | // Little check to make sure we don't try and start from above the world. 17 | if (startY > VoxelData.ChunkHeight - 1) { 18 | startY = VoxelData.ChunkHeight - 1; 19 | } 20 | 21 | // Keep check of whether the light has hit a block with opacity 22 | let obstructed = false; 23 | 24 | for (let y = startY; y > -1; --y) { 25 | const index = chunkData.getVoxelIndex(x, y, z); 26 | const voxelID = chunkData.voxelID[index]; 27 | const properties = Globals.world.blockTypes[voxelID]; 28 | 29 | if (obstructed) { 30 | chunkData.voxelLight[index] = 0; 31 | } else if (properties.opacity > 0) { 32 | chunkData.voxelLight[index] = 0; 33 | obstructed = true; 34 | } else { 35 | chunkData.voxelLight[index] = 15; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /js/biome_attributes.js: -------------------------------------------------------------------------------- 1 | export class BiomeAttributes { 2 | constructor(options) { 3 | options = options || {}; 4 | this.name = options.name || ""; 5 | this.offset = options.offset || 0; 6 | this.scale = options.scale || 1; 7 | 8 | this.terrainHeight = options.terrainHeight || 0; 9 | this.terrainScale = options.terrainScale || 1; 10 | 11 | this.surfaceBlock = options.surfaceBlock || 0; 12 | this.subSurfaceBlock = options.subSurfaceBlock || 0; 13 | 14 | this.majorFloraIndex = options.majorFloraIndex || 0; 15 | this.majorFloraZoneScale = options.majorFloraZoneScale || 1.3; // [0.1, 1] 16 | this.majorFloraPlacementScale = options.majorFloraPlacementScale || 15; // [0.1, 1] 17 | this.majorFloraPlacementThreshold = options.majorFloraPlacementThreshold || 0.8; 18 | this.placeMajorFlora = options.placeMajorFlora !== undefined ? options.placeMajorFlora : true; 19 | 20 | this.maxHeight = options.maxHeight || 12; 21 | this.minHeight = options.minHeight || 5; 22 | 23 | this.lodes = options.lodes || []; 24 | } 25 | } 26 | 27 | export class Lode { 28 | constructor(options) { 29 | options = options || {}; 30 | this.name = options.name || ""; 31 | this.blockID = options.blockID || 0; 32 | this.minHeight = options.minHeight || 0; 33 | this.maxHeight = options.maxHeight || 0; 34 | this.scale = options.scale || 1; 35 | this.threshold = options.threshold || 0; 36 | this.noiseOffset = options.noiseOffset || 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /js/voxel_data.js: -------------------------------------------------------------------------------- 1 | export const VoxelData = { }; 2 | 3 | VoxelData.ChunkWidth = 16; 4 | VoxelData.ChunkHeight = 128; 5 | 6 | VoxelData.WorldSizeInChunks = 40; 7 | 8 | // Lighting values 9 | VoxelData.minLightLevel = 0.1; 10 | VoxelData.maxLightLevel = 0.9; 11 | 12 | VoxelData.unitOfLight = 1 / 16; 13 | 14 | VoxelData.seed = 0; 15 | 16 | VoxelData.WorldCenter = (VoxelData.WorldSizeInChunks * VoxelData.ChunkWidth) / 2; 17 | 18 | VoxelData.WorldSizeInVoxels = VoxelData.WorldSizeInChunks * VoxelData.ChunkWidth; 19 | 20 | VoxelData.TextureWidth = 256; 21 | VoxelData.NormalizedTexturePixelSize = 1 / VoxelData.TextureWidth; 22 | VoxelData.TextureAtlasSizeInBlocks = 16; 23 | VoxelData.NormalizedBlockTextureSize = 1 / VoxelData.TextureAtlasSizeInBlocks; 24 | 25 | VoxelData.HalfWorldSizeInChunks = (VoxelData.WorldSizeInChunks / 2)|0; 26 | VoxelData.ViewDistanceInChunks = 10; 27 | VoxelData.HalfViewDistanceInChunks = (VoxelData.ViewDistanceInChunks / 2)|0; 28 | VoxelData.WorldSizeInBlocks = VoxelData.WorldSizeInChunks * VoxelData.ChunkWidth; 29 | VoxelData.ChunkWidthHeight = VoxelData.ChunkWidth * VoxelData.ChunkHeight; 30 | VoxelData.ChunkWidthWidth = VoxelData.ChunkWidth * VoxelData.ChunkWidth; 31 | 32 | VoxelData.VoxelVerts = [ 33 | [0, 0, 0], 34 | [1, 0, 0], 35 | [1, 1, 0], 36 | [0, 1, 0], 37 | [0, 0, 1], 38 | [1, 0, 1], 39 | [1, 1, 1], 40 | [0, 1, 1], 41 | ]; 42 | 43 | VoxelData.VoxelNormals = [ 44 | [0, 0, -1], // Back 45 | [0, 0, 1], // Front 46 | [0, 1, 0], // Top 47 | [0, -1, 0], // Bottom 48 | [-1, 0, 0], // Left 49 | [1, 0, 0] // Right 50 | ]; 51 | 52 | VoxelData.VoxelTris = [ 53 | [0, 3, 1, 2], // Back Face 54 | [5, 6, 4, 7], // Front Face 55 | [3, 7, 2, 6], // Top Face 56 | [1, 5, 0, 4], // Bottom Face 57 | [4, 7, 0, 3], // Left Face 58 | [1, 2, 5, 6] // Right Face 59 | ]; 60 | 61 | VoxelData.FaceChecks = [ 62 | [0, 0, -1], 63 | [0, 0, 1], 64 | [0, 1, 0], 65 | [0, -1, 0], 66 | [-1, 0, 0], 67 | [1, 0, 0] 68 | ]; 69 | 70 | VoxelData.RevFaceCheckIndex = [ 1, 0, 3, 2, 5, 4 ]; 71 | -------------------------------------------------------------------------------- /js/gpu/cube_mesh.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | export class CubeMesh { 3 | constructor(device) { 4 | this.device = device; 5 | 6 | this.vertexBuffer = device.createBuffer({ 7 | size: cubeVertexArray.byteLength, 8 | usage: GPUBufferUsage.VERTEX, 9 | mappedAtCreation: true 10 | }); 11 | 12 | new Float32Array(this.vertexBuffer.getMappedRange()).set(cubeVertexArray); 13 | this.vertexBuffer.unmap(); 14 | } 15 | } 16 | 17 | CubeMesh.vertexSize = 4 * 10; // Byte size of one cube vertex. 18 | CubeMesh.positionOffset = 0; // Byte offset of cube vertex position attribute. 19 | CubeMesh.colorOffset = 4 * 4; // Byte offset of cube vertex color attribute. 20 | CubeMesh.uvOffset = 4 * 8; // Byte offset of cube uv attribute. 21 | 22 | const cubeVertexArray = new Float32Array([ 23 | // float4 position, float4 color, float2 uv, 24 | 1, -1, 1, 1, 1, 0, 1, 1, 1, 1, 25 | -1, -1, 1, 1, 0, 0, 1, 1, 0, 1, 26 | -1, -1, -1, 1, 0, 0, 0, 1, 0, 0, 27 | 1, -1, -1, 1, 1, 0, 0, 1, 1, 0, 28 | 1, -1, 1, 1, 1, 0, 1, 1, 1, 1, 29 | -1, -1, -1, 1, 0, 0, 0, 1, 0, 0, 30 | 31 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 32 | 1, -1, 1, 1, 1, 0, 1, 1, 0, 1, 33 | 1, -1, -1, 1, 1, 0, 0, 1, 0, 0, 34 | 1, 1, -1, 1, 1, 1, 0, 1, 1, 0, 35 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36 | 1, -1, -1, 1, 1, 0, 0, 1, 0, 0, 37 | 38 | -1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 39 | 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 40 | 1, 1, -1, 1, 1, 1, 0, 1, 0, 0, 41 | -1, 1, -1, 1, 0, 1, 0, 1, 1, 0, 42 | -1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 43 | 1, 1, -1, 1, 1, 1, 0, 1, 0, 0, 44 | 45 | -1, -1, 1, 1, 0, 0, 1, 1, 1, 1, 46 | -1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 47 | -1, 1, -1, 1, 0, 1, 0, 1, 0, 0, 48 | -1, -1, -1, 1, 0, 0, 0, 1, 1, 0, 49 | -1, -1, 1, 1, 0, 0, 1, 1, 1, 1, 50 | -1, 1, -1, 1, 0, 1, 0, 1, 0, 0, 51 | 52 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 53 | -1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 54 | -1, -1, 1, 1, 0, 0, 1, 1, 0, 0, 55 | -1, -1, 1, 1, 0, 0, 1, 1, 0, 0, 56 | 1, -1, 1, 1, 1, 0, 1, 1, 1, 0, 57 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 58 | 59 | 1, -1, -1, 1, 1, 0, 0, 1, 1, 1, 60 | -1, -1, -1, 1, 0, 0, 0, 1, 0, 1, 61 | -1, 1, -1, 1, 0, 1, 0, 1, 0, 0, 62 | 1, 1, -1, 1, 1, 1, 0, 1, 1, 0, 63 | 1, -1, -1, 1, 1, 0, 0, 1, 1, 1, 64 | -1, 1, -1, 1, 0, 1, 0, 1, 0, 0 65 | ]); 66 | -------------------------------------------------------------------------------- /js/gpu/texture.js: -------------------------------------------------------------------------------- 1 | import { TextureUtil } from "./texture_util.js"; 2 | 3 | /* eslint-disable no-undef */ 4 | export class Texture { 5 | constructor(device, options) { 6 | this.device = device; 7 | this.state = 0; 8 | 9 | if (options) { 10 | this.configure(options); 11 | } 12 | } 13 | 14 | static renderBuffer(device, width, height, format) { 15 | return new Texture(device, { 16 | width: width, 17 | height: height, 18 | format: format ?? "rgba8unorm", 19 | usage: GPUTextureUsage.RENDER_ATTACHMENT }); 20 | } 21 | 22 | configure(options) { 23 | this._generateMipmap = !!options.mipmap; 24 | 25 | if (options.url) { 26 | this.url = options.url ?? ""; 27 | this.loadUrl(url, options.callback); 28 | return; 29 | } 30 | 31 | if (options.width && options.height) { 32 | this.create(options); 33 | return; 34 | } 35 | } 36 | 37 | destroy() { 38 | if (!this.gpu) { 39 | return; 40 | } 41 | this.gpu.destroy(); 42 | this.gpu = null; 43 | } 44 | 45 | create(options) { 46 | if (!options.width) return; 47 | if (!options.height) return; 48 | const width = options.width; 49 | const height = options.height; 50 | const format = options.format ?? "rgba8unorm"; 51 | const usage = options.usage ?? GPUTextureUsage.TEXTURE_BINDING; 52 | 53 | this.gpu = this.device.createTexture({ 54 | size: [width, height], 55 | format: format, 56 | usage: usage 57 | }); 58 | 59 | this.state = 1; 60 | if (options.callback) { 61 | options.callback(this); 62 | } 63 | } 64 | 65 | async loadUrl(url, callback) { 66 | const device = this.device; 67 | 68 | const img = document.createElement('img'); 69 | img.src = url; 70 | 71 | await img.decode(); 72 | const imageBitmap = await createImageBitmap(img); 73 | 74 | if (this._generateMipmap) { 75 | this.gpu = TextureUtil.get(this.device).generateMipmap(imageBitmap); 76 | } else { 77 | this.gpu = device.createTexture({ 78 | size: [ imageBitmap.width, imageBitmap.height ], 79 | format: "rgba8unorm", 80 | usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT 81 | }); 82 | 83 | device.queue.copyExternalImageToTexture( 84 | { source: imageBitmap }, 85 | { texture: this.gpu }, 86 | { width: imageBitmap.width, height: imageBitmap.height } 87 | ); 88 | } 89 | 90 | this.state = 1; 91 | if (callback) { 92 | callback(this); 93 | } 94 | } 95 | 96 | createView() { 97 | return this.gpu.createView(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /js/camera.js: -------------------------------------------------------------------------------- 1 | import { Globals } from "./globals.js"; 2 | import { DegreeToRadian } from "./math/math.js"; 3 | import { Matrix4 } from "./math/matrix4.js"; 4 | import { Transform } from "./transform.js"; 5 | 6 | export class Camera extends Transform { 7 | constructor(parent) { 8 | super(parent); 9 | Globals.camera = this; 10 | this._aspect = 1.0; 11 | this._fov = 60.0; 12 | 13 | this.setPosition(0, 1.8, 0); 14 | 15 | this._projectionDirty = true; 16 | this._projection = new Matrix4(0); 17 | 18 | this._worldToViewDirty = true; 19 | this._modelViewProjectionDirty = true; 20 | this._worldToView = new Matrix4(); 21 | this._modelViewProjection = new Matrix4(0); 22 | } 23 | 24 | get fov() { return this._fov; } 25 | 26 | set fov(v) { 27 | if (this._fov === v) return; 28 | this._fov = v; 29 | this.projectionDirty = true; 30 | } 31 | 32 | get aspect() { return this._aspect; } 33 | 34 | set aspect(v) { 35 | if (this._aspect == v) return; 36 | this._aspect = v; 37 | this.projectionDirty = true; 38 | } 39 | 40 | get projectionDiry() { return this._projectionDirty; } 41 | 42 | set projectionDirty(v) { 43 | this._projectionDirty = v; 44 | if (v) { 45 | this._modelViewProjectionDirty = true; 46 | } 47 | } 48 | 49 | get projection() { 50 | if (this._projectionDirty) { 51 | this._projection.setPerspective(this.fov * DegreeToRadian, this.aspect, 0.3, 1000); 52 | this._projectionDirty = false; 53 | } 54 | return this._projection; 55 | } 56 | 57 | get localDirty() { return this._localDirty; } 58 | 59 | set localDirty(v) { 60 | this._localDirty = v; 61 | if (v) { 62 | this._worldToViewDirty = true; 63 | this._modelViewProjectionDirty = true; 64 | this.worldDirty = true; 65 | } 66 | } 67 | 68 | get worldDirty() { return this._worldDirty; } 69 | 70 | set worldDirty(v) { 71 | this._worldDirty = v; 72 | if (v) { 73 | this._worldToViewDirty = true; 74 | this._modelViewProjectionDirty = true; 75 | for (const c of this.children) { 76 | c.worldDirty = true; 77 | } 78 | } 79 | } 80 | 81 | get worldToView() { 82 | if (this._worldToViewDirty) { 83 | const t = this.worldTransform; 84 | Matrix4.invert(t, this._worldToView); 85 | this._worldToViewDirty = false; 86 | } 87 | return this._worldToView; 88 | } 89 | 90 | get modelViewProjection() { 91 | if (this._modelViewProjectionDirty) { 92 | const modelView = this.worldToView; 93 | const projection = this.projection; 94 | Matrix4.multiply(projection, modelView, this._modelViewProjection); 95 | this._modelViewProjectionDirty = false; 96 | } 97 | return this._modelViewProjection; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /js/transform.js: -------------------------------------------------------------------------------- 1 | import { Matrix4 } from "./math/matrix4.js"; 2 | import { Vector3 } from "./math/vector3.js"; 3 | 4 | export class Transform { 5 | constructor(parent) { 6 | this.parent = parent ?? null; 7 | this.children = []; 8 | this._position = new Vector3(0, 0, 0); 9 | this._rotation = new Vector3(0, 0, 0); 10 | 11 | this._localDirty = true; 12 | this._worldDirty = true; 13 | 14 | this._transform = new Matrix4(); 15 | this._worldTransform = new Matrix4(); 16 | 17 | if (parent) { 18 | parent.children.push(this); 19 | } 20 | } 21 | 22 | addChild(c) { 23 | this.children.push(c); 24 | c.parent = this; 25 | c.worldDirty = true; 26 | } 27 | 28 | get position() { return this._position; } 29 | 30 | set position(v) { 31 | this._position.set(v); 32 | this.localDirty = true; 33 | } 34 | 35 | setPosition(x, y, z) { 36 | this._position.setFrom(x, y, z); 37 | this.localDirty = true; 38 | } 39 | 40 | get rotation() { return this._rotation; } 41 | 42 | set rotation(v) { 43 | this._rotation.set(v); 44 | this.localDirty = true; 45 | } 46 | 47 | setRotation(x, y, z) { 48 | this._rotation.setFrom(x, y, z); 49 | this.localDirty = true; 50 | } 51 | 52 | get localDirty() { return this._localDirty; } 53 | 54 | set localDirty(v) { 55 | this._localDirty = v; 56 | if (v) { 57 | this.worldDirty = true; 58 | } 59 | } 60 | 61 | get worldDirty() { return this._worldDirty; } 62 | 63 | set worldDirty(v) { 64 | this._worldDirty = v; 65 | if (v) { 66 | for (const c of this.children) { 67 | c.worldDirty = true; 68 | } 69 | } 70 | } 71 | 72 | get transform() { 73 | if (this._localDirty) { 74 | this._localDirty = false; 75 | this._transform.setTranslate(this.position); 76 | this._transform.rotateEuler(this.rotation); 77 | } 78 | return this._transform; 79 | } 80 | 81 | get worldTransform() { 82 | if (!this.parent) { 83 | return this.transform; 84 | } 85 | 86 | if (this._worldDirty) { 87 | const t = this.transform; 88 | const p = this.parent.worldTransform; 89 | Matrix4.multiply(p, t, this._worldTransform); 90 | this._worldDirty = false; 91 | } 92 | 93 | return this._worldTransform; 94 | } 95 | 96 | getWorldRight(out) { 97 | const t = this.worldTransform; 98 | return t.getColumn3(0, out); 99 | } 100 | 101 | getWorldUp(out) { 102 | const t = this.worldTransform; 103 | return t.getColumn3(1, out); 104 | } 105 | 106 | getWorldForward(out) { 107 | const t = this.worldTransform; 108 | return t.getColumn3(2, out); 109 | } 110 | 111 | getWorldPosition(out) { 112 | const t = this.worldTransform; 113 | return t.getColumn3(3, out); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /js/world_data.js: -------------------------------------------------------------------------------- 1 | import { ChunkData } from "./chunk_data.js"; 2 | import { VoxelData } from "./voxel_data.js"; 3 | 4 | export class WorldData { 5 | constructor(options) { 6 | options = options || {}; 7 | 8 | this.name = options.name || "World"; 9 | this.seed = options.seed || 12345; 10 | 11 | this._chunks = new Map(); 12 | this._modifiedChunks = []; 13 | } 14 | 15 | get chunks() { return this._chunks; } 16 | 17 | get modifiedChunks() { return this._modifiedChunks; } 18 | 19 | addToModifiedChunkList(chunk) { 20 | if (!this._modifiedChunks.includes(chunk)) { 21 | this._modifiedChunks.push(chunk); 22 | } 23 | } 24 | 25 | getChunk(x, z) { 26 | const m = this._chunks.get(z); 27 | if (!m) { 28 | return null; 29 | } 30 | if (!m.has(x)) { 31 | return null; 32 | } 33 | return m.get(x); 34 | } 35 | 36 | setChunk(x, z, chunk) { 37 | if (!this._chunks.has(z)) { 38 | this._chunks.set(z, new Map()); 39 | } 40 | this._chunks.get(z).set(x, chunk); 41 | } 42 | 43 | requestChunk(x, z, create) { 44 | let c = this.getChunk(x, z); 45 | if (!c && create) { 46 | c = this.loadChunk(x, z); 47 | } 48 | return c; 49 | } 50 | 51 | loadChunk(x, z) { 52 | let c = this.getChunk(x ,z); 53 | if (c) { 54 | return c; 55 | } 56 | 57 | c = new ChunkData(x, z); 58 | this.setChunk(x, z, c); 59 | c.populate(); 60 | 61 | return c; 62 | } 63 | 64 | isVoxelInWorld(x, y) { 65 | return y >= 0 && y < VoxelData.ChunkHeight; 66 | } 67 | 68 | setVoxelID(x, y, z, value) { 69 | if (!this.isVoxelInWorld(x, y, z)) { 70 | return; 71 | } 72 | 73 | const cx = (Math.floor(x / VoxelData.ChunkWidth) | 0) * VoxelData.ChunkWidth; 74 | const cz = (Math.floor(z / VoxelData.ChunkWidth) | 0) * VoxelData.ChunkWidth; 75 | 76 | const chunk = this.requestChunk(cx, cz, true); 77 | 78 | chunk.modifyVoxel((x - cx) | 0, y | 0, (z - cz) | 0, value); 79 | } 80 | 81 | getVoxelID(x, y, z) { 82 | if (x && x.constructor === Array) { 83 | y = x[1]; 84 | z = x[2]; 85 | x = x[0]; 86 | } 87 | 88 | if (!this.isVoxelInWorld(x, y, z)) { 89 | return 0; 90 | } 91 | 92 | const cx = Math.floor(x / VoxelData.ChunkWidth) * VoxelData.ChunkWidth; 93 | const cz = Math.floor(z / VoxelData.ChunkWidth) * VoxelData.ChunkWidth; 94 | 95 | const chunk = this.requestChunk(cx, cz, false); 96 | if (!chunk) { 97 | return 0; 98 | } 99 | 100 | return chunk.getVoxelID((x - cx) | 0, y | 0, (z - cz) | 0); 101 | } 102 | 103 | getVoxelLight(x, y, z) { 104 | if (x && x.constructor === Array) { 105 | y = x[1]; 106 | z = x[2]; 107 | x = x[0]; 108 | } 109 | 110 | if (!this.isVoxelInWorld(x, y, z)) { 111 | return 0; 112 | } 113 | 114 | const cx = Math.floor(x / VoxelData.ChunkWidth) * VoxelData.ChunkWidth; 115 | const cz = Math.floor(z / VoxelData.ChunkWidth) * VoxelData.ChunkWidth; 116 | 117 | const chunk = this.requestChunk(cx, cz, false); 118 | if (!chunk) { 119 | return 0; 120 | } 121 | 122 | return chunk.getVoxelLight((x - cx) | 0, y | 0, (z - cz) | 0); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /js/math/noise.js: -------------------------------------------------------------------------------- 1 | export class Noise { 2 | /// 2D perlin noise function. 3 | static perlinNoise2(x, y) { 4 | x = Math.abs(x); 5 | y = Math.abs(y); 6 | 7 | // Find the unit cube that contains the point. 8 | const xi = Math.floor(x); 9 | const yi = Math.floor(y); 10 | 11 | const X = xi & 0xff; 12 | const Y = yi & 0xff; 13 | 14 | x -= xi; 15 | y -= yi; 16 | 17 | // Compute fade curves for each x,y 18 | const u = _fade(Math.min(x, 1)); 19 | const v = _fade(Math.min(y, 1)); 20 | 21 | // Hash coordinates of the 8 cube corners. 22 | const A = _perm[X] + Y; 23 | const AA = _perm[A]; 24 | const AB = _perm[A + 1]; 25 | const B = _perm[X + 1] + Y; 26 | const BA = _perm[B]; 27 | const BB = _perm[B + 1]; 28 | 29 | const result = _lerp(v, _lerp(u, _grad2(_perm[AA], x, y), 30 | _grad2(_perm[BA], x - 1, y)), 31 | _lerp(u, _grad2(_perm[AB], x, y - 1), 32 | _grad2(_perm[BB], x - 1, y - 1))); 33 | 34 | // normalize the results to [0,1] 35 | return (result + 0.69) / (0.793 + 0.69); 36 | } 37 | } 38 | 39 | function _lerp(t, a, b) { 40 | return a + t * (b - a); 41 | } 42 | 43 | function _fade(t) { 44 | return (t * t * t) * ((t * ((t * 6) - 15)) + 10); 45 | } 46 | 47 | function _grad2(hash, x, y) { 48 | let h = hash & 0xf; 49 | let u = h < 8 ? x : y; 50 | let v = h < 4 ? y : (h == 12 || h == 14) ? x : 0; 51 | return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); 52 | } 53 | 54 | const _perm = new Uint8Array([ 55 | 151,160,137,91,90,15, 56 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 57 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 58 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 59 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 60 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 61 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 62 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 63 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 64 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 65 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 66 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 67 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180, 68 | 151,160,137,91,90,15, 69 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 70 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 71 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 72 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 73 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 74 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 75 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 76 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 77 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 78 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 79 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 80 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 81 | ]); 82 | -------------------------------------------------------------------------------- /js/chunk_data.js: -------------------------------------------------------------------------------- 1 | import { VoxelData } from "./voxel_data.js"; 2 | import { Lighting } from "./lighting.js"; 3 | import { Globals } from "./globals.js"; 4 | 5 | export class ChunkData { 6 | constructor() { 7 | if (arguments.length === 1) { 8 | this.position = new Int32Array(arguments[0]); 9 | } else if (arguments.length === 2) { 10 | this.position = new Int32Array(2); 11 | this.position[0] = arguments[0]; 12 | this.position[1] = arguments[1]; 13 | } else { 14 | this.position = new Int32Array(2); 15 | } 16 | 17 | this._chunk = null; 18 | 19 | this.voxelID = new Uint8Array(VoxelData.ChunkWidth * VoxelData.ChunkHeight * VoxelData.ChunkWidth); 20 | this.voxelLight = new Uint8Array(VoxelData.ChunkWidth * VoxelData.ChunkHeight * VoxelData.ChunkWidth); 21 | } 22 | 23 | get chunk() { return this._chunk; } 24 | 25 | set chunk(v) { this._chunk = v; } 26 | 27 | populate() { 28 | const w = VoxelData.ChunkWidth; 29 | const h = VoxelData.ChunkHeight; 30 | for (let y = 0, vi = 0; y < h; ++y) { 31 | for (let z = 0; z < w; ++z) { 32 | for (let x = 0; x < w; ++x, ++vi) { 33 | const gx = x + this.position[0]; 34 | const gy = y; 35 | const gz = z + this.position[1]; 36 | 37 | this.voxelID[vi] = Globals.world.calculateVoxel(gx, gy, gz); 38 | this.voxelLight[vi] = 0; 39 | } 40 | } 41 | } 42 | 43 | Lighting.recalculateNaturalLight(this); 44 | Globals.world.worldData.addToModifiedChunkList(this); 45 | } 46 | 47 | getVoxelProperties(id) { 48 | return Globals.world.blockTypes[id]; 49 | } 50 | 51 | getVoxelIndex(x, y, z) { 52 | return y * VoxelData.ChunkWidthWidth + z * VoxelData.ChunkWidth + x; 53 | } 54 | 55 | getVoxelID(x, y, z) { 56 | return this.voxelID[y * VoxelData.ChunkWidthWidth + z * VoxelData.ChunkWidth + x]; 57 | } 58 | 59 | setVoxelID(x, y, z, v) { 60 | this.voxelID[y * VoxelData.ChunkWidthWidth + z * VoxelData.ChunkWidth + x] = v; 61 | } 62 | 63 | getVoxelLight(x, y, z) { 64 | return this.voxelLight[y * VoxelData.ChunkWidthWidth + z * VoxelData.ChunkWidth + x]; 65 | } 66 | 67 | setVoxelLight(x, y, z, v) { 68 | this.voxelLight[y * VoxelData.ChunkWidthWidth + z * VoxelData.ChunkWidth + x] = v; 69 | } 70 | 71 | modifyVoxel(x, y, z, id) { 72 | const voxel = this.getVoxelID(x, y, z); 73 | if (voxel == id) { 74 | return; 75 | } 76 | 77 | const oldProperties = this.getVoxelProperties(voxel); 78 | const oldOpacity = oldProperties.opacity; 79 | 80 | this.setVoxelID(x, y, z, id); 81 | 82 | const newProperties = this.getVoxelProperties(id); 83 | 84 | // If the opacity values of the voxel have changed and the voxel above is in direct 85 | // sunlight (or is above the world), recast light from that voxel downward. 86 | if (newProperties.opacity != oldOpacity && 87 | (y == VoxelData.ChunkHeight - 1 || this.getVoxelLight(x, y + 1, z) == 15)) { 88 | Lighting.castNaturalLight(this, x, z, y + 1); 89 | } 90 | 91 | // Add this ChunkData to the modified chunks list. 92 | Globals.world.worldData.addToModifiedChunkList(this); 93 | 94 | // If we have a chunk attached, add that for updating. 95 | if (this._chunk) { 96 | Globals.world.addChunkToUpdate(this._chunk); 97 | } 98 | } 99 | 100 | isVoxelInChunk(x, y, z) { 101 | return x >= 0 && x < VoxelData.ChunkWidth && 102 | y >= 0 && y < VoxelData.ChunkHeight && 103 | z >= 0 && z < VoxelData.ChunkWidth; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /js/gpu/texture_util.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | export class TextureUtil { 3 | static get(device) { 4 | let t = TextureUtil._devices.get(device); 5 | if (t) return t; 6 | t = new TextureUtil(device); 7 | TextureUtil._devices.set(device, t); 8 | return t; 9 | } 10 | 11 | constructor(device) { 12 | this.device = device; 13 | 14 | this.mipmapSampler = device.createSampler({ minFilter: 'linear' }); 15 | 16 | const shaderModule = device.createShaderModule({ code: mipmapShader }); 17 | 18 | this.mipmapPipeline = device.createRenderPipeline({ 19 | vertex: { 20 | module: shaderModule, 21 | entryPoint: 'vertexMain' 22 | }, 23 | fragment: { 24 | module: shaderModule, 25 | entryPoint: 'fragmentMain', 26 | targets: [ { format: 'rgba8unorm' } ] 27 | }, 28 | primitive: { 29 | topology: 'triangle-strip', 30 | stripIndexFormat: 'uint32' 31 | }, 32 | layout: "auto" 33 | }); 34 | } 35 | 36 | static getNumMipmapLevels(w, h) { 37 | return Math.floor(Math.log2(Math.max(w, h))) + 1; 38 | } 39 | 40 | generateMipmap(imageBitmap) { 41 | const mipLevelCount = TextureUtil.getNumMipmapLevels(imageBitmap.width, imageBitmap.height); 42 | 43 | const textureSize = { 44 | width: imageBitmap.width, 45 | height: imageBitmap.height, 46 | }; 47 | 48 | const texture = this.device.createTexture({ 49 | size: textureSize, 50 | format: "rgba8unorm", 51 | usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, 52 | mipLevelCount: mipLevelCount 53 | }); 54 | 55 | this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, textureSize); 56 | 57 | const commandEncoder = this.device.createCommandEncoder({}); 58 | 59 | const bindGroupLayout = this.mipmapPipeline.getBindGroupLayout(0); 60 | 61 | for (let i = 1; i < mipLevelCount; ++i) { 62 | const bindGroup = this.device.createBindGroup({ 63 | layout: bindGroupLayout, 64 | entries: [ 65 | { 66 | binding: 0, 67 | resource: this.mipmapSampler, 68 | }, 69 | { 70 | binding: 1, 71 | resource: texture.createView({ 72 | baseMipLevel: i - 1, 73 | mipLevelCount: 1 74 | }) 75 | } 76 | ] 77 | }); 78 | 79 | const passEncoder = commandEncoder.beginRenderPass({ 80 | colorAttachments: [{ 81 | view: texture.createView({ 82 | baseMipLevel: i, 83 | mipLevelCount: 1 84 | }), 85 | loadOp: "load", 86 | storeOp: "store" 87 | }] 88 | }); 89 | 90 | passEncoder.setPipeline(this.mipmapPipeline); 91 | passEncoder.setBindGroup(0, bindGroup); 92 | passEncoder.draw(3, 1, 0, 0); 93 | passEncoder.end(); 94 | 95 | textureSize.width = Math.ceil(textureSize.width / 2); 96 | textureSize.height = Math.ceil(textureSize.height / 2); 97 | } 98 | 99 | this.device.queue.submit([ commandEncoder.finish() ]); 100 | 101 | return texture; 102 | } 103 | } 104 | 105 | TextureUtil._devices = new Map(); 106 | 107 | const mipmapShader = ` 108 | var posTex: array, 3> = array, 3>( 109 | vec4(-1.0, 1.0, 0.0, 0.0), 110 | vec4(3.0, 1.0, 2.0, 0.0), 111 | vec4(-1.0, -3.0, 0.0, 2.0)); 112 | 113 | struct VertexOutput { 114 | @builtin(position) v_position: vec4, 115 | @location(0) v_uv : vec2 116 | }; 117 | 118 | @vertex 119 | fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { 120 | var output: VertexOutput; 121 | 122 | output.v_uv = posTex[vertexIndex].zw; 123 | output.v_position = vec4(posTex[vertexIndex].xy, 0.0, 1.0); 124 | 125 | return output; 126 | } 127 | 128 | @binding(0) @group(0) var imgSampler: sampler; 129 | @binding(1) @group(0) var img: texture_2d; 130 | 131 | @fragment 132 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { 133 | return textureSample(img, imgSampler, input.v_uv); 134 | }`; 135 | -------------------------------------------------------------------------------- /js/engine.js: -------------------------------------------------------------------------------- 1 | import { Camera } from "./camera.js"; 2 | import { Texture } from "./gpu/texture.js"; 3 | import { Skybox } from "./skybox.js"; 4 | import { Globals } from "./globals.js"; 5 | import { Input } from "./input.js"; 6 | import { Player } from "./player.js"; 7 | import { World } from "./world.js"; 8 | import { VoxelMaterial } from "./voxel_material.js"; 9 | 10 | export class Engine { 11 | constructor() { 12 | this.initialized = false; 13 | } 14 | 15 | async run(canvas, options) { 16 | options = options || {}; 17 | 18 | Globals.engine = this; 19 | Globals.canvas = canvas; 20 | Globals.input = new Input(canvas); 21 | 22 | this.canvas = canvas; 23 | this.adapter = await navigator.gpu.requestAdapter(); 24 | this.device = await this.adapter.requestDevice({ requiredFeatures: this.adapter.features, requiredLimits: this.adapter.limits }); 25 | this.context = this.canvas.getContext("webgpu"); 26 | this.preferredFormat = navigator.gpu.getPreferredCanvasFormat(); 27 | 28 | const device = this.device; 29 | 30 | this.context.configure({ 31 | device, 32 | format: this.preferredFormat, 33 | alphaMode: "opaque", 34 | }); 35 | 36 | this.depthTexture = Texture.renderBuffer( 37 | this.device, 38 | this.canvas.width, 39 | this.canvas.height, 40 | "depth24plus-stencil8" 41 | ); 42 | 43 | this.colorAttachment = { 44 | view: undefined, // this is set in the render loop 45 | loadOp: "clear", 46 | clearValue: { r: 0.1, g: 0.1, b: 0.2, a: 1.0 }, 47 | storeOp: "store", 48 | }; 49 | 50 | this.depthAttachment = { 51 | view: this.depthTexture.createView(), 52 | depthLoadOp: "clear", 53 | depthClearValue: 1.0, 54 | depthStoreOp: "store", 55 | stencilLoadOp: "clear", 56 | stencilClearValue: 0, 57 | stencilStoreOp: "store", 58 | }; 59 | 60 | this.renderPassDescriptor = { 61 | colorAttachments: [this.colorAttachment], 62 | depthStencilAttachment: this.depthAttachment, 63 | }; 64 | 65 | this.autoResizeCanvas = !!options.autoResizeCanvas; 66 | if (options.autoResizeCanvas) { 67 | this.updateCanvasResolution(); 68 | } 69 | 70 | this.skybox = new Skybox(this.device, this.preferredFormat); 71 | 72 | this.camera = new Camera(); 73 | 74 | this.player = new Player(this.camera); 75 | this.world = new World(); 76 | 77 | this.voxelMaterial = new VoxelMaterial(this.device, this.preferredFormat); 78 | 79 | this.world.start(); 80 | 81 | this.initialized = true; 82 | 83 | Globals.time = Globals.now() * 0.01; 84 | 85 | const self = this; 86 | const frame = function () { 87 | requestAnimationFrame(frame); 88 | const lastTime = Globals.time; 89 | Globals.time = Globals.now() * 0.01; 90 | Globals.deltaTime = Globals.time - lastTime; 91 | self.update(); 92 | self.render(); 93 | }; 94 | requestAnimationFrame(frame); 95 | } 96 | 97 | updateCanvasResolution() { 98 | const canvas = this.canvas; 99 | const rect = canvas.getBoundingClientRect(); 100 | if (rect.width != canvas.width || rect.height != canvas.height) { 101 | canvas.width = rect.width; 102 | canvas.height = rect.height; 103 | this._onCanvasResize(); 104 | } 105 | } 106 | 107 | update() { 108 | if (this.autoResizeCanvas) { 109 | this.updateCanvasResolution(); 110 | } 111 | 112 | this.camera.aspect = this.canvas.width / this.canvas.height; 113 | 114 | this.world.update(this.device); 115 | this.player.update(); 116 | 117 | this.voxelMaterial.updateCamera(this.camera); 118 | } 119 | 120 | render() { 121 | this.colorAttachment.view = this.context.getCurrentTexture().createView(); 122 | 123 | const commandEncoder = this.device.createCommandEncoder(); 124 | const passEncoder = commandEncoder.beginRenderPass( 125 | this.renderPassDescriptor 126 | ); 127 | 128 | if (this.voxelMaterial.textureLoaded) { 129 | if (Globals.deltaTime > Globals.maxDeltaTime) { 130 | Globals.maxDeltaTime = Globals.deltaTime; 131 | } 132 | } 133 | 134 | const world = this.world; 135 | const numObjects = world.children.length; 136 | let drawCount = 0; 137 | for (let i = 0; i < numObjects; ++i) { 138 | const chunk = world.children[i]; 139 | if (!chunk.active) continue; 140 | 141 | if (chunk.mesh) { 142 | if (!drawCount) { 143 | this.voxelMaterial.startRender(passEncoder); 144 | } 145 | this.voxelMaterial.drawChunk(chunk, passEncoder); 146 | drawCount++; 147 | } 148 | } 149 | 150 | this.skybox.draw(this.camera, passEncoder); 151 | 152 | passEncoder.end(); 153 | this.device.queue.submit([commandEncoder.finish()]); 154 | } 155 | 156 | _onCanvasResize() { 157 | if (!this.depthTexture) return; 158 | 159 | this.depthTexture.destroy(); 160 | this.depthTexture = Texture.renderBuffer( 161 | this.device, 162 | this.canvas.width, 163 | this.canvas.height, 164 | "depth24plus-stencil8" 165 | ); 166 | 167 | this.depthAttachment.view = this.depthTexture.createView(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /js/skybox.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { CubeMesh } from "./gpu/cube_mesh.js"; 3 | import { Texture } from "./gpu/texture.js"; 4 | import { Sampler } from "./gpu/sampler.js"; 5 | import { Matrix4 } from "./math/matrix4.js"; 6 | import { Vector3 } from "./math/vector3.js"; 7 | 8 | export class Skybox { 9 | constructor(device, format) { 10 | this.device = device; 11 | this.format = format; 12 | this.initialized = false; 13 | this.transform = new Matrix4(); 14 | this.cameraPosition = new Vector3(); 15 | 16 | this.initialize(); 17 | } 18 | 19 | async initialize() { 20 | const device = this.device; 21 | this.sampler = new Sampler(device, { 22 | minFilter: "linear", 23 | magFilter: "linear", 24 | }); 25 | this.texture = new Texture(device, { mipmap: true }); 26 | await this.texture.loadUrl("resources/sky2.jpg"); 27 | 28 | this.cube = new CubeMesh(device); 29 | 30 | this.bindGroupLayout = device.createBindGroupLayout({ 31 | entries: [ 32 | { 33 | // Transform 34 | binding: 0, 35 | visibility: GPUShaderStage.VERTEX, 36 | buffer: { type: "uniform" }, 37 | }, 38 | { 39 | // Sampler 40 | binding: 1, 41 | visibility: GPUShaderStage.FRAGMENT, 42 | sampler: { type: "filtering" }, 43 | }, 44 | { 45 | // Texture view 46 | binding: 2, 47 | visibility: GPUShaderStage.FRAGMENT, 48 | texture: { sampleType: "float" }, 49 | }, 50 | ], 51 | }); 52 | 53 | this.pipelineLayout = device.createPipelineLayout({ 54 | bindGroupLayouts: [this.bindGroupLayout], 55 | }); 56 | 57 | this.shaderModule = device.createShaderModule({ code: skyShader }); 58 | 59 | this.pipeline = device.createRenderPipeline({ 60 | layout: this.pipelineLayout, 61 | vertex: { 62 | module: this.shaderModule, 63 | entryPoint: "vertexMain", 64 | buffers: [ 65 | { 66 | arrayStride: CubeMesh.vertexSize, 67 | attributes: [ 68 | { 69 | // position 70 | shaderLocation: 0, 71 | offset: CubeMesh.positionOffset, 72 | format: "float32x4", 73 | }, 74 | ], 75 | }, 76 | ], 77 | }, 78 | fragment: { 79 | module: this.shaderModule, 80 | entryPoint: "fragmentMain", 81 | targets: [ 82 | { 83 | format: this.format, 84 | }, 85 | ], 86 | }, 87 | primitive: { 88 | topology: "triangle-list", 89 | cullMode: "none", 90 | }, 91 | depthStencil: { 92 | depthWriteEnabled: false, 93 | depthCompare: "less", 94 | format: "depth24plus-stencil8", 95 | }, 96 | }); 97 | 98 | const uniformBufferSize = 4 * 16; // 4x4 matrix 99 | this.uniformBuffer = device.createBuffer({ 100 | size: uniformBufferSize, 101 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 102 | }); 103 | 104 | this.uniformBindGroup = device.createBindGroup({ 105 | layout: this.bindGroupLayout, 106 | entries: [ 107 | { 108 | binding: 0, 109 | resource: { buffer: this.uniformBuffer }, 110 | }, 111 | { 112 | binding: 1, 113 | resource: this.sampler.gpu, 114 | }, 115 | { 116 | binding: 2, 117 | resource: this.texture.createView(), 118 | }, 119 | ], 120 | }); 121 | 122 | this.initialized = true; 123 | } 124 | 125 | draw(camera, passEncoder) { 126 | if (!this.initialized) { 127 | return; 128 | } 129 | 130 | const modelViewProjection = camera.modelViewProjection; 131 | this.transform.setTranslate(camera.getWorldPosition(this.cameraPosition)); 132 | this.transform.scale(100, 100, 100); 133 | Matrix4.multiply(modelViewProjection, this.transform, this.transform); 134 | 135 | this.device.queue.writeBuffer( 136 | this.uniformBuffer, 137 | 0, 138 | this.transform.buffer, 139 | this.transform.byteOffset, 140 | this.transform.byteLength 141 | ); 142 | 143 | passEncoder.setPipeline(this.pipeline); 144 | passEncoder.setBindGroup(0, this.uniformBindGroup); 145 | passEncoder.setVertexBuffer(0, this.cube.vertexBuffer); 146 | passEncoder.draw(36, 1, 0, 0); 147 | 148 | return; 149 | } 150 | } 151 | 152 | const skyShader = ` 153 | struct Uniforms { 154 | u_modelViewProjection: mat4x4 155 | }; 156 | 157 | @binding(0) @group(0) var uniforms : Uniforms; 158 | 159 | struct VertexInput { 160 | @location(0) position: vec4 161 | }; 162 | 163 | struct VertexOutput { 164 | @builtin(position) Position: vec4, 165 | @location(0) v_position: vec4 166 | }; 167 | 168 | @vertex 169 | fn vertexMain(input: VertexInput) -> VertexOutput { 170 | var output: VertexOutput; 171 | output.Position = uniforms.u_modelViewProjection * input.position; 172 | output.v_position = input.position; 173 | return output; 174 | } 175 | 176 | @binding(1) @group(0) var skySampler: sampler; 177 | @binding(2) @group(0) var skyTexture: texture_2d; 178 | 179 | fn polarToCartesian(V: vec3) -> vec2 { 180 | return vec2(0.5 - (atan2(V.z, V.x) / -6.28318531), 181 | 1.0 - (asin(V.y) / 1.57079633 * 0.5 + 0.5)); 182 | } 183 | 184 | @fragment 185 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { 186 | var outColor = textureSample(skyTexture, skySampler, polarToCartesian(normalize(input.v_position.xyz))); 187 | return outColor; 188 | }`; 189 | -------------------------------------------------------------------------------- /js/math/vector2.js: -------------------------------------------------------------------------------- 1 | import { isNumber } from "./math.js"; 2 | 3 | /** 4 | * A 2 dimensional vector. 5 | * @category Math 6 | */ 7 | export class Vector2 extends Float32Array { 8 | constructor() { 9 | if (arguments.length) { 10 | if (arguments.length == 1 && !arguments[0]) { 11 | super(2); 12 | } else if (arguments.length == 1 && isNumber(arguments[0])) { 13 | super(2); 14 | const x = arguments[0]; 15 | this[0] = x; 16 | this[1] = x; 17 | } else if (arguments.length == 2 && isNumber(arguments[0])) { 18 | super(2); 19 | const x = arguments[0]; 20 | const y = arguments[1]; 21 | this[0] = x; 22 | this[1] = y; 23 | } else { 24 | super(...arguments); 25 | } 26 | } else { 27 | super(2); 28 | } 29 | } 30 | 31 | clone() { 32 | return new Vector2(this); 33 | } 34 | 35 | setFrom(x, y) { 36 | this[0] = x; 37 | this[1] = y; 38 | return this; 39 | } 40 | 41 | setZero() { 42 | this[0] = 0; 43 | this[1] = 0; 44 | return this; 45 | } 46 | 47 | toArray() { return [this[0], this[1]]; } 48 | 49 | toString() { return `[${this.x}, ${this.y}]`; } 50 | 51 | get x() { return this[0]; } 52 | 53 | set x(v) { this[0] = v; } 54 | 55 | get y() { return this[1]; } 56 | 57 | set y(v) { this[1] = v; } 58 | 59 | map() { 60 | switch (arguments.length) { 61 | case 2: 62 | return new Vector2(this[arguments[0]], this[arguments[1]]); 63 | } 64 | return null; 65 | } 66 | 67 | sum() { 68 | return this[0] + this[1]; 69 | } 70 | 71 | getLength() { 72 | return Math.sqrt(this[0] * this[0] + this[1] * this[1]); 73 | } 74 | 75 | getLengthSquared() { 76 | return this[0] * this[0] + this[1] * this[1]; 77 | } 78 | 79 | normalize(out) { 80 | out = out || this; 81 | const l = this.getLength(); 82 | if (!l) { 83 | if (out !== this) { 84 | out.set(this); 85 | } 86 | return out; 87 | } 88 | out[0] = this[0] / l; 89 | out[1] = this[1] / l; 90 | return out; 91 | } 92 | 93 | negate(out) { 94 | out = out || this; 95 | out[0] = -this[0]; 96 | out[1] = -this[1]; 97 | return out; 98 | } 99 | 100 | abs(out) { 101 | out = out || this; 102 | out[0] = Math.abs(this[0]); 103 | out[1] = Math.abs(this[1]); 104 | return out; 105 | } 106 | 107 | add(b, out) { 108 | out = out || this; 109 | out[0] = this[0] + b[0]; 110 | out[1] = this[1] + b[1]; 111 | return out; 112 | } 113 | 114 | subtract(b, out) { 115 | out = out || this; 116 | out[0] = this[0] - b[0]; 117 | out[1] = this[1] - b[1]; 118 | return out; 119 | } 120 | 121 | multiply(b, out) { 122 | out = out || this; 123 | out[0] = this[0] * b[0]; 124 | out[1] = this[1] * b[1]; 125 | return out; 126 | } 127 | 128 | divide(b, out) { 129 | out = out || this; 130 | out[0] = b[0] ? this[0] / b[0] : 0; 131 | out[1] = b[1] ? this[1] / b[1] : 0; 132 | return out; 133 | } 134 | 135 | scale(s, out) { 136 | out = out || this; 137 | out[0] = this[0] * s; 138 | out[1] = this[1] * s; 139 | return out; 140 | } 141 | 142 | static negated(a, out) { 143 | out = out || new Vector2(); 144 | out.setFrom(-a[0], -a[1]); 145 | return out; 146 | } 147 | 148 | static abs(a, out) { 149 | out = out || new Vector2(); 150 | out.setFrom(Math.abs(a[0]), Math.abs(a[1])); 151 | return out; 152 | } 153 | 154 | static length(v) { return v.getLength(); } 155 | 156 | static lengthSquared(v) { return v.getLengthSquared(); } 157 | 158 | static distanceSquared(a, b) { 159 | const dx = b[0] - a[0]; 160 | const dy = b[1] - a[1]; 161 | return dx * dx + dy * dy; 162 | } 163 | 164 | static distance(a, b) { 165 | const dx = b[0] - a[0]; 166 | const dy = b[1] - a[1]; 167 | return Math.sqrt(dx * dx + dy * dy); 168 | } 169 | 170 | static normalize(a, out) { 171 | out = out || new Vector2(); 172 | const l = Vector2.getLength(a); 173 | if (!l) { 174 | out.set(a); 175 | return; 176 | } 177 | out[0] = a[0] / l; 178 | out[1] = a[1] / l; 179 | return out; 180 | } 181 | 182 | static dot(a, b) { 183 | return a[0] * b[0] + a[1] * b[1]; 184 | } 185 | 186 | static cross(a, b, out) { 187 | out = out || new Vector2(); 188 | const z = a[0] * b[1] - a[1] * b[0]; 189 | out[0] = out[1] = 0; 190 | out[2] = z; 191 | return out; 192 | } 193 | 194 | static add(a, b, out) { 195 | out = out || new Vector2(); 196 | out[0] = a[0] + b[0]; 197 | out[1] = a[1] + b[1]; 198 | return out; 199 | } 200 | 201 | static subtract(a, b, out) { 202 | out = out || new Vector2(); 203 | out[0] = a[0] - b[0]; 204 | out[1] = a[1] - b[1]; 205 | return out; 206 | } 207 | 208 | static multiply(a, b, out) { 209 | out = out || new Vector2(); 210 | out[0] = a[0] * b[0]; 211 | out[1] = a[1] * b[1]; 212 | return out; 213 | } 214 | 215 | static divide(a, b, out) { 216 | out = out || new Vector2(); 217 | out[0] = b[0] ? a[0] / b[0] : 0; 218 | out[1] = b[1] ? a[1] / b[1] : 0; 219 | return out; 220 | } 221 | 222 | static scale(a, s, out) { 223 | out = out || new Vector2(); 224 | out[0] = a[0] * s; 225 | out[1] = a[1] * s; 226 | return out; 227 | } 228 | 229 | static scaleAndAdd(a, b, s, out) { 230 | out = out || new Vector2(); 231 | out[0] = a[0] + b[0] * s; 232 | out[1] = a[1] + b[1] * s; 233 | return out; 234 | } 235 | 236 | static lerp(a, b, t, out) { 237 | out = out || new Vector2(); 238 | const ax = a[0]; 239 | const ay = a[1]; 240 | out[0] = ax + t * (b[0] - ax); 241 | out[1] = ay + t * (b[1] - ay); 242 | return out; 243 | } 244 | } 245 | 246 | Vector2.Zero = new Vector2(); 247 | Vector2.One = new Vector2(1, 1); 248 | -------------------------------------------------------------------------------- /js/chunk.js: -------------------------------------------------------------------------------- 1 | import { VoxelData } from "./voxel_data.js"; 2 | import { SceneObject } from "./scene_object.js"; 3 | import { Mesh } from "./gpu/mesh.js"; 4 | 5 | export class Chunk extends SceneObject { 6 | constructor(coord, world) { 7 | super(`${coord.x},${coord.z}`, world); 8 | 9 | this.coord = coord; 10 | this.world = world; 11 | 12 | this.vertexIndex = 0; 13 | this.vertices = []; 14 | this.triangles = []; 15 | this.transparentTriangles = []; 16 | this.uvs = []; 17 | this.normals = []; 18 | this.colors = []; 19 | 20 | this.meshData = { 21 | points: this.vertices, 22 | normals: this.normals, 23 | colors: this.colors, 24 | uvs: this.uvs, 25 | triangles: this.triangles 26 | }; 27 | 28 | const x = coord.x * VoxelData.ChunkWidth; 29 | const z = coord.z * VoxelData.ChunkWidth; 30 | this.setPosition(x, 0, z); 31 | 32 | this.chunkData = world.worldData.requestChunk(x, z, true); 33 | this.chunkData.chunk = this; 34 | 35 | world.addChunkToUpdate(this); 36 | } 37 | 38 | updateChunk() { 39 | this.clearMeshData(); 40 | 41 | for (let y = 0; y < VoxelData.ChunkHeight; ++y) { 42 | for (let x = 0; x < VoxelData.ChunkWidth; ++x) { 43 | for (let z = 0; z < VoxelData.ChunkWidth; ++z) { 44 | const voxel = this.chunkData.getVoxelID(x, y, z); 45 | if (this.world.blockTypes[voxel].isSolid) { 46 | this.updateMeshData(x, y, z); 47 | } 48 | } 49 | } 50 | } 51 | 52 | this.world.chunksToDraw.push(this); 53 | if (this.mesh) { 54 | this.mesh.dirty = true; 55 | } 56 | } 57 | 58 | clearMeshData() { 59 | this.vertexIndex = 0; 60 | this.vertices.length = 0; 61 | this.triangles.length = 0; 62 | this.transparentTriangles.length = 0; 63 | this.uvs.length = 0; 64 | this.colors.length = 0; 65 | this.normals.length = 0; 66 | } 67 | 68 | editVoxel(x, y, z, newID) { 69 | const xCheck = Math.floor(x) - Math.floor(this.position[0]); 70 | const yCheck = Math.floor(y); 71 | const zCheck = Math.floor(z) - Math.floor(this.position[2]); 72 | 73 | this.chunkData.modifyVoxel(xCheck, yCheck, zCheck, newID); 74 | 75 | this.updateSurroundingVoxels(xCheck, yCheck, zCheck); 76 | } 77 | 78 | updateSurroundingVoxels(x, y, z) { 79 | const pos = this.position; 80 | for (let p = 0; p < 6; ++p) { 81 | const cx = x + VoxelData.FaceChecks[p][0]; 82 | const cy = y + VoxelData.FaceChecks[p][1]; 83 | const cz = z + VoxelData.FaceChecks[p][2]; 84 | 85 | if (!this.chunkData.isVoxelInChunk(cx, cy, cz)) { 86 | this.world.addChunkToUpdate(this.world.getChunkFromPosition(cx + pos[0], 87 | cy + pos[1], cz + pos[2]), true); 88 | } 89 | } 90 | } 91 | 92 | getVoxelIDFromGlobalPosition(x, y, z) { 93 | const pos = this.position; 94 | const xCheck = Math.floor(x) - Math.floor(pos[0]); 95 | const yCheck = Math.floor(y); 96 | const zCheck = Math.floor(z) - Math.floor(pos[2]); 97 | return this.chunkData.getVoxelID(xCheck, yCheck, zCheck); 98 | } 99 | 100 | getVoxelLightFromGlobalPosition(x, y, z) { 101 | const pos = this.position; 102 | const xCheck = Math.floor(x) - Math.floor(pos[0]); 103 | const yCheck = Math.floor(y); 104 | const zCheck = Math.floor(z) - Math.floor(pos[2]); 105 | return this.chunkData.getVoxelLight(xCheck, yCheck, zCheck); 106 | } 107 | 108 | updateMeshData(x, y, z) { 109 | const xi = Math.floor(x); 110 | const yi = Math.floor(y); 111 | const zi = Math.floor(z); 112 | 113 | const voxelID = this.chunkData.getVoxelID(xi, yi, zi); 114 | const properties = this.world.blockTypes[voxelID]; 115 | 116 | const pos = this.position; 117 | const px = Math.floor(pos[0]); 118 | const py = Math.floor(pos[1]); 119 | const pz = Math.floor(pos[2]); 120 | 121 | const world = this.world; 122 | const worldData = world.worldData; 123 | 124 | for (let p = 0; p < 6; ++p) { 125 | const nx = px + xi + VoxelData.FaceChecks[p][0]; 126 | const ny = py + yi + VoxelData.FaceChecks[p][1]; 127 | const nz = pz + zi + VoxelData.FaceChecks[p][2]; 128 | 129 | const neighborID = worldData.getVoxelID(nx, ny, nz); 130 | const neighborProperties = world.blockTypes[neighborID]; 131 | 132 | //const neighbor = voxel.neighbors.get(p); 133 | const tri = VoxelData.VoxelTris[p]; 134 | 135 | if (world.blockTypes[neighborID].renderNeighborFaces) { 136 | this.vertices.push(x + VoxelData.VoxelVerts[tri[0]][0], 137 | y + VoxelData.VoxelVerts[tri[0]][1], 138 | z + VoxelData.VoxelVerts[tri[0]][2], 139 | x + VoxelData.VoxelVerts[tri[1]][0], 140 | y + VoxelData.VoxelVerts[tri[1]][1], 141 | z + VoxelData.VoxelVerts[tri[1]][2], 142 | x + VoxelData.VoxelVerts[tri[2]][0], 143 | y + VoxelData.VoxelVerts[tri[2]][1], 144 | z + VoxelData.VoxelVerts[tri[2]][2], 145 | x + VoxelData.VoxelVerts[tri[3]][0], 146 | y + VoxelData.VoxelVerts[tri[3]][1], 147 | z + VoxelData.VoxelVerts[tri[3]][2]); 148 | 149 | this.normals.push(VoxelData.VoxelNormals[p][0], 150 | VoxelData.VoxelNormals[p][1], 151 | VoxelData.VoxelNormals[p][2], 152 | VoxelData.VoxelNormals[p][0], 153 | VoxelData.VoxelNormals[p][1], 154 | VoxelData.VoxelNormals[p][2], 155 | VoxelData.VoxelNormals[p][0], 156 | VoxelData.VoxelNormals[p][1], 157 | VoxelData.VoxelNormals[p][2], 158 | VoxelData.VoxelNormals[p][0], 159 | VoxelData.VoxelNormals[p][1], 160 | VoxelData.VoxelNormals[p][2]); 161 | 162 | this.addTexture(properties.textures[p]); 163 | 164 | const lightLevel = worldData.getVoxelLight(nx, ny, nz) * VoxelData.unitOfLight; 165 | 166 | this.colors.push( 167 | 0, 0, 0, lightLevel, 168 | 0, 0, 0, lightLevel, 169 | 0, 0, 0, lightLevel, 170 | 0, 0, 0, lightLevel); 171 | 172 | if (!neighborProperties.renderNeighborFaces) { 173 | this.triangles.push(this.vertexIndex, 174 | this.vertexIndex + 1, 175 | this.vertexIndex + 2, 176 | this.vertexIndex + 2, 177 | this.vertexIndex + 1, 178 | this.vertexIndex + 3); 179 | } else { 180 | this.triangles.push(this.vertexIndex, 181 | this.vertexIndex + 1, 182 | this.vertexIndex + 2, 183 | this.vertexIndex + 2, 184 | this.vertexIndex + 1, 185 | this.vertexIndex + 3); 186 | } 187 | 188 | this.vertexIndex += 4; 189 | } 190 | } 191 | } 192 | 193 | addTexture(textureId) { 194 | let y = (textureId / VoxelData.TextureAtlasSizeInBlocks)|0; 195 | let x = textureId - (y * VoxelData.TextureAtlasSizeInBlocks); 196 | 197 | x *= VoxelData.NormalizedBlockTextureSize; 198 | y *= VoxelData.NormalizedBlockTextureSize; 199 | 200 | y = 1 - y - VoxelData.NormalizedBlockTextureSize; 201 | 202 | const ps = VoxelData.NormalizedTexturePixelSize * 2; 203 | 204 | x += ps; 205 | y += ps; 206 | const w = VoxelData.NormalizedBlockTextureSize - (ps * 2); 207 | 208 | this.uvs.push(x, 1 - y, 209 | x, 1 - (y + w), 210 | x + w, 1 - y, 211 | x + w, 1 - (y + w)); 212 | } 213 | 214 | createMesh(device) { 215 | if (this.mesh) { 216 | if (!this.mesh.dirty) { 217 | return; 218 | } 219 | this.mesh.destroy(); 220 | } 221 | this.mesh = new Mesh(device, this.meshData); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /js/voxel_material.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { Texture } from "./gpu/texture.js"; 3 | import { Sampler } from "./gpu/sampler.js"; 4 | 5 | export class VoxelMaterial { 6 | constructor(device, format) { 7 | this.device = device; 8 | this.format = format; 9 | 10 | this.sampler = new Sampler(device, { 11 | minFilter: "nearest", 12 | magFilter: "nearest", 13 | mipmapFilter: "linear", 14 | }); 15 | 16 | this.texture = new Texture(device, { mipmap: true }); 17 | 18 | const self = this; 19 | this.textureLoaded = false; 20 | this.texture.loadUrl("resources/BlockAtlas.png").then(() => { 21 | self.textureLoaded = true; 22 | }); 23 | 24 | this.bindGroupLayout = device.createBindGroupLayout({ 25 | entries: [ 26 | { 27 | // ViewUniforms 28 | binding: 0, 29 | visibility: GPUShaderStage.VERTEX, 30 | buffer: { type: "uniform" }, 31 | }, 32 | { 33 | // ModelUniforms 34 | binding: 1, 35 | visibility: GPUShaderStage.VERTEX, 36 | buffer: { type: "uniform" }, 37 | }, 38 | { 39 | // Sampler 40 | binding: 2, 41 | visibility: GPUShaderStage.FRAGMENT, 42 | sampler: { type: "filtering" }, 43 | }, 44 | { 45 | // Texture view 46 | binding: 3, 47 | visibility: GPUShaderStage.FRAGMENT, 48 | texture: { sampleType: "float" }, 49 | }, 50 | ], 51 | }); 52 | 53 | this.pipelineLayout = device.createPipelineLayout({ 54 | bindGroupLayouts: [this.bindGroupLayout], 55 | }); 56 | 57 | this.shaderModule = device.createShaderModule({ code: shaderSource }); 58 | 59 | this.pipeline = device.createRenderPipeline({ 60 | layout: this.pipelineLayout, 61 | vertex: { 62 | module: this.shaderModule, 63 | entryPoint: "vertexMain", 64 | buffers: [ 65 | { 66 | // Position 67 | arrayStride: 3 * 4, 68 | attributes: [ 69 | { 70 | shaderLocation: 0, 71 | offset: 0, 72 | format: "float32x3", 73 | }, 74 | ], 75 | }, 76 | { 77 | // Normal 78 | arrayStride: 3 * 4, 79 | attributes: [ 80 | { 81 | shaderLocation: 1, 82 | offset: 0, 83 | format: "float32x3", 84 | }, 85 | ], 86 | }, 87 | { 88 | // Color 89 | arrayStride: 4 * 4, 90 | attributes: [ 91 | { 92 | shaderLocation: 2, 93 | offset: 0, 94 | format: "float32x4", 95 | }, 96 | ], 97 | }, 98 | { 99 | // UV 100 | arrayStride: 2 * 4, 101 | attributes: [ 102 | { 103 | shaderLocation: 3, 104 | offset: 0, 105 | format: "float32x2", 106 | }, 107 | ], 108 | }, 109 | ], 110 | }, 111 | fragment: { 112 | module: this.shaderModule, 113 | entryPoint: "fragmentMain", 114 | targets: [{ format: this.format }], 115 | }, 116 | primitive: { 117 | topology: "triangle-list", 118 | cullMode: "none", 119 | }, 120 | depthStencil: { 121 | depthWriteEnabled: true, 122 | depthCompare: "less", 123 | format: "depth24plus-stencil8", 124 | }, 125 | }); 126 | 127 | this.viewUniformBuffer = device.createBuffer({ 128 | size: 4 * 16, 129 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 130 | }); 131 | 132 | this._bindGroups = []; 133 | this._modelBuffers = []; 134 | this._currentGroup = 0; 135 | } 136 | 137 | updateCamera(camera) { 138 | const modelViewProjection = camera.modelViewProjection; 139 | 140 | this.device.queue.writeBuffer( 141 | this.viewUniformBuffer, 142 | 0, 143 | modelViewProjection.buffer, 144 | modelViewProjection.byteOffset, 145 | modelViewProjection.byteLength 146 | ); 147 | } 148 | 149 | startRender(passEncoder) { 150 | passEncoder.setPipeline(this.pipeline); 151 | this._chunkIndex = 0; 152 | } 153 | 154 | drawChunk(chunk, passEncoder) { 155 | if (!this.textureLoaded) { 156 | return; 157 | } 158 | 159 | const chunkIndex = this._chunkIndex; 160 | const modelBuffer = this._getModelBuffer(chunkIndex); 161 | const bindGroup = this._getBindGroup(chunkIndex); 162 | 163 | const mesh = chunk.mesh; 164 | const transform = chunk.worldTransform; 165 | 166 | this.device.queue.writeBuffer( 167 | modelBuffer, 168 | 0, 169 | transform.buffer, 170 | transform.byteOffset, 171 | transform.byteLength 172 | ); 173 | 174 | passEncoder.setBindGroup(0, bindGroup); 175 | passEncoder.setVertexBuffer(0, mesh.buffers.points); 176 | passEncoder.setVertexBuffer(1, mesh.buffers.normals); 177 | passEncoder.setVertexBuffer(2, mesh.buffers.colors); 178 | passEncoder.setVertexBuffer(3, mesh.buffers.uvs); 179 | passEncoder.setIndexBuffer(mesh.buffers.triangles, "uint16"); 180 | passEncoder.drawIndexed(mesh.indexCount); 181 | 182 | this._chunkIndex++; 183 | } 184 | 185 | _getModelBuffer(index) { 186 | if (index < this._modelBuffers.length) { 187 | return this._modelBuffers[index]; 188 | } 189 | 190 | const buffer = this.device.createBuffer({ 191 | size: 4 * 16, 192 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 193 | }); 194 | this._modelBuffers.push(buffer); 195 | 196 | return buffer; 197 | } 198 | 199 | _getBindGroup(index) { 200 | if (index < this._bindGroups.length) { 201 | return this._bindGroups[index]; 202 | } 203 | 204 | const modelBuffer = this._getModelBuffer(index); 205 | 206 | const bindGroup = this.device.createBindGroup({ 207 | layout: this.bindGroupLayout, 208 | entries: [ 209 | { 210 | binding: 0, 211 | resource: { buffer: this.viewUniformBuffer }, 212 | }, 213 | { 214 | binding: 1, 215 | resource: { buffer: modelBuffer }, 216 | }, 217 | { 218 | binding: 2, 219 | resource: this.sampler.gpu, 220 | }, 221 | { 222 | binding: 3, 223 | resource: this.texture.createView(), 224 | }, 225 | ], 226 | }); 227 | 228 | this._bindGroups.push(bindGroup); 229 | 230 | return bindGroup; 231 | } 232 | } 233 | 234 | const shaderSource = ` 235 | struct ViewUniforms { 236 | viewProjection: mat4x4 237 | }; 238 | 239 | struct ModelUniforms { 240 | model: mat4x4 241 | }; 242 | 243 | @binding(0) @group(0) var viewUniforms: ViewUniforms; 244 | @binding(1) @group(0) var modelUniforms: ModelUniforms; 245 | 246 | struct VertexInput { 247 | @location(0) a_position: vec3, 248 | @location(1) a_normal: vec3, 249 | @location(2) a_color: vec4, 250 | @location(3) a_uv: vec2 251 | }; 252 | 253 | struct VertexOutput { 254 | @builtin(position) Position: vec4, 255 | @location(0) v_position: vec4, 256 | @location(1) v_normal: vec3, 257 | @location(2) v_color: vec4, 258 | @location(3) v_uv: vec2 259 | }; 260 | 261 | @vertex 262 | fn vertexMain(input: VertexInput) -> VertexOutput { 263 | var output: VertexOutput; 264 | output.Position = viewUniforms.viewProjection * modelUniforms.model * vec4(input.a_position, 1.0); 265 | output.v_position = output.Position; 266 | output.v_normal = input.a_normal; 267 | output.v_color = input.a_color; 268 | output.v_uv = input.a_uv; 269 | return output; 270 | } 271 | 272 | @binding(2) @group(0) var u_sampler: sampler; 273 | @binding(3) @group(0) var u_texture: texture_2d; 274 | 275 | @fragment 276 | fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { 277 | let GlobalLightLevel: f32 = 0.8; 278 | let minGlobalLightLevel: f32 = 0.2; 279 | let maxGlobalLightLevel: f32 = 0.9; 280 | 281 | var shade: f32 = (maxGlobalLightLevel - minGlobalLightLevel) * GlobalLightLevel + minGlobalLightLevel; 282 | shade = shade * input.v_color.a; 283 | 284 | shade = clamp(shade, minGlobalLightLevel, maxGlobalLightLevel); 285 | 286 | var light: vec4 = vec4(shade, shade, shade, 1.0); 287 | 288 | var outColor = textureSample(u_texture, u_sampler, input.v_uv) * light; 289 | 290 | return outColor; 291 | }`; 292 | -------------------------------------------------------------------------------- /js/math/vector4.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "./vector2.js"; 2 | import { Vector3 } from "./vector3.js"; 3 | import { isNumber } from "./math.js"; 4 | 5 | /** 6 | * A 4 dimensional vector. 7 | * @category Math 8 | */ 9 | export class Vector4 extends Float32Array { 10 | constructor() { 11 | if (arguments.length) { 12 | if (arguments.length == 1 && !arguments[0]) { 13 | super(4); 14 | } else if (arguments.length == 1 && isNumber(arguments[0])) { 15 | super(4); 16 | const x = arguments[0]; 17 | this[0] = x; 18 | this[1] = x; 19 | this[2] = x; 20 | this[3] = x; 21 | } else if (arguments.length == 4 && isNumber(arguments[0])) { 22 | super(4); 23 | const x = arguments[0]; 24 | const y = arguments[1]; 25 | const z = arguments[2]; 26 | const w = arguments[3]; 27 | this[0] = x; 28 | this[1] = y; 29 | this[2] = z; 30 | this[3] = w; 31 | } else if (arguments.length == 3 && isNumber(arguments[0])) { 32 | super(4); 33 | const x = arguments[0]; 34 | const y = arguments[1]; 35 | const z = arguments[2]; 36 | this[0] = x; 37 | this[1] = y; 38 | this[2] = z; 39 | this[3] = 1; 40 | } else { 41 | super(...arguments); 42 | } 43 | } else { 44 | super(4); 45 | } 46 | } 47 | 48 | clone() { 49 | return new Vector4(this); 50 | } 51 | 52 | setFrom(x, y, z, w) { 53 | this[0] = x; 54 | this[1] = y; 55 | this[2] = z; 56 | this[3] = w; 57 | return this; 58 | } 59 | 60 | setZero() { 61 | this[0] = 0; 62 | this[1] = 0; 63 | this[2] = 0; 64 | this[3] = 0; 65 | return this; 66 | } 67 | 68 | toArray() { 69 | return [this[0], this[1], this[2], this[3]]; 70 | } 71 | 72 | toString() { return `[${this.x}, ${this.y}, ${this.z}, ${this.w}]`; } 73 | 74 | get x() { return this[0]; } 75 | 76 | set x(v) { this[0] = v; } 77 | 78 | get y() { return this[1]; } 79 | 80 | set y(v) { this[1] = v; } 81 | 82 | get z() { return this[2]; } 83 | 84 | set z(v) { this[2] = v; } 85 | 86 | get w() { return this[3]; } 87 | 88 | set w(v) { this[3] = v; } 89 | 90 | map() { 91 | switch (arguments.length) { 92 | case 4: 93 | return new Vector4(this[arguments[0]], this[arguments[1]], 94 | this[arguments[2]], this[arguments[3]]); 95 | case 3: 96 | return new Vector3(this[arguments[0]], this[arguments[1]], this[arguments[2]]); 97 | case 2: 98 | return new Vector2(this[arguments[0]], this[arguments[1]]); 99 | } 100 | return null; 101 | } 102 | 103 | sum() { 104 | return this[0] + this[1] + this[2] + this[3]; 105 | } 106 | 107 | getLength() { 108 | return Math.sqrt(this[0] * this[0] + this[1] * this[1] + this[2] * this[2] + this[3] * this[3]); 109 | } 110 | 111 | getLengthSquared() { 112 | return this[0] * this[0] + this[1] * this[1] + this[2] * this[2] + this[3] * this[3]; 113 | } 114 | 115 | normalize(out) { 116 | out = out || this; 117 | const l = this.getLength(); 118 | if (!l) { 119 | if (out !== this) { 120 | out.set(this); 121 | } 122 | return out; 123 | } 124 | out[0] = this[0] / l; 125 | out[1] = this[1] / l; 126 | out[2] = this[2] / l; 127 | out[3] = this[3] / l; 128 | return out; 129 | } 130 | 131 | negate(out) { 132 | out = out || this; 133 | out[0] = -this[0]; 134 | out[1] = -this[1]; 135 | out[2] = -this[2]; 136 | out[3] = -this[3]; 137 | return out; 138 | } 139 | 140 | abs(out) { 141 | out = out || this; 142 | out[0] = Math.abs(this[0]); 143 | out[1] = Math.abs(this[1]); 144 | out[2] = Math.abs(this[2]); 145 | out[3] = Math.abs(this[3]); 146 | return out; 147 | } 148 | 149 | add(b, out) { 150 | out = out || this; 151 | out[0] = this[0] + b[0]; 152 | out[1] = this[1] + b[1]; 153 | out[2] = this[2] + b[2]; 154 | out[3] = this[3] + b[3]; 155 | return out; 156 | } 157 | 158 | subtract(b, out) { 159 | out = out || this; 160 | out[0] = this[0] - b[0]; 161 | out[1] = this[1] - b[1]; 162 | out[2] = this[2] - b[2]; 163 | out[3] = this[3] - b[3]; 164 | return out; 165 | } 166 | 167 | multiply(b, out) { 168 | out = out || this; 169 | out[0] = this[0] * b[0]; 170 | out[1] = this[1] * b[1]; 171 | out[2] = this[2] * b[2]; 172 | out[3] = this[3] * b[3]; 173 | return out; 174 | } 175 | 176 | divide(b, out) { 177 | out = out || this; 178 | out[0] = b[0] ? this[0] / b[0] : 0; 179 | out[1] = b[1] ? this[1] / b[1] : 0; 180 | out[2] = b[2] ? this[2] / b[2] : 0; 181 | out[3] = b[3] ? this[3] / b[3] : 0; 182 | return out; 183 | } 184 | 185 | scale(s, out) { 186 | out = out || this; 187 | out[0] = this[0] * s; 188 | out[1] = this[1] * s; 189 | out[2] = this[2] * s; 190 | out[3] = this[3] * s; 191 | return out; 192 | } 193 | 194 | static negated(a, out) { 195 | out = out || new Vector4(); 196 | out.setFrom(-a[0], -a[1], -a[2], -a[3]); 197 | return out; 198 | } 199 | 200 | static abs(a, out) { 201 | out = out || new Vector4(); 202 | out.setFrom(Math.abs(a[0]), Math.abs(a[1]), Math.abs(a[2]), Math.abs(a[3])); 203 | return out; 204 | } 205 | 206 | static length(a) { 207 | return Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2] + a[3] * a[3]); 208 | } 209 | 210 | static lengthSquared(a) { 211 | return a[0] * a[0] + a[1] * a[1] + a[2] * a[2] + a[3] * a[3]; 212 | } 213 | 214 | static distanceSquared(a, b) { 215 | const dx = b[0] - a[0]; 216 | const dy = b[1] - a[1]; 217 | const dz = b[2] - a[2]; 218 | const dw = b[3] - a[3]; 219 | return dx * dx + dy * dy + dz * dz + dw * dw; 220 | } 221 | 222 | static distance(a, b) { 223 | const dx = b[0] - a[0]; 224 | const dy = b[1] - a[1]; 225 | const dz = b[2] - a[2]; 226 | const dw = b[3] - a[3]; 227 | return Math.sqrt(dx * dx + dy * dy + dz * dz + dw * dw); 228 | } 229 | 230 | static normalize(a, out) { 231 | out = out || new Vector4(); 232 | const l = Vector4.getLength(a); 233 | if (!l) { 234 | out.set(a); 235 | return out; 236 | } 237 | out[0] = a[0] / l; 238 | out[1] = a[1] / l; 239 | out[2] = a[2] / l; 240 | out[3] = a[3] / l; 241 | return out; 242 | } 243 | 244 | static dot(a, b) { 245 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; 246 | } 247 | 248 | static add(a, b, out) { 249 | out = out || new Vector4(); 250 | out[0] = a[0] + b[0]; 251 | out[1] = a[1] + b[1]; 252 | out[2] = a[2] + b[2]; 253 | out[3] = a[3] + b[3]; 254 | return out; 255 | } 256 | 257 | static subtract(a, b, out) { 258 | out = out || new Vector4(); 259 | out[0] = a[0] - b[0]; 260 | out[1] = a[1] - b[1]; 261 | out[2] = a[2] - b[2]; 262 | out[3] = a[3] - b[3]; 263 | return out; 264 | } 265 | 266 | static multiply(a, b, out) { 267 | out = out || new Vector4(); 268 | out[0] = a[0] * b[0]; 269 | out[1] = a[1] * b[1]; 270 | out[2] = a[2] * b[2]; 271 | out[3] = a[3] * b[3]; 272 | return out; 273 | } 274 | 275 | static divide(a, b, out) { 276 | out = out || new Vector4(); 277 | out[0] = b[0] ? a[0] / b[0] : 0; 278 | out[1] = b[1] ? a[1] / b[1] : 0; 279 | out[2] = b[2] ? a[2] / b[2] : 0; 280 | out[3] = b[3] ? a[3] / b[3] : 0; 281 | return out; 282 | } 283 | 284 | static scale(a, s, out) { 285 | out = out || new Vector4(); 286 | out[0] = a[0] * s; 287 | out[1] = a[1] * s; 288 | out[2] = a[2] * s; 289 | out[3] = a[3] * s; 290 | return out; 291 | } 292 | 293 | static scaleAndAdd(a, b, s, out) { 294 | out = out || new Vector4(); 295 | out[0] = a[0] + b[0] * s; 296 | out[1] = a[1] + b[1] * s; 297 | out[2] = a[2] + b[2] * s; 298 | out[3] = a[3] + b[3] * s; 299 | return out; 300 | } 301 | 302 | static lerp(a, b, t, out) { 303 | out = out || new Vector4(); 304 | const ax = a[0]; 305 | const ay = a[1]; 306 | const az = a[2]; 307 | const aw = a[3]; 308 | out[0] = ax + t * (b[0] - ax); 309 | out[1] = ay + t * (b[1] - ay); 310 | out[2] = az + t * (b[2] - az); 311 | out[3] = aw + t * (b[3] - aw); 312 | return out; 313 | } 314 | } 315 | 316 | Vector4.Zero = new Vector4(); 317 | Vector4.One = new Vector4(1, 1, 1, 1); 318 | -------------------------------------------------------------------------------- /js/input.js: -------------------------------------------------------------------------------- 1 | import { Globals } from "./globals.js"; 2 | 3 | export class Input { 4 | constructor(element, options) { 5 | options = options || {}; 6 | 7 | Globals.input = this; 8 | 9 | this.element = element; 10 | this.ignoreEvents = false; 11 | this.captureWheel = true; 12 | this.dragging = false; 13 | this.lastPos = [0, 0]; 14 | 15 | this.mouse = { 16 | buttons: 0, 17 | lastButtons: 0, 18 | leftButton: false, 19 | middleButton: false, 20 | rightButton: false, 21 | position: [0, 0], 22 | x: 0, // in canvas coordinates 23 | y: 0, 24 | deltaX: 0, 25 | deltaY: 0, 26 | clientX: 0, // in client coordinates 27 | clientY: 0 28 | }; 29 | 30 | this.lastClickTime = 0; 31 | this._onMouseHandler = null; 32 | this._onKeyHandler = null; 33 | 34 | this.onMouseDown = []; 35 | this.onMouseMove = []; 36 | this.onMouseUp = []; 37 | 38 | this.init(options); 39 | } 40 | 41 | getKeyDown(key) { 42 | return !!Input.keys[key]; 43 | } 44 | 45 | getKeyUp(key) { 46 | return !Input.keys[key]; 47 | } 48 | 49 | getMouseButtonDown(button) { 50 | if (button == 0) { 51 | return this.mouse.leftButton; 52 | } else if (button == 1) { 53 | return this.mouse.middleButton; 54 | } else if (button == 2) { 55 | return this.mouse.rightButton; 56 | } 57 | return false; 58 | } 59 | 60 | lockMouse(state) { 61 | if (state) { 62 | this.element.requestPointerLock(); 63 | } else { 64 | document.exitPointerLock(); 65 | } 66 | } 67 | 68 | close() { 69 | if (this._onKeyHandler) { 70 | document.removeEventListener("keydown", this._onKeyHandler); 71 | document.removeEventListener("keyup", this._onKeyHandler); 72 | this._onKeyHandler = null; 73 | } 74 | } 75 | 76 | init(options) { 77 | if (this._onMouseHandler) { 78 | return; 79 | } 80 | 81 | const element = this.element; 82 | if (!element) { 83 | return; 84 | } 85 | 86 | this.captureWheel = options.captureWheel || this.captureWheel; 87 | 88 | this._onMouseHandler = this._onMouse.bind(this); 89 | 90 | element.addEventListener("mousedown", this._onMouseHandler); 91 | element.addEventListener("mousemove", this._onMouseHandler); 92 | element.addEventListener("dragstart", this._onMouseHandler); 93 | 94 | if (this.captureWheel) { 95 | element.addEventListener("mousewheel", this._onMouseHandler, false); 96 | element.addEventListener("wheel", this._onMouseHandler, false); 97 | } 98 | 99 | // Prevent right click context menu 100 | element.addEventListener("contextmenu", function(e) { 101 | e.preventDefault(); 102 | return false; 103 | }); 104 | 105 | this.captureKeys(); 106 | } 107 | 108 | _onMouse(e) { 109 | if (this.ignoreEvents) { 110 | return; 111 | } 112 | 113 | Input.active = this; 114 | 115 | const element = this.element; 116 | const mouse = this.mouse; 117 | 118 | //Log.debug(e.type); 119 | const oldMouseMask = mouse.buttons; 120 | this._augmentEvent(e, element); 121 | // Type cannot be overwritten, so I make a clone to allow me to overwrite 122 | e.eventType = e.eventType || e.type; 123 | const now = Globals.now(); 124 | 125 | // mouse info 126 | mouse.dragging = e.dragging; 127 | mouse.position[0] = e.canvasx; 128 | mouse.position[1] = e.canvasy; 129 | mouse.x = e.canvasx; 130 | mouse.y = e.canvasy; 131 | mouse.mouseX = e.mousex; 132 | mouse.mouseY = e.mousey; 133 | mouse.canvasX = e.canvasx; 134 | mouse.canvasY = e.canvasy; 135 | mouse.clientX = e.mousex; 136 | mouse.clientY = e.mousey; 137 | mouse.buttons = e.buttons; 138 | mouse.leftButton = !!(mouse.buttons & Input.LeftButtonMask); 139 | mouse.middleButton = !!(mouse.buttons & Input.MiddleButtonMask); 140 | mouse.rightButton = !!(mouse.buttons & Input.RightButtonMask); 141 | 142 | if (e.eventType === "mousedown") { 143 | if (oldMouseMask == 0) { //no mouse button was pressed till now 144 | element.removeEventListener("mousemove", this._onMouseHandler); 145 | const doc = element.ownerDocument; 146 | doc.addEventListener("mousemove", this._onMouseHandler); 147 | doc.addEventListener("mouseup", this._onMouseHandler); 148 | } 149 | this.lastClickTime = now; 150 | for (const cb of this.onMouseDown) { 151 | cb(e); 152 | } 153 | } else if (e.eventType === "mousemove") { 154 | for (const cb of this.onMouseMove) { 155 | cb(e); 156 | } 157 | } else if(e.eventType === "mouseup") { 158 | if (this.mouse.buttons == 0) { // no more buttons pressed 159 | element.addEventListener("mousemove", this._onMouseHandler); 160 | const doc = element.ownerDocument; 161 | doc.removeEventListener("mousemove", this._onMouseHandler); 162 | doc.removeEventListener("mouseup", this._onMouseHandler); 163 | } 164 | e.clickTime = now - this.lastClickTime; 165 | 166 | for (const cb of this.onMouseUp) { 167 | cb(e); 168 | } 169 | } else if (e.eventType === "mousewheel" || e.eventType == "wheel" || 170 | e.eventType === "DOMMouseScroll") { 171 | e.eventType = "mousewheel"; 172 | if (e.type === "wheel") { 173 | e.wheel = -e.deltaY; // in firefox deltaY is 1 while in Chrome is 120 174 | } else { 175 | e.wheel = (e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60); 176 | } 177 | 178 | // from stack overflow 179 | // firefox doesnt have wheelDelta 180 | e.delta = e.wheelDelta !== undefined ? 181 | (e.wheelDelta / 40) : 182 | (e.deltaY ? -e.deltaY / 3 : 0); 183 | } 184 | 185 | if (!e.skipPreventDefault) { 186 | if (e.eventType != "mousemove") { 187 | e.stopPropagation(); 188 | } 189 | e.preventDefault(); 190 | return; 191 | } 192 | } 193 | 194 | /// Tells the system to capture key events on the element. This will trigger onKey 195 | captureKeys() { 196 | if (this._onKeyHandler) { 197 | return; 198 | } 199 | 200 | this._onKeyHandler = this._onKey.bind(this); 201 | 202 | document.addEventListener("keydown", this._onKeyHandler); 203 | document.addEventListener("keyup", this._onKeyHandler); 204 | } 205 | 206 | _onKey(e, preventDefault) { 207 | Input.active = this; 208 | e.eventType = e.type; 209 | 210 | const targetElement = e.target.nodeName.toLowerCase(); 211 | if (targetElement === "input" || targetElement === "textarea" || 212 | targetElement === "select") { 213 | return; 214 | } 215 | 216 | e.character = String.fromCharCode(e.keyCode).toLowerCase(); 217 | const key = e.keyCode; 218 | 219 | Input.keys[key] = (e.type === "keydown"); 220 | 221 | if (preventDefault && (e.isChar || Input.blockableKeys[e.keyIdentifier || e.key])) { 222 | e.preventDefault(); 223 | } 224 | } 225 | 226 | _augmentEvent(e, rootElement) { 227 | let b = null; 228 | 229 | rootElement = rootElement || e.target || this.element; 230 | b = rootElement.getBoundingClientRect(); 231 | 232 | e.mousex = e.clientX - b.left; 233 | e.mousey = e.clientY - b.top; 234 | e.canvasx = e.mousex; 235 | e.canvasy = b.height - e.mousey; // The y coordinate is flipped for canvas coordinates 236 | e.deltax = 0; 237 | e.deltay = 0; 238 | 239 | if (e.type === "mousedown") { 240 | this.dragging = true; 241 | } else if (e.type === "mouseup") { 242 | if (e.buttons === 0) { 243 | this.dragging = false; 244 | } 245 | } 246 | 247 | if (e.movementX !== undefined) {// && !Platform.isMobile) { 248 | e.deltax = e.movementX; 249 | e.deltay = e.movementY; 250 | } else { 251 | e.deltax = e.mousex - this.lastPos[0]; 252 | e.deltay = e.mousey - this.lastPos[1]; 253 | } 254 | this.lastPos[0] = e.mousex; 255 | this.lastPos[1] = e.mousey; 256 | 257 | // insert info in event 258 | e.dragging = this.dragging; 259 | e.leftButton = !!(this.mouse.buttons & Input.LeftButtonMask); 260 | e.middleButton = !!(this.mouse.buttons & Input.MiddleButtonMask); 261 | e.rightButton = !!(this.mouse.buttons & Input.RightButtonMask); 262 | 263 | // e.buttons use 1:left,2:right,4:middle but 264 | // e.button uses 0:left,1:middle,2:right 265 | e.buttonsMask = 0; 266 | if (e.leftButton) e.buttonsMask = 1; 267 | if (e.middleButton) e.buttonsMask |= 2; 268 | if (e.rightButton) e.buttonsMask |= 4; 269 | e.isButtonPressed = function(num) { 270 | return this.buttonsMask & (1 << num); 271 | }; 272 | } 273 | } 274 | 275 | Input.blockableKeys = { 276 | "Up": true, 277 | "Down": true, 278 | "Left": true, 279 | "Right": true 280 | }; 281 | 282 | Input.keys = {}; 283 | 284 | 285 | Input.LeftButtonMask = 1; 286 | Input.RightButtonMask = 2; 287 | Input.MiddleButtonMask = 4; 288 | 289 | Input.KeyCode = { 290 | Space: 32, 291 | A: 65, 292 | D: 68, 293 | S: 83, 294 | W: 87, 295 | RightShift: 16, 296 | LeftShift: 16, 297 | RightControl: 17, 298 | LeftControl: 17, 299 | }; 300 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /js/player.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "./math/vector3.js"; 2 | import { Globals } from "./globals.js"; 3 | import { Input } from "./input.js"; 4 | import { Transform } from "./transform.js"; 5 | 6 | export class Player extends Transform { 7 | constructor(camera) { 8 | super(); 9 | 10 | Globals.player = this; 11 | 12 | this.addChild(camera); 13 | 14 | this.isGrounded = false; 15 | this.isSprinting = false; 16 | 17 | this.camera = camera; 18 | 19 | this.turnSpeed = 0.5; 20 | this.walkSpeed = 5; 21 | this.sprintSpeed = 10; 22 | this.jumpForce = 5; 23 | this.gravity = -9.8; 24 | 25 | this.playerWidth = 0.15; 26 | this.boundsTolerance = 0.1; 27 | 28 | this.horizontal = 0; 29 | this.vertical = 0; 30 | this.mouseHorizontal = 0; 31 | this.mouseVertical = 0; 32 | this.velocity = new Vector3(); 33 | this.verticalMomentum = 0; 34 | this.jumpRequest = false; 35 | 36 | this.checkIncrement = 0.1; 37 | this.reach = 8; 38 | 39 | this._forward = new Vector3(); 40 | this._up = new Vector3(0, 1, 0); 41 | this._right = new Vector3(); 42 | 43 | this.highlightPosition = new Vector3(); 44 | this.placePosition = new Vector3(); 45 | 46 | const stats = document.createElement("div"); 47 | this.stats = stats; 48 | stats.id = "stats"; 49 | stats.width = "80px"; 50 | stats.style.opacity = "0.9"; 51 | stats.style.color = "#0ff"; 52 | stats.style.backgroundColor = "rgba(0, 0, 0, 0.2)"; 53 | stats.style.padding = "5px"; 54 | stats.style.fontFamily = "Helvetica,Arial,sans-serif"; 55 | stats.style.fontSize = "14px"; 56 | stats.style.fontWeight = "bold"; 57 | stats.style.lineHeight = "15px"; 58 | stats.style.borderRadius = "5px"; 59 | stats.style.boxShadow = "0px 5px 20px rgba(0, 0, 0, 0.2)"; 60 | stats.style.position = "absolute"; 61 | stats.style.top = "40px"; 62 | stats.style.left = "0px"; 63 | stats.style.width = 200; 64 | stats.style.height = 200; 65 | stats.style.margin = "0px"; 66 | stats.style.borderRadius = "5px"; 67 | stats.style.pointerEvents = "none"; 68 | stats.display = "block"; 69 | stats.style.zIndex = "100"; 70 | document.body.appendChild(stats); 71 | this.lastTime = Globals.time; 72 | 73 | Globals.input.onMouseDown.push(this.onMouseDown.bind(this)); 74 | Globals.input.onMouseMove.push(this.onMouseMove.bind(this)); 75 | } 76 | 77 | get world() { return Globals.world; } 78 | 79 | update() { 80 | this.calculateVelocity(); 81 | if (this.jumpRequest) { 82 | this.jump(); 83 | } 84 | 85 | this.camera.rotation[0] -= this.mouseVertical; 86 | this.rotation[1] -= this.mouseHorizontal; 87 | this.position.add(this.velocity); 88 | this.camera.localDirty = true; 89 | this.localDirty = true; 90 | 91 | this.mouseHorizontal = 0; 92 | this.mouseVertical = 0; 93 | 94 | if (!document.pointerLockElement) { 95 | return; 96 | } 97 | 98 | if (!this.world) { 99 | return; 100 | } 101 | 102 | this.placeCursorBlock(); 103 | 104 | this.isSprinting = Globals.input.getKeyDown(Input.KeyCode.LeftShift); 105 | 106 | if (this.isGrounded && Globals.input.getKeyDown(Input.KeyCode.Space)) { 107 | this.jumpRequest = true; 108 | } 109 | 110 | if (Globals.input.getKeyDown(Input.KeyCode.A)) { 111 | this.horizontal = -1; 112 | } else if (Globals.input.getKeyDown(Input.KeyCode.D)) { 113 | this.horizontal = 1; 114 | } else { 115 | this.horizontal = 0; 116 | } 117 | 118 | if (Globals.input.getKeyDown(Input.KeyCode.W)) { 119 | this.vertical = -1; 120 | } else if (Globals.input.getKeyDown(Input.KeyCode.S)) { 121 | this.vertical = 1; 122 | } else { 123 | this.vertical = 0; 124 | } 125 | 126 | const editDelay = 2; 127 | 128 | if (Globals.input.getMouseButtonDown(0)) { 129 | const t = Globals.time; 130 | if ((t - this.lastTime) > editDelay) { 131 | if (this.placeActive) { 132 | const chunk = this.world.getChunkFromPosition(this.placePosition); 133 | if (chunk) { 134 | chunk.editVoxel(this.placePosition[0], this.placePosition[1], 135 | this.placePosition[2], 7); 136 | this.lastTime = t; 137 | } 138 | } 139 | } 140 | } else if (Globals.input.getMouseButtonDown(2)) { 141 | const t = Globals.time; 142 | if ((t - this.lastTime) > editDelay) { 143 | if (this.highlightActive) { 144 | const chunk = this.world.getChunkFromPosition(this.highlightPosition); 145 | if (chunk) { 146 | chunk.editVoxel(this.highlightPosition[0], this.highlightPosition[1], 147 | this.highlightPosition[2], 0); 148 | this.lastTime = t; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | placeCursorBlock() { 156 | if (!this.world) { 157 | return; 158 | } 159 | 160 | let step = this.checkIncrement; 161 | const pos = new Vector3(); 162 | const lastPos = new Vector3(); 163 | 164 | const camPos = this.camera.getWorldPosition(); 165 | const camForward = this.camera.getWorldForward(); 166 | 167 | while (step < this.reach) { 168 | pos.setFrom(camPos[0] - (camForward[0] * step), 169 | camPos[1] - (camForward[1] * step), 170 | camPos[2] - (camForward[2] * step)); 171 | 172 | if (this.world.checkForVoxel(pos[0], pos[1], pos[2])) { 173 | this.highlightActive = true; 174 | this.placeActive = true; 175 | this.highlightPosition.setFrom(Math.floor(pos[0]), Math.floor(pos[1]), Math.floor(pos[2])); 176 | this.placePosition.set(lastPos); 177 | return; 178 | } 179 | 180 | lastPos.setFrom(Math.floor(pos[0]), Math.floor(pos[1]), Math.floor(pos[2])); 181 | 182 | step += this.checkIncrement; 183 | } 184 | 185 | this.highlightActive = false; 186 | this.placeActive = false; 187 | } 188 | 189 | jump() { 190 | this.verticalMomentum = this.jumpForce; 191 | this.isGrounded = false; 192 | this.jumpRequest = false; 193 | } 194 | 195 | calculateVelocity() { 196 | // Affect vertical momentum with gravity. 197 | if (this.verticalMomentum > this.gravity) { 198 | this.verticalMomentum += Globals.fixedDeltaTime * this.gravity; 199 | } 200 | 201 | this.getWorldForward(this._forward); 202 | this.getWorldRight(this._right); 203 | 204 | // if we're sprinting, use the sprint multiplier. 205 | const speed = this.isSprinting ? this.sprintSpeed : this.walkSpeed; 206 | 207 | this.velocity.set(this._forward.scale(this.vertical) 208 | .add(this._right.scale(this.horizontal)) 209 | .scale(Globals.fixedDeltaTime * speed)); 210 | 211 | // Apply vertical momentum (falling/jumping). 212 | this.velocity.y += this.verticalMomentum * Globals.fixedDeltaTime; 213 | 214 | if ((this.velocity.z > 0 && this.front) || (this.velocity.z < 0 && this.back)) { 215 | this.velocity.z = 0; 216 | } 217 | 218 | if ((this.velocity.x > 0 && this.right) || (this.velocity.x < 0 && this.left)) { 219 | this.velocity.x = 0; 220 | } 221 | 222 | if (this.velocity.y < 0) { 223 | this.velocity.y = this.checkDownSpeed(this.velocity.y); 224 | } else if (this.velocity.y > 0) { 225 | this.velocity.y = this.checkUpSpeed(this.velocity.y); 226 | } 227 | 228 | const pos = this.position; 229 | const px = Math.floor(pos.x); 230 | const py = Math.floor(pos.y); 231 | const pz = Math.floor(pos.z); 232 | this.stats.innerText = `position: ${px} ${py} ${pz}\n` + 233 | `isGrounded: ${this.isGrounded} sprint: ${this.isSprinting}\n` + 234 | `horizontal: ${this.horizontal} vertical: ${this.vertical}\n` + 235 | `mouseHorizontal: ${this.mouseVertical} mouseVertical: ${this.mouseHorizontal}\n` + 236 | `mouseButtons: ${Globals.input.mouse.leftButton} ${Globals.input.mouse.rightButton}\n` + 237 | `deltaTime: ${(Globals.deltaTime * 100).toFixed(2)}, maxDelta: ${(Globals.maxDeltaTime * 100).toFixed(2)}\n` + 238 | `velocity: ${this.velocity}\n` + 239 | `Player Chunk: ${this.world.playerChunkCoord}\n` + 240 | `Chunks active: ${this.world.activeChunks.length} toDraw: ${this.world.chunksToDraw.length} toUpdate: ${this.world.chunksToUpdate.length}`; 241 | } 242 | 243 | onMouseMove(e) { 244 | if (document.pointerLockElement) { 245 | const turnSpeed = this.turnSpeed; 246 | this.mouseHorizontal = e.deltax * turnSpeed; 247 | this.mouseVertical = e.deltay * turnSpeed; 248 | } 249 | } 250 | 251 | onMouseDown() { 252 | Globals.input.lockMouse(true); 253 | } 254 | 255 | checkDownSpeed(downSpeed) { 256 | const pos = this.position; 257 | const world = this.world; 258 | const width = this.playerWidth; 259 | const speed = downSpeed; 260 | 261 | if (!world) { 262 | this.isGrounded = true; 263 | return 0; 264 | } 265 | 266 | if (world.checkForVoxel(pos.x - width, pos.y + speed, pos.z - width) || 267 | world.checkForVoxel(pos.x + width, pos.y + speed, pos.z - width) || 268 | world.checkForVoxel(pos.x + width, pos.y + speed, pos.z + width) || 269 | world.checkForVoxel(pos.x - width, pos.y + speed, pos.z + width)) { 270 | this.isGrounded = true; 271 | return 0; 272 | } 273 | 274 | this.isGrounded = false; 275 | return downSpeed; 276 | } 277 | 278 | checkUpSpeed(upSpeed) { 279 | const pos = this.position; 280 | const world = this.world; 281 | const width = this.playerWidth; 282 | const speed = 2 + upSpeed; 283 | 284 | if (!world) { 285 | this.isGrounded = true; 286 | return 0; 287 | } 288 | 289 | if (world.checkForVoxel(pos.x - width, pos.y + speed, pos.z - width) || 290 | world.checkForVoxel(pos.x + width, pos.y + speed, pos.z - width) || 291 | world.checkForVoxel(pos.x + width, pos.y + speed, pos.z + width) || 292 | world.checkForVoxel(pos.x - width, pos.y + speed, pos.z + width)) { 293 | return 0; 294 | } 295 | 296 | return upSpeed; 297 | } 298 | 299 | get front() { 300 | const pos = this.position; 301 | if (this.world.checkForVoxel(pos.x, pos.y, pos.z + this.playerWidth) || 302 | this.world.checkForVoxel(pos.x, pos.y + 1, pos.z + this.playerWidth)) { 303 | return true; 304 | } 305 | return false; 306 | } 307 | 308 | get back() { 309 | const pos = this.position; 310 | if (this.world.checkForVoxel(pos.x, pos.y, pos.z - this.playerWidth) || 311 | this.world.checkForVoxel(pos.x, pos.y + 1, pos.z - this.playerWidth)) { 312 | return true; 313 | } 314 | return false; 315 | } 316 | 317 | get left() { 318 | const pos = this.position; 319 | if (this.world.checkForVoxel(pos.x - this.playerWidth, pos.y, pos.z) || 320 | this.world.checkForVoxel(pos.x - this.playerWidth, pos.y + 1, pos.z)) { 321 | return true; 322 | } 323 | return false; 324 | } 325 | 326 | get right() { 327 | const pos = this.position; 328 | if (this.world.checkForVoxel(pos.x + this.playerWidth, pos.y, pos.z) || 329 | this.world.checkForVoxel(pos.x + this.playerWidth, pos.y + 1, pos.z)) { 330 | return true; 331 | } 332 | return false; 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /js/math/math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module math 3 | */ 4 | 5 | /** 6 | * Returns true if the object is a number. 7 | * @param {*} obj 8 | * @return {bool} 9 | */ 10 | export function isNumber(obj) { 11 | return obj != null && obj.constructor === Number; 12 | } 13 | 14 | /** 15 | * Apply the sign of b to a. 16 | * @param {number} a 17 | * @param {number} b 18 | * @return {number} 19 | */ 20 | export function copysign(a, b) { return Math.sign(b) * a; } 21 | 22 | /** 23 | * Convert degrees to radians. 24 | * @memberof Math 25 | * @param {number} a 26 | * @return {number} 27 | */ 28 | export function degreesToRadians(a) { return a * DegreeToRadian; } 29 | 30 | /** 31 | * Convert radians to degrees. 32 | * @param {number} a 33 | * @return {number} 34 | */ 35 | export function radiansToDegrees(a) { return a * RadianToDegree; } 36 | 37 | /** 38 | * Compare two floating-point numbers, testing if the two numbers closer than the given epsilon. 39 | * @param {number} a 40 | * @param {number} b 41 | * @param {number} epsilon 42 | * @return {bool} 43 | */ 44 | export function equals(a, b, epsilon = Epsilon) { 45 | return Math.abs(b - a) <= epsilon; 46 | } 47 | 48 | /** 49 | * Is the given number a power of 2? 50 | * @param {number} v 51 | * @return {bool} 52 | */ 53 | export function isPowerOfTwo(v) { 54 | return ((Math.log(v) / Math.log(2)) % 1) == 0; 55 | } 56 | 57 | /** 58 | * Return the closest power-of-2 of the given number. 59 | * @param {number} v 60 | * @return {number} 61 | */ 62 | export function nearestPowerOfTwo(v) { 63 | return Math.pow(2, Math.round(Math.log(v) / Math.log(2))); 64 | } 65 | 66 | /** 67 | * Clamp the value x to the range [min, max]. 68 | * @param {number} x 69 | * @param {number} min 70 | * @param {number} max 71 | * @return {number} 72 | */ 73 | export function clamp(x, min, max) { 74 | return x < min ? min : x > max ? max : x; 75 | } 76 | 77 | /** 78 | * Linear interpolate between [min, max] using x. If x is outside of the range 79 | * [min, max], the interpolated value will be a linear extrapolation. 80 | * @param {number} x The interpolation amount, in the range [0, 1]. 81 | * @param {number} min The start of the range to interpolate. 82 | * @param {number} max The end of the range to interpolate. 83 | * @return {number} The interpolated value. 84 | */ 85 | export function lerp(x, min, max) { 86 | const u = x < 0 ? 0 : x > 1 ? 1 : x; 87 | return u * (max - min) + min; 88 | } 89 | 90 | /** 91 | * If x is outside of the range [min, max], the interval will be repeated. 92 | * @param {number} x 93 | * @param {number} min 94 | * @param {number} max 95 | * @return {number} 96 | */ 97 | export function repeat(x, min, max) { 98 | const t = x - min; 99 | const l = max - min; 100 | return clamp(t - Math.floor(t / l) * l, 0, l); 101 | } 102 | 103 | /** 104 | * If x is outside of the range [min, max], the interval will be ping-ponged. 105 | * @param {number} x 106 | * @param {number} min 107 | * @param {number} max 108 | * @return {number} 109 | */ 110 | export function pingpong(x, min, max) { 111 | const t = x - min; 112 | const l = max - min; 113 | const u = clamp(t - Math.floor(t / l) * l, 0, l); 114 | return l - Math.abs(u - l); 115 | } 116 | 117 | /** 118 | * The length of a 3-dimensional vector, given by its components. 119 | * @param {number} x 120 | * @param {number} y 121 | * @param {number} z 122 | * @return {number} 123 | */ 124 | export function length3(x, y, z) { 125 | return Math.sqrt(x * x + y * y + z * z); 126 | } 127 | 128 | /** 129 | * Implementation of the C ldexp function, which combines a mantessa and exponent into a 130 | * floating-point number. 131 | * http://croquetweak.blogspot.com/2014/08/deconstructing-floats-frexp-and-ldexp.html 132 | * @param {number} mantissa 133 | * @param {number} exponent 134 | */ 135 | export function ldexp(mantissa, exponent) { 136 | const steps = Math.min(3, Math.ceil(Math.abs(exponent) / 1023)); 137 | let result = mantissa; 138 | for (let i = 0; i < steps; i++) { 139 | result *= Math.pow(2, Math.floor((exponent + i) / steps)); 140 | } 141 | return result; 142 | } 143 | 144 | /** 145 | * Convert a float32 number to a float16. 146 | * 147 | * ref: http://stackoverflow.com/questions/32633585/how-do-you-convert-to-half-floats-in-javascript 148 | * This method is faster than the OpenEXR implementation, with the additional benefit of rounding, 149 | * inspired by James Tursa?s half-precision code 150 | * 151 | * @param {number} value The number to convert 152 | * @return {number} The float16 value 153 | */ 154 | export function toFloat16(value) { 155 | _floatView[0] = value; 156 | const x = _int32View[0]; 157 | 158 | let bits = (x >> 16) & 0x8000; // Get the sign 159 | let m = (x >> 12) & 0x07ff; // Keep one extra bit for rounding 160 | const e = (x >> 23) & 0xff; // Using int is faster here 161 | 162 | // If zero, or denormal, or exponent underflows too much for a denormal 163 | // half, return signed zero. 164 | if (e < 103) { 165 | return bits; 166 | } 167 | 168 | // If NaN, return NaN. If Inf or exponent overflow, return Inf. 169 | if (e > 142) { 170 | bits |= 0x7c00; 171 | // If exponent was 0xff and one mantissa bit was set, it means NaN, 172 | // not Inf, so make sure we set one mantissa bit too. 173 | bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff); 174 | return bits; 175 | } 176 | 177 | // If exponent underflows but not too much, return a denormal. 178 | if (e < 113) { 179 | m |= 0x0800; 180 | // Extra rounding may overflow and set mantissa to 0 and exponent 181 | // to 1, which is OK. 182 | bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1); 183 | return bits; 184 | } 185 | 186 | bits |= ((e - 112) << 10) | (m >> 1); 187 | // Extra rounding. An overflow will set mantissa to 0 and increment 188 | // the exponent, which is OK. 189 | bits += m & 1; 190 | return bits; 191 | } 192 | const _floatView = new Float32Array(1); 193 | const _int32View = new Int32Array(_floatView.buffer); 194 | 195 | /** 196 | * Solves the quadratic equation: a + b*x + c*x^2 = 0 197 | * @param {number} a 198 | * @param {number} b 199 | * @param {number} c 200 | * @param {Array} roots The resulting roots 201 | * @return {number} The number of roots found 202 | */ 203 | export function solveQuadratic(a, b, c, roots) { 204 | b = -b; 205 | 206 | if (!a) { 207 | if (!b) { 208 | return 0; 209 | } 210 | roots[0] = c / b; 211 | return 1; 212 | } 213 | 214 | let d = b * b - 4.0 * a * c; 215 | 216 | // Treat values of d around 0 as 0. 217 | if ((d > -Epsilon) && (d < Epsilon)) { 218 | roots[0] = 0.5 * b / a; 219 | return 1; 220 | } else { 221 | if (d < 0.0) { 222 | return 0; 223 | } 224 | } 225 | 226 | d = Math.sqrt(d); 227 | 228 | const t = 2.0 * a; 229 | roots[0] = (b + d) / t; 230 | roots[1] = (b - d) / t; 231 | 232 | return 2; 233 | } 234 | 235 | /** 236 | * Solves the cubic equation: a + b*x + c*x^2 + d*x^3 = 0 237 | * @param {number} a 238 | * @param {number} b 239 | * @param {number} c 240 | * @param {number} d 241 | * @param {Array} roots The resulting roots 242 | * @return {number} The number of roots found 243 | */ 244 | export function solveCubic(a, b, c, d, roots) { 245 | if (!a) { 246 | return solveQuadratic(b, c, d, roots); 247 | } 248 | 249 | let a1 = b / a; 250 | let a2 = c / a; 251 | let a3 = d / a; 252 | 253 | const A2 = a1 * a1; 254 | const Q = (A2 - 3.0 * a2) / 9.0; 255 | const R = (a1 * (A2 - 4.5 * a2) + 13.5 * a3) / 27.0; 256 | const Q3 = Q * Q * Q; 257 | const R2 = R * R; 258 | let d1 = Q3 - R2; 259 | const an = a1 / 3.0; 260 | 261 | let sQ = 0.0; 262 | if (d < 0.0) { 263 | sQ = Math.pow(Math.sqrt(R2 - Q3) + R.abs(), 1.0 / 3.0); 264 | 265 | if (R < 0.0) { 266 | roots[0] = (sQ + Q / sQ) - an; 267 | } else { 268 | roots[0] = -(sQ + Q / sQ) - an; 269 | } 270 | 271 | return 1; 272 | } 273 | 274 | // Three real roots. 275 | d1 = R / Math.sqrt(Q3); 276 | 277 | const theta = Math.acos(d1) / 3.0; 278 | 279 | sQ = -2.0 * Math.sqrt(Q); 280 | 281 | roots[0] = sQ * Math.cos(theta) - an; 282 | roots[1] = sQ * Math.cos(theta + _TWO_M_PI_3) - an; 283 | roots[2] = sQ * Math.cos(theta + _FOUR_M_PI_3) - an; 284 | return 3; 285 | } 286 | 287 | /** 288 | * Solve the quartic equation: a + b*x + c*x^2 + d*x^3 + e*x^4 = 0 289 | * @param {number} a 290 | * @param {number} b 291 | * @param {number} c 292 | * @param {number} d 293 | * @param {number} e 294 | * @param {Array} roots The resulting roots 295 | * @return {number} The number of roots found 296 | */ 297 | export function solveQuartic(a, b, c, d, e, roots) { 298 | // Make sure the quartic has a leading coefficient of 1.0 299 | if (!a) { 300 | return solveCubic(b, c, d, e, roots); 301 | } 302 | 303 | const c1 = b / a; 304 | const c2 = c / a; 305 | const c3 = d / a; 306 | const c4 = e / a; 307 | 308 | // Compute the cubic resolvant 309 | const c12 = c1 * c1; 310 | let p = -0.375 * c12 + c2; 311 | const q = 0.125 * c12 * c1 - 0.5 * c1 * c2 + c3; 312 | const r = -0.01171875 * c12 * c12 + 0.0625 * c12 * c2 - 0.25 * c1 * c3 + c4; 313 | 314 | const cubic0 = 1.0; 315 | const cubic1 = -0.5 * p; 316 | const cubic2 = -r; 317 | const cubic3 = 0.5 * r * p - 0.125 * q * q; 318 | 319 | const cubicRoots = roots; 320 | let numRoots = solveCubic(cubic0, cubic1, cubic2, cubic3, roots); 321 | 322 | if (numRoots <= 0) { 323 | return 0; 324 | } 325 | 326 | const z = cubicRoots[0]; 327 | 328 | let d1 = 2.0 * z - p; 329 | let d2; 330 | if (d1 < 0.0) { 331 | if (d1 <= -Epsilon) { 332 | return 0; 333 | } 334 | d1 = 0.0; 335 | } 336 | 337 | if (d1 < Epsilon) { 338 | d2 = z * z - r; 339 | 340 | if (d2 < 0.0) { 341 | return 0; 342 | } 343 | 344 | d2 = Math.sqrt(d2); 345 | } else { 346 | d1 = Math.sqrt(d1); 347 | d2 = 0.5 * q / d1; 348 | } 349 | 350 | // Set up useful values for the quadratic factors 351 | const q1 = d1 * d1; 352 | const q2 = -0.25 * c1; 353 | 354 | numRoots = 0; 355 | 356 | // Solve the first quadratic 357 | p = q1 - 4.0 * (z - d2); 358 | if (!p) { 359 | roots[numRoots++] = -0.5 * d1 - q2; 360 | } else { 361 | if (p > 0) { 362 | p = Math.sqrt(p); 363 | roots[numRoots++] = -0.5 * (d1 + p) + q2; 364 | roots[numRoots++] = -0.5 * (d1 - p) + q2; 365 | } 366 | } 367 | 368 | // Solve the second quadratic 369 | p = q1 - 4.0 * (z + d2); 370 | 371 | if (p == 0) { 372 | roots[numRoots++] = 0.5 * d1 - q2; 373 | } else { 374 | if (p > 0) { 375 | p = Math.sqrt(p); 376 | roots[numRoots++] = 0.5 * (d1 + p) + q2; 377 | roots[numRoots++] = 0.5 * (d1 - p) + q2; 378 | } 379 | } 380 | 381 | return numRoots; 382 | } 383 | 384 | /** 385 | * Returns the sign of x, indicating whether x is positive, negative, or zero. 386 | * @function sign 387 | * @param {number} x 388 | * @return {number} 389 | */ 390 | export const sign = Math.sign; 391 | /** 392 | * Returns the square root of x. 393 | * @function sqrt 394 | * @param {number} x 395 | * @return {number} 396 | */ 397 | export const sqrt = Math.sqrt; 398 | /** 399 | * Returns the natural logarithm of x. 400 | * @function log 401 | * @param {number} x 402 | * @return {number} 403 | */ 404 | export const log = Math.log; 405 | /** 406 | * Returns the sine of x. 407 | * @function sin 408 | * @param {number} x 409 | * @return {number} 410 | */ 411 | export const sin = Math.sin; 412 | /** 413 | * Returns the cosine of x. 414 | * @function cos 415 | * @param {number} x 416 | * @return {number} 417 | */ 418 | export const cos = Math.cos; 419 | /** 420 | * Returns the tangent of x. 421 | * @function tan 422 | * @param {number} x 423 | * @return {number} 424 | */ 425 | export const tan = Math.tan; 426 | /** 427 | * Returns the arcsine of x. 428 | * @function asin 429 | * @param {number} x 430 | * @return {number} 431 | */ 432 | export const asin = Math.asin; 433 | /** 434 | * Returns the arccosine of x. 435 | * @function acos 436 | * @param {number} x 437 | * @return {number} 438 | */ 439 | export const acos = Math.acos; 440 | /** 441 | * Returns the arctangent of x. 442 | * @function sqrt 443 | * @param {number} x 444 | * @return {number} 445 | */ 446 | export const atan = Math.atan; 447 | /** 448 | * Returns the largest integer less than or equal to x. 449 | * @function floor 450 | * @param {number} x 451 | * @return {number} 452 | */ 453 | export const floor = Math.floor; 454 | /** 455 | * Returns the smallest integer greater than or equal to x. 456 | * @function ceil 457 | * @param {number} x 458 | * @return {number} 459 | */ 460 | export const ceil = Math.ceil; 461 | /** 462 | * Returns the absolute value of x. 463 | * @function abs 464 | * @param {number} x 465 | * @retrn {number} 466 | */ 467 | export const abs = Math.abs; 468 | 469 | /** 470 | * @property {number} MaxValue 471 | * General value to consider as a maximum float value. 472 | */ 473 | export const MaxValue = 1.0e30; 474 | /** 475 | * @property {number} Epsilon 476 | * General value to consider as an epsilon for float comparisons. 477 | */ 478 | export const Epsilon = 1.0e-6; 479 | /** 480 | * @property {number} PI 481 | * 3.1415... 482 | */ 483 | export const PI = Math.PI; 484 | /** 485 | * @property {number} PI_2 486 | * PI divided by 2 487 | */ 488 | export const PI_2 = Math.PI / 2; 489 | /** 490 | * @property {number} PI2 491 | * PI multiplied by 2 492 | */ 493 | export const PI2 = Math.PI * 2; 494 | /** 495 | * @property {number} DegreeToRadian 496 | * Conversion value for degrees to radians. 497 | */ 498 | export const DegreeToRadian = Math.PI / 180; 499 | /** 500 | * @property {number} RadianToDegree 501 | * Conversion value for radians to degrees. 502 | */ 503 | export const RadianToDegree = 180 / Math.PI; 504 | 505 | /** 506 | * Axis direction 507 | * @enum {number} 508 | * @readonly 509 | * @example 510 | * Axis.X: 0 511 | * Axis.Y: 1 512 | * Axis.Z: 2 513 | */ 514 | export const Axis = { 515 | X: 0, 516 | Y: 1, 517 | Z: 2 518 | }; 519 | 520 | /** 521 | * Plane or frustum clip test result type. 522 | * @readonly 523 | * @enum {number} 524 | * @example 525 | * ClipTest.Inside: 0 // The object is completely inside the frustum or in front of the plane. 526 | * ClipTest.Outside: 1 // The object is completely outside the frustum or behind the plane. 527 | * ClipTest.Overlap: 2 // The object overlaps the plane or frustum. 528 | */ 529 | export const ClipTest = { 530 | Inside: 0, 531 | Outside: 1, 532 | Overlap: 2 533 | }; 534 | 535 | /** 536 | * Order in which to apply euler rotations for a transformation. 537 | * @readonly 538 | * @enum {number} 539 | * @example 540 | * RotationOrder.Default: RotationOrder.ZYX 541 | * RotationOrder.ZYX: 0 542 | * RotationOrder.XYZ: 1, 543 | * RotationOrder.XZY: 2, 544 | * RotationOrder.YZX: 3, 545 | * RotationOrder.YXZ: 4, 546 | * RotationOrder.ZXY: 5, 547 | * 548 | */ 549 | export const RotationOrder = { 550 | Default: 0, // Default is ZYX 551 | ZYX: 0, 552 | XYZ: 1, 553 | XZY: 2, 554 | YZX: 3, 555 | YXZ: 4, 556 | ZXY: 5 557 | }; 558 | 559 | const _TWO_M_PI_3 = 2.0943951023931954923084; 560 | const _FOUR_M_PI_3 = 4.1887902047863909846168; 561 | -------------------------------------------------------------------------------- /js/world.js: -------------------------------------------------------------------------------- 1 | import { BiomeAttributes, Lode } from "./biome_attributes.js"; 2 | import { Vector3 } from "./math/vector3.js"; 3 | import { Vector4 } from "./math/vector4.js"; 4 | import { Globals } from "./globals.js"; 5 | import { WorldData } from "./world_data.js"; 6 | import { Random } from "./math/random.js"; 7 | import { BlockType } from "./block_type.js"; 8 | import { ChunkCoord } from "./chunk_coord.js"; 9 | import { VoxelData } from "./voxel_data.js"; 10 | import { Chunk } from "./chunk.js"; 11 | import { Noise } from "./math/noise.js"; 12 | import { Transform } from "./transform.js"; 13 | 14 | export class World extends Transform { 15 | constructor() { 16 | super(); 17 | Globals.world = this; 18 | 19 | this.biomes = [ 20 | new BiomeAttributes({ 21 | name: "Grasslands", 22 | offset: 1234, 23 | scale: 0.042, 24 | terrainHeight: 22, 25 | terrainScale: 0.15, 26 | surfaceBlock: 3, 27 | subSurfaceBlock: 5, 28 | majorFloraIndex: 0, 29 | majorFloraZoneScale: 1.3, 30 | majorFloraZoneThreshold: 0.6, 31 | majorFloraPlacementScale: 15, 32 | majorFloraPlacementThreshold: 0.8, 33 | placeMajorFlora: 1, 34 | maxHeight: 12, 35 | minHeight: 5, 36 | lodes: [ 37 | new Lode({ 38 | name: "Dirt", 39 | blockID: 5, 40 | minHeight: 1, 41 | maxHeight: 255, 42 | scale: 0.1, 43 | threshold: 0.5, 44 | noiseOffset: 0 45 | }), 46 | new Lode({ 47 | name: "Sand", 48 | blockID: 4, 49 | minHeight: 30, 50 | maxHeight: 60, 51 | scale: 0.2, 52 | threshold: 0.6, 53 | noiseOffset: 500 54 | }), 55 | new Lode({ 56 | name: "Caves", 57 | blockID: 0, 58 | minHeight: 5, 59 | maxHeight: 60, 60 | scale: 0.1, 61 | threshold: 0.55, 62 | noiseOffset: 43534 63 | }) 64 | ] 65 | }), 66 | new BiomeAttributes({ 67 | name: "Desert", 68 | offset: 6545, 69 | scale: 0.058, 70 | terrainHeight: 10, 71 | terrainScale: 0.05, 72 | surfaceBlock: 4, 73 | subSurfaceBlock: 4, 74 | majorFloraIndex: 1, 75 | majorFloraZoneScale: 1.06, 76 | majorFloraZoneThreshold: 0.75, 77 | majorFloraPlacementScale: 7.5, 78 | majorFloraPlacementThreshold: 0.8, 79 | placeMajorFlora: 1, 80 | maxHeight: 12, 81 | minHeight: 5, 82 | lodes: [ 83 | new Lode({ 84 | name: "Dirt", 85 | blockID: 5, 86 | minHeight: 1, 87 | maxHeight: 255, 88 | scale: 0.1, 89 | threshold: 0.5, 90 | noiseOffset: 0 91 | }), 92 | new Lode({ 93 | name: "Sand", 94 | blockID: 4, 95 | minHeight: 30, 96 | maxHeight: 60, 97 | scale: 0.2, 98 | threshold: 0.6, 99 | noiseOffset: 500 100 | }), 101 | new Lode({ 102 | name: "Caves", 103 | blockID: 0, 104 | minHeight: 5, 105 | maxHeight: 60, 106 | scale: 0.1, 107 | threshold: 0.55, 108 | noiseOffset: 43534 109 | }) 110 | ] 111 | }), 112 | new BiomeAttributes({ 113 | name: "Forest", 114 | offset: 87544, 115 | scale: 0.17, 116 | terrainHeight: 80, 117 | terrainScale: 0.3, 118 | surfaceBlock: 5, 119 | subSurfaceBlock: 5, 120 | majorFloraIndex: 0, 121 | majorFloraZoneScale: 1.3, 122 | majorFloraZoneThreshold: 0.384, 123 | majorFloraPlacementScale: 5, 124 | majorFloraPlacementThreshold: 0.755, 125 | placeMajorFlora: 1, 126 | maxHeight: 12, 127 | minHeight: 5, 128 | lodes: [ 129 | new Lode({ 130 | name: "Dirt", 131 | blockID: 5, 132 | minHeight: 1, 133 | maxHeight: 255, 134 | scale: 0.1, 135 | threshold: 0.5, 136 | noiseOffset: 0 137 | }), 138 | new Lode({ 139 | name: "Sand", 140 | blockID: 4, 141 | minHeight: 30, 142 | maxHeight: 60, 143 | scale: 0.2, 144 | threshold: 0.6, 145 | noiseOffset: 500 146 | }), 147 | new Lode({ 148 | name: "Caves", 149 | blockID: 0, 150 | minHeight: 5, 151 | maxHeight: 60, 152 | scale: 0.1, 153 | threshold: 0.55, 154 | noiseOffset: 43534 155 | }) 156 | ] 157 | }) 158 | ]; 159 | 160 | this.worldData = new WorldData({ name: "World", seed: 2147483647 }); 161 | 162 | this.settings = { 163 | "loadDistance": 4, 164 | "viewDistance": 2, 165 | "clouds": 2, 166 | "enableAnimatedChunks": false, 167 | "mouseSensitivity": 1.49643 168 | }; 169 | 170 | this.random = new Random(this.worldData.seed); 171 | 172 | this.spawnPosition = new Vector3(); 173 | 174 | this.blockTypes = [ 175 | new BlockType({ name: "Air", isSolid: false, textures: [0, 0, 0, 0, 0, 0], renderNeighborFaces: true, opacity: 0 }), 176 | new BlockType({ name: "Bedrock", isSolid: true, textures: [9, 9, 9, 9, 9, 9], renderNeighborFaces: false, opacity: 15 }), 177 | new BlockType({ name: "Stone", isSolid: true, textures: [0, 0, 0, 0, 0, 0], renderNeighborFaces: false, opacity: 15 }), 178 | new BlockType({ name: "Grass", isSolid: true, textures: [2, 2, 7, 1, 2, 2], renderNeighborFaces: false, opacity: 15 }), 179 | new BlockType({ name: "Sand", isSolid: true, textures: [10, 10, 10, 10, 10, 10], renderNeighborFaces: false, opacity: 15 }), 180 | new BlockType({ name: "Dirt", isSolid: true, textures: [1, 1, 1, 1, 1, 1], renderNeighborFaces: false, opacity: 15 }), 181 | new BlockType({ name: "Wood", isSolid: true, textures: [5, 5, 6, 6, 5, 5], renderNeighborFaces: false, opacity: 15 }), 182 | new BlockType({ name: "Planks", isSolid: true, textures: [4, 4, 4, 4, 4, 4], renderNeighborFaces: false, opacity: 15 }), 183 | new BlockType({ name: "Bricks", isSolid: true, textures: [11, 11, 11, 11, 11, 11], renderNeighborFaces: false, opacity: 15 }), 184 | new BlockType({ name: "Cobblestone", isSolid: true, textures: [8, 8, 8, 8, 8, 8], renderNeighborFaces: false, opacity: 15 }), 185 | new BlockType({ name: "Glass", isSolid: true, textures: [3, 3, 3, 3, 3, 3], renderNeighborFaces: true, opacity: 0 }), 186 | new BlockType({ name: "Leaves", isSolid: true, textures: [16, 16, 16, 16, 16, 16], renderNeighborFaces: true, opacity: 5 }), 187 | new BlockType({ name: "Cactus", isSolid: true, textures: [18, 18, 19, 19, 18, 18], renderNeighborFaces: true, opacity: 15 }), 188 | new BlockType({ name: "Cactus Top", isSolid: true, textures: [18, 18, 17, 19, 18, 18], renderNeighborFaces: true, opacity: 15 }) 189 | ]; 190 | 191 | this._chunks = new Map(); 192 | this.activeChunks = []; 193 | this.chunksToDraw = []; 194 | this.chunksToUpdate = []; 195 | this.modifications = []; 196 | this.applyingModifications = false; 197 | 198 | this.playerChunkCoord = new ChunkCoord(); 199 | this.playerLastChunkCoord = new ChunkCoord(); 200 | 201 | this.night = new Vector4(0, 0, 77/255, 1); 202 | this.day = new Vector4(0, 1, 250/255, 1); 203 | this.globalLightLevel = 1; 204 | } 205 | 206 | get camera() { return Globals.camera; } 207 | 208 | get player() { return Globals.player; } 209 | 210 | start() { 211 | this.random = new Random(this.seed); 212 | 213 | this.loadWorld(); 214 | this.setGlobalLightLevel(); 215 | 216 | this.spawnPosition.setFrom(VoxelData.WorldCenter, VoxelData.ChunkHeight - 75, 217 | VoxelData.WorldCenter); 218 | 219 | this.player.position = this.spawnPosition; 220 | 221 | this.checkViewDistance(); 222 | 223 | this.getChunkCoordFromPosition(this.player.position, this.playerLastChunkCoord); 224 | } 225 | 226 | update(device) { 227 | this.getChunkCoordFromPosition(this.player.position, this.playerChunkCoord); 228 | 229 | // Only update the chunks if the player has moved to a new chunk 230 | if (!this.playerChunkCoord.equals(this.playerLastChunkCoord)) { 231 | this.checkViewDistance(); 232 | } 233 | 234 | //while (this.chunksToDraw.length) { 235 | if (this.chunksToDraw.length) { 236 | this.chunksToDraw.pop().createMesh(device); 237 | } 238 | 239 | if (!this.applyingModifications) { 240 | this.applyModifications(); 241 | } 242 | 243 | if (this.chunksToUpdate.length) { 244 | this.updateChunks(); 245 | } 246 | } 247 | 248 | getChunk(x, z) { 249 | if (!this._chunks.get(x)) { 250 | return null; 251 | } 252 | return this._chunks.get(x).get(z); 253 | } 254 | 255 | setChunk(x, z, chunk) { 256 | let m = this._chunks.get(x); 257 | if (!m) { 258 | m = new Map(); 259 | this._chunks.set(x, m); 260 | } 261 | m.set(z, chunk); 262 | } 263 | 264 | getChunkCoordFromPosition(pos, out) { 265 | out[0] = Math.floor(pos[0] / VoxelData.ChunkWidth); 266 | out[1] = Math.floor(pos[2] / VoxelData.ChunkWidth); 267 | return out; 268 | } 269 | 270 | getChunkFromPosition(pos) { 271 | const x = Math.floor(pos[0] / VoxelData.ChunkWidth); 272 | const z = Math.floor(pos[2] / VoxelData.ChunkWidth); 273 | return this.getChunk(x, z); 274 | } 275 | 276 | loadWorld() { 277 | //const hw = 10; 278 | const hw = VoxelData.WorldSizeInChunks / 2; 279 | const distance = this.settings.loadDistance; 280 | for (let x = hw - distance; x < hw + distance; ++x) { 281 | for (let z = hw - distance; z < hw + distance; ++z) { 282 | this.worldData.loadChunk(x, z); 283 | } 284 | } 285 | } 286 | 287 | addChunkToUpdate(chunk, insert) { 288 | insert = insert || false; 289 | if (!this.chunksToUpdate.includes(chunk)) { 290 | if (insert) { 291 | this.chunksToUpdate.unshift(chunk); 292 | } else { 293 | this.chunksToUpdate.push(chunk); 294 | } 295 | } 296 | } 297 | 298 | updateChunks() { 299 | const chunk = this.chunksToUpdate.shift(); 300 | if (!chunk) { 301 | return; 302 | } 303 | chunk.updateChunk(); 304 | if (!this.activeChunks.includes(chunk.coord)) { 305 | this.activeChunks.push(chunk.coord); 306 | } 307 | } 308 | 309 | applyModifications() { 310 | this.applyingModifications = true; 311 | 312 | while (this.modifications.length) { 313 | const queue = this.modifications.pop(); 314 | for (let i = queue.length - 1; i >= 0; --i) { 315 | const v = queue[i]; 316 | const p = v.position; 317 | this.worldData.setVoxelID(p[0], p[1], p[2], v.id); 318 | } 319 | } 320 | 321 | this.applyingModifications = false; 322 | } 323 | 324 | setGlobalLightLevel() { 325 | //this.material.setProperty("minGlobalLightLevel", VoxelData.minLightLevel); 326 | //this.material.setProperty("maxGlobalLightLevel", VoxelData.maxLightLevel); 327 | //this.material.setProperty("globalLightLevel", this.globalLightLevel); 328 | 329 | this.camera.backroundColor = 330 | Vector4.lerp(this.night, this.day, this.globalLightLevel); 331 | } 332 | 333 | checkViewDistance() { 334 | //this.clouds.updateClouds(); 335 | 336 | this.playerLastChunkCoord.set(this.playerChunkCoord); 337 | 338 | const playerPos = this.player.position; 339 | const chunkX = Math.floor(playerPos.x / VoxelData.ChunkWidth); 340 | const chunkZ = Math.floor(playerPos.z / VoxelData.ChunkWidth); 341 | 342 | // clone the activeChunks array 343 | let previouslyActiveChunks = this.activeChunks.slice(0); 344 | 345 | this.activeChunks.length = 0; 346 | 347 | const viewDistance = this.settings.viewDistance; 348 | 349 | for (let x = chunkX - viewDistance; x <= chunkX + viewDistance; ++x) { 350 | for (let z = chunkZ - viewDistance; z <= chunkZ + viewDistance; ++z) { 351 | // If the chunk is within the world bounds and it has not been created. 352 | if (this.isChunkInWorld(x, z)) { 353 | let chunk = this.getChunk(x, z); 354 | if (!chunk) { 355 | chunk = new Chunk(new ChunkCoord(x, z), this); 356 | this.setChunk(x, z, chunk); 357 | } 358 | 359 | chunk.isActive = true; 360 | this.activeChunks.push(chunk.coord); 361 | } 362 | 363 | // Check if this chunk was already in the active chunks list. 364 | for (let i = previouslyActiveChunks.length - 1; i >= 0; --i) { 365 | if (previouslyActiveChunks[i].x == x && previouslyActiveChunks[i].z == z) { 366 | previouslyActiveChunks.splice(i); 367 | } 368 | } 369 | } 370 | } 371 | 372 | for (let i = 0, l = previouslyActiveChunks.length; i < l; ++i) { 373 | const coord = previouslyActiveChunks[i]; 374 | const chunk = this.getChunk(coord.x, coord.z); 375 | if (chunk) 376 | chunk.isActive = false; 377 | } 378 | } 379 | 380 | isChunkInWorld(/*x, z*/) { 381 | // An "infinite" world. 382 | return true; 383 | } 384 | 385 | isVoxelInWorld(x, y/*, z*/) { 386 | // An "infinite" world, at least in X and Z 387 | return y >= 0 && y < VoxelData.ChunkHeight; 388 | } 389 | 390 | checkForVoxel(x, y, z) { 391 | const voxel = this.worldData.getVoxelID(x, y, z); 392 | return this.blockTypes[voxel].isSolid; 393 | } 394 | 395 | getVoxelID(x, y, z) { 396 | return this.worldData.getVoxelID(x, y, z); 397 | } 398 | 399 | calculateVoxel(x, y, z) { 400 | const yPos = Math.floor(y); 401 | 402 | // If outside the world, return air 403 | if (!this.isVoxelInWorld(x, y, z)) { 404 | return 0; 405 | } 406 | 407 | // If bottom block of chunk, return bedrock. 408 | if (yPos == 0) { 409 | return 1; 410 | } 411 | 412 | let solidGroundHeight = 42; 413 | let sumOfHeights = 0; 414 | let count = 0; 415 | let strongestWeight = 0; 416 | let strongestBiomeIndex = 0; 417 | 418 | for (let i = 0, l = this.biomes.length; i < l; ++i) { 419 | const biome = this.biomes[i]; 420 | const weight = World.get2DPerlin(x, z, biome.offset, biome.scale); 421 | 422 | // Keep track of which weight is strongest 423 | if (weight > strongestWeight) { 424 | strongestWeight = weight; 425 | strongestBiomeIndex = i; 426 | } 427 | 428 | const height = biome.terrainHeight * 429 | World.get2DPerlin(x, z, 0, biome.terrainScale) * weight; 430 | 431 | if (height > 0) { 432 | sumOfHeights += height; 433 | count++; 434 | } 435 | } 436 | 437 | // Set biome to the one with the strongest weight 438 | const biome = this.biomes[strongestBiomeIndex]; 439 | 440 | // Get the average of the heights 441 | sumOfHeights /= count; 442 | 443 | const terrainHeight = Math.floor(sumOfHeights + solidGroundHeight); 444 | 445 | // Basic terrain pass 446 | let voxelValue = 0; 447 | 448 | if (yPos == terrainHeight) { 449 | voxelValue = biome.surfaceBlock; 450 | } else if (yPos < terrainHeight && yPos > terrainHeight - 4) { 451 | voxelValue = biome.subSurfaceBlock; 452 | } else if (yPos > terrainHeight) { 453 | return 0; 454 | } else { 455 | voxelValue = 2; 456 | } 457 | 458 | // Second pass 459 | if (voxelValue == 2) { 460 | for (let i = 0, l = biome.lodes.length; i < l; ++i) { 461 | const lode = biome.lodes[i]; 462 | if (yPos > lode.minHeight && yPos < lode.maxHeight) { 463 | if (World.get3DPerlin(x, y, z, lode.noiseOffset, lode.scale, lode.threshold)) { 464 | voxelValue = lode.blockID; 465 | } 466 | } 467 | } 468 | } 469 | 470 | return voxelValue; 471 | } 472 | 473 | static get2DPerlin(x, y, offset, scale) { 474 | return Noise.perlinNoise2((x + 0.1) / VoxelData.ChunkWidth * scale + offset, 475 | (y + 0.1) / VoxelData.ChunkWidth * scale + offset); 476 | } 477 | 478 | static get3DPerlin(x, y, z, offset, scale, threshold) { 479 | x = (x + offset + 0.1) * scale; 480 | y = (y + offset + 0.1) * scale; 481 | z = (z + offset + 0.1) * scale; 482 | const AB = Noise.perlinNoise2(x, y); 483 | const BC = Noise.perlinNoise2(y, z); 484 | const AC = Noise.perlinNoise2(x, z); 485 | const BA = Noise.perlinNoise2(y, x); 486 | const CB = Noise.perlinNoise2(z, y); 487 | const CA = Noise.perlinNoise2(z, x); 488 | return ((AB + BC + AC + BA + CB + CA) / 6) > threshold; 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /js/math/vector3.js: -------------------------------------------------------------------------------- 1 | import { isNumber, equals, clamp } from "./math.js"; 2 | import { Vector2 } from "./vector2.js"; 3 | 4 | /** 5 | * A 3 dimensioanl vector. 6 | * @extends Float32Array 7 | * @category Math 8 | */ 9 | export class Vector3 extends Float32Array { 10 | /** 11 | * @param {*} arguments... Variable arguments 12 | * @example 13 | * Vector3() // [0, 0, 0] 14 | * Vector3(1) // [1, 1, 1] 15 | * Vector3(1, 2, 3) // [1, 2, 3] 16 | * Vector3([1, 2, 3]) // [1, 2, 3] 17 | * Vector3(Vector3) // Copy the vector 18 | */ 19 | constructor() { 20 | if (arguments.length) { 21 | if (arguments.length == 1 && !arguments[0]) { 22 | super(3); 23 | } else if (arguments.length == 1 && isNumber(arguments[0])) { 24 | super(3); 25 | const x = arguments[0]; 26 | this[0] = x; 27 | this[1] = x; 28 | this[2] = x; 29 | } else if (arguments.length == 3 && isNumber(arguments[0])) { 30 | super(3); 31 | const x = arguments[0]; 32 | const y = arguments[1]; 33 | const z = arguments[2]; 34 | this[0] = x; 35 | this[1] = y; 36 | this[2] = z; 37 | } else { 38 | super(...arguments); 39 | } 40 | } else { 41 | super(3); 42 | } 43 | } 44 | 45 | /** 46 | * Create a copy of this vector. 47 | * @returns {Vector3} 48 | */ 49 | clone() { 50 | return new Vector3(this); 51 | } 52 | 53 | /** 54 | * Set the components of this vector. 55 | * @param {number|Array} x 56 | * @param {number} [y] 57 | * @param {number} [z] 58 | * @returns {Vector3} Returns this vector. 59 | */ 60 | setFrom(x, y, z) { 61 | if (y === undefined) { 62 | this[0] = x[0]; 63 | this[1] = x[1]; 64 | this[2] = x[2]; 65 | } else { 66 | this[0] = x; 67 | this[1] = y; 68 | this[2] = z; 69 | } 70 | return this; 71 | } 72 | 73 | /** 74 | * Set all components to 0. 75 | * @returns {Vector3} Returns this vector. 76 | */ 77 | setZero() { 78 | this[0] = 0; 79 | this[1] = 0; 80 | this[2] = 0; 81 | return this; 82 | } 83 | 84 | /** 85 | * Convert the vector to an Array. 86 | * @returns {Array} 87 | */ 88 | toArray() { return [this[0], this[1], this[2]]; } 89 | 90 | /** 91 | * Get the string representation of the vector. 92 | * @returns {String} 93 | */ 94 | toString() { return `[${this.x}, ${this.y}, ${this.z}]`; } 95 | 96 | /** 97 | * @property {number} x The x component. 98 | */ 99 | get x() { return this[0]; } 100 | 101 | set x(v) { this[0] = v; } 102 | 103 | /** 104 | * @property {number} y The y component. 105 | */ 106 | get y() { return this[1]; } 107 | 108 | set y(v) { this[1] = v; } 109 | 110 | /** 111 | * @property {number} z The z component. 112 | */ 113 | get z() { return this[2]; } 114 | 115 | set z(v) { this[2] = v; } 116 | 117 | /** 118 | * Remap the channels of this vector. 119 | * @param {number} xi The index of the channel to set x to. 120 | * @param {number} yi The index of the channel to set y to. 121 | * @param {number} zi The index of the channel to set z to. 122 | * @return {Vector3} 123 | * @example 124 | * remap(1, 2, 0) // returns [y, z, x] 125 | */ 126 | remap(xi, yi, zi) { 127 | const x = this[clamp(xi, 0, 2)]; 128 | const y = this[clamp(yi, 0, 2)]; 129 | const z = this[clamp(zi, 0, 2)]; 130 | this[0] = x; 131 | this[1] = y; 132 | this[2] = z; 133 | return this; 134 | } 135 | 136 | /** 137 | * Map this vector to a new vector. 138 | * @param {number} arguments... The variable component indices to map. 139 | * @returns {number|Vector2|Vector3} 140 | * @example 141 | * map(1) // Returns a number with the y value of this vector. 142 | * map(0, 2) // Returns a Vector2 as [x, z]. 143 | * map(2, 1, 0) // Returns a Vector3 with as [z, y, x]. 144 | */ 145 | map() { 146 | switch (arguments.length) { 147 | case 3: 148 | return new Vector3(this[arguments[0]], this[arguments[1]], this[arguments[2]]); 149 | case 2: 150 | return new Vector2(this[arguments[0]], this[arguments[1]]); 151 | case 1: 152 | return this[arguments[0]]; 153 | } 154 | return null; 155 | } 156 | 157 | /** 158 | * Returns the sum of the components, x + y + z. 159 | * @returns {number} 160 | */ 161 | sum() { 162 | return this[0] + this[1] + this[2]; 163 | } 164 | 165 | /** 166 | * Returns the length of the vector. 167 | * @returns {number} 168 | */ 169 | getLength() { 170 | return Math.sqrt(this[0] * this[0] + this[1] * this[1] + this[2] * this[2]); 171 | } 172 | 173 | /** 174 | * Returns the squared length of the vector. 175 | * @returns {number} 176 | */ 177 | getLengthSquared() { 178 | return this[0] * this[0] + this[1] * this[1] + this[2] * this[2]; 179 | } 180 | 181 | /** 182 | * Normalize the vector. 183 | * @returns {Vector3} Returns self. 184 | */ 185 | normalize() { 186 | const out = this; 187 | const l = this.getLength(); 188 | if (!l) { 189 | return out; 190 | } 191 | out[0] = this[0] / l; 192 | out[1] = this[1] / l; 193 | out[2] = this[2] / l; 194 | return out; 195 | } 196 | 197 | /** 198 | * Negate the vector, as [-x, -y, -z]. 199 | * @returns {Vector3} Returns self. 200 | */ 201 | negate() { 202 | const out = this; 203 | out[0] = -this[0]; 204 | out[1] = -this[1]; 205 | out[2] = -this[2]; 206 | return out; 207 | } 208 | 209 | /** 210 | * Make each component its absolute value, [abs(x), abs(y), abs(z)] 211 | * @returns {Vector3} Returns self. 212 | */ 213 | abs() { 214 | const out = this; 215 | out[0] = Math.abs(this[0]); 216 | out[1] = Math.abs(this[1]); 217 | out[2] = Math.abs(this[2]); 218 | return out; 219 | } 220 | 221 | /** 222 | * Add a vector to this, this + b. 223 | * @param {Vector3} b 224 | * @returns {Vector3} Returns self. 225 | */ 226 | add(b) { 227 | const out = this; 228 | out[0] = this[0] + b[0]; 229 | out[1] = this[1] + b[1]; 230 | out[2] = this[2] + b[2]; 231 | return out; 232 | } 233 | 234 | /** 235 | * Subtract a vector from this, this - b. 236 | * @param {Vector3} b 237 | * @returns {Vector3} Returns self. 238 | */ 239 | subtract(b) { 240 | const out = this; 241 | out[0] = this[0] - b[0]; 242 | out[1] = this[1] - b[1]; 243 | out[2] = this[2] - b[2]; 244 | return out; 245 | } 246 | 247 | /** 248 | * Multiply a vector with this, this * b. 249 | * @param {Vector3} b 250 | * @returns {Vector3} Returns self. 251 | */ 252 | multiply(b) { 253 | const out = this; 254 | out[0] = this[0] * b[0]; 255 | out[1] = this[1] * b[1]; 256 | out[2] = this[2] * b[2]; 257 | return out; 258 | } 259 | 260 | /** 261 | * Divide a vector to this, this / b. 262 | * @param {Vector3} b 263 | * @returns {Vector3} Returns self. 264 | */ 265 | divide(b) { 266 | const out = this; 267 | out[0] = b[0] ? this[0] / b[0] : 0; 268 | out[1] = b[1] ? this[1] / b[1] : 0; 269 | out[2] = b[2] ? this[2] / b[2] : 0; 270 | return out; 271 | } 272 | 273 | /** 274 | * Scale the vector by a number, this * s. 275 | * @param {number} s 276 | * @returns {Vector3} 277 | */ 278 | scale(s) { 279 | const out = this; 280 | out[0] = this[0] * s; 281 | out[1] = this[1] * s; 282 | out[2] = this[2] * s; 283 | return out; 284 | } 285 | 286 | rotateX(origin, rad) { 287 | const out = this; 288 | const a = this; 289 | const b = origin; 290 | // Translate point to the origin 291 | const p = [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; 292 | const r = [p[0], 293 | p[1] * Math.cos(rad) - p[2] * Math.sin(rad), 294 | p[1] * Math.sin(rad) + p[2] * Math.cos(rad)]; 295 | 296 | // translate to correct position 297 | out[0] = r[0] + b[0]; 298 | out[1] = r[1] + b[1]; 299 | out[2] = r[2] + b[2]; 300 | 301 | return out; 302 | } 303 | 304 | rotateY(origin, rad) { 305 | const out = this; 306 | const a = this; 307 | const b = origin; 308 | // Translate point to the origin 309 | const p = [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; 310 | const r = [p[2] * Math.sin(rad) + p[0] * Math.cos(rad), 311 | p[1], 312 | p[2] * Math.cos(rad) - p[0] * Math.sin(rad)]; 313 | 314 | // translate to correct position 315 | out[0] = r[0] + b[0]; 316 | out[1] = r[1] + b[1]; 317 | out[2] = r[2] + b[2]; 318 | 319 | return out; 320 | } 321 | 322 | rotateZ(origin, rad) { 323 | const out = this; 324 | const a = this; 325 | const b = origin; 326 | // Translate point to the origin 327 | const p = [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; 328 | 329 | const r = [p[0] * Math.cos(rad) - p[1] * Math.sin(rad), 330 | p[0] * Math.sin(rad) + p[1] * Math.cos(rad), 331 | p[2]]; 332 | 333 | // translate to correct position 334 | out[0] = r[0] + b[0]; 335 | out[1] = r[1] + b[1]; 336 | out[2] = r[2] + b[2]; 337 | 338 | return out; 339 | } 340 | 341 | /** 342 | * Reflect the direction vector against the normal. 343 | * @param {Vector3} direction 344 | * @param {Vector3} normal 345 | * @param {Vector3} [out] 346 | * @return {Vector3} 347 | */ 348 | static reflect(direction, normal, out) { 349 | out = out || new Vector3(); 350 | const s = -2 * Vector3.dot(normal, direction); 351 | out[0] = normal[0] * s + direction[0]; 352 | out[1] = normal[1] * s + direction[1]; 353 | out[2] = normal[2] * s + direction[2]; 354 | return out; 355 | } 356 | 357 | /** 358 | * Calculate the refraction vector against the surface normal, from IOR k1 to IOR k2. 359 | * @param {Vector3} incident Specifies the incident vector 360 | * @param {Vector3} normal Specifies the normal vector 361 | * @param {number} eta Specifies the ratio of indices of refraction 362 | * @param {Vector3} [out] Optional output storage 363 | * @return {Vector3} 364 | */ 365 | static refract(incident, normal, eta, out) { 366 | out = out || new Vector3(); 367 | 368 | // If the two index's of refraction are the same, then 369 | // the new vector is not distorted from the old vector. 370 | if (equals(eta, 1.0)) { 371 | out[0] = incident[0]; 372 | out[1] = incident[1]; 373 | out[2] = incident[2]; 374 | return out; 375 | } 376 | 377 | const dot = -Vector3.dot(incident, normal); 378 | let cs2 = 1.0 - eta * eta * (1.0 - dot * dot); 379 | 380 | // if cs2 < 0, then the new vector is a perfect reflection of the old vector 381 | if (cs2 < 0.0) { 382 | Vector3.reflect(incident, normal, out); 383 | return out; 384 | } 385 | 386 | cs2 = eta * dot - Math.sqrt(cs2); 387 | 388 | out[0] = normal[0] + (incident[0] * eta) + (normal[0] * cs2); 389 | out[1] = normal[1] + (incident[1] * eta) + (normal[1] * cs2); 390 | out[2] = normal[2] + (incident[2] * eta) + (normal[2] * cs2); 391 | 392 | return out.normalize(); 393 | } 394 | 395 | /** 396 | * Return the negated value of a vector. 397 | * @param {Vector3} a 398 | * @param {Vector3} [out] Optional storage for the output. 399 | * @returns {Vector3} 400 | */ 401 | static negated(a, out) { 402 | out = out || new Vector3(); 403 | out.setFrom(-a[0], -a[1], -a[2]); 404 | return out; 405 | } 406 | 407 | /** 408 | * Return the absoluate value of a vector. 409 | * @param {Vector3} a 410 | * @param {Vector3} [out] Optional storage for the output. 411 | * @returns {Vector3} 412 | */ 413 | static abs(a, out) { 414 | out = out || new Vector3(); 415 | out.setFrom(Math.abs(a[0]), Math.abs(a[1]), Math.abs(a[2])); 416 | return out; 417 | } 418 | 419 | /** 420 | * Calculate the length of a vector. 421 | * @param {Vector3} a 422 | * @returns {number} 423 | */ 424 | static length(a) { 425 | return Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]); 426 | } 427 | 428 | /** 429 | * Calculate the squared lenth of a vector. 430 | * @param {Vector3} a 431 | * @returns {number} 432 | */ 433 | static lengthSquared(a) { 434 | return a[0] * a[0] + a[1] * a[1] + a[2] * a[2]; 435 | } 436 | 437 | /** 438 | * Calculate the squared distance between two points. 439 | * @param {Vector3} a 440 | * @param {Vector3} b 441 | * @returns {number} 442 | */ 443 | static distanceSquared(a, b) { 444 | const dx = b[0] - a[0]; 445 | const dy = b[1] - a[1]; 446 | const dz = b[2] - a[2]; 447 | return dx * dx + dy * dy + dz * dz; 448 | } 449 | 450 | /** 451 | * Calculate the distance between two points. 452 | * @param {Vector3} a 453 | * @param {Vector3} b 454 | * @returns {number} 455 | */ 456 | static distance(a, b) { 457 | const dx = b[0] - a[0]; 458 | const dy = b[1] - a[1]; 459 | const dz = b[2] - a[2]; 460 | return Math.sqrt(dx * dx + dy * dy + dz * dz); 461 | } 462 | 463 | /** 464 | * Calculate the dot product of two vectors. 465 | * @param {Vector3} a 466 | * @param {Vector3} b 467 | * @returns {number} 468 | */ 469 | static dot(a, b) { 470 | return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; 471 | } 472 | 473 | /** 474 | * Calculate the cross product of two vectors. 475 | * @param {Vector3} a 476 | * @param {Vector3} b 477 | * @param {Vector3} [out] Optional storage for the output. 478 | * @returns {Vector3} 479 | */ 480 | static cross(a, b, out) { 481 | out = out || new Vector3(); 482 | const ax = a[0]; 483 | const ay = a[1]; 484 | const az = a[2]; 485 | const bx = b[0]; 486 | const by = b[1]; 487 | const bz = b[2]; 488 | out[0] = ay * bz - az * by; 489 | out[1] = az * bx - ax * bz; 490 | out[2] = ax * by - ay * bx; 491 | return out; 492 | } 493 | 494 | /** 495 | * Return the normalized version of the vector. 496 | * @param {Vector3} a 497 | * @param {Vector3} [out] Optional storage for the output. 498 | * @returns {Vector3} 499 | */ 500 | static normalize(a, out) { 501 | out = out || new Vector3(); 502 | const l = Vector3.length(a); 503 | if (!l) { 504 | out.set(a); 505 | return out; 506 | } 507 | out[0] = a[0] / l; 508 | out[1] = a[1] / l; 509 | out[2] = a[2] / l; 510 | return out; 511 | } 512 | 513 | /** 514 | * Add two vectors. 515 | * @param {Vector3} a 516 | * @param {Vector3} b 517 | * @param {Vector3} [out] Optional storage for the output. 518 | * @returns {Vector3} 519 | */ 520 | static add(a, b, out) { 521 | out = out || new Vector3(); 522 | out[0] = a[0] + b[0]; 523 | out[1] = a[1] + b[1]; 524 | out[2] = a[2] + b[2]; 525 | return out; 526 | } 527 | 528 | /** 529 | * Subtract two vectors 530 | * @param {Vector3} a 531 | * @param {Vector3} b 532 | * @param {Vector3} [out] 533 | * @returns {Vector3} 534 | */ 535 | static subtract(a, b, out) { 536 | out = out || new Vector3(); 537 | out[0] = a[0] - b[0]; 538 | out[1] = a[1] - b[1]; 539 | out[2] = a[2] - b[2]; 540 | return out; 541 | } 542 | 543 | /** 544 | * Multiply two vectors 545 | * @param {Vector3} a 546 | * @param {Vector3} b 547 | * @param {Vector3} [out] 548 | * @returns {Vector3} 549 | */ 550 | static multiply(a, b, out) { 551 | out = out || new Vector3(); 552 | out[0] = a[0] * b[0]; 553 | out[1] = a[1] * b[1]; 554 | out[2] = a[2] * b[2]; 555 | return out; 556 | } 557 | 558 | /** 559 | * Divide two vectors. 560 | * @param {Vector3} a 561 | * @param {Vector3} b 562 | * @param {Vector3} [out] 563 | * @returns {Vector3} 564 | */ 565 | static divide(a, b, out) { 566 | out = out || new Vector3(); 567 | out[0] = b[0] ? a[0] / b[0] : 0; 568 | out[1] = b[1] ? a[1] / b[1] : 0; 569 | out[2] = b[2] ? a[2] / b[2] : 0; 570 | return out; 571 | } 572 | 573 | /** 574 | * Scale a vector by a number. 575 | * @param {Vector3} a 576 | * @param {number} s 577 | * @param {Vector3} [out] 578 | * @returns {Vector3} 579 | */ 580 | static scale(a, s, out) { 581 | out = out || new Vector3(); 582 | out[0] = a[0] * s; 583 | out[1] = a[1] * s; 584 | out[2] = a[2] * s; 585 | return out; 586 | } 587 | 588 | /** 589 | * Each component will be the minimum of either a or b. 590 | * @param {Vector3} a 591 | * @param {Vector3} b 592 | * @param {Vector3} [out] Optional storage for the output. 593 | * @returns {Vector3} 594 | */ 595 | static min(a, b, out) { 596 | out = out || new Vector3(); 597 | out[0] = Math.min(a[0], b[0]); 598 | out[1] = Math.min(a[1], b[1]); 599 | out[2] = Math.min(a[2], b[2]); 600 | return out; 601 | } 602 | 603 | /** 604 | * Each component will be the maximum of either a or b. 605 | * @param {Vector3} a 606 | * @param {Vector3} b 607 | * @param {Vector3} [out] Optional storage for the output. 608 | * @returns {Vector3} 609 | */ 610 | static max(a, b, out) { 611 | out = out || new Vector3(); 612 | out[0] = Math.max(a[0], b[0]); 613 | out[1] = Math.max(a[1], b[1]); 614 | out[2] = Math.max(a[2], b[2]); 615 | return out; 616 | } 617 | 618 | /** 619 | * Scale and add two numbers, as out = a + b * s. 620 | * @param {Vector3} a 621 | * @param {Vector3} b 622 | * @param {number} s 623 | * @param {Vector3} [out] 624 | * @returns {Vector3} 625 | */ 626 | static scaleAndAdd(a, b, s, out) { 627 | out = out || new Vector3(); 628 | out[0] = a[0] + b[0] * s; 629 | out[1] = a[1] + b[1] * s; 630 | out[2] = a[2] + b[2] * s; 631 | return out; 632 | } 633 | 634 | /** 635 | * Linear interpolate between two vectors. 636 | * @param {Vector3} a 637 | * @param {Vector3} b 638 | * @param {number} t Interpolator between 0 and 1. 639 | * @param {Vector3} [out] Optional storage for the output. 640 | * @returns {Vector3} 641 | */ 642 | static lerp(a, b, t, out) { 643 | out = out || new Vector3(); 644 | const ax = a[0]; 645 | const ay = a[1]; 646 | const az = a[2]; 647 | out[0] = ax + t * (b[0] - ax); 648 | out[1] = ay + t * (b[1] - ay); 649 | out[2] = az + t * (b[2] - az); 650 | return out; 651 | } 652 | } 653 | 654 | /** 655 | * @property {Vector3} Zero Vector3(0, 0, 0) 656 | */ 657 | Vector3.Zero = new Vector3(); 658 | /** 659 | * @property {Vector3} One Vector3(1, 1, 1) 660 | */ 661 | Vector3.One = new Vector3(1, 1, 1); 662 | /** 663 | * @property {Vector3} X Vector3(1, 0, 0) 664 | */ 665 | Vector3.X = new Vector3(1, 0, 0); 666 | /** 667 | * @property {Vector3} Y Vector3(0, 1, 0) 668 | */ 669 | Vector3.Y = new Vector3(0, 1, 0); 670 | /** 671 | * @property {Vector3} Z Vector3(0, 0, 1) 672 | */ 673 | Vector3.Z = new Vector3(0, 0, 1); 674 | /** 675 | * @property {Vector3} Up Vector3(0, 1, 0) 676 | */ 677 | Vector3.Up = new Vector3(0, 1, 0); 678 | /** 679 | * @property {Vector3} Down Vector3(0, -1, 0) 680 | */ 681 | Vector3.Down = new Vector3(0, -1, 0); 682 | /** 683 | * @property {Vector3} Right Vector3(1, 0, 0) 684 | */ 685 | Vector3.Right = new Vector3(1, 0, 0); 686 | /** 687 | * @property {Vector3} Left Vector3(-1, 0, 0) 688 | */ 689 | Vector3.Left = new Vector3(-1, 0, 0); 690 | /** 691 | * @property {Vector3} Front Vector3(0, 0, -1) 692 | */ 693 | Vector3.Front = new Vector3(0, 0, -1); 694 | /** 695 | * @property {Vector3} Back Vector3(0, 0, 1) 696 | */ 697 | Vector3.Back = new Vector3(0, 0, 1); 698 | -------------------------------------------------------------------------------- /js/math/matrix4.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "./vector3.js"; 2 | import { Vector4 } from "./vector4.js"; 3 | import { isNumber, Epsilon, RotationOrder, DegreeToRadian, RadianToDegree } from "./math.js"; 4 | 5 | /** 6 | * A 4x4 matrix, stored as a column-major order array. 7 | * @extends Float32Array 8 | * @category Math 9 | */ 10 | export class Matrix4 extends Float32Array { 11 | /** 12 | * Creates a new matrix. 13 | * @param {...*} arguments Variable arguments for constructor overloading. 14 | * @example 15 | * // Creates an identity matrix. 16 | * new Matrix4() 17 | * 18 | * // Creates a clone of the given matrix. 19 | * new Matrix4(Matrix4) 20 | * 21 | * // Creates a Matrix4 from the given array. 22 | * new Matrix4(Array[16]) 23 | * 24 | * // Creates a zero matrix. 25 | * new Matarix4(0) 26 | * 27 | * // Create a matrix from 16 individual elements in column major order 28 | * new Matrix4(m00, m01, m02, m03, 29 | * m10, m11, m12, m13, 30 | * m20, m21, m22, m23, 31 | * m30, m31, m32, m33) 32 | */ 33 | constructor() { 34 | if (arguments.length) { 35 | if (arguments.length == 1 && !arguments[0]) { 36 | super(16); 37 | } else if (arguments.length == 1 && isNumber(arguments[0])) { 38 | super(16); 39 | const x = arguments[0]; 40 | this[0] = x; 41 | this[5] = x; 42 | this[10] = x; 43 | this[15] = x; 44 | } else if (arguments.length == 16 && isNumber(arguments[0])) { 45 | super(16); 46 | this[0] = arguments[0]; 47 | this[1] = arguments[1]; 48 | this[2] = arguments[2]; 49 | this[3] = arguments[3]; 50 | this[4] = arguments[4]; 51 | this[5] = arguments[5]; 52 | this[6] = arguments[6]; 53 | this[7] = arguments[7]; 54 | this[8] = arguments[8]; 55 | this[9] = arguments[9]; 56 | this[10] = arguments[10]; 57 | this[11] = arguments[11]; 58 | this[12] = arguments[12]; 59 | this[13] = arguments[13]; 60 | this[14] = arguments[14]; 61 | this[15] = arguments[15]; 62 | } else { 63 | super(...arguments); 64 | } 65 | } else { 66 | super(16); 67 | this[0] = 1; 68 | this[5] = 1; 69 | this[10] = 1; 70 | this[15] = 1; 71 | } 72 | } 73 | 74 | /** 75 | * Make a copy of this matrix. 76 | * @return {Matrix4} 77 | */ 78 | clone() { 79 | return new Matrix4(this); 80 | } 81 | 82 | /** 83 | * Convert the matrix to an array. 84 | * @return {Array} An array containing the values of the matrix. 85 | */ 86 | toArray() { 87 | return [this[0], this[1], this[2], this[3], 88 | this[4], this[5], this[6], this[7], 89 | this[8], this[9], this[10], this[11], 90 | this[12], this[13], this[14], this[15]]; 91 | } 92 | 93 | /** 94 | * Checks if this is an identity matrix. 95 | * @return {Boolean} true if this is an identity matrix. 96 | */ 97 | isIdentity() { 98 | const m = this; 99 | return ((m[0] === 1) && 100 | (m[1] === 0) && 101 | (m[2] === 0) && 102 | (m[3] === 0) && 103 | (m[4] === 0) && 104 | (m[5] === 1) && 105 | (m[6] === 0) && 106 | (m[7] === 0) && 107 | (m[8] === 0) && 108 | (m[9] === 0) && 109 | (m[10] === 1) && 110 | (m[11] === 0) && 111 | (m[12] === 0) && 112 | (m[13] === 0) && 113 | (m[14] === 0) && 114 | (m[15] === 1)); 115 | } 116 | 117 | /** 118 | * @example 119 | * setColumn(column, Vector3) 120 | * setColumn(column, Vector4) 121 | * setColumn(column, x, y, z) 122 | * setColumn(column, x, y, z, w) 123 | */ 124 | setColumn() { 125 | var args = Array.prototype.slice.call(arguments); 126 | const c = args.shift(); 127 | const numArgs = args.length; 128 | const x = numArgs == 1 ? args[0] : args; 129 | const i = c * 4; 130 | this[i] = x[0]; 131 | this[i + 1] = x[1]; 132 | this[i + 2] = x[2]; 133 | if (numArgs == 4) { 134 | this[i + 3] = x[3]; 135 | } 136 | return this; 137 | } 138 | 139 | /** 140 | * Get a column from the matrix. 141 | * @param {number} index 142 | * @param {Vector4?} out 143 | * @return {Vector4} 144 | */ 145 | getColumn(index, out) { 146 | out = out || new Vector4(); 147 | const i = index * 4; 148 | out[0] = this[i]; 149 | out[1] = this[i + 1]; 150 | out[2] = this[i + 2]; 151 | out[3] = this[i + 3]; 152 | return out; 153 | } 154 | 155 | /** 156 | * Get a column from the matrix. 157 | * @param {number} index 158 | * @param {Vector3?} out 159 | * @return {Vector3} 160 | */ 161 | getColumn3(index, out) { 162 | out = out || new Vector3(); 163 | const i = index * 4; 164 | out[0] = this[i]; 165 | out[1] = this[i + 1]; 166 | out[2] = this[i + 2]; 167 | return out; 168 | } 169 | 170 | /** 171 | * @example 172 | * setRow(row, Vector3) 173 | * 174 | * setRow(row, Vector4) 175 | * 176 | * setRow(row, x, y, z) 177 | * 178 | * setRow(row, x, y, z, w) 179 | */ 180 | setRow() { 181 | var args = Array.prototype.slice.call(arguments); 182 | const r = args.shift(); 183 | const numArgs = args.length; 184 | const x = numArgs == 1 ? args[0] : args; 185 | this[r] = x[0]; 186 | this[r + 4] = x[1]; 187 | this[r + 8] = x[2]; 188 | if (numArgs == 4) { 189 | this[r + 12] = x[3]; 190 | } 191 | return this; 192 | } 193 | 194 | /** 195 | * Get a row from the matrix. 196 | * @param {number} index 197 | * @param {Vector4?} out 198 | * @return {Vector4} 199 | */ 200 | getRow(index, out) { 201 | out = out || new Vector4(); 202 | out[0] = this[index]; 203 | out[1] = this[index + 4]; 204 | out[2] = this[index + 8]; 205 | out[3] = this[index + 12]; 206 | return out; 207 | } 208 | 209 | /** 210 | * Get a row from the matrix. 211 | * @param {number} index 212 | * @param {Vector3?} out 213 | * @return {Vector3} 214 | */ 215 | getRow3(r, out) { 216 | out = out || new Vector3(); 217 | out[0] = this[r]; 218 | out[1] = this[r + 4]; 219 | out[2] = this[r + 8]; 220 | return out; 221 | } 222 | 223 | /** 224 | * @example 225 | * setFrom(m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33) 226 | * 227 | * setFrom([m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33]) 228 | * 229 | * setFrom(Matrix4) 230 | */ 231 | setFrom() { 232 | const numArgs = arguments.length; 233 | const m = this; 234 | const x = numArgs == 1 ? arguments[0] : arguments; 235 | m[0] = x[0]; 236 | m[1] = x[1]; 237 | m[2] = x[2]; 238 | m[3] = x[3]; 239 | m[4] = x[4]; 240 | m[5] = x[5]; 241 | m[6] = x[6]; 242 | m[7] = x[7]; 243 | m[8] = x[8]; 244 | m[9] = x[9]; 245 | m[10] = x[10]; 246 | m[11] = x[11]; 247 | m[12] = x[12]; 248 | m[13] = x[13]; 249 | m[14] = x[14]; 250 | m[15] = x[15]; 251 | return this; 252 | } 253 | 254 | /** 255 | * Set the matrix as an identity matrix. 256 | */ 257 | setIdentity() { 258 | const m = this; 259 | m[0] = 1; 260 | m[1] = 0; 261 | m[2] = 0; 262 | m[3] = 0; 263 | m[4] = 0; 264 | m[5] = 1; 265 | m[6] = 0; 266 | m[7] = 0; 267 | m[8] = 0; 268 | m[9] = 0; 269 | m[10] = 1; 270 | m[11] = 0; 271 | m[12] = 0; 272 | m[13] = 0; 273 | m[14] = 0; 274 | m[15] = 1; 275 | return this; 276 | } 277 | 278 | /** 279 | * @example 280 | * setTranslate(x, y, z) 281 | * 282 | * setTranslate([x, y, z]) 283 | * 284 | * setTranslate(Vector3) 285 | */ 286 | setTranslate() { 287 | const numArgs = arguments.length; 288 | const x = numArgs == 1 ? arguments[0] : arguments; 289 | const out = this; 290 | out[0] = 1; 291 | out[1] = 0; 292 | out[2] = 0; 293 | out[3] = 0; 294 | out[4] = 0; 295 | out[5] = 1; 296 | out[6] = 0; 297 | out[7] = 0; 298 | out[8] = 0; 299 | out[9] = 0; 300 | out[10] = 1; 301 | out[11] = 0; 302 | out[12] = x[0]; 303 | out[13] = x[1]; 304 | out[14] = x[2]; 305 | out[15] = 1; 306 | return this; 307 | } 308 | 309 | /** 310 | * @example 311 | * setScale(x, y, z) 312 | * 313 | * setScale([x, y, z]) 314 | * 315 | * setScale(Vector3) 316 | */ 317 | setScale() { 318 | const numArgs = arguments.length; 319 | const x = numArgs == 1 ? arguments[0] : arguments; 320 | const out = this; 321 | out[0] = x[0]; 322 | out[1] = 0; 323 | out[2] = 0; 324 | out[3] = 0; 325 | out[4] = 0; 326 | out[5] = x[1]; 327 | out[6] = 0; 328 | out[7] = 0; 329 | out[8] = 0; 330 | out[9] = 0; 331 | out[10] = x[2]; 332 | out[11] = 0; 333 | out[12] = 0; 334 | out[13] = 0; 335 | out[14] = 0; 336 | out[15] = 1; 337 | return this; 338 | } 339 | 340 | /** 341 | * @example 342 | * setAxisAngle(angle, x, y, z) 343 | * 344 | * setAxisAngle(angle, [x, y, z]) 345 | * 346 | * setAxisAngle(angle, Vector3) 347 | */ 348 | setAxisAngle() { 349 | const out = this; 350 | const numArgs = arguments.length; 351 | const angle = arguments[0]; 352 | let x = numArgs == 2 ? arguments[1][0] : arguments[1]; 353 | let y = numArgs == 2 ? arguments[1][1] : arguments[2]; 354 | let z = numArgs == 2 ? arguments[1][2] : arguments[3]; 355 | let len = Math.hypot(x, y, z); 356 | 357 | if (len < Epsilon) { return null; } 358 | 359 | len = 1 / len; 360 | x *= len; 361 | y *= len; 362 | z *= len; 363 | 364 | const s = Math.sin(angle); 365 | const c = Math.cos(angle); 366 | const t = 1 - c; 367 | 368 | // Perform rotation-specific matrix multiplication 369 | out[0] = x * x * t + c; 370 | out[1] = y * x * t + z * s; 371 | out[2] = z * x * t - y * s; 372 | out[3] = 0; 373 | out[4] = x * y * t - z * s; 374 | out[5] = y * y * t + c; 375 | out[6] = z * y * t + x * s; 376 | out[7] = 0; 377 | out[8] = x * z * t + y * s; 378 | out[9] = y * z * t - x * s; 379 | out[10] = z * z * t + c; 380 | out[11] = 0; 381 | out[12] = 0; 382 | out[13] = 0; 383 | out[14] = 0; 384 | out[15] = 1; 385 | 386 | return this; 387 | } 388 | 389 | /** 390 | * Set the matrix as a rotation around the X axis. 391 | * @param {number} angle Angle of the rotation, in radians. 392 | */ 393 | setRotateX(angle) { 394 | angle *= DegreeToRadian; 395 | const c = Math.cos(angle); 396 | const s = Math.sin(angle); 397 | this[0] = 1; 398 | this[1] = 0; 399 | this[2] = 0; 400 | this[3] = 0; 401 | this[4] = 0; 402 | this[5] = c; 403 | this[6] = -s; 404 | this[7] = 0; 405 | this[8] = 0; 406 | this[9] = s; 407 | this[10] = c; 408 | this[11] = 0; 409 | this[12] = 0; 410 | this[13] = 0; 411 | this[14] = 0; 412 | this[15] = 1; 413 | return this; 414 | } 415 | 416 | /** 417 | * Set the matrix as a rotation around the Y axis. 418 | * @param {number} angle Angle of the rotation, in degrees. 419 | */ 420 | setRotateY(angle) { 421 | angle *= DegreeToRadian; 422 | const c = Math.cos(angle); 423 | const s = Math.sin(angle); 424 | this[0] = c; 425 | this[1] = 0; 426 | this[2] = s; 427 | this[3] = 0; 428 | this[4] = 0; 429 | this[5] = 1; 430 | this[6] = 0; 431 | this[7] = 0; 432 | this[8] = -s; 433 | this[9] = 0; 434 | this[10] = c; 435 | this[11] = 0; 436 | this[12] = 0; 437 | this[13] = 0; 438 | this[14] = 0; 439 | this[15] = 1; 440 | return this; 441 | } 442 | 443 | /** 444 | * Set the matrix as a rotation around the Z axis. 445 | * @param angle Angle of the rotation, in degrees. 446 | */ 447 | setRotateZ(angle) { 448 | angle *= DegreeToRadian; 449 | const c = Math.cos(angle); 450 | const s = Math.sin(angle); 451 | this[0] = c; 452 | this[1] = -s; 453 | this[2] = 0; 454 | this[3] = 0; 455 | this[4] = s; 456 | this[5] = c; 457 | this[6] = 0; 458 | this[7] = 0; 459 | this[8] = 0; 460 | this[9] = 0; 461 | this[10] = 1; 462 | this[11] = 0; 463 | this[12] = 0; 464 | this[13] = 0; 465 | this[14] = 0; 466 | this[15] = 1; 467 | return this; 468 | } 469 | 470 | /** 471 | * Set the matrix as an euler rotation. Angles are given in degrees. 472 | * @example 473 | * setEulerAngles(x, y, z, order = RotationOrder.ZYX) 474 | * 475 | * setEulerAngles(Vector3, order = RotationOrder.ZYX) 476 | */ 477 | setEulerAngles() { 478 | const numArgs = arguments.length; 479 | let x, y, z, order; 480 | if (numArgs <= 2) { 481 | x = arguments[0][0]; 482 | y = arguments[0][1]; 483 | z = arguments[0][2]; 484 | order = arguments[1] !== undefined ? arguments[1] : RotationOrder.Default; 485 | } else if (numArgs == 3 || numArgs == 4) { 486 | x = arguments[0]; 487 | y = arguments[1]; 488 | z = arguments[2]; 489 | order = arguments[3] !== undefined ? arguments[3] : RotationOrder.Default; 490 | } else { 491 | throw "invalid arguments for setEulerAngles"; 492 | } 493 | switch (order) { 494 | case RotationOrder.XYZ: 495 | this.setRotateZ(z); 496 | this.rotateY(y); 497 | this.rotateX(x); 498 | break; 499 | case RotationOrder.XZY: 500 | this.setRotateY(z); 501 | this.rotateZ(y); 502 | this.rotateX(x); 503 | break; 504 | case RotationOrder.YZX: 505 | this.setRotateX(z); 506 | this.rotateZ(y); 507 | this.rotateY(x); 508 | break; 509 | case RotationOrder.YXZ: 510 | this.setRotateZ(z); 511 | this.rotateX(y); 512 | this.rotateY(x); 513 | break; 514 | case RotationOrder.ZXY: 515 | this.setRotateY(z); 516 | this.rotateX(y); 517 | this.rotateZ(x); 518 | break; 519 | case RotationOrder.ZYX: 520 | this.setRotateX(z); 521 | this.rotateY(y); 522 | this.rotateZ(x); 523 | break; 524 | } 525 | return this; 526 | } 527 | 528 | /** 529 | * Set as a perspective projection matrix. 530 | * @param {number} fovx Horizontal field of view, in radians 531 | * @param {number} aspect Aspect ratio 532 | * @param {number} near Distance to the near clipping plane 533 | * @param {number} far Distance to the far clipping plane 534 | */ 535 | setPerspective(fovx, aspect, near, far) { 536 | const out = this; 537 | const f = 1.0 / Math.tan(fovx / 2); 538 | out[0] = f / aspect; 539 | out[1] = 0; 540 | out[2] = 0; 541 | out[3] = 0; 542 | out[4] = 0; 543 | out[5] = f; 544 | out[6] = 0; 545 | out[7] = 0; 546 | out[8] = 0; 547 | out[9] = 0; 548 | out[11] = -1; 549 | out[12] = 0; 550 | out[13] = 0; 551 | out[15] = 0; 552 | if (far !== null && far !== Infinity) { 553 | const nearFar = 1 / (near - far); 554 | out[10] = (far + near) * nearFar; 555 | out[14] = (2 * far * near) * nearFar; 556 | } else { 557 | out[10] = -1; 558 | out[14] = -2 * near; 559 | } 560 | return this; 561 | } 562 | 563 | /** 564 | * Set as an orthographic projection matrix. 565 | * @param {*} left 566 | * @param {*} right 567 | * @param {*} bottom 568 | * @param {*} top 569 | * @param {*} near 570 | * @param {*} far 571 | */ 572 | setOrtho(left, right, bottom, top, near, far) { 573 | const out = this; 574 | const lr = 1 / (left - right); 575 | const bt = 1 / (bottom - top); 576 | const nf = 1 / (near - far); 577 | out[0] = -2 * lr; 578 | out[1] = 0; 579 | out[2] = 0; 580 | out[3] = 0; 581 | out[4] = 0; 582 | out[5] = -2 * bt; 583 | out[6] = 0; 584 | out[7] = 0; 585 | out[8] = 0; 586 | out[9] = 0; 587 | out[10] = 2 * nf; 588 | out[11] = 0; 589 | out[12] = (left + right) * lr; 590 | out[13] = (top + bottom) * bt; 591 | out[14] = (far + near) * nf; 592 | out[15] = 1; 593 | return this; 594 | } 595 | 596 | /** 597 | * Set the matrix as a look-at transformation 598 | * @param {*} eye 599 | * @param {*} center 600 | * @param {*} up 601 | */ 602 | setLookAt(eye, center, up) { 603 | const out = this; 604 | const eyex = eye[0]; 605 | const eyey = eye[1]; 606 | const eyez = eye[2]; 607 | const upx = up[0]; 608 | const upy = up[1]; 609 | const upz = up[2]; 610 | const centerx = center[0]; 611 | const centery = center[1]; 612 | const centerz = center[2]; 613 | 614 | if (Math.abs(eyex - centerx) < Epsilon && 615 | Math.abs(eyey - centery) < Epsilon && 616 | Math.abs(eyez - centerz) < Epsilon) { 617 | return out.setIdentity(); 618 | } 619 | 620 | let z0 = eyex - centerx; 621 | let z1 = eyey - centery; 622 | let z2 = eyez - centerz; 623 | 624 | let len = 1 / Math.hypot(z0, z1, z2); 625 | z0 *= len; 626 | z1 *= len; 627 | z2 *= len; 628 | 629 | let x0 = upy * z2 - upz * z1; 630 | let x1 = upz * z0 - upx * z2; 631 | let x2 = upx * z1 - upy * z0; 632 | len = Math.hypot(x0, x1, x2); 633 | if (!len) { 634 | x0 = 0; 635 | x1 = 0; 636 | x2 = 0; 637 | } else { 638 | len = 1 / len; 639 | x0 *= len; 640 | x1 *= len; 641 | x2 *= len; 642 | } 643 | 644 | let y0 = z1 * x2 - z2 * x1; 645 | let y1 = z2 * x0 - z0 * x2; 646 | let y2 = z0 * x1 - z1 * x0; 647 | len = Math.hypot(y0, y1, y2); 648 | if (!len) { 649 | y0 = 0; 650 | y1 = 0; 651 | y2 = 0; 652 | } else { 653 | len = 1 / len; 654 | y0 *= len; 655 | y1 *= len; 656 | y2 *= len; 657 | } 658 | 659 | out[0] = x0; 660 | out[1] = y0; 661 | out[2] = z0; 662 | out[3] = 0; 663 | out[4] = x1; 664 | out[5] = y1; 665 | out[6] = z1; 666 | out[7] = 0; 667 | out[8] = x2; 668 | out[9] = y2; 669 | out[10] = z2; 670 | out[11] = 0; 671 | out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); 672 | out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); 673 | out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); 674 | out[15] = 1; 675 | 676 | return this; 677 | } 678 | 679 | setTargetTo(eye, target, up) { 680 | const out = this; 681 | 682 | const eyex = eye[0]; 683 | const eyey = eye[1]; 684 | const eyez = eye[2]; 685 | const upx = up[0]; 686 | const upy = up[1]; 687 | const upz = up[2]; 688 | 689 | let z0 = eyex - target[0]; 690 | let z1 = eyey - target[1]; 691 | let z2 = eyez - target[2]; 692 | let len = z0*z0 + z1*z1 + z2*z2; 693 | if (len > 0) { 694 | len = 1 / Math.sqrt(len); 695 | z0 *= len; 696 | z1 *= len; 697 | z2 *= len; 698 | } 699 | 700 | let x0 = upy * z2 - upz * z1; 701 | let x1 = upz * z0 - upx * z2; 702 | let x2 = upx * z1 - upy * z0; 703 | len = x0*x0 + x1*x1 + x2*x2; 704 | if (len > 0) { 705 | len = 1 / Math.sqrt(len); 706 | x0 *= len; 707 | x1 *= len; 708 | x2 *= len; 709 | } 710 | 711 | out[0] = x0; 712 | out[1] = x1; 713 | out[2] = x2; 714 | out[3] = 0; 715 | out[4] = z1 * x2 - z2 * x1; 716 | out[5] = z2 * x0 - z0 * x2; 717 | out[6] = z0 * x1 - z1 * x0; 718 | out[7] = 0; 719 | out[8] = z0; 720 | out[9] = z1; 721 | out[10] = z2; 722 | out[11] = 0; 723 | out[12] = eyex; 724 | out[13] = eyey; 725 | out[14] = eyez; 726 | out[15] = 1; 727 | 728 | return this; 729 | } 730 | 731 | /** 732 | * Set the matrix from a set of vector columns 733 | * @param {*} x 734 | * @param {*} y 735 | * @param {*} z 736 | * @param {*} translate 737 | */ 738 | setColumns(x, y, z, translate) { 739 | if (x) { 740 | this.setColumn(0, x); 741 | } 742 | if (y) { 743 | this.setColumn(1, y); 744 | } 745 | if (z) { 746 | this.setColumn(2, z); 747 | } 748 | if (translate) { 749 | this.setColumn(3, translate); 750 | } 751 | return this; 752 | } 753 | 754 | /** 755 | * Extracts the x-axis from this matrix. 756 | * @param {Vector3?} out optional storage for the results. 757 | * @return {Vector3} 758 | */ 759 | getX(out) { 760 | out = out || new Vector3(); 761 | out[0] = this[0]; 762 | out[1] = this[1]; 763 | out[2] = this[2]; 764 | return out; 765 | } 766 | 767 | /** 768 | * Extracts the y-axis from this matrix. 769 | * @param {Vector3?} out optional storage for the results. 770 | * @return {Vector3} 771 | */ 772 | getY(out) { 773 | out = out || new Vector3(); 774 | out[0] = this[4]; 775 | out[1] = this[5]; 776 | out[2] = this[6]; 777 | return out; 778 | } 779 | 780 | /** 781 | * Extracts the z-axis from this matrix. 782 | * @param {Vector3?} out optional storage for the results. 783 | * @return {Vector3} 784 | */ 785 | getZ(out) { 786 | out = out || new Vector3(); 787 | out[0] = -this[8]; 788 | out[1] = -this[9]; 789 | out[2] = -this[10]; 790 | return out; 791 | } 792 | 793 | /** 794 | * Extracts the translational component of this matrix. 795 | * @param {Vector3?} out optional storage for the results. 796 | * @return {Vector3} 797 | */ 798 | getTranslation(out) { 799 | out = out || new Vector3(); 800 | out[0] = this[12]; 801 | out[1] = this[13]; 802 | out[2] = this[14]; 803 | return out; 804 | } 805 | 806 | /** 807 | * Extracts the scaling component of this matrix. 808 | * @param {Vector3?} out optional storage for the results. 809 | * @return {Vector3} 810 | */ 811 | getScale(out) { 812 | out = out || new Vector3(); 813 | const mat = this; 814 | const m11 = mat[0]; 815 | const m12 = mat[1]; 816 | const m13 = mat[2]; 817 | const m21 = mat[4]; 818 | const m22 = mat[5]; 819 | const m23 = mat[6]; 820 | const m31 = mat[8]; 821 | const m32 = mat[9]; 822 | const m33 = mat[10]; 823 | 824 | out[0] = Math.hypot(m11, m12, m13); 825 | out[1] = Math.hypot(m21, m22, m23); 826 | out[2] = Math.hypot(m31, m32, m33); 827 | 828 | return out; 829 | } 830 | 831 | /** 832 | * Extract the euler rotation angles, in degrees, from the matrix. 833 | * @param {Vector3} [out] Optional output storage for the results. 834 | * @return {Vector3} 835 | */ 836 | getEulerAngles(out) { 837 | out = out || new Vector3(); 838 | 839 | let sinY = this[8]; 840 | if (sinY < -1.0) { 841 | sinY = -1.0; 842 | } 843 | 844 | if (sinY > 1.0) { 845 | sinY = 1.0; 846 | } 847 | 848 | let cosY = Math.sqrt(1.0 - sinY * sinY); 849 | if (this[0] < 0.0 && this[10] < 0.0) { 850 | cosY = -cosY; 851 | } 852 | 853 | if (Math.abs(cosY) > Epsilon) { 854 | out[0] = Math.atan2(this[9] / cosY, this[10] / cosY) * RadianToDegree; 855 | out[1] = Math.atan2(sinY, cosY) * RadianToDegree; 856 | out[2] = Math.atan2(this[4] / cosY, this[0] / cosY)* RadianToDegree; 857 | return out; 858 | } 859 | 860 | out[0] = Math.atan2(-this[6], this[5]) * RadianToDegree; 861 | out[1] = Math.asin(sinY) * RadianToDegree; 862 | out[2] = 0.0; 863 | } 864 | 865 | /** 866 | * Transpose the matrix. 867 | * @return {Matrix4} Returns this matrix. 868 | */ 869 | transpose() { 870 | const m = this; 871 | const a01 = m[1]; 872 | const a02 = m[2]; 873 | const a03 = m[3]; 874 | const a12 = m[6]; 875 | const a13 = m[7]; 876 | const a23 = m[11]; 877 | 878 | m[1] = m[4]; 879 | m[2] = m[8]; 880 | m[3] = m[12]; 881 | m[4] = a01; 882 | m[6] = m[9]; 883 | m[7] = m[13]; 884 | m[8] = a02; 885 | m[9] = a12; 886 | m[11] = m[14]; 887 | m[12] = a03; 888 | m[13] = a13; 889 | m[14] = a23; 890 | return this; 891 | } 892 | 893 | /** 894 | * Invert the matrix. 895 | * @param {Matrix4?} out Optional storage for the inverted matrix. If not provided, 896 | * invert itself. 897 | * @return {Matrix4} 898 | */ 899 | invert(out) { 900 | out = out || this; 901 | const a = this; 902 | const a00 = a[0]; 903 | const a01 = a[1]; 904 | const a02 = a[2]; 905 | const a03 = a[3]; 906 | const a10 = a[4]; 907 | const a11 = a[5]; 908 | const a12 = a[6]; 909 | const a13 = a[7]; 910 | const a20 = a[8]; 911 | const a21 = a[9]; 912 | const a22 = a[10]; 913 | const a23 = a[11]; 914 | const a30 = a[12]; 915 | const a31 = a[13]; 916 | const a32 = a[14]; 917 | const a33 = a[15]; 918 | 919 | const b00 = a00 * a11 - a01 * a10; 920 | const b01 = a00 * a12 - a02 * a10; 921 | const b02 = a00 * a13 - a03 * a10; 922 | const b03 = a01 * a12 - a02 * a11; 923 | const b04 = a01 * a13 - a03 * a11; 924 | const b05 = a02 * a13 - a03 * a12; 925 | const b06 = a20 * a31 - a21 * a30; 926 | const b07 = a20 * a32 - a22 * a30; 927 | const b08 = a20 * a33 - a23 * a30; 928 | const b09 = a21 * a32 - a22 * a31; 929 | const b10 = a21 * a33 - a23 * a31; 930 | const b11 = a22 * a33 - a23 * a32; 931 | 932 | // Calculate the determinant 933 | let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 934 | if (!det) { 935 | return out; 936 | } 937 | det = 1.0 / det; 938 | 939 | out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; 940 | out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; 941 | out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; 942 | out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; 943 | out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; 944 | out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; 945 | out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; 946 | out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; 947 | out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; 948 | out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; 949 | out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; 950 | out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; 951 | out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; 952 | out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; 953 | out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; 954 | out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; 955 | 956 | return out; 957 | } 958 | 959 | /** 960 | * Calculate the determinant of the matrix. 961 | * @return {number} 962 | */ 963 | determinant() { 964 | const m = this; 965 | const m00 = m[0]; 966 | const m01 = m[1]; 967 | const m02 = m[2]; 968 | const m03 = m[3]; 969 | const m10 = m[4]; 970 | const m11 = m[5]; 971 | const m12 = m[6]; 972 | const m13 = m[7]; 973 | const m20 = m[8]; 974 | const m21 = m[9]; 975 | const m22 = m[10]; 976 | const m23 = m[11]; 977 | const m30 = m[12]; 978 | const m31 = m[13]; 979 | const m32 = m[14]; 980 | const m33 = m[15]; 981 | 982 | const b00 = m00 * m11 - m01 * m10; 983 | const b01 = m00 * m12 - m02 * m10; 984 | const b02 = m00 * m13 - m03 * m10; 985 | const b03 = m01 * m12 - m02 * m11; 986 | const b04 = m01 * m13 - m03 * m11; 987 | const b05 = m02 * m13 - m03 * m12; 988 | const b06 = m20 * m31 - m21 * m30; 989 | const b07 = m20 * m32 - m22 * m30; 990 | const b08 = m20 * m33 - m23 * m30; 991 | const b09 = m21 * m32 - m22 * m31; 992 | const b10 = m21 * m33 - m23 * m31; 993 | const b11 = m22 * m33 - m23 * m32; 994 | 995 | // Calculate the determinant 996 | return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 997 | } 998 | 999 | /** 1000 | * Translate the matrix. 1001 | * @param {*} arguments Can either be a Vector3 or 3 numbers. 1002 | * @return {Matrix4} Returns this matrix. 1003 | * @example 1004 | * translate(Vector3) 1005 | * translate(x, y, z) 1006 | */ 1007 | translate() { 1008 | const n = arguments.length; 1009 | const v = n == 1 ? arguments[0] : arguments; 1010 | const x = v[0]; 1011 | const y = v[1]; 1012 | const z = v[2]; 1013 | const o = this; 1014 | const a = this; 1015 | o[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; 1016 | o[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; 1017 | o[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; 1018 | o[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; 1019 | 1020 | return this; 1021 | } 1022 | 1023 | /** 1024 | * Scale the matrix 1025 | * @param {*} arguments Can either be a Vector3 or 3 numbers. 1026 | * @return {Matrix4} Returns this matrix. 1027 | * @example 1028 | * scale(Vector3) 1029 | * scale(x, y, z) 1030 | */ 1031 | scale() { 1032 | if (!arguments.length) { 1033 | return; 1034 | } 1035 | const v = arguments[0].length >= 3 ? arguments[0] : arguments; 1036 | const x = v[0]; 1037 | const y = v[1]; 1038 | const z = v[2]; 1039 | const a = this; 1040 | const out = this; 1041 | out[0] = a[0] * x; 1042 | out[1] = a[1] * x; 1043 | out[2] = a[2] * x; 1044 | out[3] = a[3] * x; 1045 | out[4] = a[4] * y; 1046 | out[5] = a[5] * y; 1047 | out[6] = a[6] * y; 1048 | out[7] = a[7] * y; 1049 | out[8] = a[8] * z; 1050 | out[9] = a[9] * z; 1051 | out[10] = a[10] * z; 1052 | out[11] = a[11] * z; 1053 | out[12] = a[12]; 1054 | out[13] = a[13]; 1055 | out[14] = a[14]; 1056 | out[15] = a[15]; 1057 | return this; 1058 | } 1059 | 1060 | /** 1061 | * Rotate the matrix around the X axis. 1062 | * @param {number} angle The amount to rotate, in degrees. 1063 | * @return {Matrix4} Returns this matrix. 1064 | */ 1065 | rotateX(angle) { 1066 | const rad = angle * DegreeToRadian; 1067 | const a = this; 1068 | const out = this; 1069 | 1070 | const s = Math.sin(rad); 1071 | const c = Math.cos(rad); 1072 | const a10 = a[4]; 1073 | const a11 = a[5]; 1074 | const a12 = a[6]; 1075 | const a13 = a[7]; 1076 | const a20 = a[8]; 1077 | const a21 = a[9]; 1078 | const a22 = a[10]; 1079 | const a23 = a[11]; 1080 | 1081 | // Perform axis-specific matrix multiplication 1082 | out[4] = a10 * c + a20 * s; 1083 | out[5] = a11 * c + a21 * s; 1084 | out[6] = a12 * c + a22 * s; 1085 | out[7] = a13 * c + a23 * s; 1086 | out[8] = a20 * c - a10 * s; 1087 | out[9] = a21 * c - a11 * s; 1088 | out[10] = a22 * c - a12 * s; 1089 | out[11] = a23 * c - a13 * s; 1090 | 1091 | return this; 1092 | } 1093 | 1094 | /** 1095 | * Rotate the matrix around the Y axis. 1096 | * @param {number} angle The amount to rotate, in degrees. 1097 | * @return {Matrix4} Returns this matrix. 1098 | */ 1099 | rotateY(angle) { 1100 | const rad = angle * DegreeToRadian; 1101 | const a = this; 1102 | const out = this; 1103 | 1104 | const s = Math.sin(rad); 1105 | const c = Math.cos(rad); 1106 | const a00 = a[0]; 1107 | const a01 = a[1]; 1108 | const a02 = a[2]; 1109 | const a03 = a[3]; 1110 | const a20 = a[8]; 1111 | const a21 = a[9]; 1112 | const a22 = a[10]; 1113 | const a23 = a[11]; 1114 | 1115 | // Perform axis-specific matrix multiplication 1116 | out[0] = a00 * c - a20 * s; 1117 | out[1] = a01 * c - a21 * s; 1118 | out[2] = a02 * c - a22 * s; 1119 | out[3] = a03 * c - a23 * s; 1120 | out[8] = a00 * s + a20 * c; 1121 | out[9] = a01 * s + a21 * c; 1122 | out[10] = a02 * s + a22 * c; 1123 | out[11] = a03 * s + a23 * c; 1124 | 1125 | return this; 1126 | } 1127 | 1128 | /** 1129 | * Rotate the matrix around the Z axis. 1130 | * @param {number} angle The amount to rotate, in degrees. 1131 | * @return {Matrix4} Returns this matrix. 1132 | */ 1133 | rotateZ(angle) { 1134 | const rad = angle * DegreeToRadian; 1135 | const a = this; 1136 | const out = this; 1137 | 1138 | const s = Math.sin(rad); 1139 | const c = Math.cos(rad); 1140 | const a00 = a[0]; 1141 | const a01 = a[1]; 1142 | const a02 = a[2]; 1143 | const a03 = a[3]; 1144 | const a10 = a[4]; 1145 | const a11 = a[5]; 1146 | const a12 = a[6]; 1147 | const a13 = a[7]; 1148 | // Perform axis-specific matrix multiplication 1149 | out[0] = a00 * c + a10 * s; 1150 | out[1] = a01 * c + a11 * s; 1151 | out[2] = a02 * c + a12 * s; 1152 | out[3] = a03 * c + a13 * s; 1153 | out[4] = a10 * c - a00 * s; 1154 | out[5] = a11 * c - a01 * s; 1155 | out[6] = a12 * c - a02 * s; 1156 | out[7] = a13 * c - a03 * s; 1157 | 1158 | return this; 1159 | } 1160 | 1161 | /** 1162 | * Rotate the matrix by the given euler angles. Angles are given in degrees. 1163 | * @return {Matrix4} Returns this matrix. 1164 | * @example 1165 | * rotateEuler(x, y, z, order = RotationOrder.Default) 1166 | * rotateEuler(Vector3, order = RotationOrder.Default) 1167 | */ 1168 | rotateEuler() { 1169 | const numArgs = arguments.length; 1170 | let x, y, z, order; 1171 | if (numArgs <= 2) { 1172 | x = arguments[0][0]; 1173 | y = arguments[0][1]; 1174 | z = arguments[0][2]; 1175 | order = arguments[1] !== undefined ? arguments[1] : RotationOrder.Default; 1176 | } else if (numArgs == 3 || numArgs == 4) { 1177 | x = arguments[0]; 1178 | y = arguments[1]; 1179 | z = arguments[2]; 1180 | order = arguments[3] !== undefined ? arguments[3] : RotationOrder.Default; 1181 | } else { 1182 | throw "invalid arguments for rotateEuler"; 1183 | } 1184 | switch (order) { 1185 | case RotationOrder.ZYX: 1186 | this.rotateZ(z); 1187 | this.rotateY(y); 1188 | this.rotateX(x); 1189 | break; 1190 | case RotationOrder.YZX: 1191 | this.rotateY(z); 1192 | this.rotateZ(y); 1193 | this.rotateX(x); 1194 | break; 1195 | case RotationOrder.XZY: 1196 | this.rotateX(z); 1197 | this.rotateZ(y); 1198 | this.rotateY(x); 1199 | break; 1200 | case RotationOrder.ZXY: 1201 | this.rotateZ(z); 1202 | this.rotateX(y); 1203 | this.rotateY(x); 1204 | break; 1205 | case RotationOrder.YXZ: 1206 | this.rotateY(z); 1207 | this.rotateX(y); 1208 | this.rotateZ(x); 1209 | break; 1210 | case RotationOrder.XYZ: 1211 | this.rotateX(z); 1212 | this.rotateY(y); 1213 | this.rotateZ(x); 1214 | break; 1215 | } 1216 | return this; 1217 | } 1218 | 1219 | /** 1220 | * Remove any scaling from the matrix. 1221 | * @return {Matrix4} Returns this matrix. 1222 | */ 1223 | normalizeScale() { 1224 | const m = this; 1225 | let l = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]); 1226 | if (l != 0) { 1227 | l = 1 / l; 1228 | } 1229 | m[0] *= l; 1230 | m[1] *= l; 1231 | m[2] *= l; 1232 | 1233 | l = Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]); 1234 | if (l != 0) { 1235 | l = 1 / l; 1236 | } 1237 | m[4] *= l; 1238 | m[5] *= l; 1239 | m[6] *= l; 1240 | 1241 | l = Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]); 1242 | if (l != 0) { 1243 | l = 1 / l; 1244 | } 1245 | m[8] *= l; 1246 | m[9] *= l; 1247 | m[10] *= l; 1248 | 1249 | return this; 1250 | } 1251 | 1252 | /** 1253 | * Transform a Vector3 1254 | * @param {Vector3} v The Vector3 to transform 1255 | * @param {number?} w The w coordinate of the vector. 0 for a vector, 1 for a point. Default 1. 1256 | * @param {Vector3?} out Optional storage for the results. 1257 | * @return {Vector3} 1258 | */ 1259 | transformVector3(v, w, out) { 1260 | if (w === undefined) { 1261 | w = 1; 1262 | } 1263 | const x = v[0]; 1264 | const y = v[1]; 1265 | const z = v[2]; 1266 | const m = this; 1267 | out = out || new Vector3(); 1268 | out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; 1269 | out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; 1270 | out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; 1271 | return out; 1272 | } 1273 | 1274 | /** 1275 | * Transform a Vector4 1276 | * @param {Vector4} v The Vector4 to transform 1277 | * @param {Vector4?} out Optional storage for the results. 1278 | * @return {Vector4} 1279 | */ 1280 | transformVector4(v, out) { 1281 | const x = v[0]; 1282 | const y = v[1]; 1283 | const z = v[2]; 1284 | const w = v[3]; 1285 | const m = this; 1286 | out = out || new Vector4(); 1287 | out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; 1288 | out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; 1289 | out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; 1290 | out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; 1291 | return out; 1292 | } 1293 | 1294 | /** 1295 | * Transpose a Matrix4. 1296 | * @param {Matrix4} m 1297 | * @param {Matrix4?} out 1298 | * @return {Matrix4} 1299 | */ 1300 | static transpose(m, out) { 1301 | out = out || new Matrix4(); 1302 | out.set(m[0], m[4], m[8], m[12], 1303 | m[1], m[5], m[9], m[13], 1304 | m[2], m[6], m[10], m[14], 1305 | m[3], m[7], m[11], m[15]); 1306 | return out; 1307 | } 1308 | 1309 | /** 1310 | * Invert a Matrix4. 1311 | * @param {Matrix4} m 1312 | * @param {Matrix4?} out 1313 | * @return {Matrix4} 1314 | */ 1315 | static invert(m, out) { 1316 | out = out || new Matrix4(); 1317 | out.set(m); 1318 | return out.invert(); 1319 | } 1320 | 1321 | /** 1322 | * Translate a Matrix4. 1323 | * @param {Matrix4} m 1324 | * @param {Vector3} v 1325 | * @param {Matrix4?} out 1326 | * @return {Matrix4} 1327 | */ 1328 | static translate(m, v, out) { 1329 | if (out === undefined) { 1330 | out = this.clone(); 1331 | } else { 1332 | out.set(this); 1333 | } 1334 | return out.translate(v); 1335 | } 1336 | 1337 | /** 1338 | * Scale a Matrix4. 1339 | * @param {Matrix4} m 1340 | * @param {Vector3} v 1341 | * @param {Matrix4?} out 1342 | * @return {Matrix4} 1343 | */ 1344 | static scale(m, v, out) { 1345 | if (out === undefined) { 1346 | out = m.clone(); 1347 | } else { 1348 | out.set(m); 1349 | } 1350 | return out.scale(v); 1351 | } 1352 | 1353 | /** 1354 | * Multiply two Matrix4s. 1355 | * @param {Matrix4} a 1356 | * @param {Matrix4} b 1357 | * @param {Matrix4?} out 1358 | * @return {Matrix4} 1359 | */ 1360 | static multiply(a, b, out) { 1361 | out = out || new Matrix4(); 1362 | 1363 | const o = out; 1364 | 1365 | const a00 = a[0]; 1366 | const a01 = a[1]; 1367 | const a02 = a[2]; 1368 | const a03 = a[3]; 1369 | const a10 = a[4]; 1370 | const a11 = a[5]; 1371 | const a12 = a[6]; 1372 | const a13 = a[7]; 1373 | const a20 = a[8]; 1374 | const a21 = a[9]; 1375 | const a22 = a[10]; 1376 | const a23 = a[11]; 1377 | const a30 = a[12]; 1378 | const a31 = a[13]; 1379 | const a32 = a[14]; 1380 | const a33 = a[15]; 1381 | 1382 | let b0 = b[0]; 1383 | let b1 = b[1]; 1384 | let b2 = b[2]; 1385 | let b3 = b[3]; 1386 | o[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 1387 | o[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 1388 | o[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 1389 | o[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 1390 | 1391 | b0 = b[4]; 1392 | b1 = b[5]; 1393 | b2 = b[6]; 1394 | b3 = b[7]; 1395 | o[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 1396 | o[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 1397 | o[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 1398 | o[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 1399 | 1400 | b0 = b[8]; 1401 | b1 = b[9]; 1402 | b2 = b[10]; 1403 | b3 = b[11]; 1404 | o[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 1405 | o[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 1406 | o[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 1407 | o[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 1408 | 1409 | b0 = b[12]; 1410 | b1 = b[13]; 1411 | b2 = b[14]; 1412 | b3 = b[15]; 1413 | o[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 1414 | o[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 1415 | o[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 1416 | o[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 1417 | 1418 | return out; 1419 | } 1420 | } 1421 | 1422 | /** 1423 | * @property {Matrix4} Zero A Matrix4 filled with zeros. 1424 | */ 1425 | Matrix4.Zero = new Matrix4(0); 1426 | 1427 | /** 1428 | * @property {Matrix4} Identity An identity Matrix4. 1429 | */ 1430 | Matrix4.Identity = new Matrix4(); 1431 | --------------------------------------------------------------------------------