use super::{ProgramSource, RenderObject};
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{
HtmlCanvasElement, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlTexture,
WebGlVertexArrayObject, Window,
};
use text::TextRender;
pub use text::TextsDimensions;
pub use texture::{
LuminanceAlpha, R16f, Rgb, Rgba, Texture, TextureBuilder, TextureInternalFormat,
TextureMagFilter, TextureMinFilter, TextureParameter, TextureWrap,
};
pub use vao::VaoBuilder;
pub struct RenderEngine {
canvas: Rc<HtmlCanvasElement>,
window: Rc<Window>,
canvas_dims: CanvasDims,
gl: WebGl2RenderingContext,
current: Current,
objects: Vec<RenderObject>,
text_render: TextRender,
}
#[derive(Debug)]
struct Current {
program: Option<Rc<WebGlProgram>>,
vao: Option<Rc<WebGlVertexArrayObject>>,
textures: Textures,
}
#[derive(Debug)]
struct Textures {
textures: Box<[Option<Rc<WebGlTexture>>]>,
write_pointer: usize,
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct CanvasDims {
width: u32,
height: u32,
device_pixel_ratio: f64,
}
impl CanvasDims {
fn from_canvas_and_window(canvas: &HtmlCanvasElement, window: &web_sys::Window) -> CanvasDims {
CanvasDims {
width: canvas.client_width() as u32,
height: canvas.client_height() as u32,
device_pixel_ratio: window.device_pixel_ratio(),
}
}
pub fn device_pixels(&self) -> (u32, u32) {
(
(self.width as f64 * self.device_pixel_ratio).round() as u32,
(self.height as f64 * self.device_pixel_ratio).round() as u32,
)
}
pub fn css_pixels(&self) -> (u32, u32) {
(self.width, self.height)
}
fn set_viewport(&self, gl: &WebGl2RenderingContext) {
let (w, h) = self.device_pixels();
gl.viewport(0, 0, w as i32, h as i32);
}
fn set_canvas(&self, canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
let (w, h) = self.device_pixels();
canvas.set_width(w);
canvas.set_height(h);
Ok(())
}
}
impl Textures {
fn new(gl: &WebGl2RenderingContext) -> Result<Textures, JsValue> {
let num_textures =
gl.get_parameter(WebGl2RenderingContext::MAX_COMBINED_TEXTURE_IMAGE_UNITS)?
.as_f64()
.ok_or("MAX_COMBINED_TEXTURE_IMAGE_UNITS is not an number")? as usize;
let textures = vec![None; num_textures].into_boxed_slice();
Ok(Textures {
textures,
write_pointer: 0,
})
}
fn find_texture_unit(&self, texture: &Rc<WebGlTexture>) -> Option<i32> {
self.textures
.iter()
.enumerate()
.find_map(|(j, tex)| match tex {
Some(tex) if Rc::ptr_eq(tex, texture) => Some(j as i32),
_ => None,
})
}
fn load_texture(&mut self, gl: &WebGl2RenderingContext, texture: &Rc<WebGlTexture>) -> i32 {
let n = self.write_pointer;
self.write_pointer = (self.write_pointer + 1) % self.textures.len();
self.textures[n].replace(Rc::clone(texture));
gl.active_texture(WebGl2RenderingContext::TEXTURE0 + n as u32);
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(texture));
n as i32
}
fn bind_texture(&mut self, gl: &WebGl2RenderingContext, texture: &Rc<WebGlTexture>) {
if let Some(texture_unit) = self.find_texture_unit(texture) {
gl.active_texture(WebGl2RenderingContext::TEXTURE0 + texture_unit as u32);
} else {
self.load_texture(gl, texture);
}
}
}
impl Current {
fn new(gl: &WebGl2RenderingContext) -> Result<Current, JsValue> {
Ok(Current {
program: None,
vao: None,
textures: Textures::new(gl)?,
})
}
}
mod render_engine {
use super::*;
impl RenderEngine {
pub fn new(
canvas: Rc<HtmlCanvasElement>,
window: Rc<Window>,
document: &web_sys::Document,
) -> Result<RenderEngine, JsValue> {
let gl = canvas
.get_context("webgl2")?
.ok_or("unable to get webgl2 context")?
.dyn_into::<WebGl2RenderingContext>()?;
gl.get_context_attributes()
.ok_or("unable to get webgl2 context attributes")?
.alpha(false)
.antialias(true)
.power_preference(web_sys::WebGlPowerPreference::LowPower);
let canvas_dims = CanvasDims::from_canvas_and_window(&canvas, &window);
let current = Current::new(&gl)?;
gl.enable(WebGl2RenderingContext::BLEND);
gl.blend_func(
WebGl2RenderingContext::ONE,
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
);
Ok(RenderEngine {
canvas,
window,
canvas_dims,
gl,
current,
objects: Vec::new(),
text_render: TextRender::new(document)?,
})
}
pub fn add_object(&mut self, object: RenderObject) {
self.objects.push(object);
}
pub fn render(&mut self) -> Result<(), JsValue> {
for object in &self.objects {
self.current.draw(&self.gl, object)?;
}
Ok(())
}
pub fn make_program(&self, source: ProgramSource<'_>) -> Result<Rc<WebGlProgram>, JsValue> {
self.link_program(
&self
.compile_shader(WebGl2RenderingContext::VERTEX_SHADER, source.vertex_shader)?,
&self.compile_shader(
WebGl2RenderingContext::FRAGMENT_SHADER,
source.fragment_shader,
)?,
)
.map(Rc::new)
}
fn compile_shader(&self, shader_type: u32, source: &str) -> Result<WebGlShader, JsValue> {
let shader = self
.gl
.create_shader(shader_type)
.ok_or("failed to create shader")?;
self.gl.shader_source(&shader, source);
self.gl.compile_shader(&shader);
if self
.gl
.get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(shader)
} else {
Err(self
.gl
.get_shader_info_log(&shader)
.map(|x| JsValue::from(&x))
.unwrap_or_else(|| "unknown error creating shader".into()))
}
}
fn link_program(
&self,
vertex_shader: &WebGlShader,
fragment_shader: &WebGlShader,
) -> Result<WebGlProgram, JsValue> {
let program = self.gl.create_program().ok_or("unable to create program")?;
self.gl.attach_shader(&program, vertex_shader);
self.gl.attach_shader(&program, fragment_shader);
self.gl.link_program(&program);
if self
.gl
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(program)
} else {
Err(self
.gl
.get_program_info_log(&program)
.map(|x| JsValue::from(&x))
.unwrap_or_else(|| "unknown error linking prgram".into()))
}
}
pub fn create_vao(&mut self) -> Result<VaoBuilder<'_>, JsValue> {
VaoBuilder::new(self)
}
pub fn modify_vao(&mut self, vao: Rc<WebGlVertexArrayObject>) -> VaoBuilder<'_> {
VaoBuilder::modify_vao(self, vao)
}
pub fn create_texture(&mut self) -> Result<TextureBuilder<'_>, JsValue> {
TextureBuilder::new(self)
}
pub fn canvas_dims(&self) -> CanvasDims {
self.canvas_dims
}
pub fn resize_canvas(&mut self) -> Result<(), JsValue> {
self.canvas_dims = CanvasDims::from_canvas_and_window(&self.canvas, &self.window);
self.canvas_dims.set_canvas(&self.canvas)?;
self.canvas_dims.set_viewport(&self.gl);
Ok(())
}
pub fn render_texts_to_texture(
&mut self,
texture: &Rc<WebGlTexture>,
texts: &[String],
text_height_px: u32,
) -> Result<TextsDimensions, JsValue> {
let dimensions = self
.text_render
.render(texts, self.canvas_dims, text_height_px)?;
self.texture_from_text_render::<LuminanceAlpha>(texture)?;
Ok(dimensions)
}
pub fn text_renderer_text_width(&self, text: &str, height_px: u32) -> Result<f32, JsValue> {
self.text_render
.text_width(text, self.canvas_dims, height_px)
}
#[allow(dead_code)]
fn use_program(&mut self, program: &Rc<WebGlProgram>) {
self.current.use_program(&self.gl, program)
}
pub(super) fn bind_vertex_array(&mut self, vao: &Rc<WebGlVertexArrayObject>) {
self.current.bind_vertex_array(&self.gl, vao)
}
pub(super) fn bind_texture(&mut self, texture: &Rc<WebGlTexture>) {
self.current.textures.bind_texture(&self.gl, texture)
}
}
}
mod text;
mod texture;
mod vao;
impl Current {
fn use_program(&mut self, gl: &WebGl2RenderingContext, program: &Rc<WebGlProgram>) {
gl.use_program(Some(program));
self.program.replace(Rc::clone(program));
}
fn bind_vertex_array(&mut self, gl: &WebGl2RenderingContext, vao: &Rc<WebGlVertexArrayObject>) {
gl.bind_vertex_array(Some(vao));
self.vao.replace(Rc::clone(vao));
}
fn texture_unit(&mut self, gl: &WebGl2RenderingContext, texture: &Rc<WebGlTexture>) -> i32 {
self.textures
.find_texture_unit(texture)
.unwrap_or_else(|| self.textures.load_texture(gl, texture))
}
fn draw(&mut self, gl: &WebGl2RenderingContext, object: &RenderObject) -> Result<(), JsValue> {
if !self
.program
.as_ref()
.map_or(false, |p| Rc::ptr_eq(p, &object.program))
{
self.use_program(gl, &object.program);
}
if !self
.vao
.as_ref()
.map_or(false, |vao| Rc::ptr_eq(vao, &object.vao))
{
self.bind_vertex_array(gl, &object.vao);
}
for uniform in object.uniforms.iter() {
uniform.set_uniform(gl, &object.program);
}
for texture in object.textures.iter() {
let texture_unit = self.texture_unit(gl, texture.texture());
let sampler_location = gl
.get_uniform_location(&object.program, texture.sampler())
.ok_or("sampler uniform location not found")?;
gl.uniform1i(Some(&sampler_location), texture_unit);
}
gl.draw_elements_with_i32(
object.draw_mode as u32,
object.draw_num_indices.get() as i32,
WebGl2RenderingContext::UNSIGNED_SHORT,
(object.draw_offset_elements.get() * std::mem::size_of::<u16>()) as i32,
);
Ok(())
}
}