diff --git a/src/api/WorldComputeProxy.ts b/src/api/WorldComputeProxy.ts new file mode 100644 index 0000000..12e524d --- /dev/null +++ b/src/api/WorldComputeProxy.ts @@ -0,0 +1,165 @@ +import { Box2, Vector3 } from 'three' + +import { Block, EntityData, PatchKey } from '../common/types' +import { EntityChunk, EntityChunkStub } from '../datacontainers/EntityChunk' +import { GroundPatch, WorldCompute, WorldUtils } from '../index' +import { parseThreeStub } from '../common/utils' + +export enum ComputeApiCall { + PatchCompute = 'bakeGroundPatch', + BlocksBatchCompute = 'computeBlocksBatch', + OvergroundBufferCompute = 'computeOvergroundBuffer', + QueryEntities = 'queryEntities', + BakeEntities = 'queryBakeEntities', + BattleBoardCompute = 'computeBoardData', +} + +export type ComputeApiParams = Partial<{ + rememberMe: boolean // allow for caching value + preCacheRadius: number // pre-caching next requests + includeEntitiesBlocks: boolean // skip or include entities blocks +}> + +/** + * Exposing world compute api with ability to run inside optional worker + * When provided all request are proxied to worker instead of main thread + */ +export class WorldComputeProxy { + // eslint-disable-next-line no-use-before-define + static singleton: WorldComputeProxy + // eslint-disable-next-line no-undef + workerInstance: Worker | undefined + resolvers: Record = {} + count = 0 + + static get instance() { + this.singleton = this.singleton || new WorldComputeProxy() + return this.singleton + } + + get worker() { + return this.workerInstance + } + + // eslint-disable-next-line no-undef + set worker(workerInstance: Worker | undefined) { + this.workerInstance = workerInstance + if (workerInstance) { + workerInstance.onmessage = ({ data }) => { + if (data.id !== undefined) { + this.resolvers[data.id]?.(data.data) + delete this.resolvers[data.id] + } + } + + workerInstance.onerror = error => { + console.error(error) + } + + workerInstance.onmessageerror = error => { + console.error(error) + } + } + } + + /** + * Proxying request to worker + */ + workerCall(apiName: ComputeApiCall, args: any[]) { + if (this.worker) { + const id = this.count++ + this.worker.postMessage({ id, apiName, args }) + return new Promise(resolve => (this.resolvers[id] = resolve)) + } + return null + } + + async computeBlocksBatch( + blockPosBatch: Vector3[], + params = { includeEntitiesBlocks: false }, + ) { + const blocks = !this.worker + ? WorldCompute.computeBlocksBatch(blockPosBatch, params) + : ((await this.workerCall(ComputeApiCall.BlocksBatchCompute, [ + blockPosBatch, + params, + ])?.then((blocksStubs: Block[]) => + // parse worker's data to recreate original objects + blocksStubs.map(blockStub => { + blockStub.pos = WorldUtils.parseThreeStub(blockStub.pos) + return blockStub + }), + )) as Block[]) + + return blocks + } + + // *iterEntitiesBaking(entityKeys: EntityKey[]) { + // for (const entityKey of entityKeys) { + // const entityChunk = WorldCompute.bakeChunkEntity(entityKey) + // yield entityChunk + // } + // } + + async queryEntities(queriedRegion: Box2) { + const entitiesData = !this.worker + ? WorldCompute.queryEntities(queriedRegion) + : ((await this.workerCall( + ComputeApiCall.QueryEntities, + [queriedRegion], // [emptyPatch.bbox] + )?.then(stubs => + stubs.map((stub: EntityData) => ({ + ...stub, + bbox: parseThreeStub(stub.bbox), + })), + )) as EntityData[]) + return entitiesData + } + + async *iterPatchCompute(patchKeysBatch: PatchKey[]) { + for (const patchKey of patchKeysBatch) { + const patch = !this.worker + ? WorldCompute.bakeGroundPatch(patchKey) + : ((await this.workerCall( + ComputeApiCall.PatchCompute, + [patchKey], // [emptyPatch.bbox] + )?.then(patchStub => + new GroundPatch().fromStub(patchStub), + )) as GroundPatch) + + yield patch + } + } + + async bakeGroundPatch(boundsOrPatchKey: Box2 | string) { + const patchStub = !this.worker + ? WorldCompute.bakeGroundPatch(boundsOrPatchKey) + : await this.workerCall(ComputeApiCall.PatchCompute, [boundsOrPatchKey]) + // ?.then(patchStub => new GroundPatch().fromStub(patchStub)) as GroundPatch + + return patchStub + } + + async bakeEntities(queriedRange: Box2) { + const entityChunks = !this.worker + ? WorldCompute.queryBakeEntities(queriedRange) + : await this.workerCall(ComputeApiCall.BakeEntities, [ + queriedRange, + ])?.then((entityChunks: EntityChunkStub[]) => + // parse worker's data to recreate original objects + entityChunks.map(chunkStub => EntityChunk.fromStub(chunkStub)), + ) + return entityChunks + } + + // async requestBattleBoard(boardCenter: Vector3, boardParams: BoardParams, lastBoardBounds: Box2) { + // const boardData = !this.worker ? + // WorldCompute.computeBoardData(boardCenter, boardParams, lastBoardBounds) : + // await this.workerCall( + // ComputeApiCall.BattleBoardCompute, + // [boardCenter, boardParams, lastBoardBounds], + // ) + // const board = new BoardContainer().fromStub(boardData) + // return board + // } +} diff --git a/src/api/world-compute.ts b/src/api/world-compute.ts new file mode 100644 index 0000000..82259a3 --- /dev/null +++ b/src/api/world-compute.ts @@ -0,0 +1,180 @@ +import { Box2, Vector3 } from 'three' + +import { EntityType, GroundPatch } from '../index' +import { Biome, BlockType } from '../procgen/Biome' +import { Heightmap } from '../procgen/Heightmap' +import { Block, EntityData, PatchKey } from '../common/types' +import { asBox3, asVect2, asVect3 } from '../common/utils' +import { WorldEntities } from '../procgen/WorldEntities' +import { EntityChunk, EntityChunkStub } from '../datacontainers/EntityChunk' +import { BlockData } from '../datacontainers/GroundPatch' +// import { BoardInputParams } from '../feats/BoardContainer' + +/** + * Individual blocks requests + */ + +/** + * + * @param blockPosBatch + * @param params + * @returns + */ +export const computeBlocksBatch = ( + blockPosBatch: Vector3[], + params = { includeEntitiesBlocks: false }, +) => { + const { includeEntitiesBlocks } = params + const blocksBatch = blockPosBatch.map(({ x, z }) => { + const blockPos = new Vector3(x, 0, z) + const blockData = computeGroundBlock(blockPos) + if (includeEntitiesBlocks) { + const entityRange = new Box2().setFromPoints([asVect2(blockPos)]) + entityRange.max.addScalar(1) + const [foundEntity] = queryEntities(entityRange).map(entityData => { + const { min, max } = entityData.bbox + const custChunkBox = asBox3(entityRange) + custChunkBox.min.y = min.y + custChunkBox.max.y = max.y + return new EntityChunk(entityData, custChunkBox) + }) + const blocksBuffer = foundEntity?.voxelize() + const lastBlockIndex = blocksBuffer?.findLastIndex(elt => elt) + if (blocksBuffer && lastBlockIndex && lastBlockIndex >= 0) { + blockData.level += lastBlockIndex + blockData.type = blocksBuffer[lastBlockIndex] as BlockType + } + } + blockPos.y = blockData.level + const block: Block = { + pos: blockPos, + data: blockData, + } + return block + }) + return blocksBatch +} + +export const computeGroundBlock = (blockPos: Vector3) => { + const biomeContribs = Biome.instance.getBiomeInfluence(blockPos) + const mainBiome = Biome.instance.getMainBiome(biomeContribs) + const rawVal = Heightmap.instance.getRawVal(blockPos) + const blockTypes = Biome.instance.getBlockType(rawVal, mainBiome) + const level = Heightmap.instance.getGroundLevel( + blockPos, + rawVal, + biomeContribs, + ) + // const pos = new Vector3(blockPos.x, level, blockPos.z) + const type = blockTypes.grounds[0] as BlockType + // const entityType = blockTypes.entities?.[0] as EntityType + // let offset = 0 + // if (lastBlock && entityType) { + + // } + // level += offset + const block: BlockData = { level, type } + return block +} + +/** + * Patch requests + */ + +// Ground +export const bakeGroundPatch = (boundsOrPatchKey: PatchKey | Box2) => { + const groundPatch = new GroundPatch(boundsOrPatchKey) + const { min, max } = groundPatch.bounds + const blocks = groundPatch.iterBlocksQuery(undefined, false) + const level = { + min: 512, + max: 0, + } + let blockIndex = 0 + for (const block of blocks) { + const blockData = computeGroundBlock(block.pos) + level.min = Math.min(min.y, blockData.level) + level.max = Math.max(max.y, blockData.level) + groundPatch.writeBlockData(blockIndex, blockData) + blockIndex++ + } + return groundPatch +} + +// Battle board +// export const computeBoardData = (boardPos: Vector3, boardParams: BoardInputParams, lastBoardBounds: Box2) => { +// const boardMap = new BoardContainer(boardPos, boardParams, lastBoardBounds) +// await boardMap.fillGroundData() +// await boardMap.populateEntities() +// const boardStub = boardMap.toStub() +// return boardStub +// } + +/** + * Entity queries/baking + */ + +export const queryEntities = (queriedRegion: Box2) => { + const spawnablePlaces = WorldEntities.instance.queryDistributionMap( + EntityType.TREE_APPLE, + )(queriedRegion) + const spawnedEntities = spawnablePlaces + .map(entLoc => + WorldEntities.instance.getEntityData( + EntityType.TREE_PINE, + asVect3(entLoc), + ), + ) + .filter(entity => confirmFinalizeEntity(entity)) + return spawnedEntities +} + +/** + * + * @param entityPos + * @returns + */ +const confirmFinalizeEntity = (entity: EntityData) => { + const entityPos = entity.bbox.getCenter(new Vector3()) + // use global coords in case entity center is from adjacent patch + const rawVal = Heightmap.instance.getRawVal(entityPos) + const mainBiome = Biome.instance.getMainBiome(entityPos) + const blockTypes = Biome.instance.getBlockType(rawVal, mainBiome) + const entityType = blockTypes.entities?.[0] as EntityType + // confirm this kind of entity can spawn over here + if (entityType) { + entity.bbox.min.y = Heightmap.instance.getGroundLevel(entityPos, rawVal) + entity.bbox.max.y += entity.bbox.min.y + return entity + } + return null +} + +export const queryBakeEntities = (queriedRange: Box2) => { + const entitiesData = queryEntities(queriedRange) + return bakeEntitiesBatch(entitiesData) +} + +export const bakeEntitiesBatch = (entities: EntityData[]) => { + const entitiesChunks: EntityChunkStub[] = entities + .map(entityData => new EntityChunk(entityData)) + .map(entityChunk => { + entityChunk.voxelize() + return entityChunk.toStub() + }) + return entitiesChunks +} + +// /** +// * return all entity types which can spwawn over specific region +// */ +// const getSpawnableEntities = (region: Box2) => { +// // TODO +// } + +// /** +// * granular check in transition place (spline or biome transitions) +// */ +// const confirmSpawnability = () => { + +// } diff --git a/src/common/math.ts b/src/common/math.ts new file mode 100644 index 0000000..9d37d68 --- /dev/null +++ b/src/common/math.ts @@ -0,0 +1,19 @@ +import { Box2, Vector2 } from 'three' + +export const findBoundingBox = ( + point: Vector2, + points: Vector2[], + bounds: Box2, +) => { + const { min, max } = bounds.clone() + + for (const p of points) { + min.x = p.x < point.x ? Math.max(p.x, min.x) : min.x + min.y = p.y < point.y ? Math.max(p.y, min.y) : min.y + max.x = p.x > point.x ? Math.min(p.x, max.x) : max.x + max.y = p.y > point.y ? Math.min(p.y, max.y) : max.y + } + + const bbox = new Box2(min, max) + return bbox +} diff --git a/src/common/types.ts b/src/common/types.ts index ee28251..d21a182 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,12 +1,19 @@ -import { Vector3 } from 'three' +import { Box3, Vector2, Vector3 } from 'three' +import { BlockData } from '../datacontainers/GroundPatch' import { BiomeType, BlockType } from '../procgen/Biome' import { LinkedList } from './misc' export type Block = { pos: Vector3 - type: BlockType + data: BlockData + buffer?: Uint16Array +} + +export type PatchBlock = Block & { + index: number + localPos: Vector3 } export enum Adjacent2dPos { @@ -107,3 +114,19 @@ export enum EntityType { TREE_APPLE = 'apple_tree', TREE_PINE = 'pine_tree', } + +export type EntityData = { + type: EntityType + bbox: Box3 + params: { + radius: number + size: number + } +} + +export type EntityKey = string + +export type PatchKey = string +export type PatchId = Vector2 +export type ChunkKey = string +export type ChunkId = Vector3 diff --git a/src/common/utils.ts b/src/common/utils.ts index 628042e..f306620 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,10 +1,16 @@ -import { Box3, Vector2, Vector3 } from 'three' +import { Box2, Box3, Vector2, Vector2Like, Vector3, Vector3Like } from 'three' + +import { WorldConf } from '../index' import { Adjacent2dPos, Adjacent3dPos, + ChunkId, + ChunkKey, MappingRange, MappingRanges, + PatchId, + PatchKey, } from './types' // Clamp number between two values: @@ -16,6 +22,17 @@ const roundToDec = (val: number, n_pow: number) => { return Math.round(val * num) / num } +const vectRoundToDec = (input: Vector2 | Vector3, n_pow: number) => { + let { x, y } = input + x = roundToDec(x, n_pow) + y = roundToDec(y, n_pow) + const output = + input instanceof Vector3 + ? new Vector3(x, y, roundToDec(input.z, n_pow)) + : new Vector2(x, y) + return output +} + // const MappingRangeFinder = (item: LinkedList, inputVal: number) => item.next && inputVal > (item.next.data as MappingData).x export const MappingRangeSorter = (item1: MappingRange, item2: MappingRange) => item1.x - item2.x @@ -47,12 +64,24 @@ const interpolatePoints = (p1: Vector2, p2: Vector2, t: number) => { } /** - * Direct neighbours e.g. + * Orthogonal or direct 2D neighbours e.g. + * - TOP/BOTTOM, + * - LEFT/RIGHT + */ +const directNeighbours2D = [ + Adjacent2dPos.left, + Adjacent2dPos.right, + Adjacent2dPos.top, + Adjacent2dPos.bottom, +] + +/** + * Orthogonal or direct 3D neighbours e.g. * - FRONT/BACK, * - TOP/BOTTOM, * - LEFT/RIGHT */ -const AdjacentNeighbours3d = [ +const directNeighbours3D = [ Adjacent3dPos.xPy0z0, Adjacent3dPos.xMy0z0, // right, left Adjacent3dPos.x0yPz0, @@ -147,18 +176,24 @@ const getAdjacent3dCoords = (pos: Vector3, dir: Adjacent3dPos): Vector3 => { } } -const getAllNeighbours2dCoords = (pos: Vector2): Vector2[] => { - const neighbours = Object.values(Adjacent3dPos) - .filter(v => !isNaN(Number(v))) - .map(type => getAdjacent2dCoords(pos, type as number)) - return neighbours +const getNeighbours2D = ( + pos: Vector2, + directNeighboursOnly = false, +): Vector2[] => { + const neighbours = directNeighboursOnly + ? directNeighbours2D + : Object.values(Adjacent2dPos).filter(v => !isNaN(Number(v))) + return neighbours.map(type => getAdjacent2dCoords(pos, type as number)) } -const getAllNeighbours3dCoords = (pos: Vector3): Vector3[] => { - const neighbours = Object.values(Adjacent3dPos) - .filter(v => !isNaN(Number(v))) - .map(type => getAdjacent3dCoords(pos, type as number)) - return neighbours +const getNeighbours3D = ( + pos: Vector3, + directNeighboursOnly = false, +): Vector3[] => { + const neighbours = directNeighboursOnly + ? directNeighbours3D + : Object.values(Adjacent3dPos).filter(v => !isNaN(Number(v))) + return neighbours.map(type => getAdjacent3dCoords(pos, type as number)) } const getPatchPoints = (patchBBox: Box3, clearY = true) => { @@ -184,16 +219,181 @@ const bboxContainsPointXZ = (bbox: Box3, point: Vector3) => { ) } +const asVect2 = (v3: Vector3) => { + return new Vector2(v3.x, v3.z) +} + +const asVect3 = (v2: Vector2, yVal = 0) => { + return new Vector3(v2.x, yVal, v2.y) +} + +const asBox2 = (box3: Box3) => { + return new Box2(asVect2(box3.min), asVect2(box3.max)) +} + +const asBox3 = (box2: Box2) => { + return new Box3(asVect3(box2.min), asVect3(box2.max)) +} + +const isVect2Stub = (stub: Vector2Like) => { + return ( + stub !== undefined && + stub.x !== undefined && + stub.y !== undefined && + (stub as any).z === undefined + ) +} + +const isVect3Stub = (stub: Vector3Like) => { + return ( + stub !== undefined && + stub.x !== undefined && + stub.y !== undefined && + stub.z !== undefined + ) +} + +const parseVect3Stub = (stub: Vector3Like) => { + let res + if (isVect3Stub(stub)) { + res = new Vector3(...Object.values(stub)) + } + return res +} + +const parseVect2Stub = (stub: Vector2Like) => { + let res + if (isVect2Stub(stub)) { + res = new Vector2(...Object.values(stub)) + } + return res +} + +const parseBox2Stub = (stub: Box2) => { + let res + if (isVect2Stub(stub.min) && isVect2Stub(stub.max)) { + const min = parseVect2Stub(stub.min) + const max = parseVect2Stub(stub.max) + res = new Box2(min, max) + } + return res +} + +const parseBox3Stub = (stub: Box3) => { + let res + if (isVect3Stub(stub.min) && isVect3Stub(stub.max)) { + const min = parseVect3Stub(stub.min) + const max = parseVect3Stub(stub.max) + res = new Box3(min, max) + } + return res +} + +const parseThreeStub = (stub: any) => { + return stub + ? parseBox3Stub(stub) || + parseVect3Stub(stub) || + parseBox2Stub(stub) || + parseVect2Stub(stub) || + stub + : stub +} + +const parsePatchKey = (patchKey: PatchKey) => { + let patchId + if (patchKey?.length > 0) { + patchId = new Vector2( + parseInt(patchKey.split(':')[0] as string), + parseInt(patchKey.split(':')[1] as string), + ) + } + return patchId +} + +const getPatchId = (position: Vector2, patchSize: Vector2) => { + const patchId = position.clone().divide(patchSize).floor() + return patchId +} + +const patchUpperId = (position: Vector2, patchSize: Vector2) => { + const patchId = position.clone().divide(patchSize).ceil() + return patchId +} + +const serializePatchId = (patchId: PatchId | undefined) => { + let patchKey = '' + if (patchId) { + const { x, y } = patchId + patchKey = `${x}:${y}` + } + return patchKey +} + +const patchBoxFromKey = (patchKey: string, patchDims: Vector2) => { + const patchCoords = parsePatchKey(patchKey) + const bbox = new Box2() + if (patchCoords) { + bbox.min = patchCoords.clone().multiply(patchDims) + bbox.max = patchCoords.clone().addScalar(1).multiply(patchDims) + } + return bbox +} + +const parseChunkKey = (chunkKey: ChunkKey) => { + const chunkId = new Vector3( + parseInt(chunkKey.split('_')[1] as string), + parseInt(chunkKey.split('_')[2] as string), + parseInt(chunkKey.split('_')[3] as string), + ) + return chunkId +} + +const serializeChunkId = (chunkId: Vector3) => { + return `chunk_${chunkId.x}_${chunkId.y}_${chunkId.z}` +} + +function genChunkIds(patchId: PatchId, ymin: number, ymax: number) { + const chunk_ids = [] + for (let y = ymax; y >= ymin; y--) { + const chunk_coords = asVect3(patchId, y) + chunk_ids.push(chunk_coords) + } + return chunk_ids +} + +const chunkBoxFromId = ( + chunkId: ChunkId, + patchSize: number = WorldConf.patchSize, +) => { + const bmin = chunkId.clone().multiplyScalar(patchSize) + const bmax = chunkId.clone().addScalar(1).multiplyScalar(patchSize) + const chunkBbox = new Box3(bmin, bmax) + chunkBbox.expandByScalar(1) + return chunkBbox +} + export { roundToDec, + vectRoundToDec, clamp, findMatchingRange, interpolatePoints, - AdjacentNeighbours3d, - getAdjacent2dCoords, - getAdjacent3dCoords, - getAllNeighbours2dCoords, - getAllNeighbours3dCoords, + getNeighbours2D, + getNeighbours3D, bboxContainsPointXZ, getPatchPoints, + parseThreeStub, + asVect2, + asVect3, + asBox2, + asBox3, + parsePatchKey, + getPatchId, + patchUpperId, + serializePatchId, + patchBoxFromKey, + parseChunkKey, + serializeChunkId, + chunkBoxFromId, + genChunkIds, } diff --git a/src/datacontainers/DataContainers.ts b/src/datacontainers/DataContainers.ts new file mode 100644 index 0000000..1ee7935 --- /dev/null +++ b/src/datacontainers/DataContainers.ts @@ -0,0 +1,205 @@ +import { Vector2, Box2 } from 'three' + +import { PatchKey } from '../common/types' +import { + getPatchId, + parsePatchKey, + patchBoxFromKey, + patchUpperId, + serializePatchId, +} from '../common/utils' +import { WorldConf } from '../index' + +const getDefaultPatchDim = () => + new Vector2(WorldConf.patchSize, WorldConf.patchSize) + +/** + * Multi purpose low level data container + */ +export abstract class DataContainer { + bounds: Box2 + dimensions: Vector2 + margin = 0 + key = '' // needed for patch export + patchId: Vector2 | undefined + abstract rawData: T + + constructor(boundsOrPatchKey: Box2 | PatchKey = new Box2(), margin = 0) { + //, bitLength = BitLength.Uint16) { + const bounds = + boundsOrPatchKey instanceof Box2 + ? boundsOrPatchKey.clone() + : patchBoxFromKey(boundsOrPatchKey, getDefaultPatchDim()) + this.bounds = bounds + this.dimensions = bounds.getSize(new Vector2()) + this.margin = margin + const patchId = + typeof boundsOrPatchKey === 'string' + ? parsePatchKey(boundsOrPatchKey) + : null + if (patchId) { + this.id = patchId + } + // this.rawData = getArrayConstructor(bitLength) + } + + get id() { + return this.patchId + } + + set id(patchId: Vector2 | undefined) { + this.patchId = patchId + this.key = serializePatchId(patchId) + } + + get extendedBounds() { + return this.bounds.clone().expandByScalar(this.margin) + } + + get extendedDims() { + return this.extendedBounds.getSize(new Vector2()) + } + + get localBox() { + const localBox = new Box2(new Vector2(0), this.dimensions.clone()) + return localBox + } + + get localExtendedBox() { + return this.localBox.expandByScalar(this.margin) + } + + init(bounds: Box2) { + this.bounds = bounds + this.dimensions = bounds.getSize(new Vector2()) + } + + // copy occurs only on the overlapping global pos region of both containers + static copySourceOverTargetContainer(source: any, target: any) { + const adjustOverlapMargins = (overlap: Box2) => { + const margin = Math.min(target.margin, source.margin) || 0 + overlap.min.x -= target.bounds.min.x === overlap.min.x ? margin : 0 + overlap.min.y -= target.bounds.min.y === overlap.min.y ? margin : 0 + overlap.max.x += target.bounds.max.x === overlap.max.x ? margin : 0 + overlap.max.y += target.bounds.max.y === overlap.max.y ? margin : 0 + } + + if (source.bounds.intersectsBox(target.bounds)) { + const overlap = target.bounds.clone().intersect(source.bounds) + adjustOverlapMargins(overlap) + for (let { x } = overlap.min; x < overlap.max.x; x++) { + // const globalStartPos = new Vector3(x, 0, overlap.min.y) + const globalStartPos = new Vector2(x, overlap.min.y) + const targetLocalStartPos = target.toLocalPos(globalStartPos) + const sourceLocalStartPos = source.toLocalPos(globalStartPos) + let targetIndex = target.getIndex(targetLocalStartPos) + let sourceIndex = source.getIndex(sourceLocalStartPos) + for (let { y } = overlap.min; y < overlap.max.y; y++) { + const sourceVal = source.rawData[sourceIndex] + if (sourceVal) { + target.rawData[targetIndex] = sourceVal + } + sourceIndex++ + targetIndex++ + } + } + } + } + + inLocalRange(localPos: Vector2) { + return ( + localPos.x >= 0 && + localPos.x < this.dimensions.x && + localPos.y >= 0 && + localPos.y < this.dimensions.y + ) + } + + inWorldRange(globalPos: Vector2) { + return ( + globalPos.x >= this.bounds.min.x && + globalPos.x < this.bounds.max.x && + globalPos.y >= this.bounds.min.y && + globalPos.y < this.bounds.max.y + ) + } + + getIndex(localPos: Vector2) { + return localPos.x * this.dimensions.y + localPos.y + } + + getLocalPosFromIndex(index: number) { + const y = index % this.dimensions.y + const x = Math.floor(index / this.dimensions.y) + return new Vector2(x, y) + } + + // toLocalPos(pos: T): T + // toGlobalPos(pos: T): T + + toLocalPos(pos: Vector2) { + const origin = this.bounds.min.clone() + return pos.clone().sub(origin) + } + + toWorldPos(pos: Vector2) { + const origin = this.bounds.min.clone() + return origin.add(pos) + } + + containsPoint(pos: Vector2) { + return this.bounds.containsPoint(pos) + // return ( + // blockPos.x >= this.bounds.min.x && + // blockPos.z >= this.bounds.min.z && + // blockPos.x < this.bounds.max.x && + // blockPos.z < this.bounds.max.z + // ) + } + + // abstract get chunkIds(): ChunkId[] + // abstract toChunks(): any +} + +/** + * PatchesMap base class + */ +export class PatchesMapBase { + patchDimensions: Vector2 + constructor(patchDim: Vector2) { + this.patchDimensions = patchDim + } + + getPatchRange(bounds: Box2) { + const rangeMin = getPatchId(bounds.min, this.patchDimensions) + const rangeMax = patchUpperId(bounds.max, this.patchDimensions) // .addScalar(1) + const patchRange = new Box2(rangeMin, rangeMax) + return patchRange + } + + getPatchIds(bounds: Box2) { + const patchIds = [] + const patchRange = this.getPatchRange(bounds) + // iter elements on computed range + const { min, max } = patchRange + for (let { x } = min; x <= max.x; x++) { + for (let { y } = min; y <= max.y; y++) { + patchIds.push(new Vector2(x, y)) + } + } + return patchIds + } + + getRoundedBox(bbox: Box2) { + const { min, max } = this.getPatchRange(bbox) + min.multiply(this.patchDimensions) + max.multiply(this.patchDimensions) + const extBbox = new Box2(min, max) + return extBbox + } + + /** + * Merges all patches as single data container + */ + asMergedContainer() {} +} diff --git a/src/datacontainers/EntityChunk.ts b/src/datacontainers/EntityChunk.ts new file mode 100644 index 0000000..4dc0948 --- /dev/null +++ b/src/datacontainers/EntityChunk.ts @@ -0,0 +1,99 @@ +import { Box3, Vector3, Vector2 } from 'three' + +import { EntityData } from '../common/types' +import { asVect2 } from '../common/utils' +import { BlockType, WorldUtils } from '../index' +import { TreeGenerators } from '../tools/TreeGenerator' + +import { WorldChunk } from './WorldChunk' + +export type EntityChunkStub = { + box: Box3 + data: Uint16Array + entity?: EntityData +} + +const adjustChunkBox = (entityBox: Box3, chunkBox?: Box3) => { + if (chunkBox instanceof Vector3) { + const blockStart = new Vector3(chunkBox.x, entityBox.min.y, chunkBox.z) + const blockEnd = blockStart + .clone() + .add(new Vector3(1, entityBox.max.y - entityBox.min.y, 1)) + chunkBox = new Box3(blockStart, blockEnd) + } + + return chunkBox || entityBox +} + +export class EntityChunk extends WorldChunk { + entityData: EntityData + + constructor(entityData: EntityData, customChunkBox?: Box3) { + super(adjustChunkBox(entityData.bbox, customChunkBox)) + this.entityData = entityData + } + + voxelize() { + const { bbox, params, type } = this.entityData + const { size: treeSize, radius: treeRadius } = params + const entityPos = bbox.getCenter(new Vector3()) + const { min, max } = this.chunkBox + let index = 0 + for (let { z } = min; z < max.z; z++) { + for (let { x } = min; x < max.x; x++) { + for (let { y } = min; y < max.y; y++) { + const xzProj = new Vector2(x, z).sub(asVect2(entityPos)) + if (xzProj.length() > 0) { + if (y < min.y + treeSize) { + // empty space around trunk between ground and trunk top + this.chunkData[index++] = BlockType.NONE + } else { + // tree foliage + const blockType = TreeGenerators[type]( + xzProj.length(), + y - (min.y + treeSize + treeRadius), + treeRadius, + ) + this.chunkData[index++] = blockType + } + } else { + // tree trunk + this.chunkData[index++] = BlockType.TREE_TRUNK + } + } + } + } + return this.chunkData + } + + getBlocksBuffer(blockPos: Vector3) { + const { chunkBox, chunkData } = this + const chunkDims = chunkBox.getSize(new Vector3()) + const chunkLocalPos = blockPos.clone().sub(chunkBox.min) + const buffIndex = + chunkLocalPos.z * chunkDims.x * chunkDims.y + + chunkLocalPos.x * chunkDims.y + const buffer = chunkData.slice(buffIndex, buffIndex + chunkDims.y) + return buffer + } + + toStub() { + const { chunkBox, chunkData, entityData } = this + const entityChunk: EntityChunkStub = { + box: chunkBox, + data: chunkData, + entity: entityData, + } + return entityChunk + } + + static fromStub(chunkStub: EntityChunkStub) { + const entityChunkData = chunkStub.data + const entityChunkBox = WorldUtils.parseThreeStub(chunkStub.box) + const entityData = chunkStub.entity as EntityData + entityData.bbox = WorldUtils.parseThreeStub(entityData.bbox) + const entityChunk = new EntityChunk(entityData, entityChunkBox) + entityChunk.chunkData = entityChunkData + return entityChunk + } +} diff --git a/src/datacontainers/GroundPatch.ts b/src/datacontainers/GroundPatch.ts new file mode 100644 index 0000000..182cc47 --- /dev/null +++ b/src/datacontainers/GroundPatch.ts @@ -0,0 +1,287 @@ +import { Box2, Vector2, Vector3 } from 'three' + +import { Block, PatchBlock, PatchKey } from '../common/types' +import { + parsePatchKey, + parseThreeStub, + asVect3, + asVect2, + asBox2, +} from '../common/utils' +import { WorldComputeProxy, WorldConf } from '../index' +import { BlockType } from '../procgen/Biome' + +import { DataContainer } from './DataContainers' +import { EntityChunk } from './EntityChunk' +import { WorldChunk } from './WorldChunk' + +export enum BlockMode { + DEFAULT, + BOARD_CONTAINER, +} + +export type BlockData = { + level: number + type: BlockType + mode?: BlockMode +} + +export type PatchStub = { + key?: string + bounds: Box2 + rawData: Uint32Array +} + +// bits allocated per block data type +// total bits required to store a block: 9+10+3 = 22 bits +const BlockDataBitAllocation = { + level: 9, // support level values ranging from 0 to 512 + type: 10, // support up to 1024 different block types + mode: 3, // support for 8 different block mode +} + +// for debug use only +const highlightPatchBorders = (localPos: Vector3, blockType: BlockType) => { + return WorldConf.debug.patch.borderHighlightColor && + (localPos.x === 1 || localPos.z === 1) + ? WorldConf.debug.patch.borderHighlightColor + : blockType +} + +export type BlockIteratorRes = IteratorResult + +export class GroundPatch extends DataContainer { + rawData: Uint32Array + + constructor(boundsOrPatchKey: Box2 | PatchKey = new Box2(), margin = 1) { + super(boundsOrPatchKey, margin) + this.rawData = new Uint32Array(this.extendedDims.x * this.extendedDims.y) + } + + override init(bounds: Box2): void { + super.init(bounds) + this.rawData = new Uint32Array(this.extendedDims.x * this.extendedDims.y) + } + + decodeBlockData(rawData: number): BlockData { + const shift = BlockDataBitAllocation + const level = + (rawData >> (shift.type + shift.mode)) & ((1 << shift.level) - 1) // Extract 9 bits for level + const type = (rawData >> shift.mode) & ((1 << shift.type) - 1) // Extract 10 bits for type + const mode = rawData & ((1 << shift.mode) - 1) // Extract 3 bits for mode + const blockData: BlockData = { + level, + type, + mode, + } + return blockData + } + + encodeBlockData(blockData: BlockData): number { + const { level, type, mode } = blockData + const shift = BlockDataBitAllocation + let blockRawVal = level + blockRawVal = (blockRawVal << shift.type) | type + blockRawVal = (blockRawVal << shift.mode) | (mode || BlockMode.DEFAULT) + return blockRawVal + } + + readBlockData(blockIndex: number): BlockData { + const blockRawData = this.rawData[blockIndex] + const blockData = this.decodeBlockData(blockRawData as number) + return blockData + } + + writeBlockData(blockIndex: number, blockData: BlockData) { + this.rawData[blockIndex] = this.encodeBlockData(blockData) + } + + adjustRangeBox(rangeBox: Box2 | Vector2, local = false) { + rangeBox = + rangeBox instanceof Box2 ? rangeBox : new Box2(rangeBox, rangeBox) + const { min, max } = local ? this.localBox : this.bounds + const rangeMin = new Vector2( + Math.max(Math.floor(rangeBox.min.x), min.x), + Math.max(Math.floor(rangeBox.min.y), min.y), + ) + const rangeMax = new Vector2( + Math.min(Math.floor(rangeBox.max.x), max.x), + Math.min(Math.floor(rangeBox.max.y), max.y), + ) + return local + ? new Box2(rangeMin, rangeMax) + : new Box2(this.toLocalPos(rangeMin), this.toLocalPos(rangeMax)) + } + + override getIndex(localPos: Vector2 | Vector3) { + localPos = localPos instanceof Vector2 ? localPos : asVect2(localPos) + return ( + (localPos.x + this.margin) * this.extendedDims.y + + localPos.y + + this.margin + ) + } + + getBlock(inputPos: Vector2 | Vector3, isLocalPos = false) { + inputPos = inputPos instanceof Vector2 ? inputPos : asVect2(inputPos) + const isWithingRange = isLocalPos + ? this.inLocalRange(inputPos) + : this.inWorldRange(inputPos) + let block: PatchBlock | undefined + if (isWithingRange) { + const localPos = isLocalPos ? inputPos : this.toLocalPos(inputPos) + const pos = isLocalPos ? this.toWorldPos(inputPos) : inputPos + const blockIndex = this.getIndex(localPos) + const blockData = this.readBlockData(blockIndex) || BlockType.NONE + block = { + index: blockIndex, + pos: asVect3(pos, blockData.level), + localPos: asVect3(localPos, blockData.level), + data: blockData, + } + } + return block + } + + setBlock( + inputPos: Vector2 | Vector3, + blockData: BlockData, + isLocalPos = false, + ) { + inputPos = inputPos instanceof Vector2 ? inputPos : asVect2(inputPos) + const isWithingPatch = isLocalPos + ? this.inLocalRange(inputPos) + : this.inWorldRange(inputPos) + if (isWithingPatch) { + const localPos = isLocalPos ? inputPos : this.toLocalPos(inputPos) + const blockIndex = this.getIndex(localPos) + this.writeBlockData(blockIndex, blockData) + } + // const levelMax = blockLevel + blockData.over.length + // bounds.min.y = Math.min(bounds.min.y, levelMax) + // bounds.max.y = Math.max(bounds.max.y, levelMax) + } + + /** + * + * @param rangeBox iteration range as global coords + * @param skipMargin + */ + *iterBlocksQuery(rangeBox?: Box2 | Vector2, skipMargin = true) { + // convert to local coords to speed up iteration + const localBbox = rangeBox + ? this.adjustRangeBox(rangeBox) + : this.localExtendedBox + + const isMarginBlock = ({ x, y }: { x: number; y: number }) => + !rangeBox && + this.margin > 0 && + (x === localBbox.min.x || + x === localBbox.max.x - 1 || + y === localBbox.min.y || + y === localBbox.max.y - 1) + + let index = 0 + for (let { x } = localBbox.min; x < localBbox.max.x; x++) { + for (let { y } = localBbox.min; y < localBbox.max.y; y++) { + const localPos = new Vector2(x, y) + if (!skipMargin || !isMarginBlock(localPos)) { + index = rangeBox ? this.getIndex(localPos) : index + const blockData = this.readBlockData(index) || BlockType.NONE + const block: PatchBlock = { + index, + pos: asVect3(this.toWorldPos(localPos), blockData.level), + localPos: asVect3(localPos, blockData.level), + data: blockData, + } + yield block + } + index++ + } + } + } + + toStub() { + const { bounds, rawData } = this + const patchStub: PatchStub = { + bounds, + rawData, + } + if (this.key && this.key !== '') patchStub.key = this.key + return patchStub + } + + fromStub(patchStub: PatchStub) { + this.init(parseThreeStub(patchStub.bounds) as Box2) + this.id = patchStub.key ? parsePatchKey(patchStub.key) : this.id + this.rawData.set(patchStub.rawData) + this.bounds.min.y = patchStub.bounds.min.y + this.bounds.max.y = patchStub.bounds.max.y + return this + } + + async fillGroundData() { + const stub: PatchStub = await WorldComputeProxy.instance.bakeGroundPatch( + this.key || this.bounds, + ) + this.rawData.set(stub.rawData) + // this.bounds.min = min + // this.bounds.max = max + // this.bounds.getSize(this.dimensions) + } + + fillChunk(worldChunk: WorldChunk) { + const blocks = this.iterBlocksQuery(undefined, false) + for (const block of blocks) { + const blockData = block.data + const blockType = block.data.type + const blockLocalPos = block.localPos as Vector3 + blockLocalPos.x += 1 + // block.localPos.y = patch.bbox.max.y + blockLocalPos.z += 1 + blockData.type = + highlightPatchBorders(blockLocalPos, blockType) || blockType + worldChunk.writeBlock(blockLocalPos, blockData, block.buffer || []) + } + } + + // TODO rename mergeWithEntities + mergeEntityVoxels(entityChunk: EntityChunk, worldChunk: WorldChunk) { + // return overlapping blocks between entity and container + const patchBlocksIter = this.iterBlocksQuery(asBox2(entityChunk.chunkBox)) + // iter over entity blocks + for (const block of patchBlocksIter) { + // const buffer = entityChunk.data.slice(chunkBufferIndex, chunkBufferIndex + entityDims.y) + let bufferData = entityChunk.getBlocksBuffer(block.pos) + const buffOffset = entityChunk.chunkBox.min.y - block.pos.y + const buffSrc = Math.abs(Math.min(0, buffOffset)) + const buffDest = Math.max(buffOffset, 0) + bufferData = bufferData.copyWithin(buffDest, buffSrc) + bufferData = + buffOffset < 0 + ? bufferData.fill(BlockType.NONE, buffOffset) + : bufferData + block.localPos.x += 1 + block.localPos.z += 1 + worldChunk.writeBlock(block.localPos, block.data, bufferData) + } + } + + // getBlocksRow(zRowIndex: number) { + // const rowStart = zRowIndex * this.dimensions.y + // const rowEnd = rowStart + this.dimensions.x + // const rowRawData = this.rawData.slice(rowStart, rowEnd) + // return rowRawData + // } + + // getBlocksCol(xColIndex: number) { + + // } + + /** + * Split container into fixed size patches + */ + // splitAsPatchMap() { + + // } +} diff --git a/src/datacontainers/GroundPatchesMap.ts b/src/datacontainers/GroundPatchesMap.ts new file mode 100644 index 0000000..6d6f297 --- /dev/null +++ b/src/datacontainers/GroundPatchesMap.ts @@ -0,0 +1,157 @@ +import { Box2, Vector2, Vector3 } from 'three' + +import { PatchKey } from '../common/types' +import { GroundPatch, WorldComputeProxy, WorldConf } from '../index' + +import { PatchesMap } from './PatchesMap' + +const getDefaultPatchDim = () => + new Vector2(WorldConf.patchSize, WorldConf.patchSize) + +/** + * Blocks cache + */ +export class CacheContainer extends PatchesMap { + static cachePowRadius = 2 + static cacheSize = WorldConf.patchSize * 5 + // eslint-disable-next-line no-use-before-define + static singleton: CacheContainer + pendingRefresh = false + builtInCache = false // specify whether cache is managed internally or separately + + static get instance() { + this.singleton = this.singleton || new CacheContainer(getDefaultPatchDim()) + return this.singleton + } + + async populate(batch: PatchKey[]) { + this.pendingRefresh = true + const batchIter = WorldComputeProxy.instance.iterPatchCompute(batch) + // populate cache without blocking execution + for await (const patch of batchIter) { + if (patch.key) { + this.patchLookup[patch.key] = patch + this.bbox.union(patch.bounds) + } + } + this.pendingRefresh = false + } + + /** + * + * @param center + * @param dryRun + * @returns true if cache was update, false otherwise + */ + async refresh(bbox: Box2) { + //, patchMask = () => true) { + let changesDiff + if (!this.pendingRefresh) { + const emptyContainer = new PatchesMap(this.patchDimensions) + emptyContainer.init(bbox) + changesDiff = emptyContainer.compareWith(CacheContainer.instance) + const hasChanged = Object.keys(changesDiff).length > 0 + + // (!cacheCenter.equals(this.cacheCenter) || cachePatchCount === 0) + if (hasChanged) { + // backup patches that will remain in cache + const backup = this.availablePatches.filter(patch => patch) + // reinit cache + super.init(bbox) + // restore remaining patches backup + this.populateFromExisting(backup) + this.builtInCache && (await this.populate(this.missingPatchKeys)) + } + } + // return patch keys changes + return changesDiff + } + + getOverlappingPatches(inputBounds: Box2) { + const overlappingBounds = (bounds1: Box2, bounds2: Box2) => + !( + bounds1.max.x <= bounds2.min.x || + bounds1.min.x >= bounds2.max.x || + bounds1.max.y <= bounds2.min.y || + bounds1.min.y >= bounds2.max.y + ) + return this.availablePatches.filter(patch => + overlappingBounds(patch.bounds, inputBounds), + ) + } + + getNearPatches(patch: GroundPatch) { + const dim = patch.dimensions + const patchCenter = patch.bounds.getCenter(new Vector2()) + const minX = patchCenter.clone().add(new Vector3(-dim.x, 0)) + const maxX = patchCenter.clone().add(new Vector3(dim.x, 0)) + const minZ = patchCenter.clone().add(new Vector3(0, -dim.y)) + const maxZ = patchCenter.clone().add(new Vector3(0, dim.y)) + const minXminZ = patchCenter.clone().add(new Vector3(-dim.x, -dim.y)) + const minXmaxZ = patchCenter.clone().add(new Vector3(-dim.x, dim.y)) + const maxXminZ = patchCenter.clone().add(new Vector3(dim.x, -dim.y)) + const maxXmaxZ = patchCenter.clone().add(new Vector3(dim.x, dim.y)) + const neighboursCenters = [ + minX, + maxX, + minZ, + maxZ, + minXminZ, + minXmaxZ, + maxXminZ, + maxXmaxZ, + ] + const patchNeighbours: GroundPatch[] = neighboursCenters + .map(patchCenter => this.findPatch(patchCenter)) + .filter(patch => patch) as GroundPatch[] + return patchNeighbours + } + + // getGroundBlock(globalPos: Vector3) { + // const { bbox } = this + // let blockRes + // globalPos.y = bbox.getCenter(new Vector3()).y + // if (bbox.containsPoint(globalPos)) { + // const patch = this.findPatch(globalPos) + // if (patch) { + // const localPos = globalPos.clone().sub(patch.bbox.min) + // blockRes = patch.getBlock(localPos) as BlockData + // } + // } else { + // const batchRes = WorldComputeApi.instance.computeBlocksBatch([globalPos]) + // const blockRes = batchRes instanceof Promise ? batchRes.then(batchRes => batchRes[0]) : batchRes[0] + // if (!blockRes) { + // console.log(blockRes) + // } + // } + // return blockRes + // } + + // async getUpperBlock(globalPos: Vector3) { + // const block = await this.getGroundBlock(globalPos) + // if (block) { + // const blocksBuffer = (await WorldApi.instance.call( + // WorldApiName.OvergroundBufferCompute, + // [block.pos], + // )) as BlockType[] + // const lastBlockIndex = blocksBuffer.findLastIndex(elt => elt) + // if (lastBlockIndex >= 0) { + // block.pos.y += lastBlockIndex + // block.type = blocksBuffer[lastBlockIndex] as BlockType + // } + // } + // return block + // } + + // setBlock(globalPos: Vector3, block: BlockData) { + // // find patch containing point in cache + // const patch = this.findPatch(globalPos) + // if (patch) { + // const localPos = globalPos.clone().sub(patch.bbox.min) + // patch.setBlock(localPos, block.type) + // } else { + // console.log(globalPos) + // } + // return block + // } +} diff --git a/src/datacontainers/PatchesMap.ts b/src/datacontainers/PatchesMap.ts new file mode 100644 index 0000000..92181e7 --- /dev/null +++ b/src/datacontainers/PatchesMap.ts @@ -0,0 +1,184 @@ +import { Box2, Vector2 } from 'three' + +import { PatchKey } from '../common/types' + +import { DataContainer, PatchesMapBase } from './DataContainers' + +/** + * Finite map made from patch aggregation + */ +export class PatchesMap> extends PatchesMapBase { + bbox: Box2 = new Box2() + patchLookup: Record = {} + + init( + bbox: Box2, + // patchBboxFilter = (patchBbox: Box3) => patchBbox, + ) { + this.bbox = bbox + this.patchLookup = {} + const { min, max } = this.getPatchRange() + for (let { x } = min; x < max.x; x++) { + for (let { y } = min; y < max.y; y++) { + const patchKey = `${x}:${y}` + // if (patchBboxFilter(patchBox)) { + this.patchLookup[patchKey] = null + // } + } + } + } + + override getPatchRange() { + return super.getPatchRange(this.bbox) + } + + override getRoundedBox() { + return super.getRoundedBox(this.bbox) + } + + get count() { + return Object.keys(this.patchLookup).length + } + + get patchKeys() { + return Object.keys(this.patchLookup) + } + + get availablePatches() { + return Object.values(this.patchLookup).filter(val => val) as T[] + } + + get missingPatchKeys() { + return Object.keys(this.patchLookup).filter( + key => !this.patchLookup[key], + ) as PatchKey[] + } + + // autoFill(fillingVal=0){ + // this.patchKeys.forEach(key=>this.patchLookup[key] = new GroundPatch(key)) + // this.availablePatches.forEach(patch=>patch.iterOverBlocks) + // } + + populateFromExisting(patches: T[], cloneObjects = false) { + // const { min, max } = this.bbox + patches + .filter(patch => this.patchLookup[patch.key] !== undefined) + .forEach(patch => { + this.patchLookup[patch.key] = cloneObjects + ? patch // (patch.duplicate() as T) + : patch + // min.y = Math.min(patch.bbox.min.y, min.y) + // max.y = Math.max(patch.bbox.max.y, max.y) + }) + } + + compareWith(otherContainer: PatchesMap) { + const patchKeysDiff: Record = {} + // added keys e.g. keys in current container but not found in other + Object.keys(this.patchLookup) + .filter(patchKey => otherContainer.patchLookup[patchKey] === undefined) + .forEach(patchKey => (patchKeysDiff[patchKey] = true)) + // missing keys e.g. found in other container but not in current + Object.keys(otherContainer.patchLookup) + .filter(patchKey => this.patchLookup[patchKey] === undefined) + .forEach(patchKey => (patchKeysDiff[patchKey] = false)) + return patchKeysDiff + } + + findPatch(blockPos: Vector2) { + const res = this.availablePatches.find(patch => + patch.containsPoint(blockPos), + ) + return res + } + + // getBlock(blockPos: Vector3) { + // return this.findPatch(blockPos)?.getBlock(blockPos, false) + // } + + // getAllPatchesEntities(skipDuplicate = true) { + // const entities: EntityData[] = [] + // for (const patch of this.availablePatches) { + // patch.entities.forEach(entity => { + // if (!skipDuplicate || !entities.find(ent => ent.bbox.equals(entity.bbox))) { + // entities.push(entity) + // } + // }) + // } + // return entities + // } + + // *iterAllPatchesBlocks() { + // for (const patch of this.availablePatches) { + // const blocks = patch.iterOverBlocks(undefined, false, false) + // for (const block of blocks) { + // yield block + // } + // } + // } + + // getMergedRows(zRowIndex: number) { + // const sortedPatchesRows = this.availablePatches + // .filter( + // patch => zRowIndex >= patch.bbox.min.z && zRowIndex <= patch.bbox.min.z, + // ) + // .sort((p1, p2) => p1.bbox.min.x - p2.bbox.min.x) + // .map(patch => patch.getBlocksRow(zRowIndex)) + // const mergedRows = sortedPatchesRows.reduce((arr1, arr2) => { + // const mergedArray = new Uint32Array(arr1.length + arr2.length) + // mergedArray.set(arr1) + // mergedArray.set(arr2, arr1.length) + // return mergedArray + // }) + // return mergedRows + // } + + // iterMergedRows() { + // const { min, max } = this.patchRange + // for (let zPatchIndex = min.z; zPatchIndex <= max.z; zPatchIndex++) { + // for (let zRowIndex = min.z; zRowIndex < max.z; zRowIndex++) {} + // } + // } + + // getMergedCols(xColIndex: number) { + + // } + + // mergedLinesIteration() { + // const { min, max } = this.bbox + // for (let x = min.x; x < max.x; x++) { + // for (let z = min.z; z < max.z; z++) { + + // } + // } + // } + + // toMergedContainer() { + // const mergedBox = this.availablePatches.map(patch => patch.bbox) + // .reduce((merge, bbox) => merge.union(bbox), new Box3()) + // // const mergedContainer = + // } + + // static fromMergedContainer() { + + // } + // mergeBlocks(blocksContainer: BlocksContainer) { + // // // for each patch override with blocks from blocks container + // this.availablePatches.forEach(patch => { + // const blocksIter = patch.iterOverBlocks(blocksContainer.bbox) + // for (const target_block of blocksIter) { + // const source_block = blocksContainer.getBlock(target_block.pos, false) + // if (source_block && source_block.pos.y > 0 && target_block.index) { + // let block_type = source_block.type ? BlockType.SAND : BlockType.NONE + // block_type = + // source_block.type === BlockType.TREE_TRUNK + // ? BlockType.TREE_TRUNK + // : block_type + // const block_level = blocksContainer.bbox.min.y // source_block?.pos.y + // patch.writeBlock(target_block.index, block_level, block_type) + // // console.log(source_block?.pos.y) + // } + // } + // }) + // } +} diff --git a/src/datacontainers/RandomDistributionMap.ts b/src/datacontainers/RandomDistributionMap.ts new file mode 100644 index 0000000..4c24d9f --- /dev/null +++ b/src/datacontainers/RandomDistributionMap.ts @@ -0,0 +1,151 @@ +import alea from 'alea' +import { Box2, Vector2 } from 'three' + +import { ProcLayer } from '../procgen/ProcLayer' +import { BlueNoisePattern } from '../procgen/BlueNoisePattern' +import { EntityData } from '../common/types' +import { WorldConf } from '../index' + +import { PatchesMapBase } from './DataContainers' +// import { Adjacent2dPos } from '../common/types' +// import { getAdjacent2dCoords } from '../common/utils' + +const probabilityThreshold = Math.pow(2, 8) +const bmin = new Vector2(0, 0) +const bmax = new Vector2( + WorldConf.defaultDistMapPeriod, + WorldConf.defaultDistMapPeriod, +) +const distMapDefaultBox = new Box2(bmin, bmax) +const distMapDefaults = { + aleaSeed: 'treeMap', + minDistance: 8, + maxDistance: 100, + tries: 20, +} + +/** + * Infinite map using repeatable seamless pattern to provide + * independant, deterministic and approximated random distribution + * Enable querying/iterating randomly distributed items at block + * level or from custom box range + */ +export class PseudoDistributionMap extends PatchesMapBase { + repeatedPattern: BlueNoisePattern + densityMap: ProcLayer + + constructor( + bbox: Box2 = distMapDefaultBox, + distParams: any = distMapDefaults, + ) { + super(bbox.getSize(new Vector2())) + this.repeatedPattern = new BlueNoisePattern(bbox, distParams) + this.densityMap = new ProcLayer(distParams.aleaSeed || '') + } + + spawnProbabilityEval(pos: Vector2) { + const maxCount = 1 // 16 * Math.round(Math.exp(10)) + const val = this.densityMap?.eval(pos) + const adjustedVal = val + ? (16 * Math.round(Math.exp((1 - val) * 10))) / maxCount + : 0 + return adjustedVal + } + + hasSpawned(itemPos: Vector2, spawnProbabilty?: number) { + // eval spawn probability at entity center + spawnProbabilty = + spawnProbabilty && !isNaN(spawnProbabilty) + ? spawnProbabilty + : this.spawnProbabilityEval(itemPos) + const itemId = itemPos.x + ':' + itemPos.y + const prng = alea(itemId) + const hasSpawned = prng() * spawnProbabilty < probabilityThreshold + return hasSpawned + } + + /** + * + * @param entityShaper + * @param inputPointOrArea either test point or range box + * @param spawnProbabilityOverride + * @returns all locations from which entity contains input point or overlaps with range box + */ + querySpawnLocations( + testRange: Vector2 | Box2, + overlapsTest: (testRange: Box2, entityPos: Vector2) => boolean, + spawnProbabilityOverride?: (entityPos?: Vector2) => number, + // entityMask = (_entity: EntityData) => false + ) { + const testBox = + testRange instanceof Box2 + ? testRange + : new Box2().setFromPoints([testRange]) + // const offset = testBox.min.clone().divide(this.patchDimensions).floor().multiply(this.patchDimensions) + // const localTestBox = testBox.clone().translate(offset.clone().negate()) + // const overlappingEntities = this.repeatedPattern.elements + // .filter(entityPos => overlapsTest(localTestBox, entityPos)) + // .map(relativePos => relativePos.clone().add(offset)) + const overlappingEntities: Vector2[] = [] + const patchIds = this.getPatchIds(testBox) + for (const patchId of patchIds) { + const offset = patchId.clone().multiply(this.patchDimensions) + const localTestBox = testBox.clone().translate(offset.clone().negate()) + // look for entities overlapping with input point or area + for (const relativePos of this.repeatedPattern.elements) { + if (overlapsTest(localTestBox, relativePos)) { + const entityPos = relativePos.clone().add(offset) + overlappingEntities.push(entityPos) + } + } + } + const spawnedEntities = overlappingEntities.filter(entityPos => + this.hasSpawned(entityPos, spawnProbabilityOverride?.(entityPos)), + ) + return spawnedEntities + } + + // /** + // * Randomly spawn entites according to custom distribution + // */ + // static spawnEntity(pos: Vector2) { + // // return Math.sin(0.01 * pos.x * pos.y) > 0.99 + // const offset = 10 + // return pos.x % 20 === offset && pos.y % 20 === offset + // } +} + +/** + * Storing entities at biome level with overlap at biomes' transitions + */ +export class OverlappingEntitiesMap { + // extends RandomDistributionMap { + // entities stored per biome + static biomeMapsLookup: Record = {} + // getAdjacentEntities() { + // const adjacentEntities = [] + // const adjacentKeys = Object.values(Adjacent2dPos) + // .filter(v => !isNaN(Number(v)) && v !== Adjacent2dPos.center) + // .map(adjKey => { + // const adjCoords = getAdjacent2dCoords(patchCoords, adjKey as Adjacent2dPos) + // const mapKey = `map_${adjCoords.x % repeatPeriod}_${adjCoords.y % repeatPeriod}` + // return mapKey + // }) + // const adjacentMaps = adjacentKeys.map(mapKey => RandomDistributionMap.mapsLookup[mapKey]) + // return adjacentEntities + // } + + // Gen all entities belonging to specific biome + // populate(blockPos: Vector3) { + // // find biome at given block pos + // // discover biome extent + // // generate entities over all biome + // } + + // override *iterate(input: Box3 | Vector3) { + // // find if biome cached entities exists for given block or patch + // // if not populate biomes cache with entities + // // if block or patch contained withing unique biome, return matching entities + // // else if overlapping across several biomes, compute transition + // } +} diff --git a/src/datacontainers/WorldChunk.ts b/src/datacontainers/WorldChunk.ts new file mode 100644 index 0000000..31acd87 --- /dev/null +++ b/src/datacontainers/WorldChunk.ts @@ -0,0 +1,77 @@ +import { Box3, MathUtils, Vector3 } from 'three' + +import { ChunkKey } from '../common/types' +import { ChunkFactory } from '../index' + +import { BlockData, BlockMode } from './GroundPatch' + +export type ChunkDataContainer = { + box: Box3 + data: Uint16Array +} + +export type WorldChunkStub = { + key: ChunkKey + data: Uint16Array | null +} + +export class WorldChunk { + chunkBox: Box3 + chunkData: Uint16Array + + constructor(chunkBox: Box3) { + this.chunkBox = chunkBox + const chunkDims = chunkBox.getSize(new Vector3()) + this.chunkData = new Uint16Array(chunkDims.x * chunkDims.y * chunkDims.z) + } + + writeBlock( + blockLocalPos: Vector3, + blockData: BlockData, + bufferOver: Uint16Array | [], + ) { + const { chunkBox, chunkData } = this + const chunk_size = chunkBox.getSize(new Vector3()).x // Math.round(Math.pow(chunkData.length, 1 / 3)) + + let written_blocks_count = 0 + + const level = MathUtils.clamp( + blockLocalPos.y + bufferOver.length, + chunkBox.min.y, + chunkBox.max.y, + ) + let buff_index = Math.max(level - blockLocalPos.y, 0) + let h = level - chunkBox.min.y // local height + // debug_mode && is_edge(local_pos.z, local_pos.x, h, patch_size - 2) + // ? BlockType.SAND + // : block_cache.type + let depth = 0 + while (h >= 0) { + const blocksIndex = + blockLocalPos.z * Math.pow(chunk_size, 2) + + h * chunk_size + + blockLocalPos.x + const blockType = buff_index > 0 ? bufferOver[buff_index] : blockData.type + const skip = + buff_index > 0 && + chunkData[blocksIndex] !== undefined && + !bufferOver[buff_index] + if (!skip && blockType !== undefined) { + // #hack: disable block mode below ground to remove checkerboard excess + const skipBlockMode = + depth > 0 && + (bufferOver.length === 0 || bufferOver[buff_index] || buff_index < 0) + const blockMode = skipBlockMode ? BlockMode.DEFAULT : blockData.mode + chunkData[blocksIndex] = ChunkFactory.defaultInstance.voxelDataEncoder( + blockType, + blockMode, + ) + blockType && written_blocks_count++ + } + h-- + buff_index-- + depth++ + } + return written_blocks_count + } +} diff --git a/src/feats/BoardContainer.ts b/src/feats/BoardContainer.ts new file mode 100644 index 0000000..5021df4 --- /dev/null +++ b/src/feats/BoardContainer.ts @@ -0,0 +1,400 @@ +import { Box2, Vector2, Vector3, Vector3Like } from 'three' + +import { Block, EntityData, PatchBlock } from '../common/types' +import { asVect2, asVect3 } from '../common/utils' +import { + BlockType, + DataContainer, + GroundPatch, + ProcLayer, + WorldComputeProxy, +} from '../index' +import { PseudoDistributionMap } from '../datacontainers/RandomDistributionMap' +import { findBoundingBox } from '../common/math' +import { BlockMode, PatchStub } from '../datacontainers/GroundPatch' + +export enum BlockCategory { + FLAT = 0, + HOLE = 1, + OBSTACLE = 2, +} + +export type BoardBlock = { + type: BlockType + category: BlockCategory +} + +export type BoardOutputData = { + origin: Vector3Like + size: Vector2 + data: BoardBlock[] +} + +export type BoardInputParams = { + radius: number + thickness: number +} + +export type BoardInput = BoardInputParams & { center: Vector3 } + +// map block type to board block type +const blockTypeCategoryMapper = (blockType: BlockType) => { + switch (blockType) { + case BlockType.TREE_TRUNK: + return BlockCategory.OBSTACLE + case BlockType.BOARD_HOLE: + return BlockCategory.HOLE + default: + return BlockCategory.FLAT + } +} + +export type BoardStub = PatchStub & { + input: BoardInput + output: BoardOutputData +} + +/** + * Board entities distribution conf + */ +// Start positions +// const startPosDistParams = { +// aleaSeed: 'boardStartPos', +// minDistance: 10, +// maxDistance: 16, +// tries: 20, +// } + +// Holes +const holesDistParams = { + aleaSeed: 'boardHoles', + minDistance: 10, + maxDistance: 16, + tries: 20, +} +/** + * Building steps + * - compute initial bounds from input + * - fill with ground blocks + * - override original blocks and adjust external bounds + * - add board entities (trimmed trees, holes) + * + * Board data export format: + * - bounds + * - data as array of block's type + */ +export class BoardContainer extends GroundPatch { + // static prevContainerBounds: Box2 | undefined + // static singleton: BoardContainer + // static get instance(){ + // return this.singleton + // } + static holesDistribution = new PseudoDistributionMap( + undefined, + holesDistParams, + ) + + static holesMapDistribution = new ProcLayer('holesMap') + // static startPosDistribution = new PseudoDistributionMap(undefined, startPosDistParams) + + // board input params + input: BoardInput = { + center: new Vector3(), + radius: 0, + thickness: 0, + } + + // board data output + output: BoardOutputData & { overridingContainer: GroundPatch | undefined } = { + origin: new Vector3(), + size: new Vector2(), + data: [], + overridingContainer: undefined, + } + + entities: { + obstacles: EntityData[] + holes: EntityData[] + } = { + obstacles: [], + holes: [], + } + // swapContainer!: GroundPatch //Uint32Array + + /** + * + * @param center + * @param radius + * @param previousBounds // used for handling previous board removal + * @returns + */ + static getInitialBounds = ( + center: Vector3, + radius: number, + previousBounds?: Box2, + ) => { + // const previousBounds = BoardContainer.prevContainerBounds + const defaultBounds = new Box2().setFromCenterAndSize( + asVect2(center), + new Vector2(radius, radius).multiplyScalar(2), + ) + return previousBounds ? defaultBounds.union(previousBounds) : defaultBounds + } + + constructor( + boardCenter = new Vector3(), + boardParams?: BoardInputParams, + lastBoardBounds?: Box2, + ) { + super( + BoardContainer.getInitialBounds( + boardCenter, + boardParams?.radius || 0, + lastBoardBounds, + ), + ) + const { input } = this + input.center = boardCenter.clone().floor() + input.radius = boardParams?.radius || input.radius + input.thickness = boardParams?.thickness || input.thickness + + BoardContainer.holesMapDistribution.sampling.periodicity = 0.25 + } + + get overridingContainer() { + if (!this.output.overridingContainer) { + const overridingContainer = this.shapeBoard() + // const boardEntitiesBlocks: Block[] = [] + const obstacles: Block[] = this.trimTrees(overridingContainer) + const holes: Block[] = this.getHolesAreasBis( + overridingContainer, + obstacles, + ) + holes.forEach(block => this.digGroundHole(block, overridingContainer)) + this.output.origin = asVect3( + overridingContainer.bounds.min, + this.input.center.y, + ) + overridingContainer.bounds.getSize(this.output.size) + this.output.overridingContainer = overridingContainer + DataContainer.copySourceOverTargetContainer(overridingContainer, this) + } + return this.output.overridingContainer + } + + async retrieveBoardData() { + await this.fillGroundData() + // populate external entities (trees) from world-compute + const trees = await WorldComputeProxy.instance.queryEntities(this.bounds) + // query local entities (holes) + // const holes = this.queryLocalEntities(boardContainer, BoardContainer.holesDistribution) + this.entities.obstacles.push(...trees) + } + + isWithinBoard(blockPos: Vector3) { + let isInsideBoard = false + const { thickness, radius, center } = this.input + if (blockPos) { + const heightDiff = Math.abs(blockPos.y - center.y) + const dist = asVect2(blockPos).distanceTo(asVect2(center)) + isInsideBoard = dist <= radius && heightDiff <= thickness + } + return isInsideBoard + } + + isOverlappingWithBoard = (bounds: Box2) => { + const testedBlocks = this.iterBlocksQuery(bounds) + for (const block of testedBlocks) { + if (this.isWithinBoard(block.pos)) { + return true + } + } + return false + }; + + *iterBoardBlock() { + const blocks = this.iterBlocksQuery(undefined, true) + // const blocks = this.iterPatchesBlocks() + for (const block of blocks) { + // discard blocks not included in board shape + if (this.isWithinBoard(block.pos)) { + yield block + } + } + } + + /** + * Override original ground blocks with board blocks + * and adjust final board bounds + * @returns + */ + shapeBoard() { + const { center } = this.input + // const { ymin, ymax } = this.getMinMax() + // const avg = Math.round(ymin + (ymax - ymin) / 2) + const tempContainer = new GroundPatch(this.bounds) + const finalBounds = new Box2(asVect2(center), asVect2(center)) + const boardBlocks = this.iterBlocksQuery(undefined, false) + // const boardBlocks = this.iterBoardBlock() + for (const block of boardBlocks) { + // tempContainer.setBlock(boardBlock.pos, boardBlock.data, false) + if (this.isWithinBoard(block.pos)) { + block.data.mode = BlockMode.BOARD_CONTAINER + block.data.level = center.y + // override block data + tempContainer.writeBlockData(block.index, block.data) + finalBounds.expandByPoint(asVect2(block.pos)) + } + } + + // copy content over final container + const bounds = new Box2(asVect2(center), asVect2(center)) + bounds.expandByVector(new Vector2(1, 1).multiplyScalar(10)) + const finalBoardContainer = new GroundPatch(finalBounds) + DataContainer.copySourceOverTargetContainer( + tempContainer, + finalBoardContainer, + ) + return finalBoardContainer + } + + // perform local query + queryLocalEntities( + boardContainer: GroundPatch, + distMap: PseudoDistributionMap, + entityRadius = 2, + ) { + const intersectsEntity = (testRange: Box2, entityPos: Vector2) => + testRange.distanceToPoint(entityPos) <= entityRadius + const spawnLocs = distMap.querySpawnLocations( + boardContainer.bounds, + intersectsEntity, + () => 1, + ) + const entities = spawnLocs + .map(loc => { + const startPos = asVect3(loc) + const block = boardContainer.getBlock(startPos) + return block + }) + .filter(block => block && this.isWithinBoard(block.pos)) as PatchBlock[] + // TODO prune entities spawning over existing entities + return entities + } + + isGroundHole(testPos: Vector3) { + return BoardContainer.holesMapDistribution.eval(testPos) < 0.15 + } + + digGroundHole(holeBlock: Block, boardContainer: GroundPatch) { + holeBlock.data.type = BlockType.BOARD_HOLE + holeBlock.data.level -= 1 // dig hole in the ground + holeBlock.data.mode = BlockMode.DEFAULT + boardContainer.setBlock(holeBlock.pos, holeBlock.data) + } + + getHolesMonoBlocks(boardContainer: GroundPatch) { + const holesSingleBlocks = this.queryLocalEntities( + boardContainer, + BoardContainer.holesDistribution, + ).map(({ pos, data }) => ({ pos, data })) + return holesSingleBlocks + } + + getHolesAreas(boardContainer: GroundPatch, forbiddenBlocks: Block[]) { + const forbiddenPos = forbiddenBlocks.map(({ pos }) => asVect2(pos)) + const holesMono = this.queryLocalEntities( + boardContainer, + BoardContainer.holesDistribution, + ) + const holesMulti: PatchBlock[] = [] + // for each monoblock hole, find maximum bounding box around + holesMono.forEach(hole => { + const pos = asVect2(hole.pos) + const holeBounds = findBoundingBox( + pos, + forbiddenPos, + boardContainer.bounds, + ) + const holeBlocks = boardContainer.iterBlocksQuery(holeBounds) + for (const block of holeBlocks) { + holesMulti.push(block) + } + }) + return holesMulti.map(({ pos, data }) => ({ pos, data }) as Block) + } + + getHolesAreasBis(boardContainer: GroundPatch, forbiddenBlocks: Block[]) { + // prevent holes from spreading over forbidden blocks + const isForbiddenPos = (testPos: Vector3) => + !!forbiddenBlocks.find(block => block.pos.equals(testPos)) + const blocks = boardContainer.iterBlocksQuery() + const holes: Block[] = [] + for (const block of blocks) { + const testPos = block.pos + if ( + this.isWithinBoard(testPos) && + this.isGroundHole(testPos) && + !isForbiddenPos(testPos) + ) { + holes.push(block) + } + } + return holes.map(({ pos, data }) => ({ pos, data }) as Block) + } + + trimTrees(boardContainer: GroundPatch) { + const trunks = this.entities.obstacles + .map(entity => { + const entityCenter = entity.bbox.getCenter(new Vector3()) + const entityCenterBlock = boardContainer.getBlock(entityCenter) + entityCenter.y = entity.bbox.min.y + return entityCenterBlock + }) + .filter( + trunkBlock => trunkBlock && this.isWithinBoard(trunkBlock.pos), + ) as PatchBlock[] + + trunks.forEach(trunkBlock => { + trunkBlock.data.type = BlockType.TREE_TRUNK + trunkBlock.data.mode = BlockMode.DEFAULT + trunkBlock.data.level += 1 + boardContainer.setBlock(trunkBlock.pos, trunkBlock.data) + }) + return trunks.map(({ pos, data }) => ({ pos, data }) as Block) + } + + override fromStub(boardStub: BoardStub) { + super.fromStub(boardStub) + const { input, output } = boardStub + this.input = input + this.output = { ...output, overridingContainer: undefined } + return this + } + + override toStub(): BoardStub { + const { input, output } = this + const boardStub: BoardStub = { + ...super.toStub(), + input, + output, + } + return boardStub + } + + exportBoardData() { + const boardBlocks = this.overridingContainer.iterBlocksQuery() + for (const block of boardBlocks) { + const blockType = block.data.type + const blockCat = blockTypeCategoryMapper(blockType) + const boardBlock: BoardBlock = { + type: blockType, + category: blockCat, + } + this.output.data.push(boardBlock) + } + const { origin, size, data } = this.output + const boardOutputData: BoardOutputData = { origin, size, data } + return boardOutputData + } +} diff --git a/src/index.ts b/src/index.ts index cd0a36a..1d81e0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,20 @@ +export { Biome, BlockType } from './procgen/Biome' +export { WorldConf } from './misc/WorldConfig' export { Heightmap } from './procgen/Heightmap' export { NoiseSampler } from './procgen/NoiseSampler' export { ProcLayer } from './procgen/ProcLayer' -export { BlocksPatch } from './world/WorldPatch' -export { Biome, BlockType } from './procgen/Biome' -export { EntitiesMap, RepeatableEntitiesMap } from './procgen/EntitiesMap' +export { PseudoDistributionMap } from './datacontainers/RandomDistributionMap' +export { BoardContainer } from './feats/BoardContainer' export { EntityType } from './common/types' -export { WorldApi, WorldWorkerApi, WorldApiName } from './world/WorldApi' -export { WorldCache } from './world/WorldCache' -export { WorldCompute } from './world/WorldCompute' +export { BlockMode, GroundPatch } from './datacontainers/GroundPatch' +export { PatchesMap } from './datacontainers/PatchesMap' +export { CacheContainer as WorldCacheContainer } from './datacontainers/GroundPatchesMap' +export { ChunkFactory } from './tools/ChunkFactory' +export { WorldComputeProxy } from './api/WorldComputeProxy' +export { DataContainer } from './datacontainers/DataContainers' + +export * as WorldCompute from './api/world-compute' +export * as WorldUtils from './common/utils' // export type { MappingConf, MappingData, MappingRanges } from "./common/types" // export { DevHacks } from './tools/DevHacks' diff --git a/src/misc/WorldConfig.ts b/src/misc/WorldConfig.ts new file mode 100644 index 0000000..d7ee1e1 --- /dev/null +++ b/src/misc/WorldConfig.ts @@ -0,0 +1,25 @@ +import { BlockType } from '../index' + +export class WorldConf { + static patchPowSize = 6 // as a power of two + static get patchSize() { + return Math.pow(2, this.patchPowSize) + } + + // max cache radius as a power of two + static cachePowLimit = 2 // 4 => 16 patches radius + static get cacheLimit() { + return Math.pow(2, this.cachePowLimit) + } + + static defaultDistMapPeriod = 4 * WorldConf.patchSize + static debug = { + patch: { + borderHighlightColor: BlockType.NONE, + }, + board: { + startPosHighlightColor: BlockType.NONE, + splitSidesColoring: false, + }, + } +} diff --git a/src/procgen/Biome.ts b/src/procgen/Biome.ts index 57a3623..98f23ca 100644 --- a/src/procgen/Biome.ts +++ b/src/procgen/Biome.ts @@ -25,6 +25,12 @@ export enum BlockType { MUD, ROCK, SNOW, + BOARD_HOLE, + DBG_LIGHT, + DBG_DARK, + DBG_PURPLE, + DBG_ORANGE, + DBG_GREEN, } export enum BiomeType { diff --git a/src/procgen/BlueNoisePattern.ts b/src/procgen/BlueNoisePattern.ts new file mode 100644 index 0000000..e1b0a0c --- /dev/null +++ b/src/procgen/BlueNoisePattern.ts @@ -0,0 +1,84 @@ +import alea from 'alea' +import PoissonDiskSampling from 'poisson-disk-sampling' +import { Box2, Vector2 } from 'three' + +/** + * Self repeating seamless pattern + */ +export class BlueNoisePattern { + bbox: Box2 + params + elements: Vector2[] = [] + + constructor(bbox: Box2, distParams: any) { + this.bbox = bbox + this.params = distParams + this.populate() + } + + get dimensions() { + return this.bbox.getSize(new Vector2()) + } + + // populate with discrete elements using relative pos + populate() { + const { dimensions, params } = this + const { aleaSeed } = this.params + const prng = alea(aleaSeed || '') + const p = new PoissonDiskSampling( + { + shape: [dimensions.x, dimensions.y], + ...params, + }, + prng, + ) + this.elements = p + .fill() + .map(point => new Vector2(point[0] as number, point[1] as number).round()) + this.makeSeamless() + } + + // make seamless repeatable pattern + makeSeamless() { + const { dimensions, params } = this + const radius = params.minDistance / 2 + const edgePoints = this.elements + .map(point => { + const pointCopy = point.clone() + if (point.x - radius < 0) { + pointCopy.x += dimensions.x + } else if (point.x + radius > dimensions.x) { + pointCopy.x -= dimensions.x + } + if (point.y - radius < 0) { + pointCopy.y += dimensions.y + } else if (point.y + radius > dimensions.y) { + pointCopy.y -= dimensions.y + } + return pointCopy.round().equals(point) ? null : pointCopy + }) + .filter(pointCopy => pointCopy) + edgePoints.forEach(edgePoint => edgePoint && this.elements.push(edgePoint)) + } + + getPatchOrigin(patchId: Vector2) { + return patchId.clone().multiply(this.dimensions) + } + + toPatchLocalPos(pos: Vector2, patchId: Vector2) { + return pos.clone().sub(this.getPatchOrigin(patchId)) + } + + toPatchWorldPos(relativePos: Vector2, patchId: Vector2) { + return relativePos.clone().add(this.getPatchOrigin(patchId)) + } + + // DO NOT USE SLOW + *iterPatchElements(patchOffset: Vector2) { + // relative to global pos conv + for (const relativePos of this.elements) { + const pos = this.toPatchWorldPos(relativePos, patchOffset) + yield pos + } + } +} diff --git a/src/procgen/EntitiesMap.ts b/src/procgen/EntitiesMap.ts deleted file mode 100644 index d39f07f..0000000 --- a/src/procgen/EntitiesMap.ts +++ /dev/null @@ -1,320 +0,0 @@ -import PoissonDiskSampling from 'poisson-disk-sampling' -import alea from 'alea' -import { Box3, Vector2, Vector3 } from 'three' - -import { TreeGenerators } from '../tools/TreeGenerator' -import { EntityType } from '../index' -import { BlocksPatch } from '../world/WorldPatch' - -import { ProcLayer } from './ProcLayer' -import { BlockType } from './Biome' -// import { Adjacent2dPos } from '../common/types' -// import { getAdjacent2dCoords } from '../common/utils' - -export type EntityData = { - // xzProj: number - level: number - type: EntityType - bbox: Box3 - edgesOverlaps?: any - params: { - radius: 5 - size: 10 - } -} - -const probabilityThreshold = Math.pow(2, 8) - -/** - * Common interface for querying/iterating entities at block or patch level - * Custom implementation left to child class - */ -export class EntitiesMap { - static density = new ProcLayer('treemap') - - probabilityEval(pos: Vector3) { - const maxCount = 1 // 16 * Math.round(Math.exp(10)) - const val = EntitiesMap.density?.eval(pos) - const adjustedVal = val - ? (16 * Math.round(Math.exp((1 - val) * 10))) / maxCount - : 0 - return adjustedVal - } - - mergeBuffers(srcBuffer: BlockType[], dstBuffer: BlockType[]) { - // console.log(`merging buffers: `, srcBuffer, dstBuffer) - const merged = [] - srcBuffer.reverse() - dstBuffer.reverse() - while (srcBuffer.length || dstBuffer.length) { - const val = srcBuffer.pop() || dstBuffer.pop() - merged.push(val) - } - // console.log(`result: `, merged) - return merged - } - - /** - * all entities belonging or overlapping with given box area - * or all entities found at given block position - * @param patchCoords - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - *iterate(_input: Box3 | Vector3) { - const entities: EntityData[] = [] - // return entities in patch local coords - for (const entity of entities) { - yield entity - } - } - - /** - * Using precached tree data and block level to fill tree blocks buffer - * @param treeData - * @param blockLevel - * @param treeParams - * @returns - */ - static fillBlockBuffer( - blockPos: Vector3, - entity: EntityData, - buffer: BlockType[], - ) { - // const { treeRadius, treeSize } = entity.params - const treeRadius = 5 - const treeSize = 10 - const entityPos = entity.bbox.getCenter(new Vector3()) - entityPos.y = entity.bbox.min.y - const treeBuffer: BlockType[] = [] - const vDiff = blockPos.clone().sub(entityPos) - const offset = vDiff.y - const count = treeSize - offset - vDiff.y = 0 - const xzProj = vDiff.length() - if (xzProj && count > 0) { - // fill tree base - new Array(count) - .fill(BlockType.NONE) - .forEach(item => treeBuffer.push(item)) - // tree foliage - for (let y = -treeRadius; y < treeRadius; y++) { - const blockType = TreeGenerators[entity.type as EntityType]( - xzProj, - y, - treeRadius, - ) - treeBuffer.push(blockType) - } - } else { - try { - // a bit of an hack for now => TODO: find good fix - new Array(count + treeRadius - Math.floor(treeSize * 0.4)) - .fill(BlockType.TREE_TRUNK) - .forEach(item => treeBuffer.push(item)) - } catch { - // console.log(error) - } - } - const sum = treeBuffer.reduce((sum, val) => sum + val, 0) - if (sum > 0) { - treeBuffer.forEach((elt, i) => { - const current = buffer[i] - if (current !== undefined) { - buffer[i] = !buffer[i] ? elt : current - } else { - buffer.push(elt) - } - }) - } - - return sum > 0 ? treeBuffer : [] - } - - /** - * Randomly spawn entites according to custom distribution - */ - static spawnEntity(pos: Vector2) { - // return Math.sin(0.01 * pos.x * pos.y) > 0.99 - const offset = 10 - return pos.x % 20 === offset && pos.y % 20 === offset - } -} - -/** - * Storing entities on repeatable seamless pattern - */ -export class RepeatableEntitiesMap extends EntitiesMap { - // eslint-disable-next-line no-use-before-define - static singleton: RepeatableEntitiesMap - static get instance() { - RepeatableEntitiesMap.singleton = - RepeatableEntitiesMap.singleton || new RepeatableEntitiesMap() - return RepeatableEntitiesMap.singleton - } - - // entities stored in pattern - entities: EntityData[] = [] - // period of repeated pattern - period - constructor() { - super() - this.period = BlocksPatch.patchSize * 2 - } - - // one time init of repeatable pattern - populate() { - const { period } = this - const prng = alea('poisson_disk_sampling') - const p = new PoissonDiskSampling( - { - shape: [period, period], - minDistance: 8, - maxDistance: 100, - tries: 20, - // distanceFunction: function (p) { - // return getImagePixelValueSomehow(p[0], p[1]); // value between 0 and 1 - // } - }, - prng, - ) - const points = p.fill() - this.entities = points.map(point => { - const mapPos = new Vector3( - Math.round(point[0] as number), - 0, - Math.round(point[1] as number), - ) - // const mapKey = `map_${Math.floor(pos.x / patchSize)}_${Math.floor(pos.y / patchSize)}` - // const localPos = new Vector3(pos.x % patchSize, 0, pos.y % patchSize) - const dimensions = new Vector3(10, 0, 10) - const bbox = new Box3().setFromCenterAndSize(mapPos, dimensions) - const type = EntityType.NONE - const entity: EntityData = { - level: 0, - type, - bbox, - params: { - radius: 5, - size: 10, - }, - } - return entity - }) - const edgeEntities = this.entities.filter( - entity => - entity.bbox.min.x < 0 || - entity.bbox.min.z < 0 || - entity.bbox.max.x > period || - entity.bbox.max.z > period, - ) - edgeEntities.map(entity => { - const bmin = entity.bbox.min.clone() - const bmax = entity.bbox.max.clone() - if (bmin.x < 0) { - bmin.x += period - bmax.x += period - } else if (bmax.x > period) { - bmin.x -= period - bmax.x -= period - } - if (bmin.z < 0) { - bmin.z += period - bmax.z += period - } else if (bmax.z > period) { - bmin.z -= period - bmax.z -= period - } - const entityCopy = { ...entity } - entityCopy.bbox = new Box3(bmin, bmax) - this.entities.push(entityCopy) - return entityCopy - }) - } - - override *iterate(input: Box3 | Vector3) { - if (this.entities.length === 0) RepeatableEntitiesMap.instance.populate() - const { period } = this - const pos = input instanceof Box3 ? input.min : input - const mapShift = new Vector3( - Math.floor(pos.x / period), - 0, - Math.floor(pos.z / period), - ).multiplyScalar(period) - - // find virtual map coords - const dims = - input instanceof Box3 - ? input.getSize(new Vector3()) - : new Vector3(1, 1, 1) - const point = input instanceof Box3 ? input.min : input - const mapPoint = new Vector3( - point.x % this.period, - 0, - point.z % this.period, - ) - mapPoint.x += mapPoint.x < 0 ? this.period : 0 - mapPoint.z += mapPoint.z < 0 ? this.period : 0 - const mapEnd = mapPoint.clone().add(dims) - mapEnd.y = 512 - const mapBox = new Box3(mapPoint, mapEnd) - - const entities = this.entities.filter(entity => - mapBox - ? entity.bbox.intersectsBox(mapBox) - : entity.bbox.containsPoint(input as Vector3), - ) - for (const entity of entities) { - const mapLocalPos = entity.bbox.min - // switch to global position - const entityDims = entity.bbox.getSize(new Vector3()) - const bmin = mapShift.clone().add(mapLocalPos.clone()) - const bmax = bmin.clone().add(entityDims) - const bbox = new Box3(bmin, bmax) - const centerPos = bbox.getCenter(new Vector3()) - // eval spawn probability at entity center - const spawnProbabilty = this.probabilityEval(centerPos) - const entityId = centerPos.x + '_' + centerPos.z - const prng = alea(entityId) - const hasSpawned = prng() * spawnProbabilty < probabilityThreshold - if (hasSpawned) { - const entityCopy = { ...entity } - entityCopy.bbox = bbox - yield entityCopy - } - } - } -} - -/** - * Storing entities at biome level with overlap at biomes' transitions - */ -export class OverlappingEntitiesMap extends EntitiesMap { - // entities stored per biome - static biomeMapsLookup: Record = {} - // getAdjacentEntities() { - // const adjacentEntities = [] - // const adjacentKeys = Object.values(Adjacent2dPos) - // .filter(v => !isNaN(Number(v)) && v !== Adjacent2dPos.center) - // .map(adjKey => { - // const adjCoords = getAdjacent2dCoords(patchCoords, adjKey as Adjacent2dPos) - // const mapKey = `map_${adjCoords.x % repeatPeriod}_${adjCoords.y % repeatPeriod}` - // return mapKey - // }) - // const adjacentMaps = adjacentKeys.map(mapKey => EntitiesMap.mapsLookup[mapKey]) - // return adjacentEntities - // } - - // Gen all entities belonging to specific biome - // populate(blockPos: Vector3) { - // // find biome at given block pos - // // discover biome extent - // // generate entities over all biome - // } - - // override *iterate(input: Box3 | Vector3) { - // // find if biome cached entities exists for given block or patch - // // if not populate biomes cache with entities - // // if block or patch contained withing unique biome, return matching entities - // // else if overlapping across several biomes, compute transition - // } -} diff --git a/src/procgen/WorldEntities.ts b/src/procgen/WorldEntities.ts new file mode 100644 index 0000000..ff4d06c --- /dev/null +++ b/src/procgen/WorldEntities.ts @@ -0,0 +1,60 @@ +import { Box2, Box3, Vector2 } from 'three' +import { Vector3 } from 'three/src/math/Vector3' + +import { EntityData, EntityType } from '../common/types' +import { PseudoDistributionMap } from '../index' + +// TODO remove hardcoded entity dimensions to compute from entity type +const entityDefaultDims = new Vector3(10, 20, 10) + +// TODO rename as WorldDistribution +export class WorldEntities { + // eslint-disable-next-line no-use-before-define + static singleton: WorldEntities + static get instance() { + this.singleton = this.singleton || new WorldEntities() + return this.singleton + } + + entityDistributionMapping: Record + + constructor() { + const treeDistribution = new PseudoDistributionMap() + + this.entityDistributionMapping = { + [EntityType.NONE]: treeDistribution, + [EntityType.TREE_APPLE]: treeDistribution, + [EntityType.TREE_PINE]: treeDistribution, + } + } + + queryDistributionMap(entityType: EntityType) { + const entityRadius = this.getEntityData(entityType).params.radius + const intersectsEntity = (testRange: Box2, entityPos: Vector2) => + testRange.distanceToPoint(entityPos) <= entityRadius + const distributionMap = this.entityDistributionMapping[entityType] + const query = (bbox: Box2) => + distributionMap.querySpawnLocations(bbox, intersectsEntity) + return query + } + + getEntityData(entityType: EntityType, entityPos?: Vector3) { + // TODO custom entity shape and params from entity type + entityPos = entityPos || new Vector3() + entityPos.y = entityDefaultDims.y / 2 + const entityShape = new Box3().setFromCenterAndSize( + entityPos, + entityDefaultDims, + ) + const entityParams = { + radius: 5, + size: 10, + } + const entityData: EntityData = { + type: entityType, + bbox: entityShape, + params: entityParams, + } + return entityData // entityBox.translate(entityPos) + } +} diff --git a/src/tools/ChunkFactory.ts b/src/tools/ChunkFactory.ts new file mode 100644 index 0000000..7364544 --- /dev/null +++ b/src/tools/ChunkFactory.ts @@ -0,0 +1,65 @@ +import { PatchId } from '../common/types' +import { asVect3, chunkBoxFromId, serializeChunkId } from '../common/utils' +import { EntityChunk } from '../datacontainers/EntityChunk' +import { WorldChunk, WorldChunkStub } from '../datacontainers/WorldChunk' +import { BlockMode, BlockType, GroundPatch, WorldConf } from '../index' + +export class ChunkFactory { + // eslint-disable-next-line no-use-before-define + static defaultInstance: ChunkFactory + // eslint-disable-next-line @typescript-eslint/no-unused-vars + voxelDataEncoder = (blockType: BlockType, _blockMode?: BlockMode) => + blockType || BlockType.NONE + + chunksRange = { + ymin: 0, + ymax: 5, + } + + static get default() { + this.defaultInstance = this.defaultInstance || new ChunkFactory() + return this.defaultInstance + } + + setChunksGenRange(ymin: number, ymax: number) { + this.chunksRange.ymin = ymin + this.chunksRange.ymax = ymax + } + + genChunksIdsFromPatchId(patchId: PatchId) { + const { ymin, ymax } = this.chunksRange + const chunk_ids = [] + for (let y = ymax; y >= ymin; y--) { + const chunk_coords = asVect3(patchId, y) + chunk_ids.push(chunk_coords) + } + return chunk_ids + } + + /** + * chunkify or chunksAssembly + * Assembles world building blocks (GroundPatch, EntityChunk) together + * to form final world chunk + */ + chunkify(patch: GroundPatch, patchEntities: EntityChunk[]) { + const patchChunkIds = patch.id + ? ChunkFactory.default.genChunksIdsFromPatchId(patch.id) + : [] + const worldChunksStubs = patchChunkIds.map(chunkId => { + const chunkBox = chunkBoxFromId(chunkId, WorldConf.patchSize) + const worldChunk = new WorldChunk(chunkBox) + // Ground pass + patch.fillChunk(worldChunk) + // Entities pass + patchEntities.forEach(entityChunk => + patch.mergeEntityVoxels(entityChunk, worldChunk), + ) + const worldChunkStub: WorldChunkStub = { + key: serializeChunkId(chunkId), + data: worldChunk.chunkData, + } + return worldChunkStub + }) + return worldChunksStubs + } +} diff --git a/src/world/WorldApi.ts b/src/world/WorldApi.ts deleted file mode 100644 index 5def29f..0000000 --- a/src/world/WorldApi.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { WorldCompute } from './WorldCompute' - -export enum WorldApiName { - PatchCompute = 'buildPatch', - BlocksBatchCompute = 'computeBlocksBatch', - GroundBlockCompute = 'computeGroundBlock', - OvergroundBlocksCompute = 'computeOvergroundBlocks', -} - -/** - * Frontend to access world api defaulting to using local world instance - * can be overriden to provide custom implementation - */ -export class WorldApi { - // eslint-disable-next-line no-use-before-define - static usedApi: WorldApi - - static get instance() { - WorldApi.usedApi = WorldApi.usedApi || new WorldApi() - return WorldApi.usedApi - } - - // call(api: WorldApiName, args: any[]): T | Promise - - async call(apiName: WorldApiName, args: any) { - return await WorldCompute[apiName](args[0]) - } -} - -/** - * World api provider to access worker instance - */ -export class WorldWorkerApi extends WorldApi { - // static usedApi: WorldWorkerApi - // eslint-disable-next-line no-undef - worker: Worker - count = 0 - resolvers: Record = {} - - // eslint-disable-next-line no-undef - constructor(worker: Worker) { - super() - this.worker = worker - this.worker.onmessage = ({ data }) => { - if (data.id !== undefined) { - this.resolvers[data.id]?.(data.data) - delete this.resolvers[data.id] - } else { - if (data) { - // data.kept?.length > 0 && PatchBlocksCache.cleanDeprecated(data.kept) - // data.created?.forEach(blocks_cache => { - // const blocks_patch = new PatchBlocksCache(blocks_cache) - // PatchBlocksCache.instances.push(blocks_patch) - // // patchRenderQueue.push(blocksPatch) - // }) - } - } - } - - this.worker.onerror = error => { - console.error(error) - } - - this.worker.onmessageerror = error => { - console.error(error) - } - } - - override call(apiName: WorldApiName, args: any[]) { - const id = this.count++ - this.worker.postMessage({ id, apiName, args }) - return new Promise(resolve => (this.resolvers[id] = resolve)) - } - - // static get instance() { - // WorldWorkerApi.usedApi = - // WorldWorkerApi.usedApi || new WorldWorkerApi() - // return WorldWorkerApi.usedApi - // } -} diff --git a/src/world/WorldCache.ts b/src/world/WorldCache.ts deleted file mode 100644 index 2efe8ac..0000000 --- a/src/world/WorldCache.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { Box3, Vector2, Vector3 } from 'three' - -import { BlockType } from '../index' - -import { WorldApi, WorldApiName } from './WorldApi' -import { - BlockData, - BlocksPatch, - BlockStub, - EntityChunk, - PatchStub, -} from './WorldPatch' - -/** - * Blocks cache - */ -export class WorldCache { - static patchLookupIndex: Record = {} - static bbox = new Box3() // global cache extent - static pendingRefresh = false - static cacheCenter = new Vector2(0, 0) - static cachePowRadius = 3 - static cacheSize = BlocksPatch.patchSize * 5 - // static worldApi = new WorldApi() - - // groundBlocks: Uint16Array = new Uint16Array(Math.pow(PatchBase.patchSize, 2)) - - entitiesChunks: EntityChunk[] = [] - - addPatch(patchStub: PatchStub) { - const patch = BlocksPatch.fromStub(patchStub) - WorldCache.bbox.union(patch.bbox) - } - - static async *processBatchItems(batchContent: string[]) { - for (const patchKey of batchContent) { - const patchStub = await WorldApi.instance.call( - WorldApiName.PatchCompute, - [patchKey], - ) - yield patchStub as PatchStub - } - } - - static async processBlocksBatch(batchContent: Vector3[]) { - const batchRes = await WorldApi.instance.call( - WorldApiName.BlocksBatchCompute, - [batchContent], - ) - return batchRes - } - - static async refresh( - center: Vector3, - // worldProxy: WorldProxy = PatchProcessing, - // asyncMode = false, - ) { - const { patchSize } = BlocksPatch - const { cachePowRadius } = this - const range = Math.pow(2, cachePowRadius) - const center_x = Math.floor(center.x / patchSize) - const center_z = Math.floor(center.z / patchSize) - const cacheCenter = new Vector2(center_x, center_z) - const cachePatchCount = Object.keys(this.patchLookupIndex).length - const batchContent: string[] = [] - if ( - !this.pendingRefresh && - (!cacheCenter.equals(this.cacheCenter) || cachePatchCount === 0) - ) { - this.pendingRefresh = true - this.cacheCenter = cacheCenter - - const existing: BlocksPatch[] = [] - for (let xmin = center_x - range; xmin < center_x + range; xmin += 1) { - for (let zmin = center_z - range; zmin < center_z + range; zmin += 1) { - // const patchStart = new Vector2(xmin, zmin) - const patchIndexKey = 'patch_' + xmin + '_' + zmin - // look for existing patch in current cache - const patch = this.patchLookupIndex[patchIndexKey] // || new BlocksPatch(patchStart) //BlocksPatch.getPatch(patchBbox, true) as BlocksPatch - if (!patch) { - // patch = new BlocksPatch(patchStart) - // add all patch needing to be filled up - batchContent.push(patchIndexKey) - } else { - existing.push(patch) - } - } - } - - // const updated = existing.filter(patch => patch.state < PatchState.Finalised) - // const removedCount = Object.keys(WorldCache.patchLookupIndex).length - existing.length - WorldCache.patchLookupIndex = {} - existing.forEach( - patch => (WorldCache.patchLookupIndex[patch.key] = patch), - ) - - const batchIter = WorldCache.processBatchItems(batchContent) - for await (const patchStub of batchIter) { - const patch = BlocksPatch.fromStub(patchStub) - WorldCache.patchLookupIndex[patch.key] = patch - WorldCache.bbox.union(patch.bbox) - } - this.pendingRefresh = false - return batchContent - } - return batchContent - } - - static getPatch(inputPoint: Vector2 | Vector3) { - const point = new Vector3( - inputPoint.x, - 0, - inputPoint instanceof Vector3 ? inputPoint.z : inputPoint.y, - ) - - const res = Object.values(this.patchLookupIndex).find( - patch => - point.x >= patch.bbox.min.x && - point.z >= patch.bbox.min.z && - point.x < patch.bbox.max.x && - point.z < patch.bbox.max.z, - ) - return res - } - - static getPatches(inputBbox: Box3) { - const bbox = inputBbox.clone() - bbox.min.y = 0 - bbox.max.y = 512 - const res = Object.values(this.patchLookupIndex).filter(patch => - patch.bbox.intersectsBox(bbox), - ) - return res - } - - getNearPatches(patch: BlocksPatch) { - const dim = patch.dimensions - const patchCenter = patch.bbox.getCenter(new Vector3()) - const minX = patchCenter.clone().add(new Vector3(-dim.x, 0, 0)) - const maxX = patchCenter.clone().add(new Vector3(dim.x, 0, 0)) - const minZ = patchCenter.clone().add(new Vector3(0, 0, -dim.z)) - const maxZ = patchCenter.clone().add(new Vector3(0, 0, dim.z)) - const minXminZ = patchCenter.clone().add(new Vector3(-dim.x, 0, -dim.z)) - const minXmaxZ = patchCenter.clone().add(new Vector3(-dim.x, 0, dim.z)) - const maxXminZ = patchCenter.clone().add(new Vector3(dim.x, 0, -dim.z)) - const maxXmaxZ = patchCenter.clone().add(new Vector3(dim.x, 0, dim.z)) - const neighboursCenters = [ - minX, - maxX, - minZ, - maxZ, - minXminZ, - minXmaxZ, - maxXminZ, - maxXmaxZ, - ] - const patchNeighbours: BlocksPatch[] = neighboursCenters - .map(patchCenter => WorldCache.getPatch(patchCenter)) - .filter(patch => patch) as BlocksPatch[] - return patchNeighbours - } - - static getGroundBlock(globalPos: Vector3) { - let res - globalPos.y = WorldCache.bbox.getCenter(new Vector3()).y - if (WorldCache.bbox.containsPoint(globalPos)) { - const patch = WorldCache.getPatch(globalPos) - if (patch) { - const localPos = globalPos.clone().sub(patch.bbox.min) - res = patch.getBlock(localPos) as BlockData - } - } else { - res = WorldApi.instance - .call(WorldApiName.GroundBlockCompute, [globalPos]) - .then(blockStub => { - const block = { - pos: new Vector3( - globalPos.x, - (blockStub as BlockStub).level, - globalPos.z, - ), - type: (blockStub as BlockStub).type, - } - return block - }) - if (!res) { - console.log(res) - } - } - return res - } - - static async getOvergroundBlock(globalPos: Vector3) { - const block = await WorldCache.getGroundBlock(globalPos) - if (block) { - const blocksBuffer = (await WorldApi.instance.call( - WorldApiName.OvergroundBlocksCompute, - [block.pos], - )) as BlockType[] - const lastBlockIndex = blocksBuffer.findLastIndex(elt => elt) - if (lastBlockIndex >= 0) { - block.pos.y += lastBlockIndex - block.type = blocksBuffer[lastBlockIndex] as BlockType - } - } - return block - } - - static setBlock(globalPos: Vector3, block: BlockData) { - // find patch containing point in cache - const patch = this.getPatch(globalPos) - if (patch) { - const localPos = globalPos.clone().sub(patch.bbox.min) - patch.setBlock(localPos, block.type) - } else { - console.log(globalPos) - } - return block - } -} diff --git a/src/world/WorldCompute.ts b/src/world/WorldCompute.ts deleted file mode 100644 index 2b7d4ab..0000000 --- a/src/world/WorldCompute.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { Box3, Vector3 } from 'three' - -import { EntityType } from '../index' -import { Biome, BlockType } from '../procgen/Biome' -import { Heightmap } from '../procgen/Heightmap' -import { - EntitiesMap, - EntityData, - RepeatableEntitiesMap, -} from '../procgen/EntitiesMap' - -import { BlocksPatch, EntityChunk } from './WorldPatch' - -export class WorldCompute { - static pendingTask = false - startTime = Date.now() - elapsedTime = 0 - count = 0 - - // patch keys as input - inputKeys: string[] = [] - // patch stubs as output - outputStubs: BlocksPatch[] = [] - - constructor(inputKeys: string[]) { - this.inputKeys = inputKeys - } - - static computeOvergroundBlocks(blockPos: Vector3) { - let blocksBuffer: BlockType[] = [] - // query entities at current block - const entitiesIter = RepeatableEntitiesMap.instance.iterate(blockPos) - for (const entity of entitiesIter) { - // use global coords in case entity center is from adjacent patch - const entityPos = entity.bbox.getCenter(new Vector3()) - const rawVal = Heightmap.instance.getRawVal(entityPos) - const mainBiome = Biome.instance.getMainBiome(entityPos) - const blockTypes = Biome.instance.getBlockType(rawVal, mainBiome) - const entityType = blockTypes.entities?.[0] as EntityType - if (entityType) { - const entityLevel = Heightmap.instance.getGroundLevel(entityPos, rawVal) - entity.bbox.min.y = entityLevel - entity.bbox.max.y = entityLevel + 10 - entity.type = entityType - blocksBuffer = EntitiesMap.fillBlockBuffer( - blockPos, - entity, - blocksBuffer, - ) - } - } - return blocksBuffer - } - - static computeGroundBlock(blockPos: Vector3) { - const biomeContribs = Biome.instance.getBiomeInfluence(blockPos) - const mainBiome = Biome.instance.getMainBiome(biomeContribs) - const rawVal = Heightmap.instance.getRawVal(blockPos) - const blockTypes = Biome.instance.getBlockType(rawVal, mainBiome) - const level = Heightmap.instance.getGroundLevel( - blockPos, - rawVal, - biomeContribs, - ) - // const pos = new Vector3(blockPos.x, level, blockPos.z) - const type = blockTypes.grounds[0] as BlockType - // const entityType = blockTypes.entities?.[0] as EntityType - // let offset = 0 - // if (lastBlock && entityType) { - - // } - // level += offset - const block = { level, type } - return block - } - - static computeBlocksBatch(batchContent: [], includeEntities = true) { - const batchRes = batchContent.map(({ x, z }) => { - const block_pos = new Vector3(x, 0, z) - const block = WorldCompute.computeGroundBlock(block_pos) - if (includeEntities) { - const blocksBuffer = WorldCompute.computeOvergroundBlocks(block_pos) - const lastBlockIndex = blocksBuffer.findLastIndex(elt => elt) - if (lastBlockIndex >= 0) { - block.level += lastBlockIndex - block.type = blocksBuffer[lastBlockIndex] as BlockType - } - } - return block - }) - return batchRes - } - - static buildPatch(patchKey: string) { - const patch = new BlocksPatch(patchKey) - // asyncMode && (await new Promise(resolve => setTimeout(resolve, 0))) - WorldCompute.buildGroundPatch(patch) - WorldCompute.buildEntitiesChunks(patch) - return patch - } - - static buildEntityChunk(patch: BlocksPatch, entity: EntityData) { - const entityChunk: EntityChunk = { - bbox: new Box3(), - data: [], - } - const blocksIter = patch.getBlocks(entity.bbox, true) - for (const block of blocksIter) { - const blocksBuffer = EntitiesMap.fillBlockBuffer(block.pos, entity, []) - patch.bbox.max.y = Math.max( - patch.bbox.max.y, - block.pos.y + blocksBuffer.length, - ) - const serialized = blocksBuffer - .reduce((str, val) => str + ',' + val, '') - .slice(1) - entityChunk.data.push(serialized) - entityChunk.bbox.expandByPoint(block.pos) - } - entityChunk.bbox = entity.bbox - return entityChunk - } - - static buildEntitiesChunks(patch: BlocksPatch) { - const entitiesIter = RepeatableEntitiesMap.instance.iterate(patch.bbox) - for (const entity of entitiesIter) { - // use global coords in case entity center is from adjacent patch - const entityPos = entity.bbox.getCenter(new Vector3()) - const biome = Biome.instance.getMainBiome(entityPos) - const rawVal = Heightmap.instance.getRawVal(entityPos) - const blockTypes = Biome.instance.getBlockType(rawVal, biome) - const entityType = blockTypes.entities?.[0] as EntityType - // const patchLocalBmin = new Vector3(min.x % patch.dimensions.x + min.x >= 0 ? 0 : patch.dimensions.x, - // 0, - // max.z % patch.dimensions.z + max.z >= 0 ? 0 : patch.dimensions.z) - if (entityType) { - const dims = entity.bbox.getSize(new Vector3()) - dims.y = 10 - const localBmin = entity.bbox.min.clone().sub(patch.bbox.min) - localBmin.y = Heightmap.instance.getGroundLevel(entityPos) - const localBmax = localBmin.clone().add(dims) - const localBbox = new Box3(localBmin, localBmax) - entity.bbox = localBbox - entity.type = entityType - const entityChunk = WorldCompute.buildEntityChunk(patch, entity) - patch.entitiesChunks.push(entityChunk) - // let item: BlockIteratorRes = blocksIter.next() - } - } - } - - /** - * Gen blocks data that will be sent to blocks cache - */ - static buildGroundPatch(patch: BlocksPatch) { - const { min, max } = patch.bbox - // const patchId = min.x + ',' + min.z + '-' + max.x + ',' + max.z - // const prng = alea(patchId) - // const refPoints = this.isTransitionPatch ? this.buildRefPoints() : [] - // const blocksPatch = new PatchBlocksCache(new Vector2(min.x, min.z)) - const blocksPatchIter = patch.iterBlocks() - min.y = 512 - max.y = 0 - let blockIndex = 0 - - for (const blockData of blocksPatchIter) { - const blockPos = blockData.pos - // const patchCorner = points.find(pt => pt.distanceTo(blockData.pos) < 2) - const block = this.computeGroundBlock(blockPos) - min.y = Math.min(min.y, block.level) - max.y = Math.max(max.y, block.level) - patch.writeBlockAtIndex(blockIndex, block.level, block.type) - blockIndex++ - } - patch.bbox.min = min - patch.bbox.max = max - patch.bbox.getSize(patch.dimensions) - // PatchBlocksCache.bbox.union(patch.bbox) - - // patch.state = PatchState.Filled - return patch - } -} diff --git a/src/world/WorldPatch.ts b/src/world/WorldPatch.ts deleted file mode 100644 index 713f8e3..0000000 --- a/src/world/WorldPatch.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { Box3, Vector2, Vector3 } from 'three' - -import { BlockType } from '../procgen/Biome' - -export type BlockData = { - pos: Vector3 - type: BlockType - localPos?: Vector3 - buffer?: BlockType[] -} - -export type BlockStub = { - level: number - type: BlockType -} - -export type EntityChunk = { - bbox: Box3 - data: string[] -} - -export type PatchStub = { - key: string - bbox: Box3 - groundBlocks: { - type: Uint16Array - level: Uint16Array - } - entitiesChunks: EntityChunk[] -} - -export type BlockIteratorRes = IteratorResult - -export class BlocksPatch { - // eslint-disable-next-line no-use-before-define - // static cache: BlocksPatch[] = [] - static patchSize = Math.pow(2, 6) - static bbox = new Box3() - - coords: Vector2 - key: string - bbox: Box3 - dimensions = new Vector3() - - groundBlocks = { - type: new Uint16Array(Math.pow(BlocksPatch.patchSize, 2)), - level: new Uint16Array(Math.pow(BlocksPatch.patchSize, 2)), - } - - entitiesChunks: EntityChunk[] = [] - - constructor(patchKey: string) { - const { patchSize } = BlocksPatch - const patchOrigin = new Vector3( - parseInt(patchKey.split('_')[1] as string), - 0, - parseInt(patchKey.split('_')[2] as string), - ) - this.coords = new Vector2(patchOrigin.x, patchOrigin.z) - const bmin = patchOrigin.clone().multiplyScalar(patchSize) - const bmax = patchOrigin.clone().addScalar(1).multiplyScalar(patchSize) - bmax.y = 512 - this.key = patchKey - this.bbox = new Box3(bmin, bmax) - this.bbox.getSize(this.dimensions) - } - - writeBlockAtIndex( - blockIndex: number, - blockLevel: number, - blockType: BlockType, - ) { - this.groundBlocks.level[blockIndex] = blockLevel - this.groundBlocks.type[blockIndex] = blockType - } - - getBlock(localPos: Vector3) { - let block - if ( - localPos.x >= 0 && - localPos.x < this.dimensions.x && - localPos.z >= 0 && - localPos.z < this.dimensions.z - ) { - const blockIndex = localPos.x * this.dimensions.x + localPos.z - const pos = localPos.clone() - pos.y = this.groundBlocks.level[blockIndex] || 0 - const type = this.groundBlocks.type[blockIndex] - block = { - pos, - type, - } - } - return block - } - - setBlock(localPos: Vector3, blockType: BlockType) { - const blockIndex = localPos.x * this.dimensions.x + localPos.z - const blockLevel = localPos.y - this.writeBlockAtIndex(blockIndex, blockLevel, blockType) - // const levelMax = blockLevel + blockData.over.length - // bbox.min.y = Math.min(bbox.min.y, levelMax) - // bbox.max.y = Math.max(bbox.max.y, levelMax) - } - - *getBlocks(bbox: Box3, useLocalPos = false) { - const { patchSize } = BlocksPatch - const bmin = new Vector3( - Math.max(bbox.min.x, useLocalPos ? 0 : this.bbox.min.x), - 0, - Math.max(bbox.min.z, useLocalPos ? 0 : this.bbox.min.z), - ) - const bmax = new Vector3( - Math.min(bbox.max.x, useLocalPos ? patchSize : this.bbox.max.x), - 0, - Math.min(bbox.max.z, useLocalPos ? patchSize : this.bbox.max.z), - ) - for (let { x } = bmin; x < bmax.x; x++) { - for (let { z } = bmin; z < bmax.z; z++) { - const pos = new Vector3(x, 0, z) - const localPos = useLocalPos ? pos : pos.clone().sub(this.bbox.min) - const index = localPos.x * this.dimensions.x + localPos.z - const type = this.groundBlocks.type[index] || BlockType.NONE - const level = this.groundBlocks.level[index] || 0 - pos.y = level - localPos.y = level - const blockData: BlockData = { - pos, - localPos, - type, - } - yield blockData - } - } - } - - *iterBlocks(useLocalCoords?: boolean) { - const bbox = useLocalCoords - ? new Box3(new Vector3(0), this.dimensions) - : this.bbox - - let index = 0 - for (let { x } = bbox.min; x < bbox.max.x; x++) { - for (let { z } = bbox.min; z < bbox.max.z; z++) { - const pos = new Vector3(x, 0, z) - // highlight patch edges - // blockType = x === bbox.min.x ? BlockType.MUD : blockType - // blockType = x === bbox.max.x - 1 ? BlockType.ROCK : blockType - // blockType = z === bbox.min.z ? BlockType.MUD : blockType - // blockType = z === bbox.max.z - 1 ? BlockType.ROCK : blockType - const type = this.groundBlocks.type[index] || BlockType.NONE - const level = this.groundBlocks?.level[index] || 0 - pos.y = level - const blockData = { - index, - pos, - type, - } - index++ - yield blockData - } - } - } - - getPatchCoords() {} - - toStub() { - const { key } = this - return { - key, - } - } - - static fromStub(patchStub: PatchStub) { - const { key, groundBlocks, entitiesChunks } = patchStub - const patch = new BlocksPatch(key) - patch.groundBlocks = groundBlocks - patch.entitiesChunks = entitiesChunks - patch.bbox.min.y = patchStub.bbox.min.y - patch.bbox.max.y = patchStub.bbox.max.y - // patchStub.entitiesChunks?.forEach((entityChunk: EntityChunk) => - // patch.entitiesChunks.push(entityChunk), - // ) - return patch - } - - static getPatchOrigin(input: Box3 | Vector3 | Vector2) { - const { patchSize } = this - const inputCopy: Vector3 | Box3 = - input instanceof Vector2 - ? new Vector3(input.x, 0, input.y) - : input.clone() - const point = - inputCopy instanceof Box3 - ? (inputCopy as Box3).getCenter(new Vector3()) - : (inputCopy as Vector3).clone() - let minx = point.x - (point.x % patchSize) - minx -= point.x < 0 && point.x % this.patchSize !== 0 ? patchSize : 0 - let minz = point.z - (point.z % patchSize) - minz -= point.z < 0 && point.z % this.patchSize !== 0 ? patchSize : 0 - const patchOrigin = new Vector2(minx, minz) - return patchOrigin - } -}