├── 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 | 
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 |
--------------------------------------------------------------------------------