Remove AppContext and UI coupling

This commit is contained in:
Lucas Dower 2022-03-03 20:34:45 +00:00
parent a99799d539
commit b66a489af2
20 changed files with 172 additions and 118 deletions

View File

@ -3,11 +3,12 @@ import { Litematic, Schematic } from './schematic';
import { Renderer } from './renderer';
import { Mesh } from './mesh';
import { ObjImporter } from './importers/obj_importer';
import { ASSERT, CustomError, CustomWarning, LOG, LOG_ERROR, LOG_WARN } from './util';
import { ASSERT, ColourSpace, CustomError, CustomWarning, LOG, LOG_ERROR, LOG_WARN } from './util';
import { remote } from 'electron';
import { VoxelMesh } from './voxel_mesh';
import { BlockMesh } from './block_mesh';
import { VoxelMesh, VoxelMeshParams } from './voxel_mesh';
import { BlockMesh, BlockMeshParams } from './block_mesh';
import { TextureFiltering } from './texture';
/* eslint-disable */
export enum ActionReturnType {
@ -45,18 +46,14 @@ export class AppContext {
private _loadedVoxelMesh?: VoxelMesh;
private _loadedBlockMesh?: BlockMesh;
private _warnings: string[];
private _ui: UI;
private _actionMap = new Map<Action, {
action: () => void;
onFailure?: () => void
}>();
private static _instance: AppContext;
public static get Get() {
return this._instance || (this._instance = new this());
}
private constructor() {
public constructor() {
const gl = (<HTMLCanvasElement>document.getElementById('canvas')).getContext('webgl');
if (!gl) {
throw Error('Could not load WebGL context');
@ -98,29 +95,29 @@ export class AppContext {
},
});
UI.Get.build();
UI.Get.registerEvents();
UI.Get.disable(Action.Simplify);
this._ui = new UI(this);
this._ui.build();
this._ui.registerEvents();
this._ui.disable(Action.Simplify);
}
public do(action: Action) {
UI.Get.disable(action + 1);
this._ui.disable(action + 1);
this._warnings = [];
const groupName = UI.Get.uiOrder[action];
const groupName = this._ui.uiOrder[action];
LOG(`Doing ${action}`);
UI.Get.cacheValues(action);
this._ui.cacheValues(action);
const delegate = this._actionMap.get(action)!;
try {
delegate.action();
} catch (err: any) {
LOG_ERROR(err);
if (err instanceof CustomError) {
UI.Get.layoutDull[groupName].output.setMessage(err.message, ActionReturnType.Failure);
this._ui.layoutDull[groupName].output.setMessage(err.message, ActionReturnType.Failure);
} else if (err instanceof CustomWarning) {
UI.Get.layoutDull[groupName].output.setMessage(err.message, ActionReturnType.Warning);
this._ui.layoutDull[groupName].output.setMessage(err.message, ActionReturnType.Warning);
} else {
UI.Get.layoutDull[groupName].output.setMessage(ReturnMessages.get(action)!.onFailure, ActionReturnType.Failure);
this._ui.layoutDull[groupName].output.setMessage(ReturnMessages.get(action)!.onFailure, ActionReturnType.Failure);
}
if (delegate.onFailure) {
delegate.onFailure();
@ -131,17 +128,20 @@ export class AppContext {
const successMessage = ReturnMessages.get(action)!.onSuccess;
if (this._warnings.length !== 0) {
const allWarnings = this._warnings.join('<br>');
UI.Get.layoutDull[groupName].output.setMessage(successMessage + `, with ${this._warnings.length} warning(s):` + '<br><b>' + allWarnings + '</b>', ActionReturnType.Warning);
this._ui.layoutDull[groupName].output.setMessage(successMessage + `, with ${this._warnings.length} warning(s):` + '<br><b>' + allWarnings + '</b>', ActionReturnType.Warning);
} else {
UI.Get.layoutDull[groupName].output.setMessage(successMessage, ActionReturnType.Success);
this._ui.layoutDull[groupName].output.setMessage(successMessage, ActionReturnType.Success);
}
LOG(`Finished ${action}`);
UI.Get.enable(action + 1);
this._ui.enable(action + 1);
}
private _import() {
this._loadedMesh = new ObjImporter().createMesh();
const uiElements = this._ui.layout.import.elements;
const filePath = uiElements.input.getCachedValue();
this._loadedMesh = new ObjImporter().createMesh(filePath);
Renderer.Get.useMesh(this._loadedMesh);
}
@ -151,22 +151,36 @@ export class AppContext {
private _voxelise() {
ASSERT(this._loadedMesh);
this._loadedVoxelMesh = new VoxelMesh();
this._loadedVoxelMesh.voxelise(this._loadedMesh);
const uiElements = this._ui.layout.build.elements;
const voxelMeshParams: VoxelMeshParams = {
desiredHeight: uiElements.height.getCachedValue() as number,
useMultisampleColouring: uiElements.multisampleColouring.getCachedValue() === 'on',
textureFiltering: uiElements.textureFiltering.getCachedValue() === 'linear' ? TextureFiltering.Linear : TextureFiltering.Nearest,
ambientOcclusionEnabled: uiElements.ambientOcclusion.getCachedValue() === 'on',
};
this._loadedVoxelMesh = VoxelMesh.createFromMesh(this._loadedMesh, voxelMeshParams);
Renderer.Get.useVoxelMesh(this._loadedVoxelMesh);
}
private _palette() {
ASSERT(this._loadedVoxelMesh);
this._loadedBlockMesh = new BlockMesh();
this._loadedBlockMesh.assignBlocks(this._loadedVoxelMesh);
const uiElements = this._ui.layout.palette.elements;
const blockMeshParams: BlockMeshParams = {
textureAtlas: uiElements.textureAtlas.getCachedValue(),
blockPalette: uiElements.blockPalette.getCachedValue(),
ditheringEnabled: uiElements.dithering.getCachedValue() === 'on',
colourSpace: uiElements.colourSpace.getCachedValue() === 'rgb' ? ColourSpace.RGB : ColourSpace.LAB,
};
this._loadedBlockMesh = BlockMesh.createFromVoxelMesh(this._loadedVoxelMesh, blockMeshParams);
Renderer.Get.useBlockMesh(this._loadedBlockMesh);
}
private _export() {
const exportFormat = UI.Get.layout.export.elements.export.getCachedValue() as string;
const exportFormat = this._ui.layout.export.elements.export.getCachedValue() as string;
const exporter = (exportFormat === 'schematic') ? new Schematic() : new Litematic();
const filePath = remote.dialog.showSaveDialogSync({

View File

@ -1,14 +1,14 @@
import { BlockAtlas, BlockInfo } from './block_atlas';
import { ASSERT, RGB } from './util';
import { ASSERT, ColourSpace, RGB } from './util';
import { Vector3 } from './vector';
interface BlockAssigner {
assignBlock(voxelColour: RGB, voxelPosition: Vector3): BlockInfo;
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace): BlockInfo;
}
export class BasicBlockAssigner implements BlockAssigner {
assignBlock(voxelColour: RGB, voxelPosition: Vector3): BlockInfo {
return BlockAtlas.Get.getBlock(voxelColour);
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace): BlockInfo {
return BlockAtlas.Get.getBlock(voxelColour, colourSpace);
}
}
@ -36,7 +36,7 @@ export class OrderedDitheringBlockAssigner implements BlockAssigner {
return (OrderedDitheringBlockAssigner._mapMatrix[index] / (size * size * size)) - 0.5;
}
assignBlock(voxelColour: RGB, voxelPosition: Vector3): BlockInfo {
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace): BlockInfo {
const size = OrderedDitheringBlockAssigner._size;
const map = this._getThresholdValue(
Math.abs(voxelPosition.x % size),
@ -50,6 +50,6 @@ export class OrderedDitheringBlockAssigner implements BlockAssigner {
((255 * voxelColour.b) + map * OrderedDitheringBlockAssigner._threshold) / 255,
);
return BlockAtlas.Get.getBlock(newVoxelColour);
return BlockAtlas.Get.getBlock(newVoxelColour, colourSpace);
}
}

View File

@ -1,5 +1,5 @@
import { HashMap } from './hash_map';
import { UV, RGB, ASSERT, fileExists } from './util';
import { UV, RGB, ASSERT, fileExists, ColourSpace } from './util';
import { Vector3 } from './vector';
import fs from 'fs';
@ -94,7 +94,7 @@ export class BlockAtlas {
this._paletteLoaded = true;
}
public getBlock(voxelColour: RGB): BlockInfo {
public getBlock(voxelColour: RGB, colourSpace: ColourSpace): BlockInfo {
ASSERT(this._atlasLoaded, 'No atlas has been loaded');
ASSERT(this._paletteLoaded, 'No palette has been loaded');
@ -110,7 +110,7 @@ export class BlockAtlas {
const block: BlockInfo = this._blocks[i];
if (this._palette.includes(block.name)) {
const blockAvgColour = block.colour as RGB;
const distance = RGB.distance(blockAvgColour, voxelColour);
const distance = RGB.distance(blockAvgColour, voxelColour, colourSpace);
if (distance < minDistance) {
minDistance = distance;

View File

@ -1,43 +1,52 @@
import { BasicBlockAssigner, OrderedDitheringBlockAssigner } from './block_assigner';
import { Voxel, VoxelMesh } from './voxel_mesh';
import { BlockAtlas, BlockInfo } from './block_atlas';
import { CustomError, LOG } from './util';
import { ColourSpace, CustomError, LOG } from './util';
import { Renderer } from './renderer';
import { UI } from './ui/layout';
interface Block {
voxel: Voxel;
blockInfo: BlockInfo;
}
export interface BlockMeshParams {
textureAtlas: string,
blockPalette: string,
ditheringEnabled: boolean,
colourSpace: ColourSpace,
}
export class BlockMesh {
private _blockPalette: string[];
private _blocks: Block[];
private _voxelMesh?: VoxelMesh;
private _voxelMesh: VoxelMesh;
public constructor() {
public static createFromVoxelMesh(voxelMesh: VoxelMesh, blockMeshParams: BlockMeshParams) {
const blockMesh = new BlockMesh(voxelMesh);
blockMesh._assignBlocks(blockMeshParams);
return blockMesh;
}
private constructor(voxelMesh: VoxelMesh) {
LOG('New block mesh');
this._blockPalette = [];
this._blocks = [];
this._voxelMesh = voxelMesh;
}
public assignBlocks(voxelMesh: VoxelMesh) {
private _assignBlocks(blockMeshParams: BlockMeshParams) {
LOG('Assigning blocks');
const textureAtlas = UI.Get.layout.palette.elements.textureAtlas.getCachedValue() as string;
BlockAtlas.Get.loadAtlas(textureAtlas);
BlockAtlas.Get.loadAtlas(blockMeshParams.textureAtlas);
BlockAtlas.Get.loadPalette(blockMeshParams.blockPalette);
const blockPalette = UI.Get.layout.palette.elements.blockPalette.getCachedValue() as string;
BlockAtlas.Get.loadPalette(blockPalette);
const ditheringEnabled = UI.Get.layout.palette.elements.dithering.getCachedValue() as string === 'on';
const blockAssigner = ditheringEnabled ? new OrderedDitheringBlockAssigner() : new BasicBlockAssigner();
const blockAssigner = blockMeshParams.ditheringEnabled ? new OrderedDitheringBlockAssigner() : new BasicBlockAssigner();
const voxels = voxelMesh.getVoxels();
const voxels = this._voxelMesh.getVoxels();
for (let voxelIndex = 0; voxelIndex < voxels.length; ++voxelIndex) {
const voxel = voxels[voxelIndex];
const block = blockAssigner.assignBlock(voxel.colour, voxel.position);
const block = blockAssigner.assignBlock(voxel.colour, voxel.position, blockMeshParams.colourSpace);
this._blocks.push({
voxel: voxel,
@ -47,8 +56,6 @@ export class BlockMesh {
this._blockPalette.push(block.name);
}
}
this._voxelMesh = voxelMesh;
}
public getBlocks(): Block[] {

View File

@ -24,7 +24,7 @@ addEvent('canvas', 'mousemove', (e) => {
// Begin draw loop
const context = AppContext.Get;
const context = new AppContext();
function render() {
context.draw();
requestAnimationFrame(render);

View File

@ -12,5 +12,5 @@ export namespace AppConfig {
export const DEBUG_ENABLED = true;
export const MULTISAMPLE_COUNT = 64;
export const MULTISAMPLE_COUNT = 4;
}

View File

@ -1,5 +1,6 @@
import { Mesh } from './mesh';
import { Warnable } from './util';
export abstract class IImporter {
export abstract class IImporter extends Warnable {
abstract createMesh(filePath: string): Mesh;
}

View File

@ -2,12 +2,10 @@ import { IImporter } from '../importer';
import { MaterialType, Mesh, SolidMaterial, TexturedMaterial, Tri } from '../mesh';
import { Vector3 } from '../vector';
import { UV, ASSERT, RGB, CustomError, LOG, REGEX_NUMBER, RegExpBuilder, REGEX_NZ_ANY, LOG_ERROR } from '../util';
import { UI } from '../ui/layout';
import { checkFractional, checkNaN } from '../math';
import fs from 'fs';
import path from 'path';
import { AppContext } from '../app_context';
export class ObjImporter extends IImporter {
private _vertices: Vector3[] = [];
@ -196,8 +194,7 @@ export class ObjImporter extends IImporter {
},
];
override createMesh(): Mesh {
const filePath = UI.Get.layout.import.elements.input.getCachedValue();
override createMesh(filePath: string): Mesh {
ASSERT(path.isAbsolute(filePath));
this._objPath = path.parse(filePath);
@ -205,7 +202,7 @@ export class ObjImporter extends IImporter {
this._parseOBJ(filePath);
if (this._mtlLibs.length === 0) {
AppContext.Get.addWarning('Could not find associated .mtl file');
this.addWarning('Could not find associated .mtl file');
}
for (let i = 0; i < this._mtlLibs.length; ++i) {
const mtlLib = this._mtlLibs[i];

View File

@ -1,8 +1,7 @@
import { Vector3 } from './vector';
import { UV, Bounds, LOG, ASSERT, CustomError, LOG_WARN } from './util';
import { UV, Bounds, LOG, ASSERT, CustomError, LOG_WARN, Warnable, getRandomID } from './util';
import { UVTriangle } from './triangle';
import { RGB } from './util';
import { AppContext } from './app_context';
import path from 'path';
import fs from 'fs';
@ -24,21 +23,24 @@ export interface SolidMaterial { colour: RGB; type: MaterialType.solid }
export interface TexturedMaterial { path: string; type: MaterialType.textured }
export type MaterialMap = {[key: string]: (SolidMaterial | TexturedMaterial)};
export class Mesh {
export class Mesh extends Warnable {
public vertices!: Vector3[];
public uvs!: UV[];
public tris!: Tri[];
public materials!: MaterialMap;
public readonly id: string;
public static desiredHeight = 8.0;
constructor(vertices: Vector3[], uvs: UV[], tris: Tri[], materials: MaterialMap) {
super();
LOG('New mesh');
this.vertices = vertices;
this.uvs = uvs;
this.tris = tris;
this.materials = materials;
this.id = getRandomID();
this._checkMesh();
this._checkMaterials();
@ -101,7 +103,7 @@ export class Mesh {
}
if (wasRemapped) {
LOG_WARN('Triangles use these materials but they were not found', missingMaterials);
AppContext.Get.addWarning('Some materials were not loaded correctly');
this.addWarning('Some materials were not loaded correctly');
this.materials[debugName] = {
type: MaterialType.solid,
colour: RGB.white,
@ -114,7 +116,7 @@ export class Mesh {
if (material.type === MaterialType.textured) {
ASSERT(path.isAbsolute(material.path), 'Material texture path not absolute');
if (!fs.existsSync(material.path)) {
AppContext.Get.addWarning(`Could not find ${material.path}`);
this.addWarning(`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,

View File

@ -5,7 +5,7 @@ import { RenderBuffer } from './buffer';
import { GeometryTemplates } from './geometry';
import { Mesh, SolidMaterial, TexturedMaterial, MaterialType } from './mesh';
import { BlockAtlas } from './block_atlas';
import { LOG, RGB } from './util';
import { ASSERT, LOG, RGB } from './util';
import { VoxelMesh } from './voxel_mesh';
import { BlockMesh } from './block_mesh';

View File

@ -4,10 +4,9 @@ import path from 'path';
import { NBT, TagType, writeUncompressed } from 'prismarine-nbt';
import { Vector3 } from './vector';
import { BlockMesh } from './block_mesh';
import { LOG } from './util';
import { AppContext } from './app_context';
import { LOG, Warnable } from './util';
export abstract class Exporter {
export abstract class Exporter extends Warnable {
protected _sizeVector!: Vector3;
public abstract convertToNBT(blockMesh: BlockMesh): NBT
@ -68,7 +67,7 @@ export class Schematic extends Exporter {
}
if (unsupportedBlocks.size > 0) {
AppContext.Get.addWarning(`${numBlocksUnsupported} blocks (${unsupportedBlocks.size} unique) are not supported by the .schematic format, Stone block are used in their place. Try using the schematic-friendly palette, or export using .litematica`);
this.addWarning(`${numBlocksUnsupported} blocks (${unsupportedBlocks.size} unique) are not supported by the .schematic format, Stone block are used in their place. Try using the schematic-friendly palette, or export using .litematica`);
LOG('Unsupported blocks', unsupportedBlocks);
}

View File

@ -1,10 +1,10 @@
import { ASSERT } from '../../util';
export abstract class BaseUIElement {
export abstract class BaseUIElement<Type> {
protected _id: string;
protected _label: string;
protected _isEnabled: boolean;
protected _value: any;
protected _value?: Type;
protected _cachedValue?: any;
constructor(label: string) {
@ -18,12 +18,13 @@ export abstract class BaseUIElement {
this._onEnabledChanged();
}
public getCachedValue() {
public getCachedValue(): Type {
ASSERT(this._cachedValue !== undefined, 'Attempting to access value before cached');
return this._cachedValue;
return this._cachedValue as Type;
}
protected getValue() {
ASSERT(this._value);
return this._value;
}

View File

@ -1,7 +1,7 @@
import { BaseUIElement } from './base';
import { ASSERT } from '../../util';
export class ButtonElement extends BaseUIElement {
export class ButtonElement extends BaseUIElement<any> {
private _onClick: () => void;
public constructor(label: string, onClick: () => void) {

View File

@ -6,7 +6,7 @@ export interface ComboBoxItem {
displayText: string;
}
export class ComboBoxElement extends LabelledElement {
export class ComboBoxElement extends LabelledElement<string> {
private _items: ComboBoxItem[];
public constructor(id: string, items: ComboBoxItem[]) {

View File

@ -4,7 +4,7 @@ import { ASSERT } from '../../util';
import { remote } from 'electron';
import * as path from 'path';
export class FileInputElement extends LabelledElement {
export class FileInputElement extends LabelledElement<string> {
private _fileExtension: string;
private _loadedFilePath: string;

View File

@ -1,7 +1,7 @@
import { BaseUIElement } from './base';
import { LabelElement } from './label';
export abstract class LabelledElement extends BaseUIElement {
export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
private _labelElement: LabelElement;
public constructor(label: string) {

View File

@ -2,7 +2,7 @@ import { ASSERT } from '../../util';
import { clamp } from '../../math';
import { LabelledElement } from './labelled_element';
export class SliderElement extends LabelledElement {
export class SliderElement extends LabelledElement<number> {
private _min: number;
private _max: number;
private _decimals: number;
@ -18,7 +18,7 @@ export class SliderElement extends LabelledElement {
}
public generateInnerHTML() {
const norm = (this._value - this._min) / (this._max - this._min);
const norm = (this.getValue() - this._min) / (this._max - this._min);
return `
<div style="display: flex; flex-direction: row;">
<div class="slider-value" id="${this._id + '-value'}">
@ -76,10 +76,6 @@ export class SliderElement extends LabelledElement {
elementValue.innerHTML = this._value.toFixed(this._decimals);
}
protected getValue() {
return this._value;
}
protected _onEnabledChanged() {
super._onEnabledChanged();

View File

@ -12,7 +12,7 @@ import path from 'path';
export interface Group {
label: string;
elements: { [key: string]: BaseUIElement };
elements: { [key: string]: BaseUIElement<any> };
elementsOrder: string[];
submitButton: ButtonElement;
output: OutputElement;
@ -28,7 +28,7 @@ export class UI {
},
elementsOrder: ['input'],
submitButton: new ButtonElement('Load mesh', () => {
AppContext.Get.do(Action.Import);
this._appContext.do(Action.Import);
}),
output: new OutputElement(),
},
@ -39,7 +39,7 @@ export class UI {
},
elementsOrder: ['ratio'],
submitButton: new ButtonElement('Simplify mesh', () => {
AppContext.Get.do(Action.Simplify);
this._appContext.do(Action.Simplify);
}),
output: new OutputElement(),
},
@ -62,7 +62,7 @@ export class UI {
},
elementsOrder: ['height', 'ambientOcclusion', 'multisampleColouring', 'textureFiltering'],
submitButton: new ButtonElement('Voxelise mesh', () => {
AppContext.Get.do(Action.Voxelise);
this._appContext.do(Action.Voxelise);
}),
output: new OutputElement(),
},
@ -82,7 +82,7 @@ export class UI {
},
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'colourSpace'],
submitButton: new ButtonElement('Assign blocks', () => {
AppContext.Get.do(Action.Palette);
this._appContext.do(Action.Palette);
}),
output: new OutputElement(),
},
@ -96,20 +96,16 @@ export class UI {
},
elementsOrder: ['export'],
submitButton: new ButtonElement('Export structure', () => {
AppContext.Get.do(Action.Export);
this._appContext.do(Action.Export);
}),
output: new OutputElement(),
},
};
private _uiDull: { [key: string]: Group } = this._ui;
private _appContext: AppContext;
private static _instance: UI;
public static get Get() {
return this._instance || (this._instance = new this());
}
constructor() {
constructor(appContext: AppContext) {
this._appContext = appContext;
}
public build() {
@ -174,7 +170,7 @@ export class UI {
`;
}
private _buildSubcomponent(element: BaseUIElement) {
private _buildSubcomponent(element: BaseUIElement<any>) {
return `
<div class="item item-body">
${element.generateHTML()}

View File

@ -4,7 +4,6 @@ import { Vector3 } from './vector';
const convert = require('color-convert');
import fs from 'fs';
import { UI } from './ui/layout';
export class UV {
public u: number;
@ -15,6 +14,14 @@ export class UV {
this.v = v;
}
}
/* eslint-disable */
export enum ColourSpace {
RGB,
LAB
}
/* eslint-enable */
export class RGB {
public r: number;
public g: number;
@ -49,15 +56,15 @@ export class RGB {
return [this.r, this.g, this.b];
}
public static distance(a: RGB, b: RGB): number {
const useLAB = UI.Get.layout.palette.elements.colourSpace.getCachedValue() === 'lab';
if (useLAB) {
public static distance(a: RGB, b: RGB, colourSpace: ColourSpace): number {
if (colourSpace === ColourSpace.LAB) {
const aLAB = convert.rgb.lab(a.r * 255, a.g * 255, a.b * 255);
const bLAB = convert.rgb.lab(b.r * 255, b.g * 255, b.b * 255);
const _a = Vector3.fromArray(aLAB);
const _b = Vector3.fromArray(bLAB);
return _a.sub(_b).magnitude();
} else {
ASSERT(colourSpace === ColourSpace.RGB);
const _a = a.toVector3();
const _b = b.toVector3();
return _a.sub(_b).magnitude();
@ -225,3 +232,23 @@ export class RegExpBuilder {
return new RegExp(this._components.join(''));
}
}
export class Warnable {
private _warnings: string[];
constructor() {
this._warnings = [];
}
public addWarning(warning: string) {
this._warnings.push(warning);
}
public getWarnings() {
return this._warnings;
}
}
export function getRandomID(): string {
return (Math.random() + 1).toString(36).substring(7);
}

View File

@ -7,7 +7,6 @@ import { OcclusionManager } from './occlusion';
import { Axes, generateRays, rayIntersectTriangle } from './ray';
import { Texture, TextureFiltering } from './texture';
import { Triangle, UVTriangle } from './triangle';
import { UI } from './ui/layout';
import { Bounds, LOG, RGB, UV } from './util';
import { Vector3 } from './vector';
@ -15,20 +14,38 @@ export interface Voxel {
position: Vector3;
colour: RGB;
collisions: number;
}
export interface VoxelMeshParams {
desiredHeight: number,
useMultisampleColouring: boolean,
textureFiltering: TextureFiltering,
ambientOcclusionEnabled: boolean,
}
export class VoxelMesh {
private _mesh: Mesh;
private _voxelMeshParams: VoxelMeshParams;
private _voxelSize: number;
private _voxels: Voxel[];
private _voxelsHash: HashMap<Vector3, number>;
private _loadedTextures: { [materialName: string]: Texture };
private _bounds: Bounds;
public constructor() {
LOG('New voxel mesh');
public static createFromMesh(mesh: Mesh, voxelMeshParams: VoxelMeshParams) {
const voxelMesh = new VoxelMesh(mesh, voxelMeshParams);
voxelMesh._voxelise();
return voxelMesh;
}
const desiredHeight = UI.Get.layout.build.elements.height.getCachedValue() as number;
this._voxelSize = 8.0 / Math.round(desiredHeight);
private constructor(mesh: Mesh, voxelMeshParams: VoxelMeshParams) {
LOG('New voxel mesh');
this._mesh = mesh;
this._voxelMeshParams = voxelMeshParams;
this._voxelSize = 8.0 / Math.round(voxelMeshParams.desiredHeight);
this._voxels = [];
this._voxelsHash = new HashMap(2048);
this._loadedTextures = {};
@ -43,17 +60,17 @@ export class VoxelMesh {
return this._voxelsHash.has(pos);
}
public voxelise(mesh: Mesh) {
private _voxelise() {
LOG('Voxelising');
mesh.tris.forEach((tri, index) => {
const material = mesh.materials[tri.material];
this._mesh.tris.forEach((tri, index) => {
const material = this._mesh.materials[tri.material];
if (material.type == MaterialType.textured) {
if (!(tri.material in this._loadedTextures)) {
this._loadedTextures[tri.material] = new Texture(material.path);
}
}
const uvTriangle = mesh.getUVTriangle(index);
const uvTriangle = this._mesh.getUVTriangle(index);
this._voxeliseTri(uvTriangle, material, tri.material);
});
}
@ -81,8 +98,7 @@ export class VoxelMesh {
}
let voxelColour: RGB;
const useMultisampleColouring = UI.Get.layout.build.elements.multisampleColouring.getCachedValue() as string === 'on';
if (useMultisampleColouring && material.type === MaterialType.textured) {
if (this._voxelMeshParams.useMultisampleColouring && material.type === MaterialType.textured) {
const samples: RGB[] = [];
for (let i = 0; i < AppConfig.MULTISAMPLE_COUNT; ++i) {
const samplePosition = Vector3.mulScalar(Vector3.add(voxelPosition, Vector3.random().addScalar(-0.5)), this._voxelSize);
@ -116,8 +132,7 @@ export class VoxelMesh {
triangle.uv0.v * w0 + triangle.uv1.v * w1 + triangle.uv2.v * w2,
);
const filtering = UI.Get.layout.build.elements.textureFiltering.getCachedValue() as string === 'linear' ? TextureFiltering.Linear : TextureFiltering.Nearest;
return this._loadedTextures[materialName].getRGB(uv, filtering);
return this._loadedTextures[materialName].getRGB(uv, this._voxelMeshParams.textureFiltering);
}
private _addVoxel(pos: Vector3, colour: RGB) {
@ -160,12 +175,11 @@ export class VoxelMesh {
{ name: 'normal', numComponents: 3 },
]);
const ambientOcclusionEnabled = UI.Get.layout.build.elements.ambientOcclusion.getCachedValue() as string === 'on';
for (const voxel of this._voxels) {
// Each vertex of a face needs the occlusion data for the other 3 vertices
// in it's face, not just itself. Also flatten occlusion data.
let occlusions: number[];
if (ambientOcclusionEnabled) {
if (this._voxelMeshParams.ambientOcclusionEnabled) {
occlusions = OcclusionManager.Get.getOcclusions(voxel.position, this);
} else {
occlusions = OcclusionManager.Get.getBlankOcclusions();