Optimisations to ray-based voxeliser and occlusion manager (~1.55x), added linear allocator

This commit is contained in:
Lucas Dower 2023-04-16 01:12:31 +01:00
parent f2579a9732
commit 33abd2d968
No known key found for this signature in database
GPG Key ID: B3EE6B8499593605
8 changed files with 195 additions and 118 deletions

View File

@ -165,7 +165,7 @@ export class BlockMesh {
const examinedBlock = this._blocks[examined.id];
const topBlockPosition = Vector3.add(examinedBlock.voxel.position, new Vector3(0, 1, 0));
const topBlockIndex = this._voxelMesh.getVoxelIndex(topBlockPosition);
const topBlockIndex = 0; //this._voxelMesh.getVoxelIndex(topBlockPosition);
if (topBlockIndex !== undefined && !AppRuntimeConstants.Get.TRANSPARENT_BLOCKS.includes(this._blocks[topBlockIndex].blockInfo.name)) {
const block = atlasPalette.getBlock(examined.voxelColour, nonGrassLikeBlockCollection, examined.faceVisibility, examined.errWeight);
@ -211,7 +211,7 @@ export class BlockMesh {
});
if (bestBlock !== undefined) {
const blockIndex = this._voxelMesh.getVoxelIndex(pos);
const blockIndex = 0; //this._voxelMesh.getVoxelIndex(pos);
ASSERT(blockIndex !== undefined, 'Setting emissive block of block that doesn\'t exist');
this._blocks[blockIndex].blockInfo = bestBlock;
@ -222,7 +222,7 @@ export class BlockMesh {
}
public getBlockAt(pos: Vector3): TOptional<Block> {
const index = this._voxelMesh.getVoxelIndex(pos);
const index = 0; //this._voxelMesh.getVoxelIndex(pos);
if (index !== undefined) {
return this._blocks[index];
}

48
src/linear_allocator.ts Normal file
View File

@ -0,0 +1,48 @@
export class LinearAllocator<T> {
private _items: Array<T>;
private _nextIndex: number;
private _max: number;
private _itemConstructor: () => T;
public constructor(getNewItem: () => T) {
this._items = new Array<T>();
this._nextIndex = 0;
this._max = 0;
this._itemConstructor = getNewItem;
}
private _add(item: T) {
this._items[this._nextIndex] = item;
++this._nextIndex;
this._max = Math.max(this._max, this._nextIndex);
}
public reset() {
this._nextIndex = 0;
}
public get(index: number): T | undefined {
return this._items[index];
}
public size() {
return this._nextIndex;
}
public place(): T {
if (this._nextIndex >= this._max) {
//console.log('Adding new item at index', this._nextIndex);
const newItem = this._itemConstructor();
this._add(newItem);
return newItem;
} else {
++this._nextIndex;
//console.log('Returning item at index', this._nextIndex - 1);
return this._items[this._nextIndex - 1];
}
}
public max() {
return this._max;
}
}

View File

@ -27,14 +27,14 @@ export class OcclusionManager {
public getOcclusions(centre: Vector3, voxelMesh: VoxelMesh) {
// Cache local neighbours
const neighbourData = voxelMesh.getNeighbourhoodMap().get(centre.hash());
const neighbourData = voxelMesh.getNeighbours(centre);
if (neighbourData === undefined) {
// This voxel has no neighbours within a 1-block radius
return this.getBlankOcclusions();
}
for (let i = 0; i < 27; ++i) {
this._localNeighbourhoodCache[i] = (neighbourData.value & (1 << i)) > 0 ? 1 : 0;
this._localNeighbourhoodCache[i] = (neighbourData & (1 << i)) > 0 ? 1 : 0;
}
// For each face

View File

@ -10,9 +10,10 @@ import { Vector3 } from './vector';
import { RenderNextVoxelMeshChunkParams, VoxeliseParams } from './worker_types';
export interface Voxel {
position: Vector3;
colour: RGBA;
collisions: number;
position: Vector3,
colour: RGBA,
collisions: number,
neighbours: number,
}
export type TVoxelOverlapRule = 'first' | 'average';
@ -20,30 +21,26 @@ export type TVoxelOverlapRule = 'first' | 'average';
export type TVoxelMeshParams = Pick<VoxeliseParams.Input, 'voxelOverlapRule' | 'enableAmbientOcclusion'>;
export class VoxelMesh {
private _voxels: (Voxel & { collisions: number })[];
private _voxelsHash: Map<number, number>;
private _voxels: Map<number, Voxel>;
private _bounds: Bounds;
private _neighbourMap: Map<number, { value: number }>;
private _voxelMeshParams: TVoxelMeshParams;
public constructor(voxelMeshParams: TVoxelMeshParams) {
this._voxels = [];
this._voxelsHash = new Map();
this._neighbourMap = new Map();
this._voxels = new Map();
this._bounds = Bounds.getInfiniteBounds();
this._voxelMeshParams = voxelMeshParams;
this._recreateBuffer = true;
}
public getVoxels() {
return this._voxels;
public getVoxels(): Voxel[] {
return Array.from(this._voxels.values());
}
public isVoxelAt(pos: Vector3) {
return this._voxelsHash.has(pos.hash());
public isVoxelAt(pos: Vector3): boolean {
return this._voxels.has(pos.hash());
}
public isOpaqueVoxelAt(pos: Vector3) {
public isOpaqueVoxelAt(pos: Vector3): boolean {
const voxel = this.getVoxelAt(pos);
if (voxel) {
return voxel.colour.a == 1.0;
@ -52,12 +49,7 @@ export class VoxelMesh {
}
public getVoxelAt(pos: Vector3): TOptional<Voxel> {
const voxelIndex = this._voxelsHash.get(pos.hash());
if (voxelIndex !== undefined) {
const voxel = this._voxels[voxelIndex];
ASSERT(voxel !== undefined);
return voxel;
}
return this._voxels.get(pos.hash());
}
public static getFullFaceVisibility(): EFaceVisibility {
@ -93,27 +85,22 @@ export class VoxelMesh {
}
const pos = inPos.copy().round();
const voxel = this._voxels.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];
if (voxel !== undefined) {
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 {
// This is a new voxel
this._voxels.push({
this._voxels.set(pos.hash(), {
position: pos,
colour: colour,
collisions: 1,
neighbours: 0,
});
this._voxelsHash.set(hash, this._voxels.length - 1);
this._bounds.extendByPoint(pos);
this._updateNeighbours(pos);
}
}
@ -121,15 +108,11 @@ export class VoxelMesh {
return this._bounds;
}
public getVoxelIndex(pos: Vector3) {
return this._voxelsHash.get(pos.hash());
public getVoxelCount(): number {
return this._voxels.size;
}
public getVoxelCount() {
return this._voxels.length;
}
private _neighbours = [
private static _Neighbours = [
new Vector3(1, 1, -1),
new Vector3(0, 1, -1),
new Vector3(-1, 1, -1),
@ -150,37 +133,45 @@ export class VoxelMesh {
new Vector3(1, -1, 1),
new Vector3(0, -1, 1),
new Vector3(-1, -1, 1),
];
].map((neighbourOffset) => {
const inverseOffset = neighbourOffset.copy().negate();
private _updateNeighbours(pos: Vector3) {
if (this._voxelMeshParams.enableAmbientOcclusion) {
for (const neighbourOffset of this._neighbours) {
const neighbour = Vector3.add(pos, neighbourOffset);
const inverseOffset = neighbourOffset.copy().negate();
const inverseIndex = OcclusionManager.getNeighbourIndex(inverseOffset);
// ASSERT(inverseIndex >= 0 && inverseIndex < 27);
const neighbourData = this.getNeighbours(neighbour);
neighbourData.value |= (1 << inverseIndex);
// ASSERT((this.getNeighbours(neighbour).value & (1 << inverseIndex)) !== 0);
}
return {
offset: neighbourOffset,
index: OcclusionManager.getNeighbourIndex(neighbourOffset),
inverseIndex: OcclusionManager.getNeighbourIndex(inverseOffset),
};
});
/**
* Goes through each voxel and calculates what voxel neighbours it has.
* This is used for ambient occlusion.
* @note This does NOT check all 27 neighbours, i.e. it does not check voxels
* directly up, down, north, south, east, west as they're not needed.
*/
public calculateNeighbours() {
if (!this._voxelMeshParams.enableAmbientOcclusion) {
return;
}
const pos = new Vector3(0, 0, 0);
this._voxels.forEach((voxel) => {
voxel.neighbours = 0;
VoxelMesh._Neighbours.forEach((neighbour) => {
pos.setFrom(voxel.position);
pos.add(neighbour.offset);
if (this.isVoxelAt(pos)) {
voxel.neighbours |= (1 << neighbour.index);
}
});
});
}
public getNeighbours(pos: Vector3) {
ASSERT(this._voxelMeshParams.enableAmbientOcclusion, 'Ambient occlusion is disabled');
const hash = pos.hash();
const neighbours = this._neighbourMap.get(hash);
if (neighbours === undefined) {
this._neighbourMap.set(hash, { value: 0 });
return this._neighbourMap.get(hash)!;
} else {
return neighbours;
}
}
public getNeighbourhoodMap() {
return this._neighbourMap;
return this._voxels.get(pos.hash())?.neighbours ?? 0;
}
/*
@ -188,7 +179,7 @@ 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).value & (1 << OcclusionManager.getNeighbourIndex(offset))) > 0;
return (this.getNeighbours(pos) & (1 << OcclusionManager.getNeighbourIndex(offset))) > 0;
}
private _renderParams?: RenderNextVoxelMeshChunkParams.Input;

View File

@ -1,4 +1,5 @@
import { Bounds } from '../bounds';
import { LinearAllocator } from '../linear_allocator';
import { Mesh } from '../mesh';
import { ProgressManager } from '../progress';
import { Axes, Ray, rayIntersectTriangle } from '../ray';
@ -61,26 +62,40 @@ export class RayVoxeliser extends IVoxeliser {
return this._voxelMesh;
}
private _rayList = new LinearAllocator<Ray>(() => {
const ray: Ray = { origin: new Vector3(0, 0, 0), axis: Axes.x };
return ray;
});
private _voxeliseTri(triangle: UVTriangle, materialName: string) {
const rayList = this._generateRays(triangle.v0, triangle.v1, triangle.v2);
this._rayList.reset();
this._generateRays(triangle.v0, triangle.v1, triangle.v2);
ASSERT(this._mesh !== undefined);
ASSERT(this._voxeliseParams !== undefined);
ASSERT(this._voxelMesh !== undefined);
for (const ray of rayList) {
const voxelPosition = new Vector3(0, 0, 0);
const size = this._rayList.size();
for (let i = 0; i < size; ++i) {
const ray = this._rayList.get(i)!;
const intersection = rayIntersectTriangle(ray, triangle.v0, triangle.v1, triangle.v2);
if (intersection) {
let voxelPosition: Vector3;
switch (ray.axis) {
case Axes.x:
voxelPosition = new Vector3(Math.round(intersection.x), intersection.y, intersection.z);
voxelPosition.x = Math.round(intersection.x);
voxelPosition.y = intersection.y;
voxelPosition.z = intersection.z;
break;
case Axes.y:
voxelPosition = new Vector3(intersection.x, Math.round(intersection.y), intersection.z);
voxelPosition.x = intersection.x;
voxelPosition.y = Math.round(intersection.y);
voxelPosition.z = intersection.z;
break;
case Axes.z:
voxelPosition = new Vector3(intersection.x, intersection.y, Math.round(intersection.z));
voxelPosition.x = intersection.x;
voxelPosition.y = intersection.y;
voxelPosition.z = Math.round(intersection.z);
break;
}
@ -97,56 +112,55 @@ export class RayVoxeliser extends IVoxeliser {
};
}
private _generateRays(v0: Vector3, v1: Vector3, v2: Vector3): Array<Ray> {
const bounds: Bounds = new Bounds(
new Vector3(
Math.floor(Math.min(v0.x, v1.x, v2.x)),
Math.floor(Math.min(v0.y, v1.y, v2.y)),
Math.floor(Math.min(v0.z, v1.z, v2.z)),
),
new Vector3(
Math.ceil(Math.max(v0.x, v1.x, v2.x)),
Math.ceil(Math.max(v0.y, v1.y, v2.y)),
Math.ceil(Math.max(v0.z, v1.z, v2.z)),
),
);
private _tmpBounds: Bounds = new Bounds(new Vector3(0, 0, 0), new Vector3(0, 0, 0));
private _generateRays(v0: Vector3, v1: Vector3, v2: Vector3) {
this._tmpBounds.min.x = Math.floor(Math.min(v0.x, v1.x, v2.x));
this._tmpBounds.min.y = Math.floor(Math.min(v0.y, v1.y, v2.y));
this._tmpBounds.min.z = Math.floor(Math.min(v0.z, v1.z, v2.z));
const rayList: Array<Ray> = [];
this._traverseX(rayList, bounds);
this._traverseY(rayList, bounds);
this._traverseZ(rayList, bounds);
return rayList;
this._tmpBounds.max.x = Math.floor(Math.max(v0.x, v1.x, v2.x));
this._tmpBounds.max.y = Math.floor(Math.max(v0.y, v1.y, v2.y));
this._tmpBounds.max.z = Math.floor(Math.max(v0.z, v1.z, v2.z));
//const rayList: Array<Ray> = [];
this._traverseX(this._tmpBounds);
this._traverseY(this._tmpBounds);
this._traverseZ(this._tmpBounds);
//return rayList;
}
private _traverseX(rayList: Array<Ray>, bounds: Bounds) {
private _traverseX(bounds: Bounds) {
for (let y = bounds.min.y; y <= bounds.max.y; ++y) {
for (let z = bounds.min.z; z <= bounds.max.z; ++z) {
rayList.push({
origin: new Vector3(bounds.min.x - 1, y, z),
axis: Axes.x,
});
const ray = this._rayList.place();
ray.origin.x = bounds.min.x - 1;
ray.origin.y = y;
ray.origin.z = z;
ray.axis = Axes.x;
}
}
}
private _traverseY(rayList: Array<Ray>, bounds: Bounds) {
private _traverseY(bounds: Bounds) {
for (let x = bounds.min.x; x <= bounds.max.x; ++x) {
for (let z = bounds.min.z; z <= bounds.max.z; ++z) {
rayList.push({
origin: new Vector3(x, bounds.min.y - 1, z),
axis: Axes.y,
});
const ray = this._rayList.place();
ray.origin.x = x;
ray.origin.y = bounds.min.y - 1;
ray.origin.z = z;
ray.axis = Axes.y;
}
}
}
private _traverseZ(rayList: Array<Ray>, bounds: Bounds) {
private _traverseZ(bounds: Bounds) {
for (let x = bounds.min.x; x <= bounds.max.x; ++x) {
for (let y = bounds.min.y; y <= bounds.max.y; ++y) {
rayList.push({
origin: new Vector3(x, y, bounds.min.z - 1),
axis: Axes.z,
});
const ray = this._rayList.place();
ray.origin.x = x;
ray.origin.y = y;
ray.origin.z = bounds.min.z - 1;
ray.axis = Axes.z;
}
}
}

View File

@ -120,6 +120,7 @@ export class WorkerClient {
const voxeliser: IVoxeliser = VoxeliserFactory.GetVoxeliser(params.voxeliser);
this._loadedVoxelMesh = voxeliser.voxelise(this._loadedMesh, params);
this._loadedVoxelMesh.calculateNeighbours();
this._voxelMeshChunkIndex = 0;

View File

@ -0,0 +1,27 @@
import { LinearAllocator } from '../src/linear_allocator';
import { Vector3 } from '../src/vector';
import { TEST_PREAMBLE } from './preamble';
test('RegExpBuilder', () => {
TEST_PREAMBLE();
const vec = new LinearAllocator<Vector3>(() => {
return new Vector3(0, 0, 0);
});
expect(vec.size()).toBe(0);
expect(vec.max()).toBe(0);
const first = vec.place();
first.x = 1;
expect(vec.size()).toBe(1);
expect(vec.max()).toBe(1);
const second = vec.place();
second.x = 2;
expect(vec.size()).toBe(2);
expect(vec.max()).toBe(2);
vec.reset();
expect(vec.size()).toBe(0);
expect(vec.max()).toBe(2);
const newFirst = vec.place();
expect(newFirst.x).toBe(1);
});

View File

@ -12,18 +12,14 @@ test('Voxel neighbours', () => {
enableAmbientOcclusion: true,
});
voxelMesh.addVoxel(new Vector3(1, 2, 3), RGBAColours.WHITE);
voxelMesh.addVoxel(new Vector3(0, 0, 0), RGBAColours.WHITE);
voxelMesh.addVoxel(new Vector3(1, 1, 0), RGBAColours.WHITE);
voxelMesh.calculateNeighbours();
expect(voxelMesh.getNeighbours(new Vector3(1, 2, 3)).value).toBe(0);
// Even though this neighbour does exist, it is not calculated as
// this relationship is never tested
expect(voxelMesh.hasNeighbour(new Vector3(1, 2, 3).add(new Vector3(1, 0, 0)), new Vector3(-1, 0, 0))).toBe(false);
expect(voxelMesh.hasNeighbour(new Vector3(1, 2, 3).add(new Vector3(1, 1, 0)), new Vector3(-1, -1, 0))).toBe(true);
expect(voxelMesh.hasNeighbour(new Vector3(1, 2, 3).add(new Vector3(-1, -1, 0)), new Vector3(1, 1, 0))).toBe(true);
expect(voxelMesh.hasNeighbour(new Vector3(1, 2, 3).add(new Vector3(1, -1, 1)), new Vector3(-1, 1, -1))).toBe(true);
expect(voxelMesh.hasNeighbour(new Vector3(1, 2, 3).add(new Vector3(-1, 0, 1)), new Vector3(1, 0, -1))).toBe(true);
expect(voxelMesh.hasNeighbour(new Vector3(0, 0, 0), new Vector3(1, 1, 0))).toBe(true);
expect(voxelMesh.hasNeighbour(new Vector3(0, 0, 0), new Vector3(-1, -1, 0))).toBe(false);
expect(voxelMesh.hasNeighbour(new Vector3(1, 1, 0), new Vector3(-1, -1, 0))).toBe(true);
expect(voxelMesh.hasNeighbour(new Vector3(1, 1, 0), new Vector3(1, 1, 0))).toBe(false);
});
test('Add voxel', () => {