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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ path = "examples/3d/wireframe.rs"
name = "z_sort_debug"
path = "examples/3d/z_sort_debug.rs"

[[example]]
name = "flat_shading"
path = "examples/3d/flat_shading.rs"

# Application
[[example]]
name = "custom_loop"
Expand Down
9 changes: 9 additions & 0 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ pub struct StandardMaterial {
#[render_resources(ignore)]
#[shader_def]
pub unlit: bool,
/// Flat shading makes the shader generate normals per face instead of per vertex.
/// This means that you get a uniform color over the whole triangle (think Virtua Racing).
/// The method uses fragment shader derivatives which can cause some artifacts in some cases.
/// An alternative is using `Mesh::duplicate_vertices()` and `Mesh::compute_flat_normals()`
/// and setting this setting to false, this will make sure no artifacts occur.
#[render_resources(ignore)]
#[shader_def]
pub flat_shading: bool,
}

impl Default for StandardMaterial {
Expand Down Expand Up @@ -69,6 +77,7 @@ impl Default for StandardMaterial {
emissive: Color::BLACK,
emissive_texture: None,
unlit: false,
flat_shading: false,
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion crates/bevy_pbr/src/render_graph/pbr_pipeline/pbr.frag
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,17 @@ struct PointLight {
vec4 color;
vec4 lightParams;
};

struct DirectionalLight {
vec4 direction;
vec4 color;
};

layout(location = 0) in vec3 v_WorldPosition;

#ifndef STANDARDMATERIAL_FLAT_SHADING
layout(location = 1) in vec3 v_WorldNormal;
#endif
layout(location = 2) in vec2 v_Uv;

#ifdef STANDARDMATERIAL_NORMAL_MAP
Expand Down Expand Up @@ -368,7 +371,11 @@ void main() {

float roughness = perceptualRoughnessToRoughness(perceptual_roughness);

# 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...

# else
vec3 N = normalize(v_WorldNormal);
# endif

# ifdef STANDARDMATERIAL_NORMAL_MAP
vec3 T = normalize(v_WorldTangent.xyz);
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_pbr/src/render_graph/pbr_pipeline/pbr.vert
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
#version 450

layout(location = 0) in vec3 Vertex_Position;
#ifndef STANDARDMATERIAL_FLAT_SHADING
layout(location = 1) in vec3 Vertex_Normal;
#endif
layout(location = 2) in vec2 Vertex_Uv;

#ifdef STANDARDMATERIAL_NORMAL_MAP
layout(location = 3) in vec4 Vertex_Tangent;
#endif

layout(location = 0) out vec3 v_WorldPosition;
#ifndef STANDARDMATERIAL_FLAT_SHADING
layout(location = 1) out vec3 v_WorldNormal;
#endif
layout(location = 2) out vec2 v_Uv;

layout(set = 0, binding = 0) uniform CameraViewProj {
Expand All @@ -27,7 +31,9 @@ layout(set = 2, binding = 0) uniform Transform {
void main() {
vec4 world_position = Model * vec4(Vertex_Position, 1.0);
v_WorldPosition = world_position.xyz;
#ifndef STANDARDMATERIAL_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.

Expand Down
61 changes: 61 additions & 0 deletions examples/3d/flat_shading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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.


/// This example shows the difference between flat shading
/// and smooth shading (default) in `StandardMaterial`.
/// Flat shading gives a much more "Polygonal" or "Retro" look to meshes.
fn main() {
App::new()
.insert_resource(Msaa { samples: 4 })
.add_plugins(DefaultPlugins)
.add_startup_system(setup)
.run();
}

/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Flat shaded icosphere (ORANGE)
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: 0.5,
subdivisions: 4,
})),
material: materials.add(StandardMaterial {
base_color: Color::ORANGE,
flat_shading: true,
..Default::default()
}),
transform: Transform::from_xyz(-0.55, 0.5, 0.0),
..Default::default()
});
// Smooth shaded icosphere (BLUE)
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: 0.5,
subdivisions: 4,
})),
material: materials.add(Color::BLUE.into()),
transform: Transform::from_xyz(0.55, 0.5, 0.0),
..Default::default()
});

// plane
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })),
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
..Default::default()
});
// light
commands.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(5.0, 5.0, 5.0),
..Default::default()
});
// camera
commands.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(1.0, 3.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
..Default::default()
});
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 smooth (default) shading

## Application

Expand Down