impl: PBR!

This commit is contained in:
zack 2025-01-12 18:08:02 -05:00
parent 45498f28be
commit 46ab3dd700
No known key found for this signature in database
GPG key ID: EE8A2B709E2401D1
7 changed files with 359 additions and 355 deletions

8
Cargo.lock generated
View file

@ -2464,14 +2464,6 @@ dependencies = [
"roxmltree",
]
[[package]]
name = "shaders"
version = "0.1.0"
dependencies = [
"shaders-shared",
"spirv-std",
]
[[package]]
name = "shaders-shared"
version = "0.1.0"

View file

@ -1,12 +1,7 @@
[workspace]
resolver = "2"
members = [
"crates/rt-helper",
"crates/shaders",
"crates/shaders-shared",
"crates/vk-rs",
]
members = ["crates/rt-helper", "crates/shaders-shared", "crates/vk-rs"]
[workspace.dependencies]
ash = { version = "0.37.3" }

View file

@ -17,8 +17,8 @@ pub struct UniformBufferObject {
pub model: Mat4,
pub view: Mat4,
pub proj: Mat4,
// pub camera_pos: Vec3,
// pub material: Material,
pub camera_pos: Vec3,
pub material: Material,
}
#[repr(C)]

View file

@ -1,12 +0,0 @@
[package]
name = "shaders"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["dylib"]
[dependencies]
spirv-std.workspace = true
shaders-shared = { path = "../shaders-shared" }

View file

@ -1,69 +0,0 @@
#![cfg_attr(target_arch = "spirv", no_std)]
use shaders_shared::UniformBufferObject;
use spirv_std::num_traits::Float;
use spirv_std::{
glam::{Mat3, UVec2, Vec2, Vec3, Vec4, Vec4Swizzles},
image::Image2d,
spirv, Sampler,
};
pub const PI: f32 = core::f32::consts::PI;
fn fresnel_schlick(cos_theta: f32, f0: Vec3) -> Vec3 {
f0 + (Vec3::ONE - f0) * (1.0 - cos_theta).powf(5.0)
}
fn distribution_ggx(n: Vec3, h: Vec3, roughness: f32) -> f32 {
let a = roughness * roughness;
let a2 = a * a;
let n_dot_h = n.dot(h).max(0.0);
let n_dot_h2 = n_dot_h * n_dot_h;
let nom = a2;
let denom = (n_dot_h2 * (a2 - 1.0) + 1.0);
let denom = PI * denom * denom;
nom / denom.max(0.001)
}
fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 {
let r = roughness + 1.0;
let k = (r * r) / 8.0;
let nom = n_dot_v;
let denom = n_dot_v * (1.0 - k) + k;
nom / denom
}
fn geometry_smith(n: Vec3, v: Vec3, l: Vec3, roughness: f32) -> f32 {
let n_dot_v = n.dot(v).max(0.0);
let n_dot_l = n.dot(l).max(0.0);
let ggx2 = geometry_schlick_ggx(n_dot_v, roughness);
let ggx1 = geometry_schlick_ggx(n_dot_l, roughness);
ggx1 * ggx2
}
#[spirv(vertex)]
pub fn main_vs(
#[spirv(position)] in_pos: Vec3,
in_normal: Vec3,
in_tex_coord: Vec2,
#[spirv(uniform, descriptor_set = 0, binding = 0)] ubo: &UniformBufferObject,
out_world_pos: &mut Vec3,
out_world_normal: &mut Vec3,
out_tex_coord: &mut Vec2,
#[spirv(position)] out_pos: &mut Vec4,
) {
let pos = ubo.model * Vec4::from((in_pos, 1.0));
*out_world_pos = pos.truncate();
// Transform normal to world space
let normal_matrix = Mat3::from_mat4(ubo.model).inverse().transpose();
*out_world_normal = (normal_matrix * in_normal).normalize();
*out_pos = ubo.proj * ubo.view * pos;
*out_tex_coord = in_tex_coord;
}

View file

