godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.18k stars 21.2k forks source link

Vulkan Mobile: Decals pop away when there are more than 7 decals on a mesh resource (instead of the expected 8) #70231

Closed elvisish closed 1 year ago

elvisish commented 1 year ago

Godot version

4.0 beta 8

System information

Windows 10

Issue description

Adding some decals to a scene works well, until trying to rotate the camera or move around them.

https://user-images.githubusercontent.com/16231628/208269637-c1514f6f-5b9b-45a8-bff4-ea9131c9f730.mp4

Steps to reproduce

Create a basic decal with a texture and leave everything at default. Add the decals to the scene. Rotate the camera and move around.

Minimal reproduction project

min_rep_decals.zip

Calinou commented 1 year ago

Related to https://github.com/godotengine/godot/issues/49639.

@elvisish Please upload a minimal reproduction project to make this easier to troubleshoot.

elvisish commented 1 year ago

Related to #49639.

@elvisish Please upload a minimal reproduction project to make this easier to troubleshoot.

min_rep_decals.zip

Try walking (WASD) rotating the camera (mouse) and clicking the mouse to shoot at surfaces (sorry they're misaligned on the walls, haven't had time to fix that).

elvisish commented 1 year ago

Okay, I think it might have something to do with it being added to a large mesh as it doesn't seem to happen on smaller objects (and trenchbroom imports levels as one big mesh). I'm guessing it's an optimization issue? It still happens on small meshes, especially if they're overlapping or close to each other.

elvisish commented 1 year ago

Still broken on latest beta 10, decals are totally useless for me right now (since they're a purely visual effect, they haven't got any use other than displaying correctly).

Calinou commented 1 year ago

I can confirm this on 4.0.beta b6e06038f (Linux, AMD Radeon RX 6900XT with Mesa RADV). I can't reproduce this after switching to the Forward Plus (clustered) backend, so this is an issue specific to the mobile backend.

The presence of a DirectionalLight3D in the scene doesn't affect this issue (the MRP doesn't have any, it's only a preview in the editor).

Edit: This is actually (probably) not a bug. Decals start flickering after you place more than 7 of them because the mobile backend is limited to 8 decals per mesh resource. The only thing I find unusual is that decals start to break after you have more than 7 of them, rather than 8 of them. I can reproduce that in a blank project too.

In general, for small decals such as bullet holes, I recommend placing a MeshInstance with a PlaneMesh instead. This is faster to render, allows for any shader to be used (including BaseMaterial3D parallax) and has no limitations on the number of "decals" you can render, regardless of your backend (even OpenGL). Rendering errors due to decals flying in the air are unlikely to be noticeable during gameplay given the decals' small size.

cc @BastiaanOlij

elvisish commented 1 year ago

I can confirm this on 4.0.beta b6e0603 (Linux, AMD Radeon RX 6900XT with Mesa RADV). I can't reproduce this after switching to the Forward Plus (clustered) backend, so this is an issue specific to the mobile backend.

The presence of a DirectionalLight3D in the scene doesn't affect this issue (the MRP doesn't have any, it's only a preview in the editor).

Edit: This is actually (probably) not a bug. Decals start flickering after you place more than 7 of them because the mobile backend is limited to 8 decals per mesh resource. The only thing I find unusual is that decals start to break after you have more than 7 of them, rather than 8 of them. I can reproduce that in a blank project too.

In general, for small decals such as bullet holes, I recommend placing a MeshInstance with a PlaneMesh instead. This is faster to render, allows for any shader to be used (including BaseMaterial3D parallax) and has no limitations on the number of "decals" you can render, regardless of your backend (even OpenGL). Rendering errors due to decals flying in the air are unlikely to be noticeable during gameplay given the decals' small size.

cc @BastiaanOlij

Is it possible to raise the limit of the decals per mesh resource? I used mobile as it seemed like a good renderer for retro graphics that don't need high overhead but can still use Vulkan. I also used decals as it it's annoying when bullet holes are misaligned on surfaces; still, good to know that PlaneMesh is faster to render as I assumed Decals would have been far mroe optimised for this kind of thing.

Calinou commented 1 year ago

Is it possible to raise the limit of the decals per mesh resource?

No, it's not technically possible to do so due to how the renderer is architectured.

elvisish commented 1 year ago

Is it possible to raise the limit of the decals per mesh resource?

No, it's not technically possible to do so due to how the renderer is architectured.

Could it be re-architectured so it does work on mobile? I can't imagine any reason why mobile couldn't benefit from decals working as well as Forward+? Also, I think there should probably be a warning if Mobile is used with decals.

Calinou commented 1 year ago

Could it be re-architectured so it does work on mobile? I can't imagine any reason why mobile couldn't benefit from decals working as well as Forward+?

The Forward Mobile backend uses a single-pass approach to light and decal rendering, but without clustering (as clustering is expensive on its own, and not suited to mobile GPUs). To keep performance high and shader compilation times reasonable, it's not possible to support a high number of lights/decals per mesh with a traditional single-pass forward approach.

Either way, you should be splitting your level into several meshes in a real world project (so that it can benefit from frustum/occlusion culling). You need to find a balance to avoid having too many draw calls though. Usually, you want each room in the level to be its own MeshInstance3D node.

elvisish commented 1 year ago

