diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index c0b5668e1a761..e3ac3914512cb 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -64,6 +64,8 @@ pub const SHADOWS_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 11350275143789590502); pub const PBR_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4805239651767701046); +pub const PBR_FUNCTIONS_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16550102964439850292); pub const SHADOW_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 1836745567947005696); @@ -104,6 +106,12 @@ impl Plugin for PbrPlugin { "render/shadows.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + PBR_FUNCTIONS_HANDLE, + "render/pbr_functions.wgsl", + Shader::from_wgsl + ); load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl); load_internal_asset!( app, diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 7c7c2823265b5..3f169d5104f66 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -6,6 +6,7 @@ #import bevy_pbr::clustered_forward #import bevy_pbr::lighting #import bevy_pbr::shadows +#import bevy_pbr::pbr_functions struct FragmentInput { [[builtin(front_facing)]] is_front: bool; @@ -24,22 +25,31 @@ struct FragmentInput { [[stage(fragment)]] fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { var output_color: vec4 = material.base_color; - #ifdef VERTEX_COLORS +#ifdef VERTEX_COLORS output_color = output_color * in.color; - #endif +#endif if ((material.flags & STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { output_color = output_color * textureSample(base_color_texture, base_color_sampler, in.uv); } - // // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit if ((material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { + // Prepare a 'processed' StandardMaterial by sampling all textures to resolve + // the material members + var pbr_input: PbrInput; + + pbr_input.material.base_color = output_color; + pbr_input.material.reflectance = material.reflectance; + pbr_input.material.flags = material.flags; + pbr_input.material.alpha_cutoff = material.alpha_cutoff; + // TODO use .a for exposure compensation in HDR var emissive: vec4 = material.emissive; if ((material.flags & STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { emissive = vec4(emissive.rgb * textureSample(emissive_texture, emissive_sampler, in.uv).rgb, 1.0); } + pbr_input.material.emissive = emissive; - // calculate non-linear roughness from linear perceptualRoughness var metallic: f32 = material.metallic; var perceptual_roughness: f32 = material.perceptual_roughness; if ((material.flags & STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { @@ -48,158 +58,34 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { metallic = metallic * metallic_roughness.b; perceptual_roughness = perceptual_roughness * metallic_roughness.g; } - let roughness = perceptualRoughnessToRoughness(perceptual_roughness); + pbr_input.material.metallic = metallic; + pbr_input.material.perceptual_roughness = perceptual_roughness; var occlusion: f32 = 1.0; if ((material.flags & STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { occlusion = textureSample(occlusion_texture, occlusion_sampler, in.uv).r; } + pbr_input.occlusion = occlusion; - var N: vec3 = normalize(in.world_normal); - -#ifdef VERTEX_TANGENTS -#ifdef STANDARDMATERIAL_NORMAL_MAP - // NOTE: The mikktspace method of normal mapping explicitly requires that these NOT be - // normalized nor any Gram-Schmidt applied to ensure the vertex normal is orthogonal to the - // vertex tangent! Do not change this code unless you really know what you are doing. - // http://www.mikktspace.com/ - var T: vec3 = in.world_tangent.xyz; - var B: vec3 = in.world_tangent.w * cross(N, T); -#endif -#endif + pbr_input.frag_coord = in.frag_coord; + pbr_input.world_position = in.world_position; + pbr_input.world_normal = in.world_normal; - if ((material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u) { - if (!in.is_front) { - N = -N; -#ifdef VERTEX_TANGENTS -#ifdef STANDARDMATERIAL_NORMAL_MAP - T = -T; - B = -B; -#endif -#endif - } - } + pbr_input.is_orthographic = view.projection[3].w == 1.0; + pbr_input.N = prepare_normal( + in.world_normal, #ifdef VERTEX_TANGENTS #ifdef STANDARDMATERIAL_NORMAL_MAP - let TBN = mat3x3(T, B, N); - // Nt is the tangent-space normal. - var Nt: vec3; - if ((material.flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u) { - // Only use the xy components and derive z for 2-component normal maps. - Nt = vec3(textureSample(normal_map_texture, normal_map_sampler, in.uv).rg * 2.0 - 1.0, 0.0); - Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); - } else { - Nt = textureSample(normal_map_texture, normal_map_sampler, in.uv).rgb * 2.0 - 1.0; - } - // Normal maps authored for DirectX require flipping the y component - if ((material.flags & STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u) { - Nt.y = -Nt.y; - } - // NOTE: The mikktspace method of normal mapping applies maps the tangent-space normal from - // the normal map texture in this way to be an EXACT inverse of how the normal map baker - // calculates the normal maps so there is no error introduced. Do not change this code - // unless you really know what you are doing. - // http://www.mikktspace.com/ - N = normalize(Nt.x * T + Nt.y * B + Nt.z * N); + in.world_tangent, #endif #endif - - if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u) { - // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 - output_color.a = 1.0; - } else if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) { - if (output_color.a >= material.alpha_cutoff) { - // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque - output_color.a = 1.0; - } else { - // NOTE: output_color.a < material.alpha_cutoff should not is not rendered - // NOTE: This and any other discards mean that early-z testing cannot be done! - discard; - } - } - - var V: vec3; - // If the projection is not orthographic - let is_orthographic = view.projection[3].w == 1.0; - if (is_orthographic) { - // Orthographic view vector - V = normalize(vec3(view.view_proj[0].z, view.view_proj[1].z, view.view_proj[2].z)); - } else { - // Only valid for a perpective projection - V = normalize(view.world_position.xyz - in.world_position.xyz); - } - - // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" - let NdotV = max(dot(N, V), 0.0001); - - // Remapping [0,1] reflectance to F0 - // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping - let reflectance = material.reflectance; - let F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic; - - // Diffuse strength inversely related to metallicity - let diffuse_color = output_color.rgb * (1.0 - metallic); - - let R = reflect(-V, N); - - // accumulate color - var light_accum: vec3 = vec3(0.0); - - let view_z = dot(vec4( - view.inverse_view[0].z, - view.inverse_view[1].z, - view.inverse_view[2].z, - view.inverse_view[3].z - ), in.world_position); - let cluster_index = fragment_cluster_index(in.frag_coord.xy, view_z, is_orthographic); - let offset_and_count = unpack_offset_and_count(cluster_index); - for (var i: u32 = offset_and_count[0]; i < offset_and_count[0] + offset_and_count[1]; i = i + 1u) { - let light_id = get_light_id(i); - let light = point_lights.data[light_id]; - var shadow: f32 = 1.0; - if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u - && (light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_point_shadow(light_id, in.world_position, in.world_normal); - } - let light_contrib = point_light(in.world_position.xyz, light, roughness, NdotV, N, V, R, F0, diffuse_color); - light_accum = light_accum + light_contrib * shadow; - } - - let n_directional_lights = lights.n_directional_lights; - for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { - let light = lights.directional_lights[i]; - var shadow: f32 = 1.0; - if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u - && (light.flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_directional_shadow(i, in.world_position, in.world_normal); - } - let light_contrib = directional_light(light, roughness, NdotV, N, V, R, F0, diffuse_color); - light_accum = light_accum + light_contrib * shadow; - } - - let diffuse_ambient = EnvBRDFApprox(diffuse_color, 1.0, NdotV); - let specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV); - - output_color = vec4( - light_accum + - (diffuse_ambient + specular_ambient) * lights.ambient_color.rgb * occlusion + - emissive.rgb * output_color.a, - output_color.a); - - output_color = cluster_debug_visualization( - output_color, - view_z, - is_orthographic, - offset_and_count, - cluster_index, + in.uv, + in.is_front, ); + pbr_input.V = calculate_view(in.world_position, pbr_input.is_orthographic); - // tone_mapping - output_color = vec4(reinhard_luminance(output_color.rgb), output_color.a); - // Gamma correction. - // Not needed with sRGB buffer - // output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2)); + output_color = pbr(pbr_input); } return output_color; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl new file mode 100644 index 0000000000000..a2349fcbdeee3 --- /dev/null +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -0,0 +1,196 @@ +#define_import_path bevy_pbr::pbr_functions + +// NOTE: This ensures that the world_normal is normalized and if +// vertex tangents and normal maps then normal mapping may be applied. +fn prepare_normal( + world_normal: vec3, +#ifdef VERTEX_TANGENTS +#ifdef STANDARDMATERIAL_NORMAL_MAP + world_tangent: vec4, +#endif +#endif + uv: vec2, + is_front: bool, +) -> vec3 { + var N: vec3 = normalize(world_normal); + +#ifdef VERTEX_TANGENTS +#ifdef STANDARDMATERIAL_NORMAL_MAP + // NOTE: The mikktspace method of normal mapping explicitly requires that these NOT be + // normalized nor any Gram-Schmidt applied to ensure the vertex normal is orthogonal to the + // vertex tangent! Do not change this code unless you really know what you are doing. + // http://www.mikktspace.com/ + var T: vec3 = world_tangent.xyz; + var B: vec3 = world_tangent.w * cross(N, T); +#endif +#endif + + if ((material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u) { + if (!is_front) { + N = -N; +#ifdef VERTEX_TANGENTS +#ifdef STANDARDMATERIAL_NORMAL_MAP + T = -T; + B = -B; +#endif +#endif + } + } + +#ifdef VERTEX_TANGENTS +#ifdef STANDARDMATERIAL_NORMAL_MAP + // Nt is the tangent-space normal. + var Nt: vec3; + if ((material.flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u) { + // Only use the xy components and derive z for 2-component normal maps. + Nt = vec3(textureSample(normal_map_texture, normal_map_sampler, uv).rg * 2.0 - 1.0, 0.0); + Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); + } else { + Nt = textureSample(normal_map_texture, normal_map_sampler, uv).rgb * 2.0 - 1.0; + } + // Normal maps authored for DirectX require flipping the y component + if ((material.flags & STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u) { + Nt.y = -Nt.y; + } + // NOTE: The mikktspace method of normal mapping applies maps the tangent-space normal from + // the normal map texture in this way to be an EXACT inverse of how the normal map baker + // calculates the normal maps so there is no error introduced. Do not change this code + // unless you really know what you are doing. + // http://www.mikktspace.com/ + N = normalize(Nt.x * T + Nt.y * B + Nt.z * N); +#endif +#endif + + return N; +} + +// NOTE: Correctly calculates the view vector depending on whether +// the projection is orthographic or perspective. +fn calculate_view( + world_position: vec4, + is_orthographic: bool, +) -> vec3 { + var V: vec3; + if (is_orthographic) { + // Orthographic view vector + V = normalize(vec3(view.view_proj[0].z, view.view_proj[1].z, view.view_proj[2].z)); + } else { + // Only valid for a perpective projection + V = normalize(view.world_position.xyz - world_position.xyz); + } + return V; +} + +struct PbrInput { + material: StandardMaterial; + occlusion: f32; + frag_coord: vec4; + world_position: vec4; + world_normal: vec3; + N: vec3; + V: vec3; + is_orthographic: bool; +}; + +fn pbr( + in: PbrInput, +) -> vec4 { + var output_color: vec4 = in.material.base_color; + + // TODO use .a for exposure compensation in HDR + let emissive = in.material.emissive; + + // calculate non-linear roughness from linear perceptualRoughness + let metallic = in.material.metallic; + let perceptual_roughness = in.material.perceptual_roughness; + let roughness = perceptualRoughnessToRoughness(perceptual_roughness); + + let occlusion = in.occlusion; + + if ((in.material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u) { + // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 + output_color.a = 1.0; + } else if ((in.material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) { + if (output_color.a >= in.material.alpha_cutoff) { + // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque + output_color.a = 1.0; + } else { + // NOTE: output_color.a < in.material.alpha_cutoff should not is not rendered + // NOTE: This and any other discards mean that early-z testing cannot be done! + discard; + } + } + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(in.N, in.V), 0.0001); + + // Remapping [0,1] reflectance to F0 + // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping + let reflectance = in.material.reflectance; + let F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic; + + // Diffuse strength inversely related to metallicity + let diffuse_color = output_color.rgb * (1.0 - metallic); + + let R = reflect(-in.V, in.N); + + // accumulate color + var light_accum: vec3 = vec3(0.0); + + let view_z = dot(vec4( + view.inverse_view[0].z, + view.inverse_view[1].z, + view.inverse_view[2].z, + view.inverse_view[3].z + ), in.world_position); + let cluster_index = fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); + let offset_and_count = unpack_offset_and_count(cluster_index); + for (var i: u32 = offset_and_count[0]; i < offset_and_count[0] + offset_and_count[1]; i = i + 1u) { + let light_id = get_light_id(i); + let light = point_lights.data[light_id]; + var shadow: f32 = 1.0; + if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = fetch_point_shadow(light_id, in.world_position, in.world_normal); + } + let light_contrib = point_light(in.world_position.xyz, light, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); + light_accum = light_accum + light_contrib * shadow; + } + + let n_directional_lights = lights.n_directional_lights; + for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { + let light = lights.directional_lights[i]; + var shadow: f32 = 1.0; + if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (light.flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = fetch_directional_shadow(i, in.world_position, in.world_normal); + } + let light_contrib = directional_light(light, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); + light_accum = light_accum + light_contrib * shadow; + } + + let diffuse_ambient = EnvBRDFApprox(diffuse_color, 1.0, NdotV); + let specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV); + + output_color = vec4( + light_accum + + (diffuse_ambient + specular_ambient) * lights.ambient_color.rgb * occlusion + + emissive.rgb * output_color.a, + output_color.a); + + output_color = cluster_debug_visualization( + output_color, + view_z, + in.is_orthographic, + offset_and_count, + cluster_index, + ); + + // tone_mapping + output_color = vec4(reinhard_luminance(output_color.rgb), output_color.a); + // Gamma correction. + // Not needed with sRGB buffer + // output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2)); + + return output_color; +}