From 88d6ae3e4854c5fb4b37f75a29aba4f13dcfb382 Mon Sep 17 00:00:00 2001
From: HampusM <hampus@hampusmat.com>
Date: Mon, 19 Feb 2024 23:06:17 +0100
Subject: refactor(engine): create shaders on start

---
 engine/src/lib.rs           |   8 +++
 engine/src/object.rs        | 116 +++++++-------------------------
 engine/src/opengl/shader.rs |  11 +---
 engine/src/renderer/mod.rs  | 102 +++++++++++++++++++++++------
 engine/src/shader.rs        | 156 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 272 insertions(+), 121 deletions(-)
 create mode 100644 engine/src/shader.rs

(limited to 'engine')

diff --git a/engine/src/lib.rs b/engine/src/lib.rs
index fada83f..a1d9e2f 100644
--- a/engine/src/lib.rs
+++ b/engine/src/lib.rs
@@ -25,6 +25,7 @@ pub mod material;
 pub mod math;
 pub mod mesh;
 pub mod object;
+pub mod shader;
 pub mod texture;
 pub mod vertex;
 
@@ -140,6 +141,10 @@ where
     /// Will return `Err` if updating the window fails.
     pub fn start(&mut self, mut func: impl FnMut(&mut Self)) -> Result<(), Error>
     {
+        self.renderer
+            .create_shader_programs(self.objects.values().map(Object::shader))
+            .map_err(Error::ShaderCreationFailed)?;
+
         let mut prev_frame_start: Option<Instant> = None;
 
         while !self.window.should_close() {
@@ -288,6 +293,9 @@ pub enum Error
 
     #[error("Rendering failed")]
     RenderingFailed(#[source] renderer::Error),
+
+    #[error("Failed to create shaders")]
+    ShaderCreationFailed(#[source] renderer::Error),
 }
 
 #[derive(Debug)]
diff --git a/engine/src/object.rs b/engine/src/object.rs
index 5391cf5..0fd049c 100644
--- a/engine/src/object.rs
+++ b/engine/src/object.rs
@@ -1,27 +1,24 @@
 use std::collections::HashMap;
-use std::fs::read_to_string;
-use std::io::Error as IoError;
-use std::path::{Path, PathBuf};
+use std::path::Path;
 
 use crate::material::Material;
 use crate::mesh::Mesh;
-use crate::opengl::shader::{
+use crate::shader::{
     Error as ShaderError,
     Kind as ShaderKind,
     Program as ShaderProgram,
     Shader,
 };
-use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor};
 use crate::texture::{Id as TextureId, Texture};
 use crate::transform::Transform;
 use crate::vector::Vec3;
 use crate::vertex::Vertex;
 
-const SHADER_DIR: &str = "engine";
-
 const VERTEX_SHADER_FILE: &str = "vertex.glsl";
 const FRAGMENT_SHADER_FILE: &str = "fragment.glsl";
 
+const SHADER_DIR: &str = "engine";
+
 #[derive(Debug)]
 pub struct Object
 {
@@ -162,58 +159,23 @@ impl Builder
     /// - Linking the shader program fails
     pub fn build(self, id: Id) -> Result<Object, Error>
     {
-        let vertex_shader_file = Path::new(SHADER_DIR).join(VERTEX_SHADER_FILE);
-
-        let fragment_shader_file = Path::new(SHADER_DIR).join(FRAGMENT_SHADER_FILE);
-
-        let vertex_shader_content =
-            read_to_string(&vertex_shader_file).map_err(|err| {
-                Error::ReadShaderFailed {
-                    source: err,
-                    shader_file: vertex_shader_file.clone(),
-                }
-            })?;
-
-        let fragment_shader_content =
-            read_to_string(&fragment_shader_file).map_err(|err| {
-                Error::ReadShaderFailed {
-                    source: err,
-                    shader_file: fragment_shader_file.clone(),
-                }
-            })?;
-
-        let shader_preprocessor = ShaderPreprocessor::new(PathBuf::from(SHADER_DIR));
-
-        let vertex_shader_content_preprocessed = shader_preprocessor
-            .preprocess(vertex_shader_content, &vertex_shader_file)
-            .map_err(|err| Error::PreprocessShaderFailed {
-                source: err,
-                shader_file: vertex_shader_file,
-            })?;
-
-        let frag_shader_content_preprocessed = shader_preprocessor
-            .preprocess(fragment_shader_content, &fragment_shader_file)
-            .map_err(|err| Error::PreprocessShaderFailed {
-                source: err,
-                shader_file: fragment_shader_file,
-            })?;
-
-        let vertex_shader =
-            Self::create_shader(ShaderKind::Vertex, &vertex_shader_content_preprocessed)
-                .map_err(Error::CreateVertexShaderFailed)?;
-
-        let fragment_shader =
-            Self::create_shader(ShaderKind::Fragment, &frag_shader_content_preprocessed)
-                .map_err(Error::CreateFragmentShaderFailed)?;
-
-        let shader_program = ShaderProgram::new();
-
-        shader_program.attach(&vertex_shader);
-        shader_program.attach(&fragment_shader);
-
-        shader_program
-            .link()
-            .map_err(Error::LinkShaderProgramFailed)?;
+        let mut shader_program = ShaderProgram::new();
+
+        shader_program.push_shader(
+            Shader::read_shader_file(
+                ShaderKind::Vertex,
+                &Path::new(SHADER_DIR).join(VERTEX_SHADER_FILE),
+            )?
+            .preprocess()?,
+        );
+
+        shader_program.push_shader(
+            Shader::read_shader_file(
+                ShaderKind::Fragment,
+                &Path::new(SHADER_DIR).join(FRAGMENT_SHADER_FILE),
+            )?
+            .preprocess()?,
+        );
 
         let mesh = Mesh::new(self.vertices, self.indices);
 
@@ -228,16 +190,6 @@ impl Builder
             material: self.material.ok_or(Error::NoMaterial)?,
         })
     }
-
-    fn create_shader(kind: ShaderKind, source: &str) -> Result<Shader, ShaderError>
-    {
-        let shader = Shader::new(kind);
-
-        shader.set_source(source)?;
-        shader.compile()?;
-
-        Ok(shader)
-    }
 }
 
 impl Default for Builder
@@ -252,30 +204,8 @@ impl Default for Builder
 #[derive(Debug, thiserror::Error)]
 pub enum Error
 {
-    #[error("Failed to create vertex shader")]
-    CreateVertexShaderFailed(#[source] ShaderError),
-
-    #[error("Failed to create fragment shader")]
-    CreateFragmentShaderFailed(#[source] ShaderError),
-
-    #[error("Failed to link shader program")]
-    LinkShaderProgramFailed(#[source] ShaderError),
-
-    #[error("Failed to read shader {}", shader_file.display())]
-    ReadShaderFailed
-    {
-        #[source]
-        source: IoError,
-        shader_file: PathBuf,
-    },
-
-    #[error("Failed to preprocess shader {}", shader_file.display())]
-    PreprocessShaderFailed
-    {
-        #[source]
-        source: ShaderPreprocessorError,
-        shader_file: PathBuf,
-    },
+    #[error(transparent)]
+    ShaderError(#[from] ShaderError),
 
     #[error("Object builder has no material")]
     NoMaterial,
diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs
index 224a9bc..2312adc 100644
--- a/engine/src/opengl/shader.rs
+++ b/engine/src/opengl/shader.rs
@@ -3,6 +3,7 @@ use std::ptr::null_mut;
 
 use crate::matrix::Matrix;
 use crate::opengl::currently_bound::CurrentlyBound;
+use crate::shader::Kind;
 use crate::vector::Vec3;
 
 #[derive(Debug)]
@@ -90,14 +91,6 @@ impl Drop for Shader
     }
 }
 
-/// Shader kind.
-#[derive(Debug, Clone, Copy)]
-pub enum Kind
-{
-    Vertex,
-    Fragment,
-}
-
 impl Kind
 {
     fn into_gl(self) -> gl::types::GLenum
@@ -110,7 +103,7 @@ impl Kind
 }
 
 /// Shader program
-#[derive(Debug)]
+#[derive(Debug, PartialEq, Eq, Hash)]
 pub struct Program
 {
     program: gl::types::GLuint,
diff --git a/engine/src/renderer/mod.rs b/engine/src/renderer/mod.rs
index c72e1ce..64d5e3c 100644
--- a/engine/src/renderer/mod.rs
+++ b/engine/src/renderer/mod.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
 use std::ffi::{c_void, CString};
 use std::process::abort;
 
@@ -18,11 +19,16 @@ use crate::opengl::buffer::{
 use crate::opengl::currently_bound::CurrentlyBound;
 #[cfg(feature = "debug")]
 use crate::opengl::debug::{MessageSeverity, MessageSource, MessageType};
-use crate::opengl::shader::Program as ShaderProgram;
+use crate::opengl::shader::{
+    Error as GlShaderError,
+    Program as GlShaderProgram,
+    Shader as GlShader,
+};
 use crate::opengl::texture::{set_active_texture_unit, TextureUnit};
 use crate::opengl::vertex_array::{PrimitiveKind, VertexArray};
 use crate::opengl::{clear_buffers, enable, BufferClearMask, Capability};
 use crate::projection::new_perspective;
+use crate::shader::Program as ShaderProgram;
 use crate::vector::{Vec2, Vec3};
 use crate::vertex::Vertex;
 
@@ -30,6 +36,7 @@ use crate::vertex::Vertex;
 pub struct Renderer<CameraT>
 {
     camera: CameraT,
+    shader_programs: HashMap<u64, GlShaderProgram>,
 }
 
 impl<CameraT> Renderer<CameraT>
@@ -63,7 +70,44 @@ where
 
         enable(Capability::DepthTest);
 
-        Ok(Self { camera })
+        Ok(Self {
+            camera,
+            shader_programs: HashMap::new(),
+        })
+    }
+
+    pub fn create_shader_programs<'program>(
+        &mut self,
+        programs: impl IntoIterator<Item = &'program ShaderProgram>,
+    ) -> Result<(), Error>
+    {
+        for program in programs {
+            let gl_shaders = program
+                .shaders()
+                .iter()
+                .map(|shader| {
+                    let gl_shader = GlShader::new(shader.kind());
+
+                    gl_shader.set_source(shader.source())?;
+                    gl_shader.compile()?;
+
+                    Ok::<_, Error>(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()?;
+
+            self.shader_programs
+                .insert(program.u64_hash(), gl_shader_program);
+        }
+
+        Ok(())
     }
 
     pub fn render<'obj>(
@@ -76,15 +120,27 @@ where
         clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH);
 
         for obj in objects {
-            obj.shader().activate(|shader_program_curr_bound| {
+            let shader_program = self
+                .shader_programs
+                .get(&obj.shader().u64_hash())
+                .ok_or(Error::MissingShaderProgram)?;
+
+            shader_program.activate(|shader_program_curr_bound| {
                 apply_transformation_matrices(
                     obj,
+                    shader_program,
                     &self.camera,
                     window_size,
                     &shader_program_curr_bound,
                 );
 
-                apply_light(obj, light_source, &self.camera, &shader_program_curr_bound);
+                apply_light(
+                    obj,
+                    shader_program,
+                    light_source,
+                    &self.camera,
+                    &shader_program_curr_bound,
+                );
 
                 for (texture_id, texture) in obj.textures() {
                     let texture_unit = TextureUnit::from_texture_id(*texture_id)
@@ -97,7 +153,7 @@ where
 
                 Self::draw_object(obj);
 
-                Ok(())
+                Ok::<_, Error>(())
             })?;
         }
 
@@ -220,16 +276,23 @@ pub enum Error
 
     #[error("Texture ID is a invalid texture unit")]
     TextureIdIsInvalidTextureUnit,
+
+    #[error(transparent)]
+    GlShader(#[from] GlShaderError),
+
+    #[error("No shader program object was found for object")]
+    MissingShaderProgram,
 }
 
 fn apply_transformation_matrices(
     object: &Object,
+    gl_shader_program: &GlShaderProgram,
     camera: &impl Camera,
     window_size: &WindowSize,
-    shader_program_curr_bound: &CurrentlyBound<ShaderProgram>,
+    shader_program_curr_bound: &CurrentlyBound<GlShaderProgram>,
 )
 {
-    object.shader().set_uniform_matrix_4fv(
+    gl_shader_program.set_uniform_matrix_4fv(
         shader_program_curr_bound,
         cstr!("model"),
         &object.transform().as_matrix(),
@@ -237,7 +300,7 @@ fn apply_transformation_matrices(
 
     let view = create_view(camera);
 
-    object.shader().set_uniform_matrix_4fv(
+    gl_shader_program.set_uniform_matrix_4fv(
         shader_program_curr_bound,
         cstr!("view"),
         &view,
@@ -251,7 +314,7 @@ fn apply_transformation_matrices(
         0.1,
     );
 
-    object.shader().set_uniform_matrix_4fv(
+    gl_shader_program.set_uniform_matrix_4fv(
         shader_program_curr_bound,
         cstr!("projection"),
         &projection,
@@ -260,12 +323,13 @@ fn apply_transformation_matrices(
 
 fn apply_light(
     obj: &Object,
+    gl_shader_program: &GlShaderProgram,
     light_source: Option<&LightSource>,
     camera: &impl Camera,
-    shader_program_curr_bound: &CurrentlyBound<ShaderProgram>,
+    shader_program_curr_bound: &CurrentlyBound<GlShaderProgram>,
 )
 {
-    obj.shader().set_uniform_vec_3fv(
+    gl_shader_program.set_uniform_vec_3fv(
         shader_program_curr_bound,
         cstr!("light.position"),
         &light_source.map_or_else(Vec3::default, |light_source| {
@@ -273,7 +337,7 @@ fn apply_light(
         }),
     );
 
-    obj.shader().set_uniform_vec_3fv(
+    gl_shader_program.set_uniform_vec_3fv(
         shader_program_curr_bound,
         cstr!("light.ambient"),
         &light_source
@@ -283,7 +347,7 @@ fn apply_light(
             .into(),
     );
 
-    obj.shader().set_uniform_vec_3fv(
+    gl_shader_program.set_uniform_vec_3fv(
         shader_program_curr_bound,
         cstr!("light.diffuse"),
         &light_source
@@ -293,7 +357,7 @@ fn apply_light(
             .into(),
     );
 
-    obj.shader().set_uniform_vec_3fv(
+    gl_shader_program.set_uniform_vec_3fv(
         shader_program_curr_bound,
         cstr!("light.specular"),
         &light_source
@@ -304,33 +368,33 @@ fn apply_light(
     );
 
     #[allow(clippy::cast_possible_wrap)]
-    obj.shader().set_uniform_1i(
+    gl_shader_program.set_uniform_1i(
         shader_program_curr_bound,
         cstr!("material.ambient"),
         obj.material().ambient_map().into_inner() as i32,
     );
 
     #[allow(clippy::cast_possible_wrap)]
-    obj.shader().set_uniform_1i(
+    gl_shader_program.set_uniform_1i(
         shader_program_curr_bound,
         cstr!("material.diffuse"),
         obj.material().diffuse_map().into_inner() as i32,
     );
 
     #[allow(clippy::cast_possible_wrap)]
-    obj.shader().set_uniform_1i(
+    gl_shader_program.set_uniform_1i(
         shader_program_curr_bound,
         cstr!("material.specular"),
         obj.material().specular_map().into_inner() as i32,
     );
 
-    obj.shader().set_uniform_1fv(
+    gl_shader_program.set_uniform_1fv(
         shader_program_curr_bound,
         cstr!("material.shininess"),
         obj.material().shininess(),
     );
 
-    obj.shader().set_uniform_vec_3fv(
+    gl_shader_program.set_uniform_vec_3fv(
         shader_program_curr_bound,
         cstr!("view_pos"),
         &camera.position(),
diff --git a/engine/src/shader.rs b/engine/src/shader.rs
new file mode 100644
index 0000000..428e5a8
--- /dev/null
+++ b/engine/src/shader.rs
@@ -0,0 +1,156 @@
+use std::collections::hash_map::DefaultHasher;
+use std::fs::read_to_string;
+use std::hash::{Hash, Hasher};
+use std::path::{Path, PathBuf};
+
+use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor};
+
+/// Shader program
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
+pub struct Program
+{
+    shaders: Vec<Shader>,
+}
+
+impl Program
+{
+    #[must_use]
+    pub fn new() -> Self
+    {
+        Self { shaders: Vec::new() }
+    }
+
+    pub fn push_shader(&mut self, shader: Shader)
+    {
+        self.shaders.push(shader);
+    }
+
+    pub fn append_shaders(&mut self, shaders: impl IntoIterator<Item = Shader>)
+    {
+        self.shaders.extend(shaders);
+    }
+
+    #[must_use]
+    pub fn shaders(&self) -> &[Shader]
+    {
+        &self.shaders
+    }
+
+    pub(crate) fn u64_hash(&self) -> u64
+    {
+        let mut hasher = DefaultHasher::new();
+
+        self.hash(&mut hasher);
+
+        hasher.finish()
+    }
+}
+
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct Shader
+{
+    kind: Kind,
+    source: String,
+    file: PathBuf,
+}
+
+impl Shader
+{
+    /// Reads a shader from the specified source file.
+    ///
+    /// # Errors
+    /// Will return `Err` if:
+    /// - Reading the file fails
+    /// - The shader source is not ASCII
+    pub fn read_shader_file(kind: Kind, shader_file: &Path) -> Result<Self, Error>
+    {
+        let source = read_to_string(shader_file).map_err(|err| Error::ReadFailed {
+            source: err,
+            shader_file: shader_file.to_path_buf(),
+        })?;
+
+        if !source.is_ascii() {
+            return Err(Error::SourceNotAscii);
+        }
+
+        Ok(Self {
+            kind,
+            source,
+            file: shader_file.to_path_buf(),
+        })
+    }
+
+    /// Preprocesses the shaders.
+    ///
+    /// # Errors
+    /// Returns `Err` if preprocessing fails.
+    pub fn preprocess(self) -> Result<Self, Error>
+    {
+        let shader_preprocessor = ShaderPreprocessor::new(
+            self.file
+                .parent()
+                .ok_or(Error::SourcePathHasNoParent)?
+                .to_path_buf(),
+        );
+
+        let source_preprocessed = shader_preprocessor
+            .preprocess(self.source, &self.file)
+            .map_err(|err| Error::PreprocessFailed {
+                source: err,
+                shader_file: self.file.clone(),
+            })?;
+
+        Ok(Self {
+            kind: self.kind,
+            source: source_preprocessed,
+            file: self.file.clone(),
+        })
+    }
+
+    #[must_use]
+    pub fn kind(&self) -> Kind
+    {
+        self.kind
+    }
+
+    #[must_use]
+    pub fn source(&self) -> &str
+    {
+        &self.source
+    }
+}
+
+/// Shader kind.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Kind
+{
+    Vertex,
+    Fragment,
+}
+
+/// Shader error
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+    #[error("Failed to read shader {}", shader_file.display())]
+    ReadFailed
+    {
+        #[source]
+        source: std::io::Error,
+        shader_file: PathBuf,
+    },
+
+    #[error("Shader source is not ASCII")]
+    SourceNotAscii,
+
+    #[error("Failed to preprocess shader {}", shader_file.display())]
+    PreprocessFailed
+    {
+        #[source]
+        source: ShaderPreprocessorError,
+        shader_file: PathBuf,
+    },
+
+    #[error("Shader source path has no parent")]
+    SourcePathHasNoParent,
+}
-- 
cgit v1.2.3-18-g5258