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

1
.gitattributes vendored
View file

@ -1 +1,2 @@
assets/models/* filter=lfs diff=lfs merge=lfs -text
assets/exr/* filter=lfs diff=lfs merge=lfs -text

114
Cargo.lock generated
View file

@ -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"

BIN
assets/exr/citrus_orchard_road_puresky_4k.exr (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/exr/qwantani_sunset_puresky_2k.exr (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -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);
}

View file

@ -101,8 +101,8 @@ fn main() {
let platform = GliumPlatform::initialize(
engine,
"Raidillon".to_string(),
1920,
1080,
2560,
1440,
);
platform.run()
};

View file

@ -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"] }

View file

@ -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<E: EngineTrait<PlatformCtx = PlatformContext>> {
event_loop: EventLoop<()>,
@ -44,7 +45,8 @@ impl<E: EngineTrait<PlatformCtx = PlatformContext>> Platform<E> 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::<SkyboxRenderingSystem>(&display, &window);
rendering_system_manager.add::<BasicMeshRenderingSystem>(&display, &window);
rendering_system_manager.add::<ImguiBridge>(&display, &window);
@ -95,12 +97,14 @@ impl<E: EngineTrait<PlatformCtx = PlatformContext>> Platform<E> 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();
}
_ => {},

View file

@ -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::<GliumAssetManager>() {
// &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();

View file

@ -1,4 +1,6 @@
mod basic;
pub mod debug_ui;
mod skybox;
pub use basic::BasicMeshRenderingSystem;
pub use skybox::SkyboxRenderingSystem;

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 }
}

View file

@ -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<RefCell<DebugUIBuffer>>,
pub env_light_dir: Vec3,
}
/// The internal "rendering system" trait of glium_platform.