Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for flat shading with indiced meshes #3008

Closed

Conversation

sweepline
Copy link

I made this as an alternative for #1808 when for example
making procedually generated terrain.

Objective

Flat shading for meshes with indices, specifically terrain from heightmaps in my use case.

Solution

Extend StandardMaterial to use the partial derivative (dFdx, dFdy) for computing facet normals
in the fragment shader.

Use the partial derivative (dFdx, dFdy) for computing facet normals
in the fragment shader.

I made this as an alternative for bevyengine#1808 when for example
making procedually generated terrain.
@github-actions github-actions bot added the S-Needs-Triage This issue needs to be labelled label Oct 22, 2021
@alice-i-cecile alice-i-cecile added A-Rendering Drawing game state to the screen C-Examples An addition or correction to our examples C-Feature A new feature, making something new possible and removed S-Needs-Triage This issue needs to be labelled C-Examples An addition or correction to our examples labels Oct 22, 2021
@alice-i-cecile
Copy link
Member

Awesome, thanks for the contribution! Rendering is currently being reworked; can you retarget this to the new renderer and the pipelined-rendering branch?

I'll take a look now though, and vet what I can :)

@@ -42,6 +42,10 @@ pub struct StandardMaterial {
#[render_resources(ignore)]
#[shader_def]
pub unlit: bool,
/// This allows for flat shading even with indiced meshes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment could be a bit clearer: rendering can be intimidating for beginners so it's helpful to be explicit in the docs.

Rhetorically: what is flat shading, and why does it interact with indiced meshes?

@@ -107,6 +107,7 @@ Example | File | Description
`update_gltf_scene` | [`3d/update_gltf_scene.rs`](./3d/update_gltf_scene.rs) | Update a scene from a gltf file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene
`wireframe` | [`3d/wireframe.rs`](./3d/wireframe.rs) | Showcases wireframe rendering
`z_sort_debug` | [`3d/z_sort_debug.rs`](./3d/z_sort_debug.rs) | Visualizes camera Z-ordering
`flat_shading` | [`3d/flat_shading.rs`](./3d/flat_shading.rs) | Simple 3D scene showing flat and normal (smooth) shading
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`flat_shading` | [`3d/flat_shading.rs`](./3d/flat_shading.rs) | Simple 3D scene showing flat and normal (smooth) shading
`flat_shading` | [`3d/flat_shading.rs`](./3d/flat_shading.rs) | Simple 3D scene showing flat and smooth (default) shading

mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// flat
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be clearer if you gave the meshes different colors, and then referenced those colors in the code comments.

The Color consts listed here https://docs.rs/bevy/0.5.0/bevy/render/color/enum.Color.html#impl are very helpful for clarity there.

@@ -0,0 +1,57 @@
use bevy::prelude::*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the top of this example, I'd love to see a quick write-up of what flat vs smooth shading means, and why you might prefer one over the other.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've improved the commenting.

Copy link
Contributor

@OptimisticPeach OptimisticPeach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments about the method. The derivative method can be a bit iffy and the specific method implemented should probably be left up to the user in my opinion, and this be somewhat of a debug feature like unlit shading.

# ifdef STANDARDMATERIAL_FLAT_SHADING
vec3 N = normalize(cross(dFdy(v_WorldPosition), dFdx(v_WorldPosition)));
# endif
# ifndef STANDARDMATERIAL_FLAT_SHADING
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this become an #else?

vec3 N = normalize(cross(dFdy(v_WorldPosition), dFdx(v_WorldPosition)));
# endif
# ifndef STANDARDMATERIAL_FLAT_SHADING
vec3 N = v_WorldNormal;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be normalize-ed because the interpolations occurs throughout the triangle and produces non-normalized results.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that was definitely not intentional to remove.

@@ -368,7 +371,12 @@ void main() {

float roughness = perceptualRoughnessToRoughness(perceptual_roughness);

vec3 N = normalize(v_WorldNormal);
# ifdef STANDARDMATERIAL_FLAT_SHADING
vec3 N = normalize(cross(dFdy(v_WorldPosition), dFdx(v_WorldPosition)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the derivative solution to making flat shading can cause artifacts should the edge of the rasterized mesh not contain the entire group (IIRC, dfdx and dfdy are computed using the neighbouring pixels' values, given that fragments are calculated in 2x2 groups), I'd probably make it clear that this is not foolproof and might not work in all situations.

Some other solutions which are more complex:

  • Use flat interpolators, however that's only possible in certain special cases of geometry (Only possible when you can assign one vertex to each triangle).
  • Index using primitive_id or equivalent into a buffer containing precalculated normal values. This is a bit of a pain because it takes a while to set up, however it lets you save on space because then you don't need to store values per-vertex. Although, another thing to note is that it's not incredibly portable since it uses the same hardware as geometry shaders (although it itself is not a geometry shader based approach).
  • Use geometry shaders (Not a good idea).
  • Flatten the indices by copying vertices, and then calculating the new normals. This approach isn't great because of memory usage, but is definitely the simplest to explain.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a better docstring to the field saying it can cause artifacts, I also added a docstring to the field saying its possible to do the last option by calling Mesh::duplicate_vertices() and Mesh::compute_flat_normals() while not setting this field true. Would that be acceptable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that the new docstring is good; the other methods I described should really be up to the user.

🤔 I really should write up a blog post somewhere about the various methods to achieve flat shading...

v_WorldNormal = mat3(Model) * Vertex_Normal;
#endif
v_Uv = Vertex_Uv;
#ifdef STANDARDMATERIAL_NORMAL_MAP
v_WorldTangent = vec4(mat3(Model) * Vertex_Tangent.xyz, Vertex_Tangent.w);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you given any thought as to how this interacts with tangents? (Just curious as though I don't know how I'd approach this to be honest).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not, I assumed since the tangent uses N and not v_WorldNormal in the fragment shader it would kind of just work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thinking this through a little harder, the tangent is used in tandem with the normal to create a bitangent. After that, all three are used to create a matrix to transform the sampled normal from the map.

As a user I'd expect one of two things to happen:

  • The tangent to not vary and instead be calculated using the dFdx method as well (just running dFdx on the position will give you the tangent, and the bitangent is calculated as normal. Don't forget about normalizing).
  • Flat shading and normal map sampling are mutually exclusive, since you're not really flat shading anymore.

While I would probably feel like the first option would be what I go with, it seems that the second one is more logical.

@OptimisticPeach
Copy link
Contributor

Other than the comments I've added above, I'm glad to see someone try to bring flat shading into bevy!

Thanks for your contributions!

@mockersf
Copy link
Member

there has been quite a few changes on pbr on the rendering rework... for example, this is the new shader replacing the one you changed in this pr: https://github.com/bevyengine/bevy/blob/pipelined-rendering/pipelined/bevy_pbr2/src/render/pbr.wgsl

@sweepline
Copy link
Author

there has been quite a few changes on pbr on the rendering rework... for example, this is the new shader replacing the one you changed in this pr: https://github.com/bevyengine/bevy/blob/pipelined-rendering/pipelined/bevy_pbr2/src/render/pbr.wgsl

I will port this to the pipelined-rendering branch, should I close this PR then? I assume that branch is close to release?

Fixed not normalizing normal in fragment shader

Many docstring and comment improvements
@sweepline sweepline force-pushed the indice-based-flat-shading branch from 7c6a80a to 01d2f01 Compare October 23, 2021 10:32
@alice-i-cecile
Copy link
Member

alice-i-cecile commented Oct 23, 2021

I will port this to the pipelined-rendering branch, should I close this PR then?

You should be able to just retarget this pr, which would be preferred if it's not too much of a mess so we can preserve the thread.

Very rough guess is that the branch is a week or two off.

@sweepline
Copy link
Author

I talked to @superdump on discord and I think it was decided that it was not currently feasible to implement this in a nice way on the StandardMaterial on the new renderer branch. So I'm closing this for now, ill probably make a seperate thing for this and post it later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants