use std::path::PathBuf; use std::rc::Rc; use std::cell::RefCell; use std::sync::{Arc, Mutex}; 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 glium::uniforms::{MagnifySamplerFilter, MinifySamplerFilter, SamplerWrapFunction}; use glam::{Mat4, Vec2, Vec3}; use winit::event_loop::EventLoop; 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: Arc>, event_loop: &EventLoop<()>) -> 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 mut sampler = self.equirect_srgb.sampled(); sampler = sampler.wrap_function(SamplerWrapFunction::Repeat); sampler = sampler.minify_filter(MinifySamplerFilter::Linear); sampler = sampler.magnify_filter(MagnifySamplerFilter::Linear); let uniforms = uniform! { view: view.to_cols_array_2d(), projection: cam.projection().to_cols_array_2d(), equirect: sampler, }; 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 } }