Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement efficient distance field calculation #24

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
51 changes: 28 additions & 23 deletions src/game/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
4 changes: 4 additions & 0 deletions src/game/util/Matrix3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
}
Expand Down
178 changes: 178 additions & 0 deletions src/game/world/raymarcher/DistanceField.ts
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading