From dd4732c1c194ffdc04a92bace2e0364b7a742a0b Mon Sep 17 00:00:00 2001 From: EmaMaker Date: Tue, 16 Aug 2022 21:50:43 +0200 Subject: [PATCH] only generate and mesh one chunk per loop cycle add setup for voxel size modeling/meshing needs optimization switch to using a bitfield instead of queues --- .gitignore | 5 +- .../voxeltest/intervaltrees/Voxel.java | 20 ++-- .../intervaltrees/renderer/ChunkRenderer.java | 99 ++-------------- .../voxeltest/intervaltrees/utils/Config.java | 3 + .../voxeltest/intervaltrees/utils/Utils.java | 5 +- .../voxeltest/intervaltrees/world/Chunk.java | 43 ++++--- .../intervaltrees/world/WorldManager.java | 112 +++++++++++++++--- 7 files changed, 158 insertions(+), 129 deletions(-) diff --git a/.gitignore b/.gitignore index 1e584a5..0515374 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +.idea .vscode .gradle bin/** -bin \ No newline at end of file +bin +build/ +hs_* diff --git a/src/main/java/com/emamaker/voxeltest/intervaltrees/Voxel.java b/src/main/java/com/emamaker/voxeltest/intervaltrees/Voxel.java index de679c4..dd1bbb0 100644 --- a/src/main/java/com/emamaker/voxeltest/intervaltrees/Voxel.java +++ b/src/main/java/com/emamaker/voxeltest/intervaltrees/Voxel.java @@ -1,7 +1,9 @@ package com.emamaker.voxeltest.intervaltrees; +import com.emamaker.voxeltest.intervaltrees.data.IntervalMap; import com.emamaker.voxeltest.intervaltrees.utils.Config; import com.emamaker.voxeltest.intervaltrees.world.WorldManager; +import com.emamaker.voxeltest.intervaltrees.world.blocks.Blocks; import com.jme3.app.SimpleApplication; import com.jme3.math.Vector3f; import com.jme3.renderer.RenderManager; @@ -15,36 +17,34 @@ import com.jme3.util.BufferUtils; public class Voxel extends SimpleApplication { WorldManager worldManager = new WorldManager(this); - Vector3f oldCamPos = new Vector3f(), pos = new Vector3f(); + public Vector3f oldCamPos = new Vector3f(), pos = new Vector3f(); public static void main(String[] args) { Voxel app = new Voxel(); - app.setShowSettings(false); // Settings dialog not supported on mac + app.setShowSettings(true); // Settings dialog not supported on mac app.start(); BufferUtils.setTrackDirectMemoryEnabled(true); - } @Override public void simpleInitApp() { - getFlyByCamera().setMoveSpeed(20f); - getCamera().setLocation(new Vector3f(32f, 32f, 32f)); + getFlyByCamera().setMoveSpeed(40f); + getCamera().setLocation(new Vector3f(Config.RENDER_DISTANCE, Config.RENDER_DISTANCE, Config.RENDER_DISTANCE).mult(Config.CHUNK_SIZE)); getCamera().lookAt(new Vector3f(Config.CHUNK_SIZE,Config.CHUNK_SIZE, Config.CHUNK_SIZE), Vector3f.UNIT_Y); + pos.set(this.getCamera().getLocation()); worldManager.initWorld(); - worldManager.render(); } - - @Override public void simpleUpdate(float tpf) { pos.set(this.getCamera().getLocation()); // if (!(pos.equals(oldCamPos))) System.out.println(pos); oldCamPos.set(pos); - } + worldManager.render(); + } + @Override public void simpleRender(RenderManager rm) { - // add render code here (if any) } } diff --git a/src/main/java/com/emamaker/voxeltest/intervaltrees/renderer/ChunkRenderer.java b/src/main/java/com/emamaker/voxeltest/intervaltrees/renderer/ChunkRenderer.java index a7a100e..c74745c 100644 --- a/src/main/java/com/emamaker/voxeltest/intervaltrees/renderer/ChunkRenderer.java +++ b/src/main/java/com/emamaker/voxeltest/intervaltrees/renderer/ChunkRenderer.java @@ -1,99 +1,25 @@ package com.emamaker.voxeltest.intervaltrees.renderer; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Queue; - -import com.emamaker.voxeltest.intervaltrees.data.IntervalTree; import com.emamaker.voxeltest.intervaltrees.utils.Config; import com.emamaker.voxeltest.intervaltrees.world.Chunk; import com.emamaker.voxeltest.intervaltrees.world.WorldManager; import com.emamaker.voxeltest.intervaltrees.world.blocks.Blocks; import com.jme3.material.Material; -import com.jme3.material.RenderState.FaceCullMode; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.Mesh; -import com.jme3.scene.Node; import com.jme3.scene.VertexBuffer.Type; import com.jme3.scene.shape.Box; import com.jme3.util.BufferUtils; +import java.util.Arrays; + public class ChunkRenderer { - // Just render a full cube for every block - public void stupidMeshing(WorldManager mgr, Chunk chunk) { - // Breadth-first visit each node of the tree - chunk.blocks.print(); - - Queue.TreeNode> queue = new ArrayDeque<>(); - queue.add(chunk.blocks.getRoot()); - - IntervalTree.TreeNode t; - Box b; - Geometry geom; - Material mat; - int[] coords1, coords2; - - while (!queue.isEmpty()) { - t = queue.poll(); - if (t.getLeft() != null) - queue.add(t.getLeft()); - if (t.getRight() != null) - queue.add(t.getRight()); - - if (!t.getValue().equals(Blocks.AIR)) { - - int start = t.getInterval().getLow(); - for (int i = t.getInterval().getLow(); i <= t.getInterval().getHigh(); i++) { - - if ((i % Config.CHUNK_SIZE == 0 && i != t.getInterval().getLow()) - || i == t.getInterval().getHigh()) { - if (i == t.getInterval().getHigh()) - i++; - b = new Box((i - start) * 0.5f, 0.5f, 0.5f); - - geom = new Geometry("Box", b); - mat = new Material(mgr.game.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.getAdditionalRenderState().setWireframe(true); - geom.setMaterial(mat); - - coords1 = Chunk.coord1DTo3D(start); - coords2 = Chunk.coord1DTo3D(i); - - geom.getLocalTransform().setTranslation(coords1[0] + b.xExtent, coords1[1] + b.yExtent, - coords1[2] + b.zExtent); - System.out.println(Arrays.toString(coords1) + "-" + Arrays.toString(coords2) - + "\t\tBox with extext " + b.xExtent + " (both directions) with center" - + geom.getLocalTransform().getTranslation()); - - chunk.chunkNode.attachChild(geom); - - if (t.getValue() == Blocks.DIRT) { - mat.setColor("Color", ColorRGBA.Brown); - } else if (t.getValue() == Blocks.STONE) { - mat.setColor("Color", ColorRGBA.Gray); - } else if (t.getValue() == Blocks.GRASS) { - mat.setColor("Color", ColorRGBA.Green); - } - - start = i; - } - } - - // System.out.println("Placing a box long " + b.xExtent + " at " - // + Arrays.toString(coords)); - - } - } - } - + Blocks[] blocks; public void stupidArrayMeshing(WorldManager mgr, Chunk chunk) { - Blocks[] blocks = chunk.treeTo1DArray(); + blocks = chunk.treeTo1DArray(); int idx; for (int i = 0; i < Config.CHUNK_SIZE; i++) { @@ -149,21 +75,22 @@ public class ChunkRenderer { * containing all the quads. In the future, maybe traslucent blocks and liquids * will need a separate mesh, but still on a per-chunk basis */ - Vector3f[] vertices = new Vector3f[Config.CHUNK_3DCOORD_MAX_INDEX + 1]; - float[] colors = new float[(Config.CHUNK_3DCOORD_MAX_INDEX + 1) * 4]; - short[] indices = new short[(Config.CHUNK_3DCOORD_MAX_INDEX + 1) * 4]; + Vector3f[] vertices = new Vector3f[(Config.CHUNK_3DCOORD_MAX_INDEX + 1) * 8]; + float[] colors = new float[(Config.CHUNK_3DCOORD_MAX_INDEX + 1) * 8]; + short[] indices = new short[(Config.CHUNK_3DCOORD_MAX_INDEX + 1) * 8]; short vIndex = 0; short iIndex = 0; short cIndex = 0; public void greedyMeshing(WorldManager mgr, Chunk chunk) { + vIndex = 0; iIndex = 0; cIndex = 0; // convert tree to array since it is easier to work with it - Blocks[] blocks = chunk.treeTo1DArray(); + blocks = chunk.treeTo1DArray(); // System.out.println(Arrays.toString(blocks)); @@ -328,10 +255,10 @@ public class ChunkRenderer { // System.out.println(vIndex + ", " + iIndex); - vertices[vIndex] = bottomLeft; - vertices[vIndex + 1] = bottomRight; - vertices[vIndex + 2] = topLeft; - vertices[vIndex + 3] = topRight; + vertices[vIndex] = bottomLeft.mult(Config.VOXEL_SIZE); + vertices[vIndex + 1] = bottomRight.mult(Config.VOXEL_SIZE); + vertices[vIndex + 2] = topLeft.mult(Config.VOXEL_SIZE); + vertices[vIndex + 3] = topRight.mult(Config.VOXEL_SIZE); if (backFace) { indices[iIndex] = (short) (vIndex + 2); diff --git a/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Config.java b/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Config.java index c89c01e..7e7d321 100644 --- a/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Config.java +++ b/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Config.java @@ -8,5 +8,8 @@ public class Config { public static int CHUNK_SIZE = 16; // return x + maxX * (y + z * maxY); public static int CHUNK_3DCOORD_MAX_INDEX = (CHUNK_SIZE-1) + CHUNK_SIZE * ( (CHUNK_SIZE - 1) + (CHUNK_SIZE - 1) * CHUNK_SIZE); + public static int RENDER_DISTANCE = 8; + public static int VOXEL_SIZE = 1 ; + public static int CHUNK_LENGTH = CHUNK_SIZE * VOXEL_SIZE; } diff --git a/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Utils.java b/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Utils.java index adba5c8..34fc142 100644 --- a/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Utils.java +++ b/src/main/java/com/emamaker/voxeltest/intervaltrees/utils/Utils.java @@ -1,7 +1,10 @@ package com.emamaker.voxeltest.intervaltrees.utils; public class Utils { - + + public static boolean withinDistance(int startx, int starty, int startz, int x, int y, int z, int dist) { + return (x-startx)*(x-startx) + (y - starty)*(y-starty) + (z-startz)*(z-startz) <= dist*dist; + } // https://stackoverflow.com/questions/20266201/3d-array-1d-flat-indexing //flatten 3d coords to 1d array cords public static int coord3DTo1D (int x, int y, int z, int maxX, int maxY, int maxZ){ diff --git a/src/main/java/com/emamaker/voxeltest/intervaltrees/world/Chunk.java b/src/main/java/com/emamaker/voxeltest/intervaltrees/world/Chunk.java index ae6ef11..daa41c7 100644 --- a/src/main/java/com/emamaker/voxeltest/intervaltrees/world/Chunk.java +++ b/src/main/java/com/emamaker/voxeltest/intervaltrees/world/Chunk.java @@ -1,12 +1,6 @@ package com.emamaker.voxeltest.intervaltrees.world; -import java.util.ArrayDeque; -import java.util.Queue; -import java.util.TreeMap; - -import com.emamaker.voxeltest.intervaltrees.data.Interval; import com.emamaker.voxeltest.intervaltrees.data.IntervalMap; -import com.emamaker.voxeltest.intervaltrees.data.IntervalTree; import com.emamaker.voxeltest.intervaltrees.utils.Config; import com.emamaker.voxeltest.intervaltrees.utils.Utils; import com.emamaker.voxeltest.intervaltrees.world.blocks.Blocks; @@ -18,15 +12,19 @@ public class Chunk { public Vector3f pos; public IntervalMap blocks = new IntervalMap<>(); public Node chunkNode = new Node(); - public boolean loaded = false; - + // A bit field representing the state of the chunk, where each bit is an operation done on the chunk + private byte state = 0; + // Convenient access to the bit field, each state is the position in the byte (0 is LSB) + public static byte CHUNK_STATE_GENERATED = 1; + public static byte CHUNK_STATE_MESHED = 2; + public static byte CHUNK_STATE_LOADED = 4; public Chunk() { this(Vector3f.ZERO); } public Chunk(Vector3f pos_) { this.pos = pos_; - chunkNode.setLocalTranslation(pos.mult(Config.CHUNK_SIZE)); + chunkNode.setLocalTranslation(pos.mult(Config.CHUNK_SIZE).mult(Config.VOXEL_SIZE)); // I still have to decided if this is necessary. With an empty tree this // takes O(1) @@ -34,6 +32,17 @@ public class Chunk { blocks.insert(0, Config.CHUNK_3DCOORD_MAX_INDEX, Blocks.AIR); } + public void setState(byte nstate, boolean value){ + if(value) state |= nstate; + else state &= ~nstate; + } + + public boolean bgetState(byte nstate){ + return (state & nstate) != 0; + } + public int getState(byte nstate){ + return state & nstate; + } public static int coord3DTo1D(int x, int y, int z) { return Utils.coord3DTo1D(x, y, z, Config.CHUNK_SIZE, Config.CHUNK_SIZE, Config.CHUNK_SIZE); } @@ -42,6 +51,14 @@ public class Chunk { return Utils.coord1DTo3D(idx, Config.CHUNK_SIZE, Config.CHUNK_SIZE, Config.CHUNK_SIZE); } + /* + * Set blocks. Interval to be intended in the same way as in interval tree + * (flatten 1d index of 3d coords) + */ + public void setBlocks(Blocks block, int intLow, int intHigh) { + this.blocks.insert( Math.max(0,intLow), Math.min(Config.CHUNK_3DCOORD_MAX_INDEX+1, intHigh),block); + } + public Blocks getBlock(int x, int y, int z) { return blocks.valueAtKey(Chunk.coord3DTo1D(x, y, z)); } @@ -108,14 +125,6 @@ public class Chunk { this.treeFrom1DArray(array); } - /* - * Set blocks. Interval to be intended in the same way as in interval tree - * (flatten 1d index of 3d coords) - */ - public void setBlocks(Blocks block, int intLow, int intHigh) { - this.blocks.insert( Math.max(0,intLow), Math.min(Config.CHUNK_3DCOORD_MAX_INDEX+1, intHigh),block); - } - public Blocks[] treeTo1DArray() { // System.out.println(Config.CHUNK_3DCOORD_MAX_INDEX); Blocks[] result = new Blocks[Config.CHUNK_3DCOORD_MAX_INDEX + 1]; diff --git a/src/main/java/com/emamaker/voxeltest/intervaltrees/world/WorldManager.java b/src/main/java/com/emamaker/voxeltest/intervaltrees/world/WorldManager.java index 9fdb5b3..32da052 100644 --- a/src/main/java/com/emamaker/voxeltest/intervaltrees/world/WorldManager.java +++ b/src/main/java/com/emamaker/voxeltest/intervaltrees/world/WorldManager.java @@ -1,42 +1,126 @@ package com.emamaker.voxeltest.intervaltrees.world; -import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.HashMap; import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; import com.emamaker.voxeltest.intervaltrees.Voxel; import com.emamaker.voxeltest.intervaltrees.renderer.ChunkRenderer; +import com.emamaker.voxeltest.intervaltrees.utils.Config; +import com.emamaker.voxeltest.intervaltrees.utils.Utils; import com.jme3.math.Vector3f; +import com.jme3.scene.Node; public class WorldManager { - public final Voxel game; + public final Voxel game; + + public int camx, camy, camz; ChunkRenderer chunkRenderer = new ChunkRenderer(); - HashMap chunks = new HashMap<>(); - Queue chunkToModel = new ArrayDeque(); - ArrayList chunksToRender = new ArrayList<>(); + Node worldNode = new Node(); + + ConcurrentHashMap chunks = new ConcurrentHashMap<>(); public WorldManager(final Voxel game_) { this.game = game_; } public void initWorld() { - chunks.put(Vector3f.ZERO, new Chunk()); - chunks.get(Vector3f.ZERO).arrayGenerateCorner(); + // game.getRootNode().attachChild(worldNode); + // System.out.println(Arrays.toString(game.getRootNode().getChildren().toArray())); - chunkToModel.add(chunks.get(Vector3f.ZERO)); + +// Chunk c = new Chunk(new Vector3f(0,0,0)); +// c.arrayGenerateCorner(); +// chunkRenderer.greedyMeshing(this, c); +// game.getRootNode().attachChild(c.chunkNode); +// chunks.put(new Vector3f(0,0,0), new Chunk(new Vector3f(0,0,0))); +// chunks.get(new Vector3f(0,0,0)).generatePlane(); +// chunkRenderer.stupidArrayMeshing(this, chunks.get(new Vector3f(0,0,0))); +// game.getRootNode().attachChild(chunks.get(new Vector3f(0,0,0)).chunkNode); + +// chunks.put(new Vector3f(0,0,1), new Chunk(new Vector3f(0,0,1))); +// chunks.get(new Vector3f(0,0,1)).generatePlane(); +// chunkRenderer.stupidArrayMeshing(this, chunks.get(new Vector3f(0,0,1))); +// game.getRootNode().attachChild(chunks.get(new Vector3f(0,0,1)).chunkNode); +// chunks.put(new Vector3f(0,0,2), new Chunk(new Vector3f(0,0,2))); +// chunks.get(new Vector3f(0,0,2)).generatePlane(); +// chunkRenderer.stupidArrayMeshing(this, chunks.get(new Vector3f(0,0,2))); +// game.getRootNode().attachChild(chunks.get(new Vector3f(0,0,2)).chunkNode); } + Chunk c; + Vector3f v = new Vector3f(); + int chunkX, chunkY, chunkZ; + public boolean generated = false, meshed = false; + public void render() { - for (Chunk c : chunkToModel) { - // chunkRenderer.stupidArrayMeshing(this, c); - chunkRenderer.greedyMeshing(this, c); - - game.getRootNode().attachChild(c.chunkNode); + camx = (int) (game.pos.getX()); + camy = (int) (game.pos.getY()); + camz = (int) (game.pos.getZ()); + + // // clamp to "chunk" coordinates + // // The chunk with origin at x,y,z in chunk coords actually has the origin at + // x*chunksize, y*chunksize, z*chunksize in world coords + chunkX = (int) (camx / Config.CHUNK_LENGTH); + chunkY = (int) (camy / Config.CHUNK_LENGTH); + chunkZ = (int) (camz / Config.CHUNK_LENGTH); + + // System.out.println("camera at" + game.pos + new Vector3f(chunkX, chunkY, + // chunkZ)); + + for (int i = Math.max(0, chunkX - Config.RENDER_DISTANCE); i < chunkX + Config.RENDER_DISTANCE; i++) { + for (int j = Math.max(0, chunkY - Config.RENDER_DISTANCE); j < chunkY + Config.RENDER_DISTANCE; j++) { + // for (int j = Math.max(0,chunkY - Config.RENDER_DISTANCE); j < chunkY + + // Config.RENDER_DISTANCE; j++) { + for (int k = Math.max(0, chunkZ - Config.RENDER_DISTANCE); k < chunkZ + Config.RENDER_DISTANCE; k++) { + if(!Utils.withinDistance(chunkX, chunkY, chunkZ, i,j,k,Config.RENDER_DISTANCE)) continue; + + v = new Vector3f(i, j, k); // this part has to be revised, i can see spikes in memory usage + + if (chunks.get(v) == null) { + c = new Chunk(v); + c.arrayGenerateCorner(); + chunkRenderer.greedyMeshing(this, c); + game.getRootNode().attachChild(c.chunkNode); + chunks.put(v, c); + } + v = null; + } + } } + for(Chunk c : chunks.values()){ + if(!generated && !c.bgetState(Chunk.CHUNK_STATE_GENERATED)) { + c.arrayGenerateCorner(); + c.setState(Chunk.CHUNK_STATE_GENERATED, true); + generated = true; + } + if(!meshed && c.bgetState(Chunk.CHUNK_STATE_GENERATED)&& !c.bgetState(Chunk.CHUNK_STATE_MESHED)) { + chunkRenderer.greedyMeshing(this, c); + c.setState(Chunk.CHUNK_STATE_MESHED, true); + meshed = true; + } + if(!c.bgetState(Chunk.CHUNK_STATE_LOADED) && c.bgetState(Chunk.CHUNK_STATE_GENERATED) && c.bgetState(Chunk.CHUNK_STATE_MESHED)){ + game.getRootNode().attachChild(c.chunkNode); + c.setState(Chunk.CHUNK_STATE_LOADED, true); + } + if(!Utils.withinDistance(chunkX, chunkY, chunkZ, (int)c.pos.x, (int)c.pos.y, (int)c.pos.z, Config.RENDER_DISTANCE)){ + c.chunkNode.removeFromParent(); + chunks.remove(c.pos); + } + } + + + } + + public boolean chunkInCameraLimits(Chunk c) { + int vx = (int) c.pos.x; + int vy = (int) c.pos.y; + int vz = (int) c.pos.z; + return vx >= Math.min(0, chunkX - Config.RENDER_DISTANCE ) && vx < Math.min(0, chunkX + Config.RENDER_DISTANCE) && vy >= chunkY - Config.RENDER_DISTANCE && vy < Math.min(0, chunkY + Config.RENDER_DISTANCE) && vz >= chunkZ - Config.RENDER_DISTANCE && vz < Math.min(0, chunkZ + Config.RENDER_DISTANCE); } }