Skip to main content

macroquad_ply/
material.rs

1//! Custom materials - shaders, uniforms.
2
3use crate::{get_context, quad_gl::GlPipeline, texture::Texture2D, tobytes::ToBytes, Error};
4use miniquad::{PipelineParams, UniformDesc};
5use std::sync::Arc;
6
7#[derive(PartialEq)]
8struct GlPipelineGuarded(GlPipeline);
9
10impl Drop for GlPipelineGuarded {
11    fn drop(&mut self) {
12        get_context().gl.delete_pipeline(self.0);
13    }
14}
15
16/// Material instance loaded on GPU.
17#[derive(Clone, PartialEq)]
18pub struct Material {
19    pipeline: Arc<GlPipelineGuarded>,
20}
21
22impl std::fmt::Debug for Material {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("Material").finish()
25    }
26}
27
28impl Material {
29    /// Set GPU uniform value for this material.
30    /// "name" should be from "uniforms" list used for material creation.
31    /// Otherwise uniform value would be silently ignored.
32    pub fn set_uniform<T>(&self, name: &str, uniform: T) {
33        get_context().gl.set_uniform(self.pipeline.0, name, uniform);
34    }
35
36    pub fn set_uniform_array<T: ToBytes>(&self, name: &str, uniform: &[T]) {
37        get_context()
38            .gl
39            .set_uniform_array(self.pipeline.0, name, uniform);
40    }
41
42    pub fn set_texture(&self, name: &str, texture: Texture2D) {
43        get_context().gl.set_texture(self.pipeline.0, name, texture);
44    }
45}
46
47/// Params used for material loading.
48/// It is not possible to change material params at runtime, so this
49/// struct is used only once - at "load_material".
50#[derive(Default)]
51pub struct MaterialParams {
52    /// miniquad pipeline configuration for this material.
53    /// Things like blending, culling, depth dest
54    pub pipeline_params: PipelineParams,
55
56    /// List of custom uniforms used in this material
57    pub uniforms: Vec<UniformDesc>,
58
59    /// List of textures used in this material
60    pub textures: Vec<String>,
61}
62
63/// Creates custom material
64///
65/// For OpenGL and Metal examples check examples/custom_material.rs.
66///
67/// # Default variables for OpenGL backend:
68/// ## Attributes (order doesn't matter, any could be skipped):
69/// ```glsl
70/// attribute vec3 position;
71/// attribute vec4 color0;
72/// attribute vec2 texcoord;
73/// ```
74/// ## Uniforms (order doesn't matter, any could be skipped):
75/// ```glsl
76/// uniform mat4 Model;
77/// uniform mat4 Projection;
78/// uniform float4 _Time;
79/// ```
80/// ## Textures (order doesn't matter, any could be skipped):
81/// ```glsl
82/// uniform sampler2D Texture;
83/// uniform sampler2D _ScreenTexture; // check examples/screen_texture.rs to see how it works
84/// ```
85///
86/// # Default variables for Metal backend:
87/// ## Attributes (order doesn't matter, any could be skipped, should have exact index in attribute()):
88/// ```msl
89/// struct Vertex
90/// {
91///     float3 position    [[attribute(0)]];
92///     float2 texcoord    [[attribute(1)]];
93///     float4 color0      [[attribute(2)]];
94/// };
95/// ```
96/// ## Uniforms (**order matters, all fields before needed one should present**):
97/// **All uniforms are in the same buffer, so order matters also for custom additional uniforms**
98/// ```msl
99/// struct Uniforms
100/// {
101///     float4x4 Model;
102///     float4x4 Projection;
103///     float4 _Time;
104///     ...
105///     additional uniforms
106/// };
107/// // same for vertexShader
108/// // Only buffer(0) is correct here
109/// fragment float4 fragmentShader(..., constant Uniforms& u [[buffer(0)]], ...) {...}
110/// ```
111/// ## Textures (order doesn't matter, any could be skipped, should have exact indices in `texture()` and `sampler()`):
112/// ```msl
113/// // same for vertexShader
114/// fragment float4 fragmentShader(...,
115///     texture2d<float> Texture [[texture(0)]],
116///     sampler TextureSmplr [[sampler(0)]],
117///     _ScreenTexture is not working for metal for now
118///     ...
119/// ) {...}
120/// ```
121///
122pub fn load_material(
123    shader: crate::ShaderSource,
124    params: MaterialParams,
125) -> Result<Material, Error> {
126    let context = &mut get_context();
127
128    let pipeline = context.gl.make_pipeline(
129        &mut *context.quad_context,
130        shader,
131        params.pipeline_params,
132        params.uniforms,
133        params.textures,
134    )?;
135
136    Ok(Material {
137        pipeline: Arc::new(GlPipelineGuarded(pipeline)),
138    })
139}
140
141/// All following macroquad rendering calls will use the given material.
142pub fn gl_use_material(material: &Material) {
143    get_context().gl.pipeline(Some(material.pipeline.0));
144}
145
146/// Use default macroquad material.
147pub fn gl_use_default_material() {
148    get_context().gl.pipeline(None);
149}
150
151#[doc(hidden)]
152pub mod shaders {
153    type IncludeFilename = String;
154    type IncludeContent = String;
155
156    #[derive(Debug, Clone)]
157    pub struct PreprocessorConfig {
158        pub includes: Vec<(IncludeFilename, IncludeContent)>,
159    }
160    impl Default for PreprocessorConfig {
161        fn default() -> PreprocessorConfig {
162            PreprocessorConfig { includes: vec![] }
163        }
164    }
165
166    impl PreprocessorConfig {}
167
168    pub fn preprocess_shader(source: &str, config: &PreprocessorConfig) -> String {
169        let mut res = source.chars().collect::<Vec<_>>();
170
171        fn find(data: &[char], n: &mut usize, target: &str) -> bool {
172            if *n >= data.len() {
173                return false;
174            }
175            let target = target.chars().collect::<Vec<_>>();
176
177            'outer: for i in *n..data.len() {
178                for j in 0..target.len() {
179                    if data[i + j] != target[j] {
180                        *n += 1;
181                        continue 'outer;
182                    }
183                }
184                return true;
185            }
186            false
187        }
188
189        fn skip_character(data: &[char], n: &mut usize, target: char) {
190            while *n < data.len() && data[*n] == target {
191                *n += 1;
192            }
193        }
194
195        let mut i = 0;
196        while find(&res, &mut i, "#include") {
197            let directive_start_ix = i;
198            i += "#include".len();
199            skip_character(&res, &mut i, ' ');
200            assert!(res[i] == '\"');
201            i += 1;
202            let filename_start_ix = i;
203            find(&res, &mut i, "\"");
204            let filename_end_ix = i;
205            let filename = res[filename_start_ix..filename_end_ix]
206                .iter()
207                .cloned()
208                .collect::<String>();
209
210            let include_content = config
211                .includes
212                .iter()
213                .find(|(name, _)| name == &filename)
214                .expect(&format!(
215                    "Include file {filename} in not on \"includes\" list"
216                ));
217
218            let _ = res
219                .splice(
220                    directive_start_ix..filename_end_ix + 1,
221                    include_content.1.chars(),
222                )
223                .collect::<Vec<_>>();
224        }
225
226        res.into_iter().collect()
227    }
228
229    #[test]
230    fn preprocessor_test() {
231        let shader_string = r#"
232#version blah blah
233
234asd
235asd
236
237#include "hello.glsl"
238
239qwe
240"#;
241
242        let preprocessed = r#"
243#version blah blah
244
245asd
246asd
247
248iii
249jjj
250
251qwe
252"#;
253
254        let result = preprocess_shader(
255            shader_string,
256            &PreprocessorConfig {
257                includes: vec![("hello.glsl".to_string(), "iii\njjj".to_string())],
258                ..Default::default()
259            },
260        );
261
262        assert_eq!(result, preprocessed);
263    }
264}