Mesh vertex optimization problem

I have a problem in my block world with my mesh optimization. I have optimized my custom mesh generation to not add vertices for block faces that are not visible. In fact this was already done for me in the cubes framework. However that framework did not remove chunk boundary vertices, and in a 16x256x16 size chunk that makes a BIG difference especially since I am loading a 10 chunk radius around the player.

So I added the code to remove chunk boundaries and it was a big deal because my game was unplayable without it. However, when breaking a block on a chunk boundary, it will show a hole in the world since those face vertices are not there on the neighboring chunk. Well, no problem I just mark that neighbor chunk as dirty. This mostly works except now there is a frame where the hole is still visible.

One key here is that I submit a Callable to a ScheduledThreadPoolExecutor for building my mesh. There are no priorities on this scheduling. I thought that it might be a timing issue where the neighbor chunk was getting rebuilt after the current chunk so I tested that theory by making my chunk loads be threaded but my updates run on the main thread. This was slow but it did fix the problem so itā€™s certainly a timing issue. Itā€™s like I need both meshes rebuilt in a threaded manner but added to the scene at the same time. Not sure if thatā€™s possible.

Whew, hope you are still reading. Are there any suggestions about what I am doing here? The game is mostly smooth and runs at 60 fps but breaking those border blocks and seeing a flicker is pretty annoying and greatly affects gameplay in my opinion.

Any help is appreciated.

Post some code and pictures.

Well itā€™s a lot of code but Iā€™ll try to post the important parts. The pictures are impossible because the problem is only visible for a frame or 2.

Here is my BlockTerrainControl

	@Override
protected void controlUpdate(final float lastTimePerFrame)
{
	world.update(lastTimePerFrame);
	world.calculateLight();
	updateSpatial();
}

public void updateSpatial()
{
	profiler.startSection("ChunkSpatial");
	
	//Update meshes
	for(BlockChunkControl chunk : chunks.values())
	{
		chunk.updateSpatial();
	}
	//Add chunks
	Chunk addedChunk = world.chunkRenderQueue.poll();
	if(addedChunk != null)//Game runs smoother when we load 1 chunk per frame for some reason
	{
		BlockChunkControl control = new BlockChunkControl(this, addedChunk);
		this.spatial.addControl(control);
		chunks.put(ChunkCoordIntPair.chunkXZ2Int(addedChunk.location.x, addedChunk.location.z), control);
	}
	
	//Remove chunks
	Chunk removedChunk = world.chunkUnloadQueue.poll();
	if(removedChunk != null)
	{
		long chunkKey = ChunkCoordIntPair.chunkXZ2Int(removedChunk.location.x, removedChunk.location.z);
		BlockChunkControl control = chunks.get(chunkKey);
		if(control != null)
		{
			control.detachNode();
			chunks.remove(chunkKey);
		}
	}
	
	profiler.endSection();
}

The BlockChunkControl class

	public boolean updateSpatial()
{
	if(chunk.needsMeshUpdate)
	{
		if(opaqueMeshFuture == null && transparentMeshFuture == null)
		{
			opaqueMeshFuture = terrain.world.executor.submit(opaqueMeshBuilder);
			transparentMeshFuture = terrain.world.executor.submit(transparentMeshBuilder);
		}
		else
		{
			if(opaqueMeshFuture.isDone() && transparentMeshFuture.isDone())
			{
				if(optimizedGeometry_Opaque == null)
				{
					optimizedGeometry_Opaque = new Geometry("");
					optimizedGeometry_Opaque.setQueueBucket(Bucket.Opaque);
					node.attachChild(optimizedGeometry_Opaque);
					optimizedGeometry_Opaque.setMaterial(terrain.getSettings().getBlockMaterial());
				}
				if(optimizedGeometry_Transparent == null)
				{
					optimizedGeometry_Transparent = new Geometry("");
					optimizedGeometry_Transparent.setQueueBucket(Bucket.Transparent);
					node.attachChild(optimizedGeometry_Transparent);
					optimizedGeometry_Transparent.setMaterial(terrain.getSettings().getBlockMaterial());
				}
				try
				{
					optimizedGeometry_Opaque.setMesh(opaqueMeshFuture.get());
					optimizedGeometry_Transparent.setMesh(transparentMeshFuture.get());
				}
				catch(Exception e)
				{
					e.printStackTrace();
					//Try to generate it non threaded
					optimizedGeometry_Opaque.setMesh(MeshGenerator.generateOptimizedMesh(chunk, false));
					optimizedGeometry_Transparent.setMesh(MeshGenerator.generateOptimizedMesh(chunk, true));
				}
				opaqueMeshFuture = null;
				transparentMeshFuture = null;
				chunk.needsMeshUpdate = false;
			}
		}
		return true;
	}
	return false;
}

private class MeshBuilder implements Callable<Mesh>
{
	private final Chunk chunk;
	private final boolean isTransparent;
	
	public MeshBuilder(Chunk chunk, boolean isTransparent)
	{
		this.chunk = chunk;
		this.isTransparent = isTransparent;
	}
	
