use std::rc::Rc;
#[cfg(any(feature = "image-loading", doc, doctest))]
use {
crate::image::ImageFileFormat,
image::GenericImageView,
std::fs::File,
std::io::{BufRead, BufReader, Seek},
std::path::Path
};
use crate::color::Color;
use crate::dimen::{UVec2, Vec2};
use crate::error::{BacktraceError, Context, ErrorMessage};
use crate::font::{FormattedGlyph, FormattedTextBlock};
use crate::font_cache::GlyphCache;
use crate::glwrapper::*;
use crate::image::{ImageDataType, ImageHandle, ImageSmoothingMode};
use crate::{Polygon, RawBitmapData, Rect, Rectangle};
struct AttributeBuffers
{
position: Vec<f32>,
color: Vec<f32>,
texture_coord: Vec<f32>,
texture_mix: Vec<f32>,
circle_mix: Vec<f32>,
glbuf_position: GLBuffer,
glbuf_color: GLBuffer,
glbuf_texture_coord: GLBuffer,
glbuf_texture_mix: GLBuffer,
glbuf_circle_mix: GLBuffer
}
impl AttributeBuffers
{
pub fn new(
context: &GLContextManager,
program: &GLProgram
) -> Result<Self, BacktraceError<ErrorMessage>>
{
Ok(AttributeBuffers {
position: Vec::new(),
color: Vec::new(),
texture_coord: Vec::new(),
texture_mix: Vec::new(),
circle_mix: Vec::new(),
glbuf_position: context
.new_buffer(
GLBufferTarget::Array,
2,
program
.get_attribute_handle(Renderer2D::ATTR_NAME_POSITION)
.context("Failed to get attribute POSITION")?
)
.context("Failed to create buffer for attribute POSITION")?,
glbuf_color: context
.new_buffer(
GLBufferTarget::Array,
4,
program
.get_attribute_handle(Renderer2D::ATTR_NAME_COLOR)
.context("Failed to get attribute COLOR")?
)
.context("Failed to create buffer for attribute COLOR")?,
glbuf_texture_coord: context
.new_buffer(
GLBufferTarget::Array,
2,
program
.get_attribute_handle(Renderer2D::ATTR_NAME_TEXTURE_COORD)
.context("Failed to get attribute TEXTURE_COORD")?
)
.context("Failed to create buffer for attribute TEXTURE_COORD")?,
glbuf_texture_mix: context
.new_buffer(
GLBufferTarget::Array,
1,
program
.get_attribute_handle(Renderer2D::ATTR_NAME_TEXTURE_MIX)
.context("Failed to get attribute TEXTURE_MIX")?
)
.context("Failed to create buffer for attribute TEXTURE_MIX")?,
glbuf_circle_mix: context
.new_buffer(
GLBufferTarget::Array,
1,
program
.get_attribute_handle(Renderer2D::ATTR_NAME_CIRCLE_MIX)
.context("Failed to get attribute CIRCLE_MIX")?
)
.context("Failed to create buffer for attribute CIRCLE_MIX")?
})
}
#[inline]
pub fn get_vertex_count(&self) -> usize
{
self.texture_mix.len()
}
pub fn upload_and_clear(&mut self, context: &GLContextManager)
{
self.glbuf_position.set_data(context, &self.position);
self.glbuf_color.set_data(context, &self.color);
self.glbuf_texture_coord
.set_data(context, &self.texture_coord);
self.glbuf_texture_mix.set_data(context, &self.texture_mix);
self.glbuf_circle_mix.set_data(context, &self.circle_mix);
self.clear();
}
pub fn clear(&mut self)
{
self.position.clear();
self.color.clear();
self.texture_coord.clear();
self.texture_mix.clear();
self.circle_mix.clear();
}
#[inline]
pub fn append(
&mut self,
position: &Vec2,
color: &Color,
texture_coord: &Vec2,
texture_mix: f32,
circle_mix: f32
)
{
AttributeBuffers::push_vec2(&mut self.position, position);
AttributeBuffers::push_color(&mut self.color, color);
AttributeBuffers::push_vec2(&mut self.texture_coord, texture_coord);
self.texture_mix.push(texture_mix);
self.circle_mix.push(circle_mix);
}
#[inline]
fn push_vec2(dest: &mut Vec<f32>, vertices: &Vec2)
{
dest.push(vertices.x);
dest.push(vertices.y);
}
#[inline]
fn push_color(dest: &mut Vec<f32>, color: &Color)
{
dest.push(color.r());
dest.push(color.g());
dest.push(color.b());
dest.push(color.a());
}
}
struct Uniforms
{
scale_x: GLUniformHandle,
scale_y: GLUniformHandle,
texture: GLUniformHandle
}
impl Uniforms
{
fn new(
context: &GLContextManager,
program: &Rc<GLProgram>
) -> Result<Uniforms, BacktraceError<ErrorMessage>>
{
Ok(Uniforms {
scale_x: program
.get_uniform_handle(context, Renderer2D::UNIFORM_NAME_SCALE_X)
.context("Failed to find SCALE_X uniform")?,
scale_y: program
.get_uniform_handle(context, Renderer2D::UNIFORM_NAME_SCALE_Y)
.context("Failed to find SCALE_Y uniform")?,
texture: program
.get_uniform_handle(context, Renderer2D::UNIFORM_NAME_TEXTURE)
.context("Failed to find TEXTURE uniform")?
})
}
fn set_viewport_size_pixels(
&self,
context: &GLContextManager,
viewport_size_pixels: UVec2
)
{
self.scale_x
.set_value_float(context, 2.0 / viewport_size_pixels.x as f32);
self.scale_y
.set_value_float(context, -2.0 / viewport_size_pixels.y as f32);
}
fn set_texture_unit(&self, context: &GLContextManager, texture_unit: i32)
{
self.texture.set_value_int(context, texture_unit);
}
}
pub(crate) struct Renderer2DVertex
{
pub position: Vec2,
pub texture_coord: Vec2,
pub color: Color,
pub texture_mix: f32,
pub circle_mix: f32
}
impl Renderer2DVertex
{
#[inline]
fn append_to_attribute_buffers(&self, attribute_buffers: &mut AttributeBuffers)
{
attribute_buffers.append(
&self.position,
&self.color,
&self.texture_coord,
self.texture_mix,
self.circle_mix
);
}
}
pub(crate) struct Renderer2DAction
{
pub texture: Option<GLTexture>,
pub vertices_clockwise: [Renderer2DVertex; 3]
}
impl Renderer2DAction
{
#[inline]
fn update_current_texture_if_empty(
&self,
current_texture: &mut Option<GLTexture>
) -> bool
{
match &self.texture {
None => true,
Some(own_texture) => match current_texture {
None => {
*current_texture = Some(own_texture.clone());
true
}
Some(current_texture) => *current_texture == *own_texture
}
}
}
#[inline]
fn append_to_attribute_buffers(&self, attribute_buffers: &mut AttributeBuffers)
{
for vertex in self.vertices_clockwise.iter() {
vertex.append_to_attribute_buffers(attribute_buffers);
}
}
}
enum RenderQueueItem
{
FormattedTextBlock
{
position: Vec2,
color: Color,
block: FormattedTextBlock
},
FormattedTextGlyph
{
position: Vec2,
color: Color,
glyph: FormattedGlyph,
crop_window: Rect
},
CircleSectionColored
{
vertex_positions_clockwise: [Vec2; 3],
vertex_colors_clockwise: [Color; 3],
vertex_normalized_circle_coords_clockwise: [Vec2; 3]
},
TriangleColored
{
vertex_positions_clockwise: [Vec2; 3],
vertex_colors_clockwise: [Color; 3]
},
TriangleTextured
{
vertex_positions_clockwise: [Vec2; 3],
vertex_colors_clockwise: [Color; 3],
vertex_texture_coords_clockwise: [Vec2; 3],
texture: GLTexture
}
}
impl RenderQueueItem
{
#[inline]
fn generate_actions(
&self,
glyph_cache: &GlyphCache,
runner: &mut impl FnMut(Renderer2DAction)
)
{
match self {
RenderQueueItem::FormattedTextBlock {
position,
color,
block
} => {
for line in block.iter_lines() {
for glyph in line.iter_glyphs() {
glyph_cache.get_renderer2d_actions(
glyph, *position, *color, None, runner
);
}
}
}
RenderQueueItem::FormattedTextGlyph {
glyph,
position,
color,
crop_window
} => {
glyph_cache.get_renderer2d_actions(
glyph,
*position,
*color,
Some(crop_window),
runner
);
}
RenderQueueItem::CircleSectionColored {
vertex_positions_clockwise,
vertex_colors_clockwise,
vertex_normalized_circle_coords_clockwise
} => runner(Renderer2DAction {
texture: None,
vertices_clockwise: [
Renderer2DVertex {
position: vertex_positions_clockwise[0],
texture_coord: vertex_normalized_circle_coords_clockwise[0],
color: vertex_colors_clockwise[0],
texture_mix: 0.0,
circle_mix: 1.0
},
Renderer2DVertex {
position: vertex_positions_clockwise[1],
texture_coord: vertex_normalized_circle_coords_clockwise[1],
color: vertex_colors_clockwise[1],
texture_mix: 0.0,
circle_mix: 1.0
},
Renderer2DVertex {
position: vertex_positions_clockwise[2],
texture_coord: vertex_normalized_circle_coords_clockwise[2],
color: vertex_colors_clockwise[2],
texture_mix: 0.0,
circle_mix: 1.0
}
]
}),
RenderQueueItem::TriangleColored {
vertex_positions_clockwise,
vertex_colors_clockwise
} => runner(Renderer2DAction {
texture: None,
vertices_clockwise: [
Renderer2DVertex {
position: vertex_positions_clockwise[0],
texture_coord: Vec2::ZERO,
color: vertex_colors_clockwise[0],
texture_mix: 0.0,
circle_mix: 0.0
},
Renderer2DVertex {
position: vertex_positions_clockwise[1],
texture_coord: Vec2::ZERO,
color: vertex_colors_clockwise[1],
texture_mix: 0.0,
circle_mix: 0.0
},
Renderer2DVertex {
position: vertex_positions_clockwise[2],
texture_coord: Vec2::ZERO,
color: vertex_colors_clockwise[2],
texture_mix: 0.0,
circle_mix: 0.0
}
]
}),
RenderQueueItem::TriangleTextured {
vertex_positions_clockwise,
vertex_colors_clockwise,
vertex_texture_coords_clockwise,
texture
} => runner(Renderer2DAction {
texture: Some(texture.clone()),
vertices_clockwise: [
Renderer2DVertex {
position: vertex_positions_clockwise[0],
texture_coord: vertex_texture_coords_clockwise[0],
color: vertex_colors_clockwise[0],
texture_mix: 1.0,
circle_mix: 0.0
},
Renderer2DVertex {
position: vertex_positions_clockwise[1],
texture_coord: vertex_texture_coords_clockwise[1],
color: vertex_colors_clockwise[1],
texture_mix: 1.0,
circle_mix: 0.0
},
Renderer2DVertex {
position: vertex_positions_clockwise[2],
texture_coord: vertex_texture_coords_clockwise[2],
color: vertex_colors_clockwise[2],
texture_mix: 1.0,
circle_mix: 0.0
}
]
})
}
}
}
pub struct Renderer2D
{
context: GLContextManager,
program: Rc<GLProgram>,
render_queue: Vec<RenderQueueItem>,
glyph_cache: GlyphCache,
attribute_buffers: AttributeBuffers,
current_texture: Option<GLTexture>,
#[allow(dead_code)]
uniforms: Uniforms
}
impl Renderer2D
{
const ATTR_NAME_POSITION: &'static str = "in_Position";
const ATTR_NAME_COLOR: &'static str = "in_Color";
const ATTR_NAME_TEXTURE_COORD: &'static str = "in_TextureCoord";
const ATTR_NAME_TEXTURE_MIX: &'static str = "in_TextureMix";
const ATTR_NAME_CIRCLE_MIX: &'static str = "in_CircleMix";
const UNIFORM_NAME_SCALE_X: &'static str = "in_ScaleX";
const UNIFORM_NAME_SCALE_Y: &'static str = "in_ScaleY";
const UNIFORM_NAME_TEXTURE: &'static str = "in_Texture";
const ALL_ATTRIBUTES: [&'static str; 5] = [
Renderer2D::ATTR_NAME_POSITION,
Renderer2D::ATTR_NAME_COLOR,
Renderer2D::ATTR_NAME_TEXTURE_COORD,
Renderer2D::ATTR_NAME_TEXTURE_MIX,
Renderer2D::ATTR_NAME_CIRCLE_MIX
];
pub fn new(
context: &GLContextManager,
viewport_size_pixels: UVec2
) -> Result<Self, BacktraceError<ErrorMessage>>
{
log::info!("Creating vertex shader");
let (vertex_shader_src, fragment_shader_src) = match context.version() {
GLVersion::OpenGL2_0 => {
log::info!("Using OpenGL 2.0 shaders");
(
include_str!("shaders/r2d_vertex_v110.glsl"),
include_str!("shaders/r2d_fragment_v110.glsl")
)
}
GLVersion::WebGL2_0 => {
log::info!("Using WebGL 2.0 shaders");
(
include_str!("shaders/r2d_vertex_v300es.glsl"),
include_str!("shaders/r2d_fragment_v300es.glsl")
)
}
};
let vertex_shader = context
.new_shader(GLShaderType::Vertex, vertex_shader_src)
.context("Failed to create Renderer2D vertex shader")?;
log::info!("Creating fragment shader");
let fragment_shader = context
.new_shader(GLShaderType::Fragment, fragment_shader_src)
.context("Failed to create Renderer2D fragment shader")?;
log::info!("Compiling program");
let program = context
.new_program(
&vertex_shader,
&fragment_shader,
&Renderer2D::ALL_ATTRIBUTES
)
.context("Failed to create Renderer2D program")?;
let attribute_buffers = AttributeBuffers::new(context, &program)?;
let uniforms = Uniforms::new(context, &program)?;
context.use_program(&program);
uniforms.set_texture_unit(context, 0);
uniforms.set_viewport_size_pixels(context, viewport_size_pixels);
context.set_viewport_size(viewport_size_pixels);
Ok(Renderer2D {
context: context.clone(),
program,
render_queue: Vec::new(),
glyph_cache: GlyphCache::new(),
attribute_buffers,
current_texture: None,
uniforms
})
}
pub fn set_viewport_size_pixels(&self, viewport_size_pixels: UVec2)
{
self.uniforms
.set_viewport_size_pixels(&self.context, viewport_size_pixels);
self.context.set_viewport_size(viewport_size_pixels);
}
pub fn finish_frame(&mut self)
{
self.flush_render_queue();
self.glyph_cache.on_new_frame_start();
}
fn flush_render_queue(&mut self)
{
if self.render_queue.is_empty() {
return;
}
self.attribute_buffers.clear();
let mut has_text = false;
for item in &self.render_queue {
match item {
RenderQueueItem::FormattedTextBlock {
block, position, ..
} => {
for line in block.iter_lines() {
for glyph in line.iter_glyphs() {
self.glyph_cache.add_to_cache(
&self.context,
glyph,
*position
);
}
}
has_text = true;
}
RenderQueueItem::FormattedTextGlyph {
glyph, position, ..
} => {
self.glyph_cache
.add_to_cache(&self.context, glyph, *position);
has_text = true;
}
RenderQueueItem::CircleSectionColored { .. }
| RenderQueueItem::TriangleColored { .. }
| RenderQueueItem::TriangleTextured { .. } => {}
}
}
if has_text {
if let Err(err) = self.glyph_cache.prepare_for_draw(&self.context) {
log::error!("Error updating font texture, continuing anyway: {:?}", err);
}
}
{
let current_texture = &mut self.current_texture;
let context = &self.context;
let program = &self.program;
let attribute_buffers = &mut self.attribute_buffers;
for item in &self.render_queue {
item.generate_actions(&self.glyph_cache, &mut |action| {
if !action.update_current_texture_if_empty(current_texture) {
Renderer2D::draw_buffers(
context,
program,
attribute_buffers,
current_texture
);
current_texture.clone_from(&action.texture);
}
action.append_to_attribute_buffers(attribute_buffers);
});
}
}
self.render_queue.clear();
Renderer2D::draw_buffers(
&self.context,
&self.program,
&mut self.attribute_buffers,
&mut self.current_texture
);
}
fn draw_buffers(
context: &GLContextManager,
program: &Rc<GLProgram>,
attribute_buffers: &mut AttributeBuffers,
current_texture: &mut Option<GLTexture>
)
{
let vertex_count = attribute_buffers.get_vertex_count();
if vertex_count == 0 {
return;
}
context.use_program(program);
attribute_buffers.upload_and_clear(context);
let current_texture = current_texture.take();
match ¤t_texture {
None => context.unbind_texture(),
Some(texture) => context.bind_texture(texture)
}
context.draw_triangles(
GLBlendEnabled::Enabled(GLBlendMode::OneMinusSrcAlpha),
vertex_count
);
}
pub(crate) fn create_image_from_raw_pixels<S: Into<UVec2>>(
&self,
data_type: ImageDataType,
smoothing_mode: ImageSmoothingMode,
size: S,
data: &[u8]
) -> Result<ImageHandle, BacktraceError<ErrorMessage>>
{
let size = size.into();
let pixel_bytes = match data_type {
ImageDataType::RGB => 3,
ImageDataType::RGBA => 4
};
{
let expected_bytes = pixel_bytes * size.x as usize * size.y as usize;
if expected_bytes != data.len() {
return Err(ErrorMessage::msg(format!(
"Expecting {} bytes ({}x{}x{}), got {}",
expected_bytes,
size.x,
size.y,
pixel_bytes,
data.len()
)));
}
}
let gl_format = data_type.into();
let gl_smoothing = match smoothing_mode {
ImageSmoothingMode::NearestNeighbor => GLTextureSmoothing::NearestNeighbour,
ImageSmoothingMode::Linear => GLTextureSmoothing::Linear
};
let texture = self
.context
.new_texture()
.context("Failed to create GPU texture")?;
texture
.set_image_data(&self.context, gl_format, gl_smoothing, &size, data)
.context("Failed to upload image data")?;
Ok(ImageHandle { size, texture })
}
#[cfg(any(feature = "image-loading", doc, doctest))]
pub fn create_image_from_file_path<P: AsRef<Path>>(
&mut self,
data_type: Option<ImageFileFormat>,
smoothing_mode: ImageSmoothingMode,
path: P
) -> Result<ImageHandle, BacktraceError<ErrorMessage>>
{
let file = File::open(path.as_ref()).context(format!(
"Failed to open file '{:?}' for reading",
path.as_ref()
))?;
self.create_image_from_file_bytes(data_type, smoothing_mode, BufReader::new(file))
}
#[cfg(any(feature = "image-loading", doc, doctest))]
pub fn create_image_from_file_bytes<R: Seek + BufRead>(
&mut self,
data_type: Option<ImageFileFormat>,
smoothing_mode: ImageSmoothingMode,
file_bytes: R
) -> Result<ImageHandle, BacktraceError<ErrorMessage>>
{
let mut reader = image::io::Reader::new(file_bytes);
match data_type {
None => {
reader = reader
.with_guessed_format()
.context("Could not guess file format")?
}
Some(format) => reader.set_format(match format {
ImageFileFormat::PNG => image::ImageFormat::Png,
ImageFileFormat::JPEG => image::ImageFormat::Jpeg,
ImageFileFormat::GIF => image::ImageFormat::Gif,
ImageFileFormat::BMP => image::ImageFormat::Bmp,
ImageFileFormat::ICO => image::ImageFormat::Ico,
ImageFileFormat::TIFF => image::ImageFormat::Tiff,
ImageFileFormat::WebP => image::ImageFormat::WebP,
ImageFileFormat::AVIF => image::ImageFormat::Avif,
ImageFileFormat::PNM => image::ImageFormat::Pnm,
ImageFileFormat::DDS => image::ImageFormat::Dds,
ImageFileFormat::TGA => image::ImageFormat::Tga,
ImageFileFormat::Farbfeld => image::ImageFormat::Farbfeld
})
}
let image = reader.decode().context("Failed to parse image data")?;
let dimensions = image.dimensions();
let bytes_rgba8 = image.into_rgba8().into_raw();
self.create_image_from_raw_pixels(
ImageDataType::RGBA,
smoothing_mode,
dimensions,
bytes_rgba8.as_slice()
)
}
#[inline]
pub(crate) fn clear_screen(&mut self, color: Color)
{
if color.a() < 1.0 {
self.flush_render_queue();
} else {
self.render_queue.clear();
}
self.context.clear_screen(color);
}
#[inline]
fn add_to_render_queue(&mut self, item: RenderQueueItem)
{
self.render_queue.push(item);
if self.render_queue.len() > 100000 {
self.flush_render_queue();
}
}
#[inline]
pub(crate) fn draw_polygon<V: Into<Vec2>>(
&mut self,
polygon: &Polygon,
offset: V,
color: Color
)
{
let color = [color; 3];
let offset = offset.into();
for triangle in polygon.triangles.iter() {
let triangle = triangle.map(|vertex| vertex + offset);
self.draw_triangle_three_color(triangle, color);
}
}
#[inline]
pub(crate) fn draw_triangle_three_color(
&mut self,
vertex_positions_clockwise: [Vec2; 3],
vertex_colors_clockwise: [Color; 3]
)
{
self.add_to_render_queue(RenderQueueItem::TriangleColored {
vertex_positions_clockwise,
vertex_colors_clockwise
})
}
#[inline]
pub(crate) fn draw_triangle_image_tinted(
&mut self,
vertex_positions_clockwise: [Vec2; 3],
vertex_colors_clockwise: [Color; 3],
vertex_texture_coords_clockwise: [Vec2; 3],
image: &ImageHandle
)
{
self.add_to_render_queue(RenderQueueItem::TriangleTextured {
vertex_positions_clockwise,
vertex_colors_clockwise,
vertex_texture_coords_clockwise,
texture: image.texture.clone()
})
}
#[inline]
pub(crate) fn draw_text<V: Into<Vec2>>(
&mut self,
position: V,
color: Color,
text: &FormattedTextBlock
)
{
self.add_to_render_queue(RenderQueueItem::FormattedTextBlock {
position: position.into(),
color,
block: text.clone()
})
}
#[inline]
pub(crate) fn draw_text_cropped<V: Into<Vec2>>(
&mut self,
position: V,
crop_window: Rect,
color: Color,
text: &FormattedTextBlock
)
{
let position = position.into();
for line in text.iter_lines() {
for glyph in line.iter_glyphs() {
if let Some(glyph_outline) = glyph.pixel_bounding_box() {
let glyph_outline = glyph_outline.with_offset(position);
if glyph_outline.intersect(&crop_window).is_some() {
self.add_to_render_queue(RenderQueueItem::FormattedTextGlyph {
position,
color,
glyph: glyph.clone(),
crop_window: crop_window.clone()
})
}
}
}
}
}
#[inline]
pub(crate) fn draw_circle_section(
&mut self,
vertex_positions_clockwise: [Vec2; 3],
vertex_colors_clockwise: [Color; 3],
vertex_normalized_circle_coords_clockwise: [Vec2; 3]
)
{
self.add_to_render_queue(RenderQueueItem::CircleSectionColored {
vertex_positions_clockwise,
vertex_colors_clockwise,
vertex_normalized_circle_coords_clockwise
})
}
#[inline]
pub(crate) fn set_clip(&mut self, rect: Option<Rectangle<i32>>)
{
self.flush_render_queue();
match rect {
None => self.context.set_enable_scissor(false),
Some(rect) => {
self.context.set_enable_scissor(true);
self.context.set_clip(
rect.top_left().x,
rect.top_left().y,
rect.width(),
rect.height()
)
}
}
}
pub(crate) fn capture(&mut self, format: ImageDataType) -> RawBitmapData
{
self.flush_render_queue();
self.context.capture(format)
}
}