Merge branch '0.7' into 0.7-constraint

This commit is contained in:
Lucas Dower 2022-11-17 21:24:39 +00:00
parent 610ef893db
commit 0f3e7d5a77
No known key found for this signature in database
GPG Key ID: B3EE6B8499593605
65 changed files with 9652 additions and 14723 deletions

2
package-lock.json generated
View File

@ -13978,4 +13978,4 @@
"dev": true
}
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 494 KiB

View File

@ -16,5 +16,8 @@
"CAMERA_SENSITIVITY_ZOOM": 0.005,
"CONSTRAINT_MAXIMUM_WIDTH": 1024,
"CONSTRAINT_MAXIMUM_HEIGHT": 380,
"CONSTRAINT_MAXIMUM_DEPTH": 1024
"CONSTRAINT_MAXIMUM_DEPTH": 1024,
"DITHER_MAGNITUDE": 32,
"SMOOTHNESS_MAX": 3.0,
"CAMERA_SMOOTHING": 0.1
}

15
res/emissive_blocks.json Normal file
View File

@ -0,0 +1,15 @@
{
"emissive_blocks": [
"minecraft:respawn_anchor",
"minecraft:magma_block",
"minecraft:sculk_catalyst",
"minecraft:crying_obsidian",
"minecraft:shroomlight",
"minecraft:sea_lantern",
"minecraft:jack_o_lantern",
"minecraft:glowstone",
"minecraft:pearlescent_froglight",
"minecraft:verdant_froglight",
"minecraft:ochre_froglight"
]
}

View File

@ -1,85 +1,86 @@
{
"version": 1,
"blocks": [
"minecraft:black_concrete",
"minecraft:black_concrete_powder",
"minecraft:black_glazed_terracotta",
"minecraft:black_terracotta",
"minecraft:black_wool",
"minecraft:blue_concrete",
"minecraft:blue_concrete_powder",
"minecraft:blue_glazed_terracotta",
"minecraft:blue_terracotta",
"minecraft:blue_wool",
"minecraft:brown_concrete",
"minecraft:brown_concrete_powder",
"minecraft:brown_glazed_terracotta",
"minecraft:brown_terracotta",
"minecraft:brown_wool",
"minecraft:cyan_concrete",
"minecraft:cyan_concrete_powder",
"minecraft:cyan_glazed_terracotta",
"minecraft:cyan_terracotta",
"minecraft:cyan_wool",
"minecraft:gray_concrete",
"minecraft:gray_concrete_powder",
"minecraft:gray_glazed_terracotta",
"minecraft:gray_terracotta",
"minecraft:gray_wool",
"minecraft:green_concrete",
"minecraft:green_concrete_powder",
"minecraft:green_glazed_terracotta",
"minecraft:green_terracotta",
"minecraft:green_wool",
"minecraft:light_blue_concrete",
"minecraft:light_blue_concrete_powder",
"minecraft:light_blue_glazed_terracotta",
"minecraft:light_blue_terracotta",
"minecraft:light_blue_wool",
"minecraft:light_gray_concrete",
"minecraft:light_gray_concrete_powder",
"minecraft:light_gray_glazed_terracotta",
"minecraft:light_gray_terracotta",
"minecraft:light_gray_wool",
"minecraft:lime_concrete",
"minecraft:lime_concrete_powder",
"minecraft:lime_glazed_terracotta",
"minecraft:lime_terracotta",
"minecraft:lime_wool",
"minecraft:magenta_concrete",
"minecraft:magenta_concrete_powder",
"minecraft:magenta_glazed_terracotta",
"minecraft:magenta_terracotta",
"minecraft:magenta_wool",
"minecraft:orange_concrete",
"minecraft:orange_concrete_powder",
"minecraft:orange_glazed_terracotta",
"minecraft:orange_terracotta",
"minecraft:orange_wool",
"minecraft:pink_concrete",
"minecraft:pink_concrete_powder",
"minecraft:pink_glazed_terracotta",
"minecraft:pink_terracotta",
"minecraft:pink_wool",
"minecraft:purple_concrete",
"minecraft:purple_concrete_powder",
"minecraft:purple_glazed_terracotta",
"minecraft:purple_terracotta",
"minecraft:purple_wool",
"minecraft:red_concrete",
"minecraft:red_concrete_powder",
"minecraft:red_glazed_terracotta",
"minecraft:red_terracotta",
"minecraft:red_wool",
"minecraft:white_concrete",
"minecraft:white_concrete_powder",
"minecraft:white_glazed_terracotta",
"minecraft:white_terracotta",
"minecraft:white_wool",
"minecraft:yellow_concrete",
"minecraft:yellow_concrete_powder",
"minecraft:yellow_glazed_terracotta",
"minecraft:yellow_terracotta",
"minecraft:yellow_wool"
"black_concrete",
"black_concrete_powder",
"black_glazed_terracotta",
"black_terracotta",
"black_wool",
"blue_concrete",
"blue_concrete_powder",
"blue_glazed_terracotta",
"blue_terracotta",
"blue_wool",
"brown_concrete",
"brown_concrete_powder",
"brown_glazed_terracotta",
"brown_terracotta",
"brown_wool",
"cyan_concrete",
"cyan_concrete_powder",
"cyan_glazed_terracotta",
"cyan_terracotta",
"cyan_wool",
"gray_concrete",
"gray_concrete_powder",
"gray_glazed_terracotta",
"gray_terracotta",
"gray_wool",
"green_concrete",
"green_concrete_powder",
"green_glazed_terracotta",
"green_terracotta",
"green_wool",
"light_blue_concrete",
"light_blue_concrete_powder",
"light_blue_glazed_terracotta",
"light_blue_terracotta",
"light_blue_wool",
"light_gray_concrete",
"light_gray_concrete_powder",
"light_gray_glazed_terracotta",
"light_gray_terracotta",
"light_gray_wool",
"lime_concrete",
"lime_concrete_powder",
"lime_glazed_terracotta",
"lime_terracotta",
"lime_wool",
"magenta_concrete",
"magenta_concrete_powder",
"magenta_glazed_terracotta",
"magenta_terracotta",
"magenta_wool",
"orange_concrete",
"orange_concrete_powder",
"orange_glazed_terracotta",
"orange_terracotta",
"orange_wool",
"pink_concrete",
"pink_concrete_powder",
"pink_glazed_terracotta",
"pink_terracotta",
"pink_wool",
"purple_concrete",
"purple_concrete_powder",
"purple_glazed_terracotta",
"purple_terracotta",
"purple_wool",
"red_concrete",
"red_concrete_powder",
"red_glazed_terracotta",
"red_terracotta",
"red_wool",
"white_concrete",
"white_concrete_powder",
"white_glazed_terracotta",
"white_terracotta",
"white_wool",
"yellow_concrete",
"yellow_concrete_powder",
"yellow_glazed_terracotta",
"yellow_terracotta",
"yellow_wool",
"glowstone",
"sea_lantern"
]
}

View File

@ -2,11 +2,13 @@ precision mediump float;
uniform sampler2D u_texture;
uniform float u_atlasSize;
uniform bool u_nightVision;
varying float v_lighting;
varying vec4 v_occlusion;
varying vec2 v_texcoord;
varying vec2 v_blockTexcoord;
varying float v_blockLighting;
float dither8x8(vec2 position, float alpha) {
int x = int(mod(position.x, 8.0));
@ -103,5 +105,5 @@ void main() {
discard;
}
gl_FragColor = vec4(diffuse.rgb * v_lighting * g, 1.0);
gl_FragColor = vec4(diffuse.rgb * v_lighting * g * (u_nightVision ? 1.0 : v_blockLighting), 1.0);
}

View File

@ -4,17 +4,20 @@ uniform mat4 u_worldViewProjection;
uniform sampler2D u_texture;
uniform float u_voxelSize;
uniform vec3 u_gridOffset;
uniform bool u_nightVision;
attribute vec3 position;
attribute vec3 normal;
attribute vec4 occlusion;
attribute vec2 texcoord;
attribute vec2 blockTexcoord;
attribute float lighting;
varying float v_lighting;
varying vec4 v_occlusion;
varying vec2 v_texcoord;
varying vec2 v_blockTexcoord;
varying float v_blockLighting;
vec3 light = vec3(0.78, 0.98, 0.59);
@ -23,6 +26,7 @@ void main() {
v_occlusion = occlusion;
v_blockTexcoord = blockTexcoord;
v_lighting = dot(light, abs(normal));
v_blockLighting = lighting;
gl_Position = u_worldViewProjection * vec4((position.xyz + u_gridOffset) * u_voxelSize, 1.0);
}

View File

@ -4,6 +4,88 @@ uniform vec4 u_fillColour;
varying float v_lighting;
float dither8x8(vec2 position, float alpha) {
int x = int(mod(position.x, 8.0));
int y = int(mod(position.y, 8.0));
int index = x + y * 8;
float limit = 0.0;
if (x < 8) {
if (index == 0) limit = 0.015625;
if (index == 1) limit = 0.515625;
if (index == 2) limit = 0.140625;
if (index == 3) limit = 0.640625;
if (index == 4) limit = 0.046875;
if (index == 5) limit = 0.546875;
if (index == 6) limit = 0.171875;
if (index == 7) limit = 0.671875;
if (index == 8) limit = 0.765625;
if (index == 9) limit = 0.265625;
if (index == 10) limit = 0.890625;
if (index == 11) limit = 0.390625;
if (index == 12) limit = 0.796875;
if (index == 13) limit = 0.296875;
if (index == 14) limit = 0.921875;
if (index == 15) limit = 0.421875;
if (index == 16) limit = 0.203125;
if (index == 17) limit = 0.703125;
if (index == 18) limit = 0.078125;
if (index == 19) limit = 0.578125;
if (index == 20) limit = 0.234375;
if (index == 21) limit = 0.734375;
if (index == 22) limit = 0.109375;
if (index == 23) limit = 0.609375;
if (index == 24) limit = 0.953125;
if (index == 25) limit = 0.453125;
if (index == 26) limit = 0.828125;
if (index == 27) limit = 0.328125;
if (index == 28) limit = 0.984375;
if (index == 29) limit = 0.484375;
if (index == 30) limit = 0.859375;
if (index == 31) limit = 0.359375;
if (index == 32) limit = 0.0625;
if (index == 33) limit = 0.5625;
if (index == 34) limit = 0.1875;
if (index == 35) limit = 0.6875;
if (index == 36) limit = 0.03125;
if (index == 37) limit = 0.53125;
if (index == 38) limit = 0.15625;
if (index == 39) limit = 0.65625;
if (index == 40) limit = 0.8125;
if (index == 41) limit = 0.3125;
if (index == 42) limit = 0.9375;
if (index == 43) limit = 0.4375;
if (index == 44) limit = 0.78125;
if (index == 45) limit = 0.28125;
if (index == 46) limit = 0.90625;
if (index == 47) limit = 0.40625;
if (index == 48) limit = 0.25;
if (index == 49) limit = 0.75;
if (index == 50) limit = 0.125;
if (index == 51) limit = 0.625;
if (index == 52) limit = 0.21875;
if (index == 53) limit = 0.71875;
if (index == 54) limit = 0.09375;
if (index == 55) limit = 0.59375;
if (index == 56) limit = 1.0;
if (index == 57) limit = 0.5;
if (index == 58) limit = 0.875;
if (index == 59) limit = 0.375;
if (index == 60) limit = 0.96875;
if (index == 61) limit = 0.46875;
if (index == 62) limit = 0.84375;
if (index == 63) limit = 0.34375;
}
return alpha < limit ? 0.0 : 1.0;
}
void main() {
float alpha = dither8x8(gl_FragCoord.xy, u_fillColour.a);
if (alpha < 0.5)
{
discard;
}
gl_FragColor = vec4(u_fillColour.rgb * v_lighting, u_fillColour.a);
}

5
res/static/bulb.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-sun" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#00abfb" fill="none" stroke-linecap="round" stroke-linejoin="round" id="bulb-svg">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="4" />
<path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" />
</svg>

After

Width:  |  Height:  |  Size: 435 B

BIN
res/static/debug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,34 @@
{
"transparent_blocks": [
"minecraft:frosted_ice",
"minecraft:glass",
"minecraft:white_stained_glass",
"minecraft:orange_stained_glass",
"minecraft:magenta_stained_glass",
"minecraft:light_blue_stained_glass",
"minecraft:yellow_stained_glass",
"minecraft:lime_stained_glass",
"minecraft:pink_stained_glass",
"minecraft:gray_stained_glass",
"minecraft:light_gray_stained_glass",
"minecraft:cyan_stained_glass",
"minecraft:purple_stained_glass",
"minecraft:blue_stained_glass",
"minecraft:brown_stained_glass",
"minecraft:green_stained_glass",
"minecraft:red_stained_glass",
"minecraft:black_stained_glass",
"minecraft:ice",
"minecraft:oak_leaves",
"minecraft:spruce_leaves",
"minecraft:birch_leaves",
"minecraft:jungle_leaves",
"minecraft:acacia_leaves",
"minecraft:dark_oak_leaves",
"minecraft:mangrove_leaves",
"minecraft:azalea_leaves",
"minecraft:flowering_azalea_leaves",
"minecraft:slime_block",
"minecraft:honey_block"
]
}

View File

