good_web_game/graphics/
shader.rs

1use crate::{Context, GameError, GameResult};
2use miniquad::{
3    BlendFactor, BlendState, BlendValue, BufferLayout, Equation, PipelineParams, VertexAttribute,
4    VertexFormat, VertexStep,
5};
6use std::cell::RefCell;
7use std::io::Read;
8use std::path::Path;
9use std::rc::Rc;
10
11use crate::graphics::context::default_shader;
12use bytemuck::Pod;
13use cgmath::Matrix4;
14pub use miniquad::{ShaderMeta, UniformBlockLayout, UniformDesc, UniformType};
15
16/// An enum for specifying default and custom blend modes
17///
18/// If you want to know what these actually do take a look at the implementation of `From<BlendMode> for Blend`
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum BlendMode {
21    /// When combining two fragments, add their values together, saturating
22    /// at 1.0
23    Add,
24    /// When combining two fragments, subtract the source value from the
25    /// destination value
26    Subtract,
27    /// When combining two fragments, add the value of the source times its
28    /// alpha channel with the value of the destination multiplied by the inverse
29    /// of the source alpha channel. Has the usual transparency effect: mixes the
30    /// two colors using a fraction of each one specified by the alpha of the source.
31    Alpha,
32    /// When combining two fragments, multiply their values together (including alpha)
33    Multiply,
34    /// When combining two fragments, choose the source value (including source alpha)
35    Replace,
36    /// When using premultiplied alpha, use this.
37    ///
38    /// You usually want to use this blend mode for drawing canvases
39    /// containing semi-transparent imagery.
40    /// For an explanation on this see: `<https://github.com/ggez/ggez/issues/694#issuecomment-853724926>`
41    Premultiplied,
42}
43
44impl From<BlendMode> for (BlendState, BlendState) {
45    fn from(bm: BlendMode) -> Self {
46        match bm {
47            BlendMode::Add => (
48                BlendState::new(
49                    Equation::Add,
50                    BlendFactor::Value(BlendValue::SourceAlpha),
51                    BlendFactor::One,
52                ),
53                BlendState::new(
54                    Equation::Add,
55                    BlendFactor::Value(BlendValue::SourceAlpha),
56                    BlendFactor::One,
57                ),
58            ),
59            BlendMode::Subtract => (
60                BlendState::new(
61                    Equation::ReverseSubtract,
62                    BlendFactor::Value(BlendValue::SourceAlpha),
63                    BlendFactor::One,
64                ),
65                BlendState::new(Equation::Add, BlendFactor::Zero, BlendFactor::One),
66            ),
67            BlendMode::Alpha => (
68                BlendState::new(
69                    Equation::Add,
70                    BlendFactor::Value(BlendValue::SourceAlpha),
71                    BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
72                ),
73                BlendState::new(
74                    Equation::Add,
75                    BlendFactor::OneMinusValue(BlendValue::DestinationAlpha),
76                    BlendFactor::One,
77                ),
78            ),
79            BlendMode::Premultiplied => (
80                BlendState::new(
81                    Equation::Add,
82                    BlendFactor::One,
83                    BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
84                ),
85                BlendState::new(
86                    Equation::Add,
87                    BlendFactor::OneMinusValue(BlendValue::DestinationAlpha),
88                    BlendFactor::One,
89                ),
90            ),
91            BlendMode::Multiply => (
92                BlendState::new(
93                    Equation::Add,
94                    BlendFactor::Value(BlendValue::DestinationColor),
95                    BlendFactor::Zero,
96                ),
97                BlendState::new(
98                    Equation::Add,
99                    BlendFactor::Value(BlendValue::DestinationAlpha),
100                    BlendFactor::Zero,
101                ),
102            ),
103            BlendMode::Replace => (
104                BlendState::new(Equation::Add, BlendFactor::One, BlendFactor::Zero),
105                BlendState::new(Equation::Add, BlendFactor::One, BlendFactor::Zero),
106            ),
107        }
108    }
109}
110
111/// An ID used by the good-web-game graphics context to uniquely identify a shader
112pub type ShaderId = usize;
113
114const MATRIX_SIZE: isize = std::mem::size_of::<Matrix4<f32>>() as isize;
115
116#[derive(Debug)]
117pub struct Shader {
118    pub(crate) pipeline: miniquad::Pipeline,
119    pub(crate) uniforms: Vec<u8>,
120}
121
122impl Shader {
123    /// Creates a shader from a miniquad shader without adding it to the shader vec of the gfx context.
124    /// Useful for creating a shader before the context is initialized.
125    pub(crate) fn from_mini_shader(
126        quad_ctx: &mut miniquad::Context,
127        mini_shader: miniquad::Shader,
128        blend_mode: Option<BlendMode>,
129    ) -> Self {
130        let (color_blend, alpha_blend) = blend_mode.unwrap_or(BlendMode::Alpha).into();
131
132        let new_shader_pipeline = miniquad::Pipeline::with_params(
133            quad_ctx,
134            &[
135                BufferLayout::default(),
136                BufferLayout {
137                    step_func: VertexStep::PerInstance,
138                    ..Default::default()
139                },
140            ],
141            &[
142                VertexAttribute::with_buffer("position", VertexFormat::Float2, 0),
143                VertexAttribute::with_buffer("texcoord", VertexFormat::Float2, 0),
144                VertexAttribute::with_buffer("color0", VertexFormat::Float4, 0),
145                VertexAttribute::with_buffer("Source", VertexFormat::Float4, 1),
146                VertexAttribute::with_buffer("Color", VertexFormat::Float4, 1),
147                VertexAttribute::with_buffer("Model", VertexFormat::Mat4, 1),
148            ],
149            mini_shader,
150            PipelineParams {
151                color_blend: Some(color_blend),
152                alpha_blend: Some(alpha_blend),
153                ..Default::default()
154            },
155        );
156        Shader {
157            pipeline: new_shader_pipeline,
158            uniforms: vec![0u8; MATRIX_SIZE as usize], // we need to make space for the projection matrix here
159        }
160    }
161
162    #[allow(clippy::new_ret_no_self)]
163    /// Create a new `Shader` given source files, constants and a name.
164    pub fn new<P: AsRef<Path>>(
165        ctx: &mut Context,
166        quad_ctx: &mut miniquad::graphics::GraphicsContext,
167        vertex_path: P,
168        pixel_path: P,
169        shader_meta: ShaderMeta,
170        blend_mode: Option<BlendMode>,
171    ) -> GameResult<ShaderId> {
172        let vertex_source = {
173            let mut buf = Vec::new();
174            let mut reader = ctx.filesystem.open(vertex_path)?;
175            let _ = reader.read_to_end(&mut buf)?;
176            buf
177        };
178        let pixel_source = {
179            let mut buf = Vec::new();
180            let mut reader = ctx.filesystem.open(pixel_path)?;
181            let _ = reader.read_to_end(&mut buf)?;
182            buf
183        };
184        Self::from_u8(
185            ctx,
186            quad_ctx,
187            &vertex_source,
188            &pixel_source,
189            shader_meta,
190            blend_mode,
191        )
192    }
193
194    /// Create a new `Shader` directly from GLSL source code, given as byte slices.
195    pub fn from_u8(
196        ctx: &mut Context,
197        quad_ctx: &mut miniquad::graphics::GraphicsContext,
198        vertex_source: &[u8],
199        pixel_source: &[u8],
200        shader_meta: ShaderMeta,
201        blend_mode: Option<BlendMode>,
202    ) -> GameResult<ShaderId> {
203        fn to_shader_error(e: std::str::Utf8Error) -> GameError {
204            GameError::ShaderProgramError(e.to_string())
205        }
206
207        let vertex_source = std::str::from_utf8(vertex_source).map_err(to_shader_error)?;
208        let pixel_source = std::str::from_utf8(pixel_source).map_err(to_shader_error)?;
209
210        Self::from_str(
211            ctx,
212            quad_ctx,
213            vertex_source,
214            pixel_source,
215            shader_meta,
216            blend_mode,
217        )
218    }
219
220    /// Create a new `Shader` directly from GLSL source code.
221    pub fn from_str(
222        ctx: &mut Context,
223        quad_ctx: &mut miniquad::graphics::GraphicsContext,
224        vertex_source: &str,
225        pixel_source: &str,
226        shader_meta: ShaderMeta,
227        blend_mode: Option<BlendMode>,
228    ) -> GameResult<ShaderId> {
229        let miniquad_shader =
230            miniquad::graphics::Shader::new(quad_ctx, vertex_source, pixel_source, shader_meta)?;
231
232        let shader = Self::from_mini_shader(quad_ctx, miniquad_shader, blend_mode);
233
234        let id = ctx.gfx_context.shaders.len();
235        ctx.gfx_context.shaders.push(shader);
236
237        Ok(id)
238    }
239}
240
241/// A lock for RAII shader regions. The shader automatically gets cleared once
242/// the lock goes out of scope, restoring the previous shader (if any).
243///
244/// Essentially, binding a [`Shader`](type.Shader.html) will return one of these,
245/// and the shader will remain active as long as this object exists.  When this is
246/// dropped, the previous shader is restored.
247#[derive(Debug, Clone)]
248pub struct ShaderLock {
249    cell: Rc<RefCell<ShaderId>>,
250    previous_shader: ShaderId,
251}
252
253impl Drop for ShaderLock {
254    fn drop(&mut self) {
255        *self.cell.borrow_mut() = self.previous_shader;
256    }
257}
258
259/// Use a shader until the returned lock goes out of scope
260pub fn use_shader(ctx: &mut Context, ps: ShaderId) -> ShaderLock {
261    let cell = Rc::clone(&ctx.gfx_context.current_shader);
262    let previous_shader = *cell.borrow();
263    set_shader(ctx, ps);
264    ShaderLock {
265        cell,
266        previous_shader,
267    }
268}
269
270/// Set the current shader for the `Context` to render with
271pub fn set_shader(ctx: &mut Context, ps: ShaderId) {
272    *ctx.gfx_context.current_shader.borrow_mut() = ps;
273}
274
275/// Clears the the current shader for the `Context`, restoring the default shader.
276///
277/// However, calling this and then dropping a [`ShaderLock`](struct.ShaderLock.html)
278/// will still set the shader to whatever was set when the `ShaderLock` was created.
279pub fn clear_shader(ctx: &mut Context) {
280    *ctx.gfx_context.current_shader.borrow_mut() = default_shader::SHADER_ID;
281}
282
283/// Sets the additional uniforms used by the given shader.
284///
285/// Note that the `Projection` uniform is calculated and then appended by good-web-game internally.
286pub fn set_uniforms<U: Pod>(ctx: &mut Context, id: ShaderId, extra_uniforms: U) {
287    let shader = &mut ctx.gfx_context.shaders[id];
288    shader.uniforms.clear();
289    shader
290        .uniforms
291        .extend_from_slice(bytemuck::bytes_of(&extra_uniforms));
292    shader
293        .uniforms
294        .extend_from_slice(&[0u8; std::mem::size_of::<Matrix4<f32>>()]);
295}
296
297/// Apply the uniforms for the given shader.
298pub(crate) fn apply_uniforms(
299    ctx: &mut Context,
300    quad_ctx: &mut miniquad::graphics::GraphicsContext,
301    shader_id: ShaderId,
302    batch_model: Option<Matrix4<f32>>,
303) {
304    let projection = if let Some(model) = batch_model {
305        ctx.gfx_context.projection * model
306    } else {
307        ctx.gfx_context.projection
308    };
309    let projection_bytes = &projection as *const _ as *const u8;
310
311    let current_shader = &mut ctx.gfx_context.shaders[shader_id];
312    unsafe {
313        let after_last_byte = current_shader
314            .uniforms
315            .as_mut_ptr_range()
316            .end
317            .offset(-MATRIX_SIZE);
318        for offset in 0..MATRIX_SIZE {
319            *after_last_byte.offset(offset) = *projection_bytes.offset(offset);
320        }
321    }
322    quad_ctx.apply_uniforms_from_bytes(
323        current_shader.uniforms.as_ptr(),
324        current_shader.uniforms.len(),
325    );
326}