maia_wasm/render/
engine.rs

1use super::{ProgramSource, RenderObject};
2use std::rc::Rc;
3use wasm_bindgen::JsCast;
4use wasm_bindgen::prelude::*;
5use web_sys::{
6    HtmlCanvasElement, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlTexture,
7    WebGlVertexArrayObject, Window,
8};
9
10use text::TextRender;
11pub use text::TextsDimensions;
12pub use texture::{
13    LuminanceAlpha, R16f, Rgb, Rgba, Texture, TextureBuilder, TextureInternalFormat,
14    TextureMagFilter, TextureMinFilter, TextureParameter, TextureWrap,
15};
16pub use vao::VaoBuilder;
17
18// There are some modules defined below
19
20/// Render engine.
21///
22/// The render engine is the main object used for rendering. [`RenderObject`]'s
23/// are added to the engine using [`RenderEngine::add_object`]. A call to
24/// [`RenderEngine::render`] renders the scene.
25///
26/// The render engine also gives additional functionality, such as creation and
27/// modification of textures and VAOs, and rendering of text to a texture.
28pub struct RenderEngine {
29    canvas: Rc<HtmlCanvasElement>,
30    window: Rc<Window>,
31    canvas_dims: CanvasDims,
32    gl: WebGl2RenderingContext,
33    current: Current,
34    objects: Vec<RenderObject>,
35    text_render: TextRender,
36}
37
38#[derive(Debug)]
39struct Current {
40    program: Option<Rc<WebGlProgram>>,
41    vao: Option<Rc<WebGlVertexArrayObject>>,
42    textures: Textures,
43}
44
45#[derive(Debug)]
46struct Textures {
47    textures: Box<[Option<Rc<WebGlTexture>>]>,
48    write_pointer: usize,
49}
50
51/// Canvas dimensions.
52///
53/// This structure holds the canvas dimensions and can perform some calculations
54/// involving device pixels and CSS pixels.
55#[derive(Debug, Copy, Clone, PartialEq)]
56pub struct CanvasDims {
57    // These are in CSS pixels
58    width: u32,
59    height: u32,
60    // This is used to obtain device pixels
61    device_pixel_ratio: f64,
62}
63
64impl CanvasDims {
65    fn from_canvas_and_window(canvas: &HtmlCanvasElement, window: &web_sys::Window) -> CanvasDims {
66        CanvasDims {
67            width: canvas.client_width() as u32,
68            height: canvas.client_height() as u32,
69            device_pixel_ratio: window.device_pixel_ratio(),
70        }
71    }
72
73    /// Returns the canvas dimensions in device pixels.
74    pub fn device_pixels(&self) -> (u32, u32) {
75        (
76            (self.width as f64 * self.device_pixel_ratio).round() as u32,
77            (self.height as f64 * self.device_pixel_ratio).round() as u32,
78        )
79    }
80
81    /// Returns the canvas dimensions in CSS pixels.
82    pub fn css_pixels(&self) -> (u32, u32) {
83        (self.width, self.height)
84    }
85
86    fn set_viewport(&self, gl: &WebGl2RenderingContext) {
87        let (w, h) = self.device_pixels();
88        gl.viewport(0, 0, w as i32, h as i32);
89    }
90
91    fn set_canvas(&self, canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
92        let (w, h) = self.device_pixels();
93        canvas.set_width(w);
94        canvas.set_height(h);
95        Ok(())
96    }
97}
98
99impl Textures {
100    fn new(gl: &WebGl2RenderingContext) -> Result<Textures, JsValue> {
101        let num_textures =
102            gl.get_parameter(WebGl2RenderingContext::MAX_COMBINED_TEXTURE_IMAGE_UNITS)?
103                .as_f64()
104                .ok_or("MAX_COMBINED_TEXTURE_IMAGE_UNITS is not an number")? as usize;
105        let textures = vec![None; num_textures].into_boxed_slice();
106        Ok(Textures {
107            textures,
108            write_pointer: 0,
109        })
110    }
111
112    fn find_texture_unit(&self, texture: &Rc<WebGlTexture>) -> Option<i32> {
113        self.textures
114            .iter()
115            .enumerate()
116            .find_map(|(j, tex)| match tex {
117                Some(tex) if Rc::ptr_eq(tex, texture) => Some(j as i32),
118                _ => None,
119            })
120    }
121
122    fn load_texture(&mut self, gl: &WebGl2RenderingContext, texture: &Rc<WebGlTexture>) -> i32 {
123        let n = self.write_pointer;
124        self.write_pointer = (self.write_pointer + 1) % self.textures.len();
125        self.textures[n].replace(Rc::clone(texture));
126        gl.active_texture(WebGl2RenderingContext::TEXTURE0 + n as u32);
127        gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(texture));
128        n as i32
129    }
130
131    fn bind_texture(&mut self, gl: &WebGl2RenderingContext, texture: &Rc<WebGlTexture>) {
132        if let Some(texture_unit) = self.find_texture_unit(texture) {
133            // Texture is already bound to a texture unit. We only need to
134            // activate the texture unit.
135            gl.active_texture(WebGl2RenderingContext::TEXTURE0 + texture_unit as u32);
136        } else {
137            // Texture is not bound to any texture unit. We "load" it, which
138            // leaves it bound to the current active texture unit.
139            self.load_texture(gl, texture);
140        }
141    }
142}
143
144impl Current {
145    fn new(gl: &WebGl2RenderingContext) -> Result<Current, JsValue> {
146        Ok(Current {
147            program: None,
148            vao: None,
149            textures: Textures::new(gl)?,
150        })
151    }
152}
153
154// This module is only used to make the RenderEngine methods defined here appear
155// in the docs before those defined in the texture module.
156mod render_engine {
157    use super::*;
158
159    impl RenderEngine {
160        /// Creates a new render engine.
161        ///
162        /// The `canvas` is the HTML canvas element that will be used for the
163        /// render output.
164        pub fn new(
165            canvas: Rc<HtmlCanvasElement>,
166            window: Rc<Window>,
167            document: &web_sys::Document,
168        ) -> Result<RenderEngine, JsValue> {
169            let gl = canvas
170                .get_context("webgl2")?
171                .ok_or("unable to get webgl2 context")?
172                .dyn_into::<WebGl2RenderingContext>()?;
173            let gl_attrs = gl
174                .get_context_attributes()
175                .ok_or("unable to get webgl2 context attributes")?;
176            gl_attrs.set_alpha(false);
177            gl_attrs.set_antialias(true);
178            gl_attrs.set_power_preference(web_sys::WebGlPowerPreference::LowPower);
179            let canvas_dims = CanvasDims::from_canvas_and_window(&canvas, &window);
180            let current = Current::new(&gl)?;
181
182            // We use pre-multiplied alpha to obtain correct results with bilinear
183            // interpolation. See
184            // http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
185            gl.enable(WebGl2RenderingContext::BLEND);
186            gl.blend_func(
187                WebGl2RenderingContext::ONE,
188                WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
189            );
190
191            Ok(RenderEngine {
192                canvas,
193                window,
194                canvas_dims,
195                gl,
196                current,
197                objects: Vec::new(),
198                text_render: TextRender::new(document)?,
199            })
200        }
201
202        /// Adds a render object to the scene.
203        pub fn add_object(&mut self, object: RenderObject) {
204            self.objects.push(object);
205        }
206
207        /// Renders the scene to the canvas.
208        ///
209        /// The scene is formed by the objects that have been previously added
210        /// with [`RenderEngine::add_object`].
211        pub fn render(&mut self) -> Result<(), JsValue> {
212            for object in &self.objects {
213                if object.enabled.get() {
214                    self.current.draw(&self.gl, object)?;
215                }
216            }
217            Ok(())
218        }
219
220        /// Compiles a WebGL2 program.
221        ///
222        /// This function compiles the vertex and fragment shaders given in
223        /// `source` and links them as a program.
224        pub fn make_program(&self, source: ProgramSource<'_>) -> Result<Rc<WebGlProgram>, JsValue> {
225            self.link_program(
226                &self
227                    .compile_shader(WebGl2RenderingContext::VERTEX_SHADER, source.vertex_shader)?,
228                &self.compile_shader(
229                    WebGl2RenderingContext::FRAGMENT_SHADER,
230                    source.fragment_shader,
231                )?,
232            )
233            .map(Rc::new)
234        }
235
236        fn compile_shader(&self, shader_type: u32, source: &str) -> Result<WebGlShader, JsValue> {
237            let shader = self
238                .gl
239                .create_shader(shader_type)
240                .ok_or("failed to create shader")?;
241            self.gl.shader_source(&shader, source);
242            self.gl.compile_shader(&shader);
243            if self
244                .gl
245                .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
246                .as_bool()
247                .unwrap_or(false)
248            {
249                Ok(shader)
250            } else {
251                Err(self
252                    .gl
253                    .get_shader_info_log(&shader)
254                    .map(|x| JsValue::from(&x))
255                    .unwrap_or_else(|| "unknown error creating shader".into()))
256            }
257        }
258
259        fn link_program(
260            &self,
261            vertex_shader: &WebGlShader,
262            fragment_shader: &WebGlShader,
263        ) -> Result<WebGlProgram, JsValue> {
264            let program = self.gl.create_program().ok_or("unable to create program")?;
265            self.gl.attach_shader(&program, vertex_shader);
266            self.gl.attach_shader(&program, fragment_shader);
267            self.gl.link_program(&program);
268            if self
269                .gl
270                .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
271                .as_bool()
272                .unwrap_or(false)
273            {
274                Ok(program)
275            } else {
276                Err(self
277                    .gl
278                    .get_program_info_log(&program)
279                    .map(|x| JsValue::from(&x))
280                    .unwrap_or_else(|| "unknown error linking prgram".into()))
281            }
282        }
283
284        /// Creates a new VAO.
285        ///
286        /// This function creates a new Vertex Array Object. The VAO is
287        /// constructed using a [`VaoBuilder`].
288        pub fn create_vao(&mut self) -> Result<VaoBuilder<'_>, JsValue> {
289            VaoBuilder::new(self)
290        }
291
292        /// Modifies an existing VAO.
293        ///
294        /// This function returns a [`VaoBuilder`] that can modify the existing
295        /// Vertex Array Object `vao`.
296        pub fn modify_vao(&mut self, vao: Rc<WebGlVertexArrayObject>) -> VaoBuilder<'_> {
297            VaoBuilder::modify_vao(self, vao)
298        }
299
300        /// Creates a new texture.
301        ///
302        /// The texture is constructed using a [`TextureBuilder`].
303        pub fn create_texture(&mut self) -> Result<TextureBuilder<'_>, JsValue> {
304            TextureBuilder::new(self)
305        }
306
307        /// Returns the current canvas dimensions.
308        pub fn canvas_dims(&self) -> CanvasDims {
309            self.canvas_dims
310        }
311
312        /// Resizes the canvas.
313        ///
314        /// Resizes the canvas according to the current dimensions of the HTML
315        /// canvas element and the device pixel ratio. This function should be
316        /// called whenever any of these parameters change, in order to update
317        /// the render engine accordingly.
318        pub fn resize_canvas(&mut self) -> Result<(), JsValue> {
319            self.canvas_dims = CanvasDims::from_canvas_and_window(&self.canvas, &self.window);
320            self.canvas_dims.set_canvas(&self.canvas)?;
321            self.canvas_dims.set_viewport(&self.gl);
322            Ok(())
323        }
324
325        /// Renders a series of texts into a texture.
326        ///
327        /// Given a slice of text strings, this function uses an auxiliarly HTML
328        /// canvas element (which does not form part of the document) to render
329        /// all of these strings into the canvas and load the resulting image
330        /// into a texture. The information about the coordinates of the
331        /// bounding boxes of each text is return, allowing parts of the texture
332        /// to be used to display each text.
333        ///
334        /// A fixed text height in pixels, `text_height_px`, is used to set the
335        /// size of the font and of the image loaded in the texture.
336        pub fn render_texts_to_texture(
337            &mut self,
338            texture: &Rc<WebGlTexture>,
339            texts: &[String],
340            text_height_px: u32,
341        ) -> Result<TextsDimensions, JsValue> {
342            let dimensions = self
343                .text_render
344                .render(texts, self.canvas_dims, text_height_px)?;
345            self.texture_from_text_render::<LuminanceAlpha>(texture)?;
346            Ok(dimensions)
347        }
348
349        /// Returns the corresponding text width for a given string of text.
350        ///
351        /// The string of text is measured with a text height of `height_px`
352        /// pixels, and the width in screen coordinates is returned.
353        ///
354        /// This function can be used to find out how much screen space a given
355        /// text will occupy before the text is even rendered.
356        pub fn text_renderer_text_width(&self, text: &str, height_px: u32) -> Result<f32, JsValue> {
357            self.text_render
358                .text_width(text, self.canvas_dims, height_px)
359        }
360
361        #[allow(dead_code)]
362        fn use_program(&mut self, program: &Rc<WebGlProgram>) {
363            self.current.use_program(&self.gl, program)
364        }
365
366        pub(super) fn bind_vertex_array(&mut self, vao: &Rc<WebGlVertexArrayObject>) {
367            self.current.bind_vertex_array(&self.gl, vao)
368        }
369
370        pub(super) fn bind_texture(&mut self, texture: &Rc<WebGlTexture>) {
371            self.current.textures.bind_texture(&self.gl, texture)
372        }
373    }
374}
375
376mod text;
377mod texture;
378mod vao;
379
380impl Current {
381    fn use_program(&mut self, gl: &WebGl2RenderingContext, program: &Rc<WebGlProgram>) {
382        gl.use_program(Some(program));
383        self.program.replace(Rc::clone(program));
384    }
385
386    fn bind_vertex_array(&mut self, gl: &WebGl2RenderingContext, vao: &Rc<WebGlVertexArrayObject>) {
387        gl.bind_vertex_array(Some(vao));
388        self.vao.replace(Rc::clone(vao));
389    }
390
391    fn texture_unit(&mut self, gl: &WebGl2RenderingContext, texture: &Rc<WebGlTexture>) -> i32 {
392        self.textures
393            .find_texture_unit(texture)
394            .unwrap_or_else(|| self.textures.load_texture(gl, texture))
395    }
396
397    fn draw(&mut self, gl: &WebGl2RenderingContext, object: &RenderObject) -> Result<(), JsValue> {
398        if !self
399            .program
400            .as_ref()
401            .is_some_and(|p| Rc::ptr_eq(p, &object.program))
402        {
403            // Program doesn't match. Load new program.
404            self.use_program(gl, &object.program);
405        }
406
407        if !self
408            .vao
409            .as_ref()
410            .is_some_and(|vao| Rc::ptr_eq(vao, &object.vao))
411        {
412            // VAO doesn't match. Load new VAO.
413            self.bind_vertex_array(gl, &object.vao);
414        }
415
416        for uniform in object.uniforms.iter() {
417            uniform.set_uniform(gl, &object.program);
418        }
419
420        for texture in object.textures.iter() {
421            let texture_unit = self.texture_unit(gl, texture.texture());
422            let sampler_location = gl
423                .get_uniform_location(&object.program, texture.sampler())
424                .ok_or("sampler uniform location not found")?;
425            gl.uniform1i(Some(&sampler_location), texture_unit);
426        }
427
428        gl.draw_elements_with_i32(
429            object.draw_mode as u32,
430            object.draw_num_indices.get() as i32,
431            WebGl2RenderingContext::UNSIGNED_SHORT,
432            (object.draw_offset_elements.get() * std::mem::size_of::<u16>()) as i32,
433        );
434
435        Ok(())
436    }
437}