Skip to content

Commit

Permalink
Adds fast way for client to specify that a chunk should be entirely f…
Browse files Browse the repository at this point in the history
…illed

with a given voxel ID (air/dirt/etc). This lets the client skip over some
init, and will speed up the chunk scan and initial meshing.
Adds an internal lookup for "plain" blocks, to speed up most common path
when scanning voxel data.
  • Loading branch information
fenomas committed Jul 16, 2022
1 parent 70d13dd commit 453a0f5
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 31 deletions.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,14 @@ please try to be sorta-kinda consistent with what's already there.

## Change logs

See [history.md](docs/history.md) for changes and migration for each version.
See [history.md](docs/history.md) for full changes and migration for each version.

Recent changes:

* `v0.33`:
* Signature of `noa.registry.registerMaterial` changed to take an options object
* Terrain now supports texture atlases! See `registry.registerMaterial`.
* Key binding with `noa.inputs.bind` now uses [KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code) strings
* Binding to mouse buttons now uses `Mouse1`, `Mouse2`..
* Mouse move/scroll data (`dx,dy,scrollx,scrolly`) are moved from
`noa.inputs.state` to `noa.inputs.pointerState`
* Added a fast way to specify that a worldgen chunk is entirely air/dirt/etc.
* Modernized keybinds to use [KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code) strings, and changed several binding state properties
* `v0.32`: Fixes npm versioning issue - no code changes.
* `v0.31`:
* Change the speed of the world! See `noa.timeScale`
Expand Down
46 changes: 36 additions & 10 deletions src/lib/chunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default Chunk
*/