	@Override
	public Mesh call() throws Exception
	{
		return MeshGenerator.generateOptimizedMesh(this.chunk, this.isTransparent);
	}
}

Hope that helps

Is this world all procedure generated ? Voxel like minecraft ?

Mostly, except Iā€™m using the jme3 SkyControl plugin for the sky.

I think generating both chunks together is really the only way around this. You ā€˜justā€™ (I know not trivial) change the unit upon which your background threads are operatingā€¦ instead of single chunks, potentially sets of chunks.

@pspeed I was praying no one would say thatā€¦lol.

@pspeed is my architecture just wrong here? Do you not thread mesh building in Mythruna? It seems like minecraft and other games of this sort must do this.

Sure I thread mesh building. But the only way to avoid gaps in your chunks is to build them both at the same time. This is not mutually exclusive with threading.

That being said, I do use a priority queue for mine and give neighbors an earlier priority than the chunk Iā€™m in. I canā€™t remember if I cycle all chunks below a certain priority before updating the scene, though. As I recall, the most jarring blinks are when a block is removed so generating the neighbors first fixes that one.

My architecture is slightly difficult to unravel for this question because itā€™s complicated by separate relighting and mesh generationā€¦ not to mention that I have two completely different engines now and my memory gets muddy between the two without diving in.

Thanks. A priority queue seems easier to implement than trying to make sets of chunks that get updated. Plus it seems like it might seem laggy if I only update sets of chunks at a time.

Side question if you donā€™t mind. Do you do junit tests on your ā€œbusiness logicā€? Or does threading make that pretty much impossible in most cases?

Well, somehow you are submitting a callable to a thread pool. One way is a chunk per callable One way is array of chunks per callable.

Either way, you will have to have both chunks built before being display or you will see a gap. So if there is going to be lag there will be lag and your choice would be between lag and gaps. But really, if you are seeing lag then chunk generation is taking too long anyway.

I believe unit tests have limited utility in general and especially in game development.

Hereā€™s the math I use:

  1. unit tests are best for determining regressions during refactoring. Itā€™s like 95% of their reason for existing.
  2. unit tests will never catch all regressions.
  3. testing the game itself will find most regressions.
  4. you have to test the game anyway.
  5. a significant portion of the time, unit test regressions turn out to just be problems with the unit tests, ie: more work to no good end.
  6. without 100% code coverage the gap in (2) is so huge to make the whole process nearly irrelevant.

Most of the real bugs you will have trouble with will not be caught by (2) or (3)ā€¦ the time writing unit tests could have been better spent testing/debugging those gaps.

ā€¦though I suppose it depends on your definition of ā€œunit testā€. I come from some formal software dev environments so I use the real meaning. Some people use ā€œunit testā€ to mean ā€œautomated tests that run with my buildā€. I would call those automated tests that run with my buildā€¦ which I also donā€™t do that often but there is a lot of utility there.

The unit testing we have been doing at my day job over the last two years has reaped significant measurable benefits. Although a business app is much more unit testable than a game app because of it seems game apps depend a lot on graphics rendering. I have refactored my code to try and separate logic like lighting and physics and overall world model to prepare for unit testing in the future if needed.

Not to get on a soap box about unit testing butā€¦

  1. I have found that unit testing causes me to catch more bugs while developing, and without having to run the app
  2. I agree that when unit tests break, itā€™s likely the test not the code. Though not always

Anyway, I have discovered the game dev community has a negative skew on unit testing and I am finding out why with my experience. I was just curious about your opinion.

Some of it is down to development style. I tend to prefer highly iterative approaches where I can have something running, build it in steps, and run each step as I go. I find bugs quickly. I also shun debuggers (personally) because I feel they dull my ability to see the bugs in the code before I run it. (This is also why I can often spot bugs in other peoplesā€™ code just by glancing through it.) Each personā€™s experience will vary and the tools they need to make themselves productive will also varyā€¦ but often we get in a mindset that something is good without really think about the costs.

Unit tests make a lot of sense when:
-the code is predictable
-100% code coverage is achievable
-others will be using your code and depending on its behavior.

Else, if itā€™s just you developing then there are other strategies that can be just as, if not more, effective in the long run. Game development tends to be signified by pivots and wholesale cuttingā€¦ all of those unit tests would have been wasted time.

I have open source libraries with 100% unit test coverage. Iā€™m not against it but itā€™s a heavy axe to wield and not suitable for a lot of problems. Itā€™s an extremely expensive practice as often unit test development time meets or exceeds the cost of actual code development. Sometimes itā€™s worth it. Often not.

Like, if I make a mathd OSS library then Iā€™d consider unit testing it (even if those tests are 90% ā€˜did this field really get set when I set itā€™ā€¦ grr.) but like the IsoSurface stuff would be hard to unit test in any meaningful way. As with most libraries higher than the absolute lowest level, some thoughtful integration tests would be waaay better.

Itā€™s also possible that I have a tapestry of ā€œstrange practicesā€ that shield me from some of the benefits of unit tests. In large teams following the ā€œrational unified processā€, it was a convenient way to keep entry-level devs busy and learning, though. :slight_smile:

1 Like