Begin WGSL port (sprites work, pbr lights are broken)

This commit is contained in:
Carter Anderson 2021-06-28 15:36:50 -07:00
parent 829f723bf3
commit 61c8475069
13 changed files with 620 additions and 776 deletions

View file

@ -48,9 +48,6 @@ fn setup(
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(StandardMaterial {
base_color: Color::PINK,
perceptual_roughness: 0.0,
metallic: 1.0,
reflectance: 1.0,
..Default::default()
}),
transform: Transform::from_xyz(0.0, 1.0, 0.0),

View file

@ -78,8 +78,9 @@ impl FromWorld for ShadowShaders {
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
// TODO: verify this is correct
min_binding_size: BufferSize::new(ViewUniform::std140_size_static() as u64),
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(80),
},
count: None,
},
@ -120,8 +121,8 @@ impl FromWorld for ShadowShaders {
},
],
}],
module: &pbr_shaders.vertex_shader_module,
entry_point: "main",
module: &pbr_shaders.shader_module,
entry_point: "vertex",
},
fragment: None,
depth_stencil: Some(DepthStencilState {

View file

@ -14,12 +14,11 @@ use bevy_render2::{
renderer::{RenderContext, RenderDevice, RenderQueue},
shader::Shader,
texture::{BevyDefault, GpuImage, Image, TextureFormatPixelInfo},
view::{ExtractedView, ViewMeta, ViewUniform, ViewUniformOffset},
view::{ExtractedView, ViewMeta, ViewUniformOffset},
};
use bevy_transform::components::GlobalTransform;
use bevy_utils::slab::{FrameSlabMap, FrameSlabMapKey};
use crevice::std140::AsStd140;
use std::borrow::Cow;
use wgpu::{
Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, TextureDimension, TextureFormat,
TextureViewDescriptor,
@ -29,7 +28,7 @@ use crate::{StandardMaterial, StandardMaterialUniformData};
pub struct PbrShaders {
pipeline: RenderPipeline,
vertex_shader_module: ShaderModule,
shader_module: ShaderModule,
view_layout: BindGroupLayout,
material_layout: BindGroupLayout,
mesh_layout: BindGroupLayout,
@ -41,26 +40,9 @@ pub struct PbrShaders {
impl FromWorld for PbrShaders {
fn from_world(world: &mut World) -> Self {
let render_device = world.get_resource::<RenderDevice>().unwrap();
let vertex_shader = Shader::from_glsl(ShaderStage::VERTEX, include_str!("pbr.vert"))
.get_spirv_shader(None)
.unwrap();
let fragment_shader = Shader::from_glsl(ShaderStage::FRAGMENT, include_str!("pbr.frag"))
.get_spirv_shader(None)
.unwrap();
let vertex_spirv = vertex_shader.get_spirv(None).unwrap();
let fragment_spirv = fragment_shader.get_spirv(None).unwrap();
let vertex_shader_module = render_device.create_shader_module(&ShaderModuleDescriptor {
flags: ShaderFlags::default(),
label: None,
source: ShaderSource::SpirV(Cow::Borrowed(&vertex_spirv)),
});
let fragment_shader_module = render_device.create_shader_module(&ShaderModuleDescriptor {
flags: ShaderFlags::default(),
label: None,
source: ShaderSource::SpirV(Cow::Borrowed(&fragment_spirv)),
});
let shader = Shader::from_wgsl(include_str!("pbr.wgsl"));
let shader_module = render_device.create_shader_module(&shader);
println!("{}", GpuLights::std140_size_static());
// TODO: move this into ViewMeta?
let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
@ -72,8 +54,9 @@ impl FromWorld for PbrShaders {
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
// TODO: verify this is correct
min_binding_size: BufferSize::new(ViewUniform::std140_size_static() as u64),
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(80),
},
count: None,
},
@ -84,7 +67,9 @@ impl FromWorld for PbrShaders {
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(GpuLights::std140_size_static() as u64),
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(1264),
},
count: None,
},
@ -262,12 +247,12 @@ impl FromWorld for PbrShaders {
},
],
}],
module: &&vertex_shader_module,
entry_point: "main",
module: &shader_module,
entry_point: "vertex",
},
fragment: Some(FragmentState {
module: &&fragment_shader_module,
entry_point: "main",
module: &shader_module,
entry_point: "fragment",
targets: &[ColorTargetState {
format: TextureFormat::bevy_default(),
blend: Some(BlendState {
@ -359,7 +344,7 @@ impl FromWorld for PbrShaders {
view_layout,
material_layout,
mesh_layout,
vertex_shader_module,
shader_module,
dummy_white_gpu_image,
}
}
@ -473,7 +458,9 @@ fn image_handle_to_view_sampler<'a>(
&pbr_shaders.dummy_white_gpu_image.sampler,
),
|image_handle| {
let gpu_image = gpu_images.get(image_handle).expect("only materials with valid textures should be drawn");
let gpu_image = gpu_images
.get(image_handle)
.expect("only materials with valid textures should be drawn");
(&gpu_image.texture_view, &gpu_image.sampler)
},
)

View file

@ -1,423 +0,0 @@
#version 450
// From the Filament design doc
// https://google.github.io/filament/Filament.html#table_symbols
// Symbol Definition
// v View unit vector
// l Incident light unit vector
// n Surface normal unit vector
// h Half unit vector between l and v
// f BRDF
// f_d Diffuse component of a BRDF
// f_r Specular component of a BRDF
// α Roughness, remapped from using input perceptualRoughness
// σ Diffuse reflectance
// Ω Spherical domain
// f0 Reflectance at normal incidence
// f90 Reflectance at grazing angle
// χ+(a) Heaviside function (1 if a>0 and 0 otherwise)
// nior Index of refraction (IOR) of an interface
// ⟨n⋅l⟩ Dot product clamped to [0..1]
// ⟨a⟩ Saturated value (clamped to [0..1])
// The Bidirectional Reflectance Distribution Function (BRDF) describes the surface response of a standard material
// and consists of two components, the diffuse component (f_d) and the specular component (f_r):
// f(v,l) = f_d(v,l) + f_r(v,l)
//
// The form of the microfacet model is the same for diffuse and specular
// f_r(v,l) = f_d(v,l) = 1 / { |n⋅v||n⋅l| } ∫_Ω D(m,α) G(v,l,m) f_m(v,l,m) (v⋅m) (l⋅m) dm
//
// In which:
// D, also called the Normal Distribution Function (NDF) models the distribution of the microfacets
// G models the visibility (or occlusion or shadow-masking) of the microfacets
// f_m is the microfacet BRDF and differs between specular and diffuse components
//
// The above integration needs to be approximated.
layout(location = 0) in vec4 v_WorldPosition;
layout(location = 1) in vec3 v_WorldNormal;
layout(location = 2) in vec2 v_Uv;
layout(location = 0) out vec4 o_Target;
struct OmniLight {
vec4 color;
float range;
float radius;
vec3 position;
mat4 view_projection;
};
// NOTE: this must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs
// TODO: this can be removed if we move to storage buffers for light arrays
const int MAX_OMNI_LIGHTS = 10;
struct StandardMaterial_t {
vec4 base_color;
vec4 emissive;
float perceptual_roughness;
float metallic;
float reflectance;
// 'flags' is a bit field indicating various option. uint is 32 bits so we have up to 32 options.
uint flags;
};
// NOTE: These must match those defined in bevy_pbr2/src/material.rs
const uint FLAGS_BASE_COLOR_TEXTURE_BIT = (1 << 0);
const uint FLAGS_EMISSIVE_TEXTURE_BIT = (1 << 1);
const uint FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT = (1 << 2);
const uint FLAGS_OCCLUSION_TEXTURE_BIT = (1 << 3);
const uint FLAGS_DOUBLE_SIDED_BIT = (1 << 4);
const uint FLAGS_UNLIT_BIT = (1 << 5);
// View bindings - set 0
layout(set = 0, binding = 0) uniform View {
mat4 ViewProj;
vec3 ViewWorldPosition;
};
layout(std140, set = 0, binding = 1) uniform Lights {
vec4 AmbientColor;
uint NumLights;
OmniLight OmniLights[MAX_OMNI_LIGHTS];
};
layout(set = 0, binding = 2) uniform texture2DArray t_Shadow;
layout(set = 0, binding = 3) uniform samplerShadow s_Shadow;
// Material bindings - set 2
layout(set = 2, binding = 0) uniform StandardMaterial {
StandardMaterial_t Material;
};
layout(set = 2, binding = 1) uniform texture2D base_color_texture;
layout(set = 2, binding = 2) uniform sampler base_color_sampler;
layout(set = 2, binding = 3) uniform texture2D emissive_texture;
layout(set = 2, binding = 4) uniform sampler emissive_sampler;
layout(set = 2, binding = 5) uniform texture2D metallic_roughness_texture;
layout(set = 2, binding = 6) uniform sampler metallic_roughness_sampler;
layout(set = 2, binding = 7) uniform texture2D occlusion_texture;
layout(set = 2, binding = 8) uniform sampler occlusion_sampler;
# define saturate(x) clamp(x, 0.0, 1.0)
const float PI = 3.141592653589793;
float pow5(float x) {
float x2 = x * x;
return x2 * x2 * x;
}
// distanceAttenuation is simply the square falloff of light intensity
// combined with a smooth attenuation at the edge of the light radius
//
// light radius is a non-physical construct for efficiency purposes,
// because otherwise every light affects every fragment in the scene
float getDistanceAttenuation(float distanceSquare, float inverseRangeSquared) {
float factor = distanceSquare * inverseRangeSquared;
float smoothFactor = saturate(1.0 - factor * factor);
float attenuation = smoothFactor * smoothFactor;
return attenuation * 1.0 / max(distanceSquare, 1e-4);
}
// Normal distribution function (specular D)
// Based on https://google.github.io/filament/Filament.html#citation-walter07
// D_GGX(h,α) = α^2 / { π ((n⋅h)^2 (α21) + 1)^2 }
// Simple implementation, has precision problems when using fp16 instead of fp32
// see https://google.github.io/filament/Filament.html#listing_speculardfp16
float D_GGX(float roughness, float NoH, const vec3 h) {
float oneMinusNoHSquared = 1.0 - NoH * NoH;
float a = NoH * roughness;
float k = roughness / (oneMinusNoHSquared + a * a);
float d = k * k * (1.0 / PI);
return d;
}
// Visibility function (Specular G)
// V(v,l,a) = G(v,l,α) / { 4 (n⋅v) (n⋅l) }
// such that f_r becomes
// f_r(v,l) = D(h,α) V(v,l,α) F(v,h,f0)
// where
// V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1α2) + α2) + n⋅v sqrt((n⋅l)^2 (1α2) + α2) }
// Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv
float V_SmithGGXCorrelated(float roughness, float NoV, float NoL) {
float a2 = roughness * roughness;
float lambdaV = NoL * sqrt((NoV - a2 * NoV) * NoV + a2);
float lambdaL = NoV * sqrt((NoL - a2 * NoL) * NoL + a2);
float v = 0.5 / (lambdaV + lambdaL);
return v;
}
// Fresnel function
// see https://google.github.io/filament/Filament.html#citation-schlick94
// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 f_0) (1 v⋅h)^5
vec3 F_Schlick(const vec3 f0, float f90, float VoH) {
// not using mix to keep the vec3 and float versions identical
return f0 + (f90 - f0) * pow5(1.0 - VoH);
}
float F_Schlick(float f0, float f90, float VoH) {
// not using mix to keep the vec3 and float versions identical
return f0 + (f90 - f0) * pow5(1.0 - VoH);
}
vec3 fresnel(vec3 f0, float LoH) {
// f_90 suitable for ambient occlusion
// see https://google.github.io/filament/Filament.html#lighting/occlusion
float f90 = saturate(dot(f0, vec3(50.0 * 0.33)));
return F_Schlick(f0, f90, LoH);
}
// Specular BRDF
// https://google.github.io/filament/Filament.html#materialsystem/specularbrdf
// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m
// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) }
vec3 specular(vec3 f0, float roughness, const vec3 h, float NoV, float NoL,
float NoH, float LoH, float specularIntensity) {
float D = D_GGX(roughness, NoH, h);
float V = V_SmithGGXCorrelated(roughness, NoV, NoL);
vec3 F = fresnel(f0, LoH);
return (specularIntensity * D * V) * F;
}
// Diffuse BRDF
// https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf
// fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm
// simplest approximation
// float Fd_Lambert() {
// return 1.0 / PI;
// }
//
// vec3 Fd = diffuseColor * Fd_Lambert();
// Disney approximation
// See https://google.github.io/filament/Filament.html#citation-burley12
// minimal quality difference
float Fd_Burley(float roughness, float NoV, float NoL, float LoH) {
float f90 = 0.5 + 2.0 * roughness * LoH * LoH;
float lightScatter = F_Schlick(1.0, f90, NoL);
float viewScatter = F_Schlick(1.0, f90, NoV);
return lightScatter * viewScatter * (1.0 / PI);
}
// From https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
vec3 EnvBRDFApprox(vec3 f0, float perceptual_roughness, float NoV) {
const vec4 c0 = { -1, -0.0275, -0.572, 0.022 };
const vec4 c1 = { 1, 0.0425, 1.04, -0.04 };
vec4 r = perceptual_roughness * c0 + c1;
float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
return f0 * AB.x + AB.y;
}
float perceptualRoughnessToRoughness(float perceptualRoughness) {
// clamp perceptual roughness to prevent precision problems
// According to Filament design 0.089 is recommended for mobile
// Filament uses 0.045 for non-mobile
float clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0);
return clampedPerceptualRoughness * clampedPerceptualRoughness;
}
// from https://64.github.io/tonemapping/
// reinhard on RGB oversaturates colors
vec3 reinhard(vec3 color) {
return color / (1.0 + color);
}
vec3 reinhard_extended(vec3 color, float max_white) {
vec3 numerator = color * (1.0f + (color / vec3(max_white * max_white)));
return numerator / (1.0 + color);
}
// luminance coefficients from Rec. 709.
// https://en.wikipedia.org/wiki/Rec._709
float luminance(vec3 v) {
return dot(v, vec3(0.2126, 0.7152, 0.0722));
}
vec3 change_luminance(vec3 c_in, float l_out) {
float l_in = luminance(c_in);
return c_in * (l_out / l_in);
}
vec3 reinhard_luminance(vec3 color) {
float l_old = luminance(color);
float l_new = l_old / (1.0f + l_old);
return change_luminance(color, l_new);
}
vec3 reinhard_extended_luminance(vec3 color, float max_white_l) {
float l_old = luminance(color);
float numerator = l_old * (1.0f + (l_old / (max_white_l * max_white_l)));
float l_new = numerator / (1.0f + l_old);
return change_luminance(color, l_new);
}
vec3 omni_light(OmniLight light, float roughness, float NdotV, vec3 N, vec3 V, vec3 R, vec3 F0, vec3 diffuseColor) {
vec3 light_to_frag = light.position.xyz - v_WorldPosition.xyz;
float distance_square = dot(light_to_frag, light_to_frag);
float rangeAttenuation =
getDistanceAttenuation(distance_square, light.range);
// Specular.
// Representative Point Area Lights.
// see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16
float a = roughness;
vec3 centerToRay = dot(light_to_frag, R) * R - light_to_frag;
vec3 closestPoint = light_to_frag + centerToRay * saturate(light.radius * inversesqrt(dot(centerToRay, centerToRay)));
float LspecLengthInverse = inversesqrt(dot(closestPoint, closestPoint));
float normalizationFactor = a / saturate(a + (light.radius * 0.5 * LspecLengthInverse));
float specularIntensity = normalizationFactor * normalizationFactor;
vec3 L = closestPoint * LspecLengthInverse; // normalize() equivalent?
vec3 H = normalize(L + V);
float NoL = saturate(dot(N, L));
float NoH = saturate(dot(N, H));
float LoH = saturate(dot(L, H));
vec3 specular = specular(F0, roughness, H, NdotV, NoL, NoH, LoH, specularIntensity);
// Diffuse.
// Comes after specular since its NoL is used in the lighting equation.
L = normalize(light_to_frag);
H = normalize(L + V);
NoL = saturate(dot(N, L));
NoH = saturate(dot(N, H));
LoH = saturate(dot(L, H));
vec3 diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH);
// Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩
// where
// f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color
// Φ is light intensity
// our rangeAttentuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius
// It's not 100% clear where the 1/4π goes in the derivation, but we follow the filament shader and leave it out
// See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation
// TODO compensate for energy loss https://google.github.io/filament/Filament.html#materialsystem/improvingthebrdfs/energylossinspecularreflectance
// light.color.rgb is premultiplied with light.intensity on the CPU
return ((diffuse + specular) * light.color.rgb) * (rangeAttenuation * NoL);
}
float fetch_shadow(int light_id, vec4 homogeneous_coords) {
if (homogeneous_coords.w <= 0.0) {
return 1.0;
}
// compensate for the Y-flip difference between the NDC and texture coordinates
const vec2 flip_correction = vec2(0.5, -0.5);
// compute texture coordinates for shadow lookup
vec4 light_local = vec4(
homogeneous_coords.xy * flip_correction/homogeneous_coords.w + 0.5,
light_id,
homogeneous_coords.z / homogeneous_coords.w
);
// do the lookup, using HW PCF and comparison
return texture(sampler2DArrayShadow(t_Shadow, s_Shadow), light_local);
}
void main() {
vec4 output_color = Material.base_color;
if ((Material.flags & FLAGS_BASE_COLOR_TEXTURE_BIT) != 0) {
output_color *= texture(sampler2D(base_color_texture, base_color_sampler), v_Uv);
}
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
if ((Material.flags & FLAGS_UNLIT_BIT) == 0) {
// TODO use .a for exposure compensation in HDR
vec4 emissive = Material.emissive;
if ((Material.flags & FLAGS_EMISSIVE_TEXTURE_BIT) != 0) {
emissive.rgb *= texture(sampler2D(emissive_texture, emissive_sampler), v_Uv).rgb;
}
// calculate non-linear roughness from linear perceptualRoughness
float metallic = Material.metallic;
float perceptual_roughness = Material.perceptual_roughness;
if ((Material.flags & FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0) {
vec4 metallic_roughness = texture(sampler2D(metallic_roughness_texture, metallic_roughness_sampler), v_Uv);
// Sampling from GLTF standard channels for now
metallic *= metallic_roughness.b;
perceptual_roughness *= metallic_roughness.g;
}
float roughness = perceptualRoughnessToRoughness(perceptual_roughness);
float occlusion = 1.0;
if ((Material.flags & FLAGS_OCCLUSION_TEXTURE_BIT) != 0) {
occlusion = texture(sampler2D(occlusion_texture, occlusion_sampler), v_Uv).r;
}
vec3 N = normalize(v_WorldNormal);
// FIXME: Normal maps need an additional vertex attribute and vertex stage output/fragment stage input
// Just use a separate shader for lit with normal maps?
// # ifdef STANDARDMATERIAL_NORMAL_MAP
// vec3 T = normalize(v_WorldTangent.xyz);
// vec3 B = cross(N, T) * v_WorldTangent.w;
// # endif
if ((Material.flags & FLAGS_DOUBLE_SIDED_BIT) != 0) {
N = gl_FrontFacing ? N : -N;
// # ifdef STANDARDMATERIAL_NORMAL_MAP
// T = gl_FrontFacing ? T : -T;
// B = gl_FrontFacing ? B : -B;
// # endif
}
// # ifdef STANDARDMATERIAL_NORMAL_MAP
// mat3 TBN = mat3(T, B, N);
// N = TBN * normalize(texture(sampler2D(normal_map, normal_map_sampler), v_Uv).rgb * 2.0 - 1.0);
// # endif
vec3 V;
if (ViewProj[3][3] != 1.0) { // If the projection is not orthographic
// Only valid for a perpective projection
V = normalize(ViewWorldPosition.xyz - v_WorldPosition.xyz);
} else {
// Ortho view vec
V = normalize(vec3(-ViewProj[0][2], -ViewProj[1][2], -ViewProj[2][2]));
}
// Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"
float NdotV = max(dot(N, V), 1e-4);
// Remapping [0,1] reflectance to F0
// See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping
float reflectance = Material.reflectance;
vec3 F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic;
// Diffuse strength inversely related to metallicity
vec3 diffuse_color = output_color.rgb * (1.0 - metallic);
vec3 R = reflect(-V, N);
// accumulate color
vec3 light_accum = vec3(0.0);
for (int i = 0; i < int(NumLights); ++i) {
OmniLight light = OmniLights[i];
vec3 light_contrib = omni_light(light, roughness, NdotV, N, V, R, F0, diffuse_color);
float shadow = fetch_shadow(i, light.view_projection * v_WorldPosition);
light_accum += light_contrib * shadow;
}
vec3 diffuse_ambient = EnvBRDFApprox(diffuse_color, 1.0, NdotV);
vec3 specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV);
output_color.rgb = light_accum;
output_color.rgb += (diffuse_ambient + specular_ambient) * AmbientColor.rgb * occlusion;
output_color.rgb += emissive.rgb * output_color.a;
// tone_mapping
output_color.rgb = reinhard_luminance(output_color.rgb);
// Gamma correction.
// Not needed with sRGB buffer
// output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2));
}
o_Target = output_color;
}

View file

@ -1,28 +0,0 @@
#version 450
layout(location = 0) in vec3 Vertex_Position;
layout(location = 1) in vec3 Vertex_Normal;
layout(location = 2) in vec2 Vertex_Uv;
layout(location = 0) out vec4 v_WorldPosition;
layout(location = 1) out vec3 v_WorldNormal;
layout(location = 2) out vec2 v_Uv;
layout(set = 0, binding = 0) uniform View {
mat4 ViewProj;
vec3 ViewWorldPosition;
};
layout(set = 1, binding = 0) uniform MeshTransform {
mat4 Model;
};
void main() {
v_Uv = Vertex_Uv;
vec4 world_position = Model * vec4(Vertex_Position, 1.0);
v_WorldPosition = world_position;
// FIXME: The inverse transpose of the model matrix should be used to correctly handle scaling
// of normals
v_WorldNormal = mat3(Model) * Vertex_Normal;
gl_Position = ViewProj * world_position;
}

View file

@ -0,0 +1,470 @@
// TODO: try merging this block with the binding?
[[block]]
struct View {
view_proj: mat4x4<f32>;
world_position: vec3<f32>;
};
[[group(0), binding(0)]]
var view: View;
[[block]]
struct Mesh {
transform: mat4x4<f32>;
};
[[group(1), binding(0)]]
var mesh: Mesh;
struct Vertex {
[[location(0)]] position: vec3<f32>;
[[location(1)]] normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
};
struct VertexOutput {
[[builtin(position)]] world_position: vec4<f32>;
[[location(0)]] world_normal: vec3<f32>;
[[location(1)]] uv: vec2<f32>;
};
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
let world_position = mesh.transform * vec4<f32>(vertex.position, 1.0);
var out: VertexOutput;
out.uv = vertex.uv;
out.world_position = view.view_proj * world_position;
// FIXME: The inverse transpose of the model matrix should be used to correctly handle scaling
// of normals
out.world_normal = mat3x3<f32>(mesh.transform.x.xyz, mesh.transform.y.xyz, mesh.transform.z.xyz) * vertex.normal;
return out;
}
// From the Filament design doc
// https://google.github.io/filament/Filament.html#table_symbols
// Symbol Definition
// v View unit vector
// l Incident light unit vector
// n Surface normal unit vector
// h Half unit vector between l and v
// f BRDF
// f_d Diffuse component of a BRDF
// f_r Specular component of a BRDF
// α Roughness, remapped from using input perceptualRoughness
// σ Diffuse reflectance
// Ω Spherical domain
// f0 Reflectance at normal incidence
// f90 Reflectance at grazing angle
// χ+(a) Heaviside function (1 if a>0 and 0 otherwise)
// nior Index of refraction (IOR) of an interface
// nl Dot product clamped to [0..1]
// a Saturated value (clamped to [0..1])
// The Bidirectional Reflectance Distribution Function (BRDF) describes the surface response of a standard material
// and consists of two components, the diffuse component (f_d) and the specular component (f_r):
// f(v,l) = f_d(v,l) + f_r(v,l)
//
// The form of the microfacet model is the same for diffuse and specular
// f_r(v,l) = f_d(v,l) = 1 / { |nv||nl| } D(m,α) G(v,l,m) f_m(v,l,m) (vm) (lm) dm
//
// In which:
// D, also called the Normal Distribution Function (NDF) models the distribution of the microfacets
// G models the visibility (or occlusion or shadow-masking) of the microfacets
// f_m is the microfacet BRDF and differs between specular and diffuse components
//
// The above integration needs to be approximated.
[[block]]
struct StandardMaterial {
base_color: vec4<f32>;
emissive: vec4<f32>;
perceptual_roughness: f32;
metallic: f32;
reflectance: f32;
// 'flags' is a bit field indicating various option. uint is 32 bits so we have up to 32 options.
flags: u32;
};
struct OmniLight {
color: vec4<f32>;
range: f32;
radius: f32;
position: vec3<f32>;
view_projection: mat4x4<f32>;
};
[[block]]
struct Lights {
ambient_color: vec4<f32>;
num_lights: u32;
// NOTE: this array size must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs
// TODO: this can be removed if we move to storage buffers for light arrays
omni_lights: array<OmniLight, 10>;
};
let FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u;
let FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 2u;
let FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u;
let FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u;
let FLAGS_DOUBLE_SIDED_BIT: u32 = 16u;
let FLAGS_UNLIT_BIT: u32 = 32u;
[[group(0), binding(1)]]
var lights: Lights;
[[group(0), binding(2)]]
var shadow_textures: texture_depth_2d_array;
[[group(0), binding(3)]]
var shadow_textures_sampler: sampler_comparison;
[[group(2), binding(0)]]
var material: StandardMaterial;
[[group(2), binding(1)]]
var base_color_texture: texture_2d<f32>;
[[group(2), binding(2)]]
var base_color_sampler: sampler;
[[group(2), binding(3)]]
var emissive_texture: texture_2d<f32>;
[[group(2), binding(4)]]
var emissive_sampler: sampler;
[[group(2), binding(5)]]
var metallic_roughness_texture: texture_2d<f32>;
[[group(2), binding(6)]]
var metallic_roughness_sampler: sampler;
[[group(2), binding(7)]]
var occlusion_texture: texture_2d<f32>;
[[group(2), binding(8)]]
var occlusion_sampler: sampler;
let PI: f32 = 3.141592653589793;
fn saturate(value: f32) -> f32 {
return clamp(value, 0.0, 1.0);
}
// distanceAttenuation is simply the square falloff of light intensity
// combined with a smooth attenuation at the edge of the light radius
//
// light radius is a non-physical construct for efficiency purposes,
// because otherwise every light affects every fragment in the scene
fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 {
let factor = distanceSquare * inverseRangeSquared;
let smoothFactor = saturate(1.0 - factor * factor);
let attenuation = smoothFactor * smoothFactor;
return attenuation * 1.0 / max(distanceSquare, 0.0001);
}
// Normal distribution function (specular D)
// Based on https://google.github.io/filament/Filament.html#citation-walter07
// D_GGX(h,α) = α^2 / { π ((nh)^2 (α21) + 1)^2 }
// Simple implementation, has precision problems when using fp16 instead of fp32
// see https://google.github.io/filament/Filament.html#listing_speculardfp16
fn D_GGX(roughness: f32, NoH: f32, h: vec3<f32>) -> f32 {
let oneMinusNoHSquared = 1.0 - NoH * NoH;
let a = NoH * roughness;
let k = roughness / (oneMinusNoHSquared + a * a);
let d = k * k * (1.0 / PI);
return d;
}
// Visibility function (Specular G)
// V(v,l,a) = G(v,l,α) / { 4 (nv) (nl) }
// such that f_r becomes
// f_r(v,l) = D(h,α) V(v,l,α) F(v,h,f0)
// where
// V(v,l,α) = 0.5 / { nl sqrt((nv)^2 (1α2) + α2) + nv sqrt((nl)^2 (1α2) + α2) }
// Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv
fn V_SmithGGXCorrelated(roughness: f32, NoV: f32, NoL: f32) -> f32 {
let a2 = roughness * roughness;
let lambdaV = NoL * sqrt((NoV - a2 * NoV) * NoV + a2);
let lambdaL = NoV * sqrt((NoL - a2 * NoL) * NoL + a2);
let v = 0.5 / (lambdaV + lambdaL);
return v;
}
// Fresnel function
// see https://google.github.io/filament/Filament.html#citation-schlick94
// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 f_0) (1 vh)^5
fn F_Schlick_vec(f0: vec3<f32>, f90: f32, VoH: f32) -> vec3<f32> {
// not using mix to keep the vec3 and float versions identical
return f0 + (f90 - f0) * pow(1.0 - VoH, 5.0);
}
fn F_Schlick(f0: f32, f90: f32, VoH: f32) -> f32 {
// not using mix to keep the vec3 and float versions identical
return f0 + (f90 - f0) * pow(1.0 - VoH, 5.0);
}
fn fresnel(f0: vec3<f32>, LoH: f32) -> vec3<f32> {
// f_90 suitable for ambient occlusion
// see https://google.github.io/filament/Filament.html#lighting/occlusion
let f90 = saturate(dot(f0, vec3<f32>(50.0 * 0.33)));
return F_Schlick_vec(f0, f90, LoH);
}
// Specular BRDF
// https://google.github.io/filament/Filament.html#materialsystem/specularbrdf
// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m
// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (nv) (nl) }
fn specular(f0: vec3<f32>, roughness: f32, h: vec3<f32>, NoV: f32, NoL: f32,
NoH: f32, LoH: f32, specularIntensity: f32) -> vec3<f32> {
let D = D_GGX(roughness, NoH, h);
let V = V_SmithGGXCorrelated(roughness, NoV, NoL);
let F = fresnel(f0, LoH);
return (specularIntensity * D * V) * F;
}
// Diffuse BRDF
// https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf
// fd(v,l) = σ/π * 1 / { |nv||nl| } Ω D(m,α) G(v,l,m) (vm) (lm) dm
//
// simplest approximation
// float Fd_Lambert() {
// return 1.0 / PI;
// }
//
// vec3 Fd = diffuseColor * Fd_Lambert();
//
// Disney approximation
// See https://google.github.io/filament/Filament.html#citation-burley12
// minimal quality difference
fn Fd_Burley(roughness: f32, NoV: f32, NoL: f32, LoH: f32) -> f32 {
let f90 = 0.5 + 2.0 * roughness * LoH * LoH;
let lightScatter = F_Schlick(1.0, f90, NoL);
let viewScatter = F_Schlick(1.0, f90, NoV);
return lightScatter * viewScatter * (1.0 / PI);
}
// From https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
fn EnvBRDFApprox(f0: vec3<f32>, perceptual_roughness: f32, NoV: f32) -> vec3<f32> {
let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022);
let c1 = vec4<f32>(1.0, 0.0425, 1.04, -0.04);
let r = perceptual_roughness * c0 + c1;
let a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
let AB = vec2<f32>(-1.04, 1.04) * a004 + r.zw;
return f0 * AB.x + AB.y;
}
fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 {
// clamp perceptual roughness to prevent precision problems
// According to Filament design 0.089 is recommended for mobile
// Filament uses 0.045 for non-mobile
let clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0);
return clampedPerceptualRoughness * clampedPerceptualRoughness;
}
// from https://64.github.io/tonemapping/
// reinhard on RGB oversaturates colors
fn reinhard(color: vec3<f32>) -> vec3<f32> {
return color / (1.0 + color);
}
fn reinhard_extended(color: vec3<f32>, max_white: f32) -> vec3<f32> {
let numerator = color * (1.0f + (color / vec3<f32>(max_white * max_white)));
return numerator / (1.0 + color);
}
// luminance coefficients from Rec. 709.
// https://en.wikipedia.org/wiki/Rec._709
fn luminance(v: vec3<f32>) -> f32 {
return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));
}
fn change_luminance(c_in: vec3<f32>, l_out: f32) -> vec3<f32> {
let l_in = luminance(c_in);
return c_in * (l_out / l_in);
}
fn reinhard_luminance(color: vec3<f32>) -> vec3<f32> {
let l_old = luminance(color);
let l_new = l_old / (1.0f + l_old);
return change_luminance(color, l_new);
}
fn reinhard_extended_luminance(color: vec3<f32>, max_white_l: f32) -> vec3<f32> {
let l_old = luminance(color);
let numerator = l_old * (1.0f + (l_old / (max_white_l * max_white_l)));
let l_new = numerator / (1.0f + l_old);
return change_luminance(color, l_new);
}
fn omni_light(
world_position: vec3<f32>, light: OmniLight, roughness: f32, NdotV: f32, N: vec3<f32>, V: vec3<f32>,
R: vec3<f32>, F0: vec3<f32>, diffuseColor: vec3<f32>
) -> vec3<f32> {
let light_to_frag = light.position.xyz - world_position.xyz;
let distance_square = dot(light_to_frag, light_to_frag);
let rangeAttenuation =
getDistanceAttenuation(distance_square, light.range);
// Specular.
// Representative Point Area Lights.
// see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16
let a = roughness;
let centerToRay = dot(light_to_frag, R) * R - light_to_frag;
let closestPoint = light_to_frag + centerToRay * saturate(light.radius * inverseSqrt(dot(centerToRay, centerToRay)));
let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint));
let normalizationFactor = a / saturate(a + (light.radius * 0.5 * LspecLengthInverse));
let specularIntensity = normalizationFactor * normalizationFactor;
var L: vec3<f32> = closestPoint * LspecLengthInverse; // normalize() equivalent?
var H: vec3<f32> = normalize(L + V);
var NoL: f32 = saturate(dot(N, L));
var NoH: f32 = saturate(dot(N, H));
var LoH: f32 = saturate(dot(L, H));
let specular_light = specular(F0, roughness, H, NdotV, NoL, NoH, LoH, specularIntensity);
// Diffuse.
// Comes after specular since its NoL is used in the lighting equation.
L = normalize(light_to_frag);
H = normalize(L + V);
NoL = saturate(dot(N, L));
NoH = saturate(dot(N, H));
LoH = saturate(dot(L, H));
let diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH);
// Lout = f(v,l) Φ / { 4 π d^2 }nl
// where
// f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color
// Φ is light intensity
// our rangeAttentuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius
// It's not 100% clear where the 1/4π goes in the derivation, but we follow the filament shader and leave it out
// See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation
// TODO compensate for energy loss https://google.github.io/filament/Filament.html#materialsystem/improvingthebrdfs/energylossinspecularreflectance
// light.color.rgb is premultiplied with light.intensity on the CPU
return ((diffuse + specular_light) * light.color.rgb) * (rangeAttenuation * NoL);
}
fn fetch_shadow(light_id: i32, homogeneous_coords: vec4<f32>) -> f32 {
if (homogeneous_coords.w <= 0.0) {
return 1.0;
}
// compensate for the Y-flip difference between the NDC and texture coordinates
let flip_correction = vec2<f32>(0.5, -0.5);
let proj_correction = 1.0 / homogeneous_coords.w;
// compute texture coordinates for shadow lookup
let light_local = homogeneous_coords.xy * flip_correction * proj_correction + vec2<f32>(0.5, 0.5);
// do the lookup, using HW PCF and comparison
return textureSampleCompare(shadow_textures, shadow_textures_sampler, light_local, i32(light_id), homogeneous_coords.z * proj_correction);
}
struct FragmentInput {
[[builtin(front_facing)]] is_front: bool;
[[builtin(position)]] world_position: vec4<f32>;
[[location(0)]] world_normal: vec3<f32>;
[[location(1)]] uv: vec2<f32>;
};
[[stage(fragment)]]
fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
var output_color: vec4<f32> = material.base_color;
if ((material.flags & 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
if ((material.flags & FLAGS_UNLIT_BIT) == 0u) {
// TODO use .a for exposure compensation in HDR
var emissive: vec4<f32> = material.emissive;
if ((material.flags & FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
emissive = vec4<f32>(emissive.rgb * textureSample(emissive_texture, emissive_sampler, in.uv).rgb, 1.0);
}
// calculate non-linear roughness from linear perceptualRoughness
var metallic: f32 = material.metallic;
var perceptual_roughness: f32 = material.perceptual_roughness;
if ((material.flags & FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
let metallic_roughness = textureSample(metallic_roughness_texture, metallic_roughness_sampler, in.uv);
// Sampling from GLTF standard channels for now
metallic = metallic * metallic_roughness.b;
perceptual_roughness = perceptual_roughness * metallic_roughness.g;
}
let roughness = perceptualRoughnessToRoughness(perceptual_roughness);
var occlusion: f32 = 1.0;
if ((material.flags & FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
occlusion = textureSample(occlusion_texture, occlusion_sampler, in.uv).r;
}
var N: vec3<f32> = normalize(in.world_normal);
// FIXME: Normal maps need an additional vertex attribute and vertex stage output/fragment stage input
// Just use a separate shader for lit with normal maps?
// # ifdef STANDARDMATERIAL_NORMAL_MAP
// vec3 T = normalize(v_WorldTangent.xyz);
// vec3 B = cross(N, T) * v_WorldTangent.w;
// # endif
if ((material.flags & FLAGS_DOUBLE_SIDED_BIT) != 0u) {
if (!in.is_front) {
N = -N;
}
// # ifdef STANDARDMATERIAL_NORMAL_MAP
// T = gl_FrontFacing ? T : -T;
// B = gl_FrontFacing ? B : -B;
// # endif
}
// # ifdef STANDARDMATERIAL_NORMAL_MAP
// mat3 TBN = mat3(T, B, N);
// N = TBN * normalize(texture(sampler2D(normal_map, normal_map_sampler), v_Uv).rgb * 2.0 - 1.0);
// # endif
var V: vec3<f32>;
if (view.view_proj.z.z != 1.0) { // If the projection is not orthographic
// Only valid for a perpective projection
V = normalize(view.world_position.xyz - in.world_position.xyz);
} else {
// Ortho view vec
V = normalize(vec3<f32>(-view.view_proj.x.z, -view.view_proj.y.z, -view.view_proj.z.z));
}
// 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<f32> = vec3<f32>(0.0);
for (var i: i32 = 0; i < i32(lights.num_lights); i = i + 1) {
let light = lights.omni_lights[i];
let light_contrib = omni_light(in.world_position.xyz, light, roughness, NdotV, N, V, R, F0, diffuse_color);
let shadow = fetch_shadow(i, light.view_projection * in.world_position);
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<f32>(
light_accum +
(diffuse_ambient + specular_ambient) * lights.ambient_color.rgb * occlusion +
emissive.rgb * output_color.a,
output_color.a);
// tone_mapping
output_color = vec4<f32>(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;
}

View file

@ -30,6 +30,7 @@ image = { version = "0.23.12", default-features = false }
# misc
wgpu = "0.8"
naga = { git = "https://github.com/gfx-rs/naga", rev = "0cf5484bba530f1134badbd2a1c1a8e9daf2e9c3", features = ["glsl-in", "spv-out", "spv-in", "wgsl-in"] }
serde = { version = "1", features = ["derive"] }
bitflags = "1.2.1"
smallvec = { version = "1.6", features = ["union", "const_generics"] }
@ -43,15 +44,6 @@ hexasphere = "3.4"
parking_lot = "0.11.0"
crevice = { path = "../../crates/crevice" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
spirv-reflect = "0.2.3"
[target.'cfg(any(all(target_arch="x86_64", target_os="linux", target_env="gnu"), all(target_arch="x86_64", target_os="macos"), all(target_arch="aarch64", target_os="android"), all(target_arch="armv7", target_os="androidabi"), all(target_arch="x86_64", target_os="windows", target_env="msvc")))'.dependencies]
bevy-glsl-to-spirv = "0.2.0"
[target.'cfg(not(any(target_arch = "wasm32", all(target_arch="x86_64", target_os="linux", target_env="gnu"), all(target_arch="x86_64", target_os="macos"), all(target_arch="aarch64", target_os="android"), all(target_arch="armv7", target_os="androidabi"), all(target_arch="x86_64", target_os="windows", target_env="msvc"))))'.dependencies]
shaderc = "0.7.0"
[features]
png = ["image/png"]
hdr = ["image/hdr"]

View file

@ -36,8 +36,8 @@ impl RenderDevice {
/// Creates a shader module from either SPIR-V or WGSL source code.
#[inline]
pub fn create_shader_module(&self, desc: &wgpu::ShaderModuleDescriptor) -> wgpu::ShaderModule {
self.device.create_shader_module(desc)
pub fn create_shader_module<'a>(&self, desc: impl Into<wgpu::ShaderModuleDescriptor<'a>>) -> wgpu::ShaderModule {
self.device.create_shader_module(&desc.into())
}
/// Check for resource cleanups and mapping callbacks.

View file

@ -1,159 +1,10 @@
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
use bevy_reflect::{TypeUuid, Uuid};
use bevy_utils::{tracing::error, BoxedFuture};
use std::marker::Copy;
use naga::{valid::ModuleInfo, Module};
use std::{borrow::Cow, marker::Copy};
use thiserror::Error;
use wgpu::ShaderStage;
/// An error that occurs during shader handling.
#[derive(Error, Debug)]
pub enum ShaderError {
/// Shader compilation error.
#[error("Shader compilation error:\n{0}")]
Compilation(String),
#[cfg(not(any(
target_arch = "wasm32",
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "android"),
all(target_arch = "armv7", target_os = "androidabi"),
all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
)))]
/// shaderc error.
#[error("shaderc error: {0}")]
ShaderC(#[from] shaderc::Error),
#[cfg(not(any(
target_arch = "wasm32",
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "android"),
all(target_arch = "armv7", target_os = "androidabi"),
all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
)))]
#[error("Error initializing shaderc Compiler")]
ErrorInitializingShadercCompiler,
#[cfg(not(any(
target_arch = "wasm32",
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "android"),
all(target_arch = "armv7", target_os = "androidabi"),
all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
)))]
#[error("Error initializing shaderc CompileOptions")]
ErrorInitializingShadercCompileOptions,
}
#[cfg(any(
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "android"),
all(target_arch = "armv7", target_os = "androidabi"),
all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
))]
fn convert_stage(s: ShaderStage) -> bevy_glsl_to_spirv::ShaderType {
match s {
ShaderStage::VERTEX => bevy_glsl_to_spirv::ShaderType::Vertex,
ShaderStage::FRAGMENT => bevy_glsl_to_spirv::ShaderType::Fragment,
ShaderStage::COMPUTE => bevy_glsl_to_spirv::ShaderType::Compute,
_ => panic!("unsupported stage type"),
}
}
#[cfg(any(
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "android"),
all(target_arch = "armv7", target_os = "androidabi"),
all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
))]
pub fn glsl_to_spirv(
glsl_source: &str,
stage: ShaderStage,
shader_defs: Option<&[String]>,
) -> Result<Vec<u32>, ShaderError> {
bevy_glsl_to_spirv::compile(glsl_source, convert_stage(stage), shader_defs)
.map_err(ShaderError::Compilation)
}
#[cfg(not(any(
target_arch = "wasm32",
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "android"),
all(target_arch = "armv7", target_os = "androidabi"),
all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
)))]
impl Into<shaderc::ShaderKind> for ShaderStage {
fn into(self) -> shaderc::ShaderKind {
match self {
ShaderStage::Vertex => shaderc::ShaderKind::Vertex,
ShaderStage::Fragment => shaderc::ShaderKind::Fragment,
ShaderStage::Compute => shaderc::ShaderKind::Compute,
}
}
}
#[cfg(not(any(
target_arch = "wasm32",
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
all(target_arch = "x86_64", target_os = "macos"),
all(target_arch = "aarch64", target_os = "android"),
all(target_arch = "armv7", target_os = "androidabi"),
all(target_arch = "x86_64", target_os = "windows", target_env = "msvc"),
)))]
pub fn glsl_to_spirv(
glsl_source: &str,
stage: ShaderStage,
shader_defs: Option<&[String]>,
) -> Result<Vec<u32>, ShaderError> {
let mut compiler =
shaderc::Compiler::new().ok_or(ShaderError::ErrorInitializingShadercCompiler)?;
let mut options = shaderc::CompileOptions::new()
.ok_or(ShaderError::ErrorInitializingShadercCompileOptions)?;
if let Some(shader_defs) = shader_defs {
for def in shader_defs.iter() {
options.add_macro_definition(def, None);
}
}
let binary_result = compiler.compile_into_spirv(
glsl_source,
stage.into(),
"shader.glsl",
"main",
Some(&options),
)?;
Ok(binary_result.as_binary().to_vec())
}
fn bytes_to_words(bytes: &[u8]) -> Vec<u32> {
let mut words = Vec::new();
for bytes4 in bytes.chunks(4) {
words.push(u32::from_le_bytes([
bytes4[0], bytes4[1], bytes4[2], bytes4[3],
]));
}
words
}
/// The full "source" of a shader
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum ShaderSource {
Spirv(Vec<u32>),
Glsl(String),
}
impl ShaderSource {
pub fn spirv_from_bytes(bytes: &[u8]) -> ShaderSource {
ShaderSource::Spirv(bytes_to_words(bytes))
}
}
use wgpu::{ShaderFlags, ShaderModuleDescriptor, ShaderSource};
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
pub struct ShaderId(Uuid);
@ -165,72 +16,77 @@ impl ShaderId {
}
}
#[derive(Error, Debug)]
pub enum ShaderReflectError {
#[error(transparent)]
WgslParse(#[from] naga::front::wgsl::ParseError),
#[error(transparent)]
SpirVParse(#[from] naga::front::spv::Error),
#[error(transparent)]
Validation(#[from] naga::valid::ValidationError),
}
/// A shader, as defined by its [ShaderSource] and [ShaderStage]
#[derive(Clone, Debug, TypeUuid)]
#[derive(Debug, TypeUuid)]
#[uuid = "d95bc916-6c55-4de3-9622-37e7b6969fda"]
pub struct Shader {
pub source: ShaderSource,
pub stage: ShaderStage,
pub enum Shader {
Wgsl(Cow<'static, str>),
SpirV(Vec<u8>),
// TODO: consider the following
// PrecompiledSpirVMacros(HashMap<HashSet<String>, Vec<u32>>)
// NagaModule(Module) ... Module impls Serialize/Deserialize
}
pub struct ShaderReflection {
pub module: Module,
pub module_info: ModuleInfo,
}
impl ShaderReflection {
pub fn get_spirv(&self) -> Result<Vec<u32>, naga::back::spv::Error> {
naga::back::spv::write_vec(
&self.module,
&self.module_info,
&naga::back::spv::Options {
flags: naga::back::spv::WriterFlags::empty(),
..naga::back::spv::Options::default()
},
)
}
}
impl Shader {
pub fn new(stage: ShaderStage, source: ShaderSource) -> Shader {
Shader { source, stage }
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_spirv(spirv: &[u8]) -> Result<Shader, ShaderError> {
use spirv_reflect::{types::ReflectShaderStageFlags, ShaderModule};
let module = ShaderModule::load_u8_data(spirv)
.map_err(|msg| ShaderError::Compilation(msg.to_string()))?;
let stage = match module.get_shader_stage() {
ReflectShaderStageFlags::VERTEX => ShaderStage::VERTEX,
ReflectShaderStageFlags::FRAGMENT => ShaderStage::FRAGMENT,
other => panic!("cannot load {:?} shader", other),
pub fn reflect(&self) -> Result<ShaderReflection, ShaderReflectError> {
let module = match &self {
// TODO: process macros here
Shader::Wgsl(source) => naga::front::wgsl::parse_str(&source)?,
Shader::SpirV(source) => naga::front::spv::parse_u8_slice(
&source,
&naga::front::spv::Options {
adjust_coordinate_space: false,
..naga::front::spv::Options::default()
},
)?,
};
let module_info = naga::valid::Validator::new(
naga::valid::ValidationFlags::default(),
naga::valid::Capabilities::default(),
)
.validate(&module)?;
Ok(Shader {
source: ShaderSource::spirv_from_bytes(spirv),
stage,
Ok(ShaderReflection {
module,
module_info,
})
}
pub fn from_glsl(stage: ShaderStage, glsl: &str) -> Shader {
Shader {
source: ShaderSource::Glsl(glsl.to_string()),
stage,
}
pub fn from_wgsl(source: impl Into<Cow<'static, str>>) -> Shader {
Shader::Wgsl(source.into())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_spirv(&self, macros: Option<&[String]>) -> Result<Vec<u32>, ShaderError> {
match self.source {
ShaderSource::Spirv(ref bytes) => Ok(bytes.clone()),
ShaderSource::Glsl(ref source) => glsl_to_spirv(&source, self.stage, macros),
}
pub fn from_spirv(source: Vec<u8>) -> Shader {
Shader::SpirV(source)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_spirv_shader(&self, macros: Option<&[String]>) -> Result<Shader, ShaderError> {
Ok(Shader {
source: ShaderSource::Spirv(self.get_spirv(macros)?),
stage: self.stage,
})
}
}
/// All stages in a shader program
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ShaderStages {
pub vertex: ShaderId,
pub fragment: Option<ShaderId>,
}
/// All stages in a compute shader program
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ComputeShaderStages {
pub compute: ShaderId,
}
#[derive(Default)]
@ -246,12 +102,8 @@ impl AssetLoader for ShaderLoader {
let ext = load_context.path().extension().unwrap().to_str().unwrap();
let shader = match ext {
"vert" => Shader::from_glsl(ShaderStage::VERTEX, std::str::from_utf8(bytes)?),
"frag" => Shader::from_glsl(ShaderStage::FRAGMENT, std::str::from_utf8(bytes)?),
#[cfg(not(target_arch = "wasm32"))]
"spv" => Shader::from_spirv(bytes)?,
#[cfg(target_arch = "wasm32")]
"spv" => panic!("cannot load .spv file on wasm"),
"spv" => Shader::from_spirv(Vec::from(bytes)),
"wgsl" => Shader::from_wgsl(String::from_utf8(Vec::from(bytes))?),
_ => panic!("unhandled extension: {}", ext),
};
@ -261,6 +113,24 @@ impl AssetLoader for ShaderLoader {
}
fn extensions(&self) -> &[&str] {
&["vert", "frag", "spv"]
&["spv", "wgsl"]
}
}
impl<'a> From<&'a Shader> for ShaderModuleDescriptor<'a> {
fn from(shader: &'a Shader) -> Self {
ShaderModuleDescriptor {
flags: ShaderFlags::default(),
label: None,
source: match shader {
Shader::Wgsl(source) => ShaderSource::Wgsl(source.clone()),
Shader::SpirV(_) => {
// TODO: we can probably just transmute the u8 array to u32?
let x = shader.reflect().unwrap();
let spirv = x.get_spirv().unwrap();
ShaderSource::SpirV(Cow::Owned(spirv))
}
},
}
}
}

View file

@ -2,22 +2,10 @@ use crate::Sprite;
use bevy_asset::{Assets, Handle};
use bevy_ecs::{prelude::*, system::SystemState};
use bevy_math::{Mat4, Vec2, Vec3, Vec4Swizzles};
use bevy_render2::{
core_pipeline::Transparent2dPhase,
mesh::{shape::Quad, Indices, Mesh, VertexAttributeValues},
render_asset::RenderAssets,
render_graph::{Node, NodeRunError, RenderGraphContext},
render_phase::{Draw, DrawFunctions, Drawable, RenderPhase, TrackedRenderPass},
render_resource::*,
renderer::{RenderContext, RenderDevice},
shader::Shader,
texture::{BevyDefault, Image},
view::{ViewMeta, ViewUniform, ViewUniformOffset},
};
use bevy_render2::{core_pipeline::Transparent2dPhase, mesh::{shape::Quad, Indices, Mesh, VertexAttributeValues}, render_asset::RenderAssets, render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::{Draw, DrawFunctions, Drawable, RenderPhase, TrackedRenderPass}, render_resource::*, renderer::{RenderContext, RenderDevice}, shader::Shader, texture::{BevyDefault, Image}, view::{ViewMeta, ViewUniform, ViewUniformOffset}};
use bevy_transform::components::GlobalTransform;
use bevy_utils::slab::{FrameSlabMap, FrameSlabMapKey};
use bytemuck::{Pod, Zeroable};
use std::borrow::Cow;
pub struct SpriteShaders {
pipeline: RenderPipeline,
@ -29,25 +17,8 @@ pub struct SpriteShaders {
impl FromWorld for SpriteShaders {
fn from_world(world: &mut World) -> Self {
let render_device = world.get_resource::<RenderDevice>().unwrap();
let vertex_shader = Shader::from_glsl(ShaderStage::VERTEX, include_str!("sprite.vert"))
.get_spirv_shader(None)
.unwrap();
let fragment_shader = Shader::from_glsl(ShaderStage::FRAGMENT, include_str!("sprite.frag"))
.get_spirv_shader(None)
.unwrap();
let vertex_spirv = vertex_shader.get_spirv(None).unwrap();
let fragment_spirv = fragment_shader.get_spirv(None).unwrap();
let vertex = render_device.create_shader_module(&ShaderModuleDescriptor {
flags: ShaderFlags::default(),
label: None,
source: ShaderSource::SpirV(Cow::Borrowed(&vertex_spirv)),
});
let fragment = render_device.create_shader_module(&ShaderModuleDescriptor {
flags: ShaderFlags::default(),
label: None,
source: ShaderSource::SpirV(Cow::Borrowed(&fragment_spirv)),
});
let shader = Shader::from_wgsl(include_str!("sprite.wgsl"));
let shader_module = render_device.create_shader_module(&shader);
let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
@ -114,12 +85,12 @@ impl FromWorld for SpriteShaders {
},
],
}],
module: &vertex,
entry_point: "main",
module: &shader_module,
entry_point: "vertex",
},
fragment: Some(FragmentState {
module: &fragment,
entry_point: "main",
module: &shader_module,
entry_point: "fragment",
targets: &[ColorTargetState {
format: TextureFormat::bevy_default(),
blend: Some(BlendState {

View file

@ -1,11 +0,0 @@
#version 450
layout(location = 0) in vec2 v_Uv;
layout(location = 0) out vec4 o_Target;
layout(set = 1, binding = 0) uniform texture2D sprite_texture;
layout(set = 1, binding = 1) uniform sampler sprite_sampler;
void main() {
o_Target = texture(sampler2D(sprite_texture, sprite_sampler), v_Uv);
}

View file

@ -1,16 +0,0 @@
#version 450
layout(location = 0) in vec3 Vertex_Position;
layout(location = 1) in vec2 Vertex_Uv;
layout(location = 0) out vec2 v_Uv;
layout(set = 0, binding = 0) uniform View {
mat4 ViewProj;
vec3 ViewWorldPosition;
};
void main() {
v_Uv = Vertex_Uv;
gl_Position = ViewProj * vec4(Vertex_Position, 1.0);
}

View file

@ -0,0 +1,34 @@
// TODO: try merging this block with the binding?
[[block]]
struct View {
view_proj: mat4x4<f32>;
world_position: vec3<f32>;
};
[[group(0), binding(0)]]
var view: View;
struct VertexOutput {
[[location(0)]] uv: vec2<f32>;
[[builtin(position)]] position: vec4<f32>;
};
[[stage(vertex)]]
fn vertex(
[[location(0)]] vertex_position: vec3<f32>,
[[location(1)]] vertex_uv: vec2<f32>
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
return out;
}
[[group(1), binding(0)]]
var sprite_texture: texture_2d<f32>;
[[group(1), binding(1)]]
var sprite_sampler: sampler;
[[stage(fragment)]]
fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
return textureSample(sprite_texture, sprite_sampler, in.uv);
}