@ -1,12 +1,12 @@
use ash::{
extensions::khr::{Surface, Swapchain},
vk::{self, Buffer},
vk::{self, Buffer, DescriptorType},
Device,
};
use egui::Vec2;
use egui_ash::EguiCommand;
use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, Allocator};
use shaders_shared::{PushConstants, UniformBufferObject};
use shaders_shared::{Material, PushConstants, UniformBufferObject};
use spirv_std::glam::{Mat4, Vec3, Vec4};
use std::{
ffi::CString,
@ -41,84 +41,6 @@ pub struct DefaultTextures {
sampler: vk::Sampler, // Common sampler for all textures
}
pub struct Material {
base_color: Vec4,
metallic_factor: f32,
roughness_factor: f32,
base_color_texture: Option<Arc<Texture>>,
// metallic_roughness_texture: Option<Arc<Texture>>,
// normal_texture: Option<Arc<Texture>>,
}
impl Material {
fn from_gltf(
material: &gltf::Material,
device: &Device,
allocator: Arc<Mutex<Allocator>>,
command_pool: vk::CommandPool,
queue: vk::Queue,
path: &Path,
texture_cache: &mut TextureCache,
buffers: &[gltf::buffer::Data],
) -> Self {
let pbr = material.pbr_metallic_roughness();
let base_color = pbr.base_color_factor();
let metallic_factor = pbr.metallic_factor();
let roughness_factor = pbr.roughness_factor();
let base_color_texture = pbr.base_color_texture().and_then(|tex| {
let key = format!("{:?}", tex.texture().source().source());
texture_cache.get_or_load_texture(key, || {
load_texture_from_gltf(
device,
allocator.clone(),
command_pool,
queue,
&tex.texture(),
buffers,
path,
)
})
});
// let metallic_roughness_texture = pbr.metallic_roughness_texture().and_then(|tex| {
// load_texture_from_gltf(
// device,
// allocator.clone(),
// command_pool,
// queue,
// &tex.texture(),
// buffers,
// path,
// )
// .map(Arc::new)
// });
//
// let normal_texture = material.normal_texture().and_then(|tex| {
// load_texture_from_gltf(
// device,
// allocator.clone(),
// command_pool,
// queue,
// &tex.texture(),
// buffers,
// path,
// )
// .map(Arc::new)
// });
Self {
base_color: Vec4::from(base_color),
metallic_factor,
roughness_factor,
base_color_texture,
// metallic_roughness_texture,
// normal_texture,
}
}
}
#[repr(C)]
#[derive(Debug, Clone)]
struct Vertex {
@ -434,92 +356,17 @@ impl Texture {
}
}
impl DefaultTextures {
fn new(
device: &Device,
allocator: Arc<Mutex<Allocator>>,
command_pool: vk::CommandPool,
queue: vk::Queue,
) -> Self {
// Create a 1x1 white texture for default albedo
let white_data = vec![255u8, 255, 255, 255];
let white = Texture::new(
device,
allocator.clone(),
command_pool,
queue,
1,
1,
&white_data,
);
// Create a 1x1 default metallic-roughness texture
// R: unused, G: roughness (0.5), B: metallic (0.0)
let metallic_roughness_data = vec![0u8, 128, 0, 255];
let metallic_roughness = Texture::new(
device,
allocator.clone(),
command_pool,
queue,
1,
1,
&metallic_roughness_data,
);
// Create a 1x1 default normal map (pointing up)
let normal_data = vec![128u8, 128, 255, 255];
let normal = Texture::new(device, allocator, command_pool, queue, 1, 1, &normal_data);
// Create a common sampler
let sampler_create_info = vk::SamplerCreateInfo::builder()
.mag_filter(vk::Filter::LINEAR)
.min_filter(vk::Filter::LINEAR)
.address_mode_u(vk::SamplerAddressMode::REPEAT)
.address_mode_v(vk::SamplerAddressMode::REPEAT)
.address_mode_w(vk::SamplerAddressMode::REPEAT)
.anisotropy_enable(true)
.max_anisotropy(16.0)
.border_color(vk::BorderColor::INT_OPAQUE_BLACK)
.unnormalized_coordinates(false)
.compare_enable(false)
.compare_op(vk::CompareOp::ALWAYS)
.mipmap_mode(vk::SamplerMipmapMode::LINEAR)
.mip_lod_bias(0.0)
.min_lod(0.0)
.max_lod(0.0);
let sampler = unsafe {
device
.create_sampler(&sampler_create_info, None)
.expect("Failed to create sampler")
};
Self {
white,
metallic_roughness,
normal,
sampler,
}
}
fn destroy(&mut self, device: &Device, allocator: &mut Allocator) {
unsafe {
self.white.destroy(device, allocator);
self.metallic_roughness.destroy(device, allocator);
self.normal.destroy(device, allocator);
device.destroy_sampler(self.sampler, None);
}
}
}
pub struct Mesh {
vertex_buffer: Buffer,
vertex_buffer_allocation: Option<Allocation>,
vertex_count: u32,
transform: Mat4,
texture: Option<Arc<Texture>>,
material: Material,
default_textures: DefaultTextures,
albedo_texture: Option<Arc<Texture>>,
metallic_roughness_texture: Option<Arc<Texture>>,
normal_texture: Option<Arc<Texture>>,
metallic_factor: f32,
roughness_factor: f32,
base_color: Vec4,
descriptor_sets: Vec<vk::DescriptorSet>,
}
@ -539,12 +386,33 @@ impl Model {
.expect("Failed to free memory");
}
self.texture_cache.cleanup(device, allocator);
mesh.default_textures.destroy(device, allocator);
}
}
}
}
fn resize_texture(image: &image::DynamicImage, max_dimension: u32) -> image::DynamicImage {
let width = image.width();
let height = image.height();
if width <= max_dimension && height <= max_dimension {
return image.clone();
}
let aspect_ratio = width as f32 / height as f32;
let (new_width, new_height) = if width > height {
let w = max_dimension;
let h = (w as f32 / aspect_ratio) as u32;
(w, h)
} else {
let h = max_dimension;
let w = (h as f32 * aspect_ratio) as u32;
(w, h)
};
image.resize(new_width, new_height, image::imageops::FilterType::Lanczos3)
}
fn load_texture_from_gltf(
device: &Device,
allocator: Arc<Mutex<Allocator>>,
@ -557,33 +425,26 @@ fn load_texture_from_gltf(
let img_data =
gltf::image::Data::from_source(texture.source().source(), Some(path), buffers).ok()?;
tracing::info!(
"Original texture dimensions: {}x{}",
img_data.width,
img_data.height
tracing::info!("WIDTH: {}", img_data.width);
tracing::info!("HEIGHT: {}", img_data.height);
// Resize texture if needed
let image = image::DynamicImage::ImageRgb8(
image::RgbImage::from_raw(img_data.width, img_data.height, img_data.pixels).unwrap(),
);
// Convert to RGB/RGBA
let pixels_rgba = if img_data.pixels.len() == (img_data.width * img_data.height * 3) as usize {
// Image is RGB, convert to RGBA
tracing::info!("Converting RGB to RGBA");
let mut rgba_data = Vec::with_capacity((img_data.width * img_data.height * 4) as usize);
for chunk in img_data.pixels.chunks_exact(3) {
rgba_data.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
}
rgba_data
} else {
// Assume it's already RGBA
img_data.pixels
};
const MAX_TEXTURE_DIMENSION: u32 = 2048; // Adjust this value based on your needs
let resized = resize_texture(&image, MAX_TEXTURE_DIMENSION);
let pixels_rgba = resized.to_rgba8().into_raw();
Some(Texture::new(
device,
allocator,
command_pool,
queue,
img_data.width,
img_data.height,
resized.width(),
resized.height(),
&pixels_rgba,
))
}
@ -650,25 +511,17 @@ fn process_node(
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
let material = Material::from_gltf(
&primitive.material(),
device,
allocator.clone(),
command_pool,
queue,
path,
texture_cache,
buffers,
);
let material = primitive.material();
let pbr = material.pbr_metallic_roughness();
let texture = primitive
.material()
.pbr_metallic_roughness()
.base_color_texture()
.and_then(|tex| {
// Create a unique key for the texture
let key = format!("{:?}", tex.texture().source().source());
texture_cache.get_or_load_texture(key, || {
let base_color = pbr.base_color_factor();
let metallic_factor = pbr.metallic_factor();
let roughness_factor = pbr.roughness_factor();
let albedo_texture = pbr.base_color_texture().and_then(|tex| {
texture_cache.get_or_load_texture(
format!("albedo_{:?}", tex.texture().source().source()),
|| {
load_texture_from_gltf(
device,
allocator.clone(),
@ -678,8 +531,43 @@ fn process_node(
buffers,
path,
)
})
});
},
)
});
let metallic_roughness_texture = pbr.base_color_texture().and_then(|tex| {
texture_cache.get_or_load_texture(
format!("mr_{:?}", tex.texture().source().source()),
|| {
load_texture_from_gltf(
device,
allocator.clone(),
command_pool,
queue,
&tex.texture(),
buffers,
path,
)
},
)
});
let normal_texture = pbr.base_color_texture().and_then(|tex| {
texture_cache.get_or_load_texture(
format!("norm_{:?}", tex.texture().source().source()),
|| {
load_texture_from_gltf(
device,
allocator.clone(),
command_pool,
queue,
&tex.texture(),
buffers,
path,
)
},
)
});
let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
@ -724,18 +612,17 @@ fn process_node(
let (vertex_buffer, vertex_buffer_allocation) =
create_vertex_buffer(device, allocator.clone(), command_pool, queue, &vertices);
let default_textures =
DefaultTextures::new(device, allocator.clone(), command_pool, queue);
meshes.push(Mesh {
vertex_buffer,
vertex_buffer_allocation: Some(vertex_buffer_allocation),
vertex_count: vertices.len() as u32,
// Store identity matrix, it is not used here anymore
albedo_texture,
metallic_roughness_texture,
normal_texture,
metallic_factor,
roughness_factor,
base_color: base_color.into(),
transform: Mat4::IDENTITY,
texture,
material,
default_textures,
descriptor_sets: Vec::new(),
});
}
@ -1109,23 +996,27 @@ impl RendererInner {
swapchain_count: usize,
) -> Vec<vk::DescriptorSetLayout> {
let bindings = [
// Binding 0: Uniform buffer
vk::DescriptorSetLayoutBinding::builder()
.binding(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)
.build(),
// Binding 1: Texture sampler
vk::DescriptorSetLayoutBinding::builder()
.binding(1)
.descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build(),
vk::DescriptorSetLayoutBinding::builder()
.binding(2)
.descriptor_type(vk::DescriptorType::SAMPLER)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build(),
vk::DescriptorSetLayoutBinding::builder()
.binding(3)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.build(),
@ -1147,6 +1038,9 @@ impl RendererInner {
descriptor_pool: vk::DescriptorPool,
descriptor_set_layouts: &[vk::DescriptorSetLayout],
uniform_buffers: &[Buffer],
allocator: Arc<Mutex<Allocator>>,
command_pool: vk::CommandPool,
queue: vk::Queue,
mesh: &Mesh,
) -> Vec<vk::DescriptorSet> {
let descriptor_sets = unsafe {
@ -1159,6 +1053,17 @@ impl RendererInner {
.expect("failed to allocate descriptor sets")
};
let default_texture =
create_default_texture(device, allocator.clone(), command_pool, queue);
let default_metallic_roughness = create_default_metallic_roughness_texture(
device,
allocator.clone(),
command_pool,
queue,
);
let default_normal =
create_default_normal_texture(device, allocator.clone(), command_pool, queue);
for (index, &descriptor_set) in descriptor_sets.iter().enumerate() {
let buffer_info = vk::DescriptorBufferInfo::builder()
.buffer(uniform_buffers[index])
@ -1166,43 +1071,59 @@ impl RendererInner {
.range(vk::WHOLE_SIZE)
.build();
let mut descriptor_writes = vec![vk::WriteDescriptorSet::builder()
.dst_set(descriptor_set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
.buffer_info(std::slice::from_ref(&buffer_info))
.build()];
let albedo_texture = mesh.albedo_texture.as_ref().unwrap_or(&default_texture);
let albedo_info = vk::DescriptorImageInfo::builder()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(albedo_texture.image_view)
.sampler(albedo_texture.sampler)
.build();
if let Some(ref texture) = mesh.texture {
// Image view descriptor
let image_info = vk::DescriptorImageInfo::builder()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(texture.image_view)
.build();
let metallic_roughness_texture = mesh
.metallic_roughness_texture
.as_ref()
.unwrap_or(&default_metallic_roughness);
let metallic_roughness_info = vk::DescriptorImageInfo::builder()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(metallic_roughness_texture.image_view)
.sampler(metallic_roughness_texture.sampler)
.build();
descriptor_writes.push(
vk::WriteDescriptorSet::builder()
.dst_set(descriptor_set)
.dst_binding(1)
.descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
.image_info(std::slice::from_ref(&image_info))
.build(),
);
let normal_texture = mesh.normal_texture.as_ref().unwrap_or(&default_normal);
let normal_info = vk::DescriptorImageInfo::builder()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(normal_texture.image_view)
.sampler(normal_texture.sampler)
.build();
// Sampler descriptor
let sampler_info = vk::DescriptorImageInfo::builder()
.sampler(texture.sampler)
.build();
descriptor_writes.push(
vk::WriteDescriptorSet::builder()
.dst_set(descriptor_set)
.dst_binding(2)
.descriptor_type(vk::DescriptorType::SAMPLER)
.image_info(std::slice::from_ref(&sampler_info))
.build(),
);
}
let descriptor_writes = [
vk::WriteDescriptorSet::builder()
.dst_set(descriptor_set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
.buffer_info(std::slice::from_ref(&buffer_info))
.build(),
// albedo
vk::WriteDescriptorSet::builder()
.dst_set(descriptor_set)
.dst_binding(1)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&albedo_info))
.build(),
// metallic
vk::WriteDescriptorSet::builder()
.dst_set(descriptor_set)
.dst_binding(2)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&metallic_roughness_info))
.build(),
// norm
vk::WriteDescriptorSet::builder()
.dst_set(descriptor_set)
.dst_binding(3)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&normal_info))
.build(),
];
unsafe {
device.update_descriptor_sets(&descriptor_writes, &[]);
@ -1788,7 +1709,10 @@ impl RendererInner {
descriptor_pool,
&descriptor_set_layouts,
&uniform_buffers,
mesh,
allocator.clone(),
command_pool,
queue,
&mesh,
);
}
@ -2023,10 +1947,13 @@ impl RendererInner {
0.1,
1000.0,
),
// camera_pos: self.camera_position,
// metallic_factor: mesh.material.metallic_factor,
// roughness_factor: mesh.material.roughness_factor,
// base_color: mesh.material.base_color,
camera_pos: self.camera_position,
material: Material {
base_color: mesh.base_color,
metallic_factor: mesh.metallic_factor,
roughness_factor: mesh.roughness_factor,
_padding: [0.0, 0.0],
},
};
let ptr = self.uniform_buffer_allocations[self.current_frame]
@ -2224,3 +2151,57 @@ fn clamped_color(color: Vec3) -> Vec3 {
color.z.clamp(0.0, 1.0),
)
}
fn create_default_texture(
device: &Device,
allocator: Arc<Mutex<Allocator>>,
command_pool: vk::CommandPool,
queue: vk::Queue,
) -> Arc<Texture> {
let white_pixel = vec![255u8, 255, 255, 255];
Arc::new(Texture::new(
device,
allocator,
command_pool,
queue,
1,
1,
&white_pixel,
))
}
fn create_default_metallic_roughness_texture(
device: &Device,
allocator: Arc<Mutex<Allocator>>,
command_pool: vk::CommandPool,
queue: vk::Queue,
) -> Arc<Texture> {
let pixel = vec![0u8, 0, 255, 255]; // Non-metallic (0), rough (1.0)
Arc::new(Texture::new(
device,
allocator,
command_pool,
queue,
1,
1,
&pixel,
))
}
fn create_default_normal_texture(
device: &Device,
allocator: Arc<Mutex<Allocator>>,
command_pool: vk::CommandPool,
queue: vk::Queue,
) -> Arc<Texture> {
let pixel = vec![128u8, 128, 255, 255]; // Default normal pointing up
Arc::new(Texture::new(
device,
allocator,
command_pool,
queue,
1,
1,
&pixel,
))
}

View file

@ -4,14 +4,131 @@ layout(location = 0) in vec3 frag_world_position;
layout(location = 1) in vec3 frag_world_normal;
layout(location = 2) in vec2 frag_tex_coord;
layout(set = 0, binding = 1) uniform sampler2D tex_sampler;
struct Material {
vec4 base_color;
float metallic_factor;
float roughness_factor;
vec2 _padding;
};
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
vec3 camera_pos;
float _padding;
Material material;
} ubo;
layout(set = 0, binding = 1) uniform sampler2D albedo_map;
layout(set = 0, binding = 2) uniform sampler2D metallic_roughness_map;
layout(set = 0, binding = 3) uniform sampler2D normal_map;
layout(location = 0) out vec4 out_color;
void main() {
// Sample the texture
vec4 base_color = texture(tex_sampler, frag_tex_coord);
// Output final color
out_color = vec4(base_color.rgb, base_color.a);
const float PI = 3.14159265359;
// PBR functions
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness*roughness;
float a2 = a*a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH*NdotH;
float nom = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / max(denom, 0.0000001);
}
float GeometrySchlickGGX(float NdotV, float roughness) {
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
void main() {
// Sample textures
vec4 albedo = texture(albedo_map, frag_tex_coord);
vec2 metallic_roughness = texture(metallic_roughness_map, frag_tex_coord).bg;
vec3 normal = normalize(2.0 * texture(normal_map, frag_tex_coord).rgb - 1.0);
float metallic = metallic_roughness.x * ubo.material.metallic_factor;
float roughness = metallic_roughness.y * ubo.material.roughness_factor;
vec3 N = normalize(normal);
vec3 V = normalize(ubo.camera_pos - frag_world_position);
// Calculate reflectance at normal incidence
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo.rgb, metallic);
// Light parameters
vec3 light_positions[4] = vec3[](
vec3(5.0, 5.0, 5.0),
vec3(-5.0, 5.0, 5.0),
vec3(5.0, -5.0, 5.0),
vec3(-5.0, -5.0, 5.0)
);
vec3 light_colors[4] = vec3[](
vec3(23.47, 21.31, 20.79),
vec3(23.47, 21.31, 20.79),
vec3(23.47, 21.31, 20.79),
vec3(23.47, 21.31, 20.79)
);
// Reflectance equation
vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i) {
vec3 L = normalize(light_positions[i] - frag_world_position);
vec3 H = normalize(V + L);
float distance = length(light_positions[i] - frag_world_position);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = light_colors[i] * attenuation;
// Cook-Torrance BRDF
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
vec3 specular = numerator / max(denominator, 0.001);
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic;
float NdotL = max(dot(N, L), 0.0);
Lo += (kD * albedo.rgb / PI + specular) * radiance * NdotL;
}
vec3 ambient = vec3(0.03) * albedo.rgb;
vec3 color = ambient + Lo;
// HDR tonemapping
color = color / (color + vec3(1.0));
// gamma correction
color = pow(color, vec3(1.0/2.2));
out_color = vec4(color, albedo.a);
}