diff --git a/examples/3rd_person.rs b/examples/3rd_person.rs index 3628bc28a..afd74c727 100644 --- a/examples/3rd_person.rs +++ b/examples/3rd_person.rs @@ -28,6 +28,7 @@ pub mod shared; use crate::shared::{create_ui, fix_shadows_distance, Game, GameScene}; use rg3d::core::algebra::Vector2; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ event::{Event, VirtualKeyCode, WindowEvent}, event_loop::ControlFlow, @@ -170,7 +171,12 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - game.engine.renderer.set_frame_size(size.into()); + if let Err(e) = game.engine.renderer.set_frame_size(size.into()) { + Log::writeln( + MessageKind::Error, + format!("Unable to set frame size: {:?}", e), + ); + } // Root UI node should be resized too, otherwise progress bar will stay // in wrong position after resize. diff --git a/examples/lightmap.rs b/examples/lightmap.rs index e03b75bfc..af52a87a4 100644 --- a/examples/lightmap.rs +++ b/examples/lightmap.rs @@ -12,6 +12,7 @@ pub mod shared; use crate::shared::create_camera; use rg3d::core::futures; use rg3d::engine::resource_manager::MaterialSearchOptions; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ core::{ algebra::{UnitQuaternion, Vector2, Vector3}, @@ -535,7 +536,12 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - engine.renderer.set_frame_size(size.into()); + if let Err(e) = engine.renderer.set_frame_size(size.into()) { + Log::writeln( + MessageKind::Error, + format!("Unable to set frame size: {:?}", e), + ); + } // Root UI node should be resized too, otherwise progress bar will stay // in wrong position after resize. diff --git a/examples/lod.rs b/examples/lod.rs index 3e114d2fa..06af34145 100644 --- a/examples/lod.rs +++ b/examples/lod.rs @@ -11,6 +11,7 @@ pub mod shared; use crate::shared::create_camera; use rg3d::core::algebra::{UnitQuaternion, Vector3}; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ core::{color::Color, pool::Handle}, engine::resource_manager::{MaterialSearchOptions, ResourceManager}, @@ -247,7 +248,9 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - engine.renderer.set_frame_size(size.into()); + if let Err(e) = engine.renderer.set_frame_size(size.into()) { + Log::writeln(MessageKind::Error, format!("Unable to set frame size: {:?}", e)); + } } WindowEvent::KeyboardInput { input, .. } => { if let Some(key_code) = input.virtual_keycode { diff --git a/examples/navmesh.rs b/examples/navmesh.rs index dd29818f2..fa03773c6 100644 --- a/examples/navmesh.rs +++ b/examples/navmesh.rs @@ -10,6 +10,7 @@ pub mod shared; use crate::shared::create_camera; use rg3d::engine::resource_manager::MaterialSearchOptions; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ core::{ algebra::{Matrix4, UnitQuaternion, Vector2, Vector3}, @@ -285,7 +286,12 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - engine.renderer.set_frame_size(size.into()); + if let Err(e) = engine.renderer.set_frame_size(size.into()) { + Log::writeln( + MessageKind::Error, + format!("Unable to set frame size: {:?}", e), + ); + } } WindowEvent::KeyboardInput { input, .. } => { // Handle key input events via `WindowEvent`, not via `DeviceEvent` (#32) diff --git a/examples/save_load.rs b/examples/save_load.rs index e9c71aead..bf0d9ee6d 100644 --- a/examples/save_load.rs +++ b/examples/save_load.rs @@ -21,6 +21,7 @@ pub mod shared; use crate::shared::{create_ui, fix_shadows_distance, Game, GameScene, LocomotionMachine, Player}; use rg3d::core::algebra::Vector2; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ core::{ visitor::VisitResult, @@ -255,7 +256,9 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - game.engine.renderer.set_frame_size(size.into()); + if let Err(e) = game.engine.renderer.set_frame_size(size.into()) { + Log::writeln(MessageKind::Error, format!("Unable to set frame size: {:?}", e)); + } // Root UI node should be resized too, otherwise progress bar will stay // in wrong position after resize. diff --git a/examples/scene.rs b/examples/scene.rs index ca80dc77e..30b68642d 100644 --- a/examples/scene.rs +++ b/examples/scene.rs @@ -13,6 +13,7 @@ pub mod shared; use crate::shared::create_camera; use rg3d::engine::resource_manager::MaterialSearchOptions; use rg3d::scene::base::BaseBuilder; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ core::{ algebra::{UnitQuaternion, Vector3}, @@ -190,7 +191,12 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - engine.renderer.set_frame_size(size.into()); + if let Err(e) = engine.renderer.set_frame_size(size.into()) { + Log::writeln( + MessageKind::Error, + format!("Unable to set frame size: {:?}", e), + ); + } } WindowEvent::KeyboardInput { input, .. } => { // Handle key input events via `WindowEvent`, not via `DeviceEvent` (#32) diff --git a/examples/screenshots/3rd_person_gamma.png b/examples/screenshots/3rd_person_gamma.png new file mode 100644 index 000000000..458436e87 Binary files /dev/null and b/examples/screenshots/3rd_person_gamma.png differ diff --git a/examples/sound.rs b/examples/sound.rs index 7dc6755f3..adf3e7c06 100644 --- a/examples/sound.rs +++ b/examples/sound.rs @@ -12,6 +12,7 @@ pub mod shared; use crate::shared::{create_ui, fix_shadows_distance, Game, GameScene}; use rg3d::core::algebra::Vector2; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ animation::AnimationSignal, event::{Event, VirtualKeyCode, WindowEvent}, @@ -232,7 +233,9 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - game.engine.renderer.set_frame_size(size.into()); + if let Err(e) = game.engine.renderer.set_frame_size(size.into()) { + Log::writeln(MessageKind::Error, format!("Unable to set frame size: {:?}", e)); + } // Root UI node should be resized too, otherwise progress bar will stay // in wrong position after resize. diff --git a/examples/ui.rs b/examples/ui.rs index 72aefa16b..59487160f 100644 --- a/examples/ui.rs +++ b/examples/ui.rs @@ -12,6 +12,7 @@ pub mod shared; use crate::shared::create_camera; use rg3d::engine::resource_manager::MaterialSearchOptions; +use rg3d::utils::log::{Log, MessageKind}; use rg3d::{ animation::Animation, core::{ @@ -472,10 +473,15 @@ fn main() { )); // Due to some weird bug in winit it does not send Resized event. - engine.renderer.set_frame_size(( + if let Err(e) = engine.renderer.set_frame_size(( video_mode.size().width, video_mode.size().height, - )); + )) { + Log::writeln( + MessageKind::Error, + format!("Unable to set frame size: {:?}", e), + ); + } } } } @@ -498,7 +504,12 @@ fn main() { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. - engine.renderer.set_frame_size(size.into()); + if let Err(e) = engine.renderer.set_frame_size(size.into()) { + Log::writeln( + MessageKind::Error, + format!("Unable to set frame size: {:?}", e), + ); + } } WindowEvent::KeyboardInput { input, .. } => { if let Some(key_code) = input.virtual_keycode { diff --git a/rg3d-core/src/color.rs b/rg3d-core/src/color.rs index 8455a0234..eabfc4524 100644 --- a/rg3d-core/src/color.rs +++ b/rg3d-core/src/color.rs @@ -192,6 +192,30 @@ impl Color { Self { r, g, b, a } } + #[must_use] + pub fn srgb_to_linear(self) -> Self { + let r = ((self.r as f32 / 255.0).powf(2.2).clamp(0.0, 1.0) * 255.0) as u8; + let g = ((self.g as f32 / 255.0).powf(2.2).clamp(0.0, 1.0) * 255.0) as u8; + let b = ((self.b as f32 / 255.0).powf(2.2).clamp(0.0, 1.0) * 255.0) as u8; + Self::from_rgba(r, g, b, self.a) + } + + #[must_use] + pub fn srgb_to_linear_f32(self) -> Vector4 { + let r = (self.r as f32 / 255.0).powf(2.2).clamp(0.0, 1.0); + let g = (self.g as f32 / 255.0).powf(2.2).clamp(0.0, 1.0); + let b = (self.b as f32 / 255.0).powf(2.2).clamp(0.0, 1.0); + Vector4::new(r, g, b, self.a as f32 / 255.0) + } + + #[must_use] + pub fn linear_to_srgb(self) -> Self { + let r = ((self.r as f32 / 255.0).powf(1.0 / 2.2).clamp(0.0, 1.0) * 255.0) as u8; + let g = ((self.g as f32 / 255.0).powf(1.0 / 2.2).clamp(0.0, 1.0) * 255.0) as u8; + let b = ((self.b as f32 / 255.0).powf(1.0 / 2.2).clamp(0.0, 1.0) * 255.0) as u8; + Self::from_rgba(r, g, b, self.a) + } + pub fn as_frgba(self) -> Vector4 { Vector4::new( f32::from(self.r) / 255.0, diff --git a/src/engine/framework.rs b/src/engine/framework.rs index 5de9b11dc..ae789ecae 100644 --- a/src/engine/framework.rs +++ b/src/engine/framework.rs @@ -4,6 +4,7 @@ //! Once you get familiar with the engine, you should **not** use the framework because it is too //! limiting and may slow you down. +use crate::utils::log::{Log, MessageKind}; use crate::{ core::instant::Instant, engine::{error::EngineError, Engine}, @@ -134,7 +135,12 @@ impl Framework { match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Resized(size) => { - engine.renderer.set_frame_size(size.into()); + if let Err(e) = engine.renderer.set_frame_size(size.into()) { + Log::writeln( + MessageKind::Error, + format!("Unable to set frame size: {:?}", e), + ); + } } _ => (), } diff --git a/src/renderer/deferred_light_renderer.rs b/src/renderer/deferred_light_renderer.rs index a1c35c7e1..34e5c602d 100644 --- a/src/renderer/deferred_light_renderer.rs +++ b/src/renderer/deferred_light_renderer.rs @@ -569,7 +569,7 @@ impl DeferredLightRenderer { |program_binding| { program_binding .set_matrix4(&self.ambient_light_shader.wvp_matrix, &frame_matrix) - .set_color(&self.ambient_light_shader.ambient_color, &ambient_color) + .set_linear_color(&self.ambient_light_shader.ambient_color, &ambient_color) .set_texture( &self.ambient_light_shader.diffuse_texture, &gbuffer_diffuse_map, @@ -833,7 +833,7 @@ impl DeferredLightRenderer { .set_vector3(&shader.light_direction, &emit_direction) .set_f32(&shader.light_radius, light_radius) .set_matrix4(&shader.inv_view_proj_matrix, &inv_view_projection) - .set_color(&shader.light_color, &light.color()) + .set_linear_color(&shader.light_color, &light.color()) .set_f32( &shader.half_hotspot_cone_angle_cos, (spot_light.hotspot_cone_angle() * 0.5).cos(), @@ -880,7 +880,7 @@ impl DeferredLightRenderer { .set_vector3(&shader.light_position, &light_position) .set_f32(&shader.light_radius, light_radius) .set_matrix4(&shader.inv_view_proj_matrix, &inv_view_projection) - .set_color(&shader.light_color, &light.color()) + .set_linear_color(&shader.light_color, &light.color()) .set_matrix4(&shader.wvp_matrix, &frame_matrix) .set_vector3(&shader.camera_position, &camera_global_position) .set_f32(&shader.shadow_bias, point_light.shadow_bias()) @@ -919,7 +919,7 @@ impl DeferredLightRenderer { program_binding .set_vector3(&shader.light_direction, &emit_direction) .set_matrix4(&shader.inv_view_proj_matrix, &inv_view_projection) - .set_color(&shader.light_color, &light.color()) + .set_linear_color(&shader.light_color, &light.color()) .set_matrix4(&shader.wvp_matrix, &frame_matrix) .set_vector3(&shader.camera_position, &camera_global_position) .set_texture(&shader.depth_sampler, &gbuffer_depth_map) diff --git a/src/renderer/forward_renderer.rs b/src/renderer/forward_renderer.rs index f41f6e80a..a49500387 100644 --- a/src/renderer/forward_renderer.rs +++ b/src/renderer/forward_renderer.rs @@ -124,7 +124,7 @@ impl ForwardRenderer { &(view_projection * instance.world_transform), ) .set_bool(&self.shader.use_skeletal_animation, batch.is_skinned) - .set_color(&self.shader.color, &instance.color) + .set_linear_color(&self.shader.color, &instance.color) .set_matrix4_array( &self.shader.bone_matrices, instance.bone_matrices.as_slice(), diff --git a/src/renderer/framework/gpu_program.rs b/src/renderer/framework/gpu_program.rs index 9b4c6c84e..a51c6e7f8 100644 --- a/src/renderer/framework/gpu_program.rs +++ b/src/renderer/framework/gpu_program.rs @@ -251,7 +251,18 @@ impl<'a> GpuProgramBinding<'a> { } #[inline(always)] - pub fn set_color(self, location: &UniformLocation, value: &Color) -> Self { + pub fn set_linear_color(self, location: &UniformLocation, value: &Color) -> Self { + unsafe { + let srgb_a = value.srgb_to_linear_f32(); + self.state + .gl + .uniform_4_f32(Some(&location.id), srgb_a.x, srgb_a.y, srgb_a.z, srgb_a.w); + } + self + } + + #[inline(always)] + pub fn set_srgb_color(self, location: &UniformLocation, value: &Color) -> Self { unsafe { let rgba = value.as_frgba(); self.state diff --git a/src/renderer/framework/gpu_texture.rs b/src/renderer/framework/gpu_texture.rs index 018ca668a..3745c5ccd 100644 --- a/src/renderer/framework/gpu_texture.rs +++ b/src/renderer/framework/gpu_texture.rs @@ -75,7 +75,9 @@ pub enum PixelKind { D16, D24S8, RGBA8, + SRGBA8, RGB8, + SRGB8, BGRA8, BGR8, RG8, @@ -92,6 +94,8 @@ pub enum PixelKind { RGBA32F, R8RGTC, RG8RGTC, + R11G11B10F, + RGB10A2, } impl From for PixelKind { @@ -129,14 +133,18 @@ impl PixelKind { match self { Self::RGBA16 | Self::RGB16 | Self::RGBA32F => 8, Self::RGBA8 + | Self::SRGBA8 | Self::RGB8 + | Self::SRGB8 | Self::BGRA8 | Self::BGR8 | Self::RG16 | Self::R16 | Self::D24S8 | Self::D32F - | Self::F32 => 4, + | Self::F32 + | Self::R11G11B10F + | Self::RGB10A2 => 4, Self::RG8 | Self::D16 | Self::F16 => 2, Self::R8 | Self::R8UI => 1, Self::DXT1RGB @@ -161,7 +169,9 @@ impl PixelKind { Self::RGBA16 | Self::RGB16 | Self::RGBA8 + | Self::SRGBA8 | Self::RGB8 + | Self::SRGB8 | Self::BGRA8 | Self::BGR8 | Self::RG16 @@ -174,34 +184,39 @@ impl PixelKind { | Self::F16 | Self::R8 | Self::R8UI - | Self::RGBA32F => false, + | Self::RGBA32F + | Self::R11G11B10F + | Self::RGB10A2 => false, } } pub fn element_kind(self) -> PixelElementKind { match self { - PixelKind::F32 | PixelKind::F16 | PixelKind::RGBA32F | PixelKind::D32F => { + Self::F32 | Self::F16 | Self::RGBA32F | Self::D32F | Self::R11G11B10F => { PixelElementKind::Float } - PixelKind::D16 - | PixelKind::D24S8 - | PixelKind::RGBA8 - | PixelKind::RGB8 - | PixelKind::BGRA8 - | PixelKind::BGR8 - | PixelKind::RG8 - | PixelKind::RG16 - | PixelKind::R8 - | PixelKind::R16 - | PixelKind::RGB16 - | PixelKind::RGBA16 - | PixelKind::DXT1RGB - | PixelKind::DXT1RGBA - | PixelKind::DXT3RGBA - | PixelKind::DXT5RGBA - | PixelKind::R8RGTC - | PixelKind::RG8RGTC => PixelElementKind::NormalizedUnsignedInteger, - PixelKind::R8UI => PixelElementKind::UnsignedInteger, + Self::D16 + | Self::D24S8 + | Self::RGBA8 + | Self::SRGBA8 + | Self::RGB8 + | Self::SRGB8 + | Self::BGRA8 + | Self::BGR8 + | Self::RG8 + | Self::RG16 + | Self::R8 + | Self::R16 + | Self::RGB16 + | Self::RGBA16 + | Self::DXT1RGB + | Self::DXT1RGBA + | Self::DXT3RGBA + | Self::DXT5RGBA + | Self::R8RGTC + | Self::RG8RGTC + | Self::RGB10A2 => PixelElementKind::NormalizedUnsignedInteger, + Self::R8UI => PixelElementKind::UnsignedInteger, } } } @@ -232,12 +247,15 @@ fn image_3d_size_bytes(pixel_kind: PixelKind, width: usize, height: usize, depth PixelKind::RGBA16 => 8 * pixel_count, PixelKind::RGB16 => 6 * pixel_count, PixelKind::RGBA8 + | PixelKind::SRGBA8 | PixelKind::BGRA8 | PixelKind::RG16 | PixelKind::D24S8 | PixelKind::D32F - | PixelKind::F32 => 4 * pixel_count, - PixelKind::RGB8 | PixelKind::BGR8 => 3 * pixel_count, + | PixelKind::F32 + | PixelKind::R11G11B10F + | PixelKind::RGB10A2 => 4 * pixel_count, + PixelKind::RGB8 | PixelKind::SRGB8 | PixelKind::BGR8 => 3 * pixel_count, PixelKind::RG8 | PixelKind::R16 | PixelKind::D16 | PixelKind::F16 => 2 * pixel_count, PixelKind::R8 | PixelKind::R8UI => pixel_count, PixelKind::DXT1RGB | PixelKind::DXT1RGBA | PixelKind::R8RGTC => { @@ -258,12 +276,15 @@ fn image_2d_size_bytes(pixel_kind: PixelKind, width: usize, height: usize) -> us PixelKind::RGBA16 => 8 * pixel_count, PixelKind::RGB16 => 6 * pixel_count, PixelKind::RGBA8 + | PixelKind::SRGBA8 | PixelKind::BGRA8 | PixelKind::RG16 | PixelKind::D24S8 | PixelKind::D32F - | PixelKind::F32 => 4 * pixel_count, - PixelKind::RGB8 | PixelKind::BGR8 => 3 * pixel_count, + | PixelKind::F32 + | PixelKind::R11G11B10F + | PixelKind::RGB10A2 => 4 * pixel_count, + PixelKind::RGB8 | PixelKind::SRGB8 | PixelKind::BGR8 => 3 * pixel_count, PixelKind::RG8 | PixelKind::R16 | PixelKind::D16 | PixelKind::F16 => 2 * pixel_count, PixelKind::R8 | PixelKind::R8UI => pixel_count, PixelKind::DXT1RGB | PixelKind::DXT1RGBA | PixelKind::R8RGTC => { @@ -283,12 +304,15 @@ fn image_1d_size_bytes(pixel_kind: PixelKind, length: usize) -> usize { PixelKind::RGBA16 => 8 * length, PixelKind::RGB16 => 6 * length, PixelKind::RGBA8 + | PixelKind::SRGBA8 | PixelKind::BGRA8 | PixelKind::RG16 | PixelKind::D24S8 | PixelKind::D32F - | PixelKind::F32 => 4 * length, - PixelKind::RGB8 | PixelKind::BGR8 => 3 * length, + | PixelKind::F32 + | PixelKind::R11G11B10F + | PixelKind::RGB10A2 => 4 * length, + PixelKind::RGB8 | PixelKind::SRGB8 | PixelKind::BGR8 => 3 * length, PixelKind::RG8 | PixelKind::R16 | PixelKind::D16 | PixelKind::F16 => 2 * length, PixelKind::R8 | PixelKind::R8UI => length, PixelKind::DXT1RGB | PixelKind::DXT1RGBA | PixelKind::R8RGTC => { @@ -612,7 +636,9 @@ impl<'a> TextureBinding<'a> { glow::DEPTH24_STENCIL8, ), PixelKind::RGBA8 => (glow::UNSIGNED_BYTE, glow::RGBA, glow::RGBA8), + PixelKind::SRGBA8 => (glow::UNSIGNED_BYTE, glow::RGBA, glow::SRGB8_ALPHA8), PixelKind::RGB8 => (glow::UNSIGNED_BYTE, glow::RGB, glow::RGB8), + PixelKind::SRGB8 => (glow::UNSIGNED_BYTE, glow::RGB, glow::SRGB8), PixelKind::RG8 => (glow::UNSIGNED_BYTE, glow::RG, glow::RG8), PixelKind::R8 => (glow::UNSIGNED_BYTE, glow::RED, glow::R8), PixelKind::R8UI => (glow::UNSIGNED_BYTE, glow::RED_INTEGER, glow::R8UI), @@ -622,6 +648,7 @@ impl<'a> TextureBinding<'a> { PixelKind::R16 => (glow::UNSIGNED_SHORT, glow::RED, glow::R16), PixelKind::RGB16 => (glow::UNSIGNED_SHORT, glow::RGB, glow::RGB16), PixelKind::RGBA16 => (glow::UNSIGNED_SHORT, glow::RGBA, glow::RGBA16), + PixelKind::RGB10A2 => (glow::UNSIGNED_BYTE, glow::RGBA, glow::RGB10_A2), PixelKind::DXT1RGB => (0, 0, GL_COMPRESSED_RGB_S3TC_DXT1_EXT), PixelKind::DXT1RGBA => (0, 0, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT), PixelKind::DXT3RGBA => (0, 0, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT), @@ -629,6 +656,7 @@ impl<'a> TextureBinding<'a> { PixelKind::R8RGTC => (0, 0, COMPRESSED_RED_RGTC1), PixelKind::RG8RGTC => (0, 0, COMPRESSED_RG_RGTC2), PixelKind::RGBA32F => (glow::FLOAT, glow::RGBA, glow::RGBA32F), + PixelKind::R11G11B10F => (glow::FLOAT, glow::RGB, glow::R11F_G11F_B10F), }; let is_compressed = pixel_kind.is_compressed(); diff --git a/src/renderer/framework/shaders/shared.glsl b/src/renderer/framework/shaders/shared.glsl index 621c4f7b9..68b05059c 100644 --- a/src/renderer/framework/shaders/shared.glsl +++ b/src/renderer/framework/shaders/shared.glsl @@ -287,4 +287,14 @@ vec2 S_ComputeParallaxTextureCoordinates(in sampler2D heightTexture, vec3 eyeVec float weight = nextH / (nextH - prevH); return prev * weight + currentTexCoords * (1.0 - weight); +} + +vec4 S_LinearToSRGB(vec4 color) { + vec3 rgb = pow(color.rgb, vec3(1.0 / 2.2)); + return vec4(rgb, color.a); +} + +vec4 S_SRGBToLinear(vec4 color) { + vec3 rgb = pow(color.rgb, vec3(2.2)); + return vec4(rgb, color.a); } \ No newline at end of file diff --git a/src/renderer/gamma.rs b/src/renderer/gamma.rs new file mode 100644 index 000000000..5bb38c7c1 --- /dev/null +++ b/src/renderer/gamma.rs @@ -0,0 +1,101 @@ +use crate::renderer::framework::framebuffer::FrameBuffer; +use crate::{ + core::{ + algebra::{Matrix4, Vector3}, + math::Rect, + }, + renderer::framework::{ + error::FrameworkError, + framebuffer::{CullFace, DrawParameters}, + gpu_program::{GpuProgram, UniformLocation}, + gpu_texture::GpuTexture, + state::PipelineState, + }, + renderer::{GeometryCache, RenderPassStatistics}, + scene::mesh::surface::SurfaceData, +}; +use std::{cell::RefCell, rc::Rc}; + +struct Shader { + pub program: GpuProgram, + pub wvp_matrix: UniformLocation, + pub screen_texture: UniformLocation, +} + +impl Shader { + pub fn new(state: &mut PipelineState) -> Result { + let fragment_source = include_str!("shaders/gamma_fs.glsl"); + let vertex_source = include_str!("shaders/flat_vs.glsl"); + + let program = + GpuProgram::from_source(state, "GammaShader", vertex_source, fragment_source)?; + Ok(Self { + wvp_matrix: program.uniform_location(state, "worldViewProjection")?, + screen_texture: program.uniform_location(state, "screenTexture")?, + program, + }) + } +} + +pub struct GammaCorrectionPass { + shader: Shader, + quad: SurfaceData, +} + +impl GammaCorrectionPass { + pub fn new(state: &mut PipelineState) -> Result { + Ok(Self { + shader: Shader::new(state)?, + quad: SurfaceData::make_unit_xy_quad(), + }) + } + + pub(in crate) fn render( + &self, + state: &mut PipelineState, + viewport: Rect, + frame_texture: Rc>, + frame_buffer: &mut FrameBuffer, + geom_cache: &mut GeometryCache, + ) -> RenderPassStatistics { + let mut statistics = RenderPassStatistics::default(); + + let quad = geom_cache.get(state, &self.quad); + + let frame_matrix = Matrix4::new_orthographic( + 0.0, + viewport.w() as f32, + viewport.h() as f32, + 0.0, + -1.0, + 1.0, + ) * Matrix4::new_nonuniform_scaling(&Vector3::new( + viewport.w() as f32, + viewport.h() as f32, + 0.0, + )); + + statistics += frame_buffer.draw( + quad, + state, + viewport, + &self.shader.program, + &DrawParameters { + cull_face: CullFace::Back, + culling: false, + color_write: Default::default(), + depth_write: false, + stencil_test: false, + depth_test: false, + blend: false, + }, + |program_binding| { + program_binding + .set_matrix4(&self.shader.wvp_matrix, &frame_matrix) + .set_texture(&self.shader.screen_texture, &frame_texture); + }, + ); + + statistics + } +} diff --git a/src/renderer/gbuffer/mod.rs b/src/renderer/gbuffer/mod.rs index 1d033dd89..e5034847c 100644 --- a/src/renderer/gbuffer/mod.rs +++ b/src/renderer/gbuffer/mod.rs @@ -1,3 +1,10 @@ +//! GBuffer Layout: +//! +//! RT0: sRGBA8 - Diffuse color. +//! RT1: RGBA8 - Normal (xyz) + specular (w) +//! RT2: RGBA8 - Ambient light + emission +//! RT3: R8UI - Decal mask + use crate::{ core::{ algebra::{Matrix4, Vector2, Vector4}, @@ -87,7 +94,7 @@ impl GBuffer { let mut diffuse_texture = GpuTexture::new( state, GpuTextureKind::Rectangle { width, height }, - PixelKind::RGBA8, + PixelKind::SRGBA8, MinificationFilter::Nearest, MagnificationFilter::Nearest, 1, @@ -187,7 +194,7 @@ impl GBuffer { let frame_texture = GpuTexture::new( state, GpuTextureKind::Rectangle { width, height }, - PixelKind::RGBA8, + PixelKind::RGB10A2, MinificationFilter::Linear, MagnificationFilter::Linear, 1, @@ -434,7 +441,10 @@ impl GBuffer { initial_view_projection }; program_binding - .set_color(shader.diffuse_color.as_ref().unwrap(), &instance.color) + .set_linear_color( + shader.diffuse_color.as_ref().unwrap(), + &instance.color, + ) .set_matrix4( shader.wvp_matrix.as_ref().unwrap(), &(view_projection * instance.world_transform), @@ -535,7 +545,7 @@ impl GBuffer { .set_texture(&shader.normal_texture, &normal_texture) .set_texture(&shader.decal_mask, &decal_mask) .set_u32(&shader.layer_index, decal.layer() as u32) - .set_color(&shader.color, &decal.color()); + .set_linear_color(&shader.color, &decal.color()); }, ); } diff --git a/src/renderer/light_volume.rs b/src/renderer/light_volume.rs index 3fa3394c6..26c57650f 100644 --- a/src/renderer/light_volume.rs +++ b/src/renderer/light_volume.rs @@ -240,7 +240,10 @@ impl LightVolumeRenderer { .set_vector3(&shader.light_position, &position) .set_vector3(&shader.light_direction, &direction) .set_texture(&shader.depth_sampler, &depth_map) - .set_vector3(&shader.light_color, &light.color().as_frgba().xyz()) + .set_vector3( + &shader.light_color, + &light.color().srgb_to_linear_f32().xyz(), + ) .set_vector3(&shader.scatter_factor, &light.scatter()); }, ) @@ -321,7 +324,10 @@ impl LightVolumeRenderer { .set_vector3(&shader.light_position, &position) .set_texture(&shader.depth_sampler, &depth_map) .set_f32(&shader.light_radius, point.radius()) - .set_vector3(&shader.light_color, &light.color().as_frgba().xyz()) + .set_vector3( + &shader.light_color, + &light.color().srgb_to_linear_f32().xyz(), + ) .set_vector3(&shader.scatter_factor, &light.scatter()); }, ) diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 6d439ff68..f2c644786 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -19,6 +19,7 @@ mod deferred_light_renderer; mod flat_shader; mod forward_renderer; mod fxaa; +mod gamma; mod gbuffer; mod light_volume; mod particle_system_renderer; @@ -29,6 +30,7 @@ mod ssao; mod ui_renderer; use crate::renderer::cache::CacheEntry; +use crate::renderer::gamma::GammaCorrectionPass; use crate::scene::camera::Camera; use crate::{ core::{ @@ -416,6 +418,71 @@ impl TextureUploadSender { } } +struct PostProcessingChain { + framebuffers: [FrameBuffer; 2], + swap: bool, +} + +struct PostProcessingPair<'a> { + screen_texture: Rc>, + framebuffer: &'a mut FrameBuffer, +} + +impl PostProcessingChain { + fn new(state: &mut PipelineState, width: usize, height: usize) -> Result { + fn make_framebuffer( + state: &mut PipelineState, + width: usize, + height: usize, + ) -> Result { + let frame_texture = GpuTexture::new( + state, + GpuTextureKind::Rectangle { width, height }, + PixelKind::RGB10A2, + MinificationFilter::Nearest, + MagnificationFilter::Nearest, + 1, + None, + )?; + + FrameBuffer::new( + state, + None, + vec![Attachment { + kind: AttachmentKind::Color, + texture: Rc::new(RefCell::new(frame_texture)), + }], + ) + } + + Ok(Self { + framebuffers: [ + make_framebuffer(state, width, height)?, + make_framebuffer(state, width, height)?, + ], + swap: false, + }) + } + + fn begin_pass(&mut self) -> PostProcessingPair { + let out = if self.swap { + PostProcessingPair { + screen_texture: self.framebuffers[0].color_attachments()[0].texture.clone(), + framebuffer: &mut self.framebuffers[1], + } + } else { + PostProcessingPair { + screen_texture: self.framebuffers[1].color_attachments()[0].texture.clone(), + framebuffer: &mut self.framebuffers[0], + } + }; + + self.swap = !self.swap; + + out + } +} + /// See module docs. pub struct Renderer { backbuffer: FrameBuffer, @@ -453,6 +520,8 @@ pub struct Renderer { renderer2d: Renderer2d, texture_upload_receiver: Receiver, texture_upload_sender: Sender, + post_processing_chain: PostProcessingChain, + gamma_correction_pass: GammaCorrectionPass, // TextureId -> FrameBuffer mapping. This mapping is used for temporal frame buffers // like ones used to render UI instances. ui_frame_buffers: HashMap, @@ -694,6 +763,12 @@ impl Renderer { fxaa_renderer: FxaaRenderer::new(&mut state)?, statistics: Statistics::default(), renderer2d: Renderer2d::new(&mut state)?, + gamma_correction_pass: GammaCorrectionPass::new(&mut state)?, + post_processing_chain: PostProcessingChain::new( + &mut state, + frame_size.0 as usize, + frame_size.1 as usize, + )?, texture_upload_receiver, texture_upload_sender, state, @@ -738,12 +813,20 @@ impl Renderer { /// /// Input values will be set to 1 pixel if new size is 0. Rendering cannot /// be performed into 0x0 texture. - pub fn set_frame_size(&mut self, new_size: (u32, u32)) { - self.deferred_light_renderer - .set_frame_size(&mut self.state, new_size) - .unwrap(); + pub fn set_frame_size(&mut self, new_size: (u32, u32)) -> Result<(), FrameworkError> { self.frame_size.0 = new_size.0.max(1); self.frame_size.1 = new_size.1.max(1); + + self.deferred_light_renderer + .set_frame_size(&mut self.state, new_size)?; + + self.post_processing_chain = PostProcessingChain::new( + &mut self.state, + self.frame_size.0 as usize, + self.frame_size.1 as usize, + )?; + + Ok(()) } /// Returns current (width, height) pair of back buffer size. @@ -906,6 +989,15 @@ impl Renderer { let backbuffer_width = self.frame_size.0 as f32; let backbuffer_height = self.frame_size.1 as f32; + let post_processing_pair = self.post_processing_chain.begin_pass(); + post_processing_pair.framebuffer.clear( + &mut self.state, + window_viewport, + Some(Color::BLACK), + None, + None, + ); + for (scene_handle, scene) in scenes.pair_iter().filter(|(_, s)| s.enabled) { let graph = &scene.graph; @@ -1086,12 +1178,12 @@ impl Renderer { state, viewport, gbuffer.frame_texture(), - &mut self.backbuffer, + post_processing_pair.framebuffer, &mut self.geometry_cache, ); } else { let shader = &self.flat_shader; - self.statistics.geometry += self.backbuffer.draw( + self.statistics.geometry += post_processing_pair.framebuffer.draw( self.geometry_cache.get(state, &self.quad), state, viewport, @@ -1131,14 +1223,26 @@ impl Renderer { self.statistics += self.renderer2d.render( &mut self.state, - &mut self.backbuffer, + post_processing_pair.framebuffer, Vector2::new(backbuffer_width, backbuffer_height), scenes2d, &mut self.texture_cache, self.white_dummy.clone(), )?; - // Render UI on top of everything. + drop(post_processing_pair); + + let post_processing_pair = self.post_processing_chain.begin_pass(); + + self.statistics += self.gamma_correction_pass.render( + &mut self.state, + window_viewport, + post_processing_pair.screen_texture, + &mut self.backbuffer, + &mut self.geometry_cache, + ); + + // Render UI on top of everything without gamma correction. self.statistics += self.ui_renderer.render(UiRenderContext { state: &mut self.state, viewport: window_viewport, diff --git a/src/renderer/renderer2d/mod.rs b/src/renderer/renderer2d/mod.rs index bb4e8270a..5ea6871de 100644 --- a/src/renderer/renderer2d/mod.rs +++ b/src/renderer/renderer2d/mod.rs @@ -165,7 +165,7 @@ impl BatchStorage { batch.instances.push(Instance { gpu_data: InstanceData { - color: sprite.color(), + color: sprite.color().srgb_to_linear(), world_matrix: sprite.global_transform() * Matrix4::new_scaling(sprite.size()), }, @@ -307,7 +307,7 @@ impl Renderer2d { if viewport_f32.intersects_circle(position, radius) { let light_num = light_count as usize; - let color = light.color().as_frgb(); + let color = light.color().srgb_to_linear().as_frgb(); light_position_direction[light_num] = Vector4::new(position.x, position.y, direction.x, direction.y); diff --git a/src/renderer/renderer2d/shaders/sprite_fs.glsl b/src/renderer/renderer2d/shaders/sprite_fs.glsl index 3fc3c262f..2ddcdc20c 100644 --- a/src/renderer/renderer2d/shaders/sprite_fs.glsl +++ b/src/renderer/renderer2d/shaders/sprite_fs.glsl @@ -36,5 +36,5 @@ void main() lighting += lightColor * (distanceAttenuation * directionalAttenuation); } - FragColor = vec4(lighting, 1.0) * color * texture(diffuseTexture, texCoord); + FragColor = vec4(lighting, 1.0) * color * S_SRGBToLinear(texture(diffuseTexture, texCoord)); } \ No newline at end of file diff --git a/src/renderer/shaders/gamma_fs.glsl b/src/renderer/shaders/gamma_fs.glsl new file mode 100644 index 000000000..207119dcf --- /dev/null +++ b/src/renderer/shaders/gamma_fs.glsl @@ -0,0 +1,10 @@ +#version 330 core + +uniform sampler2D screenTexture; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor = S_LinearToSRGB(texture(screenTexture, texCoord)); +} diff --git a/src/renderer/shaders/particle_system_fs.glsl b/src/renderer/shaders/particle_system_fs.glsl index 44a8fbc86..a2d3533a7 100644 --- a/src/renderer/shaders/particle_system_fs.glsl +++ b/src/renderer/shaders/particle_system_fs.glsl @@ -22,6 +22,6 @@ void main() float sceneDepth = toProjSpace(texture(depthBufferTexture, gl_FragCoord.xy * invScreenSize).r); float fragmentDepth = toProjSpace(gl_FragCoord.z); float depthOpacity = smoothstep((sceneDepth - fragmentDepth) * softBoundarySharpnessFactor, 0.0, 1.0); - FragColor = color * texture(diffuseTexture, texCoord).r; + FragColor = color * S_SRGBToLinear(texture(diffuseTexture, texCoord)).r; FragColor.a *= depthOpacity; } \ No newline at end of file diff --git a/src/renderer/shaders/point_volumetric_fs.glsl b/src/renderer/shaders/point_volumetric_fs.glsl index ae26bd1a1..c4c6d447c 100644 --- a/src/renderer/shaders/point_volumetric_fs.glsl +++ b/src/renderer/shaders/point_volumetric_fs.glsl @@ -35,5 +35,5 @@ void main() } } - FragColor = vec4(lightColor * scatter, 1.0); + FragColor = vec4(lightColor * pow(scatter, vec3(2.2)), 1.0); } \ No newline at end of file diff --git a/src/renderer/shaders/skybox_fs.glsl b/src/renderer/shaders/skybox_fs.glsl index a7540a2b6..c791d6515 100644 --- a/src/renderer/shaders/skybox_fs.glsl +++ b/src/renderer/shaders/skybox_fs.glsl @@ -8,5 +8,5 @@ in vec3 texCoord; void main() { - FragColor = texture(cubemapTexture, texCoord); + FragColor = S_SRGBToLinear(texture(cubemapTexture, texCoord)); } diff --git a/src/renderer/shaders/spot_volumetric_fs.glsl b/src/renderer/shaders/spot_volumetric_fs.glsl index cbefdc980..1de982def 100644 --- a/src/renderer/shaders/spot_volumetric_fs.glsl +++ b/src/renderer/shaders/spot_volumetric_fs.glsl @@ -59,5 +59,5 @@ void main() } } - FragColor = vec4(lightColor * scatter, 1.0); + FragColor = vec4(lightColor * pow(scatter, vec3(2.2)), 1.0); } \ No newline at end of file diff --git a/src/renderer/shaders/sprite_fs.glsl b/src/renderer/shaders/sprite_fs.glsl index 78edef10e..ceac1e3a1 100644 --- a/src/renderer/shaders/sprite_fs.glsl +++ b/src/renderer/shaders/sprite_fs.glsl @@ -9,5 +9,5 @@ in vec2 texCoord; void main() { - FragColor = color * texture(diffuseTexture, texCoord).r; + FragColor = color * S_SRGBToLinear(texture(diffuseTexture, texCoord)).r; } \ No newline at end of file diff --git a/src/renderer/shaders/ui_fs.glsl b/src/renderer/shaders/ui_fs.glsl index b98df02d2..cdd321a14 100644 --- a/src/renderer/shaders/ui_fs.glsl +++ b/src/renderer/shaders/ui_fs.glsl @@ -1,6 +1,10 @@ #version 330 core +// IMPORTANT: UI is rendered in sRGB color space! + uniform sampler2D diffuseTexture; +uniform bool isRenderTarget; + uniform bool isFont; uniform vec4 solidColor; uniform float opacity; @@ -71,13 +75,18 @@ void main() fragColor = mix(gradientColors[current], gradientColors[next], mix_factor); } + vec4 diffuseColor = texture(diffuseTexture, texCoord); + if (isRenderTarget) { + diffuseColor = S_LinearToSRGB(diffuseColor); + } + if (isFont) { - fragColor.a *= texture(diffuseTexture, texCoord).r; + fragColor.a *= diffuseColor.r; } else { - fragColor *= texture(diffuseTexture, texCoord); + fragColor *= diffuseColor; } fragColor.a *= opacity; diff --git a/src/renderer/sprite_renderer.rs b/src/renderer/sprite_renderer.rs index a43874238..829a7c89e 100644 --- a/src/renderer/sprite_renderer.rs +++ b/src/renderer/sprite_renderer.rs @@ -29,7 +29,8 @@ impl SpriteShader { pub fn new(state: &mut PipelineState) -> Result { let fragment_source = include_str!("shaders/sprite_fs.glsl"); let vertex_source = include_str!("shaders/sprite_vs.glsl"); - let program = GpuProgram::from_source(state, "FlatShader", vertex_source, fragment_source)?; + let program = + GpuProgram::from_source(state, "SpriteShader", vertex_source, fragment_source)?; Ok(Self { view_projection_matrix: program.uniform_location(state, "viewProjectionMatrix")?, world_matrix: program.uniform_location(state, "worldMatrix")?, @@ -147,7 +148,7 @@ impl SpriteRenderer { .set_vector3(&self.shader.camera_up_vector, &camera_up) .set_vector3(&self.shader.camera_side_vector, &camera_side) .set_f32(&self.shader.size, sprite.size()) - .set_color(&self.shader.color, &sprite.color()) + .set_linear_color(&self.shader.color, &sprite.color()) .set_f32(&self.shader.rotation, sprite.rotation()); }, ); diff --git a/src/renderer/ui_renderer.rs b/src/renderer/ui_renderer.rs index 4c5750c9b..7dece9006 100644 --- a/src/renderer/ui_renderer.rs +++ b/src/renderer/ui_renderer.rs @@ -48,6 +48,7 @@ struct UiShader { bounds_min: UniformLocation, bounds_max: UniformLocation, opacity: UniformLocation, + is_render_target: UniformLocation, } impl UiShader { @@ -70,6 +71,7 @@ impl UiShader { bounds_max: program.uniform_location(state, "boundsMax")?, resolution: program.uniform_location(state, "resolution")?, opacity: program.uniform_location(state, "opacity")?, + is_render_target: program.uniform_location(state, "isRenderTarget")?, program, }) } @@ -179,6 +181,9 @@ impl UiRenderer { for cmd in drawing_context.get_commands() { let mut diffuse_texture = white_dummy.clone(); let mut is_font_texture = false; + // Render targets are in linear color space, so we must convert them to sRGB + // before drawing on screen. + let mut is_render_target = false; let mut clip_bounds = cmd.clip_bounds; clip_bounds.position.x = clip_bounds.position.x.floor(); @@ -287,9 +292,9 @@ impl UiRenderer { } CommandTexture::Texture(texture) => { if let Ok(texture) = texture.clone().0.downcast::>() { - if let Some(texture) = - texture_cache.get(state, &Texture(Resource::from(texture))) - { + let resource = Resource::from(texture); + is_render_target = resource.data_ref().is_render_target(); + if let Some(texture) = texture_cache.get(state, &Texture(resource)) { diffuse_texture = texture; } } @@ -334,6 +339,7 @@ impl UiRenderer { .set_vector2(&shader.bounds_min, &cmd.bounds.position) .set_vector2(&shader.bounds_max, &bounds_max) .set_bool(&shader.is_font, is_font_texture) + .set_bool(&shader.is_render_target, is_render_target) .set_i32( &shader.brush_type, match cmd.brush { @@ -342,7 +348,7 @@ impl UiRenderer { Brush::RadialGradient { .. } => 2, }, ) - .set_color( + .set_srgb_color( &shader.solid_color, &match cmd.brush { Brush::Solid(color) => color, diff --git a/src/resource/texture.rs b/src/resource/texture.rs index 4448b9efb..9a19b3154 100644 --- a/src/resource/texture.rs +++ b/src/resource/texture.rs @@ -158,6 +158,7 @@ pub struct TextureData { anisotropy: f32, serialize_content: bool, data_hash: u64, + is_render_target: bool, } impl ResourceData for TextureData { @@ -223,6 +224,7 @@ impl Default for TextureData { anisotropy: 16.0, serialize_content: false, data_hash: 0, + is_render_target: false, } } } @@ -254,6 +256,7 @@ impl Texture { anisotropy: 1.0, serialize_content: false, data_hash: 0, + is_render_target: true, }))) } @@ -966,6 +969,11 @@ impl TextureData { &self.bytes } + /// Returns true if the texture is used as render target. + pub fn is_render_target(&self) -> bool { + self.is_render_target + } + /// Max samples for anisotropic filtering. Default value is 16.0 (max). /// However real value passed to GPU will be clamped to maximum supported /// by current GPU. To disable anisotropic filtering set this to 1.0. diff --git a/src/scene/light/mod.rs b/src/scene/light/mod.rs index 4222ba50b..46cb7b456 100644 --- a/src/scene/light/mod.rs +++ b/src/scene/light/mod.rs @@ -235,6 +235,12 @@ impl BaseLight { self.scatter } + /// Returns current scatter factor in linear color space. + #[inline] + pub fn scatter_linear(&self) -> Vector3 { + self.scatter.map(|v| v.powf(2.2)) + } + /// Enables or disables light scattering. #[inline] pub fn enable_scatter(&mut self, state: bool) { diff --git a/src/scene/particle_system/mod.rs b/src/scene/particle_system/mod.rs index 7e3903c0c..217a0df42 100644 --- a/src/scene/particle_system/mod.rs +++ b/src/scene/particle_system/mod.rs @@ -334,12 +334,14 @@ impl ParticleSystem { for (i, particle_index) in sorted_particles.iter().enumerate() { let particle = self.particles.get(*particle_index as usize).unwrap(); + let linear_color = particle.color.srgb_to_linear(); + draw_data.vertices.push(Vertex { position: particle.position, tex_coord: Vector2::default(), size: particle.size, rotation: particle.rotation, - color: particle.color, + color: linear_color, }); draw_data.vertices.push(Vertex { @@ -347,7 +349,7 @@ impl ParticleSystem { tex_coord: Vector2::new(1.0, 0.0), size: particle.size, rotation: particle.rotation, - color: particle.color, + color: linear_color, }); draw_data.vertices.push(Vertex { @@ -355,7 +357,7 @@ impl ParticleSystem { tex_coord: Vector2::new(1.0, 1.0), size: particle.size, rotation: particle.rotation, - color: particle.color, + color: linear_color, }); draw_data.vertices.push(Vertex { @@ -363,7 +365,7 @@ impl ParticleSystem { tex_coord: Vector2::new(0.0, 1.0), size: particle.size, rotation: particle.rotation, - color: particle.color, + color: linear_color, }); let base_index = (i * 4) as u32; diff --git a/src/utils/lightmap.rs b/src/utils/lightmap.rs index 5928f1398..38f8e8713 100644 --- a/src/utils/lightmap.rs +++ b/src/utils/lightmap.rs @@ -277,7 +277,7 @@ impl Lightmap { .up_vector() .try_normalize(std::f32::EPSILON) .unwrap_or_else(Vector3::y), - color: light.color().as_frgb(), + color: light.color().srgb_to_linear().as_frgb(), })) } Light::Spot(spot) => lights.push(LightDefinition::Spot(SpotLightDefinition { @@ -285,7 +285,7 @@ impl Lightmap { intensity: 1.0, edge0: ((spot.hotspot_cone_angle() + spot.falloff_angle_delta()) * 0.5).cos(), edge1: (spot.hotspot_cone_angle() * 0.5).cos(), - color: light.color().as_frgb(), + color: light.color().srgb_to_linear().as_frgb(), direction: light .up_vector() .try_normalize(std::f32::EPSILON) @@ -298,7 +298,7 @@ impl Lightmap { handle, intensity: 1.0, position: light.global_position(), - color: light.color().as_frgb(), + color: light.color().srgb_to_linear().as_frgb(), radius: point.radius(), sqr_radius: point.radius() * point.radius(), })),