Kickstart a new rewrite of the raidillon engine

Endless refactors, refactor everyday, refactor relentlessly, refactor
every morning, every day, every hour.
This commit is contained in:
reo 2025-08-06 11:43:04 +03:00
commit f7d5c14caf
19 changed files with 4391 additions and 0 deletions

View file

@ -0,0 +1,30 @@
use raidillon_platform::AssetManager;
use crate::model::Model;
use std::path::Path;
use raidillon_platform::assets::ModelHandle;
use crate::gltf_loader::load_gltf;
use glium::backend::Facade;
use std::collections::HashMap;
/// Glium platform asset manager implementation.
pub struct GliumAssetManager {
models: Vec<Option<Model>>,
facade: Box<dyn Facade>,
next_model_id: usize,
}
impl AssetManager for GliumAssetManager {
fn load_gltf(&mut self, path: &Path) -> ModelHandle {
let model_id = self.next_model_id;
let model = load_gltf(path, self.facade.as_ref()).unwrap();
self.models.push(Some(model));
self.next_model_id += 1;
model_id
}
fn unload_model(&mut self, model_handle: ModelHandle) {
if model_handle < self.models.len() {
self.models[model_handle] = None;
}
}
}

View file

@ -0,0 +1,170 @@
use anyhow::{Context, Result};
use glium::{backend::Facade, IndexBuffer, VertexBuffer};
use glium::index::PrimitiveType;
use std::{fmt::Debug, path::Path};
use crate::model::{Vertex, Mesh, Material, Model};
use glium::texture::{RawImage2d, Texture2d, SrgbTexture2d};
use glium::uniforms::{SamplerWrapFunction, MinifySamplerFilter, MagnifySamplerFilter};
use gltf::image::Format as GltfFormat;
use glam::Vec2;
/// Load a glTF 2.0 file from disk and upload the first primitive to the GPU.
pub fn load_gltf<P>(path: P, facade: &dyn Facade) -> Result<Model>
where
P: AsRef<Path> + Debug,
{
// -- parse the asset & bring buffer blobs into memory --
let (doc, buffers, images) = gltf::import(path.as_ref()).context("failed to import glTF file")?;
// -- grab the very first mesh / primitive --
let mesh = doc.meshes().next().context("glTF has no meshes")?;
let primitive = mesh.primitives().next().context("mesh has no primitives")?;
// ---------- MATERIAL ----------
let mut mat = Material::default();
let mat_idx = primitive.material().index().context("primitive has no material")?;
let material = doc.materials().nth(mat_idx).unwrap();
let pbr = material.pbr_metallic_roughness();
// Factors --------------------------------------------------
mat.base_color_factor = pbr.base_color_factor();
mat.metal_factor = pbr.metallic_factor();
mat.roughness_factor = pbr.roughness_factor();
mat.emissive_factor = material.emissive_factor();
// Helper to update sampler settings from glTF sampler
fn update_sampler(mat: &mut Material, t: &gltf::texture::Texture<'_>) {
let sampler_info = t.sampler();
mat.sampler.wrap_function.0 = match sampler_info.wrap_s() {
gltf::texture::WrappingMode::ClampToEdge => SamplerWrapFunction::Clamp,
gltf::texture::WrappingMode::MirroredRepeat => SamplerWrapFunction::Mirror,
gltf::texture::WrappingMode::Repeat => SamplerWrapFunction::Repeat,
};
mat.sampler.wrap_function.1 = match sampler_info.wrap_t() {
gltf::texture::WrappingMode::ClampToEdge => SamplerWrapFunction::Clamp,
gltf::texture::WrappingMode::MirroredRepeat => SamplerWrapFunction::Mirror,
gltf::texture::WrappingMode::Repeat => SamplerWrapFunction::Repeat,
};
if let Some(f) = sampler_info.mag_filter() {
mat.sampler.magnify_filter = match f {
gltf::texture::MagFilter::Nearest => MagnifySamplerFilter::Nearest,
gltf::texture::MagFilter::Linear => MagnifySamplerFilter::Linear,
};
}
if let Some(f) = sampler_info.min_filter() {
mat.sampler.minify_filter = match f {
gltf::texture::MinFilter::Nearest => MinifySamplerFilter::Nearest,
gltf::texture::MinFilter::Linear => MinifySamplerFilter::Linear,
gltf::texture::MinFilter::NearestMipmapNearest => MinifySamplerFilter::NearestMipmapNearest,
gltf::texture::MinFilter::NearestMipmapLinear => MinifySamplerFilter::NearestMipmapLinear,
gltf::texture::MinFilter::LinearMipmapNearest => MinifySamplerFilter::LinearMipmapNearest,
gltf::texture::MinFilter::LinearMipmapLinear => MinifySamplerFilter::LinearMipmapLinear,
};
}
}
// Base-color texture (sRGB)
if let Some(info) = pbr.base_color_texture() {
update_sampler(&mut mat, &info.texture());
let view = info.texture().source().index();
mat.base_color = Some(glium_srgb_texture(facade, &images[view])?);
}
// Metallic-Roughness (linear)
if let Some(info) = pbr.metallic_roughness_texture() {
update_sampler(&mut mat, &info.texture());
let view = info.texture().source().index();
mat.metallic_roughness = Some(glium_linear_texture(facade, &images[view])?);
}
// Normal map (linear)
if let Some(info) = primitive.material().normal_texture() {
update_sampler(&mut mat, &info.texture());
let view = info.texture().source().index();
mat.normal = Some(glium_linear_texture(facade, &images[view])?);
}
// Occlusion (linear)
if let Some(info) = primitive.material().occlusion_texture() {
update_sampler(&mut mat, &info.texture());
let view = info.texture().source().index();
mat.occlusion = Some(glium_linear_texture(facade, &images[view])?);
}
// Emissive (sRGB)
if let Some(info) = primitive.material().emissive_texture() {
update_sampler(&mut mat, &info.texture());
let view = info.texture().source().index();
mat.emissive = Some(glium_srgb_texture(facade, &images[view])?);
}
// KHR_texture_transform
if let Some(tex) = pbr.base_color_texture() {
if let Some(xform) = tex.texture_transform() {
mat.uv_offset = Vec2::new(xform.offset()[0], xform.offset()[1]);
mat.uv_scale = Vec2::new(xform.scale()[0], xform.scale()[1]);
}
}
// ---- Vertex/index data ----
let reader = primitive.reader(|buf| Some(&buffers[buf.index()].0));
let positions: Vec<[f32; 3]> = reader.read_positions().context("missing POSITION")?.collect();
let normals: Vec<[f32; 3]> = reader.read_normals().context("missing NORMAL")?.collect();
let tex_coords: Vec<[f32; 2]> = reader.read_tex_coords(0).map(|tc| tc.into_f32().collect()).unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
let indices: Vec<u32> = reader.read_indices().context("missing indices")?.into_u32().collect();
// Interleave
let vertices: Vec<Vertex> = (0..positions.len()).map(|i| Vertex { position: positions[i], normal: normals[i], tex_coords: tex_coords[i] }).collect();
let vbuf = VertexBuffer::immutable(facade, &vertices)?;
let ibuf = IndexBuffer ::immutable(facade, PrimitiveType::TrianglesList, &indices)?;
Ok(Model { mesh: Mesh { vbuf, ibuf }, material: mat })
}
/// Linear-space texture (RGBA8) from glTF image data.
fn glium_linear_texture(facade: &dyn Facade, img: &gltf::image::Data) -> Result<Texture2d>
{
let rgba = to_rgba(img);
let raw = RawImage2d::from_raw_rgba(rgba, (img.width, img.height));
Ok(Texture2d::new(facade, raw)?)
}
/// sRGB texture from glTF image data.
fn glium_srgb_texture(facade: &dyn Facade, img: &gltf::image::Data) -> Result<SrgbTexture2d>
{
let rgba = to_rgba(img);
let raw = RawImage2d::from_raw_rgba(rgba, (img.width, img.height));
Ok(SrgbTexture2d::new(facade, raw)?)
}
/// Convert various glTF image formats to RGBA8 as expected by glium.
fn to_rgba(img: &gltf::image::Data) -> Vec<u8> {
match img.format {
GltfFormat::R8G8B8A8 => img.pixels.clone(),
GltfFormat::R8G8B8 => {
// Expand RGB to RGBA with alpha=255
img.pixels
.chunks(3)
.flat_map(|rgb| [rgb[0], rgb[1], rgb[2], 255u8])
.collect()
}
GltfFormat::R8G8 => {
// Treat RG as luminance+alpha? For simplicity, replicate first channel into RGB, second as alpha.
img.pixels
.chunks(2)
.flat_map(|rg| [rg[0], rg[0], rg[0], rg[1]])
.collect()
}
GltfFormat::R8 => {
// Grayscale: replicate into RGB, alpha=255
img.pixels
.iter()
.flat_map(|l| [*l, *l, *l, 255u8])
.collect()
}
_ => img.pixels.clone(),
}
}