@ -3,30 +3,37 @@ import path from 'path';
import { FallableBehaviour } from './block_mesh';
import { ArcballCamera } from './camera';
import { RGBA, RGBAUtil } from './colour';
import { AppConfig } from './config';
import { EAppEvent, EventManager } from './event';
import { IExporter } from './exporters/base_exporter';
import { ExporterFactory, TExporters } from './exporters/exporters';
import { MaterialMap, MaterialType, SolidMaterial, TexturedMaterial } from './mesh';
import { Renderer } from './renderer';
import { StatusHandler, StatusMessage } from './status';
import { TextureFiltering } from './texture';
import { SolidMaterialUIElement, TextureMaterialUIElement } from './ui/elements/material';
import { OutputStyle } from './ui/elements/output';
import { UI } from './ui/layout';
import { UIMessageBuilder } from './ui/misc';
import { UIMessageBuilder, UITreeBuilder } from './ui/misc';
import { ColourSpace, EAction } from './util';
import { ASSERT } from './util/error_util';
import { LOG_ERROR, Logger } from './util/log_util';
import { AppPaths, PathUtil } from './util/path_util';
import { Vector3 } from './vector';
import { TWorkerJob, WorkerController } from './worker_controller';
import { TFromWorkerMessage, TToWorkerMessage } from './worker_types';
import { SetMaterialsParams, TFromWorkerMessage, TToWorkerMessage } from './worker_types';
export class AppContext {
private _ui: UI;
private _workerController: WorkerController;
private _lastAction?: EAction;
public maxConstraint?: Vector3;
private _materialMap: MaterialMap;
public constructor() {
this._materialMap = {};
Logger.Get.enableLogToFile();
Logger.Get.initLogFile('client');
Logger.Get.enableLOG();
@ -146,7 +153,7 @@ export class AppContext {
const builder = new UIMessageBuilder();
builder.addBold('action', [StatusHandler.Get.getDefaultSuccessMessage(action) + (hasInfos ? ':' : '')], 'success');
builder.addItem('action', infoStatuses, 'success');
builder.addItem('action', infoStatuses, 'none');
builder.addItem('action', warningStatuses, 'warning');
return { builder: builder, style: hasWarnings ? 'warning' : 'success' };
@ -185,9 +192,15 @@ export class AppContext {
ASSERT(payload.action === 'Import');
const outputElement = this._ui.getActionOutput(EAction.Import);
const dimensions = payload.result.dimensions;
const dimensions = new Vector3(
payload.result.dimensions.x,
payload.result.dimensions.y,
payload.result.dimensions.z,
);
dimensions.mulScalar(380 / 8.0).floor();
this.maxConstraint = dimensions;
this._materialMap = payload.result.materials;
this._onMaterialMapChanged();
if (payload.result.triangleCount < AppConfig.Get.RENDER_TRIANGLE_THRESHOLD) {
outputElement.setTaskInProgress('render', '[Renderer]: Processing...');
@ -201,6 +214,125 @@ export class AppContext {
return { id: 'Import', payload: payload, callback: callback };
}
private _sendMaterialsToWorker(callback: (result: SetMaterialsParams.Output) => void) {
const payload: TToWorkerMessage = {
action: 'SetMaterials',
params: {
materials: this._materialMap,
},
};
const job: TWorkerJob = {
id: 'SetMaterial',
payload: payload,
callback: (result: TFromWorkerMessage) => {
ASSERT(result.action === 'SetMaterials');
// TODO: Check the action didn't fail
this._materialMap = result.result.materials;
this._onMaterialMapChanged();
this._ui.enableTo(EAction.Voxelise);
callback(result.result);
},
};
this._workerController.addJob(job);
this._ui.disableAll();
}
public onMaterialTypeSwitched(materialName: string) {
const oldMaterial = this._materialMap[materialName];
if (oldMaterial.type == MaterialType.textured) {
this._materialMap[materialName] = {
type: MaterialType.solid,
colour: RGBAUtil.random(),
edited: true,
canBeTextured: oldMaterial.canBeTextured,
set: false,
};
} else {
this._materialMap[materialName] = {
type: MaterialType.textured,
alphaFactor: 1.0,
path: PathUtil.join(AppPaths.Get.static, 'debug.png'),
edited: true,
canBeTextured: oldMaterial.canBeTextured,
};
}
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
// TODO: Check the action didn't fail
Renderer.Get.recreateMaterialBuffer(materialName, result.materials[materialName]);
});
}
public onMaterialTextureReplace(materialName: string, newTexturePath: string) {
const oldMaterial = this._materialMap[materialName];
ASSERT(oldMaterial.type === MaterialType.textured);
this._materialMap[materialName] = {
type: MaterialType.textured,
alphaFactor: oldMaterial.alphaFactor,
alphaPath: oldMaterial.alphaPath,
path: newTexturePath,
edited: true,
canBeTextured: oldMaterial.canBeTextured,
};
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
Renderer.Get.updateMeshMaterialTexture(materialName, result.materials[materialName] as TexturedMaterial);
});
}
public onMaterialColourChanged(materialName: string, newColour: RGBA) {
ASSERT(this._materialMap[materialName].type === MaterialType.solid);
const oldMaterial = this._materialMap[materialName] as TexturedMaterial;
this._materialMap[materialName] = {
type: MaterialType.solid,
colour: newColour,
edited: true,
canBeTextured: oldMaterial.canBeTextured,
set: true,
};
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
Renderer.Get.recreateMaterialBuffer(materialName, result.materials[materialName] as SolidMaterial);
});
}
private _onMaterialMapChanged() {
// Add material information to the output log
const outputElement = this._ui.getActionOutput(EAction.Import);
const messageBuilder = outputElement.getMessage();
const tree = UITreeBuilder.create('Materials');
for (const [materialName, material] of Object.entries(this._materialMap)) {
if (materialName === 'DEFAULT_UNASSIGNED') {
continue;
}
const subTree = UITreeBuilder.create(material.edited ? `<i>'${materialName}'*</i>` : `'${materialName}'`);
if (material.type === MaterialType.solid) {
const uiElement = new SolidMaterialUIElement(materialName, this, material);
subTree.addChild({ html: uiElement.buildHTML(), warning: uiElement.hasWarning()}, () => {
uiElement.registerEvents();
});
} else {
const uiElement = new TextureMaterialUIElement(materialName, this, material);
subTree.addChild({ html: uiElement.buildHTML(), warning: uiElement.hasWarning()}, () => {
uiElement.registerEvents();
});
}
tree.addChild(subTree);
}
messageBuilder.setTree('materials', tree);
outputElement.updateMessage();
}
private _renderMesh(): TWorkerJob {
const payload: TToWorkerMessage = {
action: 'RenderMesh',
@ -253,9 +385,9 @@ export class AppContext {
constraintAxis: uiElements.constraintAxis.getCachedValue(),
voxeliser: uiElements.voxeliser.getCachedValue(),
size: uiElements.size.getCachedValue(),
useMultisampleColouring: uiElements.multisampleColouring.getCachedValue() === 'on',
useMultisampleColouring: uiElements.multisampleColouring.getCachedValue(),
textureFiltering: uiElements.textureFiltering.getCachedValue() === 'linear' ? TextureFiltering.Linear : TextureFiltering.Nearest,
enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue() === 'on',
enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue(),
voxelOverlapRule: uiElements.voxelOverlapRule.getCachedValue(),
},
};
@ -279,8 +411,8 @@ export class AppContext {
const payload: TToWorkerMessage = {
action: 'RenderNextVoxelMeshChunk',
params: {
enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue() === 'on',
desiredHeight: uiElements.size.getCachedValue(),
enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue(),
},
};
@ -330,15 +462,21 @@ export class AppContext {
this._ui.getActionOutput(EAction.Assign)
.setTaskInProgress('action', '[Block Mesh]: Loading...');
Renderer.Get.setLightingAvailable(uiElements.calculateLighting.getCachedValue());
const payload: TToWorkerMessage = {
action: 'Assign',
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()),
calculateLighting: uiElements.calculateLighting.getCachedValue(),
lightThreshold: uiElements.lightThreshold.getCachedValue(),
contextualAveraging: uiElements.contextualAveraging.getCachedValue(),
errorWeight: uiElements.errorWeight.getCachedValue() / 10,
},
};

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,15 +1,18 @@
import fs from 'fs';
import path from 'path';
import { TAtlasVersion } from '../tools/build-atlas';
import { RGBA } from './colour';
import { AppTypes, AppUtil, TOptional, UV } from './util';
import { ASSERT } from './util/error_util';
import { AppError, ASSERT } from './util/error_util';
import { LOG } from './util/log_util';
import { AppPaths } from './util/path_util';
export type TAtlasBlockFace = {
name: string;
texcoord: UV;
name: string,
texcoord: UV,
colour: RGBA,
std: number,
}
export type TAtlasBlock = {
@ -32,7 +35,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;
@ -63,20 +65,69 @@ export class Atlas {
const atlasFile = fs.readFileSync(atlasPath, 'utf8');
const atlasJSON = JSON.parse(atlasFile);
const atlasVersion = atlasJSON.version;
if (atlasVersion === undefined || atlasVersion === 1) {
const atlasSize = atlasJSON.atlasSize as number;
atlas._atlasSize = atlasSize;
if (atlasJSON.formatVersion !== 3) {
throw new AppError(`The '${atlasName}' texture atlas uses an outdated format and needs to be recreated`);
}
const blocks = atlasJSON.blocks;
for (const block of blocks) {
const atlasBlock = block as TAtlasBlock;
atlasBlock.name = AppUtil.Text.namespaceBlock(atlasBlock.name);
atlas._blocks.set(atlasBlock.name, atlasBlock);
}
} else {
ASSERT(false, `Unrecognised .atlas file version: ${atlasVersion}`);
const atlasData = atlasJSON as TAtlasVersion;
atlas._atlasSize = atlasData.atlasSize;
const getTextureUV = (name: string) => {
const tex = atlasData.textures[name];
return new UV(
(3 * tex.atlasColumn + 1) / (atlas._atlasSize * 3),
(3 * tex.atlasRow + 1) / (atlas._atlasSize * 3),
);
};
for (const block of atlasData.blocks) {
ASSERT(AppUtil.Text.isNamespacedBlock(block.name), 'Atlas block not namespaced');
const atlasBlock: TAtlasBlock = {
name: block.name,
colour: block.colour,
faces: {
up: {
name: block.faces.up,
texcoord: getTextureUV(block.faces.up),
std: atlasData.textures[block.faces.up].std,
colour: atlasData.textures[block.faces.up].colour,
},
down: {
name: block.faces.down,
texcoord: getTextureUV(block.faces.down),
std: atlasData.textures[block.faces.down].std,
colour: atlasData.textures[block.faces.down].colour,
},
north: {
name: block.faces.north,
texcoord: getTextureUV(block.faces.north),
std: atlasData.textures[block.faces.north].std,
colour: atlasData.textures[block.faces.north].colour,
},
east: {
name: block.faces.east,
texcoord: getTextureUV(block.faces.east),
std: atlasData.textures[block.faces.east].std,
colour: atlasData.textures[block.faces.east].colour,
},
south: {
name: block.faces.south,
texcoord: getTextureUV(block.faces.south),
std: atlasData.textures[block.faces.south].std,
colour: atlasData.textures[block.faces.south].colour,
},
west: {
name: block.faces.west,
texcoord: getTextureUV(block.faces.west),
std: atlasData.textures[block.faces.west].std,
colour: atlasData.textures[block.faces.west].colour,
},
},
};
atlas._blocks.set(block.name, atlasBlock);
}
return atlas;

View File

@ -1,16 +1,28 @@
import { Atlas, TAtlasBlock } from './atlas';
import { RGBA, RGBAUtil } from './colour';
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';
export type TBlockCollection = {
blocks: Map<AppTypes.TNamespacedBlockName, TAtlasBlock>,
cache: Map<number, TAtlasBlock>,
cache: Map<BigInt, TAtlasBlock>,
}
/* eslint-disable */
export enum EFaceVisibility {
Up = 1 << 0,
Down = 1 << 1,
North = 1 << 2,
East = 1 << 3,
South = 1 << 4,
West = 1 << 5,
}
/* eslint-enable */
/**
* A new instance of AtlasPalette is created each time
* A new instance of AtlasPalette is created each time
* a new voxel mesh is voxelised.
*/
export class AtlasPalette {
@ -60,35 +72,86 @@ export class AtlasPalette {
* @param colour The colour that the returned block should match with.
* @param resolution The colour accuracy, a uint8 from 1 to 255, inclusive.
* @param blockToExclude A list of blocks that should not be used, this should be a subset of the palette blocks.
* @returns
* @returns
*/
public getBlock(colour: RGBA, blockCollection: TBlockCollection, resolution: RGBAUtil.TColourAccuracy) {
const { colourHash, binnedColour } = RGBAUtil.bin(colour, resolution);
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);
// If we've already calculated the block associated with this colour, return it.
const cachedBlock = blockCollection.cache.get(colourHash);
const cachedBlock = blockCollection.cache.get(contextHash);
if (cachedBlock !== undefined) {
return cachedBlock;
}
// Find closest block in colour
let minDistance = Infinity;
let minError = Infinity;
let blockChoice: TOptional<TAtlasBlock>;
{
blockCollection.blocks.forEach((blockData) => {
const colourDistance = RGBAUtil.squaredDistance(binnedColour, blockData.colour);
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;
}
});
}
if (blockChoice !== undefined) {
blockCollection.cache.set(colourHash, blockChoice);
blockCollection.cache.set(contextHash, blockChoice);
return blockChoice;
}
ASSERT(false, 'Unreachable, always at least one possible block');
}
public static getContextualFaceAverage(block: TAtlasBlock, faceVisibility: EFaceVisibility) {
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(averageColour, block.faces.up.colour);
averageStd += block.faces.up.std;
++count;
}
if (faceVisibility & EFaceVisibility.Down) {
RGBAUtil.add(averageColour, block.faces.down.colour);
averageStd += block.faces.down.std;
++count;
}
if (faceVisibility & EFaceVisibility.North) {
RGBAUtil.add(averageColour, block.faces.north.colour);
averageStd += block.faces.north.std;
++count;
}
if (faceVisibility & EFaceVisibility.East) {
RGBAUtil.add(averageColour, block.faces.east.colour);
averageStd += block.faces.east.std;
++count;
}
if (faceVisibility & EFaceVisibility.South) {
RGBAUtil.add(averageColour, block.faces.south.colour);
averageStd += block.faces.south.std;
++count;
}
if (faceVisibility & EFaceVisibility.West) {
RGBAUtil.add(averageColour, block.faces.west.colour);
averageStd += block.faces.west.std;
++count;
}
averageColour.r /= count;
averageColour.g /= count;
averageColour.b /= count;
averageColour.a /= count;
return {
colour: averageColour,
std: averageStd / count,
};
}
}

View File

@ -1,14 +1,16 @@
import fs from 'fs';
import { BlockAssignerFactory, TBlockAssigners } from './assigners/assigners';
import { Atlas } from './atlas';
import { Atlas, TAtlasBlock } 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 { BlockMeshLighting } from './lighting';
import { Palette } from './palette';
import { ProgressManager } from './progress';
import { StatusHandler } from './status';
import { ColourSpace } from './util';
import { ColourSpace, TOptional } from './util';
import { AppError, ASSERT } from './util/error_util';
import { LOGF } from './util/log_util';
import { AppPaths, PathUtil } from './util/path_util';
@ -26,33 +28,80 @@ 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[];
private _transparentBlocks: string[];
private _emissiveBlocks: string[];
private _atlas: Atlas;
private _lighting: BlockMeshLighting;
public static createFromVoxelMesh(voxelMesh: VoxelMesh, blockMeshParams: AssignParams.Input) {
const blockMesh = new BlockMesh(voxelMesh);
blockMesh._assignBlocks(blockMeshParams);
//blockMesh._calculateLighting(blockMeshParams.lightThreshold);
if (blockMeshParams.calculateLighting) {
blockMesh._lighting.init();
blockMesh._lighting.addSunLightValues();
blockMesh._lighting.addEmissiveBlocks();
blockMesh._lighting.addLightToDarkness(blockMeshParams.lightThreshold);
blockMesh._lighting.dumpInfo();
}
return blockMesh;
}
private constructor(voxelMesh: VoxelMesh) {
this._blocksUsed = [];
this._blocksUsed = new Set();
this._blocks = [];
this._voxelMesh = voxelMesh;
this._atlas = Atlas.getVanillaAtlas()!;
this._lighting = new BlockMeshLighting(this);
//this._lighting = new Map<string, number>();
//this._recreateBuffer = true;
const fallableBlocksString = fs.readFileSync(PathUtil.join(AppPaths.Get.resources, 'fallable_blocks.json'), 'utf-8');
this._fallableBlocks = JSON.parse(fallableBlocksString).fallable_blocks;
const transparentlocksString = fs.readFileSync(PathUtil.join(AppPaths.Get.resources, 'transparent_blocks.json'), 'utf-8');
this._transparentBlocks = JSON.parse(transparentlocksString).transparent_blocks;
const emissivelocksString = fs.readFileSync(PathUtil.join(AppPaths.Get.resources, 'emissive_blocks.json'), 'utf-8');
this._emissiveBlocks = JSON.parse(emissivelocksString).emissive_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) {
@ -67,54 +116,42 @@ 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);
const faceVisibility = blockMeshParams.contextualAveraging ?
this._voxelMesh.getFaceVisibility(voxel.position) :
VoxelMesh.getFullFaceVisibility();
let block = atlasPalette.getBlock(voxelColour, allBlockCollection, faceVisibility, blockMeshParams.errorWeight);
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, faceVisibility, blockMeshParams.errorWeight);
}
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);
@ -123,12 +160,58 @@ export class BlockMesh {
}
}
// Face order: ['north', 'south', 'up', 'down', 'east', 'west']
public getBlockLighting(position: Vector3) {
// TODO: Shouldn't only use sunlight value, take max of either
return [
this._lighting.getMaxLightLevel(new Vector3(1, 0, 0).add(position)),
this._lighting.getMaxLightLevel(new Vector3(-1, 0, 0).add(position)),
this._lighting.getMaxLightLevel(new Vector3(0, 1, 0).add(position)),
this._lighting.getMaxLightLevel(new Vector3(0, -1, 0).add(position)),
this._lighting.getMaxLightLevel(new Vector3(0, 0, 1).add(position)),
this._lighting.getMaxLightLevel(new Vector3(0, 0, -1).add(position)),
];
}
public setEmissiveBlock(pos: Vector3): boolean {
const voxel = this._voxelMesh.getVoxelAt(pos);
ASSERT(voxel !== undefined, 'Missing voxel');
const minError = Infinity;
let bestBlock: TAtlasBlock | undefined;
this._emissiveBlocks.forEach((emissiveBlockName) => {
const emissiveBlockData = this._atlas.getBlocks().get(emissiveBlockName);
if (emissiveBlockData) {
const error = RGBAUtil.squaredDistance(emissiveBlockData.colour, voxel.colour);
if (error < minError) {
bestBlock = emissiveBlockData;
}
}
});
if (bestBlock !== undefined) {
const blockIndex = this._voxelMesh.getVoxelIndex(pos);
ASSERT(blockIndex !== undefined, 'Setting emissive block of block that doesn\'t exist');
this._blocks[blockIndex].blockInfo = bestBlock;
return true;
}
throw new AppError('Block palette contains no light blocks to place');
}
public getBlockAt(pos: Vector3): TOptional<Block> {
const index = this._voxelMesh.getVoxelIndex(pos);
if (index !== undefined) {
return this._blocks[index];
}
}
public getBlocks(): Block[] {
return this._blocks;
}
public getBlockPalette() {
return this._blocksUsed;
return Array.from(this._blocksUsed);
}
public getVoxelMesh() {
@ -142,6 +225,14 @@ export class BlockMesh {
return this._atlas;
}
public isEmissiveBlock(block: Block) {
return this._emissiveBlocks.includes(block.blockInfo.name);
}
public isTransparentBlock(block: Block) {
return this._transparentBlocks.includes(block.blockInfo.name);
}
/*
private _buffer?: TBlockMeshBufferDescription;
public getBuffer(): TBlockMeshBufferDescription {

View File

@ -22,6 +22,7 @@ export type TMeshBufferDescription = {
material: SolidMaterial | (TexturedMaterial)
buffer: TMeshBuffer,
numElements: number,
materialName: string,
};
export type TVoxelMeshBuffer = {
@ -45,6 +46,7 @@ export type TBlockMeshBuffer = {
texcoord: { numComponents: 2, data: Float32Array },
normal: { numComponents: 3, data: Float32Array },
blockTexcoord: { numComponents: 2, data: Float32Array },
lighting: { numComponents: 1, data: Float32Array },
indices: { numComponents: 3, data: Uint32Array },
};
@ -70,7 +72,7 @@ export class ChunkedBufferGenerator {
for (let i = 0; i < numBufferVoxels; ++i) {
const voxelIndex = i + voxelsStartIndex;
const voxel = voxels[voxelIndex];
const voxelColourArray = [voxel.colour.r, voxel.colour.g, voxel.colour.b, voxel.colour.a];
const voxelPositionArray = voxel.position.toArray();
@ -114,6 +116,24 @@ export class ChunkedBufferGenerator {
public static fromBlockMesh(blockMesh: BlockMesh, chunkIndex: number): TBlockMeshBufferDescription & { moreBlocksToBuffer: boolean, progress: number } {
const blocks = blockMesh.getBlocks();
const lightingRamp = new Map<number, number>();
lightingRamp.set(15, 40 / 40);
lightingRamp.set(14, 40 / 40);
lightingRamp.set(13, 39 / 40);
lightingRamp.set(12, 37 / 40);
lightingRamp.set(11, 35 / 40);
lightingRamp.set(10, 32 / 40);
lightingRamp.set(9, 29 / 40);
lightingRamp.set(8, 26 / 40);
lightingRamp.set(7, 23 / 40);
lightingRamp.set(6, 20 / 40);
lightingRamp.set(5, 17 / 40);
lightingRamp.set(4, 14 / 40);
lightingRamp.set(3, 12 / 40);
lightingRamp.set(2, 9 / 40);
lightingRamp.set(1, 7 / 40);
lightingRamp.set(0, 5 / 40);
const numTotalBlocks = blocks.length;
const blocksStartIndex = chunkIndex * AppConfig.Get.VOXEL_BUFFER_CHUNK_SIZE;
const blocksEndIndex = Math.min((chunkIndex + 1) * AppConfig.Get.VOXEL_BUFFER_CHUNK_SIZE, numTotalBlocks);
@ -126,16 +146,22 @@ export class ChunkedBufferGenerator {
const faceOrder = ['north', 'south', 'up', 'down', 'east', 'west'];
let insertIndex = 0;
let lightingInsertIndex = 0;
for (let i = 0; i < numBufferBlocks; ++i) {
const blockIndex = i + blocksStartIndex;
const blockLighting = blockMesh.getBlockLighting(blocks[blockIndex].voxel.position);
for (let f = 0; f < AppConstants.FACES_PER_VOXEL; ++f) {
const faceName = faceOrder[f];
const faceLighting = lightingRamp.get(blockLighting[f] ?? 15) ?? 1.0;
const texcoord = blocks[blockIndex].blockInfo.faces[faceName].texcoord;
for (let v = 0; v < AppConstants.VERTICES_PER_FACE; ++v) {
newBuffer.blockTexcoord.data[insertIndex++] = texcoord.u;
newBuffer.blockTexcoord.data[insertIndex++] = texcoord.v;
newBuffer.lighting.data[lightingInsertIndex++] = faceLighting;
}
}
}
@ -222,6 +248,7 @@ export class BufferGenerator {
buffer: materialBuffer,
material: material,
numElements: materialBuffer.indices.data.length,
materialName: materialName,
});
});
@ -394,6 +421,10 @@ export class BufferGenerator {
numComponents: AppConstants.ComponentSize.TEXCOORD,
data: new Float32Array(numBlocks * AppConstants.VoxelMeshBufferComponentOffsets.TEXCOORD),
},
lighting: {
numComponents: AppConstants.ComponentSize.LIGHTING,
data: new Float32Array(numBlocks * AppConstants.VoxelMeshBufferComponentOffsets.LIGHTING),
},
};
}
}

View File

@ -43,10 +43,10 @@ export class ArcballCamera {
this._zNear = 0.5;
this._zFar = 100.0;
this._aspect = this._gl.canvas.width / this._gl.canvas.height;
this._distance = new SmoothVariable(AppConfig.Get.CAMERA_DEFAULT_DISTANCE_UNITS, 0.025);
this._azimuth = new SmoothVariable(AppConfig.Get.CAMERA_DEFAULT_AZIMUTH_RADIANS, 0.025);
this._elevation = new SmoothVariable(AppConfig.Get.CAMERA_DEFAULT_ELEVATION_RADIANS, 0.025);
this._target = new SmoothVectorVariable(new Vector3(0, 0, 0), 0.025);
this._distance = new SmoothVariable(AppConfig.Get.CAMERA_DEFAULT_DISTANCE_UNITS, AppConfig.Get.CAMERA_SMOOTHING);
this._azimuth = new SmoothVariable(AppConfig.Get.CAMERA_DEFAULT_AZIMUTH_RADIANS, AppConfig.Get.CAMERA_SMOOTHING);
this._elevation = new SmoothVariable(AppConfig.Get.CAMERA_DEFAULT_ELEVATION_RADIANS, AppConfig.Get.CAMERA_SMOOTHING);
this._target = new SmoothVectorVariable(new Vector3(0, 0, 0), AppConfig.Get.CAMERA_SMOOTHING);
this._elevation.setClamp(0.001, Math.PI - 0.001);
this._distance.setClamp(1.0, 100.0);

View File

@ -1,4 +1,5 @@
import { AppConfig } from './config';
import { TBrand } from './util/type_util';
export type RGBA = {
r: number,
@ -7,7 +8,69 @@ export type RGBA = {
a: number
}
export type RGBA_255 = TBrand<RGBA, '255'>;
export namespace RGBAUtil {
export function toString(a: RGBA) {
return `(${a.r}, ${a.g}, ${a.b}, ${a.a})`;
}
export function random(): RGBA {
return {
r: Math.random(),
g: Math.random(),
b: Math.random(),
a: 1.0,
};
}
export function toHexString(a: RGBA) {
const r = Math.floor(255 * a.r).toString(16).padStart(2, '0');
const g = Math.floor(255 * a.g).toString(16).padStart(2, '0');
const b = Math.floor(255 * a.b).toString(16).padStart(2, '0');
return `#${r}${g}${b}`;
}
export function fromHexString(str: string) {
return {
r: parseInt(str.substring(1, 3), 16) / 255,
g: parseInt(str.substring(3, 5), 16) / 255,
b: parseInt(str.substring(5, 7), 16) / 255,
a: 1.0,
};
}
export function toUint8String(a: RGBA) {
return `(${Math.floor(255 * a.r)}, ${Math.floor(255 * a.g)}, ${Math.floor(255 * a.b)}, ${Math.floor(255 * a.a)})`;
}
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 add(a: RGBA, b: RGBA) {
a.r += b.r;
a.g += b.g;
a.b += b.b;
a.a += b.a;
}
export function lerp(a: RGBA, b: RGBA, alpha: number) {
return {
r: a.r * (1 - alpha) + b.r * alpha,
@ -53,32 +116,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 +159,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

@ -33,6 +33,9 @@ export class AppConfig {
public readonly CONSTRAINT_MAXIMUM_WIDTH: number;
public readonly CONSTRAINT_MAXIMUM_HEIGHT: number;
public readonly CONSTRAINT_MAXIMUM_DEPTH: number;
public readonly DITHER_MAGNITUDE: number;
public readonly SMOOTHNESS_MAX: number;
public readonly CAMERA_SMOOTHING: number;
private constructor() {
this.RELEASE_MODE = false;
@ -60,6 +63,9 @@ export class AppConfig {
this.CONSTRAINT_MAXIMUM_WIDTH = configJSON.CONSTRAINT_MAXIMUM_WIDTH;
this.CONSTRAINT_MAXIMUM_HEIGHT = configJSON.CONSTRAINT_MAXIMUM_HEIGHT;
this.CONSTRAINT_MAXIMUM_DEPTH = configJSON.CONSTRAINT_MAXIMUM_DEPTH;
this.DITHER_MAGNITUDE = configJSON.DITHER_MAGNITUDE;
this.SMOOTHNESS_MAX = configJSON.SMOOTHNESS_MAX;
this.CAMERA_SMOOTHING = configJSON.CAMERA_SMOOTHING;
}
public dumpConfig() {

View File

@ -5,6 +5,7 @@ export namespace AppConstants {
export const COMPONENT_PER_SIZE_OFFSET = FACES_PER_VOXEL * VERTICES_PER_FACE;
export namespace ComponentSize {
export const LIGHTING = 1;
export const TEXCOORD = 2;
export const POSITION = 3;
export const COLOUR = 4;
@ -12,8 +13,9 @@ export namespace AppConstants {
export const INDICES = 3;
export const OCCLUSION = 4;
}
export namespace VoxelMeshBufferComponentOffsets {
export const LIGHTING = ComponentSize.LIGHTING * COMPONENT_PER_SIZE_OFFSET;
export const TEXCOORD = ComponentSize.TEXCOORD * COMPONENT_PER_SIZE_OFFSET;
export const POSITION = ComponentSize.POSITION * COMPONENT_PER_SIZE_OFFSET;
export const COLOUR = ComponentSize.COLOUR * COMPONENT_PER_SIZE_OFFSET;

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

@ -22,7 +22,7 @@ export class ObjImporter extends IImporter {
private _tris: Tri[] = [];
private _materials: { [key: string]: (SolidMaterial | TexturedMaterial) } = {
'DEFAULT_UNASSIGNED': { type: MaterialType.solid, colour: RGBAColours.WHITE },
'DEFAULT_UNASSIGNED': { type: MaterialType.solid, colour: RGBAColours.WHITE, edited: true, canBeTextured: false, set: true },
};
private _mtlLibs: string[] = [];
private _currentMaterialName: string = 'DEFAULT_UNASSIGNED';
@ -356,7 +356,7 @@ export class ObjImporter extends IImporter {
const fileLines = fileContents.split('\n');
for (const line of fileLines) {
this._parseMTLLine(line);
this._parseMTLLine(line.trim());
}
this._addCurrentMaterial();
@ -396,12 +396,22 @@ export class ObjImporter extends IImporter {
path: this._currentTexture,
alphaPath: this._currentTransparencyTexture === '' ? undefined : this._currentTransparencyTexture,
alphaFactor: this._currentAlpha,
edited: false,
canBeTextured: true,
};
this._currentTransparencyTexture = '';
} else {
this._materials[this._currentMaterialName] = {
type: MaterialType.solid,
colour: this._currentColour,
colour: {
r: this._currentColour.r,
g: this._currentColour.g,
b: this._currentColour.b,
a: this._currentAlpha,
},
edited: false,
canBeTextured: false,
set: true,
};
}
this._currentAlpha = 1.0;

470
src/lighting.ts Normal file
View File

@ -0,0 +1,470 @@
import { BlockMesh } from './block_mesh';
import { Bounds } from './bounds';
import { ASSERT } from './util/error_util';
import { LOG } from './util/log_util';
import { Vector3Hash } from './util/type_util';
import { Vector3 } from './vector';
/* eslint-disable */
enum EFace {
Up,
Down,
North,
South,
East,
West,
None,
};
/* eslint-enable */
export type TLightLevel = { blockLightValue: number, sunLightValue: number };
export type TLightUpdate = TLightLevel & { pos: Vector3, from: EFace };
export class BlockMeshLighting {
private _owner: BlockMesh;
private _limits: Map<number, { x: number, z: number, minY: number, maxY: number }>;
private _sunLightValues: Map<Vector3Hash, number>;
private _blockLightValues: Map<Vector3Hash, number>;
private _updates: number;
private _skips: number;
private _bounds: Bounds;
public constructor(owner: BlockMesh) {
this._owner = owner;
this._sunLightValues = new Map();
this._blockLightValues = new Map();
this._limits = new Map();
this._bounds = new Bounds(new Vector3(0, 0, 0), new Vector3(0, 0, 0));
this._updates = 0;
this._skips = 0;
}
public getLightLevel(vec: Vector3): TLightLevel {
const hash = vec.hash();
return {
sunLightValue: this._sunLightValues.get(hash) ?? 0,
blockLightValue: this._blockLightValues.get(hash) ?? 0,
};
}
public getMaxLightLevel(vec: Vector3): number {
const light = this.getLightLevel(vec);
//return light.blockLightValue;
//return light.sunLightValue;
return Math.max(light.blockLightValue, light.sunLightValue);
}
public addLightToDarkness(threshold: number) {
if (threshold === 0) {
return;
}
const potentialBlocks: Vector3[] = [];
this._owner.getBlocks().forEach((block) => {
if (this.getMaxLightLevel(block.voxel.position) < threshold) {
potentialBlocks.push(block.voxel.position);
}
});
while (potentialBlocks.length > 0) {
const potentialBlockPos = potentialBlocks.pop()!;
if (this.getMaxLightLevel(potentialBlockPos) < threshold) {
const success = this._owner.setEmissiveBlock(potentialBlockPos);
if (success) {
const newBlockLight = 14; // TODO: Not necessarily 14
this._blockLightValues.set(potentialBlockPos.hash(), newBlockLight);
const attenuated: TLightLevel = {
sunLightValue: this.getLightLevel(potentialBlockPos).sunLightValue - 1,
blockLightValue: newBlockLight - 1,
};
const updates: TLightUpdate[] = [];
updates.push({ pos: new Vector3(0, 1, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.Down });
updates.push({ pos: new Vector3(0, -1, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.Up });
updates.push({ pos: new Vector3(1, 0, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.South });
updates.push({ pos: new Vector3(-1, 0, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.North });
updates.push({ pos: new Vector3(0, 0, 1).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.West });
updates.push({ pos: new Vector3(0, 0, -1).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.East });
//this._handleUpdates(updates, false, true);
this._handleBlockLightUpdates(updates);
ASSERT(updates.length === 0);
}
}
}
}
private _calculateLimits() {
this._bounds = this._owner.getVoxelMesh().getBounds();
this._limits.clear();
const updateLimit = (pos: Vector3) => {
const key = pos.copy();
key.y = 0;
const blockLimit = this._limits.get(key.hash());
if (blockLimit !== undefined) {
blockLimit.maxY = Math.max(blockLimit.maxY, pos.y);
blockLimit.minY = Math.min(blockLimit.minY, pos.y);
} else {
this._limits.set(key.hash(), {
x: pos.x,
z: pos.z,
minY: pos.y,
maxY: pos.y,
});
}
};
this._owner.getBlocks().forEach((block) => {
updateLimit(block.voxel.position);
updateLimit(new Vector3(1, 0, 0).add(block.voxel.position));
updateLimit(new Vector3(-1, 0, 0).add(block.voxel.position));
updateLimit(new Vector3(0, 0, 1).add(block.voxel.position));
updateLimit(new Vector3(0, 0, -1).add(block.voxel.position));
});
}
public init() {
this._calculateLimits();
}
public addSunLightValues() {
// Actually commit the light level changes.
const updates: TLightUpdate[] = [];
this._limits.forEach((limit, key) => {
updates.push({
pos: new Vector3(0, 1, 0).add(new Vector3(limit.x, limit.maxY, limit.z)),
sunLightValue: 15,
blockLightValue: 0,
from: EFace.None,
});
});
this._handleSunLightUpdates(updates);
ASSERT(updates.length === 0, 'Updates still remaining');
}
public addEmissiveBlocks() {
const updates: TLightUpdate[] = [];
this._owner.getBlocks().forEach((block) => {
if (this._owner.isEmissiveBlock(block)) {
updates.push({
pos: block.voxel.position,
sunLightValue: 0,
blockLightValue: 14,
from: EFace.None,
});
}
});
this._handleBlockLightUpdates(updates);
ASSERT(updates.length === 0, 'Updates still remaining');
}
/**
* Goes through each block location in `updates` and sets the light value
* to the maximum of its current value and its update value.
*
* @Note **Modifies `updates`**
*/
/*
private _handleUpdates(updates: TLightUpdate[], updateSunLight: boolean, updateBlockLight: boolean) {
while (updates.length > 0) {
this._updates += 1;
const update = updates.pop()!;
// Only update light values inside the bounds of the block mesh.
// Values outside the bounds are assumed to have sunLightValue of 15
// and blockLightValue of 0.
if (updateSunLight && !updateBlockLight && update.sunLightValue < 0) {
this._skips += 1;
ASSERT(false, 'SKIP SUNLIGHT');
continue;
}
if (updateBlockLight && !updateSunLight && update.blockLightValue < 0) {
this._skips += 1;
ASSERT(false, 'SKIP BLOCKLIGHT');
continue;
}
if (!this._isPosValid(update.pos)) {
this._skips += 1;
continue;
}
const current = this.getLightLevel(update.pos);
const toSet: TLightLevel = { sunLightValue: current.sunLightValue, blockLightValue: current.blockLightValue };
const hash = update.pos.hash();
// Update sunLight value
if (updateSunLight && current.sunLightValue < update.sunLightValue) {
toSet.sunLightValue = update.sunLightValue;
this._sunLightValues.set(hash, toSet.sunLightValue);
}
// Update blockLight values
if (updateBlockLight && current.blockLightValue < update.blockLightValue) {
toSet.blockLightValue = update.blockLightValue;
this._blockLightValues.set(hash, toSet.blockLightValue);
}
const blockHere = this._owner.getBlockAt(update.pos);
const isBlockHere = blockHere !== undefined;
const shouldPropagate = isBlockHere ?
this._owner.isTransparentBlock(blockHere) :
true;
// Actually commit the light value changes and notify neighbours to
// update their values.
if (shouldPropagate) {
const sunLightChanged = current.sunLightValue !== toSet.sunLightValue && toSet.sunLightValue > 0;
const blockLightChanged = current.blockLightValue !== toSet.blockLightValue && toSet.blockLightValue > 0;
if ((sunLightChanged || blockLightChanged)) {
const attenuated: TLightLevel = {
sunLightValue: toSet.sunLightValue - 1,
blockLightValue: toSet.blockLightValue - 1,
};
if (update.from !== EFace.Up) {
updates.push({
pos: new Vector3(0, 1, 0).add(update.pos),
sunLightValue: attenuated.sunLightValue,
blockLightValue: attenuated.blockLightValue,
from: EFace.Down,
});
}
if (update.from !== EFace.Down) {
updates.push({
pos: new Vector3(0, -1, 0).add(update.pos),
sunLightValue: toSet.sunLightValue === 15 ? 15 : toSet.sunLightValue - 1,
blockLightValue: toSet.blockLightValue - 1,
from: EFace.Up,
});
}
if (update.from !== EFace.North) {
updates.push({
pos: new Vector3(1, 0, 0).add(update.pos),
sunLightValue: attenuated.sunLightValue,
blockLightValue: attenuated.blockLightValue,
from: EFace.South,
});
}
if (update.from !== EFace.South) {
updates.push({
pos: new Vector3(-1, 0, 0).add(update.pos),
sunLightValue: attenuated.sunLightValue,
blockLightValue: attenuated.blockLightValue,
from: EFace.North,
});
}
if (update.from !== EFace.East) {
updates.push({
pos: new Vector3(0, 0, 1).add(update.pos),
sunLightValue: attenuated.sunLightValue,
blockLightValue: attenuated.blockLightValue,
from: EFace.West,
});
}
if (update.from !== EFace.West) {
updates.push({
pos: new Vector3(0, 0, -1).add(update.pos),
sunLightValue: attenuated.sunLightValue,
blockLightValue: attenuated.blockLightValue,
from: EFace.East,
});
}
}
}
}
}
*/
private _handleBlockLightUpdates(updates: TLightUpdate[]) {
while (updates.length > 0) {
this._updates += 1;
const update = updates.pop()!;
if (!this._isPosValid(update.pos)) {
this._skips += 1;
continue;
}
const current = this.getLightLevel(update.pos);
let toSet = current.blockLightValue;
const hash = update.pos.hash();
// Update blockLight values
if (current.blockLightValue < update.blockLightValue) {
toSet = update.blockLightValue;
this._blockLightValues.set(hash, toSet);
}
const blockHere = this._owner.getBlockAt(update.pos);
const isBlockHere = blockHere !== undefined;
const shouldPropagate = isBlockHere ?
this._owner.isTransparentBlock(blockHere) :
true;
// Actually commit the light value changes and notify neighbours to
// update their values.
if (shouldPropagate) {
const blockLightChanged = current.blockLightValue !== toSet && toSet > 0;
if (blockLightChanged) {
if (update.from !== EFace.Up) {
updates.push({
pos: new Vector3(0, 1, 0).add(update.pos),
sunLightValue: current.sunLightValue,
blockLightValue: toSet - 1,
from: EFace.Down,
});
}
if (update.from !== EFace.Down) {
updates.push({
pos: new Vector3(0, -1, 0).add(update.pos),
sunLightValue: current.sunLightValue,
blockLightValue: toSet - 1,
from: EFace.Up,
});
}
if (update.from !== EFace.North) {
updates.push({
pos: new Vector3(1, 0, 0).add(update.pos),
sunLightValue: current.sunLightValue,
blockLightValue: toSet - 1,
from: EFace.South,
});
}
if (update.from !== EFace.South) {
updates.push({
pos: new Vector3(-1, 0, 0).add(update.pos),
sunLightValue: current.sunLightValue,
blockLightValue: toSet - 1,
from: EFace.North,
});
}
if (update.from !== EFace.East) {
updates.push({
pos: new Vector3(0, 0, 1).add(update.pos),
sunLightValue: current.sunLightValue,
blockLightValue: toSet - 1,
from: EFace.West,
});
}
if (update.from !== EFace.West) {
updates.push({
pos: new Vector3(0, 0, -1).add(update.pos),
sunLightValue: current.sunLightValue,
blockLightValue: toSet - 1,
from: EFace.East,
});
}
}
}
}
}
private _handleSunLightUpdates(updates: TLightUpdate[]) {
while (updates.length > 0) {
this._updates += 1;
const update = updates.pop()!;
if (!this._isPosValid(update.pos)) {
this._skips += 1;
continue;
}
const current = this.getLightLevel(update.pos);
let toSet = current.sunLightValue;
const hash = update.pos.hash();
// Update sunLight value
if (current.sunLightValue < update.sunLightValue) {
toSet = update.sunLightValue;
this._sunLightValues.set(hash, toSet);
}
const blockHere = this._owner.getBlockAt(update.pos);
const isBlockHere = blockHere !== undefined;
const shouldPropagate = isBlockHere ?
this._owner.isTransparentBlock(blockHere) :
true;
// Actually commit the light value changes and notify neighbours to
// update their values.
if (shouldPropagate) {
const sunLightChanged = current.sunLightValue !== toSet && toSet > 0;
if ((sunLightChanged)) {
if (update.from !== EFace.Up) {
updates.push({
pos: new Vector3(0, 1, 0).add(update.pos),
sunLightValue: toSet - 1,
blockLightValue: current.blockLightValue,
from: EFace.Down,
});
}
if (update.from !== EFace.Down) {
updates.push({
pos: new Vector3(0, -1, 0).add(update.pos),
sunLightValue: toSet === 15 ? 15 : toSet - 1,
blockLightValue: current.blockLightValue,
from: EFace.Up,
});
}
if (update.from !== EFace.North) {
updates.push({
pos: new Vector3(1, 0, 0).add(update.pos),
sunLightValue: toSet - 1,
blockLightValue: current.blockLightValue,
from: EFace.South,
});
}
if (update.from !== EFace.South) {
updates.push({
pos: new Vector3(-1, 0, 0).add(update.pos),
sunLightValue: toSet - 1,
blockLightValue: current.blockLightValue,
from: EFace.North,
});
}
if (update.from !== EFace.East) {
updates.push({
pos: new Vector3(0, 0, 1).add(update.pos),
sunLightValue: toSet - 1,
blockLightValue: current.blockLightValue,
from: EFace.West,
});
}
if (update.from !== EFace.West) {
updates.push({
pos: new Vector3(0, 0, -1).add(update.pos),
sunLightValue: toSet - 1,
blockLightValue: current.blockLightValue,
from: EFace.East,
});
}
}
}
}
}
private _isPosValid(vec: Vector3) {
const key = vec.copy();
key.y = 0;
const limit = this._limits.get(key.hash());
if (limit !== undefined) {
return vec.y >= this._bounds.min.y && vec.y <= limit.maxY + 1;
} else {
return false;
}
}
public dumpInfo() {
LOG(`Skipped ${this._skips} out of ${this._updates} (${(100 * this._skips / this._updates).toFixed(4)}%)`);
}
}

View File

@ -66,18 +66,18 @@ function createWindow() {
.execSync('git rev-parse --abbrev-ref HEAD')
.toString()
.replace('\n', '');
const commitHash: (string | Buffer) = require('child_process')
.execSync('git rev-parse --short HEAD')
.toString()
.replace('\n', '');
mainWindow.setTitle(`${baseTitle} (git ${branchName.toString()}${commitHash.toString().trim()})`);
mainWindow.setTitle(`${baseTitle} (git ${branchName.toString()} ${commitHash.toString().trim()})`);
} catch (e: any) {
mainWindow.setTitle(`${baseTitle} (git)`);
}
}
// Open the DevTools.
// mainWindow.webContents.openDevTools();

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

@ -27,12 +27,21 @@ export interface Tri {
/* eslint-disable */
export enum MaterialType { solid, textured }
/* eslint-enable */
export interface SolidMaterial { colour: RGBA; type: MaterialType.solid }
export interface TexturedMaterial {
path: string;
type: MaterialType.textured;
alphaPath?: string;
alphaFactor: number;
type BaseMaterial = {
edited: boolean,
canBeTextured: boolean,
}
export type SolidMaterial = BaseMaterial & {
type: MaterialType.solid,
colour: RGBA,
set: boolean,
}
export type TexturedMaterial = BaseMaterial & {
type: MaterialType.textured,
path: string,
alphaPath?: string,
alphaFactor: number,
}
export type MaterialMap = { [key: string]: (SolidMaterial | TexturedMaterial) };
@ -150,31 +159,54 @@ export class Mesh {
throw new AppError('Loaded mesh has no materials');
}
// Check used materials exist
let wasRemapped = false;
let debugName = (Math.random() + 1).toString(36).substring(7);
while (debugName in this._materials) {
debugName = (Math.random() + 1).toString(36).substring(7);
}
// Check used materials exist
const usedMaterials = new Set<string>();
const missingMaterials = new Set<string>();
for (const tri of this._tris) {
if (!(tri.material in this._materials)) {
// This triangle makes use of a material we don't have info about
// Try infer details about this material and add it to our materials
if (tri.texcoordIndices === undefined) {
// No texcoords are defined, therefore make a solid material
this._materials[tri.material] = {
type: MaterialType.solid,
colour: RGBAColours.MAGENTA,
edited: true,
canBeTextured: false,
set: false,
};
} else {
// Texcoords exist
this._materials[tri.material] = {
type: MaterialType.solid,
colour: RGBAUtil.random(),
edited: true,
canBeTextured: true,
set: false,
};
}
missingMaterials.add(tri.material);
wasRemapped = true;
tri.material = debugName;
}
usedMaterials.add(tri.material);
}
const materialsToRemove = new Set<string>();
for (const materialName in this._materials) {
if (!usedMaterials.has(materialName)) {
LOG_WARN(`'${materialName}' is not used by any triangles, removing...`);
materialsToRemove.add(materialName);
}
}
if (wasRemapped) {
materialsToRemove.forEach((materialName) => {
delete this._materials[materialName];
});
if (missingMaterials.size > 0) {
LOG_WARN('Triangles use these materials but they were not found', missingMaterials);
StatusHandler.Get.add(
'warning',
'Some materials were not loaded correctly',
);
this._materials[debugName] = {
type: MaterialType.solid,
colour: RGBAColours.WHITE,
};
}
// Check texture paths are absolute and exist
@ -183,14 +215,17 @@ export class Mesh {
if (material.type === MaterialType.textured) {
ASSERT(path.isAbsolute(material.path), 'Material texture path not absolute');
if (!fs.existsSync(material.path)) {
StatusHandler.Get.add(
'warning',
`Could not find ${material.path}`,
);
//StatusHandler.Get.add(
// 'warning',
// `Could not find ${material.path}`,
//);
LOG_WARN(`Could not find ${material.path} for material ${materialName}, changing to solid-white material`);
this._materials[materialName] = {
type: MaterialType.solid,
colour: RGBAColours.WHITE,
edited: true,
canBeTextured: true,
set: false,
};
}
}
@ -304,6 +339,11 @@ export class Mesh {
return this._materials[materialName];
}
public setMaterials(materialMap: MaterialMap) {
this._materials = materialMap;
this._loadTextures();
}
public getMaterials() {
return this._materials;
}

View File

@ -4,11 +4,10 @@ import { Vector3 } from './vector';
import { VoxelMesh } from './voxel_mesh';
export class OcclusionManager {
private _occlusionNeighboursIndices!: Array<Array<Array<number>>>; // Ew
private _occlusionNeighboursIndices!: number[]; // Ew
private _occlusions: number[];
private _localNeighbourhoodCache: number[];
private _occlusionsSetup: boolean;
private _faceNormals: Vector3[];
private static _instance: OcclusionManager;
public static get Get() {
@ -20,7 +19,6 @@ export class OcclusionManager {
this._setupOcclusions();
this._occlusions = new Array<number>(6 * 4 * 4);
this._localNeighbourhoodCache = Array<number>(27);
this._faceNormals = this.getFaceNormals();
}
public getBlankOcclusions() {
@ -45,7 +43,7 @@ export class OcclusionManager {
let numNeighbours = 0;
let occlusionValue = 1.0;
for (let i = 0; i < 2; ++i) {
const neighbourIndex = this._occlusionNeighboursIndices[f][v][i];
const neighbourIndex = this._occlusionNeighboursIndices[this._getOcclusionMapIndex(f, v, i)];
numNeighbours += this._localNeighbourhoodCache[neighbourIndex];
}
// If both edge blocks along this vertex exist,
@ -54,7 +52,7 @@ export class OcclusionManager {
if (numNeighbours == 2 && AppConfig.Get.AMBIENT_OCCLUSION_OVERRIDE_CORNER) {
++numNeighbours;
} else {
const neighbourIndex = this._occlusionNeighboursIndices[f][v][2];
const neighbourIndex = this._occlusionNeighboursIndices[this._getOcclusionMapIndex(f, v, 2)];
numNeighbours += this._localNeighbourhoodCache[neighbourIndex];
}
@ -64,9 +62,9 @@ export class OcclusionManager {
const baseIndex = f * 16 + v;
this._occlusions[baseIndex + 0] = occlusionValue;
this._occlusions[baseIndex + 4] = occlusionValue;
this._occlusions[baseIndex + 8] = occlusionValue;
this._occlusions[baseIndex + 0] = occlusionValue;
this._occlusions[baseIndex + 4] = occlusionValue;
this._occlusions[baseIndex + 8] = occlusionValue;
this._occlusions[baseIndex + 12] = occlusionValue;
}
}
@ -74,16 +72,8 @@ export class OcclusionManager {
return this._occlusions;
}
public getFaceNormals() {
return [
new Vector3(1, 0, 0), new Vector3(-1, 0, 0),
new Vector3(0, 1, 0), new Vector3(0, -1, 0),
new Vector3(0, 0, 1), new Vector3(0, 0, -1),
];
}
public static getNeighbourIndex(neighbour: Vector3) {
return 9*(neighbour.x+1) + 3*(neighbour.y+1) + (neighbour.z+1);
return 9 * (neighbour.x + 1) + 3 * (neighbour.y + 1) + (neighbour.z + 1);
}
private _setupOcclusions() {
@ -141,15 +131,20 @@ export class OcclusionManager {
],
];
this._occlusionNeighboursIndices = new Array<Array<Array<number>>>();
this._occlusionNeighboursIndices = [];
for (let i = 0; i < 6; ++i) {
const row = new Array<Array<number>>();
for (let j = 0; j < 4; ++j) {
row.push(occlusionNeighbours[i][j].map((x) => OcclusionManager.getNeighbourIndex(x)));
for (let k = 0; k < 3; ++k) {
const index = this._getOcclusionMapIndex(i, j, k);
this._occlusionNeighboursIndices[index] = OcclusionManager.getNeighbourIndex(occlusionNeighbours[i][j][k]);
}
}
this._occlusionNeighboursIndices.push(row);
}
this._occlusionsSetup = true;
}
private _getOcclusionMapIndex(faceIndex: number, vertexIndex: number, offsetIndex: number): number {
return (12 * faceIndex) + (3 * vertexIndex) + offsetIndex;
}
}

View File

@ -29,7 +29,7 @@ export class PaletteManager {
}
export class Palette {
public static PALETTE_NAME_REGEX: RegExp = /^[a-zA-Z\-]+$/;
public static PALETTE_NAME_REGEX: RegExp = /^[a-zA-Z\- ]+$/;
public static PALETTE_FILE_EXT: string = '.palette';
private static _FILE_VERSION: number = 1;

View File

@ -7,6 +7,7 @@ import { MaterialType, SolidMaterial, TexturedMaterial } from './mesh';
import { RenderBuffer } from './render_buffer';
import { ShaderManager } from './shaders';
import { Texture } from './texture';
import { ASSERT } from './util/error_util';
import { Vector3 } from './vector';
import { RenderMeshParams, RenderNextBlockMeshChunkParams, RenderNextVoxelMeshChunkParams } from './worker_types';
@ -45,10 +46,11 @@ export class Renderer {
private _modelsAvailable: number;
private _materialBuffers: Array<{
private _materialBuffers: Map<string, {
material: SolidMaterial | (TexturedMaterial & TextureMaterialRenderAddons)
buffer: twgl.BufferInfo,
numElements: number,
materialName: string,
}>;
public _voxelBuffer?: twgl.BufferInfo[];
private _blockBuffer?: twgl.BufferInfo[];
@ -57,6 +59,7 @@ export class Renderer {
private _isGridComponentEnabled: { [bufferComponent: string]: boolean };
private _axesEnabled: boolean;
private _nightVisionEnabled: boolean;
private _gridBuffers: {
x: { [meshType: string]: RenderBuffer };
@ -77,7 +80,7 @@ export class Renderer {
twgl.addExtensionsToContext(this._gl);
this._modelsAvailable = 0;
this._materialBuffers = [];
this._materialBuffers = new Map();
this._gridBuffers = { x: {}, y: {}, z: {} };
this._gridEnabled = false;
@ -90,6 +93,7 @@ export class Renderer {
this._isGridComponentEnabled = {};
this._axesEnabled = false;
this._nightVisionEnabled = true;
this._axisBuffer = new RenderBuffer([
{ name: 'position', numComponents: 3 },
@ -124,6 +128,14 @@ export class Renderer {
// /////////////////////////////////////////////////////////////////////////
private _lightingAvailable: boolean = false;
public setLightingAvailable(isAvailable: boolean) {
this._lightingAvailable = isAvailable;
if (!isAvailable) {
this._nightVisionEnabled = true;
}
}
public toggleIsGridEnabled() {
this._gridEnabled = !this._gridEnabled;
}
@ -140,6 +152,21 @@ export class Renderer {
this._axesEnabled = !this._axesEnabled;
}
public canToggleNightVision() {
return this._lightingAvailable;
}
public toggleIsNightVisionEnabled() {
this._nightVisionEnabled = !this._nightVisionEnabled;
if (!this._lightingAvailable) {
this._nightVisionEnabled = true;
}
}
public isNightVisionEnabled() {
return this._nightVisionEnabled;
}
public toggleIsWireframeEnabled() {
const isEnabled = !this._isGridComponentEnabled[EDebugBufferComponents.Wireframe];
this._isGridComponentEnabled[EDebugBufferComponents.Wireframe] = isEnabled;
@ -156,26 +183,89 @@ export class Renderer {
}
public clearMesh() {
this._materialBuffers = [];
this._materialBuffers = new Map();
this._modelsAvailable = 0;
this.setModelToUse(MeshType.None);
}
public useMesh(params: RenderMeshParams.Output) {
this._materialBuffers = [];
public recreateMaterialBuffer(materialName: string, material: SolidMaterial | TexturedMaterial) {
const oldBuffer = this._materialBuffers.get(materialName);
ASSERT(oldBuffer !== undefined);
if (material.type === MaterialType.solid) {
this._materialBuffers.set(materialName, {
buffer: oldBuffer.buffer,
material: material,
numElements: oldBuffer.numElements,
materialName: materialName,
});
} else {
this._materialBuffers.set(materialName, {
buffer: oldBuffer.buffer,
material: {
type: MaterialType.textured,
path: material.path,
edited: material.edited,
canBeTextured: material.canBeTextured,
texture: twgl.createTexture(this._gl, {
src: material.path,
mag: this._gl.LINEAR,
}),
alphaFactor: material.alphaFactor,
alpha: material.alphaPath ? twgl.createTexture(this._gl, {
src: material.alphaPath,
mag: this._gl.LINEAR,
}) : undefined,
useAlphaChannel: material.alphaPath ? new Texture(material.path, material.alphaPath)._useAlphaChannel() : undefined,
},
numElements: oldBuffer.numElements,
materialName: materialName,
});
}
}
for (const { material, buffer, numElements } of params.buffers) {
public updateMeshMaterialTexture(materialName: string, material: TexturedMaterial) {
this._materialBuffers.forEach((buffer) => {
if (buffer.materialName === materialName) {
buffer.material = {
type: MaterialType.textured,
path: material.path,
edited: material.edited,
canBeTextured: material.canBeTextured,
texture: twgl.createTexture(this._gl, {
src: material.path,
mag: this._gl.LINEAR,
}),
alphaFactor: material.alphaFactor,
alpha: material.alphaPath ? twgl.createTexture(this._gl, {
src: material.alphaPath,
mag: this._gl.LINEAR,
}) : undefined,
useAlphaChannel: material.alphaPath ? new Texture(material.path, material.alphaPath)._useAlphaChannel() : undefined,
};
return;
}
});
}
public useMesh(params: RenderMeshParams.Output) {
this._materialBuffers = new Map();
for (const { material, buffer, numElements, materialName } of params.buffers) {
if (material.type === MaterialType.solid) {
this._materialBuffers.push({
this._materialBuffers.set(materialName, {
buffer: twgl.createBufferInfoFromArrays(this._gl, buffer),
material: material,
numElements: numElements,
materialName: materialName,
});
} else {
this._materialBuffers.push({
this._materialBuffers.set(materialName, {
buffer: twgl.createBufferInfoFromArrays(this._gl, buffer),
material: {
edited: material.edited,
canBeTextured: material.canBeTextured,
type: MaterialType.textured,
path: material.path,
texture: twgl.createTexture(this._gl, {
@ -190,6 +280,7 @@ export class Renderer {
useAlphaChannel: material.alphaPath ? new Texture(material.path, material.alphaPath)._useAlphaChannel() : undefined,
},
numElements: numElements,
materialName: materialName,
});
}
}
@ -228,7 +319,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 +349,7 @@ export class Renderer {
this.setModelToUse(MeshType.VoxelMesh);
}
*/
public useBlockMeshChunk(params: RenderNextBlockMeshChunkParams.Output) {
if (params.isFirstChunk) {
this._blockBuffer = [];
@ -345,7 +436,7 @@ export class Renderer {
}
private _drawMesh() {
for (const materialBuffer of this._materialBuffers) {
this._materialBuffers.forEach((materialBuffer, materialName) => {
if (materialBuffer.material.type === MaterialType.textured) {
this._drawMeshBuffer(materialBuffer.buffer, materialBuffer.numElements, ShaderManager.Get.textureTriProgram, {
u_lightWorldPos: ArcballCamera.Get.getCameraPosition(0.0, 0.0),
@ -365,7 +456,7 @@ export class Renderer {
u_fillColour: RGBAUtil.toArray(materialBuffer.material.colour),
});
}
}
});
}
private _drawVoxelMesh() {
@ -393,6 +484,7 @@ export class Renderer {
u_voxelSize: this._voxelSize,
u_atlasSize: this._atlasSize,
u_gridOffset: this._gridOffset.toArray(),
u_nightVision: this.isNightVisionEnabled(),
};
this._blockBuffer?.forEach((buffer) => {
this._gl.useProgram(shader.program);

View File

@ -13,7 +13,10 @@ export abstract class BaseUIElement<Type> {
this._isEnabled = true;
}
public setEnabled(isEnabled: boolean) {
public setEnabled(isEnabled: boolean, isGroupEnable: boolean = true) {
if (isEnabled && isGroupEnable && !this._obeyGroupEnables) {
return;
}
this._isEnabled = isEnabled;
this._onEnabledChanged();
}
@ -24,7 +27,7 @@ export abstract class BaseUIElement<Type> {
}
protected getValue(): Type {
ASSERT(this._value);
ASSERT(this._value !== undefined);
return this._value;
}
@ -37,4 +40,10 @@ export abstract class BaseUIElement<Type> {
protected abstract _onEnabledChanged(): void;
private _obeyGroupEnables: boolean = true;
public setObeyGroupEnables(shouldListen: boolean) {
this._obeyGroupEnables = shouldListen;
return this;
}
}

110
src/ui/elements/checkbox.ts Normal file
View File

@ -0,0 +1,110 @@
import { getRandomID } from '../../util';
import { ASSERT } from '../../util/error_util';
import { LabelledElement } from './labelled_element';
export class CheckboxElement extends LabelledElement<boolean> {
private _checkboxId: string;
private _checkboxPipId: string;
private _checkboxTextId: string;
private _onText: string;
private _offText: string;
public constructor(label: string, value: boolean, onText: string, offText: string) {
super(label);
this._checkboxId = getRandomID();
this._checkboxPipId = getRandomID();
this._checkboxTextId = getRandomID();
this._value = value;
this._onText = onText;
this._offText = offText;
}
protected generateInnerHTML(): string {
return `
<div class="checkbox" id="${this._checkboxId}">
<svg id="${this._checkboxPipId}" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>
</div>
<div class="checkbox-text" id="${this._checkboxTextId}">${this.getValue() ? this._onText : this._offText}</div>
`;
}
public registerEvents(): void {
const checkboxElement = document.getElementById(this._checkboxId);
const checkboxPipElement = document.getElementById(this._checkboxPipId);
ASSERT(checkboxElement !== null && checkboxPipElement !== null);
checkboxElement?.addEventListener('mouseenter', () => {
if (this._isEnabled) {
checkboxElement.classList.add('checkbox-hover');
checkboxPipElement.classList.add('checkbox-pip-hover');
}
});
checkboxElement?.addEventListener('mouseleave', () => {
if (this._isEnabled) {
checkboxElement.classList.remove('checkbox-hover');
checkboxPipElement.classList.remove('checkbox-pip-hover');
}
});
checkboxElement.addEventListener('click', () => {
if (this._isEnabled) {
this._value = !this._value;
this._onValueChanged();
}
});
}
private _onValueChanged() {
const checkboxElement = document.getElementById(this._checkboxId);
const checkboxPipElement = document.getElementById(this._checkboxPipId);
ASSERT(checkboxElement !== null && checkboxPipElement !== null);
const checkboxTextElement = document.getElementById(this._checkboxTextId);
ASSERT(checkboxTextElement !== null);
checkboxTextElement.innerHTML = this.getValue() ? this._onText : this._offText;
checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden';
if (this._isEnabled) {
checkboxElement.classList.remove('checkbox-disabled');
} else {
checkboxElement.classList.add('checkbox-disabled');
}
this._onValueChangedDelegate?.(this._value!);
}
protected _onEnabledChanged(): void {
super._onEnabledChanged();
const checkboxElement = document.getElementById(this._checkboxId);
const checkboxPipElement = document.getElementById(this._checkboxPipId);
ASSERT(checkboxElement !== null && checkboxPipElement !== null);
const checkboxTextElement = document.getElementById(this._checkboxTextId);
ASSERT(checkboxTextElement !== null);
checkboxTextElement.innerHTML = this.getValue() ? this._onText : this._offText;
checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden';
if (this._isEnabled) {
checkboxElement.classList.remove('checkbox-disabled');
checkboxTextElement.classList.remove('checkbox-text-disabled');
checkboxPipElement.classList.remove('checkbox-pip-disabled');
} else {
checkboxElement.classList.add('checkbox-disabled');
checkboxTextElement.classList.add('checkbox-text-disabled');
checkboxPipElement.classList.add('checkbox-pip-disabled');
}
this._onValueChangedDelegate?.(this._value!);
}
private _onValueChangedDelegate?: (enabled: boolean) => void;
public onValueChanged(delegate: (enabled: boolean) => void) {
this._onValueChangedDelegate = delegate;
return this;
}
}

View File

@ -35,6 +35,9 @@ export class ComboBoxElement<T> extends LabelledElement<T> {
element.addEventListener('change', () => {
EventManager.Get.broadcast(EAppEvent.onComboBoxChanged, element.value);
if (this._onValueChangedDelegate) {
this._onValueChangedDelegate(element.value);
}
});
}
@ -50,5 +53,13 @@ export class ComboBoxElement<T> extends LabelledElement<T> {
const element = document.getElementById(this._id) as HTMLSelectElement;
ASSERT(element !== null);
element.disabled = !this._isEnabled;
this._onValueChangedDelegate?.(element.value);
}
private _onValueChangedDelegate?: (value: any) => void;
public onValueChanged(delegate: (value: any) => void) {
this._onValueChangedDelegate = delegate;
return this;
}
}

208
src/ui/elements/material.ts Normal file
View File

@ -0,0 +1,208 @@
import { remote } from 'electron';
import path from 'path';
import { AppContext } from '../../app_context';
import { RGBAUtil } from '../../colour';
import { SolidMaterial, TexturedMaterial } from '../../mesh';
import { getRandomID } from '../../util';
import { ASSERT } from '../../util/error_util';
import { FileUtil } from '../../util/file_util';
export abstract class MaterialUIElement {
protected readonly _materialName: string;
protected readonly _appContext: AppContext;
private _actions: { text: string, onClick: () => void, id: string }[];
private _metadata: string[];
public constructor(materialName: string, appContext: AppContext) {
this._materialName = materialName;
this._appContext = appContext;
this._actions = [];
this._metadata = [];
}
public hasWarning() {
return false;
}
public buildHTML(): string {
let html = `<div class="material-container">`;
html += this.buildChildHTML();
this._metadata.forEach((data) => {
html += `<br>${data}`;
});
this._actions.forEach((action) => {
html += `<br><a id="${action.id}">[${action.text}]</a>`;
});
html += `</div>`;
return html;
}
public registerEvents() {
this._actions.forEach((action) => {
const element = document.getElementById(action.id);
if (element !== null) {
element.addEventListener('click', () => {
action.onClick();
});
}
});
}
public addAction(text: string, onClick: () => void) {
this._actions.push({ text: text, onClick: onClick, id: getRandomID() });
}
public addMetadata(text: string) {
this._metadata.push(text);
}
protected abstract buildChildHTML(): string
}
export class TextureMaterialUIElement extends MaterialUIElement {
private _material: TexturedMaterial;
private _diffuseImageId: string;
private _alphaImageId: string;
public constructor(materialName: string, appContext: AppContext, material: TexturedMaterial) {
super(materialName, appContext);
this._material = material;
this._diffuseImageId = getRandomID();
this._alphaImageId = getRandomID();
const parsedPath = path.parse(material.path);
const isMissingTexture = parsedPath.base === 'debug.png';
super.addAction(isMissingTexture ? 'Find texture' : 'Replace texture', () => {
const files = remote.dialog.showOpenDialogSync({
title: 'Load',
buttonLabel: 'Load',
filters: [{
name: 'Images',
extensions: ['png', 'jpeg', 'jpg'],
}],
});
if (files && files[0]) {
this._appContext.onMaterialTextureReplace(materialName, files[0]);
}
});
super.addAction('Switch to colour', () => {
this._appContext.onMaterialTypeSwitched(materialName);
});
super.addMetadata(`Alpha multiplier: ${this._material.alphaFactor}`);
if (this._material.alphaPath !== undefined) {
super.addMetadata(`Alpha texture: ${this._material.alphaPath}`);
}
}
private _isMissingTexture() {
const parsedPath = path.parse(this._material.path);
const isMissingTexture = parsedPath.base === 'debug.png';
return isMissingTexture;
}
public hasWarning(): boolean {
return this._isMissingTexture();
}
protected buildChildHTML(): string {
let html = `<img id="${this._diffuseImageId}" class="texture-preview" src="${this._material.path}" width="75%" loading="lazy"></img>`;
if (this._material.alphaPath !== undefined) {
html += `<br><img id="${this._alphaImageId}" class="texture-preview" src="${this._material.alphaPath}" width="75%" loading="lazy"></img>`;
}
return html;
}
public registerEvents(): void {
super.registerEvents();
{
const element = document.getElementById(this._diffuseImageId) as HTMLLinkElement;
if (element) {
if (!this._isMissingTexture()) {
element.addEventListener('mouseover', () => {
element.classList.add('texture-hover');
});
element.addEventListener('mouseleave', () => {
element.classList.remove('texture-hover');
});
element.addEventListener('click', () => {
FileUtil.openDir(this._material.path);
});
} else {
element.classList.add('texture-preview-missing');
}
}
}
{
const element = document.getElementById(this._alphaImageId) as HTMLLinkElement;
if (element) {
if (!this._isMissingTexture()) {
element.addEventListener('mouseover', () => {
element.classList.add('texture-hover');
});
element.addEventListener('mouseleave', () => {
element.classList.remove('texture-hover');
});
element.addEventListener('click', () => {
ASSERT(this._material.alphaPath !== undefined);
FileUtil.openDir(this._material.alphaPath);
});
} else {
element.classList.add('texture-preview-missing');
}
}
}
}
}
export class SolidMaterialUIElement extends MaterialUIElement {
private _material: SolidMaterial;
private _colourId: string;
public constructor(materialName: string, appContext: AppContext, material: SolidMaterial) {
super(materialName, appContext);
this._material = material;
this._colourId = getRandomID();
if (material.canBeTextured) {
super.addAction('Switch to texture', () => {
this._appContext.onMaterialTypeSwitched(materialName);
});
}
this.addMetadata(`Alpha multiplier: ${this._material.colour.a}`);
}
protected buildChildHTML(): string {
return `<input class="colour-swatch" type="color" id="${this._colourId}" value="${RGBAUtil.toHexString(this._material.colour)}">`;
}
public hasWarning(): boolean {
return !this._material.set;
}
public registerEvents(): void {
super.registerEvents();
const colourElement = document.getElementById(this._colourId) as (HTMLInputElement | null);
if (colourElement !== null) {
colourElement.addEventListener('change', () => {
const newColour = RGBAUtil.fromHexString(colourElement.value);
newColour.a = this._material.colour.a;
this._appContext.onMaterialColourChanged(this._materialName, newColour);
});
colourElement.addEventListener('mouseenter', () => {
colourElement.classList.add('colour-swatch-hover');
});
colourElement.addEventListener('mouseleave', () => {
colourElement.classList.remove('colour-swatch-hover');
});
}
}
}

View File

@ -18,6 +18,20 @@ export class OutputElement {
`;
}
public registerEvents() {
const toggler = document.getElementsByClassName('caret');
for (let i = 0; i < toggler.length; i++) {
const temp = toggler[i];
temp.addEventListener('click', function () {
temp.parentElement?.querySelector('.nested')?.classList.toggle('active');
temp.classList.toggle('caret-down');
});
}
this._message.postBuild();
}
private _message: UIMessageBuilder;
public setMessage(message: UIMessageBuilder, style?: OutputStyle) {
@ -60,6 +74,7 @@ export class OutputElement {
ASSERT(element !== null);
element.innerHTML = this._message.toString();
this.registerEvents();
}
public setStyle(style: OutputStyle) {

View File

@ -116,7 +116,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

@ -98,8 +98,10 @@ export class ToolbarItemElement {
element.classList.remove('toolbar-item-disabled');
element.classList.remove('toolbar-item-active');
element.classList.remove('toolbar-item-disabled-active');
svgElement.classList.remove('icon-disabled');
svgElement.classList.remove('icon-active');
svgElement.classList.remove('icon-disabled-active');
if (this._isEnabled) {
if (this._isActive) {
@ -107,8 +109,13 @@ export class ToolbarItemElement {
svgElement.classList.add('icon-active');
}
} else {
element.classList.add('toolbar-item-disabled');
svgElement.classList.add('icon-disabled');
if (this._isActive) {
element.classList.add('toolbar-item-disabled-active');
svgElement.classList.add('icon-disabled-active');
} else {
element.classList.add('toolbar-item-disabled');
svgElement.classList.add('icon-disabled');
}
}
}

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

@ -1,7 +1,6 @@
import fs from 'fs';
import { AppContext } from '../app_context';
import { TBlockAssigners } from '../assigners/assigners';
import { ArcballCamera } from '../camera';
import { AppConfig } from '../config';
import { EAppEvent, EventManager } from '../event';
@ -13,10 +12,12 @@ import { ASSERT } from '../util/error_util';
import { LOG } from '../util/log_util';
import { AppPaths } from '../util/path_util';
import { TAxis } from '../util/type_util';
import { TDithering } from '../util/type_util';
import { TVoxelOverlapRule } from '../voxel_mesh';
import { TVoxelisers } from '../voxelisers/voxelisers';
import { BaseUIElement } from './elements/base';
import { ButtonElement } from './elements/button';
import { CheckboxElement } from './elements/checkbox';
import { ComboBoxElement, ComboBoxItem } from './elements/combobox';
import { FileInputElement } from './elements/file_input';
import { OutputElement } from './elements/output';
@ -99,26 +100,8 @@ export class UI {
displayText: 'Ray-based (legacy)',
},
]),
'ambientOcclusion': new ComboBoxElement('Ambient occlusion', [
{
id: 'on',
displayText: 'On (recommended)',
},
{
id: 'off',
displayText: 'Off (faster)',
},
]),
'multisampleColouring': new ComboBoxElement('Multisampling', [
{
id: 'on',
displayText: 'On (recommended)',
},
{
id: 'off',
displayText: 'Off (faster)',
},
]),
'ambientOcclusion': new CheckboxElement('Ambient occlusion', true, 'On (recommended)', 'Off (faster)'),
'multisampleColouring': new CheckboxElement('Multisampling', true, 'On (recommended)', 'Off (faster)'),
'textureFiltering': new ComboBoxElement('Texture filtering', [
{
id: 'linear',
@ -153,10 +136,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', [
{
@ -183,8 +166,20 @@ export class UI {
},
]),
'colourAccuracy': new SliderElement('Colour accuracy', 1, 8, 1, 5, 0.1),
'contextualAveraging': new CheckboxElement('Smart averaging', true, 'On (recommended)', 'Off (faster)'),
'errorWeight': new SliderElement('Smoothness', 0.0, AppConfig.Get.SMOOTHNESS_MAX, 2, 0.2, 0.01),
'calculateLighting': new CheckboxElement('Calculate lighting', false, 'On', 'Off')
.onValueChanged((value: boolean) => {
if (value) {
this._ui.assign.elements.lightThreshold.setEnabled(true, false);
} else {
this._ui.assign.elements.lightThreshold.setEnabled(false, false);
}
}),
'lightThreshold': new SliderElement('Light threshold', 0, 14, 0, 0, 1)
.setObeyGroupEnables(false),
},
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'fallable', 'colourAccuracy'],
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'fallable', 'colourAccuracy', 'contextualAveraging', 'errorWeight', 'calculateLighting', 'lightThreshold'],
submitButton: new ButtonElement('Assign blocks', () => {
this._appContext.do(EAction.Assign);
}),
@ -265,8 +260,18 @@ export class UI {
.isActive(() => {
return Renderer.Get.isAxesEnabled();
}),
'night-vision': new ToolbarItemElement({ icon: 'bulb' })
.onClick(() => {
Renderer.Get.toggleIsNightVisionEnabled();
})
.isActive(() => {
return Renderer.Get.isNightVisionEnabled();
})
.isEnabled(() => {
return Renderer.Get.canToggleNightVision();
}),
},
elementsOrder: ['grid', 'axes'],
elementsOrder: ['grid', 'axes', 'night-vision'],
},
},

View File

@ -5,11 +5,100 @@ type TMessage = {
body: string,
}
interface IUIOutputElement {
buildHTML(): string;
}
export class UITreeBuilder implements IUIOutputElement {
private _rootLabel: string;
private _children: Array<{ html: string, warning: boolean } | UITreeBuilder>;
private _postBuildDelegates: Array<() => void>;
private _warning: boolean;
private constructor(rootLabel: string) {
this._rootLabel = rootLabel;
this._children = [];
this._postBuildDelegates = [];
this._warning = false;
}
public static create(rootLabel: string): UITreeBuilder {
return new UITreeBuilder(rootLabel);
}
public setWarning() {
this._warning = true;
}
public getWarning() {
if (this._warning) {
return true;
}
for (const child of this._children) {
if (child instanceof UITreeBuilder) {
if (child.getWarning()) {
return true;
}
} else {
if (child.warning) {
return true;
}
}
}
return false;
}
public addChild(child: { html: string, warning: boolean } | UITreeBuilder, postBuildDelegate?: () => void) {
this._children.push(child);
if (postBuildDelegate !== undefined) {
this._postBuildDelegates.push(postBuildDelegate);
}
if (child instanceof UITreeBuilder) {
this._postBuildDelegates.push(() => { child.postBuild(); });
}
}
public postBuild() {
this._postBuildDelegates.forEach((delegate) => {
delegate();
});
}
public buildHTML(): string {
let childrenHTML: string = '';
this._children.forEach((child) => {
childrenHTML += '<li>';
if (child instanceof UITreeBuilder) {
childrenHTML += child.buildHTML();
} else {
childrenHTML += child.warning ? `<p style="margin:0px; color:orange;">${child.html}</p>` : child.html;
}
childrenHTML += '</li>';
});
if (this.getWarning()) {
return `
<span class="caret caret-down" style="color:orange;" >${this._rootLabel}</span>
<ul class="nested active">${childrenHTML}</ul>
`;
} else {
return `
<span class="caret">${this._rootLabel}</span>
<ul class="nested">${childrenHTML}</ul>
`;
}
}
}
export class UIMessageBuilder {
private _messages: TMessage[];
private _postBuildDelegates: Array<() => void>;
public constructor() {
this._messages = [];
this._postBuildDelegates = [];
}
public static create() {
@ -35,7 +124,38 @@ export class UIMessageBuilder {
return this;
}
public addItem(groupId: string, messages: string[], style: OutputStyle) {
public addTree(groupId: string, tree: UITreeBuilder) {
this._messages.push({
groupId: groupId,
body: `<div style="padding-left: 16px;">${tree.buildHTML()}</div>`,
});
this._postBuildDelegates.push(() => { tree.postBuild(); });
}
public setTree(groupId: string, tree: UITreeBuilder) {
let found = false;
this._messages.forEach((message) => {
if (message.groupId === groupId) {
this._postBuildDelegates = []; // TODO: Fix
message.body = `<div style="padding-left: 16px;">${tree.buildHTML()}</div>`;
this._postBuildDelegates.push(() => { tree.postBuild(); });
found = true;
}
return;
});
if (!found) {
this.addTree(groupId, tree);
}
}
public postBuild() {
this._postBuildDelegates.forEach((delegate) => {
delegate();
});
}
public addItem(groupId: string, messages: string[], style: OutputStyle, indent: number = 1) {
for (const message of messages) {
const cssColourClass = this._getStatusCSSClass(style);
this._messages.push({
@ -50,7 +170,7 @@ export class UIMessageBuilder {
this._messages.push({
groupId: groupId, body: `
<div style="display: flex; align-items: center; color: var(--text-standard)">
<div style="margin-right: 8px;" class="loader-circle spin"></div>
<div style="margin-right: 8px;" class="loader-circle spin"></div>
<b class="spin">${message}</b>
</div>
`});

View File

@ -10,7 +10,11 @@ export namespace AppUtil {
*/
export function namespaceBlock(blockName: string): AppTypes.TNamespacedBlockName {
// https://minecraft.fandom.com/wiki/Resource_location#Namespaces
return blockName.includes(':') ? blockName : ('minecraft:' + blockName);
return isNamespacedBlock(blockName) ? blockName : ('minecraft:' + blockName);
}
export function isNamespacedBlock(blockName: string): boolean {
return blockName.includes(':');
}
}
}

View File

@ -1,3 +1,4 @@
import child from 'child_process';
import fs from 'fs';
export namespace FileUtil {
@ -10,4 +11,14 @@ export namespace FileUtil {
fs.mkdirSync(path);
}
}
export function openDir(absolutePath: string) {
switch (process.platform) {
case 'darwin':
child.exec(`open -R ${absolutePath}`);
break;
case 'win32':
child.exec(`explorer /select,"${absolutePath}"`);
}
}
}

View File

@ -1 +1,7 @@
export type TBrand<K, T> = K & { __brand: T };
export type Vector3Hash = TBrand<number, 'Vector3Hash'>;
export type TDithering = 'off' | 'random' | 'ordered';
export type TAxis = 'x' | 'y' | 'z';

View File

@ -1,5 +1,6 @@
import { IHashable } from './hash_map';
import { ASSERT } from './util/error_util';
import { Vector3Hash } from './util/type_util';
export class Vector3 implements IHashable {
public x: number;
@ -236,8 +237,8 @@ export class Vector3 implements IHashable {
}
// Begin IHashable interface
public hash() {
return ((this.x + 10_000_000) << 42) + ((this.y + 10_000_000) << 21) + (this.z + 10_000_000);
public hash(): Vector3Hash {
return ((this.x + 10_000_000) << 42) + ((this.y + 10_000_000) << 21) + (this.z + 10_000_000) as Vector3Hash;
}
public equals(other: Vector3) {

View File

@ -1,3 +1,4 @@
import { EFaceVisibility } from './block_assigner';
import { Bounds } from './bounds';
import { ChunkedBufferGenerator, TVoxelMeshBufferDescription } from './buffer';
import { RGBA } from './colour';
@ -42,6 +43,14 @@ export class VoxelMesh {
return this._voxelsHash.has(pos.hash());
}
public isOpaqueVoxelAt(pos: Vector3) {
const voxel = this.getVoxelAt(pos);
if (voxel) {
return voxel.colour.a == 1.0;
}
return false;
}
public getVoxelAt(pos: Vector3): TOptional<Voxel> {
const voxelIndex = this._voxelsHash.get(pos.hash());
if (voxelIndex !== undefined) {
@ -51,6 +60,33 @@ export class VoxelMesh {
}
}
public static getFullFaceVisibility(): EFaceVisibility {
return EFaceVisibility.Up | EFaceVisibility.Down | EFaceVisibility.North | EFaceVisibility.West | EFaceVisibility.East | EFaceVisibility.South;
}
public getFaceVisibility(pos: Vector3) {
let visibility: EFaceVisibility = 0;
if (!this.isOpaqueVoxelAt(Vector3.add(pos, new Vector3(0, 1, 0)))) {
visibility += EFaceVisibility.Up;
}
if (!this.isOpaqueVoxelAt(Vector3.add(pos, new Vector3(0, -1, 0)))) {
visibility += EFaceVisibility.Down;
}
if (!this.isOpaqueVoxelAt(Vector3.add(pos, new Vector3(1, 0, 0)))) {
visibility += EFaceVisibility.North;
}
if (!this.isOpaqueVoxelAt(Vector3.add(pos, new Vector3(-1, 0, 0)))) {
visibility += EFaceVisibility.South;
}
if (!this.isOpaqueVoxelAt(Vector3.add(pos, new Vector3(0, 0, 1)))) {
visibility += EFaceVisibility.East;
}
if (!this.isOpaqueVoxelAt(Vector3.add(pos, new Vector3(0, 0, -1)))) {
visibility += EFaceVisibility.West;
}
return visibility;
}
public addVoxel(pos: Vector3, colour: RGBA) {
if (colour.a === 0) {
return;
@ -58,7 +94,8 @@ export class VoxelMesh {
pos.round();
const voxelIndex = this._voxelsHash.get(pos.hash());
const hash = pos.hash();
const voxelIndex = this._voxelsHash.get(hash);
if (voxelIndex !== undefined) {
// A voxel at this position already exists
const voxel = this._voxels[voxelIndex];
@ -74,7 +111,7 @@ export class VoxelMesh {
colour: colour,
collisions: 1,
});
this._voxelsHash.set(pos.hash(), this._voxels.length - 1);
this._voxelsHash.set(hash, this._voxels.length - 1);
this._bounds.extendByPoint(pos);
this._updateNeighbours(pos);
}
@ -84,6 +121,10 @@ export class VoxelMesh {
return this._bounds;
}
public getVoxelIndex(pos: Vector3) {
return this._voxelsHash.get(pos.hash());
}
public getVoxelCount() {
return this._voxels.length;
}

View File

@ -25,6 +25,12 @@ export function doWork(message: TToWorkerMessage): TFromWorkerMessage {
result: WorkerClient.Get.import(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
case 'SetMaterials':
return {
action: 'SetMaterials',
result: WorkerClient.Get.setMaterials(message.params),
statusMessages: StatusHandler.Get.getAllStatusMessages(),
};
case 'RenderMesh':
return {
action: 'RenderMesh',

View File

@ -12,7 +12,7 @@ import { Logger } from './util/log_util';
import { VoxelMesh } from './voxel_mesh';
import { IVoxeliser } from './voxelisers/base-voxeliser';
import { VoxeliserFactory } from './voxelisers/voxelisers';
import { AssignParams, ExportParams, ImportParams, InitParams, RenderMeshParams, RenderNextBlockMeshChunkParams, RenderNextVoxelMeshChunkParams, TFromWorkerMessage, VoxeliseParams } from './worker_types';
import { AssignParams, ExportParams, ImportParams, InitParams, RenderMeshParams, RenderNextBlockMeshChunkParams, RenderNextVoxelMeshChunkParams, SetMaterialsParams, TFromWorkerMessage, VoxeliseParams } from './worker_types';
export class WorkerClient {
private static _instance: WorkerClient;
@ -79,6 +79,17 @@ export class WorkerClient {
return {
triangleCount: this._loadedMesh.getTriangleCount(),
dimensions: this._loadedMesh.getBounds().getDimensions(),
materials: this._loadedMesh.getMaterials(),
};
}
public setMaterials(params: SetMaterialsParams.Input): SetMaterialsParams.Output {
ASSERT(this._loadedMesh !== undefined);
this._loadedMesh.setMaterials(params.materials);
return {
materials: this._loadedMesh.getMaterials(),
};
}

View File

@ -1,13 +1,14 @@
import { TBlockAssigners } from './assigners/assigners';
import { FallableBehaviour } from './block_mesh';
import { TBlockMeshBufferDescription, TMeshBufferDescription, TVoxelMeshBufferDescription } from './buffer';
import { RGBAUtil } from './colour';
import { TExporters } from './exporters/exporters';
import { MaterialMap } from './mesh';
import { StatusMessage } from './status';
import { TextureFiltering } from './texture';
import { ColourSpace } from './util';
import { AppError } from './util/error_util';
import { TAxis } from './util/type_util';
import { TDithering } from './util/type_util';
import { Vector3 } from './vector';
import { TVoxelOverlapRule } from './voxel_mesh';
import { TVoxelisers } from './voxelisers/voxelisers';
@ -20,6 +21,16 @@ export namespace InitParams {
}
}
export namespace SetMaterialsParams {
export type Input = {
materials: MaterialMap
}
export type Output = {
materials: MaterialMap
}
}
export namespace ImportParams {
export type Input = {
filepath: string,
@ -28,6 +39,7 @@ export namespace ImportParams {
export type Output = {
triangleCount: number,
dimensions: Vector3,
materials: MaterialMap
}
}
@ -95,10 +107,14 @@ export namespace AssignParams {
export type Input = {
textureAtlas: TAtlasId,
blockPalette: TPaletteId,
blockAssigner: TBlockAssigners,
dithering: TDithering,
colourSpace: ColourSpace,
fallable: FallableBehaviour,
resolution: RGBAUtil.TColourAccuracy,
calculateLighting: boolean,
lightThreshold: number,
contextualAveraging: boolean,
errorWeight: number,
}
export type Output = {
@ -159,6 +175,7 @@ export type TaskParams =
export type TToWorkerMessage =
| { action: 'Init', params: InitParams.Input }
| { action: 'Import', params: ImportParams.Input }
| { action: 'SetMaterials', params: SetMaterialsParams.Input }
| { action: 'RenderMesh', params: RenderMeshParams.Input }
| { action: 'Voxelise', params: VoxeliseParams.Input }
//| { action: 'RenderVoxelMesh', params: RenderVoxelMeshParams.Input }
@ -175,6 +192,7 @@ export type TFromWorkerMessage =
| (TStatus & (
| { action: 'Init', result: InitParams.Output }
| { action: 'Import', result: ImportParams.Output }
| { action: 'SetMaterials', result: SetMaterialsParams.Output }
| { action: 'RenderMesh', result: RenderMeshParams.Output }
| { action: 'Voxelise', result: VoxeliseParams.Output }
//| { action: 'RenderVoxelMesh', result: RenderVoxelMeshParams.Output }

View File

@ -148,7 +148,7 @@ input::-webkit-inner-spin-button {
select {
width: 100%;
height: 100%;
padding-left: 10px;
padding-left: 5px;
border-radius: var(--border-radius);
font-family: 'Lexend', sans-serif;
font-weight: 300;
@ -233,9 +233,6 @@ select:disabled {
color: #808080 !important;
}
.button-label {
}
.button-progress {
/*border: 1px solid green;*/
z-index: 5;
@ -402,6 +399,9 @@ select:disabled {
}
.toolbar-item {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 0px 8px 0px 8px;
text-align: center;
@ -423,6 +423,11 @@ select:disabled {
border: 1px solid var(--prop-accent-border-hovered) !important;
}
.toolbar-item-disabled-active {
background-color: var(--prop-accent-disabled) !important;
border: 1px solid var(--prop-accent-disabled) !important;
}
.toolbar-item-disabled {
background-color: var(--prop-disabled) !important;
border: 1px solid var(--prop-disabled) !important;
@ -447,7 +452,6 @@ select:disabled {
svg {
width: 16px;
height: 16px;
padding-top: 7.5px;
stroke: var(--text-standard);
}
@ -459,6 +463,10 @@ svg {
stroke: var(--text-disabled) !important;
}
.icon-disabled-active {
stroke: #808080 !important;
}
.palette-container {
width: 100%;
height: 200px;
@ -582,4 +590,142 @@ svg {
background-color: var(--prop-accent-standard);
height: 100%;
transition: width 0.1s;
}
/* Remove default bullets */
ul {
list-style-type: none;
margin: 0;
padding-left: 20px;
}
/* Style the caret/arrow */
.caret {
cursor: pointer;
user-select: none; /* Prevent text selection */
}
.caret:hover {
color: var(--text-standard) !important;
}
/* Create the caret/arrow with a unicode, and style it */
.caret::before {
content: "\25B6";
display: inline-block;
margin-right: 6px;
}
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
.caret-down::before {
transform: rotate(90deg);
}
/* Hide the nested list */
.nested {
display: none;
}
/* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */
.active {
display: block;
}
a {
border-bottom: 1px solid currentColor;
cursor: pointer;
color: #8C8C8C80;
}
a:hover {
color: var(--text-standard) !important;
}
.colour-swatch {
border: 1px solid #8C8C8C80;
border-radius: 5px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: none;
width: 75%;
height: 24px;
margin: 0px;
padding: 0px;
}
.colour-swatch-hover {
border-color: var(--text-standard) !important;
}
.colour-swatch::-webkit-color-swatch-wrapper {
border: none;
margin: 0px;
padding: 0px;
}
.colour-swatch::-webkit-color-swatch {
border: none;
margin: 0px;
padding: 0px;
border-radius: 5px;
}
.texture-preview {
border: 1px solid #8C8C8C80;
border-radius: 5px;
}
.texture-preview-missing {
border-color: orange !important;
}
.texture-hover {
border-color: var(--text-standard) !important;
}
.material-container {
border-left: 1px solid #8C8C8C80;
padding-left: 5px;
}
.checkbox {
height: 75%;
aspect-ratio: 1/1;
background-color: var(--prop-standard);
border-radius: 5px;
border: 1px solid var(--prop-standard);
display: flex;
align-items: center;
justify-content: center;
}
.checkbox-hover {
background-color: var(--prop-hovered) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
cursor: pointer;
}
.checkbox-disabled {
background-color: var(--prop-disabled) !important;
border: 1px solid var(--prop-disabled) !important;
}
.checkbox-text {
padding-left: 10px;
font-weight: 300;
color: var(--text-standard)
}
.checkbox-text-disabled {
color: var(--text-disabled);
}
.checkbox-pip {
width: 50%;
height: 50%;
border-radius: 3px;
}
.checkbox-pip-hover {
stroke: white;
}
.checkbox-pip-disabled {
stroke: var(--text-disabled);
}

View File

@ -20,10 +20,14 @@ 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,
calculateLighting: false,
lightThreshold: 0,
contextualAveraging: true,
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

View File

@ -20,10 +20,14 @@ 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,
calculateLighting: false,
lightThreshold: 0,
contextualAveraging: true,
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

View File

@ -20,10 +20,14 @@ 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,
calculateLighting: false,
lightThreshold: 0,
contextualAveraging: true,
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

View File

@ -20,10 +20,14 @@ 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,
calculateLighting: false,
lightThreshold: 0,
contextualAveraging: true,
errorWeight: 0.0,
},
export: {
filepath: '', // Must be an absolute path to the file (can be anywhere)

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

@ -1,422 +1,423 @@
import AdmZip from 'adm-zip';
import chalk from 'chalk';
import fs from 'fs';
import images from 'images';
import path from 'path';
import { PNG } from 'pngjs';
import prompt from 'prompt';
import * as sharp from 'sharp';
import { RGBA, RGBAUtil } from '../src/colour';
import { AppUtil } from '../src/util';
import { LOG, LOG_WARN, Logger } from '../src/util/log_util';
const AdmZip = require('adm-zip');
const copydir = require('copy-dir');
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 { log } from './logging';
import { ASSERT_EXISTS, getAverageColour, getMinecraftDir, getStandardDeviation } from './misc';
const BLOCKS_DIR = PathUtil.join(AppPaths.Get.tools, '/blocks');
const MODELS_DIR = PathUtil.join(AppPaths.Get.tools, '/models');
type TFaceData<T> = {
up: T,
down: T,
north: T,
south: T,
east: T,
west: T,
}
export type TAtlasVersion = {
formatVersion: 3,
atlasSize: number,
blocks: Array<{ name: string, faces: TFaceData<string>, colour: RGBA }>,
textures: { [texture: string]: { atlasColumn: number, atlasRow: number, colour: RGBA, std: number } },
supportedBlockNames: string[],
};
void async function main() {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
Logger.Get.enableLogToFile();
Logger.Get.initLogFile('atlas');
await getPermission();
checkMinecraftInstallation();
cleanupDirectories();
await fetchModelsAndTextures();
await buildAtlas();
cleanupDirectories();
}();
const minecraftDir = getMinecraftDir();
function checkMinecraftInstallation() {
const dir = getMinecraftDir();
if (!fs.existsSync(dir)) {
log(LogStyle.Failure, `Could not find ${dir}`);
log(LogStyle.Failure, 'To use this tool you need to install Minecraft Java Edition');
process.exit(1);
} else {
log(LogStyle.Success, `Found Minecraft Java Edition installation at ${dir}`);
}
}
function cleanupDirectories() {
fs.rmSync(BLOCKS_DIR, { recursive: true, force: true });
fs.rmSync(MODELS_DIR, { recursive: true, force: true });
}
async function getResourcePack() {
const resourcePacksDir = path.join(getMinecraftDir(), './resourcepacks');
if (!fs.existsSync(resourcePacksDir)) {
log(LogStyle.Failure, 'Could not find .minecraft/resourcepacks\n');
process.exit(1);
// Clean up temporary data from previous use
{
fs.rmSync(BLOCKS_DIR, { recursive: true, force: true });
fs.rmSync(MODELS_DIR, { recursive: true, force: true });
}
log(LogStyle.Info, 'Looking for resource packs...');
const resourcePacks = fs.readdirSync(resourcePacksDir);
log(LogStyle.None, `1) Vanilla`);
for (let i = 0; i < resourcePacks.length; ++i) {
log(LogStyle.None, `${i + 2}) ${resourcePacks[i]}`);
}
// Ask for permission to access Minecraft dir
{
log('Prompt', `This script requires files inside '${minecraftDir}'`);
const { packChoice } = await prompt.get({
properties: {
packChoice: {
description: `Which resource pack do you want to build an atlas for? (1-${resourcePacks.length + 1})`,
message: `Response must be between 1 and ${resourcePacks.length + 1}`,
required: true,
conform: (value) => {
return value >= 1 && value <= resourcePacks.length + 1;
const { permission } = await prompt.get({
properties: {
permission: {
pattern: /^[YyNn]$/,
description: 'Do you give permission to access these files? (y/n)',
message: 'Response must be Y or N',
required: true,
},
},
},
});
if (<number>packChoice == 1) {
return 'Vanilla';
}
return resourcePacks[(<number>packChoice) - 2];
}
function fetchVanillModelsAndTextures(fetchTextures: boolean) {
const versionsDir = path.join(getMinecraftDir(), './versions');
ASSERT(fs.existsSync(versionsDir), 'Could not find .minecraft/versions');
log(LogStyle.Info, '.minecraft/versions found successfully');
const versions = fs.readdirSync(versionsDir)
.filter((file) => fs.lstatSync(path.join(versionsDir, file)).isDirectory())
.map((file) => ({ file, birthtime: fs.lstatSync(path.join(versionsDir, file)).birthtime }))
.sort((a, b) => b.birthtime.getTime() - a.birthtime.getTime());
for (let i = 0; i < versions.length; ++i) {
const versionName = versions[i].file;
log(LogStyle.Info, `Searching in ${versionName} for ${versionName}.jar`);
const versionDir = path.join(versionsDir, versionName);
const versionFiles = fs.readdirSync(versionDir);
if (!versionFiles.includes(versionName + '.jar')) {
continue;
}
log(LogStyle.Success, `Found ${versionName}.jar successfully\n`);
const versionJarPath = path.join(versionDir, `${versionName}.jar`);
log(LogStyle.Info, `Upzipping ${versionName}.jar...`);
const zip = new AdmZip(versionJarPath);
const zipEntries = zip.getEntries();
zipEntries.forEach((zipEntry: any) => {
if (fetchTextures && zipEntry.entryName.startsWith('assets/minecraft/textures/block')) {
zip.extractEntryTo(zipEntry.entryName, BLOCKS_DIR, false, true);
} else if (zipEntry.entryName.startsWith('assets/minecraft/models/block')) {
zip.extractEntryTo(zipEntry.entryName, MODELS_DIR, false, true);
}
});
log(LogStyle.Success, `Extracted textures and models successfully\n`);
return;
}
}
async function fetchModelsAndTextures() {
const resourcePack = await getResourcePack();
await fetchVanillModelsAndTextures(true);
if (resourcePack === 'Vanilla') {
return;
}
log(LogStyle.Warning, 'Non-16x16 texture packs are not supported');
const resourcePackDir = path.join(getMinecraftDir(), './resourcepacks', resourcePack);
if (fs.lstatSync(resourcePackDir).isDirectory()) {
log(LogStyle.Info, `Resource pack '${resourcePack}' is a directory`);
const blockTexturesSrc = path.join(resourcePackDir, 'assets/minecraft/textures/block');
const blockTexturesDst = BLOCKS_DIR;
log(LogStyle.Info, `Copying ${blockTexturesSrc} to ${blockTexturesDst}`);
copydir(blockTexturesSrc, blockTexturesDst, {
utimes: true,
mode: true,
cover: true,
});
log(LogStyle.Success, `Copied block textures successfully`);
} else {
log(LogStyle.Info, `Resource pack '${resourcePack}' is not a directory, expecting to be a .zip`);
const zip = new AdmZip(resourcePackDir);
const zipEntries = zip.getEntries();
zipEntries.forEach((zipEntry: any) => {
if (zipEntry.entryName.startsWith('assets/minecraft/textures/block')) {
zip.extractEntryTo(zipEntry.entryName, BLOCKS_DIR, false, true);
}
});
log(LogStyle.Success, `Copied block textures successfully`);
return;
}
}
async function buildAtlas() {
// Check /blocks and /models is setup correctly
log(LogStyle.Info, 'Checking assets are provided...');
const texturesDirSetup = isDirSetup('./blocks', 'assets/minecraft/textures/block');
ASSERT(texturesDirSetup, '/blocks is not setup correctly');
log(LogStyle.Success, '/tools/blocks/ setup correctly');
const modelsDirSetup = isDirSetup('./models', 'assets/minecraft/models/block');
ASSERT(modelsDirSetup, '/models is not setup correctly');
log(LogStyle.Success, '/tools/models/ setup correctly');
// Load the ignore list
log(LogStyle.Info, 'Loading ignore list...');
let ignoreList: Array<string> = [];
const ignoreListPath = path.join(AppPaths.Get.tools, './ignore-list.txt');
if (fs.existsSync(ignoreListPath)) {
log(LogStyle.Success, 'Found ignore list');
ignoreList = fs.readFileSync(ignoreListPath, 'utf-8').replace(/\r/g, '').split('\n');
} else {
log(LogStyle.Warning, 'No ignore list found, looked for ignore-list.txt');
}
log(LogStyle.Success, `${ignoreList.length} blocks found in ignore list\n`);
/* eslint-disable */
enum parentModel {
Cube = 'minecraft:block/cube',
CubeAll = 'minecraft:block/cube_all',
CubeColumn = 'minecraft:block/cube_column',
CubeColumnHorizontal = 'minecraft:block/cube_column_horizontal',
CubeBottomTop = 'minecraft:block/cube_bottom_top',
TemplateSingleFace = 'minecraft:block/template_single_face',
TemplateGlazedTerracotta = 'minecraft:block/template_glazed_terracotta',
Leaves = 'minecraft:block/leaves',
}
/* eslint-enable */
interface Model {
name: string,
colour?: RGBA,
faces: {
[face: string]: Texture
const isResponseYes = ['Y', 'y'].includes(permission as string);
if (!isResponseYes) {
process.exit(0);
}
}
interface Texture {
name: string,
texcoord?: UV,
colour?: RGBA
}
ASSERT_EXISTS(minecraftDir);
log(LogStyle.Info, 'Loading block models...');
const faces = ['north', 'south', 'up', 'down', 'east', 'west'];
const allModels: Array<Model> = [];
const allBlockNames: Set<string> = new Set();
const usedTextures: Set<string> = new Set();
fs.readdirSync(MODELS_DIR).forEach((filename) => {
if (path.extname(filename) !== '.json') {
return;
};
// Prompt user to pick a version
let chosenVersionName: string;
let chosenVersionDir: string;
{
const versionsDir = PathUtil.join(minecraftDir, '/versions');
ASSERT_EXISTS(versionsDir);
const filePath = path.join(MODELS_DIR, filename);
const fileData = fs.readFileSync(filePath, 'utf8');
const modelData = JSON.parse(fileData);
const parsedPath = path.parse(filePath);
const modelName = parsedPath.name;
if (ignoreList.includes(filename)) {
return;
const versions = fs.readdirSync(versionsDir)
.filter((file) => fs.lstatSync(PathUtil.join(versionsDir, file)).isDirectory())
.map((file) => ({ file, birthtime: fs.lstatSync(PathUtil.join(versionsDir, file)).birthtime }))
.sort((a, b) => b.birthtime.getTime() - a.birthtime.getTime());
{
versions.forEach((version, index) => {
log('Option', `${index + 1}) ${version.file}`);
});
}
let faceData: { [face: string]: Texture } = {};
switch (modelData.parent) {
case parentModel.CubeAll:
faceData = {
up: { name: modelData.textures.all },
down: { name: modelData.textures.all },
north: { name: modelData.textures.all },
south: { name: modelData.textures.all },
east: { name: modelData.textures.all },
west: { name: modelData.textures.all },
};
break;
case parentModel.CubeColumn:
faceData = {
up: { name: modelData.textures.end },
down: { name: modelData.textures.end },
north: { name: modelData.textures.side },
south: { name: modelData.textures.side },
east: { name: modelData.textures.side },
west: { name: modelData.textures.side },
};
break;
case parentModel.CubeBottomTop:
faceData = {
up: { name: modelData.textures.top },
down: { name: modelData.textures.bottom },
north: { name: modelData.textures.side },
south: { name: modelData.textures.side },
east: { name: modelData.textures.side },
west: { name: modelData.textures.side },
};
break;
case parentModel.Cube:
faceData = {
up: { name: modelData.textures.up },
down: { name: modelData.textures.down },
north: { name: modelData.textures.north },
south: { name: modelData.textures.south },
east: { name: modelData.textures.east },
west: { name: modelData.textures.west },
};
break;
case parentModel.TemplateSingleFace:
faceData = {
up: { name: modelData.textures.texture },
down: { name: modelData.textures.texture },
north: { name: modelData.textures.texture },
south: { name: modelData.textures.texture },
east: { name: modelData.textures.texture },
west: { name: modelData.textures.texture },
};
break;
case parentModel.TemplateGlazedTerracotta:
faceData = {
up: { name: modelData.textures.pattern },
down: { name: modelData.textures.pattern },
north: { name: modelData.textures.pattern },
south: { name: modelData.textures.pattern },
east: { name: modelData.textures.pattern },
west: { name: modelData.textures.pattern },
};
break;
case parentModel.Leaves:
faceData = {
up: { name: modelData.textures.all },
down: { name: modelData.textures.all },
north: { name: modelData.textures.all },
south: { name: modelData.textures.all },
east: { name: modelData.textures.all },
west: { name: modelData.textures.all },
};
break;
default:
return;
}
for (const face of faces) {
usedTextures.add(faceData[face].name);
}
allModels.push({
name: modelName,
faces: faceData,
});
allBlockNames.add(modelName);
});
if (allModels.length === 0) {
log(LogStyle.Failure, 'No blocks loaded');
process.exit(0);
}
log(LogStyle.Success, `${allModels.length} blocks loaded\n`);
const atlasSize = Math.ceil(Math.sqrt(usedTextures.size));
const atlasWidthPixels = atlasSize * 16 * 3;
let offsetX = 0;
let offsetY = 0;
const textureDetails: { [textureName: string]: { texcoord: UV, colour: RGBA } } = {};
const { atlasName } = await prompt.get({
properties: {
atlasName: {
pattern: /^[a-zA-Z\-]+$/,
description: 'What do you want to call this texture atlas?',
message: 'Name must only be letters or dash',
required: true,
// Prompt user to pick a version
const { packChoice } = await prompt.get({
properties: {
packChoice: {
description: `Which version do you want to build an atlas for? (1-${versions.length})`,
message: `Response must be between 1 and ${versions.length}`,
required: true,
conform: (value) => {
return value >= 1 && value <= versions.length;
},
},
},
},
});
});
//const tiles: mergeImages.ImageSource[] = [];
let outputImage = sharp.default({
create: {
width: atlasWidthPixels,
height: atlasWidthPixels,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0.0 },
},
});
chosenVersionName = versions[(<number>packChoice) - 1].file;
chosenVersionDir = PathUtil.join(versionsDir, chosenVersionName);
}
const composites: sharp.OverlayOptions[] = [];
// Get vanilla models and textures
{
const jarName = `${chosenVersionName}.jar`;
const jarDir = PathUtil.join(chosenVersionDir, jarName);
ASSERT_EXISTS(jarDir);
log(LogStyle.Info, `Building ${atlasName}.png...`);
usedTextures.forEach((textureName) => {
const shortName = textureName.split('/')[1]; // Eww
const absolutePath = path.join(BLOCKS_DIR, shortName + '.png');
log('Info', `Upzipping '${jarDir}'...`);
{
const zip = new AdmZip(jarDir);
const zipEntries = zip.getEntries();
zipEntries.forEach((zipEntry: any) => {
if (zipEntry.entryName.startsWith('assets/minecraft/textures/block')) {
zip.extractEntryTo(zipEntry.entryName, BLOCKS_DIR, false, true);
} else if (zipEntry.entryName.startsWith('assets/minecraft/models/block')) {
zip.extractEntryTo(zipEntry.entryName, MODELS_DIR, false, true);
}
});
}
log('Success', `Extracted Vanilla models to '${MODELS_DIR}'`);
log('Success', `Extracted Vanilla textures to '${BLOCKS_DIR}'`);
}
for (let x = 0; x < 3; ++x) {
for (let y = 0; y < 3; ++y) {
composites.push({
input: absolutePath,
left: 16 * (3 * offsetX + x),
top: 16 * (3 * offsetY + y),
// Prompt user to pick a resource pack
let chosenResourcePackDir: string | undefined;
{
const resourcePacksDir = PathUtil.join(minecraftDir, '/resourcepacks');
ASSERT_EXISTS(resourcePacksDir);
const resourcePacks = fs.readdirSync(resourcePacksDir);
{
log('Option', `1) Vanilla`);
resourcePacks.forEach((resourcePack, index) => {
log('Option', `${index + 2}) ${resourcePack}`);
});
}
const { resourcePackChoiceIndex } = await prompt.get({
properties: {
packChoice: {
description: `Which resource pack do you want to build an atlas for? (1-${resourcePacks.length + 1})`,
message: `Response must be between 1 and ${resourcePacks.length + 1}`,
required: true,
conform: (value) => {
return value >= 1 && value <= resourcePacks.length + 1;
},
},
},
});
chosenResourcePackDir = (<number>resourcePackChoiceIndex) === 1 ? undefined : resourcePacks[(<number>resourcePackChoiceIndex) - 2];
}
// Get resource pack textures
if (chosenResourcePackDir !== undefined) {
log('Warning', 'Using non-16x16 texture packs is not supported and will result in undefined behaviour');
{
if (fs.lstatSync(chosenResourcePackDir).isDirectory()) {
log('Info', `Resource pack '${chosenResourcePackDir}' is a directory`);
const blockTexturesSrc = PathUtil.join(chosenResourcePackDir, 'assets/minecraft/textures/block');
const blockTexturesDst = BLOCKS_DIR;
log('Info', `Copying ${blockTexturesSrc} to ${blockTexturesDst}...`);
copydir(blockTexturesSrc, blockTexturesDst, {
utimes: true,
mode: true,
cover: true,
});
} else {
log('Info', `Resource pack '${chosenResourcePackDir}' is not a directory, expecting to be a .zip`);
const zip = new AdmZip(chosenResourcePackDir);
const zipEntries = zip.getEntries();
zipEntries.forEach((zipEntry: any) => {
if (zipEntry.entryName.startsWith('assets/minecraft/textures/block')) {
zip.extractEntryTo(zipEntry.entryName, BLOCKS_DIR, false, true);
}
});
}
}
const fileData = fs.readFileSync(absolutePath);
const pngData = PNG.sync.read(fileData);
textureDetails[textureName] = {
texcoord: new UV(
16 * (3 * offsetX + 1) / atlasWidthPixels,
16 * (3 * offsetY + 1) / atlasWidthPixels,
),
colour: getAverageColour(pngData),
};
++offsetX;
if (offsetX >= atlasSize) {
++offsetY;
offsetX = 0;
}
});
outputImage = outputImage.composite(composites);
// Build up the output JSON
log(LogStyle.Info, `Building ${atlasName}.atlas...\n`);
for (const model of allModels) {
const blockColour: RGBA = {
r: 0.0, g: 0.0, b: 0.0, a: 0.0,
};
for (const face of faces) {
const faceTexture = textureDetails[model.faces[face].name];
const faceColour = faceTexture.colour;
blockColour.r += faceColour.r;
blockColour.g += faceColour.g;
blockColour.b += faceColour.b;
blockColour.a += faceColour.a;
model.faces[face].texcoord = faceTexture.texcoord;
}
blockColour.r /= 6;
blockColour.g /= 6;
blockColour.b /= 6;
blockColour.a /= 6;
model.colour = blockColour;
log('Success', `Copied block textures successfully`);
}
// Load the ignore list
let ignoreList: Array<string> = [];
{
log('Info', 'Loading ignore list...');
{
const ignoreListPath = PathUtil.join(AppPaths.Get.tools, './models-ignore-list.txt');
if (fs.existsSync(ignoreListPath)) {
log('Success', `Found ignore list in '${ignoreListPath}'`);
ignoreList = fs.readFileSync(ignoreListPath, 'utf-8').replace(/\r/g, '').split('\n');
log('Info', `Found ${ignoreList.length} blocks in ignore list`);
} else {
log('Warning', `Could not find ignore list '${ignoreListPath}'`);
}
}
}
log(LogStyle.Info, 'Exporting...');
const atlasTextureDir = path.join(AppPaths.Get.atlases, `./${atlasName}.png`);
await outputImage.toFile(atlasTextureDir);
//ASSERT(success, 'Unsuccess save');
const usedTextures = new Set<string>();
const usedModels: Array<{ name: string, faces: TFaceData<string> }> = [];
log(LogStyle.Success, `${atlasName}.png exported to /resources/atlases/`);
const outputJSON = {
atlasSize: atlasSize,
blocks: allModels,
supportedBlockNames: Array.from(allBlockNames),
};
fs.writeFileSync(path.join(AppPaths.Get.atlases, `./${atlasName}.atlas`), JSON.stringify(outputJSON, null, 4));
log(LogStyle.Success, `${atlasName}.atlas exported to /resources/atlases/\n`);
// Load all models to use
{
const allModels = fs.readdirSync(MODELS_DIR);
log('Info', `Found ${allModels.length} models in '${MODELS_DIR}'`);
/* eslint-disable */
console.log(chalk.cyanBright(chalk.inverse('DONE') + ' Now run ' + chalk.inverse(' npm start ') + ' and the new texture atlas can be used'));
/* eslint-enable */
}
allModels.forEach((modelRelDir, index) => {
const modelAbsDir = PathUtil.join(MODELS_DIR, modelRelDir);
const parsed = path.parse(modelAbsDir);
if (parsed.ext !== '.json' || ignoreList.includes(parsed.base)) {
return;
}
const fileData = fs.readFileSync(modelAbsDir, 'utf8');
const modelData = JSON.parse(fileData);
const faceData: TFaceData<string> | undefined = (() => {
switch (modelData.parent) {
case 'minecraft:block/cube_column_horizontal':
return {
up: modelData.textures.side,
down: modelData.textures.side,
north: modelData.textures.end,
south: modelData.textures.end,
east: modelData.textures.side,
west: modelData.textures.side,
};
case 'minecraft:block/cube_all':
return {
up: modelData.textures.all,
down: modelData.textures.all,
north: modelData.textures.all,
south: modelData.textures.all,
east: modelData.textures.all,
west: modelData.textures.all,
};
case 'minecraft:block/cube_column':
return {
up: modelData.textures.end,
down: modelData.textures.end,
north: modelData.textures.side,
south: modelData.textures.side,
east: modelData.textures.side,
west: modelData.textures.side,
};
case 'minecraft:block/cube_bottom_top':
return {
up: modelData.textures.top,
down: modelData.textures.bottom,
north: modelData.textures.side,
south: modelData.textures.side,
east: modelData.textures.side,
west: modelData.textures.side,
};
case 'minecraft:block/cube':
return {
up: modelData.textures.up,
down: modelData.textures.down,
north: modelData.textures.north,
south: modelData.textures.south,
east: modelData.textures.east,
west: modelData.textures.west,
};
case 'minecraft:block/template_single_face':
return {
up: modelData.textures.texture,
down: modelData.textures.texture,
north: modelData.textures.texture,
south: modelData.textures.texture,
east: modelData.textures.texture,
west: modelData.textures.texture,
};
case 'minecraft:block/template_glazed_terracotta':
return {
up: modelData.textures.pattern,
down: modelData.textures.pattern,
north: modelData.textures.pattern,
south: modelData.textures.pattern,
east: modelData.textures.pattern,
west: modelData.textures.pattern,
};
case 'minecraft:block/leaves':
return {
up: modelData.textures.all,
down: modelData.textures.all,
north: modelData.textures.all,
south: modelData.textures.all,
east: modelData.textures.all,
west: modelData.textures.all,
};
}
})();
// Debug logging to file
if (faceData === undefined) {
LOG_WARN(`Could not parse '${parsed.base}'`);
return;
} else {
LOG(`Parsed '${parsed.base}'`);
}
// Check that the textures that this model uses can be found
Object.values(faceData).forEach((texture) => {
const textureBaseName = texture.split('/')[1] + '.png';
const textureAbsDir = PathUtil.join(BLOCKS_DIR, textureBaseName);
if (fs.existsSync(textureAbsDir)) {
LOG(`Found '${textureAbsDir}'`);
} else {
log('Warning', `'${parsed.base}' uses texture '${texture}' but the texture file could not be found at '${textureAbsDir}'`);
return;
}
});
// Update usedTextures and usedModels
Object.values(faceData).forEach((texture) => {
usedTextures.add(texture);
});
usedModels.push({
name: parsed.name,
faces: faceData,
});
});
LOG('All Textures', usedTextures);
LOG('All Models', usedModels);
log('Info', `Found ${usedModels.length} models to use`);
// Prompt user for an atlas name
const { atlasName } = await prompt.get({
properties: {
atlasName: {
pattern: /^[a-zA-Z\-]+$/,
description: 'What do you want to call this texture atlas?',
message: 'Name must only be letters or dash',
required: true,
},
},
});
// Create atlas texture file
const textureDetails: { [texture: string]: { atlasColumn: number, atlasRow: number, colour: RGBA, std: number } } = {};
const atlasSize = Math.ceil(Math.sqrt(usedTextures.size));
{
const atlasWidth = atlasSize * 16;
let offsetX = 0;
let offsetY = 0;
const outputImage = images(atlasWidth * 3, atlasWidth * 3);
usedTextures.forEach((texture) => {
const shortName = texture.split('/')[1]; // Eww
const absolutePath = path.join(BLOCKS_DIR, shortName + '.png');
const fileData = fs.readFileSync(absolutePath);
const pngData = PNG.sync.read(fileData);
const image = images(absolutePath);
for (let x = 0; x < 3; ++x) {
for (let y = 0; y < 3; ++y) {
outputImage.draw(image, 16 * (3 * offsetX + x), 16 * (3 * offsetY + y));
}
}
const average = getAverageColour(pngData);
textureDetails[texture] = {
/*
texcoord: new UV(
16 * (3 * offsetX + 1) / (atlasWidth * 3),
16 * (3 * offsetY + 1) / (atlasWidth * 3),
),
*/
atlasColumn: offsetX,
atlasRow: offsetY,
colour: average,
std: getStandardDeviation(pngData, average),
};
++offsetX;
if (offsetX >= atlasSize) {
++offsetY;
offsetX = 0;
}
});
const atlasDir = PathUtil.join(AppPaths.Get.atlases, `./${atlasName}.png`);
outputImage.save(atlasDir);
}
const modelDetails = new Array<{ name: string, faces: TFaceData<string>, colour: RGBA }>();
{
usedModels.forEach((model) => {
const faceColours = Object.values(model.faces)
.map((face) => textureDetails[face]!.colour);
modelDetails.push({
name: AppUtil.Text.namespaceBlock(model.name),
faces: model.faces,
colour: RGBAUtil.average(...faceColours),
});
});
}
const toExport: TAtlasVersion = {
formatVersion: 3,
atlasSize: atlasSize,
blocks: modelDetails,
textures: textureDetails,
supportedBlockNames: modelDetails.map((model) => model.name),
};
fs.writeFileSync(path.join(AppPaths.Get.atlases, `./${atlasName}.atlas`), JSON.stringify(toExport, null, 4));
}
}();

View File

@ -4,32 +4,32 @@ import prompt from 'prompt';
import { Palette } from '../src/palette';
import { AppPaths, PathUtil } from '../src/util/path_util';
import { log, LogStyle } from './logging';
import { log } from './logging';
const PALETTE_NAME_REGEX = /^[a-zA-Z\-]+$/;
void async function main() {
AppPaths.Get.setBaseDir(PathUtil.join(__dirname, '../..'));
log(LogStyle.Info, 'Creating a new palette...');
log('Info', 'Creating a new palette...');
const paletteBlocksDir = path.join(AppPaths.Get.tools, './new-palette-blocks.txt');
if (!fs.existsSync(paletteBlocksDir)) {
log(LogStyle.Failure, 'Could not find /tools/new-palette-blocks.txt');
log('Failure', 'Could not find /tools/new-palette-blocks.txt');
return;
}
log(LogStyle.Success, 'Found list of blocks to use in /tools/new-palette-blocks.txt');
log('Success', 'Found list of blocks to use in /tools/new-palette-blocks.txt');
let blocksToUse: string[] = fs.readFileSync(paletteBlocksDir, 'utf8').replace(/\r/g, '').split('\n');
blocksToUse = blocksToUse.filter((block) => {
return block.length !== 0;
});
if (blocksToUse.length === 0) {
log(LogStyle.Failure, 'No blocks listed for palette');
log(LogStyle.Info, 'List the blocks you want from /tools/all-supported-blocks.txt ');
log('Failure', 'No blocks listed for palette');
log('Info', 'List the blocks you want from /tools/all-supported-blocks.txt ');
return;
}
log(LogStyle.Info, `Found ${blocksToUse.length} blocks to use`);
log('Info', `Found ${blocksToUse.length} blocks to use`);
const schema: prompt.Schema = {
properties: {
@ -44,24 +44,24 @@ void async function main() {
const promptUser = await prompt.get(schema);
log(LogStyle.Info, 'Creating palette...');
log('Info', 'Creating palette...');
const palette = Palette.create();
if (palette === undefined) {
log(LogStyle.Failure, 'Invalid palette name');
log('Failure', 'Invalid palette name');
return;
}
log(LogStyle.Info, 'Adding blocks to palette...');
log('Info', 'Adding blocks to palette...');
for (const blockNames of blocksToUse) {
palette.add(blockNames);
}
log(LogStyle.Info, 'Saving palette...');
log('Info', 'Saving palette...');
const success = palette.save(promptUser.paletteName as string);
if (success) {
log(LogStyle.Success, 'Palette saved.');
log('Success', 'Palette saved.');
} else {
log(LogStyle.Failure, 'Could not save palette.');
log('Failure', 'Could not save palette.');
}
}();

View File

@ -18,10 +18,14 @@ 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,
calculateLighting: false,
lightThreshold: 0,
contextualAveraging: true,
errorWeight: 0.0,
},
export: {
filepath: '/Users/lucasdower/Documents/out.obj', // Must be an absolute path to the file (can be anywhere)

View File

@ -1,30 +1,61 @@
import chalk from 'chalk';
/* eslint-disable */
export enum LogStyle {
None = 'None',
Info = 'Info',
Warning = 'Warning',
Failure = 'Failure',
Success = 'Success'
}
/* eslint-enable */
export type TLogStyle = 'None' | 'Option' | 'Prompt' | 'Info' | 'Warning' | 'Failure' | 'Success';
const LogStyleDetails: {[style: string]: {style: chalk.Chalk, prefix: string}} = {};
LogStyleDetails[LogStyle.Info] = {style: chalk.blue, prefix: chalk.blue.inverse('INFO')};
LogStyleDetails[LogStyle.Warning] = {style: chalk.yellow, prefix: chalk.yellow.inverse('WARN')};
LogStyleDetails[LogStyle.Failure] = {style: chalk.red, prefix: chalk.red.inverse('UHOH')};
LogStyleDetails[LogStyle.Success] = {style: chalk.green, prefix: chalk.green.inverse(' OK ')};
export function log(style: LogStyle, message: string) {
if (style === LogStyle.None) {
/* eslint-disable */
console.log(chalk.whiteBright(message));
/* eslint-enable */
} else {
const details: {style: chalk.Chalk, prefix: string} = LogStyleDetails[style];
/* eslint-disable */
console.log(details.prefix + ' ' + details.style(message));
/* eslint-enable */
export function log(style: TLogStyle, message: string) {
switch (style) {
case 'None': {
/* eslint-disable-next-line no-console */
console.log(` ${chalk.whiteBright(message)}`);
break;
}
case 'Prompt': {
/* eslint-disable-next-line no-console */
console.log(`${chalk.blue.inverse('INFO')} ${chalk.blue(message)}`);
break;
}
case 'Option': {
/* eslint-disable-next-line no-console */
console.log(`${chalk.magenta(message)}`);
break;
}
case 'Info': {
/* eslint-disable-next-line no-console */
console.log(`${chalk.white(message)}`);
break;
}
case 'Warning': {
/* eslint-disable-next-line no-console */
console.log(`${chalk.yellow.inverse('WARN')} ${chalk.yellow(message)}`);
break;
}
case 'Failure': {
/* eslint-disable-next-line no-console */
console.log(`${chalk.red.inverse('UHOH')} ${chalk.red(message)}`);
break;
}
case 'Success': {
/* eslint-disable-next-line no-console */
console.log(`${chalk.green.inverse(' OK ')} ${chalk.green(message)}`);
break;
}
}
}
/**
* Conditionally log to the console
* @param condition The condition to evaluate
* @param trueMessage The message to print if the condition is true
* @param falseMessage The message to print if the condition is false
* @param exitOnFalse Should the process exit if the condition is false
*/
export function clog(condition: boolean, trueMessage: string, falseMessage: string, exitOnFalse: boolean = true) {
if (condition) {
log('Success', trueMessage);
} else {
log('Failure', falseMessage);
if (exitOnFalse) {
process.exit(1);
}
}
}

View File

@ -4,26 +4,31 @@ import { PNG } from 'pngjs';
import prompt from 'prompt';
import { RGBA } from '../src/colour';
import { AppPaths } from '../src/util/path_util';
import { log, LogStyle } from './logging';
import { clog, log } from './logging';
export const ASSERT = (condition: boolean, onFailMessage: string) => {
if (!condition) {
log(LogStyle.Failure, onFailMessage);
log('Failure', onFailMessage);
process.exit(0);
}
};
export function isDirSetup(relativePath: string, jarAssetDir: string) {
const dir = path.join(AppPaths.Get.tools, relativePath);
if (fs.existsSync(dir)) {
if (fs.readdirSync(dir).length > 0) {
export const ASSERT_EXISTS = (path: fs.PathLike) => {
clog(
fs.existsSync(path),
`Found '${path}'`,
`Could not find '${path}'`,
);
};
export function isDirSetup(absolutePath: string) {
if (fs.existsSync(absolutePath)) {
if (fs.readdirSync(absolutePath).length > 0) {
return true;
}
} else {
fs.mkdirSync(dir);
fs.mkdirSync(absolutePath);
}
log(LogStyle.Warning, `Copy the contents of .minecraft/versions/<version>/<version>.jar/${jarAssetDir} from a Minecraft game files into ${relativePath} or fetch them automatically`);
return false;
}
@ -54,14 +59,32 @@ 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}`);
log('Prompt', `This script requires files inside of ${directory}`);
const { permission } = await prompt.get({
properties: {
permission: {
pattern: /^[YyNn]$/,
description: 'Do you give permission to access these files? (Y/n)',
description: 'Do you give permission to access these files? (y/n)',
message: 'Response must be Y or N',
required: true,
},