//! OpenGL renderer. use std::any::type_name; use std::collections::HashMap; use std::ffi::CString; use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use std::path::Path; use ecs::actions::Actions; use ecs::entity::obtainer::Obtainer as EntityObtainer; use ecs::event::component::{Changed, Removed}; use ecs::pair::{ChildOf, Pair, Wildcard}; use ecs::phase::Phase; use ecs::query::term::Without; use ecs::sole::Single; use ecs::system::observer::Observe; use ecs::{Component, Query, declare_entity}; use glutin::display::GetGlDisplay; use glutin::prelude::GlDisplay; use glutin::surface::{ GlSurface, Surface as GlutinSurface, WindowSurface as GlutinWindowSurface, }; use opengl_bindings::debug::{ MessageIdsAction, MessageSeverity, MessageSource, MessageType, SetDebugMessageControlError as GlSetDebugMessageControlError, set_debug_message_callback, set_debug_message_control, }; use opengl_bindings::misc::{ BufferClearMask as GlBufferClearMask, Capability, SetViewportError as GlSetViewportError, clear_buffers, enable, set_enabled, }; use opengl_bindings::shader::{ Error as GlShaderError, Kind as ShaderKind, Program as GlShaderProgram, Shader as GlShader, }; use opengl_bindings::texture::{ Filtering as GlTextureFiltering, GenerateError as GlTextureGenerateError, PixelDataFormat as GlTexturePixelDataFormat, Texture as GlTexture, Wrapping as GlTextureWrapping, }; use opengl_bindings::vertex_array::{ DrawError as GlDrawError, PrimitiveKind, VertexArray, }; use opengl_bindings::{ContextWithFns, CurrentContextWithFns}; use safer_ffi::layout::ReprC; use crate::asset::{Assets, Id as AssetId}; use crate::camera::Camera; use crate::color::Color; use crate::data_types::dimens::Dimens; use crate::image::{ColorType as ImageColorType, Image}; use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; use crate::material::{Flags as MaterialFlags, Material}; use crate::matrix::Matrix; use crate::model::Model; use crate::opengl::glsl::{ PreprocessingError as GlslPreprocessingError, preprocess as glsl_preprocess, }; use crate::projection::{ClipVolume, Projection}; use crate::renderer::object::{ Id as RendererObjectId, Kind as RendererObjectKind, Object as RendererObject, Store as RendererObjectStore, }; use crate::renderer::opengl::glutin_compat::{ DisplayBuilder, Error as GlutinCompatError, }; use crate::renderer::opengl::graphics_mesh::GraphicsMesh; use crate::renderer::{ BufferClearMask, Command as RendererCommand, CommandQueue as RendererCommandQueue, CtxUsedByWindow as RendererCtxUsedByWindow, GraphicsProperties, RENDER_PHASE, SurfaceId, SurfaceSpec, WindowUsingRendererCtx, }; use crate::texture::{ Filtering as TextureFiltering, Properties as TextureProperties, Wrapping as TextureWrapping, }; use crate::transform::WorldPosition; use crate::vector::{Vec2, Vec3}; use crate::windowing::Context as WindowingContext; use crate::windowing::window::{ Closed as WindowClosed, CreationAttributes as WindowCreationAttributes, CreationReady, Window, }; mod glutin_compat; mod graphics_mesh; mod vertex; const AMBIENT_MAP_TEXTURE_UNIT: u32 = 0; const DIFFUSE_MAP_TEXTURE_UNIT: u32 = 1; const SPECULAR_MAP_TEXTURE_UNIT: u32 = 2; const DEFAULT_TEXTURE_OBJECT_ID: RendererObjectId = RendererObjectId::Other(0xaa); declare_entity!( pub POST_RENDER_PHASE, (Phase, Pair::builder().relation::().target_id(*RENDER_PHASE).build()) ); #[derive(Debug, Component)] struct WindowGlConfig { gl_config: glutin::config::Config, } #[derive(Component)] struct GraphicsContext { gl_context: ContextWithFns, shader_program: Option, graphics_mesh_store: GraphicsMeshStore, surfaces: HashMap>, } #[derive(Debug, Default)] struct GraphicsMeshStore { graphics_meshes: HashMap, } #[derive(Debug, Default)] #[non_exhaustive] pub struct Extension {} impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ecs::extension::Collector<'_>) { collector.add_declared_entity(&RENDER_PHASE); collector.add_declared_entity(&POST_RENDER_PHASE); collector.add_system(*RENDER_PHASE, super::enqueue_commands); collector.add_system(*RENDER_PHASE, handle_commands); collector.add_system(*POST_RENDER_PHASE, prepare_windows); collector.add_system(*POST_RENDER_PHASE, init_window_graphics); collector.add_observer(handle_model_removed); collector.add_observer(handle_window_changed); collector.add_observer(handle_window_removed); } } #[tracing::instrument(skip_all)] fn handle_model_removed( observe: Observe>, renderer_ctx_query: Query<( &mut GraphicsContext, Pair, )>, assets: Single, ) { for evt_match in &observe { let model_ent_id = evt_match.id(); for (renderer_ctx_ent_id, (mut graphics_ctx, renderer_ctx_used_by_window)) in renderer_ctx_query.iter_with_euids() { let GraphicsContext { ref gl_context, ref mut graphics_mesh_store, ref surfaces, .. } = *graphics_ctx; let model = evt_match.get_removed_comp(); let Some(model_spec) = assets.get(&model.spec_asset) else { continue; }; let Some(mesh_asset) = &model_spec.mesh_asset else { continue; }; if !graphics_mesh_store .graphics_meshes .contains_key(&mesh_asset.id()) { continue; } let Some(window_ent) = renderer_ctx_used_by_window.get_target_ent() else { tracing::error!( window_entity_id = %renderer_ctx_used_by_window.id().target_entity(), "Window entity does not exist" ); continue; }; let Some(surface_spec) = window_ent.get::() else { tracing::error!( window_entity_id = %window_ent.uid(), "Window entity does not have a {} component", type_name::() ); continue; }; let Some(surface) = surfaces.get(&surface_spec.id) else { tracing::error!( window_entity_id = %window_ent.uid(), "Surface specified by window entity's {} component does not exist", type_name::() ); continue; }; let curr_gl_ctx = match gl_context.make_current(surface) { Ok(curr_gl_ctx) => curr_gl_ctx, Err(err) => { tracing::error!("{err}"); continue; } }; tracing::debug!( model_entity_id=%model_ent_id, renderer_ctx_entity_id=%renderer_ctx_ent_id, "Cleaning up after model in renderer context" ); let Some(mut graphics_mesh) = graphics_mesh_store.graphics_meshes.remove(&mesh_asset.id()) else { tracing::warn!( model_entity_id=%model_ent_id, "No mesh exists for model" ); continue; }; graphics_mesh.destroy(&curr_gl_ctx); } } } #[tracing::instrument(skip_all)] fn handle_window_changed( observe: Observe>, entity_obtainer: EntityObtainer, ) { for evt_match in &observe { let window_ent = evt_match.get_ent_infallible(); tracing::trace!( new_state = ?evt_match.get_changed_comp(), "Handling window change" ); let Some(surface_spec) = window_ent.get::() else { continue; }; let Some(renderer_ctx_ent_id) = window_ent .get_matching_components( Pair::builder() .relation::() .target_id(Wildcard::uid()) .build() .id(), ) .next() .map(|comp_ref| comp_ref.id().target_entity()) else { continue; }; let Some(renderer_ctx_ent) = entity_obtainer.get_entity(renderer_ctx_ent_id) else { tracing::error!("Renderer context entity does not exist"); continue; }; let Some(graphics_context) = renderer_ctx_ent.get::() else { tracing::error!( "Renderer context entity does not have a GraphicsContext component" ); continue; }; let Some(surface) = graphics_context.surfaces.get(&surface_spec.id) else { tracing::error!( window_entity_id = %window_ent.uid(), "Surface specified by window entity's {} component does not exist", type_name::() ); continue; }; let Ok(current_graphics_context) = graphics_context.gl_context.make_current(surface) else { tracing::error!("Failed to make graphics context current"); continue; }; if let Err(err) = set_viewport( ¤t_graphics_context, Vec2::default(), evt_match.get_changed_comp().inner_size(), ) { tracing::error!("Failed to set viewport: {err}"); } } } #[tracing::instrument(skip_all)] fn handle_window_removed(observe: Observe>, mut actions: Actions) { for evt_match in &observe { let window_ent_id = evt_match.id(); let window_ent = evt_match.get_ent_infallible(); tracing::debug!( entity_id = %window_ent_id, title = %evt_match.get_removed_comp().title, "Handling removal of window" ); actions.remove_comps::<(SurfaceSpec, WindowGlConfig)>(window_ent_id); let Some(with_renderer_ctx_pair) = window_ent .get_first_wildcard_pair_match::() else { tracing::warn!( "Window entity is missing a ({}, *) pair", type_name::() ); continue; }; let renderer_context_ent_id = with_renderer_ctx_pair.id().target_entity(); actions.remove_comps::<(GraphicsContext, RendererObjectStore)>( renderer_context_ent_id, ); actions.remove_components( renderer_context_ent_id, [Pair::builder() .relation::() .target_id(window_ent_id) .build() .id()], ); actions.remove_components(window_ent_id, [with_renderer_ctx_pair.id()]); } } #[derive(Debug, Component)] struct SetupFailed; fn prepare_windows( window_query: Query< ( Option<&Window>, &mut WindowCreationAttributes, Option<&GraphicsProperties>, ), ( Without, Without, Without, Without, ), >, windowing_context: Single, mut actions: Actions, ) { let Some(display_handle) = windowing_context.display_handle() else { return; }; for (window_ent_id, (window, mut window_creation_attrs, graphics_props)) in window_query.iter_with_euids() { tracing::debug!("Preparing window entity {window_ent_id} for use in rendering"); let mut glutin_config_template_builder = glutin::config::ConfigTemplateBuilder::new(); let graphics_props = match graphics_props.as_ref() { Some(graphics_props) => &*graphics_props, None => { actions.add_components(window_ent_id, (GraphicsProperties::default(),)); &GraphicsProperties::default() } }; if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt { glutin_config_template_builder = glutin_config_template_builder .with_multisampling(multisampling_sample_cnt); } let window_handle = match window .as_ref() .map(|window| unsafe { windowing_context.get_window_as_handle(&window.wid()) }) .flatten() .transpose() { Ok(window_handle) => window_handle, Err(err) => { tracing::error!("Failed to get window handle: {err}"); actions.add_components(window_ent_id, (SetupFailed,)); continue; } }; let (new_window_creation_attrs, gl_config) = match DisplayBuilder::new() .with_window_attributes(window_creation_attrs.clone()) .build( window_handle, &display_handle, glutin_config_template_builder, |mut cfgs| cfgs.next(), ) { Ok((new_window_creation_attrs, gl_config)) => { (new_window_creation_attrs, gl_config) } Err(GlutinCompatError::WindowRequired) => { actions.add_components(window_ent_id, (CreationReady,)); continue; } Err(err) => { tracing::error!("Failed to create platform graphics display: {err}"); actions.add_components(window_ent_id, (SetupFailed,)); continue; } }; *window_creation_attrs = new_window_creation_attrs; actions.add_components(window_ent_id, (WindowGlConfig { gl_config },)); if window.is_none() { actions.add_components(window_ent_id, (CreationReady,)); } } } #[tracing::instrument(skip_all)] fn init_window_graphics( window_query: Query< (&Window, &WindowGlConfig, &GraphicsProperties), (Without, Without), >, mut actions: Actions, windowing_context: Single, ) { for (window_ent_id, (window, window_gl_config, graphics_props)) in window_query.iter_with_euids() { tracing::info!("Initializing graphics for window {window_ent_id}"); let display = window_gl_config.gl_config.display(); let window_handle = match unsafe { windowing_context.get_window_as_handle(&window.wid()) } .transpose() { Ok(Some(window_handle)) => window_handle, Ok(None) => { tracing::error!( wid = ?window.wid(), entity_id = %window_ent_id, "Windowing context does not contain window" ); actions.add_components(window_ent_id, (SetupFailed,)); continue; } Err(err) => { tracing::error!("Failed to get window handle: {err}"); actions.add_components(window_ent_id, (SetupFailed,)); continue; } }; let Some(window_inner_size) = window.inner_size().try_into_nonzero() else { tracing::error!( "Cannot create a surface for a window with a width/height of 0", ); continue; }; let surface = match unsafe { display.create_window_surface( &window_gl_config.gl_config, &glutin::surface::SurfaceAttributesBuilder::< glutin::surface::WindowSurface, >::new() .build( window_handle.as_raw(), window_inner_size.width, window_inner_size.height, ), ) } { Ok(surface) => surface, Err(err) => { tracing::error!("Failed to create window surface: {err}"); continue; } }; let context = match unsafe { display.create_context( &window_gl_config.gl_config, &glutin::context::ContextAttributesBuilder::new() .with_debug(graphics_props.debug) .build(Some(window_handle.as_raw())), ) } { Ok(context) => context, Err(err) => { tracing::error!("Failed to create graphics context: {err}"); continue; } }; let gl_context = match ContextWithFns::new(context, &surface) { Ok(context) => context, Err(err) => { tracing::error!("Failed to create graphics context: {err}"); continue; } }; let Ok(curr_gl_context) = gl_context.make_current(&surface) else { tracing::error!("Failed to make graphics context current"); continue; }; if let Err(err) = set_viewport(&curr_gl_context, Vec2 { x: 0, y: 0 }, window.inner_size()) { tracing::error!("Failed to set viewport: {err}"); } set_enabled( &curr_gl_context, Capability::DepthTest, graphics_props.depth_test, ); set_enabled( &curr_gl_context, Capability::MultiSample, graphics_props.multisampling_sample_cnt.is_some(), ); if graphics_props.debug { enable(&curr_gl_context, Capability::DebugOutput); enable(&curr_gl_context, Capability::DebugOutputSynchronous); set_debug_message_callback(&curr_gl_context, opengl_debug_message_cb); match set_debug_message_control( &curr_gl_context, None, None, None, &[], MessageIdsAction::Disable, ) { Ok(()) => {} Err(GlSetDebugMessageControlError::TooManyIds { id_cnt: _, max_id_cnt: _, }) => { unreachable!() // No ids are given } } } let surface_id = SurfaceId::new_unique(); let renderer_ctx_ent_id = actions.spawn(( GraphicsContext { gl_context, shader_program: None, graphics_mesh_store: GraphicsMeshStore::default(), surfaces: HashMap::from([(surface_id, surface)]), }, RendererObjectStore::default(), RendererCommandQueue::default(), Pair::builder() .relation::() .target_id(window_ent_id) .build(), )); actions.add_components( window_ent_id, ( SurfaceSpec { id: surface_id }, Pair::builder() .relation::() .target_id(renderer_ctx_ent_id) .build(), ), ); } } #[tracing::instrument(skip_all)] fn handle_commands( renderer_ctx_query: Query<( &mut GraphicsContext, &mut RendererObjectStore, &mut RendererCommandQueue, )>, assets: Single, ) { for (mut graphics_ctx, mut renderer_object_store, mut command_queue) in &renderer_ctx_query { let GraphicsContext { ref gl_context, ref mut shader_program, ref mut graphics_mesh_store, ref surfaces, } = *graphics_ctx; let mut opt_curr_gl_ctx: Option = None; let mut opt_curr_camera: Option<(Camera, WorldPosition)> = None; for command in command_queue.drain() { let tracing_span = tracing::info_span!("handle_cmd", command = ?command); let _tracing_span_enter = tracing_span.enter(); match command { RendererCommand::MakeCurrent(surface_id) => { let Some(surface) = surfaces.get(&surface_id) else { tracing::error!(surface_id=?surface_id, "Surface does not exist"); continue; }; let curr_gl_ctx = match gl_context.make_current(surface) { Ok(current_graphics_context) => current_graphics_context, Err(err) => { tracing::error!( "Failed to make graphics context current: {err}" ); continue; } }; opt_curr_gl_ctx = Some(curr_gl_ctx); } RendererCommand::ClearBuffers(buffer_clear_mask) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let mut clear_mask = GlBufferClearMask::empty(); clear_mask.set( GlBufferClearMask::COLOR, buffer_clear_mask.contains(BufferClearMask::COLOR), ); clear_mask.set( GlBufferClearMask::DEPTH, buffer_clear_mask.contains(BufferClearMask::DEPTH), ); clear_mask.set( GlBufferClearMask::STENCIL, buffer_clear_mask.contains(BufferClearMask::STENCIL), ); clear_buffers(&curr_gl_ctx, clear_mask); } RendererCommand::SwapBuffers(surface_id) => { let Some(surface) = surfaces.get(&surface_id) else { tracing::error!(surface_id=?surface_id, "Surface does not exist"); continue; }; if let Err(err) = surface.swap_buffers(gl_context.context()) { tracing::error!("Failed to swap buffers: {err}"); } } RendererCommand::UseShader => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let _shader_program = shader_program.get_or_insert_with(|| { create_default_shader_program(&curr_gl_ctx).unwrap() }); } RendererCommand::ActivateShader => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let Some(shader_program) = shader_program else { tracing::error!("Shader does not exist"); continue; }; shader_program.activate(&curr_gl_ctx); } RendererCommand::UseCamera(camera, camera_world_pos) => { opt_curr_camera = Some((camera, camera_world_pos)); } RendererCommand::ApplyTransform { transform, window_size } => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let Some(shader_program) = shader_program else { tracing::error!("Shader does not exist"); continue; }; let Some((camera, camera_world_pos)) = &opt_curr_camera else { tracing::error!("No current camera"); continue; }; apply_transformation_matrices( &curr_gl_ctx, Transformation { position: transform.position, scale: transform.scale, }, shader_program, &camera, &camera_world_pos, &window_size, ); } RendererCommand::SetShaderDirectionalLights(directional_lights) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let Some(shader_program) = shader_program else { tracing::error!("Shader does not exist"); continue; }; set_shader_directional_lights( curr_gl_ctx, shader_program, &directional_lights, ); } RendererCommand::SetShaderPointLights(point_lights) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let Some(shader_program) = shader_program else { tracing::error!("Shader does not exist"); continue; }; set_shader_point_lights(curr_gl_ctx, shader_program, &point_lights); } RendererCommand::CreateTexture(texture) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let Some(texture_image) = assets.get(&texture.asset_handle) else { continue; }; if let Err(err) = create_texture_object( curr_gl_ctx, &mut renderer_object_store, texture.asset_handle.id(), texture_image, &texture.properties, ) { tracing::error!("Failed to create texture object: {err}"); } } RendererCommand::UseMaterial { material_asset, material_flags, global_light, } => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let Some(shader_program) = shader_program else { tracing::error!("Shader does not exist"); continue; }; let Some((_, camera_world_pos)) = &opt_curr_camera else { tracing::error!("No current camera"); continue; }; let material = match material_asset.as_ref() { Some(material_asset) => { let Some(material) = assets.get(&material_asset) else { continue; }; material } None => &Material::default(), }; set_shader_material( curr_gl_ctx, material, &material_flags, &global_light, shader_program, camera_world_pos, ); bind_material_textures( curr_gl_ctx, material, &mut renderer_object_store, ); } RendererCommand::DrawMesh { mesh_asset } => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; let graphics_mesh = match graphics_mesh_store.graphics_meshes.get(&mesh_asset.id()) { Some(graphics_mesh) => graphics_mesh, None => { let Some(mesh) = assets.get(&mesh_asset) else { tracing::trace!("Missing model asset"); continue; }; let graphics_mesh = match GraphicsMesh::new(&curr_gl_ctx, mesh) { Ok(graphics_mesh) => graphics_mesh, Err(err) => { tracing::error!( "Failed to create {}: {err}", type_name::() ); continue; } }; graphics_mesh_store .graphics_meshes .entry(mesh_asset.id()) .or_insert(graphics_mesh) } }; if let Err(err) = draw_mesh(&curr_gl_ctx, graphics_mesh) { tracing::error!("Failed to draw mesh: {err}"); }; } RendererCommand::SetPolygonModeConfig(polygon_mode_config) => { let Some(curr_gl_ctx) = &opt_curr_gl_ctx else { tracing::error!("No GL context is current"); continue; }; opengl_bindings::misc::set_polygon_mode( &curr_gl_ctx, polygon_mode_config.face, polygon_mode_config.mode, ); } } } } } fn create_default_texture(current_context: &CurrentContextWithFns<'_>) -> GlTexture { match create_gl_texture( current_context, &Image::from_color(Dimens { width: 1, height: 1 }, Color::WHITE_U8), &TextureProperties::default(), ) { Ok(gl_texture) => gl_texture, Err( GlTextureGenerateError::SizeWidthValueTooLarge { value: _, max_value: _ } | GlTextureGenerateError::SizeHeightValueTooLarge { value: _, max_value: _ }, ) => unreachable!(), } } #[tracing::instrument(skip_all)] fn create_texture_object( curr_gl_ctx: &CurrentContextWithFns<'_>, renderer_object_store: &mut RendererObjectStore, texture_image_asset_id: AssetId, image: &Image, texture_properties: &TextureProperties, ) -> Result<(), GlTextureGenerateError> { let object_id = RendererObjectId::Asset(texture_image_asset_id); if renderer_object_store.contains_with_id(&object_id) { tracing::error!( texture_object_id=?object_id, "Renderer object store already contains object with this ID" ); return Ok(()); } renderer_object_store.insert( object_id, RendererObject::from_raw( create_gl_texture(curr_gl_ctx, image, texture_properties)?.into_raw(), RendererObjectKind::Texture, ), ); Ok(()) } fn bind_material_textures( current_context: &CurrentContextWithFns<'_>, material: &Material, renderer_object_store: &mut RendererObjectStore, ) { let material_texture_maps = [ (&material.ambient_map, AMBIENT_MAP_TEXTURE_UNIT), (&material.diffuse_map, DIFFUSE_MAP_TEXTURE_UNIT), (&material.specular_map, SPECULAR_MAP_TEXTURE_UNIT), ]; for (texture, texture_unit) in material_texture_maps { let Some(texture) = texture else { let default_texture_obj = renderer_object_store .entry(DEFAULT_TEXTURE_OBJECT_ID) .or_insert_with(|| { RendererObject::from_raw( create_default_texture(current_context).into_raw(), RendererObjectKind::Texture, ) }); let gl_texture = GlTexture::from_raw(default_texture_obj.as_raw()); gl_texture.bind_to_texture_unit(current_context, texture_unit); continue; }; let texture_object_id = RendererObjectId::Asset(texture.asset_handle.id()); let Some(texture_obj) = renderer_object_store.get_texture_obj(&texture_object_id) else { tracing::error!( texture_object_id=?texture_object_id, "Texture object does not exist" ); continue; }; let gl_texture = GlTexture::from_raw(texture_obj.as_raw()); gl_texture.bind_to_texture_unit(current_context, texture_unit); } } fn set_viewport( current_context: &CurrentContextWithFns<'_>, position: Vec2, size: &Dimens, ) -> Result<(), GlSetViewportError> { let position = opengl_bindings::data_types::Vec2:: { x: position.x, y: position.y }; let size = opengl_bindings::data_types::Dimens:: { width: size.width, height: size.height, }; opengl_bindings::misc::set_viewport(current_context, &position, &size) } fn draw_mesh( current_context: &CurrentContextWithFns<'_>, graphics_mesh: &GraphicsMesh, ) -> Result<(), GlDrawError> { graphics_mesh.vertex_arr.bind(current_context); if graphics_mesh.index_buffer.is_some() { VertexArray::draw_elements( current_context, PrimitiveKind::Triangles, 0, graphics_mesh.element_cnt, )?; } else { VertexArray::draw_arrays( current_context, PrimitiveKind::Triangles, 0, graphics_mesh.element_cnt, )?; } Ok(()) } fn create_gl_texture( current_context: &CurrentContextWithFns<'_>, image: &Image, texture_properties: &TextureProperties, ) -> Result { let gl_texture = GlTexture::new(current_context); gl_texture.generate( current_context, &image.dimensions().into(), image.as_bytes(), match image.color_type() { ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8, ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8, _ => { unimplemented!(); } }, )?; gl_texture.set_wrap( current_context, texture_wrapping_to_gl(texture_properties.wrap), ); gl_texture.set_magnifying_filter( current_context, texture_filtering_to_gl(texture_properties.magnifying_filter), ); gl_texture.set_minifying_filter( current_context, texture_filtering_to_gl(texture_properties.minifying_filter), ); Ok(gl_texture) } const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl"); const FRAGMENT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/fragment.glsl"); const VERTEX_DATA_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex_data.glsl"); const LIGHT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/light.glsl"); fn create_default_shader_program( current_context: &CurrentContextWithFns<'_>, ) -> Result { let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex); vertex_shader.set_source( current_context, &*glsl_preprocess(VERTEX_GLSL_SHADER_SRC, &get_glsl_shader_content)?, )?; vertex_shader.compile(current_context)?; let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment); fragment_shader.set_source( current_context, &*glsl_preprocess(FRAGMENT_GLSL_SHADER_SRC, &get_glsl_shader_content)?, )?; fragment_shader.compile(current_context)?; let gl_shader_program = GlShaderProgram::new(current_context); gl_shader_program.attach(current_context, &vertex_shader); gl_shader_program.attach(current_context, &fragment_shader); gl_shader_program.link(current_context)?; Ok(gl_shader_program) } #[derive(Debug, thiserror::Error)] enum CreateShaderError { #[error(transparent)] ShaderError(#[from] GlShaderError), #[error(transparent)] PreprocessingError(#[from] GlslPreprocessingError), } fn get_glsl_shader_content(path: &Path) -> Result, std::io::Error> { if path == Path::new("vertex_data.glsl") { return Ok(VERTEX_DATA_GLSL_SHADER_SRC.as_bytes().to_vec()); } if path == Path::new("light.glsl") { return Ok(LIGHT_GLSL_SHADER_SRC.as_bytes().to_vec()); } Err(IoError::new( IoErrorKind::NotFound, format!("Content for shader file {} not found", path.display()), )) } fn apply_transformation_matrices( current_context: &CurrentContextWithFns<'_>, transformation: Transformation, gl_shader_program: &mut GlShaderProgram, camera: &Camera, camera_world_pos: &WorldPosition, window_size: &Dimens, ) { gl_shader_program.set_uniform( current_context, c"model", &opengl_bindings::data_types::Matrix { items: create_transformation_matrix(transformation).items, }, ); let view_matrix = create_view_matrix(camera, &camera_world_pos.position); gl_shader_program.set_uniform( current_context, c"view", &opengl_bindings::data_types::Matrix { items: view_matrix.items }, ); #[allow(clippy::cast_precision_loss)] let proj_matrix = match &camera.projection { Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( window_size.width as f32 / window_size.height as f32, ClipVolume::NegOneToOne, ), Projection::Orthographic(orthographic_proj) => orthographic_proj .to_matrix_rh(&camera_world_pos.position, ClipVolume::NegOneToOne), }; gl_shader_program.set_uniform( current_context, c"projection", &opengl_bindings::data_types::Matrix { items: proj_matrix.items }, ); } fn set_shader_directional_lights( curr_gl_ctx: &CurrentContextWithFns<'_>, gl_shader_program: &mut GlShaderProgram, directional_lights: &[DirectionalLight], ) { debug_assert!( directional_lights.len() < 64, "Shader cannot handle more than 64 directional lights" ); for (dir_light_index, dir_light) in directional_lights.iter().enumerate() { let direction: opengl_bindings::data_types::Vec3<_> = dir_light.direction.into(); gl_shader_program.set_uniform( curr_gl_ctx, &create_light_uniform_name( "directional_lights", dir_light_index, "direction", ), &direction, ); set_light_phong_uniforms( curr_gl_ctx, gl_shader_program, "directional_lights", dir_light_index, dir_light, ); } // There probably won't be more than 2147483648 directional lights #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] gl_shader_program.set_uniform( curr_gl_ctx, c"directional_light_cnt", &(directional_lights.len() as i32), ); } fn set_shader_point_lights( curr_gl_ctx: &CurrentContextWithFns<'_>, gl_shader_program: &mut GlShaderProgram, point_lights: &[(PointLight, WorldPosition)], ) { debug_assert!( point_lights.len() < 64, "Shader cannot handle more than 64 point lights" ); for (point_light_index, (point_light, point_light_world_pos)) in point_lights.iter().enumerate() { let pos: opengl_bindings::data_types::Vec3<_> = (point_light_world_pos.position + point_light.local_position).into(); gl_shader_program.set_uniform( curr_gl_ctx, &create_light_uniform_name("point_lights", point_light_index, "position"), &pos, ); set_light_phong_uniforms( curr_gl_ctx, gl_shader_program, "point_lights", point_light_index, &*point_light, ); set_light_attenuation_uniforms( curr_gl_ctx, gl_shader_program, "point_lights", point_light_index, &*point_light, ); } // There probably won't be more than 2147483648 point lights #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] gl_shader_program.set_uniform( curr_gl_ctx, c"point_light_cnt", &(point_lights.len() as i32), ); } fn set_shader_material( curr_gl_ctx: &CurrentContextWithFns<'_>, material: &Material, material_flags: &MaterialFlags, global_light: &GlobalLight, gl_shader_program: &mut GlShaderProgram, camera_world_pos: &WorldPosition, ) { let ambient: opengl_bindings::data_types::Vec3<_> = Vec3::from(if material_flags.use_ambient_color { material.ambient.clone() } else { global_light.ambient.clone() }) .into(); gl_shader_program.set_uniform(curr_gl_ctx, c"material.ambient", &ambient); let diffuse: opengl_bindings::data_types::Vec3<_> = Vec3::from(material.diffuse.clone()).into(); gl_shader_program.set_uniform(curr_gl_ctx, c"material.diffuse", &diffuse); let specular: opengl_bindings::data_types::Vec3<_> = Vec3::from(material.specular.clone()).into(); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform(curr_gl_ctx, c"material.specular", &specular); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform( curr_gl_ctx, c"material.ambient_map", &(AMBIENT_MAP_TEXTURE_UNIT as i32), ); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform( curr_gl_ctx, c"material.diffuse_map", &(DIFFUSE_MAP_TEXTURE_UNIT as i32), ); #[allow(clippy::cast_possible_wrap)] gl_shader_program.set_uniform( curr_gl_ctx, c"material.specular_map", &(SPECULAR_MAP_TEXTURE_UNIT as i32), ); gl_shader_program.set_uniform( curr_gl_ctx, c"material.shininess", &material.shininess, ); let view_pos: opengl_bindings::data_types::Vec3<_> = camera_world_pos.position.into(); gl_shader_program.set_uniform(curr_gl_ctx, c"view_pos", &view_pos); } fn set_light_attenuation_uniforms( current_context: &CurrentContextWithFns<'_>, gl_shader_program: &mut GlShaderProgram, light_array: &str, light_index: usize, light: &PointLight, ) { gl_shader_program.set_uniform( current_context, &create_light_uniform_name( light_array, light_index, "attenuation_props.constant", ), &light.attenuation_params.constant, ); gl_shader_program.set_uniform( current_context, &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), &light.attenuation_params.linear, ); gl_shader_program.set_uniform( current_context, &create_light_uniform_name( light_array, light_index, "attenuation_props.quadratic", ), &light.attenuation_params.quadratic, ); } fn set_light_phong_uniforms( current_context: &CurrentContextWithFns<'_>, gl_shader_program: &mut GlShaderProgram, light_array: &str, light_index: usize, light: &impl Light, ) { gl_shader_program.set_uniform( current_context, &create_light_uniform_name(light_array, light_index, "phong.diffuse"), &opengl_bindings::data_types::Vec3 { x: light.diffuse().red, y: light.diffuse().green, z: light.diffuse().blue, }, ); gl_shader_program.set_uniform( current_context, &create_light_uniform_name(light_array, light_index, "phong.specular"), &opengl_bindings::data_types::Vec3 { x: light.specular().red, y: light.specular().green, z: light.specular().blue, }, ); } trait Light { fn diffuse(&self) -> &Color; fn specular(&self) -> &Color; } impl Light for PointLight { fn diffuse(&self) -> &Color { &self.diffuse } fn specular(&self) -> &Color { &self.specular } } impl Light for DirectionalLight { fn diffuse(&self) -> &Color { &self.diffuse } fn specular(&self) -> &Color { &self.specular } } fn create_light_uniform_name( light_array: &str, light_index: usize, light_field: &str, ) -> CString { unsafe { CString::from_vec_with_nul_unchecked( format!("{light_array}[{light_index}].{light_field}\0").into(), ) } } fn create_view_matrix(camera: &Camera, camera_pos: &Vec3) -> Matrix { let mut view = Matrix::new(); view.look_at(&camera_pos, &camera.target, &camera.global_up); view } #[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::{Level, event}; 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) { tracing::error!("{backtrace}"); // event!(Level::TRACE, "{backtrace}"); } } MessageType::Other => { create_event!(Level::INFO); } _ => { create_event!(Level::WARN); } }; } #[derive(Debug)] struct Transformation { position: Vec3, scale: Vec3, } fn create_transformation_matrix(transformation: Transformation) -> Matrix { let mut matrix = Matrix::new_identity(); matrix.translate(&transformation.position); matrix.scale(&transformation.scale); matrix } #[inline] fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping { match texture_wrapping { TextureWrapping::Repeat => GlTextureWrapping::Repeat, TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat, TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge, TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder, } } #[inline] fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering { match texture_filtering { TextureFiltering::Linear => GlTextureFiltering::Linear, TextureFiltering::Nearest => GlTextureFiltering::Nearest, } } impl From> for opengl_bindings::data_types::Vec2 { fn from(vec2: Vec2) -> Self { Self { x: vec2.x, y: vec2.y } } } impl From> for opengl_bindings::data_types::Vec3 { fn from(vec3: Vec3) -> Self { Self { x: vec3.x, y: vec3.y, z: vec3.z } } } impl From> for opengl_bindings::data_types::Dimens { fn from(dimens: Dimens) -> Self { Self { width: dimens.width, height: dimens.height, } } } impl From for opengl_bindings::misc::PolygonMode { fn from(mode: crate::draw_flags::PolygonMode) -> Self { match mode { crate::draw_flags::PolygonMode::Point => Self::Point, crate::draw_flags::PolygonMode::Fill => Self::Fill, crate::draw_flags::PolygonMode::Line => Self::Line, } } } impl From for opengl_bindings::misc::PolygonModeFace { fn from(face: crate::draw_flags::PolygonModeFace) -> Self { match face { crate::draw_flags::PolygonModeFace::Front => Self::Front, crate::draw_flags::PolygonModeFace::Back => Self::Back, crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack, } } }