View file

@ -0,0 +1,7 @@
pub mod platform;
pub mod assets;
pub mod model;
pub mod gltf_loader;
pub use assets::GliumAssetManager;
pub use platform::GliumPlatform;

View file

@ -0,0 +1,57 @@
use glium::{IndexBuffer, VertexBuffer, implement_vertex};
use glium::texture::{SrgbTexture2d, Texture2d};
use glium::uniforms::SamplerBehavior;
use glam::{Vec2};
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Copy, Clone)]
pub struct Vertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub tex_coords: [f32; 2],
}
implement_vertex!(Vertex, position, normal, tex_coords);
pub struct Mesh {
pub vbuf: VertexBuffer<Vertex>,
pub ibuf: IndexBuffer<u32>,
}
pub struct Material {
pub base_color: Option<SrgbTexture2d>,
pub metallic_roughness: Option<Texture2d>,
pub normal: Option<Texture2d>,
pub occlusion: Option<Texture2d>,
pub emissive: Option<SrgbTexture2d>,
pub sampler: SamplerBehavior,
pub uv_offset: Vec2,
pub uv_scale: Vec2,
pub base_color_factor: [f32; 4],
pub emissive_factor: [f32; 3],
pub metal_factor: f32,
pub roughness_factor: f32,
}
impl Default for Material {
fn default() -> Self {
Self {
base_color: None,
metallic_roughness: None,
normal: None,
occlusion: None,
emissive: None,
sampler: SamplerBehavior::default(),
uv_offset: Vec2::ZERO,
uv_scale: Vec2::ONE,
base_color_factor: [1.0; 4],
emissive_factor: [0.0; 3],
metal_factor: 1.0,
roughness_factor: 1.0,
}
}
}
pub struct Model {
pub mesh: Mesh,
pub material: Material,
}

View file

@ -0,0 +1,31 @@
use raidillon_platform::Platform;
use glium::winit::event_loop::EventLoop;
use glium::winit::window::Window;
use glium::backend::glutin::Display;
use glium::glutin::surface::WindowSurface;
use glium::backend::glutin::SimpleWindowBuilder;
pub struct GliumPlatform {
event_loop: EventLoop<()>,
window: Window,
display: Display<WindowSurface>,
}
impl Platform for GliumPlatform {
fn initialize(title: String, width: u32, height: u32) -> Self {
let event_loop = glium::winit::event_loop::EventLoop::builder()
.build()
.expect("create event-loop");
let (window, display) = SimpleWindowBuilder::new()
.with_title(title.as_str())
.with_inner_size(width, height)
.build(&event_loop);
Self {
event_loop,
window,
display
}
}
}