Implement HDRI skybox support

This commit is contained in:
reo 2025-09-26 22:44:43 +03:00
parent 44489f9fe3
commit 1e9b997aeb
12 changed files with 282 additions and 17 deletions

View file

@ -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<SkyboxVertex>,
quad_ib: IndexBuffer<u16>,
/// 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<WindowSurface>) -> (VertexBuffer<SkyboxVertex>, IndexBuffer<u16>) {
// 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<WindowSurface>, 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<WindowSurface>, _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, &params).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 }
}