diff --git a/src/game/main.ts b/src/game/main.ts index 6f70561..c255ec5 100644 --- a/src/game/main.ts +++ b/src/game/main.ts @@ -4,49 +4,54 @@ import { Resource } from '../engine/Resources' import { Benchmark } from './utilities/Benchmark' import { ChunkManager } from './world/ChunkManager' import { TerrainGenerator } from './world/TerrainGenerator' +import { DistanceField } from './world/raymarcher/DistanceField' import { WorkerManager } from './world/workers/WorkerManager' export default class Game implements Experience { resources: Resource[] = [] - constructor(private engine: Engine) { } + constructor(private engine: Engine) {} @Benchmark init(): void { const terrainGenerator = new TerrainGenerator(69420) - const chunkManager = new ChunkManager(terrainGenerator, 8, 2, 8) + const chunkManager = new ChunkManager(terrainGenerator, 0, 0, 0) - const chunks = chunkManager.createChunksAroundOrigin(0, 0, 0) + const chunks = chunkManager.createChunksAroundOrigin(0, -1, 0) const workerPath = './src/game/world/workers/TerrainGenerationWorker.ts' - const workerCount = navigator.hardwareConcurrency + const workerManager = new WorkerManager(workerPath, 1) - const workerManager = new WorkerManager(workerPath, workerCount) + const chunk = chunks[0] + this.engine.scene.add(chunk.mesh) - chunks.forEach((chunk) => { - this.engine.scene.add(chunk.mesh) + const task = chunk.prepareGeneratorWorkerData() - const task = chunk.prepareGeneratorWorkerData() + workerManager.enqueueTask({ + payload: task.payload, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (args: any) => { + task.callback(args) + chunk.updateMeshGeometry() - workerManager.enqueueTask({ - payload: task.payload, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback: (args: any) => { - task.callback(args) - requestAnimationFrame(() => chunk.updateMeshGeometry()) - } - }) - }) + const distanceField = new DistanceField(chunk.chunkData, 1) + distanceField.calculateDistanceField() - chunks.forEach((chunk) => { - setTimeout(() => { - chunk.updateMeshGeometry() - }, Math.random()) + const src = distanceField.generateTexture(20) + const image = new Image() + image.src = src + + document.body.appendChild(image) + image.setAttribute( + 'style', + 'position: absolute; height: 50%; left: 0; top: 0; border: 1px solid red; image-rendering: pixelated; ' + ) + } }) } // eslint-disable-next-line @typescript-eslint/no-unused-vars - update(delta: number): void { } + update(delta: number): void {} - resize?(): void { } + resize?(): void {} } diff --git a/src/game/util/Matrix3d.ts b/src/game/util/Matrix3d.ts index 38bd683..ce0fd25 100644 --- a/src/game/util/Matrix3d.ts +++ b/src/game/util/Matrix3d.ts @@ -12,6 +12,10 @@ export class Matrix3d { this.data = new Uint8Array(width * height * depth).fill(defaultValue) } + fill(value: number) { + this.data.fill(value) + } + get(x: number, y: number, z: number) { return this.data[this.getIndex(x, y, z)] } diff --git a/src/game/world/raymarcher/DistanceField.ts b/src/game/world/raymarcher/DistanceField.ts new file mode 100644 index 0000000..d3ae3ab --- /dev/null +++ b/src/game/world/raymarcher/DistanceField.ts @@ -0,0 +1,178 @@ +import { Vector3 } from 'three' +import { Matrix3d } from '../../util/Matrix3d' +import { ChunkData } from '../ChunkData' + +export class DistanceField extends Matrix3d { + static sweepDirections = [ + { dx: 1, dy: 0, dz: 0 }, + { dx: -1, dy: 0, dz: 0 }, + { dx: 0, dy: 1, dz: 0 }, + { dx: 0, dy: -1, dz: 0 }, + { dx: 0, dy: 0, dz: 1 }, + { dx: 0, dy: 0, dz: -1 } + ] + + static readonly directionDimensions = { + dx: 'width', + dy: 'height', + dz: 'depth' + } as const + + constructor(public voxelData: ChunkData, public resolutionFactor: number) { + super( + voxelData.width * resolutionFactor, + voxelData.height * resolutionFactor, + voxelData.depth * resolutionFactor + ) + const maxDimension = Math.max(this.width, this.height, this.depth) + const maxFieldValue = 255 + if (resolutionFactor * maxDimension >= maxFieldValue) { + throw new Error('Resolution factor is too high') + } + } + + calculateDistanceField() { + this.fill(255) + + const dimensionVector = new Vector3(this.width, this.height, this.depth) + let directionVector = new Vector3(0, 0, 1) + + for (let y = 0; y < dimensionVector.y; y++) { + for (let x = 0; x < dimensionVector.x; x++) { + let steps = 0 + for (let z = 0; z < dimensionVector.z; z++) { + const empty = this.isVoxelEmpty(x, y, z) + const onEdge = z == dimensionVector.z - 1 + + if (empty && !onEdge) { + steps++ + continue + } + + for (let delta = 0; delta <= steps; delta++) { + const position = directionVector + .clone() + .multiplyScalar(-delta) + .add(new Vector3(x, y, z)) + const value = Math.min(delta, this.get(position.x, position.y, position.z)) + this.set(position.x, position.y, position.z, value) + } + steps = 0 + } + } + } + + directionVector.multiplyScalar(-1) + + for (let y = 0; y < dimensionVector.y; y++) { + for (let x = 0; x < dimensionVector.x; x++) { + let steps = 0 + for (let z = dimensionVector.z - 1; z >= 0; z--) { + const empty = this.isVoxelEmpty(x, y, z) + const onEdge = z == 0 + + if (empty && !onEdge) { + steps++ + continue + } + + for (let delta = 0; delta <= steps; delta++) { + const position = directionVector + .clone() + .multiplyScalar(-delta) + .add(new Vector3(x, y, z)) + const value = Math.min(delta, this.get(position.x, position.y, position.z)) + this.set(position.x, position.y, position.z, value) + } + steps = 0 + } + } + } + + directionVector = new Vector3(1, 0, 0) + + for (let y = 0; y < dimensionVector.y; y++) { + for (let z = 0; z < dimensionVector.z; z++) { + let steps = 0 + for (let x = 0; x < dimensionVector.x; x++) { + const empty = this.isVoxelEmpty(x, y, z) + const onEdge = x == dimensionVector.x - 1 + + if (empty && !onEdge) { + steps++ + continue + } + + for (let delta = 0; delta <= steps; delta++) { + const position = directionVector + .clone() + .multiplyScalar(-delta) + .add(new Vector3(x, y, z)) + const value = Math.min(delta, this.get(position.x, position.y, position.z)) + this.set(position.x, position.y, position.z, value) + } + steps = 0 + } + } + } + + directionVector.multiplyScalar(-1) + + for (let y = 0; y < dimensionVector.y; y++) { + for (let z = 0; z < dimensionVector.z; z++) { + let steps = 0 + for (let x = dimensionVector.x - 1; x >= 0; x--) { + const empty = this.isVoxelEmpty(x, y, z) + const onEdge = x == 0 + + if (empty && !onEdge) { + steps++ + continue + } + + for (let delta = 0; delta <= steps; delta++) { + const position = directionVector + .clone() + .multiplyScalar(-delta) + .add(new Vector3(x, y, z)) + const value = Math.min(delta, this.get(position.x, position.y, position.z)) + this.set(position.x, position.y, position.z, value) + } + steps = 0 + } + } + } + } + + isVoxelEmpty(x: number, y: number, z: number) { + return this.getVoxelAt(x, y, z) === 0 + } + + getVoxelAt(x: number, y: number, z: number) { + return this.voxelData.get( + Math.floor(x / this.resolutionFactor), + Math.floor(y / this.resolutionFactor), + Math.floor(z / this.resolutionFactor) + ) + } + + generateTexture(yLevel: number) { + const canvas = document.createElement('canvas') + canvas.width = this.width + canvas.height = this.depth + const ctx = canvas.getContext('2d') + if (!ctx) throw new Error('Ctx could not be retrieved') + + const y = yLevel * this.resolutionFactor + + for (let x = 0; x < this.width; x++) { + for (let z = 0; z < this.depth; z++) { + const value = (this.get(x, y, z) / this.resolutionFactor) * 10 + ctx.fillStyle = `rgba(${value}, ${value}, ${value}, ${value})` + ctx.fillRect(x, z, 1, 1) + } + } + + return canvas.toDataURL() + } +}