diff --git a/.gitattributes b/.gitattributes index 7dc2605..7182511 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ assets/models/* filter=lfs diff=lfs merge=lfs -text +assets/exr/* filter=lfs diff=lfs merge=lfs -text diff --git a/Cargo.lock b/Cargo.lock index 3665a1b..25cb3cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -319,12 +325,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "cursor-icon" version = "1.2.0" @@ -384,6 +415,21 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -603,6 +649,16 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -636,12 +692,14 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", + "exr", + "moxcms", "num-traits", "png", "zune-core", @@ -769,6 +827,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.174" @@ -864,6 +928,16 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "ndk" version = "0.9.0" @@ -1267,11 +1341,11 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" -version = "0.17.16" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "crc32fast", "fdeflate", "flate2", @@ -1310,6 +1384,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +dependencies = [ + "num-traits", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -1388,9 +1471,11 @@ name = "raidillon_glium" version = "0.1.0" dependencies = [ "anyhow", + "exr", "glam", "glium", "gltf", + "image", "imgui", "imgui-glium-renderer", "imgui-winit-support", @@ -1419,6 +1504,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2408,6 +2503,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zune-jpeg" version = "0.4.20" diff --git a/assets/exr/citrus_orchard_road_puresky_4k.exr b/assets/exr/citrus_orchard_road_puresky_4k.exr new file mode 100644 index 0000000..c57cb53 --- /dev/null +++ b/assets/exr/citrus_orchard_road_puresky_4k.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8aa6cc5bb4a5a8f5fa12870cefdd6a6b600072454add837dca45e340c1549d30 +size 70720809 diff --git a/assets/exr/qwantani_sunset_puresky_2k.exr b/assets/exr/qwantani_sunset_puresky_2k.exr new file mode 100644 index 0000000..86cd3d7 --- /dev/null +++ b/assets/exr/qwantani_sunset_puresky_2k.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f738ddd35a5e1a291eb45b30eea73b4d296dc4a05a412ae2f187ef5e95dc076a +size 18335204 diff --git a/assets/shaders/skybox.frag b/assets/shaders/skybox.frag index 8418e54..864be50 100644 --- a/assets/shaders/skybox.frag +++ b/assets/shaders/skybox.frag @@ -17,6 +17,7 @@ vec2 sample_spherical_map(vec3 v) { void main() { vec2 uv = sample_spherical_map(normalize(direction)); + uv.y = 1.0 - uv.y; vec3 color = texture(equirect, uv).rgb; frag_color = vec4(color, 1.0); } diff --git a/game/src/main.rs b/game/src/main.rs index 05ae11e..bea5fe0 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -101,8 +101,8 @@ fn main() { let platform = GliumPlatform::initialize( engine, "Raidillon".to_string(), - 1920, - 1080, + 2560, + 1440, ); platform.run() }; diff --git a/glium_platform/Cargo.toml b/glium_platform/Cargo.toml index 0c0ecc2..6e51be6 100644 --- a/glium_platform/Cargo.toml +++ b/glium_platform/Cargo.toml @@ -18,3 +18,5 @@ indexmap = "2.10.0" imgui = "0.12.0" imgui-winit-support = "0.13.0" imgui-glium-renderer = "0.13.0" +exr = "1.73.0" +image = { version = "0.25.8", default-features = false, features = ["exr"] } diff --git a/glium_platform/src/platform.rs b/glium_platform/src/platform.rs index 16df321..f03cd96 100644 --- a/glium_platform/src/platform.rs +++ b/glium_platform/src/platform.rs @@ -14,8 +14,9 @@ use raidillon_core::engine::EngineTrait; use raidillon_core::time; use raidillon_core::time::Time; use crate::render::debug_ui::ImguiBridge; -use crate::render::BasicMeshRenderingSystem; +use crate::render::{BasicMeshRenderingSystem, SkyboxRenderingSystem}; use crate::GliumAssetManager; +use glam::Vec3; pub struct GliumPlatform> { event_loop: EventLoop<()>, @@ -44,7 +45,8 @@ impl> Platform for GliumPlatfor let time_cfg = time::Config::default(); let time = time::Time::new(time_cfg); - // Install rendering systems + // Install rendering systems in order + rendering_system_manager.add::(&display, &window); rendering_system_manager.add::(&display, &window); rendering_system_manager.add::(&display, &window); @@ -95,12 +97,14 @@ impl> Platform for GliumPlatfor asset_manager: self.asset_manager.clone(), window: &mut self.window, debug_ui_buffer, + env_light_dir: Vec3::new(0.0, -1.0, 0.0), }; self.rendering_system_manager .systems .values_mut() .for_each(|system| system.render(&mut context)); + target.finish().unwrap(); } _ => {}, diff --git a/glium_platform/src/render/basic.rs b/glium_platform/src/render/basic.rs index 0f1db93..8551ecd 100644 --- a/glium_platform/src/render/basic.rs +++ b/glium_platform/src/render/basic.rs @@ -55,14 +55,8 @@ impl RenderingSystem for BasicMeshRenderingSystem { } }; - // Direction from the light source (0,+Y) towards the scene. - let light_dir: Vec3 = Vec3::new(0.0, -1.0, 0.0).normalize(); - - // let asset_manager = ctx.asset_manager.borrow(); - // let any_ref: &dyn Any = &**asset_manager; - // if let Some(glium_manager) = any_ref.downcast_ref::() { - // &glium_manager.models; - // } + // Use HDR-derived environment light direction if provided, otherwise default to downward + let light_dir: Vec3 = if ctx.env_light_dir.length_squared() > 0.0 { ctx.env_light_dir.normalize() } else { Vec3::new(0.0, -1.0, 0.0) }; let asset_manager = ctx.asset_manager.borrow(); diff --git a/glium_platform/src/render/mod.rs b/glium_platform/src/render/mod.rs index c31a0d9..f49e308 100644 --- a/glium_platform/src/render/mod.rs +++ b/glium_platform/src/render/mod.rs @@ -1,4 +1,6 @@ mod basic; pub mod debug_ui; +mod skybox; pub use basic::BasicMeshRenderingSystem; +pub use skybox::SkyboxRenderingSystem; diff --git a/glium_platform/src/render/skybox.rs b/glium_platform/src/render/skybox.rs new file mode 100644 index 0000000..49ea594 --- /dev/null +++ b/glium_platform/src/render/skybox.rs @@ -0,0 +1,149 @@ +use std::path::PathBuf; +use std::rc::Rc; +use std::cell::RefCell; +use glium::{Display, Program, Surface, VertexBuffer, IndexBuffer, implement_vertex}; +use glium::glutin::surface::WindowSurface; +use glium::index::PrimitiveType; +use glium::texture::{RawImage2d, SrgbTexture2d, Texture2d}; +use glium::uniform; +use glam::{Mat4, Vec2, Vec3}; +use raidillon_assets::include_shader; +use crate::system::RenderingContext; +use crate::RenderingSystem; + +#[derive(Copy, Clone)] +struct SkyboxVertex { position: [f32; 3] } +implement_vertex!(SkyboxVertex, position); + +pub struct SkyboxRenderingSystem { + program: Program, + quad_vb: VertexBuffer, + quad_ib: IndexBuffer, + /// Equirectangular HDR image, tonemapped to sRGB for skybox view + equirect_srgb: SrgbTexture2d, + /// Dominant light direction estimated from HDRI + light_dir: Vec3, +} + +impl SkyboxRenderingSystem { + fn build_cube(display: &Display) -> (VertexBuffer, IndexBuffer) { + // Unit cube centered at origin + let p = &[ + [-1.0, -1.0, -1.0], [ 1.0, -1.0, -1.0], [ 1.0, 1.0, -1.0], [-1.0, 1.0, -1.0], // back + [-1.0, -1.0, 1.0], [ 1.0, -1.0, 1.0], [ 1.0, 1.0, 1.0], [-1.0, 1.0, 1.0], // front + ]; + let verts = vec![ + SkyboxVertex { position: p[0] }, SkyboxVertex { position: p[1] }, SkyboxVertex { position: p[2] }, SkyboxVertex { position: p[3] }, // back + SkyboxVertex { position: p[4] }, SkyboxVertex { position: p[5] }, SkyboxVertex { position: p[6] }, SkyboxVertex { position: p[7] }, // front + ]; + let idx: [u16; 36] = [ + // back face + 0,1,2, 2,3,0, + // front face + 4,6,5, 6,4,7, + // left face + 0,3,7, 7,4,0, + // right face + 1,5,6, 6,2,1, + // bottom face + 0,4,5, 5,1,0, + // top face + 3,2,6, 6,7,3, + ]; + ( + VertexBuffer::new(display, &verts).unwrap(), + IndexBuffer::new(display, PrimitiveType::TrianglesList, &idx).unwrap(), + ) + } + + fn load_hdr_equirect_and_analyze(display: &Display, path: &std::path::Path) -> (SrgbTexture2d, Vec3) { + // Use image crate to decode EXR as f32 RGB + let dyn_img = image::ImageReader::open(path).expect("open exr").with_guessed_format().expect("guess format").decode().expect("decode exr"); + let hdr = dyn_img.to_rgb32f(); + let (width, height) = hdr.dimensions(); + let width = width as usize; let height = height as usize; + let mut dir_accum = Vec3::ZERO; + let mut weight_sum = 0.0f32; + for y in 0..height { + let v = (y as f32 + 0.5) / height as f32; + let theta = (v - 0.5) * std::f32::consts::PI; + let lat_weight = theta.cos().max(0.0); + for x in 0..width { + let u = (x as f32 + 0.5) / width as f32; + let phi = (u - 0.5) * 2.0 * std::f32::consts::PI; + let px = hdr.get_pixel(x as u32, y as u32).0; + let rgb = Vec3::new(px[0], px[1], px[2]); + let lum = 0.2126*rgb.x + 0.7152*rgb.y + 0.0722*rgb.z; + if lum > 0.0 { + let dir = Vec3::new(phi.cos()*theta.cos(), theta.sin(), phi.sin()*theta.cos()); + let w = lum * lat_weight; + dir_accum += dir * w; + weight_sum += w; + } + } + } + let mut light_dir = if weight_sum > 0.0 { dir_accum / weight_sum } else { Vec3::new(0.0, -1.0, 0.0) }; + if light_dir.length_squared() < 1e-6 { light_dir = Vec3::new(0.0,-1.0,0.0); } + light_dir = light_dir.normalize(); + + // Tonemap to sRGB + let mut srgb_bytes = Vec::with_capacity(width*height*4); + for y in 0..height { + for x in 0..width { + let px = hdr.get_pixel(x as u32, y as u32).0; + let mapped = Vec3::new(px[0], px[1], px[2]) / (Vec3::new(px[0], px[1], px[2]) + Vec3::ONE); + let srgb = mapped.powf(1.0/2.2); + srgb_bytes.extend_from_slice(&[ + (srgb.x.clamp(0.0,1.0)*255.0) as u8, + (srgb.y.clamp(0.0,1.0)*255.0) as u8, + (srgb.z.clamp(0.0,1.0)*255.0) as u8, + 255u8, + ]); + } + } + let raw = RawImage2d::from_raw_rgba(srgb_bytes, (width as u32, height as u32)); + let tex = SrgbTexture2d::new(display, raw).unwrap(); + (tex, light_dir) + } +} + +impl RenderingSystem for SkyboxRenderingSystem { + fn initialize(display: &Display, _window: &glium::winit::window::Window) -> Self { + const VERT_SRC: &str = include_shader!("skybox.vert"); + const FRAG_SRC: &str = include_shader!("skybox.frag"); + let program = Program::from_source(display, VERT_SRC, FRAG_SRC, None).unwrap(); + let (quad_vb, quad_ib) = Self::build_cube(display); + + // Load EXR from assets/exr + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let path = std::path::Path::new(manifest_dir).join("../assets/exr/qwantani_sunset_puresky_2k.exr"); + let (equirect_srgb, light_dir) = Self::load_hdr_equirect_and_analyze(display, &path); + Self { program, quad_vb, quad_ib, equirect_srgb, light_dir } + } + + fn render(&mut self, ctx: &mut RenderingContext) { + // Provide view and projection without translation for skybox + let cam = match ctx.scene.world.query::<&raidillon_platform::Camera>().iter().next() { + Some((_, cam)) => *cam, + None => return, + }; + let mut view = cam.view(); + // remove translation from view matrix (only orientation) + view.col_mut(3).x = 0.0; view.col_mut(3).y = 0.0; view.col_mut(3).z = 0.0; + let uniforms = uniform! { + view: view.to_cols_array_2d(), + projection: cam.projection().to_cols_array_2d(), + equirect: &self.equirect_srgb, + }; + let params = glium::DrawParameters { depth: glium::Depth { test: glium::draw_parameters::DepthTest::IfLessOrEqual, write: false, ..Default::default() }, ..Default::default() }; + ctx.target.draw(&self.quad_vb, &self.quad_ib, &self.program, &uniforms, ¶ms).ok(); + + // Share light direction with following passes + ctx.env_light_dir = self.light_dir; + } +} + +// Provide a getter for light direction for other systems +impl SkyboxRenderingSystem { + pub fn light_direction(&self) -> Vec3 { self.light_dir } +} diff --git a/glium_platform/src/system.rs b/glium_platform/src/system.rs index 7f1f55e..891fec0 100644 --- a/glium_platform/src/system.rs +++ b/glium_platform/src/system.rs @@ -7,6 +7,7 @@ use glium::glutin::surface::WindowSurface; use raidillon_assets::ModelManagerRef; use raidillon_core::DebugUIBuffer; use raidillon_core::scene::Scene; +use glam::Vec3; pub struct RenderingContext<'a> { pub scene: &'a Scene, @@ -14,6 +15,7 @@ pub struct RenderingContext<'a> { pub window: &'a mut glium::winit::window::Window, pub asset_manager: ModelManagerRef, pub debug_ui_buffer: Rc>, + pub env_light_dir: Vec3, } /// The internal "rendering system" trait of glium_platform.