Zylann / godot_voxel

Voxel module for Godot Engine
MIT License
2.59k stars 244 forks source link

Terrain-independent multimesh culling and LOD settings #571

Open AimiIsFat opened 10 months ago

AimiIsFat commented 10 months ago

Is your feature request related to a problem? Please describe. I recently had to switch from LOD terrain to non-LOD terrain as I'm developing a multiplayer game and LOD terrain currently does not support multiplayer and I doubt I have the skills (or time, as I am very close to the early access release deadline) to develop a custom solution for that.

The switch went fairly well, except one thing: Rendering all the grass kills your frame rate, even after optimization.

This was an issue I had with LOD terrain as well but to a lesser degree since you can at least limit it to only the first LOD, which was still too much in my opinion, especially for a small mesh that is repeated tens of thousands* of times and only needs maybe ~10 meters at most for the first LOD. However, with normal terrain, it is not possible to limit it at all, and it makes it impossible to increase render distance without either getting rid of the grass or decreasing density, which has already been decreased by a lot.

*To be specific, there are 53,203 grass meshes in the scene below. My computer could handle several times the render distance of that scene if this issue were to be solved, as that grass will kill my frame rate otherwise.

Here is the scene fairly optimized. I haven't further optimized the grass mesh yet but that will probably not get many more frames as the main issue here is the distance at which grass generates and the fact that I cannot implement proper LOD.

image

(Grass is made by me, though the trees are not mine.)

Describe the solution you'd like I would like a way to use configurable distance-based LOD rather than the current chunk's LOD level. LOD distance shouldn't be uniform for every single object, as small-but-numerous objects such as grass and small rocks require very short distances between LOD levels, as well as a cull distance, while large and rare objects often need large distances between LOD levels.

Perhaps make a separate version of the VoxelInstanceLibraryMultiMeshItem (that is a rather long name) that is instead based on distance, as there is no LOD system for non-LOD terrain.

At the very least, I need a way to cull grass meshes past a certain distance. A simple "cull distance" option would be fine in this case.

Adding multiplayer support to LOD terrain would fix a good portion of the issue but it still wouldn't allow for the level of LOD control that should be a requirement for any game with large amounts of grass.

Describe alternatives you've considered I tried optimizing the grass voxel instance and shader which did bring frames up a fair amount but the issue here is the sheer number of triangles rendering on the screen at the same time. There is no real way to fix this except an LOD system, which is not supported on non-LOD terrain.

I have also tried removing grass altogether, but the scene looks very bad without it. It isn't quite an option to just remove it completely.

Godot's built-in LOD system also seems to have zero effect on anything. Only thing I could get working was distance fade using a standard material but that would not work for this specific scenario, as the grass requires a special shader.

Zylann commented 10 months ago

This instancing system was originally designed for VoxelLodTerrain, and is also designed to re-use the meshes of the terrain. It only works on VoxelTerrain because it was requested, and it wasnt too much work. But indeed, that results in it not benefiting at all from chunk LOD since there is no chunk LOD. And draw calls are high as well, unless maybe you use chunk size 32.

Making this terrain-independent isn't as easy as having a heightmap to decouple all these things. Here it's much harder because this system needs the meshes to spawn things, and they have to match instancing chunks to minimize overhead from fetching triangles and updating multimeshes. If meshes have no LOD, then it will be impacted regardless. Trying to changing it to somehow have LOD when the terrain doesn't only makes it harder to maintain because it still is primarily meant for VoxelLodTerrain. Creating a whole other system only creates more work to maintain multiple systems... so unless there is a way to have a system generic enough and simple enough working in both terrains, I don't know what will be doable here. Also if there was a terrain-independent way to do this, then it wouldnt even have to be in this module. Maybe only need a few hooks, but we'd have to know which ones. At the moment I haven't got the bandwidth to either work or even discuss medium/large design changes, I have other things in progress. But I'll eventually get working in this area in the future during a next iteration of the demo project that uses it (Solar System).

There is however a secondary LOD system which you might have overlooked: each multimesh item can have up to 4 LODs, which are distance-based within a given "chunk LOD". Distance ratios are hardcoded (0.0, 0.1, 0.25, 0.5, 1.0) but it seems to be running too with VoxelTerrain. This secondary LOD is applied per chunk, and just changes the mesh of all instances in the chunk based on distance. It doesn't use Godot's LOD system because it pre-dates it (also not sure if there is an API to control it?). It will not prevent instances from being added all the way, but at least that could help decimate vertex count in the distance, as I see each of your grass instances have many vertices. Later on perhaps it could allow setting "no mesh" for one of these LODs or have more levels.

As for Godot's LOD system not doing anything... it's a bit weird because I thought something would work per-multimesh (not per model instance). If nothing happens at all, maybe that's a bug? Unless I'm missing something I should toggle? Perhaps that should be reported to the Godot repo if you can't get it working manually with a MultiMeshInstance3D.

AimiIsFat commented 10 months ago

It seems I made a mistake when trying to implement LOD originally.

I got the error Condition "mesh_lod_count < VoxelInstanceLibraryMultiMeshItem::MAX_MESH_LODS" is true after trying to use the "secondary LOD system" without filling out all 4 LODs. I read it incorrectly and thought it meant that mesh LODs couldn't be used for non-LOD terrain. After reading what you said, I put a mesh in every LOD and it did work.

The secondary LOD system would indeed be what I was requesting, though I think it could be improved with a "max distance" value, where the max distance is the distance at which the last mesh is used, as it still has a bit of an issue which I was referring to in the original post where smaller stuff like grass needs less max distance, while larger stuff needs more. The word "terrain-independent" was a bit misleading. I was referring to making the LOD distance separate, not completely separating it from the terrain.

