mirror of
https://github.com/LucasDower/ObjToSchematic.git
synced 2025-02-17 13:39:28 +08:00
Merge pull request #83 from LucasDower/0.7-smoothness
Added smoothness option, i.e. error weighting
This commit is contained in:
commit
437eb292e0
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 |
@ -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
|
||||
}
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
10
src/atlas.ts
10
src/atlas.ts
@ -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`);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 = [];
|
||||
|
@ -24,7 +24,7 @@ export abstract class BaseUIElement<Type> {
|
||||
}
|
||||
|
||||
protected getValue(): Type {
|
||||
ASSERT(this._value);
|
||||
ASSERT(this._value !== undefined);
|
||||
return this._value;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}),
|
||||
|
@ -98,6 +98,7 @@ export namespace AssignParams {
|
||||
fallable: FallableBehaviour,
|
||||
resolution: RGBAUtil.TColourAccuracy,
|
||||
contextualAveraging: TToggle,
|
||||
errorWeight: number,
|
||||
}
|
||||
|
||||
export type Output = {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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}`);
|
||||
|
Loading…
Reference in New Issue
Block a user