chunkrenderer: greedy meshing like 0fps/roboleary

Also includes face culling to avoid rendering faces that are hidden by other blocks
treemaps-queues-iteration
EmaMaker 2022-08-11 15:26:49 +02:00
parent 8fb2af285c
commit 83beccc48c
1 changed files with 193 additions and 54 deletions

View File

@ -11,11 +11,15 @@ 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.VertexBuffer.Type;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.BufferUtils;
public class ChunkRenderer {
@ -97,7 +101,7 @@ public class ChunkRenderer {
Material mat = new Material(mgr.game.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
// mat.getAdditionalRenderState().setWireframe(true);
geom.setMaterial(mat);
geom.getLocalTransform().setTranslation(i - 0.5f, j - 0.5f, k - 0.5f);
geom.getLocalTransform().setTranslation(i + 0.5f, j + 0.5f, k + 0.5f);
chunk.chunkNode.attachChild(geom);
@ -107,9 +111,10 @@ public class ChunkRenderer {
mat.setColor("Color", ColorRGBA.Gray);
} else if (blocks[idx] == Blocks.GRASS) {
mat.setColor("Color", ColorRGBA.Green);
} else {
mat.setColor("Color", ColorRGBA.Red);
}
// else {
// mat.setColor("Color", ColorRGBA.Red);
// }
}
}
}
@ -117,76 +122,210 @@ public class ChunkRenderer {
}
/*
* Taking inspiration from 0fps and the jme3 porting at
* https://github.com/roboleary/GreedyMesh/blob/master/src/mygame/Main.java
*
* By carefully re-reading the code and the blog post, I've come to the
* realization that I wrote something very similar yet a lot messier (and
* uglier) back in
* https://github.com/EmaMaker/voxel-engine-jme3/blob/master/src/voxelengine
* /world/Chunk.java
*
* Reading roboleary's impl. I've learned how to optimize having to loop
* across different planes everytime I change dimension without having to
* write 3 separate 3-nested-for-loops
*/
public void greedyMeshing(WorldManager mgr, Chunk chunk) {
HashMap<Vector3f, Sphere> points = new HashMap<>();
// convert tree to array since it is easier to work with it
Blocks[] blocks = chunk.treeTo1DArray();
System.out.println(Arrays.toString(blocks));
// System.out.println(Arrays.toString(blocks));
Vector3f pos, belowPos;
int idx;
if (blocks.length == 0) return;
for (int j = 0; j < 1; j++) {
Blocks prevBlock = null;
int k, l, u, v, w, h, n, j, i;
int[] x = { 0, 0, 0 };
int[] q = { 0, 0, 0 };
final int[] du = new int[] { 0, 0, 0 };
final int[] dv = new int[] { 0, 0, 0 };
// For now I'm placing little spheres to show vertices instead of
// actually constructing a mesh
// Currently only meshing on XZ plane
Blocks[] mask = new Blocks[Config.CHUNK_SIZE * Config.CHUNK_SIZE];
// boolean backFace = false;
for (boolean backFace = true, b = false; b != backFace; backFace = backFace && b, b = !b) {
// iterate over 3 dimensions
for (int dim = 0; dim < 3; dim++) {
// offsets of other 2 axes
u = (dim + 1) % 3;
v = (dim + 2) % 3;
for (int k = 0; k < Config.CHUNK_SIZE; k++) {
x[0] = 0;
x[1] = 0;
x[2] = 0;
for (int i = 0; i < Config.CHUNK_SIZE; i++) {
idx = Chunk.coord3DTo1D(i, j, k);
pos = new Vector3f(i, j, k);
belowPos = new Vector3f(i, j, k - 1);
q[0] = 0;
q[1] = 0;
q[2] = 0;
q[dim] = 1; // easily mark which dimension we are comparing
// voxels
// on
// Got a new type of block, place a vertex
if (blocks[idx] != prevBlock || prevBlock == null) {
// Check if we are at the start, if so just place
// vertices
if (k == 0) {
points.put(pos, new Sphere(25, 25, 0.25f));
}
for (x[dim] = -1; x[dim] < Config.CHUNK_SIZE;) {
n = 0;
// check if we are at the second step (right after
// start)
// if so, a new vertex needs to be placed whenever a
// quad can be completed
// if a quad cannot be completed but the block has
// changed, also place a vertex 1 lower to complete a
// quad
else if (k == 1) {
points.put(pos, new Sphere(25, 25, 0.25f));
if (blocks[idx] != blocks[Chunk.coord3DTo1D(i, j, k - 1)]) points.put(belowPos, new Sphere(25, 25, 0.25f));
}
// in any other situation
else {
if (blocks[idx] == blocks[Chunk.coord3DTo1D(i, j, k - 1)]) {
points.put(pos, new Sphere(25, 25, 0.25f));
points.remove(belowPos);
System.out.println("should be moving from " + belowPos + " to " + pos);
} else if (blocks[idx] != blocks[Chunk.coord3DTo1D(i, j, k - 1)]) points.put(belowPos, new Sphere(25, 25, 0.25f));
for (x[v] = 0; x[v] < Config.CHUNK_SIZE; x[v]++) {
for (x[u] = 0; x[u] < Config.CHUNK_SIZE; x[u]++) {
Blocks b1 = (x[dim] >= 0) ? blocks[Chunk.coord3DTo1D(x[0], x[1], x[2])] : null;
Blocks b2 = (x[dim] < Config.CHUNK_SIZE - 1) ? blocks[Chunk.coord3DTo1D(x[0] + q[0], x[1] + q[1], x[2] + q[2])] : null;
// This is the original line taken from rob's code, readapted (replace voxelFace with b1 and b2).
// mask[n++] = ((voxelFace != null && voxelFace1 != null && voxelFace.equals(voxelFace1))) ? null : backFace ? voxelFace1 : voxelFace;
// Additionally checking whether b1 and b2 are AIR or null allows face culling, thus not rendering faces that cannot be seen
// Removing the control for null disables chunk borders
// This can be surely refactored in something that isn't such a big one-liner
mask[n++] = b1 != null && b2 != null && b1.equals(b2) ? null : backFace ? b1 == Blocks.AIR || b1 == null ? b2 : null : b2 == Blocks.AIR || b2 == null ? b1 : null;
}
}
x[dim]++;
n = 0;
// Actually generate the mesh from the mask. This is the
// same
// thing I used in my old crappy voxel engine
for (j = 0; j < Config.CHUNK_SIZE; j++) {
for (i = 0; i < Config.CHUNK_SIZE;) {
if (mask[n] != null) {
// First compute the width
for (w = 1; i + w < Config.CHUNK_SIZE && mask[n + w] != null && mask[n].equals(mask[n + w]); w++) {
}
boolean done = false;
for (h = 1; j + h < Config.CHUNK_SIZE; h++) {
for (k = 0; k < w; k++) {
if (mask[n + k + h * Config.CHUNK_SIZE] == null || !mask[n + k + h * Config.CHUNK_SIZE].equals(mask[n])) {
done = true;
break;
}
}
if (done) break;
}
if (mask[n] != Blocks.AIR) {
x[u] = i;
x[v] = j;
du[0] = 0;
du[1] = 0;
du[2] = 0;
du[u] = w;
dv[0] = 0;
dv[1] = 0;
dv[2] = 0;
dv[v] = h;
chunk.chunkNode.attachChild(quad(new Vector3f(x[0], x[1], x[2]), new Vector3f(x[0] + du[0], x[1] + du[1], x[2] + du[2]),
new Vector3f(x[0] + du[0] + dv[0], x[1] + du[1] + dv[1], x[2] + du[2] + dv[2]), new Vector3f(x[0] + dv[0], x[1] + dv[1], x[2] + dv[2]),
mask[n], chunk, mgr, backFace));
// System.out.println(Arrays.toString(chunk.chunkNode.getChildren().toArray()));
}
for (l = 0; l < h; ++l) {
for (k = 0; k < w; ++k) {
mask[n + k + l * Config.CHUNK_SIZE] = null;
}
}
/*
* And then finally increment the counters and
* continue
*/
i += w;
n += w;
} else {
i++;
n++;
}
}
}
prevBlock = blocks[idx];
}
}
for (Vector3f v : points.keySet()) {
System.out.println(v);
Geometry geom = new Geometry("Sphere", points.get(v));
geom.getLocalTransform().setTranslation(v);
}
}
Material mat = new Material(mgr.game.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.getAdditionalRenderState().setWireframe(true);
geom.setMaterial(mat);
final int VOXEL_SIZE = 1;
chunk.chunkNode.attachChild(geom);
Geometry quad(final Vector3f bottomLeft, final Vector3f topLeft, final Vector3f topRight, final Vector3f bottomRight, final Blocks block, final Chunk chunk,
final WorldManager mgr, boolean backFace) {
final Vector3f[] vertices = new Vector3f[4];
vertices[2] = topLeft.multLocal(VOXEL_SIZE);
vertices[3] = topRight.multLocal(VOXEL_SIZE);
vertices[0] = bottomLeft.multLocal(VOXEL_SIZE);
vertices[1] = bottomRight.multLocal(VOXEL_SIZE);
final int[] indexes = backFace ? new int[] { 2, 0, 1, 1, 3, 2 } : new int[] { 2, 3, 1, 1, 0, 2 };
final float[] colorArray = new float[4 * 4];
// System.out.println("A quad delimited by " + Arrays.toString(vertices)
// + " for " + block);
for (int i = 0; i < colorArray.length; i += 4) {
/*
* Here I set different colors for quads depending on the "type"
* attribute, just so that the different groups of voxels can be
* clearly seen.
*
*/
if (block == Blocks.STONE) {
colorArray[i] = 1.0f;
colorArray[i + 1] = 0.0f;
colorArray[i + 2] = 0.0f;
colorArray[i + 3] = 1.0f;
} else if (block == Blocks.DIRT) {
colorArray[i] = 0.0f;
colorArray[i + 1] = 1.0f;
colorArray[i + 2] = 0.0f;
colorArray[i + 3] = 1.0f;
} else {
colorArray[i] = 0.0f;
colorArray[i + 1] = 0.0f;
colorArray[i + 2] = 1.0f;
colorArray[i + 3] = 1.0f;
}
points.clear();
}
Mesh mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
mesh.setBuffer(Type.Color, 4, colorArray);
mesh.setBuffer(Type.Index, 3, BufferUtils.createIntBuffer(indexes));
mesh.updateBound();
Geometry geo = new Geometry("ColoredMesh", mesh);
Material mat = new Material(mgr.game.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setBoolean("VertexColor", true);
/*
* To see the actual rendered quads rather than the wireframe, just
* comment outthis line.
*/
// mat.getAdditionalRenderState().setWireframe(true);
// mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Front);
geo.setMaterial(mat);
return geo;
}
}
}