diff --git a/src/block_assigner.ts b/src/block_assigner.ts new file mode 100644 index 0000000..76ae750 --- /dev/null +++ b/src/block_assigner.ts @@ -0,0 +1,57 @@ +import { BlockAtlas, BlockInfo } from "./block_atlas"; +import { assert, RGB } from "./util"; +import { Vector3 } from "./vector"; + +interface BlockAssigner { + assignBlock(voxelColour: RGB, voxelPosition: Vector3): BlockInfo; +} + +export class BasicBlockAssigner implements BlockAssigner { + assignBlock(voxelColour: RGB, voxelPosition: Vector3): BlockInfo { + return BlockAtlas.Get.getBlock(voxelColour); + } +} + +export class OrderedDitheringBlockAssigner implements BlockAssigner { + + /** 4x4x4 */ + private static _size = 4; + private static _threshold = 256/4; + + private static _mapMatrix = [ + 0, 16, 2, 18, 48, 32, 50, 34, + 6, 22, 4, 20, 54, 38, 52, 36, + 24, 40, 26, 42, 8, 56, 10, 58, + 30, 46, 28, 44, 14, 62, 12, 60, + 3, 19, 5, 21, 51, 35, 53, 37, + 1, 17, 7, 23, 49, 33, 55, 39, + 27, 43, 29, 45, 11, 59, 13, 61, + 25, 41, 31, 47, 9, 57, 15, 63 + ]; + + private _getThresholdValue(x: number, y: number, z: number) { + const size = OrderedDitheringBlockAssigner._size; + assert(0 <= x && x < size && 0 <= y && y < size && 0 <= z && z < size) + const index = (x + (size * y) + (size * size * z)); + assert(0 <= index && index < size * size * size); + return OrderedDitheringBlockAssigner._mapMatrix[index] / (size * size * size); + } + + assignBlock(voxelColour: RGB, voxelPosition: Vector3): BlockInfo { + const size = OrderedDitheringBlockAssigner._size; + const map = this._getThresholdValue( + Math.abs(voxelPosition.x % size), + Math.abs(voxelPosition.y % size), + Math.abs(voxelPosition.z % size) + ); + + const newVoxelColour = { + r: ((255 * voxelColour.r) + map * OrderedDitheringBlockAssigner._threshold) / 255, + g: ((255 * voxelColour.g) + map * OrderedDitheringBlockAssigner._threshold) / 255, + b: ((255 * voxelColour.b) + map * OrderedDitheringBlockAssigner._threshold) / 255 + }; + + return BlockAtlas.Get.getBlock(newVoxelColour); + + } +} \ No newline at end of file diff --git a/src/block_atlas.ts b/src/block_atlas.ts index ccaa067..ea9b494 100644 --- a/src/block_atlas.ts +++ b/src/block_atlas.ts @@ -35,11 +35,17 @@ export enum Block { export class BlockAtlas { - private readonly _cachedBlocks: HashMap; + private _cachedBlocks: HashMap; private readonly _blocks: Array; public readonly _atlasSize: number; - constructor() { + private static _instance: BlockAtlas; + + public static get Get() { + return this._instance || (this._instance = new this()); + } + + private constructor() { this._cachedBlocks = new HashMap(1024); const _path = path.join(__dirname, "../resources/blocks.json"); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..3580c90 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,14 @@ +// TODO: Replace with UI options + +export namespace AppConfig { + + /** Recommended, but slower */ + export const AMBIENT_OCCLUSION_ENABLED = true; + + /** Darkens corner even if corner block does not exist, recommended */ + export const AMBIENT_OCCLUSION_OVERRIDE_CORNER = true; + + /** Recomended, better matches colours */ + export const DITHERING_ENABLED = true; + +} \ No newline at end of file diff --git a/src/renderer.ts b/src/renderer.ts index 4084efa..036423a 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -11,13 +11,8 @@ import { RGB, UV, rgbToArray } from "./util"; import { VoxelManager } from "./voxel_manager"; import { Triangle } from "./triangle"; import { Mesh, FillMaterial, TextureMaterial, MaterialType } from "./mesh"; -import { FaceInfo } from "./block_atlas"; - -/** Recommended, but slower */ -const ENABLE_AMBIENT_OCCLUSION = true; - -/** Darkens corner even if corner block does not exist, recommended */ -const AMBIENT_OCCLUSION_OVERRIDE_CORNER = true; +import { FaceInfo, BlockAtlas } from "./block_atlas"; +import { AppConfig } from "./config" export class Renderer { @@ -101,7 +96,7 @@ export class Renderer { let occlusions = new Array>(6); // For each face for (let f = 0; f < 6; ++f) { - occlusions[f] = [0, 0, 0, 0]; + occlusions[f] = [1, 1, 1, 1]; // Only compute ambient occlusion if this face is visible const faceNormal = Renderer._faceNormal[f]; @@ -118,7 +113,7 @@ export class Renderer { // If both edge blocks along this vertex exist, // assume corner exists (even if it doesnt) // (This is a stylistic choice) - if (numNeighbours == 2 && AMBIENT_OCCLUSION_OVERRIDE_CORNER) { + if (numNeighbours == 2 && AppConfig.AMBIENT_OCCLUSION_OVERRIDE_CORNER) { ++numNeighbours; } else { const neighbourIndex = this._occlusionNeighboursIndices[f][v][2]; @@ -144,9 +139,9 @@ export class Renderer { return blankOcclusions; } - private _registerVoxel(centre: Vector3, voxelManager: VoxelManager, blockTexcoord: FaceInfo) { + private _registerVoxel(centre: Vector3, blockTexcoord: FaceInfo) { let occlusions: number[][]; - if (ENABLE_AMBIENT_OCCLUSION) { + if (AppConfig.AMBIENT_OCCLUSION_ENABLED) { occlusions = this._calculateOcclusions(centre); } else { occlusions = Renderer._getBlankOcclusions(); @@ -182,9 +177,6 @@ export class Renderer { } public registerMesh(mesh: Mesh) { - //console.log(mesh); - this._gl.disable(this._gl.CULL_FACE); - mesh.materials.forEach(material => { const materialBuffer = new BottomlessBuffer([ { name: 'position', numComponents: 3 }, @@ -194,7 +186,6 @@ export class Renderer { material.faces.forEach(face => { const data = GeometryTemplates.getTriangleBufferData(face, false); - //console.log(data); materialBuffer.add(data); }); @@ -210,7 +201,7 @@ export class Renderer { const voxelManager = VoxelManager.Get; - this._atlasSize = voxelManager.blockAtlas._atlasSize; + this._atlasSize = BlockAtlas.Get._atlasSize; if (this._debug) { voxelManager.voxels.forEach((voxel) => { @@ -222,7 +213,7 @@ export class Renderer { const voxel = voxelManager.voxels[i]; //const colour = voxelManager.voxelColours[i]; const texcoord = voxelManager.voxelTexcoords[i]; - this._registerVoxel(voxel.position, voxelManager, texcoord); + this._registerVoxel(voxel.position, texcoord); } } } @@ -235,7 +226,6 @@ export class Renderer { compile() { this._registerDebug.compile(this._gl); this._registerVoxels.compile(this._gl); - //this._registerDefault.compile(this._gl); this._materialBuffers.forEach((materialBuffer) => { materialBuffer.buffer.compile(this._gl); @@ -376,7 +366,6 @@ export class Renderer { this._gl.blendFuncSeparate(this._gl.SRC_ALPHA, this._gl.ONE_MINUS_SRC_ALPHA, this._gl.ONE, this._gl.ONE_MINUS_SRC_ALPHA); this._gl.enable(this._gl.DEPTH_TEST); - //this._gl.enable(this._gl.CULL_FACE); this._gl.enable(this._gl.BLEND); this._gl.clearColor(this._backgroundColour.r, this._backgroundColour.g, this._backgroundColour.b, 1.0); this._gl.clear(this._gl.COLOR_BUFFER_BIT | this._gl.DEPTH_BUFFER_BIT); diff --git a/src/util.ts b/src/util.ts index 5fd9f38..d9109f3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -15,6 +15,15 @@ export interface RGB { b: number } +export function getAverageColour(colours: Array) { + let averageColour = colours.reduce((a, c) => { return { r: a.r + c.r, g: a.g + c.g, b: a.b + c.b } }); + let n = colours.length; + averageColour.r /= n; + averageColour.g /= n; + averageColour.b /= n; + return averageColour; +} + export function rgbToArray(rgb: RGB) { return [rgb.r, rgb.g, rgb.b]; } @@ -36,4 +45,10 @@ export interface Bounds { maxX: number, maxY: number, maxZ: number, +} + +export function assert(condition: boolean, errorMessage: string = "Assertion Failed") { + if (!condition) { + throw Error(errorMessage); + } } \ No newline at end of file diff --git a/src/voxel_manager.ts b/src/voxel_manager.ts index 8ab432f..79fa1c7 100644 --- a/src/voxel_manager.ts +++ b/src/voxel_manager.ts @@ -2,11 +2,13 @@ import { Vector3 } from "./vector.js"; import { HashMap } from "./hash_map"; import { Texture } from "./texture"; import { BlockAtlas, BlockInfo, FaceInfo } from "./block_atlas"; -import { RGB } from "./util"; +import { RGB, getAverageColour } from "./util"; import { Triangle } from "./triangle"; import { Mesh, MaterialType } from "./mesh"; import { triangleArea } from "./math"; import { Axes, generateRays, rayIntersectTriangle } from "./ray"; +import { BasicBlockAssigner, OrderedDitheringBlockAssigner } from "./block_assigner.js"; +import { AppConfig } from "./config.js"; interface Block { position: Vector3; @@ -22,7 +24,6 @@ export class VoxelManager { public _voxelSize: number; private voxelsHash: HashMap; - public blockAtlas: BlockAtlas; private _blockMode!: MaterialType; private _currentTexture!: Texture; private _currentColour!: RGB; @@ -43,7 +44,6 @@ export class VoxelManager { this.voxelTexcoords = []; this.voxelsHash = new HashMap(2048); - this.blockAtlas = new BlockAtlas(); this.blockPalette = []; } @@ -54,7 +54,7 @@ export class VoxelManager { private _clearVoxels() { this.voxels = []; this.voxelTexcoords = []; - this.blockPalette = [] + this.blockPalette = []; this.min = new Vector3( Infinity, Infinity, Infinity); this.max = new Vector3(-Infinity, -Infinity, -Infinity); @@ -66,33 +66,35 @@ export class VoxelManager { return this.voxelsHash.has(pos); } + private _assignBlock(voxelIndex: number, block: BlockInfo) { + this.voxels[voxelIndex].block = block.name; + this.voxelTexcoords.push(block.faces); + + if (!this.blockPalette.includes(block.name)) { + this.blockPalette.push(block.name); + } + } + public assignBlocks() { this.blockPalette = []; let meanSquaredError = 0.0; for (let i = 0; i < this.voxels.length; ++i) { - let averageColour = this.voxels[i].colours!.reduce((a, c) => {return {r: a.r + c.r, g: a.g + c.g, b: a.b + c.b}}) - let n = this.voxels[i].colours!.length; - averageColour.r /= n; - averageColour.g /= n; - averageColour.b /= n; - const block = this.blockAtlas.getBlock(averageColour); + const voxel = this.voxels[i]; + + const averageColour = getAverageColour(voxel.colours!); + + const blockAssigner = AppConfig.DITHERING_ENABLED ? new OrderedDitheringBlockAssigner() : new BasicBlockAssigner(); + const block = blockAssigner.assignBlock(averageColour, voxel.position); const squaredError = Math.pow(255 * (block.colour.r - averageColour.r), 2) + Math.pow(255 * (block.colour.g - averageColour.g), 2) + Math.pow(255 * (block.colour.b - averageColour.b), 2); meanSquaredError += squaredError; - this.voxels[i].block = block.name; - this.voxelTexcoords.push(block.faces); - - - if (!this.blockPalette.includes(block.name)) { - this.blockPalette.push(block.name); - } + this._assignBlock(i, block); } meanSquaredError /= this.voxels.length; console.log("Mean Squared Error:", meanSquaredError); - } public addVoxel(pos: Vector3, block: BlockInfo) { @@ -167,7 +169,7 @@ export class VoxelManager { } const voxelColour = this._getVoxelColour(triangle, Vector3.mulScalar(voxelPosition, voxelSize)); - const block = this.blockAtlas.getBlock(voxelColour); + const block = BlockAtlas.Get.getBlock(voxelColour); this.addVoxel(voxelPosition, block); }