bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
35.58k stars 3.52k forks source link

Meshes with negative scale transforms are not rendered correctly #4738

Closed bitshifter closed 1 year ago

bitshifter commented 2 years ago

Bevy version

0.7

Operating system & version

Windows 10

What you did

edit: this was discovered via gltf loading, however a simple way to reproduce it is to modify the 3d_scene example an apply a negative scale to the cube transform.

I downloaded Kenney Car Kit from https://kenney.nl/assets/car-kit and loaded the Models/GLTF format/suv.glb#Scene0 asset in a bevy project.

When examining the vehicle in bevy, the wheels on the left of the car are not visible and the wheel on the back of the car is partially visible.

I opened up this asset in Blender and found that the nodes that we not rendering were using negative scale on the x axis to mirror the wheel.

If you load this asset up in the scene viewer example it is a bit clearer there, the negative scale nodes are rendered, but it looks like back face culling is inverted or something.

cargo run --example scene_viewer suv.glb

What you expected to happen

Negative scaled objects should be rendered correctly.

What actually happened

Negative scaled objects were not rendered correctly.

Additional information

bevy:

bevy

blender:

blender

bjorn3 commented 2 years ago

That is likely backface culling which I believe blender has disabled by default while pretty much every game engine has enabled it ny default.

superdump commented 2 years ago

Exactly that. So unless blender or something else indicates in the glTF that those things need to be rendered double-sided or with front face culling instead, it would have to be fixed up in code. The StandardMaterial has cull_mode and double_sided. For the minimal change, I'd say try setting the StandardMaterial cull_mode on the wheels to Some(Face::Front). If the lighting looks weird, then I guess maybe set double_sided to true.

bitshifter commented 2 years ago

Just to clarify, you both are of the opinion that the asset is incorrect, that it looks correct in blender because blender doesn't back face cull and this is not a bug in bevy?

bitshifter commented 2 years ago

I don't know if this was authored in Blender, I imported it to into Blender to examine the node transforms, back face culling was enabled on the tire material. I've inspected this asset in https://gltf-viewer.donmccurdy.com (three.js) and I've imported it into Unreal and Godot, it worked fine in all cases.

Unreal and Godot don't support gltf at runtime (Bevy is unusual in this regard) but both have import pipelines that can convert gltf to their native formats. In Unreal's case it only imported the static meshes and materials, not any hierarchy. It imported unique meshes ~with the local transform (i.e. already reflected) baked in~ for each wheel mesh. In Godot I just dragged the asset into the editor and some "magic" happened. Looking at the Godot docs there are quite a few options for the gltf importer, I don't know which one was used, but all wheels render correctly and back face culling is enabled on the wheel material.

I am not familiar enough with Bevy internals to know what the problem is exactly, but it does seem like Bevy isn't able to handle negative scale gltf nodes correctly. I don't know that this is a render issue, it could be the loading process needs to transform meshes so that they can be rendered correctly.

edit1: Godot import workflow docs https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_scenes.html#import-workflows

edit2: Unreal imported a copy of each wheel mesh untransformed, so they're all exactly the same mesh but there are 5 of them,..

edit3: https://gltf-viewer.donmccurdy.com uses three.js, no idea what they are doing for culling

superdump commented 2 years ago

Interesting indeed. I wonder what they all do. Doing anything automatic with front/back face culling based on negative scale feels risky because it could well be that the mesh has negative scale because you’re supposed to be inside it and the negative scale then makes front faces outside the volume of the mesh be front faces inside the volume of the mesh and then flipping that would break such meshes.

bitshifter commented 2 years ago

Yeah I'm not sure. I was looking through the Godot importer code, there is a lot of it. Once the scene is imported, I didn't see any way of inspecting it to know if the negative scale was preserved.

I think for my use I will split these up into separate meshes and create the hierarchy in code manually, since trying to access the child components of a gltf scene was proving quite difficult. But I think this is pretty common to use negative scale to flip meshes, especially for low poly assets, so it will probably come up again.

superdump commented 2 years ago

I expect all four wheels are using the same material, and the two wheels with the negative scale are not visible as they are being back-face culled. The way to fix that would be to set the material's cull_mode to Some(Face::Front) as mentioned. However, that would make the right-side non-negative-scale wheels get culled instead. So I think you would have to duplicate the material from the right-side wheels, modify the cull_mode, add this new material asset to the appropriate assets resource to get a new material handle, and then set the material handle component of the left wheels to that. Awkward.

Needs more understanding into how other engines manage to handle this without just disabling back face culling.

bitshifter commented 2 years ago

In this case I can rotate the wheels instead of reflecting them since they are symmetrical.

I found a few suggestions on how to deal with negative scale here https://www.gamedev.net/forums/topic/640616-negative-scaling-flips-face-winding-affects-backface-culling/5045155/.

tl;dr:

I suspect this might be an issue in Bevy if I negative scale anything, without needing to go through gltf, I will check that when I have time.

bitshifter commented 2 years ago

I applied negative scale to one of the transforms in the parenting example (e.g. .with_scale(Vec3::(-1.0, 1.0, 1.0))) and found the culling is backwards in that case too. I guess in this instance it is easy for someone to change material properties, whereas with the gltf the transform can be buried deep in a hierarchy and is pretty difficult for a user to change post load (I personally couldn't work out how to access the scene child components a few levels deep). So while negative scale could possibly be handled automatically across the board, it might make sense to handle the gltf case sooner since it's harder for users to do something about it.

The StandardMaterial has cull_mode and double_sided. For the minimal change, I'd say try setting the StandardMaterial cull_mode on the wheels to Some(Face::Front). If the lighting looks weird, then I guess maybe set double_sided to true.

I tried this in the 3d_scene example by setting x scale to -1 and applying a StandardMaterial with cull_mode set to Face::Front, it rendered correctly, lighting and shadows looked OK.

superdump commented 2 years ago

My argument so far has been that we can’t absolutely know whether the intention was to use a -1 scale and the thing be viewed from inside its volume with its front faces on the interior, or if like the wheels it is still meant to be viewed from its exterior.

That can simply mean that if we detect -1 scaling then we disable face culling for children unless they use -1 scaling again. That could be a simple solution for glTF. It has a performance cost I guess as backfaces won’t be culled, and so I guess rasterisation would happen up to the point of early-z testing at least and hopefully the visible faces were already drawn. But otherwise I guess it would incur an overdraw cost.

mgustavsson commented 1 year ago

Hi I just found this conversation when I was having this issue in my own renderer (actually not bevy-related).

FYI I found out there actually is a correct way to handle this according to the GLTF spec 3.7.4:

When a mesh primitive uses any triangle-based topology (i.e., triangles, triangle strip, or triangle fan), the determinant of the node’s global transform defines the winding order of that primitive. If the determinant is a positive value, the winding order triangle faces is counterclockwise; in the opposite case, the winding order is clockwise.

https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#indices-and-names

So if you want to support this either you can change the winding order / face culling setting when rendering based on the determinant, or create a second set of indices with swapped winding order and choose between those depending on the determinant.