Either way, you should be splitting your level into several meshes in a real world project (so that it can benefit from frustum/occlusion culling). You need to find a balance to avoid having too many draw calls though. Usually, you want each room in the level to be its own MeshInstance3D node.

Impossible, I'm using TBLoader which imports the map as one giant mesh and since Godot has no built-in way of making level geometry, this is currently the best solution (outside of building in Blender, which has other drawbacks). @codecat might be able to explain why TBLoader worked best loading as one big mesh (I believe there was an early attempt at using multiple meshes that was abandoned).

Calinou commented 1 year ago

Impossible, I'm using TBLoader which imports the map as one giant mesh

This is something that should be addressed within the add-on. Having the entire level be a single mesh is going to cause performance issues in complex levels – you can see this being an issue in the tps-demo.

It is possible to procedurally perform mesh splitting, but it's difficult to get right: https://github.com/lawnjelly/godot-splerger

elvisish commented 1 year ago

Impossible, I'm using TBLoader which imports the map as one giant mesh

This is something that should be addressed within the add-on. Having the entire level be a single mesh is going to cause performance issues in complex levels – you can see this being an issue in the tps-demo.

It is possible to procedurally perform mesh splitting, but it's difficult to get right: https://github.com/lawnjelly/godot-splerger

Qodot (which TBLoader is based on though greatly improved) also loaded as one big mesh, it's a shame decals can't be limited on mobile mode to 8 per surface instead of mesh.

clayjohn commented 1 year ago

I feel like it should be possible to make the limitation a per-surface limitation. @BastiaanOlij would know more. But I think the decal list ends up getting assigned to a surface anyway so there shouldn't be any real benefit to storing it per mesh instead of per surface

BastiaanOlij commented 1 year ago

@clayjohn I think it already works per surface but I could be wrong. Not sure that would help in this case as there is still a high chance the surface covers a lot of area.

I'm with @Calinou on this, this is a flaw in the approach of the level loader. Having your whole level be one big mesh on any sizable level will be a huge issue performance wise as nothing can be culled and all geometry is handled.

Also even if we "solve" this limitation, it would mean every fragment drawn for the level mesh would be looping through all decals, so you'd have a huge performance issue.

While there are options to do a version of clustering in fragment shaders (one of my old engines works this way) I'm not sure how well that would perform on mobile hardware.

elvisish commented 1 year ago

I'm with @Calinou on this, this is a flaw in the approach of the level loader. Having your whole level be one big mesh on any sizable level will be a huge issue performance wise as nothing can be culled and all geometry is handled.

Cruelty Squad (PC Gamer Game of the Year 2021) was made using Qodot which uses a single mesh per level and had no performance issues, so it does work.

codecat commented 1 year ago

I suppose it depends on the complexity of the level geometry. You could use TBLoader to load parts of the world as separate meshes by just creating different layers in TrenchBroom (each layer has its own node & MeshInstance3D). Segmenting a single large layer could potentially be done automatically, but such a feature would be more of an issue to be discussed in the TBLoader repository instead.

BastiaanOlij commented 1 year ago

Cruelty Squad (PC Gamer Game of the Year 2021) was made using Qodot which uses a single mesh per level and had no performance issues, so it does work.

Not sure if that is a fair comparison in this case, yes it works, it works well, if you keep the lighting within the limits. I'm not even sure if Cruelty Squad uses lighting or if it does it probably just has a single directional light.

elvisish commented 1 year ago

it works well, if you keep the lighting within the limits.

Is performance affected by lights? I thought the limit of lights per mesh was raised even before 4.0, or do you mean if shadows/light maps are used?

nothing can be culled and all geometry is handled.

Even though gridmaps are highly optimised, culling/portals can't be used on them either so you'd have a similar problem if trying to cull manually; though it probably does some of this for you internally, wouldn't multiple decals per grid map still be an issue? (I'm not sure as I haven't tried yet)

BastiaanOlij commented 1 year ago

it works well, if you keep the lighting within the limits.

Is performance affected by lights? I thought the limit of lights per mesh was raised even before 4.0, or do you mean if shadows/light maps are used?

Lights are expensive to calculate. In the clustered forward renderer we run a pre-process that allows us to see which fragments (pixels) are effected by which lights, so we only do light calculations for lights that actually impact a fragment. So even if you have 1000 lights, if only 2 light this fragment, you're only doing 2 lighting calculations. Even on a big mesh that is lit by many lights, because we're able to do this filtering on a per fragment basis, the lighting calculations are kept to a minimum.

But this approach does not work well on mobile so on mobile we use a more traditional way where we determine which lights effect a mesh. This means that for every fragment we have to process every light that effects the entire mesh, even if that fragment is only lit by one or two of those lights. It also means transfering that information to the GPU before rendering each mesh, which is limited without having a performance impact.

So it's a double whammy, we're limited to how much information we sent, so that limits the maximum number of lights, and every light we check has an impact on performance, especially on large meshes hit by many lights.

nothing can be culled and all geometry is handled.

Even though gridmaps are highly optimised, culling/portals can't be used on them either so you'd have a similar problem if trying to cull manually; though it probably does some of this for you internally, wouldn't multiple decals per grid map still be an issue? (I'm not sure as I haven't tried yet)

Gridmaps use multimeshes behind the scene. A multimesh is used for each tile. So the same goes here, if a gridmap spans over a large area, it's efficiency starts to suffer as the balance tips from limiting drawcalls, to parsing a lot of geometry that isn't even on screen.