Major Refactor: separate project into multiple crates
This commit is contained in:
parent
f943e4c945
commit
d0440f3da3
24 changed files with 209 additions and 2232 deletions
24
raidillon_render/src/camera.rs
Normal file
24
raidillon_render/src/camera.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use glam::{Mat4, Vec3};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Camera {
|
||||
pub eye: Vec3,
|
||||
pub center: Vec3,
|
||||
pub up: Vec3,
|
||||
pub fovy: f32,
|
||||
pub aspect: f32,
|
||||
pub znear: f32,
|
||||
pub zfar: f32,
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn view(&self) -> Mat4 {
|
||||
Mat4::look_at_rh(self.eye, self.center, self.up)
|
||||
}
|
||||
pub fn projection(&self) -> Mat4 {
|
||||
Mat4::perspective_rh(self.fovy, self.aspect, self.znear, self.zfar)
|
||||
}
|
||||
pub fn view_proj(&self) -> Mat4 {
|
||||
self.projection() * self.view()
|
||||
}
|
||||
}
|
||||
48
raidillon_render/src/ecs_renderer.rs
Normal file
48
raidillon_render/src/ecs_renderer.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use raidillon_ecs::{Transform, ModelHandle};
|
||||
use hecs::{Entity, World};
|
||||
use crate::render::GliumRenderer;
|
||||
use crate::model::Model;
|
||||
|
||||
/// This system joins the renderer and ECS,
|
||||
/// and provides tools to use them together
|
||||
/// effectively.
|
||||
pub struct ECSRenderer {
|
||||
pub renderer: GliumRenderer,
|
||||
pub world: World,
|
||||
}
|
||||
|
||||
impl ECSRenderer {
|
||||
pub fn new(renderer: GliumRenderer, world: World) -> Self {
|
||||
Self { renderer, world }
|
||||
}
|
||||
|
||||
pub fn spawn_mesh(&mut self, model: Model, transform: Transform) -> Entity {
|
||||
let model_id = self.renderer.models.len();
|
||||
self.renderer.models.push(model);
|
||||
|
||||
self.world.spawn((
|
||||
transform,
|
||||
ModelHandle(model_id),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn despawn_mesh(&mut self, entity: Entity) {
|
||||
if let Ok(model_handle) = self.world.get::<&ModelHandle>(entity) {
|
||||
if model_handle.0 < self.renderer.models.len() {
|
||||
self.renderer.models.remove(model_handle.0);
|
||||
}
|
||||
}
|
||||
let _ = self.world.despawn(entity);
|
||||
}
|
||||
|
||||
/// Render a single frame using the internal renderer & world.
|
||||
pub fn render(&mut self) {
|
||||
self.renderer.render(&self.world);
|
||||
}
|
||||
|
||||
/// Render into an existing glium target surface. Useful for composing with
|
||||
/// other render passes (e.g. Dear ImGui).
|
||||
pub fn render_into<S: glium::Surface>(&mut self, target: &mut S) {
|
||||
self.renderer.render_into(&self.world, target);
|
||||
}
|
||||
}
|
||||
180
raidillon_render/src/gltf_loader.rs
Normal file
180
raidillon_render/src/gltf_loader.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
//! GPU-ready mesh loader for **glTF 2.0** (internal helper)
|
||||
//!
|
||||
//! Converts the first primitive of a glTF document into our engine `Model`.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use glium::{backend::Facade, IndexBuffer, VertexBuffer};
|
||||
use glium::index::PrimitiveType;
|
||||
use gltf::mesh::util::ReadIndices;
|
||||
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, F>(path: P, facade: &F) -> Result<Model>
|
||||
where
|
||||
P: AsRef<Path> + Debug,
|
||||
F: Facade + ?Sized,
|
||||
{
|
||||
// -- 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<F>(facade: &F, img: &gltf::image::Data) -> Result<Texture2d>
|
||||
where
|
||||
F: Facade + ?Sized,
|
||||
{
|
||||
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<F>(facade: &F, img: &gltf::image::Data) -> Result<SrgbTexture2d>
|
||||
where
|
||||
F: Facade + ?Sized,
|
||||
{
|
||||
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(),
|
||||
}
|
||||
}
|
||||
9
raidillon_render/src/lib.rs
Normal file
9
raidillon_render/src/lib.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod camera;
|
||||
pub mod model;
|
||||
pub mod gltf_loader;
|
||||
pub mod render;
|
||||
pub mod ecs_renderer;
|
||||
|
||||
pub use camera::Camera;
|
||||
pub use render::GliumRenderer;
|
||||
pub use ecs_renderer::ECSRenderer;
|
||||
57
raidillon_render/src/model.rs
Normal file
57
raidillon_render/src/model.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use glium::texture::{RawImage2d, SrgbTexture2d, Texture2d};
|
||||
use glium::uniforms::SamplerBehavior;
|
||||
use glam::{Vec2};
|
||||
use glium::{backend::Facade, implement_vertex, IndexBuffer, VertexBuffer};
|
||||
use glium::index::PrimitiveType;
|
||||
|
||||
#[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,
|
||||
}
|
||||
163
raidillon_render/src/render.rs
Normal file
163
raidillon_render/src/render.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
use crate::camera::Camera;
|
||||
use raidillon_ecs::{ModelHandle, Transform};
|
||||
use crate::model::{Model, Mesh};
|
||||
use glium::texture::{RawImage2d, SrgbTexture2d};
|
||||
use glium::{uniform, Program, Surface};
|
||||
use glium::uniforms::{MinifySamplerFilter, MagnifySamplerFilter, SamplerWrapFunction};
|
||||
use glam::{Vec3, Vec4};
|
||||
use hecs::World;
|
||||
use glium::glutin::surface::WindowSurface;
|
||||
use image::io::Reader as ImageReader;
|
||||
use glium::draw_parameters::DepthTest;
|
||||
|
||||
pub struct GliumRenderer {
|
||||
display: glium::Display<WindowSurface>,
|
||||
program: Program,
|
||||
white_tex: SrgbTexture2d,
|
||||
|
||||
pub models: Vec<Model>,
|
||||
|
||||
params: glium::DrawParameters<'static>,
|
||||
|
||||
skybox_program: Program,
|
||||
skybox_texture: SrgbTexture2d,
|
||||
skybox_mesh: Mesh,
|
||||
}
|
||||
|
||||
impl GliumRenderer {
|
||||
pub fn new(display: glium::Display<WindowSurface>) -> anyhow::Result<Self> {
|
||||
const VERT_SRC: &str = include_str!("../../resources/shaders/gl_textured.vert");
|
||||
const FRAG_SRC: &str = include_str!("../../resources/shaders/gl_textured.frag");
|
||||
|
||||
let program = Program::from_source(&display, VERT_SRC, FRAG_SRC, None)?;
|
||||
|
||||
let white_tex = {
|
||||
let data = vec![255u8, 255u8, 255u8, 255u8];
|
||||
let raw = RawImage2d::from_raw_rgba(data, (1, 1));
|
||||
SrgbTexture2d::new(&display, raw)?
|
||||
};
|
||||
|
||||
let params = glium::DrawParameters {
|
||||
depth: glium::Depth {
|
||||
test: glium::draw_parameters::DepthTest::IfLess,
|
||||
write: true,
|
||||
.. Default::default()
|
||||
},
|
||||
.. Default::default()
|
||||
};
|
||||
|
||||
let sky_vert = include_str!("../../resources/shaders/skybox.vert");
|
||||
let sky_frag = include_str!("../../resources/shaders/skybox.frag");
|
||||
let skybox_program = Program::from_source(&display, sky_vert, sky_frag, None)?;
|
||||
|
||||
let image = ImageReader::open("resources/skyboxes/sky_24_2k.png")?.decode()?.to_rgba8();
|
||||
let dimensions = image.dimensions();
|
||||
let raw = RawImage2d::from_raw_rgba(image.into_raw(), dimensions);
|
||||
let skybox_texture = SrgbTexture2d::new(&display, raw)?;
|
||||
|
||||
let cube_model = crate::gltf_loader::load_gltf("resources/models/cube.gltf", &display)?;
|
||||
let skybox_mesh = cube_model.mesh;
|
||||
|
||||
Ok(Self {
|
||||
display,
|
||||
program,
|
||||
white_tex,
|
||||
models: Vec::new(),
|
||||
params,
|
||||
skybox_program,
|
||||
skybox_texture,
|
||||
skybox_mesh,
|
||||
})
|
||||
}
|
||||
|
||||
fn draw_scene<S: Surface>(&self, world: &World, target: &mut S) {
|
||||
let cam = match world.query::<&Camera>().iter().next() {
|
||||
Some((_, cam)) => *cam,
|
||||
None => {
|
||||
eprintln!("[renderer] No camera component found. Skipping frame");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Direction from the light source (0,+Y) towards the scene.
|
||||
let light_dir: Vec3 = Vec3::new(0.0, -1.0, 0.0).normalize();
|
||||
|
||||
for (_, (tr, mh)) in world.query::<(&Transform, &ModelHandle)>().iter() {
|
||||
let model = &self.models[mh.0];
|
||||
let mesh = &model.mesh;
|
||||
let mat = &model.material;
|
||||
|
||||
let tex_ref: &SrgbTexture2d = mat.base_color.as_ref().unwrap_or(&self.white_tex);
|
||||
|
||||
let mut sampler = tex_ref.sampled();
|
||||
sampler = sampler.wrap_function(SamplerWrapFunction::Repeat);
|
||||
sampler = sampler.minify_filter(MinifySamplerFilter::Linear);
|
||||
sampler = sampler.magnify_filter(MagnifySamplerFilter::Linear);
|
||||
|
||||
let c = mat.base_color_factor;
|
||||
|
||||
let uniforms = uniform! {
|
||||
model: tr.matrix().to_cols_array_2d(),
|
||||
view: cam.view().to_cols_array_2d(),
|
||||
projection: cam.projection().to_cols_array_2d(),
|
||||
u_light: [light_dir.x, light_dir.y, light_dir.z],
|
||||
tex: sampler,
|
||||
color: [c[0], c[1], c[2]],
|
||||
uv_offset: [mat.uv_offset.x, mat.uv_offset.y],
|
||||
uv_scale: [mat.uv_scale.x, mat.uv_scale.y],
|
||||
};
|
||||
|
||||
target.draw(
|
||||
&mesh.vbuf,
|
||||
&mesh.ibuf,
|
||||
&self.program,
|
||||
&uniforms,
|
||||
&self.params,
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
// Render skybox
|
||||
let mut sky_view = cam.view();
|
||||
sky_view.w_axis = Vec4::new(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
let mut sampler = self.skybox_texture.sampled();
|
||||
sampler = sampler.wrap_function(SamplerWrapFunction::Clamp);
|
||||
sampler = sampler.minify_filter(MinifySamplerFilter::Linear);
|
||||
sampler = sampler.magnify_filter(MagnifySamplerFilter::Linear);
|
||||
|
||||
let uniforms = uniform! {
|
||||
view: sky_view.to_cols_array_2d(),
|
||||
projection: cam.projection().to_cols_array_2d(),
|
||||
equirect: sampler,
|
||||
};
|
||||
|
||||
let sky_params = glium::DrawParameters {
|
||||
depth: glium::Depth {
|
||||
test: DepthTest::IfLessOrEqual,
|
||||
write: false,
|
||||
.. Default::default()
|
||||
},
|
||||
.. Default::default()
|
||||
};
|
||||
|
||||
target.draw(
|
||||
&self.skybox_mesh.vbuf,
|
||||
&self.skybox_mesh.ibuf,
|
||||
&self.skybox_program,
|
||||
&uniforms,
|
||||
&sky_params,
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
pub fn render_into<S: Surface>(&mut self, world: &World, target: &mut S) {
|
||||
target.clear_color_and_depth((0.1, 0.1, 0.15, 1.0), 1.0);
|
||||
self.draw_scene(world, target);
|
||||
}
|
||||
|
||||
pub fn render(&mut self, world: &World) {
|
||||
let mut frame = self.display.draw();
|
||||
frame.clear_color_and_depth((0.1, 0.1, 0.15, 1.0), 1.0);
|
||||
self.draw_scene(world, &mut frame);
|
||||
frame.finish().unwrap();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue