mirror of
https://github.com/LucasDower/ObjToSchematic.git
synced 2025-04-12 15:00:22 +08:00
Merge branch '0.7' into 0.7-constraint
This commit is contained in:
parent
610ef893db
commit
0f3e7d5a77
2
package-lock.json
generated
2
package-lock.json
generated
@ -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 |
@ -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
15
res/emissive_blocks.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
5
res/static/bulb.svg
Normal 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
BIN
res/static/debug.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
34
res/transparent_blocks.json
Normal file
34
res/transparent_blocks.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
83
src/atlas.ts
83
src/atlas.ts
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
103
src/colour.ts
103
src/colour.ts
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
47
src/dither.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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
470
src/lighting.ts
Normal 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)}%)`);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
94
src/mesh.ts
94
src/mesh.ts
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
116
src/renderer.ts
116
src/renderer.ts
@ -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);
|
||||
|
@ -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
110
src/ui/elements/checkbox.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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
208
src/ui/elements/material.ts
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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'],
|
||||
},
|
||||
|
||||
},
|
||||
|
124
src/ui/misc.ts
124
src/ui/misc.ts
@ -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>
|
||||
`});
|
||||
|
@ -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(':');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
156
styles.css
156
styles.css
@ -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);
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}();
|
||||
|
@ -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.');
|
||||
}
|
||||
}();
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user