Major refactor to block assigning

* Removed assigners as there was significant overlap
* Added explicit dithering options
This commit is contained in:
Lucas Dower 2022-11-04 14:41:20 +00:00
parent cbc53ddfe5
commit ec532586cd
21 changed files with 163 additions and 184 deletions

View File

@ -13,5 +13,6 @@
"CAMERA_DEFAULT_AZIMUTH_RADIANS": -1.0,
"CAMERA_DEFAULT_ELEVATION_RADIANS": 1.3,
"CAMERA_SENSITIVITY_ROTATION": 0.005,
"CAMERA_SENSITIVITY_ZOOM": 0.005
"CAMERA_SENSITIVITY_ZOOM": 0.005,
"DITHER_MAGNITUDE": 32
}

View File

@ -328,7 +328,7 @@ export class AppContext {
params: {
textureAtlas: uiElements.textureAtlas.getCachedValue(),
blockPalette: uiElements.blockPalette.getCachedValue(),
blockAssigner: uiElements.dithering.getCachedValue(),
dithering: uiElements.dithering.getCachedValue(),
colourSpace: ColourSpace.RGB,
fallable: uiElements.fallable.getCachedValue() as FallableBehaviour,
resolution: Math.pow(2, uiElements.colourAccuracy.getCachedValue()),

View File

@ -1,19 +0,0 @@
import { IBlockAssigner } from './base_assigner';
import { BasicBlockAssigner } from './basic_assigner';
import { OrderedDitheringBlockAssigner } from './ordered_dithering_assigner';
import { RandomDitheringBlockAssigner } from './random_dithering_assigner';
export type TBlockAssigners = 'basic' | 'ordered-dithering' | 'random-dithering';
export class BlockAssignerFactory {
public static GetAssigner(blockAssigner: TBlockAssigners): IBlockAssigner {
switch (blockAssigner) {
case 'basic':
return new BasicBlockAssigner();
case 'ordered-dithering':
return new OrderedDitheringBlockAssigner();
case 'random-dithering':
return new RandomDitheringBlockAssigner();
}
}
}

View File

@ -1,9 +0,0 @@
import { AtlasPalette, TBlockCollection } from '../block_assigner';
import { BlockInfo } from '../block_atlas';
import { RGBA, RGBAUtil } from '../colour';
import { ColourSpace } from '../util';
import { Vector3 } from '../vector';
export interface IBlockAssigner {
assignBlock(atlasPalette: AtlasPalette, voxelColour: RGBA, voxelPosition: Vector3, resolution: RGBAUtil.TColourAccuracy, colourSpace: ColourSpace, blockCollection: TBlockCollection): BlockInfo;
}

View File

@ -1,12 +0,0 @@
import { AtlasPalette, TBlockCollection } from '../block_assigner';
import { BlockInfo } from '../block_atlas';
import { RGBA, RGBAUtil } from '../colour';
import { ColourSpace } from '../util';
import { Vector3 } from '../vector';
import { IBlockAssigner } from './base_assigner';
export class BasicBlockAssigner implements IBlockAssigner {
assignBlock(atlasPalette: AtlasPalette, voxelColour: RGBA, voxelPosition: Vector3, resolution: RGBAUtil.TColourAccuracy, colourSpace: ColourSpace, blockCollection: TBlockCollection): BlockInfo {
return atlasPalette.getBlock(voxelColour, blockCollection, resolution);
}
}

View File

@ -1,50 +0,0 @@
import { AtlasPalette, TBlockCollection } from '../block_assigner';
import { BlockInfo } from '../block_atlas';
import { RGBA, RGBAUtil } from '../colour';
import { ColourSpace } from '../util';
import { ASSERT } from '../util/error_util';
import { Vector3 } from '../vector';
import { IBlockAssigner } from './base_assigner';
export class OrderedDitheringBlockAssigner implements IBlockAssigner {
/** 4x4x4 */
private static _size = 4;
private static _threshold = 256 / 8;
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)) - 0.5;
}
assignBlock(atlasPalette: AtlasPalette, voxelColour: RGBA, voxelPosition: Vector3, resolution: RGBAUtil.TColourAccuracy, colourSpace: ColourSpace, blockCollection: TBlockCollection): 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: RGBA = {
r: ((255 * voxelColour.r) + map * OrderedDitheringBlockAssigner._threshold) / 255,
g: ((255 * voxelColour.g) + map * OrderedDitheringBlockAssigner._threshold) / 255,
b: ((255 * voxelColour.b) + map * OrderedDitheringBlockAssigner._threshold) / 255,
a: ((255 * voxelColour.a) + map * OrderedDitheringBlockAssigner._threshold) / 255,
};
return atlasPalette.getBlock(newVoxelColour, blockCollection, resolution);
}
}

