diff options
Diffstat (limited to 'engine/src/renderer/opengl.rs')
-rw-r--r-- | engine/src/renderer/opengl.rs | 531 |
1 files changed, 531 insertions, 0 deletions
diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs new file mode 100644 index 0000000..c97d8dd --- /dev/null +++ b/engine/src/renderer/opengl.rs @@ -0,0 +1,531 @@ +//! OpenGL renderer. + +use std::collections::HashMap; +use std::ffi::{c_void, CString}; +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::data_types::dimens::Dimens; +use crate::event::{Present as PresentEvent, Start as StartEvent}; +use crate::lighting::{GlobalLight, PointLight}; +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; +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>) +{ + 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<MaterialFlags>, + Transform, + )>, + point_light_query: Query<(PointLight, Transform)>, + camera_query: Query<(Camera,)>, + window: Single<Window>, + global_light: Single<GlobalLight>, + mut gl_objects: Local<GlObjects>, +) +{ + 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 transform + // can be in both the object query and the light source query + let point_lights = point_light_query + .iter() + .map(|(point_light, transform)| ((*point_light).clone(), (*transform).clone())) + .collect::<Vec<_>>(); + + 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, + point_lights.as_slice(), + &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<u64, GlShaderProgram>, + textures: HashMap<TextureId, GlTexture>, +} + +fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) +{ + 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<GlShaderProgram, GlShaderError> +{ + 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::<Result<Vec<_>, _>>()?; + + 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<Vertex>, + index_info: Option<IndexInfo>, +} + +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<u32>, +) +{ + 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, + point_lights: &[(PointLight, Transform)], + camera: &Camera, +) +{ + debug_assert!( + point_lights.len() < 64, + "Shader cannot handle more than 64 point lights" + ); + + for (point_light_index, (point_light, point_light_transform)) in + point_lights.iter().enumerate() + { + set_point_light_uniforms( + gl_shader_program, + point_light_index, + point_light, + point_light_transform, + ) + } + + gl_shader_program.set_uniform_1i(cstr!("point_light_cnt"), point_lights.len() as i32); + + gl_shader_program.set_uniform_vec_3fv( + cstr!("material.ambient"), + &if material_flags.use_ambient_color { + material.ambient.clone() + } else { + global_light.ambient.clone().into() + } + .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 set_point_light_uniforms( + gl_shader_program: &mut GlShaderProgram, + point_light_index: usize, + point_light: &PointLight, + point_light_transform: &Transform, +) +{ + gl_shader_program.set_uniform_vec_3fv( + &create_point_light_uniform_name(point_light_index, "position"), + &point_light_transform.position, + ); + + gl_shader_program.set_uniform_vec_3fv( + &create_point_light_uniform_name(point_light_index, "diffuse"), + &point_light.diffuse.clone().into(), + ); + + gl_shader_program.set_uniform_vec_3fv( + &create_point_light_uniform_name(point_light_index, "specular"), + &point_light.specular.clone().into(), + ); + + gl_shader_program.set_uniform_1fv( + &create_point_light_uniform_name(point_light_index, "constant"), + point_light.attenuation_params.constant, + ); + + gl_shader_program.set_uniform_1fv( + &create_point_light_uniform_name(point_light_index, "linear"), + point_light.attenuation_params.linear, + ); + + gl_shader_program.set_uniform_1fv( + &create_point_light_uniform_name(point_light_index, "quadratic"), + point_light.attenuation_params.quadratic, + ); +} + +fn create_point_light_uniform_name( + point_light_index: usize, + point_light_field: &str, +) -> CString +{ + unsafe { + CString::from_vec_with_nul_unchecked( + format!("point_lights[{point_light_index}].{point_light_field}\0").into(), + ) + } +} + +fn create_view(camera: &Camera) -> Matrix<f32, 4, 4> +{ + 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<u32>, + cnt: u32, +} |