Initial commit for VoxelMesh rewrite

This commit is contained in:
Lucas Dower 2023-09-08 19:05:29 +01:00
parent 0ea18700c5
commit d4f2934dcf
No known key found for this signature in database
GPG Key ID: B3EE6B8499593605
8 changed files with 303 additions and 13 deletions

View File

@ -22,8 +22,7 @@ export class Bounds {
this._max = Vector3.max(this._max, volume._max);
}
// TODO: rename to `createInfinitesimalBounds`
public static getInfiniteBounds() {
public static getEmptyBounds() {
return new Bounds(
new Vector3(Infinity, Infinity, Infinity),
new Vector3(-Infinity, -Infinity, -Infinity),
@ -48,4 +47,11 @@ export class Bounds {
public getDimensions() {
return Vector3.sub(this._max, this._min);
}
public copy() {
return new Bounds(
this._min.copy(),
this._max.copy(),
);
}
}

View File

@ -126,7 +126,7 @@ export class Mesh {
}
public getBounds() {
const bounds = Bounds.getInfiniteBounds();
const bounds = Bounds.getEmptyBounds();
if (this._transform) {
for (const vertex of this._vertices) {
bounds.extendByPoint(this._transform(vertex));

View File

@ -75,8 +75,9 @@ export class OcclusionManager {
return;
}
public static getNeighbourIndex(neighbour: Vector3) {
return 9 * (neighbour.x + 1) + 3 * (neighbour.y + 1) + (neighbour.z + 1);
public static getNeighbourIndex(x: number, y: number, z: number) {
return 9 * (x + 1) + 3 * (y + 1) + (z + 1);
}
private _setupOcclusions() {
@ -139,7 +140,8 @@ export class OcclusionManager {
for (let j = 0; j < 4; ++j) {
for (let k = 0; k < 3; ++k) {
const index = this._getOcclusionMapIndex(i, j, k);
this._occlusionNeighboursIndices[index] = OcclusionManager.getNeighbourIndex(occlusionNeighbours[i][j][k]);
const neighbour = occlusionNeighbours[i][j][k];
this._occlusionNeighboursIndices[index] = OcclusionManager.getNeighbourIndex(neighbour.x, neighbour.y, neighbour.z);
}
}
}

View File

@ -0,0 +1,278 @@
import { EFaceVisibility } from './block_assigner';
import { Bounds } from "./bounds";
import { RGBA, RGBAUtil } from './colour';
import { OcclusionManager } from "./occlusion";
import { Vector3 } from "./vector"
type OtS_Voxel = {
position: Vector3,
colour: RGBA,
}
export type OtS_Offset = -1 | 0 | 1;
type Ots_Voxel_Internal = OtS_Voxel & {
collisions: number,
}
export class OtS_VoxelMesh {
private _voxels: Map<number, Ots_Voxel_Internal>;
private _bounds: Bounds;
public constructor() {
this._voxels = new Map();
this._bounds = Bounds.getEmptyBounds();
}
public addVoxel(x: number, y: number, z: number, colour: RGBA, replaceMode: 'replace' | 'keep' | 'average') {
const key = Vector3.Hash(x, y, z);
let voxel: (Ots_Voxel_Internal | undefined) = this._voxels.get(key);
if (voxel === undefined) {
const position = new Vector3(x, y, z);
voxel = {
position: position,
colour: RGBAUtil.copy(colour),
collisions: 1,
}
this._bounds.extendByPoint(position);
} else {
if (replaceMode === 'average') {
voxel.colour.r = ((voxel.colour.r * voxel.collisions) + colour.r) / (voxel.collisions + 1);
voxel.colour.g = ((voxel.colour.g * voxel.collisions) + colour.g) / (voxel.collisions + 1);
voxel.colour.b = ((voxel.colour.b * voxel.collisions) + colour.b) / (voxel.collisions + 1);
voxel.colour.a = ((voxel.colour.a * voxel.collisions) + colour.a) / (voxel.collisions + 1);
++voxel.collisions;
} else if (replaceMode === 'replace') {
voxel.colour = RGBAUtil.copy(colour);
voxel.collisions = 1;
}
}
}
/**
* Remove a voxel from a given location.
*/
public removeVoxel(x: number, y: number, z: number): boolean {
const key = Vector3.Hash(x, y, z);
return this._voxels.delete(key);
}
/**
* Returns the colour of a voxel at a location, if one exists.
* @note Modifying the returned colour will not update the voxel's colour.
* For that, use `addVoxel` with the replaceMode set to 'replace'
*/
public getVoxelAt(x: number, y: number, z: number): (OtS_Voxel | null) {
const key = Vector3.Hash(x, y, z);
const voxel = this._voxels.get(key);
if (voxel === undefined) {
return null;
}
return {
position: voxel.position.copy(),
colour: RGBAUtil.copy(voxel.colour),
}
}
/**
* Get whether or not there is a voxel at a given location.
*/
public isVoxelAt(x: number, y: number, z: number) {
const key = Vector3.Hash(x, y, z);
return this._voxels.has(key);
}
/**
* Get whether or not there is a opaque voxel at a given location.
*/
public isOpaqueVoxelAt(x: number, y: number, z: number) {
const voxel = this.getVoxelAt(x, y, z);
return voxel === null ? false : voxel.colour.a === 1.0;
}
/**
* Get the bounds/dimensions of the VoxelMesh.
*/
public getBounds(): Bounds {
return this._bounds.copy();
}
/**
* Get the number of voxels in the VoxelMesh.
*/
public getVoxelCount(): number {
return this._voxels.size;
}
/**
* Iterate over the voxels in this VoxelMesh, note that these are copies
* and editing each entry will not modify the underlying voxel.
*/
public getVoxels(): IterableIterator<OtS_Voxel> {
const voxelsCopy: OtS_Voxel[] = Array.from(this._voxels.values()).map((voxel) => {
return {
position: voxel.position.copy(),
colour: RGBAUtil.copy(voxel.colour),
}
});
let currentIndex = 0;
return {
[Symbol.iterator]: function () {
return this;
},
next: () => {
if (currentIndex < voxelsCopy.length) {
const voxel = voxelsCopy[currentIndex++];
return { done: false, value: voxel };
} else {
return { done: true, value: undefined };
}
},
};
}
}
/**
* A util class to cache the voxel neighbours of a VoxelMesh
*/
export class OtS_VoxelMesh_Neighbourhood {
private _voxelNeighbours: Map<number, number>;
public constructor() {
this._voxelNeighbours = new Map();
}
/**
* Runs the neighbourhood calculations, i.e. will go through voxel-by-voxel,
* and cache what neighbours exist for each voxel. *Which* neighbours the
* algorithm checks is determined by the 'mode':
* - cardinal: Only checks 6 neighbours (the up/down/north/south/east/west directions)
* - full: Checks all 26 three-dimensional neighbours
*
* @note `process` takes a snapshot of the current state of voxelMesh and
* will not update if more voxels are added to voxelMesh or the state of
* voxelMesh changes in any other way.
*/
public process(voxelMesh: OtS_VoxelMesh, mode: 'cardinal' | 'full') {
this._voxelNeighbours.clear();
const neighboursToCheck = mode === 'cardinal'
? OtS_VoxelMesh_Neighbourhood._NEIGHBOURS_CARDINAL
: OtS_VoxelMesh_Neighbourhood._NEIGHBOURS_NON_CARDINAL.concat(OtS_VoxelMesh_Neighbourhood._NEIGHBOURS_CARDINAL);
const pos = new Vector3(0, 0, 0);
for (const voxel of voxelMesh.getVoxels()) {
let neighbourValue = 0;
neighboursToCheck.forEach((neighbour) => {
pos.setFrom(voxel.position);
pos.add(neighbour.offset);
if (voxelMesh.isOpaqueVoxelAt(pos.x, pos.y, pos.z)) {
neighbourValue |= (1 << neighbour.index);
}
})
this._voxelNeighbours.set(voxel.position.hash(), neighbourValue);
}
}
/**
* Returns an encoded value representing the neighbours of the voxel at this
* position. This is a confusing value to decode so instead use `hasNeighbour`
* for checking if
*/
public getNeighbours(x: number, y: number, z: number): number {
const key = Vector3.Hash(x, y, z);
const value = this._voxelNeighbours.get(key);
return value === undefined ? 0 : value;
}
/*
* Returns true if a voxel at position has a neighbour with offset 'offset'
*/
public hasNeighbour(x: number, y: number, z: number, offsetX: OtS_Offset, offsetY: number, offsetZ: OtS_Offset): boolean {
return (this.getNeighbours(x, y, z) & (1 << OcclusionManager.getNeighbourIndex(offsetX, offsetY, offsetZ))) > 0;
}
private static readonly _NEIGHBOURS_NON_CARDINAL = [
new Vector3(1, 1, -1),
new Vector3(0, 1, -1),
new Vector3(-1, 1, -1),
new Vector3(1, 0, -1),
new Vector3(-1, 0, -1),
new Vector3(1, -1, -1),
new Vector3(0, -1, -1),
new Vector3(-1, -1, -1),
new Vector3(1, 1, 0),
new Vector3(-1, 1, 0),
new Vector3(1, -1, 0),
new Vector3(-1, -1, 0),
new Vector3(1, 1, 1),
new Vector3(0, 1, 1),
new Vector3(-1, 1, 1),
new Vector3(1, 0, 1),
new Vector3(-1, 0, 1),
new Vector3(1, -1, 1),
new Vector3(0, -1, 1),
new Vector3(-1, -1, 1),
].map((neighbourOffset) => {
const inverseOffset = neighbourOffset.copy().negate();
return {
offset: neighbourOffset,
index: OcclusionManager.getNeighbourIndex(neighbourOffset.x, neighbourOffset.y, neighbourOffset.z),
inverseIndex: OcclusionManager.getNeighbourIndex(inverseOffset.x, inverseOffset.y, inverseOffset.z),
};
});
private static readonly _NEIGHBOURS_CARDINAL = [
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),
].map((neighbourOffset) => {
const inverseOffset = neighbourOffset.copy().negate();
return {
offset: neighbourOffset,
index: OcclusionManager.getNeighbourIndex(neighbourOffset.x, neighbourOffset.y, neighbourOffset.z),
inverseIndex: OcclusionManager.getNeighbourIndex(inverseOffset.x, inverseOffset.y, inverseOffset.z),
};
});
/**
* Returns whether or not you can see each face on a voxel at a given location
*/
public getFaceVisibility(x: number, y: number, z: number): EFaceVisibility {
let visibility: EFaceVisibility = EFaceVisibility.None;
if (!this.hasNeighbour(x, y, z, 1, 0, 0)) {
visibility += EFaceVisibility.North;
}
if (!this.hasNeighbour(x, y, z, -1, 0, 0)) {
visibility += EFaceVisibility.South;
}
if (!this.hasNeighbour(x, y, z, 0, 1, 0)) {
visibility += EFaceVisibility.Up;
}
if (!this.hasNeighbour(x, y, z, 0, -1, 0)) {
visibility += EFaceVisibility.Down;
}
if (!this.hasNeighbour(x, y, z, 0, 0, 1)) {
visibility += EFaceVisibility.East;
}
if (!this.hasNeighbour(x, y, z, 0, 0, -1)) {
visibility += EFaceVisibility.West;
}
return visibility;
}
}

View File

@ -238,7 +238,11 @@ export class Vector3 implements IHashable {
// Begin IHashable interface
public hash(): Vector3Hash {
return ((this.x + 10_000_000) << 42) + ((this.y + 10_000_000) << 21) + (this.z + 10_000_000) as Vector3Hash;
return Vector3.Hash(this.x, this.y, this.z) as Vector3Hash;
}
public static Hash(x: number, y: number, z: number) {
return ((x + 10_000_000) << 42) + ((y + 10_000_000) << 21) + (z + 10_000_000);
}
public equals(other: Vector3) {

View File

@ -22,7 +22,7 @@ export class VoxelMesh {
public constructor(overlapRule: TVoxelOverlapRule, ambientOcclusion: boolean) {
this._voxels = new Map();
this._bounds = Bounds.getInfiniteBounds();
this._bounds = Bounds.getEmptyBounds();
this._overlapRule = overlapRule;
this._ambientOcclusion = ambientOcclusion;
}
@ -135,8 +135,8 @@ export class VoxelMesh {
return {
offset: neighbourOffset,
index: OcclusionManager.getNeighbourIndex(neighbourOffset),
inverseIndex: OcclusionManager.getNeighbourIndex(inverseOffset),
index: OcclusionManager.getNeighbourIndex(neighbourOffset.x, neighbourOffset.y, neighbourOffset.z),
inverseIndex: OcclusionManager.getNeighbourIndex(inverseOffset.x, inverseOffset.y, inverseOffset.z),
};
});
@ -176,6 +176,6 @@ export class VoxelMesh {
* Offset must be a vector that exists within this._neighbours defined above
*/
public hasNeighbour(pos: Vector3, offset: Vector3): boolean {
return (this.getNeighbours(pos) & (1 << OcclusionManager.getNeighbourIndex(offset))) > 0;
return (this.getNeighbours(pos) & (1 << OcclusionManager.getNeighbourIndex(offset.x, offset.y, offset.z))) > 0;
}
}

View File

@ -17,7 +17,6 @@ export const headlessConfig: THeadlessConfig = {
enableAmbientOcclusion: false, // Only want true if exporting to .obj
},
assign: {
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
blockPalette: PALETTE_ALL_RELEASE, // Must be a palette name that exists in /resources/palettes
dithering: 'ordered',
ditheringMagnitude: 32,
@ -28,6 +27,7 @@ export const headlessConfig: THeadlessConfig = {
lightThreshold: 0,
contextualAveraging: true,
errorWeight: 0.0,
atlasJSON: undefined,
},
export: {
exporter: 'litematic', // 'schematic' / 'litematic',

View File

@ -22,7 +22,7 @@
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */