use std::collections::HashMap; use std::ffi::c_void; use std::process::abort; use cstr::cstr; use ecs::component::local::Local; use ecs::sole::Single; use ecs::system::{Into as _, System}; use ecs::{Component, Query}; use crate::camera::Camera; use crate::color::Color; use crate::data_types::dimens::Dimens; use crate::event::{Present as PresentEvent, Start as StartEvent}; use crate::lighting::{GlobalLight, LightSource}; use crate::material::{Flags as MaterialFlags, Material}; use crate::matrix::Matrix; use crate::mesh::Mesh; use crate::opengl::buffer::{Buffer, Usage as BufferUsage}; #[cfg(feature = "debug")] use crate::opengl::debug::{MessageSeverity, MessageSource, MessageType}; use crate::opengl::shader::{ Error as GlShaderError, Program as GlShaderProgram, Shader as GlShader, }; use crate::opengl::texture::{ set_active_texture_unit, Texture as GlTexture, TextureUnit, }; use crate::opengl::vertex_array::{ DataType as VertexArrayDataType, PrimitiveKind, VertexArray, }; use crate::opengl::{clear_buffers, enable, BufferClearMask, Capability}; use crate::projection::{new_perspective_matrix, Projection}; use crate::shader::Program as ShaderProgram; use crate::texture::{Id as TextureId, Texture}; use crate::transform::Transform; use crate::vector::{Vec2, Vec3}; use crate::vertex::{AttributeComponentType, Vertex}; use crate::window::Window; #[derive(Debug, Default)] pub struct Extension {} impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ecs::extension::Collector<'_>) { collector.add_system(StartEvent, initialize); collector.add_system( PresentEvent, render.into_system().initialize((GlObjects::default(),)), ); } } fn initialize(window: Single) { window .make_context_current() .expect("Failed to make window context current"); gl::load_with(|symbol| match window.get_proc_address(symbol) { Ok(addr) => addr as *const c_void, Err(err) => { println!( "FATAL ERROR: Failed to get adress of OpenGL function {symbol}: {err}", ); abort(); } }); #[cfg(feature = "debug")] initialize_debug(); let window_size = window.size().expect("Failed to get window size"); set_viewport(Vec2 { x: 0, y: 0 }, window_size); window.set_framebuffer_size_callback(|new_window_size| { set_viewport(Vec2::ZERO, new_window_size); }); enable(Capability::DepthTest); } fn render( query: Query<( Mesh, ShaderProgram, Material, Option, Transform, )>, light_source_query: Query<(LightSource, Transform)>, camera_query: Query<(Camera,)>, window: Single, global_light: Single, mut gl_objects: Local, ) { let Some(camera) = camera_query.iter().find_map(|(camera,)| { if !camera.current { return None; } Some(camera) }) else { #[cfg(feature = "debug")] tracing::warn!("No current camera. Nothing will be rendered"); return; }; // TODO: Maybe find a way to not clone here? Cloning here is needed since a light // source transform can be in both the object query and the light source query let light_source = light_source_query .iter() .next() .map(|(light_source, transform)| (light_source.clone(), transform.clone())); let GlObjects { shader_programs: gl_shader_programs, textures: gl_textures, } = &mut *gl_objects; clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); for (mesh, shader_program, material, material_flags, transform) in &query { let material_flags = material_flags .map(|material_flags| material_flags.clone()) .unwrap_or_default(); let shader_program = gl_shader_programs .entry(shader_program.u64_hash()) .or_insert_with(|| create_gl_shader_program(&shader_program).unwrap()); apply_transformation_matrices( &transform, shader_program, &camera, window.size().expect("Failed to get window size"), ); apply_light( &material, &material_flags, &global_light, shader_program, light_source .as_ref() .map(|(light_source, light_source_transform)| { (light_source, light_source_transform) }), &camera, ); for texture in &material.textures { let gl_texture = gl_textures .entry(texture.id()) .or_insert_with(|| create_gl_texture(texture)); let texture_unit = TextureUnit::from_texture_id(texture.id()).unwrap_or_else(|| { panic!("Texture id {} is a invalid texture unit", texture.id()); }); set_active_texture_unit(texture_unit); gl_texture.bind(); } shader_program.activate(); draw_mesh(&mesh); } } #[derive(Debug, Default, Component)] struct GlObjects { shader_programs: HashMap, textures: HashMap, } fn set_viewport(position: Vec2, size: Dimens) { crate::opengl::set_viewport(position, size); } #[cfg(feature = "debug")] fn initialize_debug() { use crate::opengl::debug::{ enable_debug_output, set_debug_message_callback, set_debug_message_control, MessageIdsAction, }; enable_debug_output(); set_debug_message_callback(opengl_debug_message_cb); set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable); } fn draw_mesh(mesh: &Mesh) { // TODO: Creating a new vertex buffer each draw is really dumb and slow this // should be rethinked let renderable = Renderable::new(mesh.vertices(), mesh.indices()); renderable.vertex_arr.bind(); if let Some(index_info) = &renderable.index_info { VertexArray::draw_elements(PrimitiveKind::Triangles, 0, index_info.cnt); } else { VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, 3); } } fn create_gl_texture(texture: &Texture) -> GlTexture { let mut gl_texture = GlTexture::new(); gl_texture.generate( *texture.dimensions(), texture.image().as_bytes(), texture.pixel_data_format(), ); gl_texture.apply_properties(texture.properties()); gl_texture } fn create_gl_shader_program( shader_program: &ShaderProgram, ) -> Result { let gl_shaders = shader_program .shaders() .iter() .map(|shader| { let gl_shader = GlShader::new(shader.kind()); gl_shader.set_source(shader.source())?; gl_shader.compile()?; Ok(gl_shader) }) .collect::, _>>()?; let gl_shader_program = GlShaderProgram::new(); for gl_shader in &gl_shaders { gl_shader_program.attach(gl_shader); } gl_shader_program.link()?; Ok(gl_shader_program) } #[derive(Debug)] struct Renderable { vertex_arr: VertexArray, /// Vertex and index buffer has to live as long as the vertex array _vertex_buffer: Buffer, index_info: Option, } impl Renderable { fn new(vertices: &[Vertex], indices: Option<&[u32]>) -> Self { let mut vertex_arr = VertexArray::new(); let mut vertex_buffer = Buffer::new(); let mut index_info = None; vertex_buffer.store(vertices, BufferUsage::Static); vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0); let mut offset = 0u32; for attrib in Vertex::attrs() { vertex_arr.enable_attrib(attrib.index); vertex_arr.set_attrib_format( attrib.index, match attrib.component_type { AttributeComponentType::Float => VertexArrayDataType::Float, }, false, offset, ); vertex_arr.set_attrib_vertex_buf_binding(attrib.index, 0); offset += attrib.component_size * attrib.component_cnt as u32; } if let Some(indices) = indices { let mut index_buffer = Buffer::new(); index_buffer.store(indices, BufferUsage::Static); vertex_arr.bind_element_buffer(&index_buffer); index_info = Some(IndexInfo { _buffer: index_buffer, cnt: indices.len().try_into().unwrap(), }); } Self { vertex_arr, _vertex_buffer: vertex_buffer, index_info, } } } fn apply_transformation_matrices( transform: &Transform, gl_shader_program: &mut GlShaderProgram, camera: &Camera, window_size: Dimens, ) { gl_shader_program.set_uniform_matrix_4fv(cstr!("model"), &transform.as_matrix()); let view = create_view(camera); gl_shader_program.set_uniform_matrix_4fv(cstr!("view"), &view); #[allow(clippy::cast_precision_loss)] let projection = match &camera.projection { Projection::Perspective(perspective) => new_perspective_matrix( perspective, window_size.width as f32 / window_size.height as f32, ), }; gl_shader_program.set_uniform_matrix_4fv(cstr!("projection"), &projection); } fn apply_light( material: &Material, material_flags: &MaterialFlags, global_light: &GlobalLight, gl_shader_program: &mut GlShaderProgram, light_source: Option<(&LightSource, &Transform)>, camera: &Camera, ) { gl_shader_program.set_uniform_vec_3fv( cstr!("light.position"), &light_source.map_or_else(Vec3::default, |(_, light_source_transform)| { light_source_transform.position }), ); gl_shader_program.set_uniform_vec_3fv( cstr!("light.ambient"), // There cannot be both a material ambient color and a global ambient color &if material_flags.use_ambient_color { Color::WHITE_F32.into() } else { global_light.ambient.clone().into() }, ); gl_shader_program.set_uniform_vec_3fv( cstr!("light.diffuse"), &light_source .map_or(Color::WHITE_F32, |(light_source, _)| { light_source.diffuse.clone() }) .into(), ); gl_shader_program.set_uniform_vec_3fv( cstr!("light.specular"), &light_source .map_or(Color::WHITE_F32, |(light_source, _)| { light_source.specular.clone() }) .into(), ); gl_shader_program.set_uniform_vec_3fv( cstr!("material.ambient"), // When using the material ambient color is disabled, the ambient color has to be // white and not black since the material's ambient color is multiplied // with the global ambient light in the shader &if material_flags.use_ambient_color { material.ambient.clone() } else { Color::WHITE_F32 } .into(), ); gl_shader_program .set_uniform_vec_3fv(cstr!("material.diffuse"), &material.diffuse.clone().into()); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform_vec_3fv( cstr!("material.specular"), &material.specular.clone().into(), ); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform_1i( cstr!("material.ambient_map"), material.ambient_map.into_inner() as i32, ); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform_1i( cstr!("material.diffuse_map"), material.diffuse_map.into_inner() as i32, ); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform_1i( cstr!("material.specular_map"), material.specular_map.into_inner() as i32, ); gl_shader_program.set_uniform_1fv(cstr!("material.shininess"), material.shininess); gl_shader_program.set_uniform_vec_3fv(cstr!("view_pos"), &camera.position); } fn create_view(camera: &Camera) -> Matrix { let mut view = Matrix::new(); view.look_at(&camera.position, &camera.target, &camera.global_up); view } #[cfg(feature = "debug")] #[tracing::instrument(skip_all)] fn opengl_debug_message_cb( source: MessageSource, ty: MessageType, id: u32, severity: MessageSeverity, message: &str, ) { use std::backtrace::{Backtrace, BacktraceStatus}; use tracing::{event, Level}; macro_rules! create_event { ($level: expr) => { event!($level, ?source, ?ty, id, ?severity, message); }; } if matches!(severity, MessageSeverity::Notification) { return; } match ty { MessageType::Error => { create_event!(Level::ERROR); let backtrace = Backtrace::capture(); if matches!(backtrace.status(), BacktraceStatus::Captured) { event!(Level::TRACE, "{backtrace}"); } } MessageType::Other => { create_event!(Level::INFO); } _ => { create_event!(Level::WARN); } }; } #[derive(Debug)] struct IndexInfo { _buffer: Buffer, cnt: u32, }