From d97e375b6dc3ad98f11fd84defa51623a63b9ea4 Mon Sep 17 00:00:00 2001 From: HF Date: Sat, 25 Jan 2020 21:55:40 +0100 Subject: [PATCH] change 3d scaling and add ChunkLoader dummy --- src/core/Palette.js | 3 + src/ui/ChunkLoader2D.js | 3 +- src/ui/ChunkLoader3D.js | 86 ++++++++++++ src/ui/ChunkRGB.js | 4 +- src/ui/ChunkRGB3D.js | 292 ++++++++++++++++++++++++++++++++++++++++ src/ui/Renderer3D.js | 123 ++++++++++++++--- 6 files changed, 488 insertions(+), 23 deletions(-) create mode 100644 src/ui/ChunkLoader3D.js create mode 100644 src/ui/ChunkRGB3D.js diff --git a/src/core/Palette.js b/src/core/Palette.js index 48043ca..701fb82 100644 --- a/src/core/Palette.js +++ b/src/core/Palette.js @@ -12,6 +12,7 @@ class Palette { rgb: Uint8Array; colors: Array; abgr: Uint32Array; + fl: Array; alpha: number = 0; constructor(colors: Array, alpha: number = 0) { @@ -20,6 +21,7 @@ class Palette { this.rgb = new Uint8Array(this.length * 3); this.colors = new Array(this.length); this.abgr = new Uint32Array(this.length); + this.fl = new Array(this.length); let cnt = 0; for (let index = 0; index < colors.length; index++) { @@ -31,6 +33,7 @@ class Palette { this.rgb[cnt++] = b; this.colors[index] = `rgb(${r}, ${g}, ${b})`; this.abgr[index] = (0xFF000000) | (b << 16) | (g << 8) | (r); + this.fl[index] = [r / 256, g / 256, b / 256]; } } diff --git a/src/ui/ChunkLoader2D.js b/src/ui/ChunkLoader2D.js index 8a85fad..b0b350c 100644 --- a/src/ui/ChunkLoader2D.js +++ b/src/ui/ChunkLoader2D.js @@ -85,7 +85,8 @@ class ChunkLoader { return chunk.image; } return loadingTiles.getTile(canvasId); - } if (fetch) { + } + if (fetch) { // fetch chunk const chunkRGB = new ChunkRGB(this.palette, chunkKey); this.chunks.set(chunkKey, chunkRGB); diff --git a/src/ui/ChunkLoader3D.js b/src/ui/ChunkLoader3D.js new file mode 100644 index 0000000..8f4703a --- /dev/null +++ b/src/ui/ChunkLoader3D.js @@ -0,0 +1,86 @@ +/* + * Loading 3D chunks + * + * @flow + */ + +import * as THREE from 'three'; + +import { + THREE_CANVAS_HEIGHT, + THREE_TILE_SIZE, +} from '../core/constants'; +import { + receiveBigChunk, +} from '../actions'; + +import Chunk from './ChunkRGB3D'; + + +class ChunkLoader { + store = null; + canvasId: number; + palette; + chunks: Map; + + constructor(store) { + console.log("Created Chunk loader"); + this.store = store; + const state = store.getState(); + const { + canvasId, + palette, + } = state.canvas; + this.canvasId = canvasId; + this.palette = palette; + this.chunks = new Map(); + } + + getVoxelUpdate( + xc: number, + zc: number, + offset: number, + color: number, + ) { + const key = `${xc}:${zc}`; + const chunk = this.chunks.get(key); + if (chunk) { + /* + const offsetXZ = offset % (THREE_TILE_SIZE ** 2); + const iy = (offset - offsetXZ) / (THREE_TILE_SIZE ** 2); + const ix = offsetXZ % THREE_TILE_SIZE; + const iz = (offsetXZ - ix) / THREE_TILE_SIZE; + */ + chunk.setVoxelByOffset(offset, color); + //this.store.dispatch(receiveBigChunk(key)); + } + } + + getChunk(xc, zc, fetch: boolean) { + const chunkKey = `${xc}:${zc}`; + console.log(`Get chunk ${chunkKey}`); + let chunk = this.chunks.get(chunkKey); + if (chunk) { + if (chunk.ready) { + return chunk.mesh; + } + return null; + } + if (fetch) { + // fetch chunk + chunk = new Chunk(this.palette, chunkKey); + this.chunks.set(chunkKey, chunk); + this.fetchChunk(xc, zc, chunk); + } + return null; + } + + async fetchChunk(xc: number, zc: number, chunk) { + const { key } = chunk; + console.log(`Fetch chunk ${key}`); + await chunk.generateSin(); + this.store.dispatch(receiveBigChunk(key)); + } +} + +export default ChunkLoader; diff --git a/src/ui/ChunkRGB.js b/src/ui/ChunkRGB.js index 852fa4f..f1ec688 100644 --- a/src/ui/ChunkRGB.js +++ b/src/ui/ChunkRGB.js @@ -19,7 +19,7 @@ class ChunkRGB { // if true => chunk got requested from api/chunk and // receives websocket pixel updates // if false => chunk is an zoomed png tile - this.isBasechunk = true; + this.isBasechunk = false; this.palette = palette; this.image = document.createElement('canvas'); this.image.width = TILE_SIZE; @@ -69,7 +69,6 @@ class ChunkRGB { */ fromBuffer(chunkBuffer: Uint8Array) { - this.ready = true; const imageData = new ImageData(TILE_SIZE, TILE_SIZE); const imageView = new Uint32Array(imageData.data.buffer); const colors = this.palette.buffer2ABGR(chunkBuffer); @@ -78,6 +77,7 @@ class ChunkRGB { }); const ctx = this.image.getContext('2d'); ctx.putImageData(imageData, 0, 0); + this.ready = true; } fromImage(img: Image) { diff --git a/src/ui/ChunkRGB3D.js b/src/ui/ChunkRGB3D.js new file mode 100644 index 0000000..da653f4 --- /dev/null +++ b/src/ui/ChunkRGB3D.js @@ -0,0 +1,292 @@ +/* + * 3D Chunk + * + * @flow + */ + +import * as THREE from 'three'; + +import { + THREE_TILE_SIZE, + THREE_CANVAS_HEIGHT, +} from '../core/constants'; + + +const faceDirs = [ + [-1, 0, 0], + [1, 0, 0], + [0, -1, 0], + [0, 1, 0], + [0, 0, -1], + [0, 0, 1], +]; + +const faceCorners = [ + // left + [ + [0, 1, 0], + [0, 0, 0], + [0, 1, 1], + [0, 0, 1], + ], + // right + [ + [1, 1, 1], + [1, 0, 1], + [1, 1, 0], + [1, 0, 0], + ], + // bottom + [ + [1, 0, 1], + [0, 0, 1], + [1, 0, 0], + [0, 0, 0], + ], + // top + [ + [0, 1, 1], + [1, 1, 1], + [0, 1, 0], + [1, 1, 0], + ], + // back + [ + [1, 0, 0], + [0, 0, 0], + [1, 1, 0], + [0, 1, 0], + ], + // front + [ + [0, 0, 1], + [1, 0, 1], + [0, 1, 1], + [1, 1, 1], + ], +]; + +const material = new THREE.MeshLambertMaterial({ + vertexColors: THREE.VertexColors, +}); + + +class Chunk { + key: string; + ready: boolean = false; + palette: Object; + buffer: Uint8Array; + mesh: THREE.Mesh = null; + faceCnt: number; + + constructor(palette, key) { + this.key = key; + this.palette = palette; + } + + getVoxel(x: number, y: number, z: number) { + const { buffer } = this; + if (!buffer) return 0; + if (x < 0 || x >= THREE_TILE_SIZE || y >= THREE_CANVAS_HEIGHT + || z < 0 || z >= THREE_TILE_SIZE) + return 0; + if (y < 0) + return 1; + // z and y are swapped in api/pixel for compatibility + // with 2D canvas + const offset = Chunk.getOffsetOfVoxel(x, y, z) + return this.buffer[offset]; + } + + async generateSin() { + let cnt = 0; + this.buffer = new Uint8Array(THREE_TILE_SIZE * THREE_TILE_SIZE * THREE_CANVAS_HEIGHT); + const cellSize = 64; + for (let y = 0; y < THREE_CANVAS_HEIGHT; ++y) { + for (let z = 0; z < THREE_TILE_SIZE; ++z) { + for (let x = 0; x < THREE_TILE_SIZE; ++x) { + const height = (Math.sin(x / cellSize * Math.PI * 2) + Math.sin(z / cellSize * Math.PI * 3)) * (cellSize / 6) + (cellSize / 2); + if (y < height) { + const offset = x + + z * THREE_TILE_SIZE + + y * THREE_TILE_SIZE * THREE_TILE_SIZE; + const clr = 1 + Math.floor(Math.random() * 31); + this.buffer[offset] = clr; + cnt += 1; + } + } + } + } + console.log(`Created buffer with ${cnt} voxels`); + this.faceCnt = Chunk.estimateNeededFaces(this.buffer); + this.renderChunk(); + this.ready = true; + } + + static estimateNeededFaces(buffer: Uint8Array) { + let totalCnt = 0; + + let u = 0; + for (let y = 0; y < THREE_CANVAS_HEIGHT; ++y) { + for (let z = 0; z < THREE_TILE_SIZE; ++z) { + for (let x = 0; x < THREE_TILE_SIZE; ++x) { + if (buffer[u] !== 0) { + if (x === 0 + || buffer[u - 1] === 0) { + totalCnt += 1; + } + if (x === THREE_TILE_SIZE - 1 + || buffer[u + 1] === 0) { + totalCnt += 1; + } + if (z === 0 + || buffer[u - THREE_TILE_SIZE] === 0) { + totalCnt += 1; + } + if (z === THREE_TILE_SIZE - 1 + || buffer[u + THREE_TILE_SIZE] === 0) { + totalCnt += 1; + } + if (y !== 0 + && buffer[u - (THREE_TILE_SIZE ** 2)] === 0) { + totalCnt += 1; + } + if (y === THREE_CANVAS_HEIGHT - 1 + || buffer[u + (THREE_TILE_SIZE ** 2)] === 0) { + totalCnt += 1; + } + } + u += 1; + } + } + } + return totalCnt; + } + + static getOffsetOfVoxel(x: number, y: number, z: number) { + return x + z * THREE_TILE_SIZE + y * THREE_TILE_SIZE * THREE_TILE_SIZE; + } + + setVoxelByOffset(offset: number, clr: number) { + this.buffer[offset] = clr; + this.faceCnt += 6; + this.renderChunk(); + } + + setVoxel(x: number, y: number, z: number, clr: number) { + const offset = Chunk.getOffsetOfVoxel(x, y, z); + this.setVoxelByOffset(offset, clr); + } + + async fromBuffer(chunkBuffer: Uint8Array) { + this.buffer = chunkBuffer; + this.renderChunk(); + this.ready = true; + } + + renderChunk() { + let time1 = Date.now(); + + let cnt = 0; + let cntv = 0; + let voxel; + const faceCnt = this.faceCnt; + const positions = new Float32Array(faceCnt * 4 * 3); + const normals = new Float32Array(faceCnt * 4 * 3); + const colors = new Uint8Array(faceCnt * 4 * 3); + const indices = new Uint32Array(faceCnt * 6); + const { rgb } = this.palette; + // just render faces that do not have an adjescent voxel + for (let y = 0; y < THREE_CANVAS_HEIGHT; ++y) { + for (let z = 0; z < THREE_TILE_SIZE; ++z) { + for (let x = 0; x < THREE_TILE_SIZE; ++x) { + voxel = this.getVoxel(x, y, z); + if (voxel !== 0) { + voxel *= 3; + cntv += 1; + for (let i = 0; i < 6; ++i) { + const dir = faceDirs[i]; + const corners = faceCorners[i]; + + const neighbor = this.getVoxel( + x + dir[0], + y + dir[1], + z + dir[2], + ); + if (neighbor === 0) { + // this voxel has no neighbor in this direction + // so we need a face + let ndx = cnt * 4 * 3; + for (let c = 0; c < 4; ++c) { + const pos = corners[c]; + positions[ndx] = pos[0] + x; + normals[ndx] = dir[0]; + colors[ndx++] = rgb[voxel]; + positions[ndx] = pos[1] + y; + normals[ndx] = dir[1]; + colors[ndx++] = rgb[voxel + 1]; + positions[ndx] = pos[2] + z; + normals[ndx] = dir[2]; + colors[ndx++] = rgb[voxel + 2]; + } + const idx = cnt * 4; + ndx = cnt * 6; + indices[ndx++] = idx; + indices[ndx++] = idx + 1; + indices[ndx++] = idx + 2; + indices[ndx++] = idx + 2; + indices[ndx++] = idx + 1; + indices[ndx] = idx + 3; + + cnt += 1; + } + } + } + } + } + } + let time2 = Date.now(); + + const geometry = (this.mesh) + ? this.mesh.geometry + : new THREE.BufferGeometry(); + + geometry.setAttribute( + 'position', + new THREE.BufferAttribute( + positions, + 3, + ), + ); + geometry.setAttribute( + 'normal', + new THREE.BufferAttribute( + normals, + 3, + ), + ); + geometry.setAttribute( + 'color', + new THREE.BufferAttribute( + colors, + 3, + true, + ), + ); + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + geometry.computeBoundingSphere(); + geometry.setDrawRange(0, cnt * 6); + + this.faceCnt = cnt; + + if (!this.mesh) { + this.mesh = new THREE.Mesh(geometry, material); + this.mesh.name = this.key; + } + + let time3 = Date.now(); + console.log(`Created mesh for ${cntv} voxels with ${cnt} faces in ${time3 - time2}ms webgl and ${time2 - time1}ms data creation`); + } +} + +export default Chunk; diff --git a/src/ui/Renderer3D.js b/src/ui/Renderer3D.js index 7591000..4c9a895 100644 --- a/src/ui/Renderer3D.js +++ b/src/ui/Renderer3D.js @@ -7,6 +7,14 @@ import * as THREE from 'three'; import VoxelPainterControls from '../controls/VoxelPainterControls'; +import ChunkLoader from './ChunkLoader3D'; +import { + getChunkOfPixel, + getOffsetOfPixel, +} from '../core/utils'; +import { + THREE_TILE_SIZE, +} from '../core/constants'; import { setHover, } from '../actions'; @@ -23,6 +31,7 @@ class Renderer { voxel: Object; voxelMaterials: Array; objects: Array; + loadedChunks: Array; plane: Object; //-- controls: Object; @@ -31,20 +40,25 @@ class Renderer { mouse; raycaster; pressTime: number; + //-- + chunkLoader: ChunkLoader = null; + forceNextRender: boolean = false; constructor(store) { this.store = store; const state = store.getState(); this.objects = []; + this.loadedChunks = new Map(); + this.chunkLoader = new ChunkLoader(store); // camera const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, - 2000, + 200, ); - camera.position.set(100, 160, 260); + camera.position.set(10, 16, 26); camera.lookAt(0, 0, 0); this.camera = camera; @@ -52,9 +66,11 @@ class Renderer { const scene = new THREE.Scene(); scene.background = new THREE.Color(0xf0f0f0); this.scene = scene; + window.scene = scene; + window.THREE = THREE; // hover helper - const rollOverGeo = new THREE.BoxBufferGeometry(10, 10, 10); + const rollOverGeo = new THREE.BoxBufferGeometry(1, 1, 1); const rollOverMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, opacity: 0.5, @@ -64,11 +80,11 @@ class Renderer { scene.add(this.rollOverMesh); // cubes - this.voxel = new THREE.BoxBufferGeometry(10, 10, 10); + this.voxel = new THREE.BoxBufferGeometry(1, 1, 1); this.initCubeMaterials(state); // grid - const gridHelper = new THREE.GridHelper(1000, 100, 0x555555, 0x555555); + const gridHelper = new THREE.GridHelper(100, 10, 0x555555, 0x555555); scene.add(gridHelper); // @@ -76,7 +92,7 @@ class Renderer { this.mouse = new THREE.Vector2(); // Plane Floor - const geometry = new THREE.PlaneBufferGeometry(5000, 5000); + const geometry = new THREE.PlaneBufferGeometry(500, 500); geometry.rotateX(-Math.PI / 2); const plane = new THREE.Mesh( geometry, @@ -106,8 +122,8 @@ class Renderer { controls.enableDamping = true; controls.dampingFactor = 0.75; controls.maxPolarAngle = Math.PI / 2; - controls.minDistance = 100.00; - controls.maxDistance = 1000.00; + controls.minDistance = 10.00; + controls.maxDistance = 100.00; this.controls = controls; const { domElement } = threeRenderer; @@ -120,6 +136,8 @@ class Renderer { domElement.addEventListener('mousedown', this.onDocumentMouseDown, false); domElement.addEventListener('mouseup', this.onDocumentMouseUp, false); window.addEventListener('resize', this.onWindowResize, false); + + this.forceNextRender = true; } destructor() { @@ -147,10 +165,50 @@ class Renderer { this.voxelMaterials = cubeMaterials; } + reloadChunks() { + console.log('Reload Chunks'); + const renderDistance = 50; + const state = this.store.getState(); + const { canvasSize, view } = state.canvas; + // const [x,, z] = view; + const [x, z] = [0, 0]; + const { + scene, + loadedChunks, + chunkLoader, + } = this; + const [xcMin, zcMin] = getChunkOfPixel(canvasSize, x - 50, z - 50, 0); + const [xcMax, zcMax] = getChunkOfPixel(canvasSize, x + 50, z + 50, 0); + console.log(`Get ${xcMin} - ${xcMax} - ${zcMin} - ${zcMax}`); + for (let zc = zcMin; zc <= zcMax; ++zc) { + for (let xc = xcMin; xc <= xcMax; ++xc) { + const chunkKey = `${xc}:${zc}`; + const chunk = chunkLoader.getChunk(xc, zc, true); + if (chunk) { + console.log(`Got Chunk ${chunkKey}`); + loadedChunks.set(chunkKey, chunk); + this.objects.push(chunk); + chunk.position.fromArray([ + xc * THREE_TILE_SIZE - canvasSize / 2, + 0, + zc * THREE_TILE_SIZE - canvasSize / 2, + ]); + window.chunk = chunk; + scene.add(chunk); + console.log(`added chunk`); + } + } + } + } + render() { if (!this.threeRenderer) { return; } + if (this.forceNextRender) { + this.reloadChunks(); + this.forceNextRender = false; + } this.controls.update(); this.threeRenderer.render(this.scene, this.camera); } @@ -190,16 +248,13 @@ class Renderer { const intersect = intersects[0]; rollOverMesh.position .copy(intersect.point) - .add(intersect.face.normal); + .add(intersect.face.normal.multiplyScalar(0.5)); rollOverMesh.position - .divideScalar(10) .floor() - .multiplyScalar(10) - .addScalar(5); + .addScalar(0.5); } const hover = rollOverMesh.position - .toArray() - .map((u) => Math.floor(u / 10)); + .toArray().map((u) => Math.floor(u)); this.store.dispatch(setHover(hover)); } @@ -247,27 +302,55 @@ class Renderer { case 0: { // left mouse button const state = store.getState(); - const { selectedColor } = state.gui; + const { selectedColor, hover } = state.gui; + const { canvasSize } = state.canvas; + //const pos = new THREE.Vector3(); + const [x, y, z] = hover; + /* + const [x, y, z] = pos.copy(intersect.point) + .add(intersect.face.normal.multiplyScalar(0.5)) + .floor() + .addScalar(0.5) + .toArray(); + */ + const offset = getOffsetOfPixel(canvasSize, x, z, y); + const [xc, zc] = getChunkOfPixel(canvasSize, x, z, y); + this.chunkLoader.getVoxelUpdate(xc, zc, offset, selectedColor); + /* const newVoxel = new THREE.Mesh( voxel, voxelMaterials[selectedColor], ); - newVoxel.position.copy(intersect.point) - .add(intersect.face.normal); - newVoxel.position.divideScalar(10) + newVoxel.position + .copy(intersect.point) + .add(intersect.face.normal.multiplyScalar(0.5)); + newVoxel.position .floor() - .multiplyScalar(10) - .addScalar(5); + .addScalar(0.5); scene.add(newVoxel); objects.push(newVoxel); + */ } break; case 2: // right mouse button + const state = store.getState(); + const { hover } = state.gui; + const { canvasSize } = state.canvas; + const normal = intersect.face.normal; + let [x, y, z] = hover; + x -= normal.x; + y -= normal.y; + z -= normal.z; + const offset = getOffsetOfPixel(canvasSize, x, z, y); + const [xc, zc] = getChunkOfPixel(canvasSize, x, z, y); + this.chunkLoader.getVoxelUpdate(xc, zc, offset, 0); + /* if (intersect.object !== plane) { scene.remove(intersect.object); objects.splice(objects.indexOf(intersect.object), 1); } + */ break; default: break;