Implement HDRI skybox support
This commit is contained in:
parent
44489f9fe3
commit
1e9b997aeb
12 changed files with 282 additions and 17 deletions
149
glium_platform/src/render/skybox.rs
Normal file
149
glium_platform/src/render/skybox.rs
Normal 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, ¶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 }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue