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
18pub 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#[derive(Debug, Copy, Clone, PartialEq)]
56pub struct CanvasDims {
57 width: u32,
59 height: u32,
60 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 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 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 gl.active_texture(WebGl2RenderingContext::TEXTURE0 + texture_unit as u32);
136 } else {
137 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
154mod render_engine {
157 use super::*;
158
159 impl RenderEngine {
160 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 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 pub fn add_object(&mut self, object: RenderObject) {
204 self.objects.push(object);
205 }
206
207 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 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 pub fn create_vao(&mut self) -> Result<VaoBuilder<'_>, JsValue> {
289 VaoBuilder::new(self)
290 }
291
292 pub fn modify_vao(&mut self, vao: Rc<WebGlVertexArrayObject>) -> VaoBuilder<'_> {
297 VaoBuilder::modify_vao(self, vao)
298 }
299
300 pub fn create_texture(&mut self) -> Result<TextureBuilder<'_>, JsValue> {
304 TextureBuilder::new(self)
305 }
306
307 pub fn canvas_dims(&self) -> CanvasDims {
309 self.canvas_dims
310 }
311
312 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 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 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 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 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}