Merge pull request #83 from LucasDower/0.7-smoothness

Added smoothness option, i.e. error weighting
This commit is contained in:
Lucas Dower 2022-11-05 19:49:07 +00:00 committed by GitHub
commit 437eb292e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 4850 additions and 2912 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View File

@ -14,5 +14,6 @@
"CAMERA_DEFAULT_ELEVATION_RADIANS": 1.3,
"CAMERA_SENSITIVITY_ROTATION": 0.005,
"CAMERA_SENSITIVITY_ZOOM": 0.005,
"DITHER_MAGNITUDE": 32
"DITHER_MAGNITUDE": 32,
"SMOOTHNESS_MAX": 3.0
}

View File

@ -333,6 +333,7 @@ export class AppContext {
fallable: uiElements.fallable.getCachedValue() as FallableBehaviour,
resolution: Math.pow(2, uiElements.colourAccuracy.getCachedValue()),
contextualAveraging: uiElements.contextualAveraging.getCachedValue(),
errorWeight: uiElements.errorWeight.getCachedValue() / 10,
},
};

View File

@ -8,9 +8,10 @@ import { LOG } from './util/log_util';
import { AppPaths } from './util/path_util';
export type TAtlasBlockFace = {
name: string;
texcoord: UV;
colour: RGBA;
name: string,
texcoord: UV,
colour: RGBA,
std: number,
}
export type TAtlasBlock = {
@ -33,7 +34,6 @@ export type TAtlasBlock = {
*/
export class Atlas {
public static ATLAS_NAME_REGEX: RegExp = /^[a-zA-Z\-]+$/;
private static _FILE_VERSION: number = 1;
private _blocks: Map<AppTypes.TNamespacedBlockName, TAtlasBlock>;
private _atlasSize: number;
@ -66,7 +66,7 @@ export class Atlas {
const atlasJSON = JSON.parse(atlasFile);
const atlasVersion = atlasJSON.version;
if (atlasVersion !== 2) {
if (atlasVersion !== 3) {
throw new AppError(`The '${atlasName}' texture atlas uses an outdated format and needs to be recreated`);
}

View File

@ -1,5 +1,6 @@
import { Atlas, TAtlasBlock } from './atlas';
import { RGBA, RGBA_255, RGBAUtil } from './colour';
import { AppMath } from './math';
import { Palette } from './palette';
import { AppTypes, TOptional } from './util';
import { ASSERT } from './util/error_util';
@ -9,6 +10,7 @@ export type TBlockCollection = {
cache: Map<BigInt, TAtlasBlock>,
}
/* eslint-disable */
export enum EFaceVisibility {
Up = 1 << 0,
Down = 1 << 1,
@ -17,6 +19,7 @@ export enum EFaceVisibility {
South = 1 << 4,
West = 1 << 5,
}
/* eslint-enable */
/**
* A new instance of AtlasPalette is created each time
@ -71,7 +74,7 @@ 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_255, blockCollection: TBlockCollection, faceVisibility: EFaceVisibility) {
public getBlock(colour: RGBA_255, blockCollection: TBlockCollection, faceVisibility: EFaceVisibility, errorWeight: number) {
const colourHash = RGBAUtil.hash255(colour);
const contextHash: BigInt = (BigInt(colourHash) << BigInt(6)) + BigInt(faceVisibility);
@ -82,16 +85,19 @@ export class AtlasPalette {
}
// Find closest block in colour
let minDistance = Infinity;
let minError = Infinity;
let blockChoice: TOptional<TAtlasBlock>;
{
blockCollection.blocks.forEach((blockData) => {
const contextualBlockColour = faceVisibility !== 0 ?
AtlasPalette.getContextualFaceAverage(blockData, faceVisibility) :
blockData.colour;
const colourDistance = RGBAUtil.squaredDistance(RGBAUtil.fromRGBA255(colour), contextualBlockColour);
if (colourDistance < minDistance) {
minDistance = colourDistance;
const context = AtlasPalette.getContextualFaceAverage(blockData, faceVisibility);
const contextualBlockColour = faceVisibility !== 0 ? context.colour : blockData.colour;
const contextualStd = faceVisibility !== 0 ? context.std : 0.0;
const floatColour = RGBAUtil.fromRGBA255(colour);
const rgbError = RGBAUtil.squaredDistance(floatColour, contextualBlockColour);
const stdError = contextualStd;
const totalError = AppMath.lerp(errorWeight, rgbError, stdError);
if (totalError < minError) {
minError = totalError;
blockChoice = blockData;
}
});
@ -106,36 +112,46 @@ export class AtlasPalette {
}
public static getContextualFaceAverage(block: TAtlasBlock, faceVisibility: EFaceVisibility) {
const average: RGBA = { r: 0, g: 0, b: 0, a: 0 };
const averageColour: RGBA = { r: 0, g: 0, b: 0, a: 0 };
let averageStd: number = 0.0; // Taking the average of a std is a bit naughty
let count = 0;
if (faceVisibility & EFaceVisibility.Up) {
RGBAUtil.add(average, block.faces.up.colour);
RGBAUtil.add(averageColour, block.faces.up.colour);
averageStd += block.faces.up.std;
++count;
}
if (faceVisibility & EFaceVisibility.Down) {
RGBAUtil.add(average, block.faces.down.colour);
RGBAUtil.add(averageColour, block.faces.down.colour);
averageStd += block.faces.down.std;
++count;
}
if (faceVisibility & EFaceVisibility.North) {
RGBAUtil.add(average, block.faces.north.colour);
RGBAUtil.add(averageColour, block.faces.north.colour);
averageStd += block.faces.north.std;
++count;
}
if (faceVisibility & EFaceVisibility.East) {
RGBAUtil.add(average, block.faces.east.colour);
RGBAUtil.add(averageColour, block.faces.east.colour);
averageStd += block.faces.east.std;
++count;
}
if (faceVisibility & EFaceVisibility.South) {
RGBAUtil.add(average, block.faces.south.colour);
RGBAUtil.add(averageColour, block.faces.south.colour);
averageStd += block.faces.south.std;
++count;
}
if (faceVisibility & EFaceVisibility.West) {
RGBAUtil.add(average, block.faces.west.colour);
RGBAUtil.add(averageColour, block.faces.west.colour);
averageStd += block.faces.west.std;
++count;
}
average.r /= count;
average.g /= count;
average.b /= count;
average.a /= count;
return average;
averageColour.r /= count;
averageColour.g /= count;
averageColour.b /= count;
averageColour.a /= count;
return {
colour: averageColour,
std: averageStd / count,
};
}
}

View File

@ -106,7 +106,7 @@ export class BlockMesh {
const faceVisibility = blockMeshParams.contextualAveraging === 'on' ?
this._voxelMesh.getFaceVisibility(voxel.position) :
VoxelMesh.getFullFaceVisibility();
let block = atlasPalette.getBlock(voxelColour, allBlockCollection, faceVisibility);
let block = atlasPalette.getBlock(voxelColour, allBlockCollection, faceVisibility, blockMeshParams.errorWeight);
// 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
@ -122,7 +122,7 @@ export class BlockMesh {
(blockMeshParams.fallable === 'replace-falling' && isBlockFallable && !isBlockSupported);
if (shouldReplaceBlock) {
block = atlasPalette.getBlock(voxelColour, nonFallableBlockCollection, faceVisibility);
block = atlasPalette.getBlock(voxelColour, nonFallableBlockCollection, faceVisibility, blockMeshParams.errorWeight);
}
this._blocks.push({

View File

@ -31,6 +31,7 @@ export class AppConfig {
public readonly CAMERA_SENSITIVITY_ROTATION: number;
public readonly CAMERA_SENSITIVITY_ZOOM: number;
public readonly DITHER_MAGNITUDE: number;
public readonly SMOOTHNESS_MAX: number;
private constructor() {
this.RELEASE_MODE = false;
@ -56,6 +57,7 @@ export class AppConfig {
this.CAMERA_SENSITIVITY_ROTATION = configJSON.CAMERA_SENSITIVITY_ROTATION;
this.CAMERA_SENSITIVITY_ZOOM = configJSON.CAMERA_SENSITIVITY_ZOOM;
this.DITHER_MAGNITUDE = configJSON.DITHER_MAGNITUDE;
this.SMOOTHNESS_MAX = configJSON.SMOOTHNESS_MAX;
}
public dumpConfig() {

View File

@ -8,6 +8,10 @@ export namespace AppMath {
export const RADIANS_180 = degreesToRadians(180.0);
export const RADIANS_270 = degreesToRadians(270.0);
export function lerp(value: number, start: number, end: number) {
return (1 - value) * start + value * end;
}
export function nearlyEqual(a: number, b: number, tolerance: number = 0.0001) {
return Math.abs(a - b) < tolerance;
}

View File

@ -228,7 +228,7 @@ export class Renderer {
this._gridBuffers.x[MeshType.VoxelMesh] = DebugGeometryTemplates.gridX(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
this._gridBuffers.y[MeshType.VoxelMesh] = DebugGeometryTemplates.gridY(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
this._gridBuffers.z[MeshType.VoxelMesh] = DebugGeometryTemplates.gridZ(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
this._modelsAvailable = 2;
this.setModelToUse(MeshType.VoxelMesh);
}
@ -258,7 +258,7 @@ export class Renderer {
this.setModelToUse(MeshType.VoxelMesh);
}
*/
public useBlockMeshChunk(params: RenderNextBlockMeshChunkParams.Output) {
if (params.isFirstChunk) {
this._blockBuffer = [];

View File

@ -24,7 +24,7 @@ export abstract class BaseUIElement<Type> {
}
protected getValue(): Type {
ASSERT(this._value);
ASSERT(this._value !== undefined);
return this._value;
}

View File

@ -101,7 +101,7 @@ export class SliderElement extends LabelledElement<number> {
if (!this._isEnabled) {
return;
}
ASSERT(this._value);
ASSERT(this._value !== undefined);
this._value -= (e.deltaY / 150) * this._step;
this._value = clamp(this._value, this._min, this._max);

View File

@ -26,7 +26,7 @@ export class VectorSpinboxElement extends LabelledElement<Vector3> {
}
public generateInnerHTML() {
ASSERT(this._value, 'Value not found');
ASSERT(this._value !== undefined, 'Value not found');
return `
<div style="display: flex; flex-direction: row;">
<div style="display: flex; flex-direction: row; width: 33%">
@ -140,7 +140,7 @@ export class VectorSpinboxElement extends LabelledElement<Vector3> {
private _updateValue(e: MouseEvent) {
ASSERT(this._isEnabled, 'Not enabled');
ASSERT(this._dragging !== EAxis.None, 'Dragging nothing');
ASSERT(this._value, 'No value to update');
ASSERT(this._value !== undefined, 'No value to update');
const deltaX = e.clientX - this._lastClientX;
this._lastClientX = e.clientX;

View File

@ -2,6 +2,7 @@ import fs from 'fs';
import { AppContext } from '../app_context';
import { ArcballCamera } from '../camera';
import { AppConfig } from '../config';
import { TExporters } from '../exporters/exporters';
import { PaletteManager } from '../palette';
import { MeshType, Renderer } from '../renderer';
@ -165,8 +166,9 @@ export class UI {
displayText: 'Off (faster)',
},
]),
'errorWeight': new SliderElement('Smoothness', 0.0, AppConfig.Get.SMOOTHNESS_MAX, 2, 0.2, 0.01),
},
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'fallable', 'colourAccuracy', 'contextualAveraging'],
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'fallable', 'colourAccuracy', 'contextualAveraging', 'errorWeight'],
submitButton: new ButtonElement('Assign blocks', () => {
this._appContext.do(EAction.Assign);
}),

View File

@ -98,6 +98,7 @@ export namespace AssignParams {
fallable: FallableBehaviour,
resolution: RGBAUtil.TColourAccuracy,
contextualAveraging: TToggle,
errorWeight: number,
}
export type Output = {

View File

@ -24,6 +24,7 @@ const baseConfig: THeadlessConfig = {
fallable: 'replace-falling',
resolution: 32,
contextualAveraging: 'on',
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

View File

@ -24,6 +24,7 @@ const baseConfig: THeadlessConfig = {
fallable: 'replace-falling',
resolution: 32,
contextualAveraging: 'on',
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

View File

@ -24,6 +24,7 @@ const baseConfig: THeadlessConfig = {
fallable: 'replace-falling',
resolution: 32,
contextualAveraging: 'on',
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

View File

@ -24,6 +24,7 @@ const baseConfig: THeadlessConfig = {
fallable: 'replace-falling',
resolution: 32,
contextualAveraging: 'on',
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

View File

@ -11,7 +11,7 @@ import { RGBA } from '../src/colour';
import { UV } from '../src/util';
import { AppPaths, PathUtil } from '../src/util/path_util';
import { log, LogStyle } from './logging';
import { ASSERT, getAverageColour, getMinecraftDir, getPermission, isDirSetup } from './misc';
import { ASSERT, getAverageColour, getMinecraftDir, getPermission, getStandardDeviation, isDirSetup } from './misc';
const BLOCKS_DIR = PathUtil.join(AppPaths.Get.tools, '/blocks');
const MODELS_DIR = PathUtil.join(AppPaths.Get.tools, '/models');
@ -197,7 +197,8 @@ async function buildAtlas() {
interface Texture {
name: string,
texcoord?: UV,
colour?: RGBA
colour?: RGBA,
std?: number,
}
log(LogStyle.Info, 'Loading block models...');
@ -318,7 +319,7 @@ async function buildAtlas() {
let offsetX = 0;
let offsetY = 0;
const textureDetails: { [textureName: string]: { texcoord: UV, colour: RGBA } } = {};
const textureDetails: { [textureName: string]: { texcoord: UV, colour: RGBA, std: number } } = {};
const { atlasName } = await prompt.get({
properties: {
@ -361,12 +362,14 @@ async function buildAtlas() {
const fileData = fs.readFileSync(absolutePath);
const pngData = PNG.sync.read(fileData);
const avgColour = getAverageColour(pngData);
textureDetails[textureName] = {
texcoord: new UV(
16 * (3 * offsetX + 1) / atlasWidthPixels,
16 * (3 * offsetY + 1) / atlasWidthPixels,
),
colour: getAverageColour(pngData),
colour: avgColour,
std: getStandardDeviation(pngData, avgColour),
};
++offsetX;
@ -394,6 +397,7 @@ async function buildAtlas() {
blockColour.a += faceColour.a;
model.faces[face].texcoord = faceTexture.texcoord;
model.faces[face].colour = faceTexture.colour;
model.faces[face].std = faceTexture.std;
}
blockColour.r /= 6;
blockColour.g /= 6;
@ -410,7 +414,7 @@ async function buildAtlas() {
log(LogStyle.Success, `${atlasName}.png exported to /resources/atlases/`);
const outputJSON = {
version: 2,
version: 3,
atlasSize: atlasSize,
blocks: allModels,
supportedBlockNames: Array.from(allBlockNames),

View File

@ -22,6 +22,7 @@ export const headlessConfig: THeadlessConfig = {
fallable: 'replace-falling',
resolution: 32,
contextualAveraging: 'on',
errorWeight: 0.0,
},
export: {
filepath: '/Users/lucasdower/Documents/out.obj', // Must be an absolute path to the file (can be anywhere)

View File

@ -54,6 +54,24 @@ export function getAverageColour(image: PNG): RGBA {
};
}
export function getStandardDeviation(image: PNG, average: RGBA): number {
let squaredDist = 0.0;
let weight = 0.0;
for (let x = 0; x < image.width; ++x) {
for (let y = 0; y < image.height; ++y) {
const index = 4 * (image.width * y + x);
const rgba = image.data.slice(index, index + 4);
const alpha = rgba[3] / 255;
weight += alpha;
const r = (rgba[0] / 255) * alpha;
const g = (rgba[1] / 255) * alpha;
const b = (rgba[2] / 255) * alpha;
squaredDist += Math.pow(r - average.r, 2) + Math.pow(g - average.g, 2) + Math.pow(b - average.b, 2);
}
}
return Math.sqrt(squaredDist / weight);
}
export async function getPermission() {
const directory = getMinecraftDir();
log(LogStyle.Info, `This script requires files inside of ${directory}`);