diff --git a/README.md b/README.md index a1eb1c9..0a53322 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A tool to convert .obj files into Minecraft Schematics ![DebugPreview](/resources/debug_preview.png) +![MeshingPreview](/resources/greedy_meshing.png) + # Progress 0.1 * ✔️ **.json model loading** @@ -15,15 +17,15 @@ A tool to convert .obj files into Minecraft Schematics * ✔️ **Basic .obj file loader UI** 0.2 -* Greedy voxel meshing +* ✔️ **Greedy voxel meshing** * Export to schematic 0.3 * Building guides * Slice viewer + # Usage -Since no build has been released yet, you'll need to: * Download and install [Node.js](https://nodejs.org/en/). * Execute `git clone https://github.com/LucasDower/ObjToSchematic.git` in your command line. * Navigate to `/ObjToSchematic-main`. diff --git a/index.html b/index.html index 316a936..3b85d1e 100644 --- a/index.html +++ b/index.html @@ -17,12 +17,11 @@
- - + + -
+ - diff --git a/resources/debug_preview.png b/resources/debug_preview.png index ecc00b8..339dfb3 100644 Binary files a/resources/debug_preview.png and b/resources/debug_preview.png differ diff --git a/resources/greedy_meshing.png b/resources/greedy_meshing.png new file mode 100644 index 0000000..4c6181f Binary files /dev/null and b/resources/greedy_meshing.png differ diff --git a/resources/preview.png b/resources/preview.png index 28ee954..4574014 100644 Binary files a/resources/preview.png and b/resources/preview.png differ diff --git a/resources/tri.obj b/resources/tri.obj new file mode 100644 index 0000000..3f92ad0 --- /dev/null +++ b/resources/tri.obj @@ -0,0 +1,14 @@ +# Blender v2.93.1 OBJ File: '' +# www.blender.org +mtllib tri.mtl +o Plane_Plane.002 +v -1.300944 -1.462577 0.805557 +v 1.955046 2.129125 1.387971 +v -0.654101 -0.666548 -2.193528 +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.000000 1.000000 +vn -0.7422 0.6699 0.0177 +usemtl None +s 1 +f 1/1/1 2/2/1 3/3/1 diff --git a/src/camera.js b/src/camera.js index b66a9f6..a883175 100644 --- a/src/camera.js +++ b/src/camera.js @@ -10,8 +10,8 @@ class ArcballCamera { this.zNear = zNear; this.zFar = zFar; - this.actualDistance = 15.0; - this.actualAzimuth = 0.6; + this.actualDistance = 6.0; + this.actualAzimuth = -1.0; this.actualElevation = 1.3; this.cameraSmoothing = 0.025; @@ -28,7 +28,7 @@ class ArcballCamera { this.mouseSensitivity = 0.005; this.scrollSensitivity = 0.005; - this.zoomDistMin = 2.0; + this.zoomDistMin = 1.0; this.zoomDistMax = 100.0; this.isRotating = false; diff --git a/src/client.js b/src/client.js index bc39e3c..2952f6d 100644 --- a/src/client.js +++ b/src/client.js @@ -3,13 +3,16 @@ const { Mesh } = require('./src/mesh.js'); const { VoxelManager } = require('./src/voxel_manager.js'); const { Vector3 } = require('./src/vector.js'); -const voxelSize = 0.5; +//const voxelSize = document.querySelector("#voxelInput").value; +const voxelSize = 0.025; let renderer = new Renderer(voxelSize); const voxelManager = new VoxelManager(voxelSize); const nav = document.querySelector("#nav"); let canvas = document.querySelector("#c"); +const showMeshing = false; +const showFailedAABBs = true; function resizeCanvas() { canvas.height = window.innerHeight - 54; @@ -17,16 +20,6 @@ function resizeCanvas() { } resizeCanvas(); - -/* -const suzanneLeft = new Mesh('./resources/suzanne_left.obj'); -voxelManager.voxeliseMesh(suzanneLeft); -renderer.registerVoxels(voxelManager.voxels); - -const suzanneRight = new Mesh('./resources/suzanne_right.obj'); -renderer.registerMesh(suzanneRight); -*/ - let loadedMesh = null; document.querySelector("#objBtn").addEventListener('click', () => { @@ -47,6 +40,7 @@ document.querySelector("#objBtn").addEventListener('click', () => { renderer.compileRegister(); }); + document.querySelector("#voxelBtn").addEventListener('click', () => { const voxelSize = document.querySelector("#voxelInput").value; @@ -55,15 +49,33 @@ document.querySelector("#voxelBtn").addEventListener('click', () => { renderer.setVoxelSize(voxelSize); voxelManager.setVoxelSize(voxelSize); - + voxelManager.voxeliseMesh(loadedMesh); - + renderer.clear(); - renderer.registerVoxels(voxelManager.voxels, false); + + const mesh = voxelManager.buildMesh(); + for (const box of mesh) { + renderer.registerBox(box.centre, box.size, false); + } + + if (showMeshing) { + renderer.setStroke(new Vector3(1.0, 0.0, 0.0)); + for (const box of mesh) { + renderer.registerBox(box.centre, box.size, true); + } + } + + if (showFailedAABBs) { + renderer.setStroke(new Vector3(0.0, 0.0, 1.0)); + for (const box of voxelManager.failedAABBs) { + renderer.registerBox(box.centre, box.size, true); + } + } + renderer.compileRegister(); }); - function render(time) { resizeCanvas(); diff --git a/src/hash_map.js b/src/hash_map.js index d064944..ac672ca 100644 --- a/src/hash_map.js +++ b/src/hash_map.js @@ -1,10 +1,14 @@ const { Vector3 } = require('./vector.js'); +const { VoxelManager } = require('./voxel_manager.js'); -class HashMap { +class HashSet { constructor(numBins) { this.numBins = numBins; this.bins = new Array(numBins); + for (let i = 0; i < numBins; ++i) { + this.bins[i] = []; + } } _getBin(key) { @@ -12,49 +16,22 @@ class HashMap { return Math.abs(hash) % this.numBins; } - add(key, value) { + add(key) { const binIndex = this._getBin(key); - console.log(binIndex); - - if (!this.bins[binIndex]) { - this.bins[binIndex] = [ {key: key, value: value} ]; - } else { - this.bins[binIndex].push({key: key, value: value}); - } + this.bins[binIndex].push(key); } - get(key) { + contains(key) { const binIndex = this._getBin(key); - if (!this.bins[binIndex]) { - return; - } - const list = this.bins[binIndex]; for (const item of list) { - if (item.key.equals(key)) { - return item.value; + if (item.equals(key)) { + return true; } } + return false; } } -const hashMap = new HashMap(4); -const v = new Vector3(2.0, 2.0, 3.0); -const v2 = new Vector3(2.0, 2.0, 5.0); -const v3 = new Vector3(-2.0, -2.0, -5.0); -const v4 = new Vector3(-2.0, -5.0, -7.0); - -hashMap.add(v, true); -hashMap.add(v2, true); -hashMap.add(v3, true); - -console.log(hashMap.bins[0]); -console.log(hashMap.bins[1]); -console.log(hashMap.bins[2]); -console.log(hashMap.bins[3]); - -console.log(hashMap.get(v)); -console.log(hashMap.get(v2)); -console.log(hashMap.get(v3)); -console.log(hashMap.get(v4)); \ No newline at end of file +module.exports.HashSet = HashSet; \ No newline at end of file diff --git a/src/renderer.js b/src/renderer.js index 9463984..806ebc0 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -32,6 +32,7 @@ class Renderer { this._voxelSize = voxelSize; this._voxelSizeVector = new Vector3(voxelSize, voxelSize, voxelSize); + this._cube = twgl.primitives.createCubeVertices(1.0); this._registersOpen = true; } @@ -118,7 +119,7 @@ class Renderer { } clear() { - console.log("clearing"); + //console.log("clearing"); this._debugRegister = this._getEmptyDebugRegister(); this._register = this._getEmptyRegister(); @@ -193,12 +194,21 @@ class Renderer { ] }; } else { - let cube = twgl.primitives.createCubeVertices(this._voxelSize); + + let cube = { + position: new Float32Array(72), + normal: new Float32Array(72), + indices: new Float32Array(72) + }; + + cube.position.set(this._cube.position); + cube.normal.set(this._cube.normal); + cube.indices.set(this._cube.indices); for (let i = 0; i < 72; i += 3) { - cube.position[i + 0] += centre.x; - cube.position[i + 1] += centre.y; - cube.position[i + 2] += centre.z; + cube.position[i + 0] = (cube.position[i + 0] * size.x) + centre.x; + cube.position[i + 1] = (cube.position[i + 1] * size.y) + centre.y; + cube.position[i + 2] = (cube.position[i + 2] * size.z) + centre.z; } return cube; @@ -262,6 +272,7 @@ class Renderer { // Use when drawing the same thing each frame registerBox(centre, size, debug) { const data = this._getBoxData(centre, size, debug); + //console.log(data); this._addDataToRegister(data, debug); } @@ -289,9 +300,16 @@ class Renderer { } registerVoxels(voxelCentres, debug) { + /* + for (let i = 0; i < voxelCentres.length; ++i) { + console.log(i / voxelCentres.length); + this.registerVoxel(voxelCentres[i], debug); + } + */ for (const voxelCentre of voxelCentres) { this.registerVoxel(voxelCentre, debug); } + } _cycleDebugRegister() { @@ -307,6 +325,10 @@ class Renderer { //console.log("Cycling Registers"); } + _willDataOverflowBuffer(data) { + + } + _addDataToRegister(data, debug) { if (!this._registersOpen) { console.error("Trying to register object when register is closed. Register before calling compileRegister()"); diff --git a/src/vector.js b/src/vector.js index d720484..63da7cf 100644 --- a/src/vector.js +++ b/src/vector.js @@ -18,6 +18,14 @@ class Vector3 { ); } + static addScalar(vec, scalar) { + return new Vector3( + vec.x + scalar, + vec.y + scalar, + vec.z + scalar + ); + } + static sub(vecA, vecB) { return new Vector3( vecA.x - vecB.x, @@ -26,6 +34,14 @@ class Vector3 { ); } + static subScalar(vec, scalar) { + return new Vector3( + vec.x - scalar, + vec.y - scalar, + vec.z - scalar + ); + } + static dot(vecA, vecB) { return vecA.x * vecB.x + vecA.y * vecB.y + vecA.z * vecB.z; } diff --git a/src/voxel_manager.js b/src/voxel_manager.js index 2e7d6dc..045776c 100644 --- a/src/voxel_manager.js +++ b/src/voxel_manager.js @@ -1,5 +1,6 @@ const { AABB, CubeAABB } = require("./aabb.js"); const { Vector3 } = require("./vector.js"); +const { HashSet } = require('./hash_map.js'); class VoxelManager { @@ -7,6 +8,14 @@ class VoxelManager { this._voxelSize = voxelSize; this.voxels = []; this.failedAABBs = []; + + this.minX = Infinity; // JavaScript crack + this.minY = Infinity; + this.minZ = Infinity; + this.maxX = -Infinity; + this.maxY = -Infinity; + this.maxZ = -Infinity; + this.voxelsHash = new HashSet(2048); } setVoxelSize(voxelSize) { @@ -16,6 +25,14 @@ class VoxelManager { clear() { this.voxels = []; this.failedAABBs = []; + + this.minX = Infinity; // JavaScript crack + this.minY = Infinity; + this.minZ = Infinity; + this.maxX = -Infinity; + this.maxY = -Infinity; + this.maxZ = -Infinity; + this.voxelsHash = new HashSet(2048); } _getTriangleCubeAABB(triangle) { @@ -35,6 +52,151 @@ class VoxelManager { return cubeAABB; } + _voxelCentreToPosition(vec) { + //Vector3.round(Vector3.subScalar(Vector3.divScalar(vec, this._voxelSize), 0.5)); + return new Vector3( + Math.round(vec.x / this._voxelSize), + Math.round(vec.y / this._voxelSize), + Math.round(vec.z / this._voxelSize) + ); + } + + _voxelPositionToCentre(vec) { + return new Vector3( + vec.x * this._voxelSize, + vec.y * this._voxelSize, + vec.z * this._voxelSize + ); + } + + addVoxel(vec) { + // (0.5, 0.5, 0.5) -> (0, 0, 0); + vec = Vector3.subScalar(vec, this._voxelSize / 2); + + const pos = this._voxelCentreToPosition(vec); + if (this.voxelsHash.contains(pos)) { + return; + } + + this.voxels.push(vec); + this.voxelsHash.add(pos, true); + + this.minX = Math.min(this.minX, vec.x); + this.minY = Math.min(this.minY, vec.y); + this.minZ = Math.min(this.minZ, vec.z); + this.maxX = Math.max(this.maxX, vec.x); + this.maxY = Math.max(this.maxY, vec.y); + this.maxZ = Math.max(this.maxZ, vec.z); + } + + _findXExtent(pos) { + let xEnd = pos.x + 1; + + while (this.voxelsHash.contains(new Vector3(xEnd, pos.y, pos.z)) && !this.seen.contains(new Vector3(xEnd, pos.y, pos.z))) { + //console.log("Marking:", new Vector3(xEnd, y, z)); + this.seen.add(new Vector3(xEnd, pos.y, pos.z)); + ++xEnd; + } + + return xEnd - 1; + } + + _findZExtent(pos, xEnd) { + let canMerge = true; + let zEnd = pos.z + 1; + + do { + //console.log("zEnd:", z, zEnd); + for (let i = pos.x; i <= xEnd; ++i) { + const here = new Vector3(i, pos.y, zEnd); + if (!this.voxelsHash.contains(here) || this.seen.contains(here)) { + canMerge = false; + break; + } + } + if (canMerge) { + // Mark all as seen + for (let i = pos.x; i <= xEnd; ++i) { + const here = new Vector3(i, pos.y, zEnd); + //console.log("Marking:", new Vector3(xEnd, y, z)); + this.seen.add(here); + } + ++zEnd; + } + } while (canMerge); + + return zEnd - 1; + } + + _findYExtent(pos, xEnd, zEnd) { + let canMerge = true; + let yEnd = pos.y + 1; + + do { + for (let i = pos.x; i <= xEnd; ++i) { + for (let j = pos.z; j <= zEnd; ++j) { + const here = new Vector3(i, yEnd, j); + if (!this.voxelsHash.contains(here) || this.seen.contains(here)) { + canMerge = false; + break; + } + } + } + + if (canMerge) { + // Mark all as seen + for (let i = pos.x; i <= xEnd; ++i) { + for (let j = pos.z; j <= zEnd; ++j) { + const here = new Vector3(i, yEnd, j); + this.seen.add(here); + } + } + ++yEnd; + } + } while (canMerge); + + return yEnd - 1; + } + + buildMesh() { + + this.mesh = []; + this.seen = new HashSet(2048); + //console.log(this.voxelsHash); + + const minPos = this._voxelCentreToPosition(new Vector3(this.minX, this.minY, this.minZ)); + const maxPos = this._voxelCentreToPosition(new Vector3(this.maxX, this.maxY, this.maxZ)); + + for (let y = minPos.y; y <= maxPos.y; ++y) { + for (let z = minPos.z; z <= maxPos.z; ++z) { + for (let x = minPos.x; x <= maxPos.x; ++x) { + + const pos = new Vector3(x, y, z); + + if (this.seen.contains(pos) || !this.voxelsHash.contains(pos)) { + continue; + } + + let xEnd = this._findXExtent(pos); + let zEnd = this._findZExtent(pos, xEnd); + let yEnd = this._findYExtent(pos, xEnd, zEnd); + + let centre = new Vector3((xEnd + x)/2, (yEnd + y)/2, (zEnd + z)/2); + let size = new Vector3(xEnd - x + 1, yEnd - y + 1, zEnd - z + 1); + + this.mesh.push({ + centre: this._voxelPositionToCentre(centre), + size: this._voxelPositionToCentre(size) + }); + } + } + } + + //console.log("Mesh:", this.mesh); + + return this.mesh; + } + voxeliseTriangle(triangle) { const cubeAABB = this._getTriangleCubeAABB(triangle); @@ -51,7 +213,8 @@ class VoxelManager { } else { // We've reached the voxel level, stop //renderer.registerBox(aabb.centre, aabb.size); - this.voxels.push(aabb.centre); + //this.voxels.push(aabb.centre); + this.addVoxel(aabb.centre); } } else { this.failedAABBs.push(aabb);