/** @param {import('../index').Engine} noa */
function Chunk(noa, requestID, ci, cj, ck, size, dataArray) {
function Chunk(noa, requestID, ci, cj, ck, size, dataArray, fillVoxelID) {
this.noa = noa
this.isDisposed = false

Expand All @@ -55,6 +55,12 @@ function Chunk(noa, requestID, ci, cj, ck, size, dataArray) {
noa._objectMesher.initChunk(this)
this._isFull = false
this._isEmpty = false
this._filledWithVoxel = -1

if (fillVoxelID >= 0) {
this.voxels.data.fill(fillVoxelID, 0, this.voxels.size)
this._filledWithVoxel = fillVoxelID
}

// references to neighboring chunks, if they exist (filled in by `world`)
var narr = Array.from(Array(27), () => null)
Expand All @@ -77,7 +83,7 @@ Chunk._createVoxelArray = function (size) {
return ndarray(arr, [size, size, size])
}

Chunk.prototype._updateVoxelArray = function (dataArray) {
Chunk.prototype._updateVoxelArray = function (dataArray, fillVoxelID) {
// dispose current object blocks
callAllBlockHandlers(this, 'onUnload')
this.noa._objectMesher.disposeChunk(this)
Expand All @@ -88,6 +94,11 @@ Chunk.prototype._updateVoxelArray = function (dataArray) {
this._blockHandlerLocs.empty()
this.noa._objectMesher.initChunk(this)
this.noa._terrainMesher.initChunk(this)
this._filledWithVoxel = -1
if (fillVoxelID >= 0) {
this._filledWithVoxel = fillVoxelID
this.voxels.data.fill(fillVoxelID, 0, this.voxels.size)
}
scanVoxelData(this)
}

Expand Down Expand Up @@ -149,6 +160,7 @@ Chunk.prototype.set = function (i, j, k, newID) {
// track full/emptiness and dirty flags for the chunk
if (!opaqueLookup[newID]) this._isFull = false
if (newID !== 0) this._isEmpty = false
this._filledWithVoxel = -1

var solidityChanged = (solidLookup[oldID] !== solidLookup[newID])
var opacityChanged = (opaqueLookup[oldID] !== opaqueLookup[newID])
Expand Down Expand Up @@ -230,31 +242,46 @@ Chunk.prototype.updateMeshes = function () {
*/

function scanVoxelData(chunk) {
// flags for tracking if chunk is entirely opaque or transparent
var fullyOpaque = true
var fullyAir = true

chunk._blockHandlerLocs.empty()
var voxels = chunk.voxels
var data = voxels.data
var len = voxels.shape[0]
var opaqueLookup = chunk.noa.registry._opacityLookup
var handlerLookup = chunk.noa.registry._blockHandlerLookup
var objectLookup = chunk.noa.registry._objectLookup
var plainLookup = chunk.noa.registry._blockIsPlainLookup
var objMesher = chunk.noa._objectMesher

// fastest case where entire chunk is air/dirt/etc
var monoID = chunk._filledWithVoxel
if (monoID >= 0 && !objMesher[monoID] && !handlerLookup[monoID]) {
chunk._isFull = !!opaqueLookup[monoID]
chunk._isEmpty = (monoID === 0)
chunk._terrainDirty = !chunk._isEmpty
return
}

// flags for tracking if chunk is entirely opaque or transparent
var fullyOpaque = true
var fullyAir = true

for (var i = 0; i < len; ++i) {
for (var j = 0; j < len; ++j) {
var index = voxels.index(i, j, 0)
for (var k = 0; k < len; ++k, ++index) {
var id = data[index]
// skip air blocks
// most common cases: air block...
if (id === 0) {
fullyOpaque = false
continue
}
// ...or plain boring block (no mesh, handlers, etc)
if (plainLookup[id]) {
fullyAir = false
continue
}
// otherwise check opacity, object mesh, and handlers
fullyOpaque = fullyOpaque && opaqueLookup[id]
fullyAir = false
// handle object blocks and handlers
if (objectLookup[id]) {
objMesher.setObjectBlock(chunk, id, i, j, k)
chunk._objectsDirty = true
Expand Down Expand Up @@ -282,7 +309,6 @@ function scanVoxelData(chunk) {




// dispose function - just clears properties and references

Chunk.prototype.dispose = function () {
Expand Down
9 changes: 9 additions & 0 deletions src/lib/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class Registry {
var blockProps = [null] // less-often accessed properties
var blockMeshes = [null] // custom mesh objects
var blockHandlers = [null] // block event handlers
var blockIsPlain = [false] // true if voxel is "boring" - solid/opaque, no special props

// this one is keyed by `blockID*6 + faceNumber`
var blockMats = [0, 0, 0, 0, 0, 0]
Expand Down Expand Up @@ -152,6 +153,12 @@ export class Registry {
var hasHandler = opts.onLoad || opts.onUnload || opts.onSet || opts.onUnset || opts.onCustomMeshCreate
blockHandlers[id] = (hasHandler) ? new BlockCallbackHolder(opts) : null

// special lookup for "plain"-ness
// plain means solid, opaque, not fluid, no mesh or events
var isPlain = blockSolidity[id] && blockOpacity[id]
&& !hasHandler && !blockIsFluid[id] && !blockIsObject[id]
blockIsPlain[id] = isPlain

return id
}

Expand Down Expand Up @@ -278,6 +285,8 @@ export class Registry {
this._blockMeshLookup = blockMeshes
/** @internal */
this._blockHandlerLookup = blockHandlers
/** @internal */
this._blockIsPlainLookup = blockIsPlain



Expand Down
4 changes: 3 additions & 1 deletion src/lib/terrainMesher.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ function buildPaddedVoxelArray(chunk) {
tgtPos[n] = tgtPosValues[coord]
}
var nab = chunk._neighbors.get(i - 1, j - 1, k - 1)
var nsrc = (nab) ? nab.voxels : null
var nsrc = 0
if (nab) nsrc = (nab._filledWithVoxel >= 0) ?
nab._filledWithVoxel : nab.voxels
copyNdarrayContents(nsrc, tgt, pos, size, tgtPos)
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ var prevRad = 0, prevAnswer = 0
// partly "unrolled" loops to copy contents of ndarrays
// when there's no source, zeroes out the array instead
export function copyNdarrayContents(src, tgt, pos, size, tgtPos) {
if (src) {
if (typeof src === 'number') {
doNdarrayFill(src, tgt, tgtPos[0], tgtPos[1], tgtPos[2],
size[0], size[1], size[2])
} else {
doNdarrayCopy(src, tgt, pos[0], pos[1], pos[2],
size[0], size[1], size[2], tgtPos[0], tgtPos[1], tgtPos[2])
} else {
doNdarrayZero(tgt, tgtPos[0], tgtPos[1], tgtPos[2],
size[0], size[1], size[2])
}
}
function doNdarrayCopy(src, tgt, i0, j0, k0, si, sj, sk, ti, tj, tk) {
Expand All @@ -85,13 +85,13 @@ function doNdarrayCopy(src, tgt, i0, j0, k0, si, sj, sk, ti, tj, tk) {
}
}

function doNdarrayZero(tgt, i0, j0, k0, si, sj, sk) {
function doNdarrayFill(value, tgt, i0, j0, k0, si, sj, sk) {
var dx = tgt.stride[2]
for (var i = 0; i < si; i++) {
for (var j = 0; j < sj; j++) {
var ix = tgt.index(i0 + i, j0 + j, k0)
for (var k = 0; k < sk; k++) {
tgt.data[ix] = 0
tgt.data[ix] = value
ix += dx
}
}
Expand Down
20 changes: 12 additions & 8 deletions src/lib/world.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,16 @@ World.prototype.isBoxUnobstructed = function (box) {

/** client should call this after creating a chunk's worth of data (as an ndarray)
* If userData is passed in it will be attached to the chunk
* @param id
* @param array
* @param userData
* @param {string} id - the string specified when the chunk was requested
* @param {*} array - an ndarray of voxel data
* @param {*} userData - an arbitrary value for game client use
* @param {number} fillVoxelID - specify a voxel ID here if you want to signify that
* the entire chunk should be solidly filled with that voxel (e.g. `0` for air).
* If you do this, the voxel array data will be overwritten and the engine will
* take a fast path through some initialization steps.
*/
World.prototype.setChunkData = function (id, array, userData) {
setChunkData(this, id, array, userData)
World.prototype.setChunkData = function (id, array, userData = null, fillVoxelID = -1) {
setChunkData(this, id, array, userData, fillVoxelID)
}


Expand Down Expand Up @@ -719,7 +723,7 @@ function requestNewChunk(world, i, j, k) {

// called when client sets a chunk's voxel data
// If userData is passed in it will be attached to the chunk
function setChunkData(world, reqID, array, userData) {
function setChunkData(world, reqID, array, userData, fillVoxelID) {
var arr = reqID.split('|')
var i = parseInt(arr.shift())
var j = parseInt(arr.shift())
Expand All @@ -736,14 +740,14 @@ function setChunkData(world, reqID, array, userData) {
if (!chunk) {
// if chunk doesn't exist, create and init
var size = world._chunkSize
chunk = new Chunk(world.noa, reqID, i, j, k, size, array)
chunk = new Chunk(world.noa, reqID, i, j, k, size, array, fillVoxelID)
world._storage.storeChunkByIndexes(i, j, k, chunk)
chunk.userData = userData
world.noa.rendering.prepareChunkForRendering(chunk)
world.emit('chunkAdded', chunk)
} else {
// else we're updating data for an existing chunk
chunk._updateVoxelArray(array)
chunk._updateVoxelArray(array, fillVoxelID)
}
// chunk can now be meshed, and ping neighbors
possiblyQueueChunkForMeshing(world, chunk)
Expand Down

0 comments on commit 453a0f5

Please sign in to comment.