View File

@ -1,23 +0,0 @@
import { AtlasPalette, TBlockCollection } from '../block_assigner';
import { BlockInfo } from '../block_atlas';
import { RGBA, RGBAUtil } from '../colour';
import { ColourSpace } from '../util';
import { Vector3 } from '../vector';
import { IBlockAssigner } from './base_assigner';
export class RandomDitheringBlockAssigner implements IBlockAssigner {
private static _deviation = 32;
assignBlock(atlasPalette: AtlasPalette, voxelColour: RGBA, voxelPosition: Vector3, resolution: RGBAUtil.TColourAccuracy, colourSpace: ColourSpace, blockCollection: TBlockCollection): BlockInfo {
const map = Math.random() - 0.5;
const newVoxelColour: RGBA = {
r: ((255 * voxelColour.r) + map * RandomDitheringBlockAssigner._deviation) / 255,
g: ((255 * voxelColour.g) + map * RandomDitheringBlockAssigner._deviation) / 255,
b: ((255 * voxelColour.b) + map * RandomDitheringBlockAssigner._deviation) / 255,
a: ((255 * voxelColour.a) + map * RandomDitheringBlockAssigner._deviation) / 255,
};
return atlasPalette.getBlock(newVoxelColour, blockCollection, resolution);
}
}

View File

@ -1,5 +1,5 @@
import { Atlas, TAtlasBlock } from './atlas';
import { RGBA, RGBAUtil } from './colour';
import { RGBA, RGBA_255, RGBAUtil } from './colour';
import { Palette } from './palette';
import { AppTypes, TOptional } from './util';
import { ASSERT } from './util/error_util';
@ -62,8 +62,8 @@ export class AtlasPalette {
* @param blockToExclude A list of blocks that should not be used, this should be a subset of the palette blocks.
* @returns
*/
public getBlock(colour: RGBA, blockCollection: TBlockCollection, resolution: RGBAUtil.TColourAccuracy) {
const { colourHash, binnedColour } = RGBAUtil.bin(colour, resolution);
public getBlock(colour: RGBA_255, blockCollection: TBlockCollection) {
const colourHash = RGBAUtil.hash255(colour);
// If we've already calculated the block associated with this colour, return it.
const cachedBlock = blockCollection.cache.get(colourHash);
@ -76,7 +76,7 @@ export class AtlasPalette {
let blockChoice: TOptional<TAtlasBlock>;
{
blockCollection.blocks.forEach((blockData) => {
const colourDistance = RGBAUtil.squaredDistance(binnedColour, blockData.colour);
const colourDistance = RGBAUtil.squaredDistance(RGBAUtil.fromRGBA255(colour), blockData.colour);
if (colourDistance < minDistance) {
minDistance = colourDistance;
blockChoice = blockData;

View File

@ -1,10 +1,11 @@
import fs from 'fs';
import { BlockAssignerFactory, TBlockAssigners } from './assigners/assigners';
import { Atlas } from './atlas';
import { AtlasPalette } from './block_assigner';
import { BlockInfo } from './block_atlas';
import { ChunkedBufferGenerator, TBlockMeshBufferDescription } from './buffer';
import { RGBA_255, RGBAUtil } from './colour';
import { Ditherer } from './dither';
import { Palette } from './palette';
import { ProgressManager } from './progress';
import { StatusHandler } from './status';
@ -26,13 +27,12 @@ export type FallableBehaviour = 'replace-falling' | 'replace-fallable' | 'place-
export interface BlockMeshParams {
textureAtlas: Atlas,
blockPalette: Palette,
blockAssigner: TBlockAssigners,
colourSpace: ColourSpace,
fallable: FallableBehaviour,
}
export class BlockMesh {
private _blocksUsed: string[];
private _blocksUsed: Set<string>;
private _blocks: Block[];
private _voxelMesh: VoxelMesh;
private _fallableBlocks: string[];
@ -45,7 +45,7 @@ export class BlockMesh {
}
private constructor(voxelMesh: VoxelMesh) {
this._blocksUsed = [];
this._blocksUsed = new Set();
this._blocks = [];
this._voxelMesh = voxelMesh;
this._atlas = Atlas.getVanillaAtlas()!;
@ -55,6 +55,32 @@ export class BlockMesh {
this._fallableBlocks = JSON.parse(fallableBlocksString).fallable_blocks;
}
/**
* Before we turn a voxel into a block we have the opportunity to alter the voxel's colour.
* This is where the colour accuracy bands colours together and where dithering is calculated.
*/
private _getFinalVoxelColour(voxel: Voxel, blockMeshParams: AssignParams.Input) {
const voxelColour = RGBAUtil.copy(voxel.colour);
const binnedColour = RGBAUtil.bin(voxelColour, blockMeshParams.resolution);
const ditheredColour: RGBA_255 = RGBAUtil.copy255(binnedColour);
switch (blockMeshParams.dithering) {
case 'off': {
break;
}
case 'random': {
Ditherer.ditherRandom(ditheredColour);
break;
}
case 'ordered': {
Ditherer.ditherOrdered(ditheredColour, voxel.position);
break;
}
}
return ditheredColour;
}
private _assignBlocks(blockMeshParams: AssignParams.Input) {
const atlas = Atlas.load(blockMeshParams.textureAtlas);
ASSERT(atlas !== undefined, 'Could not load atlas');
@ -67,54 +93,39 @@ export class BlockMesh {
const allBlockCollection = atlasPalette.createBlockCollection([]);
const nonFallableBlockCollection = atlasPalette.createBlockCollection(this._fallableBlocks);
const blockAssigner = BlockAssignerFactory.GetAssigner(blockMeshParams.blockAssigner);
let countFalling = 0;
const taskHandle = ProgressManager.Get.start('Assigning');
const voxels = this._voxelMesh.getVoxels();
for (let voxelIndex = 0; voxelIndex < voxels.length; ++voxelIndex) {
ProgressManager.Get.progress(taskHandle, voxelIndex / voxels.length);
// Convert the voxel into a block
const voxel = voxels[voxelIndex];
let block = blockAssigner.assignBlock(
atlasPalette,
voxel.colour,
voxel.position,
blockMeshParams.resolution,
blockMeshParams.colourSpace,
allBlockCollection,
);
const voxelColour = this._getFinalVoxelColour(voxel, blockMeshParams);
let block = atlasPalette.getBlock(voxelColour, allBlockCollection);
const isFallable = this._fallableBlocks.includes(block.name);
const isSupported = this._voxelMesh.isVoxelAt(Vector3.add(voxel.position, new Vector3(0, -1, 0)));
// Check that this block meets the fallable behaviour, we may need
// to choose a different block if the current one doesn't meet the requirements
const isBlockFallable = this._fallableBlocks.includes(block.name);
const isBlockSupported = this._voxelMesh.isVoxelAt(Vector3.add(voxel.position, new Vector3(0, -1, 0)));
if (isFallable && !isSupported) {
if (isBlockFallable && !isBlockSupported) {
++countFalling;
}
let shouldReplace = (blockMeshParams.fallable === 'replace-fallable' && isFallable);
shouldReplace ||= (blockMeshParams.fallable === 'replace-falling' && isFallable && !isSupported);
const shouldReplaceBlock =
(blockMeshParams.fallable === 'replace-fallable' && isBlockFallable) ||
(blockMeshParams.fallable === 'replace-falling' && isBlockFallable && !isBlockSupported);
if (shouldReplace) {
const replacedBlock = blockAssigner.assignBlock(
atlasPalette,
voxel.colour,
voxel.position,
blockMeshParams.resolution,
ColourSpace.RGB,
nonFallableBlockCollection,
);
block = replacedBlock;
if (shouldReplaceBlock) {
block = atlasPalette.getBlock(voxelColour, nonFallableBlockCollection);
}
this._blocks.push({
voxel: voxel,
blockInfo: block,
});
if (!this._blocksUsed.includes(block.name)) {
this._blocksUsed.push(block.name);
}
this._blocksUsed.add(block.name);
}
ProgressManager.Get.end(taskHandle);
@ -128,7 +139,7 @@ export class BlockMesh {
}
public getBlockPalette() {
return this._blocksUsed;
return Array.from(this._blocksUsed);
}
public getVoxelMesh() {

View File

@ -1,4 +1,5 @@
import { AppConfig } from './config';
import { TBrand } from './util/type_util';
export type RGBA = {
r: number,
@ -7,7 +8,29 @@ export type RGBA = {
a: number
}
export type RGBA_255 = TBrand<RGBA, '255'>;
export namespace RGBAUtil {
export function toRGBA255(c: RGBA): RGBA_255 {
const out: RGBA = {
r: c.r * 255,
g: c.r * 255,
b: c.r * 255,
a: c.r * 255,
};
return out as RGBA_255;
}
export function fromRGBA255(c: RGBA_255): RGBA {
const out: RGBA = {
r: c.r / 255,
g: c.g / 255,
b: c.b / 255,
a: c.a / 255,
};
return out;
}
export function lerp(a: RGBA, b: RGBA, alpha: number) {
return {
r: a.r * (1 - alpha) + b.r * alpha,
@ -53,32 +76,28 @@ export namespace RGBAUtil {
};
}
export function copy255(a: RGBA_255): RGBA_255 {
return {
r: a.r,
g: a.g,
b: a.b,
a: a.a,
} as RGBA_255;
}
export function toArray(a: RGBA): number[] {
return [a.r, a.g, a.b, a.a];
}
export function bin(col: RGBA, resolution: TColourAccuracy) {
const r = Math.floor(col.r * resolution);
const g = Math.floor(col.g * resolution);
const b = Math.floor(col.b * resolution);
const a = Math.ceil(col.a * resolution);
let hash = r;
hash = (hash << 8) + g;
hash = (hash << 8) + b;
hash = (hash << 8) + a;
const binnedColour: RGBA = {
r: r / resolution,
g: g / resolution,
b: b / resolution,
a: a / resolution,
r: Math.floor(Math.floor(col.r * resolution) * (255 / resolution)),
g: Math.floor(Math.floor(col.g * resolution) * (255 / resolution)),
b: Math.floor(Math.floor(col.b * resolution) * (255 / resolution)),
a: Math.floor(Math.ceil(col.a * resolution) * (255 / resolution)),
};
return {
colourHash: hash,
binnedColour: binnedColour,
};
return binnedColour as RGBA_255;
}
/**
@ -100,6 +119,14 @@ export namespace RGBAUtil {
return hash;
}
export function hash255(col: RGBA_255) {
let hash = col.r;
hash = (hash << 8) + col.g;
hash = (hash << 8) + col.b;
hash = (hash << 8) + col.a;
return hash;
}
export type TColourAccuracy = number;
}

View File

@ -30,6 +30,7 @@ export class AppConfig {
public readonly CAMERA_DEFAULT_ELEVATION_RADIANS: number;
public readonly CAMERA_SENSITIVITY_ROTATION: number;
public readonly CAMERA_SENSITIVITY_ZOOM: number;
public readonly DITHER_MAGNITUDE: number;
private constructor() {
this.RELEASE_MODE = false;
@ -54,6 +55,7 @@ export class AppConfig {
this.CAMERA_DEFAULT_ELEVATION_RADIANS = configJSON.CAMERA_DEFAULT_ELEVATION_RADIANS;
this.CAMERA_SENSITIVITY_ROTATION = configJSON.CAMERA_SENSITIVITY_ROTATION;
this.CAMERA_SENSITIVITY_ZOOM = configJSON.CAMERA_SENSITIVITY_ZOOM;
this.DITHER_MAGNITUDE = configJSON.DITHER_MAGNITUDE;
}
public dumpConfig() {

47
src/dither.ts Normal file
View File

@ -0,0 +1,47 @@
import { RGBA_255 } from './colour';
import { AppConfig } from './config';
import { ASSERT } from './util/error_util';
import { Vector3 } from './vector';
export class Ditherer {
public static ditherRandom(colour: RGBA_255) {
const offset = (Math.random() - 0.5) * AppConfig.Get.DITHER_MAGNITUDE;
colour.r += offset;
colour.g += offset;
colour.b += offset;
}
public static ditherOrdered(colour: RGBA_255, position: Vector3) {
const map = this._getThresholdValue(
Math.abs(position.x % 4),
Math.abs(position.y % 4),
Math.abs(position.z % 4),
);
const offset = map * AppConfig.Get.DITHER_MAGNITUDE;
colour.r += offset;
colour.g += offset;
colour.b += offset;
}
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 static _getThresholdValue(x: number, y: number, z: number) {
const size = 4;
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 (Ditherer._mapMatrix[index] / (size * size * size)) - 0.5;
}
}

View File

@ -1,7 +1,6 @@
import fs from 'fs';
import { AppContext } from '../app_context';
import { TBlockAssigners } from '../assigners/assigners';
import { ArcballCamera } from '../camera';
import { TExporters } from '../exporters/exporters';
import { PaletteManager } from '../palette';
@ -10,6 +9,7 @@ import { EAction } from '../util';
import { ASSERT } from '../util/error_util';
import { LOG } from '../util/log_util';
import { AppPaths } from '../util/path_util';
import { TDithering } from '../util/type_util';
import { TVoxelOverlapRule } from '../voxel_mesh';
import { TVoxelisers } from '../voxelisers/voxelisers';
import { BaseUIElement } from './elements/base';
@ -125,10 +125,10 @@ export class UI {
elements: {
'textureAtlas': new ComboBoxElement('Texture atlas', this._getTextureAtlases()),
'blockPalette': new ComboBoxElement('Block palette', this._getBlockPalettes()),
'dithering': new ComboBoxElement<TBlockAssigners>('Dithering', [
{ id: 'ordered-dithering', displayText: 'Ordered' },
{ id: 'random-dithering', displayText: 'Random' },
{ id: 'basic', displayText: 'Off' },
'dithering': new ComboBoxElement<TDithering>('Dithering', [
{ id: 'ordered', displayText: 'Ordered' },
{ id: 'random', displayText: 'Random' },
{ id: 'off', displayText: 'Off' },
]),
'fallable': new ComboBoxElement('Fallable blocks', [
{

3
src/util/type_util.ts Normal file
View File

@ -0,0 +1,3 @@
export type TBrand<K, T> = K & { __brand: T };
export type TDithering = 'off' | 'random' | 'ordered';

View File

@ -1,12 +1,13 @@
import { TBlockAssigners } from './assigners/assigners';
import { FallableBehaviour } from './block_mesh';
import { TBlockMeshBufferDescription, TMeshBufferDescription, TVoxelMeshBufferDescription } from './buffer';
import { RGBAUtil } from './colour';
import { Ditherer } from './dither';
import { TExporters } from './exporters/exporters';
import { StatusMessage } from './status';
import { TextureFiltering } from './texture';
import { ColourSpace } from './util';
import { AppError } from './util/error_util';
import { TDithering } from './util/type_util';
import { Vector3 } from './vector';
import { TVoxelOverlapRule } from './voxel_mesh';
import { TVoxelisers } from './voxelisers/voxelisers';
@ -92,7 +93,7 @@ export namespace AssignParams {
export type Input = {
textureAtlas: TAtlasId,
blockPalette: TPaletteId,
blockAssigner: TBlockAssigners,
dithering: TDithering,
colourSpace: ColourSpace,
fallable: FallableBehaviour,
resolution: RGBAUtil.TColourAccuracy,

View File

@ -19,7 +19,7 @@ const baseConfig: THeadlessConfig = {
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
dithering: 'ordered',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
resolution: 32,

View File

@ -19,7 +19,7 @@ const baseConfig: THeadlessConfig = {
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
dithering: 'ordered',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
resolution: 32,

View File

@ -19,7 +19,7 @@ const baseConfig: THeadlessConfig = {
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
dithering: 'ordered',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
resolution: 32,

View File

@ -19,7 +19,7 @@ const baseConfig: THeadlessConfig = {
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
dithering: 'ordered',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
resolution: 32,

View File

@ -10,7 +10,7 @@ test('Random-dither', () => {
const config = headlessConfig;
config.import.filepath = PathUtil.join(AppPaths.Get.resources, './samples/skull.obj');
config.assign.blockAssigner = 'random-dithering';
config.assign.dithering = 'random';
const worker = WorkerClient.Get;
worker.import(headlessConfig.import);

View File

@ -17,7 +17,7 @@ export const headlessConfig: THeadlessConfig = {
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: 'all-snapshot', // Must be a palette name that exists in /resources/palettes
blockAssigner: 'ordered-dithering',
dithering: 'ordered',
colourSpace: ColourSpace.RGB,
fallable: 'replace-falling',
resolution: 32,