use crate::{
Result,
backend::{Backend, ImageData, SurfaceData, VERTEX_SIZE},
geom::{Rectangle, Vector},
error::QuicksilverError,
graphics::{BlendMode, Color, GpuTriangle, Image, ImageScaleStrategy, PixelFormat, Surface, Vertex},
input::MouseCursor,
};
use std::mem::size_of;
use stdweb::{
web::{
html_element::CanvasElement,
TypedArray
},
unstable::TryInto
};
use webgl_stdweb::{
WebGLBuffer,
WebGLProgram,
WebGL2RenderingContext as gl,
WebGLShader,
WebGLTexture,
WebGLUniformLocation
};
use stdweb::web::document;
pub struct WebGLBackend {
canvas: CanvasElement,
gl_ctx: gl,
texture: Option<u32>,
vertices: Vec<f32>,
indices: Vec<u32>,
vertex_length: usize,
index_length: usize,
shader: WebGLProgram,
fragment: WebGLShader,
vertex: WebGLShader,
vbo: WebGLBuffer,
ebo: WebGLBuffer,
texture_location: Option<WebGLUniformLocation>,
texture_mode: u32,
initial_width: u32,
initial_height: u32,
textures: Vec<Option<WebGLTexture>>,
}
const DEFAULT_VERTEX_SHADER: &str = r#"attribute vec2 position;
attribute vec2 tex_coord;
attribute vec4 color;
attribute lowp float uses_texture;
varying vec2 Tex_coord;
varying vec4 Color;
varying lowp float Uses_texture;
void main() {
gl_Position = vec4(position, 0, 1);
Tex_coord = tex_coord;
Color = color;
Uses_texture = uses_texture;
}"#;
const DEFAULT_FRAGMENT_SHADER: &str = r#"varying highp vec4 Color;
varying highp vec2 Tex_coord;
varying lowp float Uses_texture;
uniform sampler2D tex;
void main() {
highp vec4 tex_color = (int(Uses_texture) != 0) ? texture2D(tex, Tex_coord) : vec4(1, 1, 1, 1);
gl_FragColor = Color * tex_color;
}"#;
fn format_gl(format: PixelFormat) -> u32 {
match format {
PixelFormat::RGB => gl::RGB,
PixelFormat::RGBA => gl::RGBA
}
}
fn try_opt<T>(opt: Option<T>, operation: &str) -> Result<T> {
match opt {
Some(val) => Ok(val),
None => {
let mut error = String::new();
error.push_str("WebGL2 operation failed: ");
error.push_str(operation);
Err(QuicksilverError::ContextError(error))
}
}
}
impl Backend for WebGLBackend {
type Platform = CanvasElement;
unsafe fn new(canvas: CanvasElement, texture_mode: ImageScaleStrategy, _multisample: bool) -> Result<WebGLBackend> {
let gl_ctx: gl = match canvas.get_context() {
Ok(ctx) => ctx,
_ => return Err(QuicksilverError::ContextError("Could not create WebGL2 context".to_owned()))
};
let texture_mode = match texture_mode {
ImageScaleStrategy::Pixelate => gl::NEAREST,
ImageScaleStrategy::Blur => gl::LINEAR
};
let vbo = try_opt(gl_ctx.create_buffer(), "Create vertex buffer")?;
let ebo = try_opt(gl_ctx.create_buffer(), "Create index buffer")?;
gl_ctx.bind_buffer(gl::ARRAY_BUFFER, Some(&vbo));
gl_ctx.bind_buffer(gl::ELEMENT_ARRAY_BUFFER, Some(&ebo));
gl_ctx.blend_func_separate(
gl::SRC_ALPHA,
gl::ONE_MINUS_SRC_ALPHA,
gl::ONE,
gl::ONE_MINUS_SRC_ALPHA,
);
gl_ctx.enable(gl::BLEND);
let vertex = try_opt(gl_ctx.create_shader(gl::VERTEX_SHADER), "Create vertex shader")?;
gl_ctx.shader_source(&vertex, DEFAULT_VERTEX_SHADER);
gl_ctx.compile_shader(&vertex);
let fragment = try_opt(gl_ctx.create_shader(gl::FRAGMENT_SHADER), "Create fragment shader")?;
gl_ctx.shader_source(&fragment, DEFAULT_FRAGMENT_SHADER);
gl_ctx.compile_shader(&fragment);
let shader = try_opt(gl_ctx.create_program(), "Create shader program")?;
gl_ctx.attach_shader(&shader, &vertex);
gl_ctx.attach_shader(&shader, &fragment);
gl_ctx.link_program(&shader);
gl_ctx.use_program(Some(&shader));
let initial_width = canvas.width();
let initial_height = canvas.height();
Ok(WebGLBackend {
canvas,
gl_ctx,
texture: None,
vertices: Vec::with_capacity(1024),
indices: Vec::with_capacity(1024),
vertex_length: 0,
index_length: 0,
shader, fragment, vertex, vbo, ebo,
texture_location: None,
texture_mode,
initial_width,
initial_height,
textures: Vec::new(),
})
}
unsafe fn clear(&mut self, col: Color) {
self.gl_ctx.clear_color(col.r, col.g, col.b, col.a);
self.gl_ctx.clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
}
unsafe fn set_blend_mode(&mut self, blend: BlendMode) {
self.gl_ctx.blend_func(gl::ONE, gl::ONE);
self.gl_ctx.blend_equation_separate(blend as u32, gl::FUNC_ADD);
}
unsafe fn reset_blend_mode(&mut self) {
self.gl_ctx.blend_func_separate(
gl::SRC_ALPHA,
gl::ONE_MINUS_SRC_ALPHA,
gl::ONE,
gl::ONE_MINUS_SRC_ALPHA,
);
self.gl_ctx.blend_equation_separate(gl::FUNC_ADD, gl::FUNC_ADD);
}
unsafe fn draw(&mut self, vertices: &[Vertex], triangles: &[GpuTriangle]) -> Result<()> {
vertices.iter().for_each(|vertex| {
self.vertices.push(vertex.pos.x);
self.vertices.push(vertex.pos.y);
let tex_pos = vertex.tex_pos.unwrap_or(Vector::ZERO);
self.vertices.push(tex_pos.x);
self.vertices.push(tex_pos.y);
self.vertices.push(vertex.col.r);
self.vertices.push(vertex.col.g);
self.vertices.push(vertex.col.b);
self.vertices.push(vertex.col.a);
self.vertices.push(if vertex.tex_pos.is_some() { 1f32 } else { 0f32 });
});
let vertex_length = size_of::<f32>() * self.vertices.len();
if vertex_length > self.vertex_length {
self.vertex_length = vertex_length * 2;
self.gl_ctx.buffer_data(gl::ARRAY_BUFFER, self.vertex_length as i64, gl::STREAM_DRAW);
let stride_distance = (VERTEX_SIZE * size_of::<f32>()) as i32;
let pos_attrib = self.gl_ctx.get_attrib_location(&self.shader, "position") as u32;
self.gl_ctx.enable_vertex_attrib_array(pos_attrib);
self.gl_ctx.vertex_attrib_pointer(pos_attrib, 2, gl::FLOAT, false, stride_distance, 0);
let tex_attrib = self.gl_ctx.get_attrib_location(&self.shader, "tex_coord") as u32;
self.gl_ctx.enable_vertex_attrib_array(tex_attrib);
self.gl_ctx.vertex_attrib_pointer(tex_attrib, 2, gl::FLOAT, false, stride_distance, 2 * size_of::<f32>() as i64);
let col_attrib = self.gl_ctx.get_attrib_location(&self.shader, "color") as u32;
self.gl_ctx.enable_vertex_attrib_array(col_attrib);
self.gl_ctx.vertex_attrib_pointer(col_attrib, 4, gl::FLOAT, false, stride_distance, 4 * size_of::<f32>() as i64);
let use_texture_attrib = self.gl_ctx.get_attrib_location(&self.shader, "uses_texture") as u32;
self.gl_ctx.enable_vertex_attrib_array(use_texture_attrib);
self.gl_ctx.vertex_attrib_pointer(use_texture_attrib, 1, gl::FLOAT, false, stride_distance, 8 * size_of::<f32>() as i64);
self.texture_location = Some(try_opt(self.gl_ctx.get_uniform_location(&self.shader, "tex"), "Get texture uniform")?);
}
let array: TypedArray<f32> = self.vertices.as_slice().into();
self.gl_ctx.buffer_sub_data(gl::ARRAY_BUFFER, 0, &array.buffer());
for triangle in triangles.iter() {
if let Some(ref img) = triangle.image {
let should_flush = match self.texture {
Some(val) => img.get_id() != val,
None => true
};
if should_flush {
self.flush();
}
self.texture = Some(img.get_id());
}
self.indices.extend(triangle.indices.iter());
}
self.flush();
self.vertices.clear();
Ok(())
}
unsafe fn flush(&mut self) {
if self.indices.len() != 0 {
let index_length = size_of::<u32>() * self.indices.len();
if index_length > self.index_length {
self.index_length = index_length * 2;
self.gl_ctx.buffer_data(gl::ELEMENT_ARRAY_BUFFER, self.index_length as i64, gl::STREAM_DRAW);
}
let array: TypedArray<u32> = self.indices.as_slice().into();
self.gl_ctx.buffer_sub_data(gl::ELEMENT_ARRAY_BUFFER, 0, &array.buffer());
self.gl_ctx.active_texture(gl::TEXTURE0);
if let Some(index) = self.texture {
self.gl_ctx.bind_texture(gl::TEXTURE_2D, self.textures[index as usize].as_ref());
self.gl_ctx.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, self.texture_mode as i32);
self.gl_ctx.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, self.texture_mode as i32);
}
match self.texture_location {
Some(ref location) => self.gl_ctx.uniform1i(Some(location), 0),
None => self.gl_ctx.uniform1i(None, 0)
}
self.gl_ctx.draw_elements(gl::TRIANGLES, self.indices.len() as i32, gl::UNSIGNED_INT, 0);
}
self.indices.clear();
self.texture = None;
}
unsafe fn create_texture(&mut self, data: &[u8], width: u32, height: u32, format: PixelFormat) -> Result<ImageData> {
let id = self.textures.len() as u32;
let format = match format {
PixelFormat::RGB => gl::RGB as i64,
PixelFormat::RGBA => gl::RGBA as i64
};
let texture = try_opt(self.gl_ctx.create_texture(), "Create GL texture")?;
self.gl_ctx.bind_texture(gl::TEXTURE_2D, Some(&texture));
self.gl_ctx.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
self.gl_ctx.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
self.gl_ctx.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as i32);
self.gl_ctx.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as i32);
let format = format as u32;
self.gl_ctx.tex_image2_d(gl::TEXTURE_2D, 0, gl::RGBA as i32, width as i32, height as i32, 0, format, gl::UNSIGNED_BYTE, Some(data));
self.gl_ctx.generate_mipmap(gl::TEXTURE_2D);
self.textures.push(Some(texture));
Ok(ImageData { id, width, height })
}
unsafe fn destroy_texture(&mut self, data: &mut ImageData) {
self.gl_ctx.delete_texture(self.textures[data.id as usize].as_ref());
}
unsafe fn create_surface(&mut self, image: &Image) -> Result<SurfaceData> {
let surface = SurfaceData {
framebuffer: try_opt(self.gl_ctx.create_framebuffer(), "Create GL framebuffer")?
};
self.gl_ctx.bind_framebuffer(gl::FRAMEBUFFER, Some(&surface.framebuffer));
self.gl_ctx.framebuffer_texture2_d(gl::FRAMEBUFFER, gl::COLOR_ATTACHMENT0, gl::TEXTURE_2D, self.textures[image.get_id() as usize].as_ref(), 0);
self.gl_ctx.draw_buffers(&[gl::COLOR_ATTACHMENT0]);
Ok(surface)
}
unsafe fn bind_surface(&mut self, surface: &Surface) {
self.gl_ctx.bind_framebuffer(gl::FRAMEBUFFER, Some(&surface.data.framebuffer));
self.gl_ctx.viewport(0, 0, surface.image.source_width() as i32, surface.image.source_height() as i32);
}
unsafe fn unbind_surface(&mut self, _surface: &Surface, viewport: &[i32]) {
self.gl_ctx.bind_framebuffer(gl::FRAMEBUFFER, None);
self.gl_ctx.viewport(viewport[0], viewport[1], viewport[2], viewport[3]);
}
unsafe fn destroy_surface(&mut self, surface: &SurfaceData) {
self.gl_ctx.delete_framebuffer(Some(&surface.framebuffer));
}
unsafe fn viewport(&self) -> [i32; 4] {
let viewport_data = self.gl_ctx.get_parameter(gl::VIEWPORT);
[
js! { @{&viewport_data}[0] }.try_into().expect("Malformed GL viewport attribute"),
js! { @{&viewport_data}[1] }.try_into().expect("Malformed GL viewport attribute"),
js! { @{&viewport_data}[2] }.try_into().expect("Malformed GL viewport attribute"),
js! { @{&viewport_data}[3] }.try_into().expect("Malformed GL viewport attribute"),
]
}
unsafe fn set_viewport(&mut self, area: Rectangle) {
self.gl_ctx.viewport(
area.x() as i32,
area.y() as i32,
area.width() as i32,
area.height() as i32
);
}
unsafe fn screenshot(&self, format: PixelFormat) -> (Vector, Vec<u8>) {
let bytes_per_pixel = match format {
PixelFormat::RGBA => 4,
PixelFormat::RGB => 3
};
let format = format_gl(format);
let [x, y, width, height] = self.viewport();
let length = (width * height * bytes_per_pixel) as usize;
let mut buffer: Vec<u8> = Vec::with_capacity(length);
let pointer = buffer.as_slice();
self.gl_ctx.read_pixels(x, y, width, height, format, gl::UNSIGNED_BYTE, Some(pointer));
buffer.set_len(length);
(Vector::new(width, height), buffer)
}
fn set_cursor(&mut self, cursor: MouseCursor) {
js!( @{&self.canvas}.style.cursor = @{cursor.into_css_style()} );
}
fn set_title(&mut self, title: &str) {
document().set_title(title);
}
fn present(&self) -> Result<()> { Ok(()) }
fn set_fullscreen(&mut self, fullscreen: bool) -> Option<Vector> {
let (width, height) = if fullscreen {
let window = stdweb::web::window();
(window.inner_width() as u32, window.inner_height() as u32)
} else {
(self.initial_width, self.initial_height)
};
self.canvas.set_width(width);
self.canvas.set_height(height);
Some(Vector::new(width, height))
}
fn resize(&mut self, size: Vector) {
self.canvas.set_width(size.x as u32);
self.canvas.set_height(size.y as u32);
}
}
impl Drop for WebGLBackend {
fn drop(&mut self) {
self.gl_ctx.delete_program(Some(&self.shader));
self.gl_ctx.delete_shader(Some(&self.fragment));
self.gl_ctx.delete_shader(Some(&self.vertex));
self.gl_ctx.delete_buffer(Some(&self.vbo));
self.gl_ctx.delete_buffer(Some(&self.ebo));
}
}