(Note: I did a bit of testing after writing this. Only LOD0 and LOD1 seem to be used in non-LOD terrain. Everything else is too far off in the distance to be used, regardless of view distance.)

Also, is there any way to disable GI for the grass? It causes strange dark spots on the ground sometimes and likely drops frame rate a fair amount based on the visual profiler results I got while testing. I am using SDFGI if that helps.

image

By the way, unrelated, but is there a roadmap (or similar) for this project anywhere? I think it would be nice to see what is being worked on and what will be coming in the future.

Anyways, thanks for the help. I should be able to optimize it better now, though I'd appreciate a max distance option and blank LODs if you ever have some time in the future.

Zylann commented 10 months ago

I got the error Condition "mesh_lod_count < VoxelInstanceLibraryMultiMeshItem::MAX_MESH_LODS" is true after trying to use the "secondary LOD system" without filling out all 4 LODs

Should probably be improved, that should be allowed I think.

smaller stuff like grass needs less max distance, while larger stuff needs more

Different settings per items could do the trick, however even with a max distance one issue is that terrain still doesnt have LOD, so instances will still get generated and processed over all visible chunks, even if they get culled by the secondary system. Also, per-item settings would mean that if your instancer has 10 different kinds of items, then the instancer will update the secondary LOD system for every chunk *10. I need to profile this to see what the cost is. It could do this at a lower rate than per frame, though.

I did a bit of testing after writing this. Only LOD0 and LOD1 seem to be used in non-LOD terrain. Everything else is too far off in the distance to be used, regardless of view distance.

Will need to investigate.

Also, is there any way to disable GI for the grass?

If it's a Godot setting it should be simple enough to expose.

By the way, unrelated, but is there a roadmap (or similar) for this project anywhere? I think it would be nice to see what is being worked on and what will be coming in the future.

There are some themes listed on the README, but there is generally no clear roadmap. I work on this module on my free time and it often depends on what I would like to do at the time. Sometimes it's to solve stuff that gets asked a lot and that I see a good reason to add, or it's to respond to my own needs when I add new features to demos such as Solar System. The last month I've been working on this, which mostly targets blocky terrain.


On a side note, this came up: https://twitter.com/wojtekpil/status/1714779095102329231 Someone created thousands of MeshInstances3D, then the same number using a single MultiMesh3D, profiled while looking at the whole scene, and the MultiMesh3D was slower somehow.

My guess is that even if everything is in view, some of the shadow cascades dont see the whole multimesh yet they still have to process all their vertices since it's a whole thing that cannot be culled individually (like MeshInstance3D can). In the context of VoxelInstancer, this is probably not as bad since there are lots of chunks instead of one single multimesh, so they can be culled to some degree. Though vertex processing of multimeshes remains higher than regular meshes apparently.

Zylann commented 10 months ago

Note: mesh LOD doesn't work in editor currently because in Godot, get_viewport().get_camera() returns null in the editor. But in game it properly returns the camera.

Zylann commented 10 months ago

Another stupidity I'm hitting is that the only way I can switch between LODs is to call MultiMesh.set_mesh when it needs to change. But that causes Godot to recompute their AABB by iterating all the instances in the multimesh, making it unnecessarily expensive. Like... REALLY stupidly expensive: it doesn't just run over the instances and does their AABBs. It also DOWNLOADS the data all the way back from the GPU to get those positions, while also stalling the CPU, and it's just incredibly slow.

I don't have a workaround for this unfortunately. One possibility maybe is to hope Godot's LOD system instead works (and it also has a "dont render" max distance feature), but from what you said it doesnt seem to work. If so maybe you should do a test with a multimesh in a scene and if it doesn't work, post it on Godot's repo

Zylann commented 10 months ago

This PR could help with VoxelInstancer's LOD system if it was merged: https://github.com/godotengine/godot/pull/79833 (until then, only Godot's LOD system can be used without overhead)

Zylann commented 10 months ago

I pushed a few fixes to the LOD system, it should work better now, but I didn't add "cull last" or custom distances yet.

https://github.com/Zylann/godot_voxel/assets/1311555/cd72f83a-a531-4306-8ea8-e83e7d180bca

Zylann commented 10 months ago

Added gi_mode in 6c89ab3a49f14cd2636ab791f8ac56462901c7bb

Zylann commented 10 months ago

Exposed distance ratios and added option to hide beyond max LOD in 57d7ef2351f098c6c42cb2d72d42b419614ec4f3 I would still investigate why Godot's built-in LOD doesnt work in your case, because that's kinda weird.

Also if you use the persistent flag in your instancer items, note that currently VoxelInstancer doesn't do multiplayer replication, so clients won't "remember" if some instances were removed.

AimiIsFat commented 10 months ago

I would still investigate why Godot's built-in LOD doesnt work in your case, because that's kinda weird.

Not entirely sure what the issue is there. It works on scene instances but not multimesh ones. Fortunately, the secondary LOD system is good enough in my case.

Also if you use the persistent flag in your instancer items, note that currently VoxelInstancer doesn't do multiplayer replication, so clients won't "remember" if some instances were removed.

I will keep that in mind. Fortunately, it isn't entirely necessary at the moment, but in the future I will probably have to do a bit of extra code. Would appreciate if it was added to the base module, though. I doubt I am the only one who may have, for example, destructible trees or rocks which should not reappear whenever they go in and out of view distance, but it shouldn't be too hard to just disable them client-side and have the server tell the client whenever one is spawned nearby.