use sdl2::{render::WindowCanvas, Sdl};
use std::{mem, ptr};
use super::*;
use crate::math::{Vec2, Vec2i};
struct Viewport {
offset: Vec2i,
dim: Vec2i,
}
impl Viewport {
fn new(window_width: i32, window_height: i32) -> Viewport {
const TARGET_ASPECT_RATIO: f32 = 1920. / 1080.;
let aspect_ratio = window_width as f32 / window_height as f32;
let (viewport_width, viewport_height) = if TARGET_ASPECT_RATIO > aspect_ratio {
let new_height = window_width as f32 / TARGET_ASPECT_RATIO;
(window_width, new_height as _)
} else if TARGET_ASPECT_RATIO < aspect_ratio {
let new_width = window_height as f32 * TARGET_ASPECT_RATIO;
(new_width as _, window_height)
} else {
(window_width, window_height)
};
let offset = Vec2i::new(
(window_width as f32 * 0.5 - viewport_width as f32 * 0.5) as _,
(window_height as f32 * 0.5 - viewport_height as f32 * 0.5) as _,
);
let dim = Vec2i::new(viewport_width as _, viewport_height as _);
Self { offset, dim }
}
unsafe fn activate(&self) {
gl::Viewport(self.offset.x, self.offset.y, self.dim.x, self.dim.y);
}
}
pub struct OpenGLRenderer {
canvas: WindowCanvas,
viewport: Viewport,
shader: VertexShader,
render_commands: RenderCommands,
}
impl OpenGLRenderer {
pub fn new(
context: &mut Sdl,
window_width: u32,
window_height: u32,
window_title: &'static str,
max_quads: usize,
) -> Result<Self, String> {
sdl2::image::init(sdl2::image::InitFlag::all())?;
let video = context.video()?;
let canvas = video
.window(window_title, window_width, window_height)
.opengl()
.position_centered()
.resizable()
.build()
.map_err(|e| e.to_string())?
.into_canvas()
.build()
.map_err(|e| e.to_string())?;
gl::load_with(|s| video.gl_get_proc_address(s) as *const _);
canvas
.window()
.gl_set_context_to_current()
.map_err(|e| e.to_string())?;
let render_commands_vert_buffer_id = create_vertex_buffer(max_quads * 4);
let render_commands_index_buffer_id = create_index_buffer(max_quads * 6);
let white_texture_id = load_texture(&[0xFFFFFFFF], 1, 1, true);
let render_commands = RenderCommands::new(
render_commands_vert_buffer_id,
render_commands_index_buffer_id,
white_texture_id,
max_quads,
);
Ok(Self {
canvas,
viewport: Viewport::new(window_width as _, window_height as _),
shader: VertexShader::new()?,
render_commands,
})
}
pub fn set_window_dim(&mut self, window_width: i32, window_height: i32) {
self.viewport = Viewport::new(window_width, window_height);
}
pub fn begin_frame(&mut self) -> &mut RenderCommands {
self.render_commands.reset();
&mut self.render_commands
}
pub fn render(&mut self) {
unsafe {
gl::Enable(gl::CULL_FACE);
gl::Enable(gl::TEXTURE_2D);
gl::Enable(gl::BLEND);
gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);
gl::FrontFace(gl::CCW);
gl::CullFace(gl::BACK);
self.viewport.activate();
self.shader.bind();
for (pos, texture) in self.render_commands.textures.iter().enumerate() {
self.shader.set_sampler(pos, *texture);
}
for command in self.render_commands.commands.iter() {
match command {
RenderCommand::ClearScreen { color } => {
gl::ClearColor(color.x, color.y, color.z, color.w);
gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT);
}
RenderCommand::RenderQuads {
offset,
num_quads,
transform,
} => {
gl::Clear(gl::DEPTH_BUFFER_BIT);
self.shader.set_view_matrix(*transform);
let total_size = num_quads * mem::size_of::<Vertex>() * 4;
let vert_offset = self.render_commands.vertices.as_ptr().add(*offset);
gl::BindBuffer(
gl::ELEMENT_ARRAY_BUFFER,
self.render_commands.index_buffer_id,
);
gl::BindBuffer(gl::ARRAY_BUFFER, self.render_commands.vert_buffer_id);
gl::BufferSubData(gl::ARRAY_BUFFER, 0, total_size as _, vert_offset as _);
let num_indices = *num_quads * 6;
gl::DrawElements(
gl::TRIANGLES,
num_indices as _,
gl::UNSIGNED_INT,
ptr::null(),
);
gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0);
gl::BindBuffer(gl::ARRAY_BUFFER, 0);
}
}
}
gl::UseProgram(0);
gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0);
gl::BindBuffer(gl::ARRAY_BUFFER, 0);
}
self.canvas.present();
}
pub fn load_texture(&mut self, image: &[u32], width: u32, height: u32) -> u32 {
load_texture(image, width, height, false)
}
}
pub struct VertexShader {
program_id: u32,
view_proj_id: i32,
#[allow(dead_code)]
max_textures: i32,
samplers: Vec<i32>,
}
impl VertexShader {
fn new() -> Result<Self, String> {
let program_id = load_shader(VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC)?;
let view_proj_id =
unsafe { gl::GetUniformLocation(program_id, b"u_view_proj\0".as_ptr() as _) };
let max_textures = get_max_textures();
let mut samplers = Vec::with_capacity(max_textures as usize);
let get_texture_slot_name = |id: i32| -> [u8; 15] {
let tens = id / 10;
assert!(tens < 10);
let mut result = *b"u_textures[\0\0\0\0";
let mut next = 11;
if tens > 0 {
result[next] = ('0' as u8) + (tens as u8);
next += 1;
}
result[next] = ('0' as u8) + ((id % 10) as u8);
result[next + 1] = ']' as u8;
result
};
for i in 0..max_textures {
let texture_slot_name = get_texture_slot_name(i);
let uniform_location =
unsafe { gl::GetUniformLocation(program_id, texture_slot_name.as_ptr() as _) };
samplers.push(uniform_location);
}
Ok(Self {
program_id,
view_proj_id,
max_textures,
samplers,
})
}
unsafe fn bind(&self) {
gl::UseProgram(self.program_id);
}
unsafe fn set_sampler(&self, sampler_loc: usize, texture_id: u32) {
assert!(sampler_loc < self.samplers.len());
let sampler = self.samplers[sampler_loc];
gl::ActiveTexture(gl::TEXTURE0 + sampler_loc as u32);
gl::BindTexture(gl::TEXTURE_2D, texture_id);
gl::Uniform1i(sampler, sampler_loc as _);
}
unsafe fn set_view_matrix(&self, m: Mat4) {
gl::UniformMatrix4fv(self.view_proj_id, 1, gl::FALSE, &m as *const _ as *const _);
}
}
pub fn create_vertex_buffer(max_vert_count: usize) -> u32 {
const VERT_SIZE: usize = mem::size_of::<Vertex>();
let buffer_size = (VERT_SIZE * max_vert_count) as isize;
unsafe {
let mut buffer_id = 0;
gl::GenBuffers(1, &mut buffer_id);
gl::BindBuffer(gl::ARRAY_BUFFER, buffer_id);
gl::BufferData(gl::ARRAY_BUFFER, buffer_size, ptr::null(), gl::DYNAMIC_DRAW);
let mut attrib = 0;
let mut total_offset = 0;
gl::EnableVertexAttribArray(attrib); gl::VertexAttribPointer(
attrib,
3,
gl::FLOAT,
gl::FALSE,
VERT_SIZE as _,
total_offset as *const _,
);
attrib += 1;
total_offset += std::mem::size_of::<Vec3>();
gl::EnableVertexAttribArray(attrib); gl::VertexAttribPointer(
attrib,
4,
gl::FLOAT,
gl::FALSE,
VERT_SIZE as _,
total_offset as *const _,
);
attrib += 1;
total_offset += std::mem::size_of::<Vec4>();
gl::EnableVertexAttribArray(attrib); gl::VertexAttribPointer(
attrib,
2,
gl::FLOAT,
gl::FALSE,
VERT_SIZE as _,
total_offset as *const _,
);
attrib += 1;
total_offset += std::mem::size_of::<Vec2>();
gl::EnableVertexAttribArray(attrib); gl::VertexAttribPointer(
attrib,
1,
gl::FLOAT,
gl::FALSE,
VERT_SIZE as _,
total_offset as *const _,
);
buffer_id
}
}
fn get_max_textures() -> i32 {
let mut max_textures = 0;
unsafe { gl::GetIntegerv(gl::MAX_TEXTURE_IMAGE_UNITS, &mut max_textures) };
max_textures
}
pub fn create_index_buffer(max_index_count: usize) -> u32 {
const INDEX_SIZE: usize = std::mem::size_of::<u32>();
let buffer_size = (max_index_count * INDEX_SIZE) as isize;
let index_buffer = {
let max_quads = max_index_count / 6;
let mut buffer = vec![0u32; max_index_count];
for quad in 0..max_quads {
buffer[quad * 6 + 0] = (quad * 4 + 0) as u32;
buffer[quad * 6 + 1] = (quad * 4 + 1) as u32;
buffer[quad * 6 + 2] = (quad * 4 + 2) as u32;
buffer[quad * 6 + 3] = (quad * 4 + 2) as u32;
buffer[quad * 6 + 4] = (quad * 4 + 3) as u32;
buffer[quad * 6 + 5] = (quad * 4 + 0) as u32;
}
buffer
};
unsafe {
let mut buffer_id = 0;
gl::CreateBuffers(1, &mut buffer_id);
gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, buffer_id);
gl::BufferData(
gl::ELEMENT_ARRAY_BUFFER,
buffer_size,
index_buffer.as_ptr() as _,
gl::STATIC_DRAW,
);
gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0);
buffer_id
}
}
pub fn load_texture(image: &[u32], width: u32, height: u32, wrap: bool) -> u32 {
unsafe {
let mut id = 0;
let clamp = if wrap { gl::REPEAT } else { gl::CLAMP_TO_EDGE };
gl::GenTextures(1, &mut id);
gl::BindTexture(gl::TEXTURE_2D, id);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, clamp as _);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, clamp as _);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as _);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as _);
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGBA as _,
width as i32,
height as i32,
0,
gl::RGBA,
gl::UNSIGNED_INT_8_8_8_8,
image.as_ptr() as _,
);
gl::BindTexture(gl::TEXTURE_2D, 0);
id
}
}
fn load_shader(vertex_source: &str, fragment_source: &str) -> Result<u32, String> {
use std::ffi;
unsafe {
let vertex_shader_id = gl::CreateShader(gl::VERTEX_SHADER);
let fragment_shader_id = gl::CreateShader(gl::FRAGMENT_SHADER);
let vertex_shader_src = ffi::CString::new(vertex_source)
.map_err(|_| "Invalid vertex shader source".to_owned())?;
let fragment_shader_src = ffi::CString::new(fragment_source)
.map_err(|_| "Invalid fragment shader source".to_owned())?;
gl::ShaderSource(
vertex_shader_id,
1,
&vertex_shader_src.as_ptr(),
ptr::null(),
);
gl::CompileShader(vertex_shader_id);
check_error_for_id(vertex_shader_id)?;
gl::ShaderSource(
fragment_shader_id,
1,
&fragment_shader_src.as_ptr(),
ptr::null(),
);
gl::CompileShader(fragment_shader_id);
check_error_for_id(fragment_shader_id)?;
let program_id = gl::CreateProgram();
gl::AttachShader(program_id, vertex_shader_id);
gl::AttachShader(program_id, fragment_shader_id);
gl::LinkProgram(program_id);
check_error_for_id(program_id)?;
gl::DetachShader(program_id, vertex_shader_id);
gl::DetachShader(program_id, fragment_shader_id);
gl::DeleteShader(vertex_shader_id);
gl::DeleteShader(fragment_shader_id);
Ok(program_id)
}
}
fn check_error_for_id(id: u32) -> Result<(), String> {
use std::ffi;
let mut log_length = 0;
unsafe {
gl::GetShaderiv(id, gl::INFO_LOG_LENGTH, &mut log_length);
if log_length != 0 {
let mut log = vec![0u8; log_length as usize];
gl::GetShaderInfoLog(id, log_length, ptr::null_mut(), log.as_mut_ptr() as _);
log.pop();
let log = ffi::CString::new(log)
.map_err(|_| "Couldn't generate shader error log".to_owned())?;
return Err(log
.into_string()
.map_err(|_| "Couldn't generate shader error log".to_owned())?);
}
}
Ok(())
}
const VERTEX_SHADER_SRC: &str = r#"
#version 330 core
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec4 a_color;
layout(location = 2) in vec2 a_tex_coord;
layout(location = 3) in float a_texture_id;
uniform mat4 u_view_proj;
out vec4 v_color;
out vec2 v_tex_coord;
out float v_texture_id;
void main() {
gl_Position = u_view_proj * vec4(a_position, 1);
v_color = a_color;
v_tex_coord = a_tex_coord;
v_texture_id = a_texture_id;
}
"#;
const FRAGMENT_SHADER_SRC: &str = r#"
#version 330 core
in vec4 v_color;
in vec2 v_tex_coord;
in float v_texture_id;
uniform sampler2D u_textures[32];
void main() {
// Unfortunately, we can't dynamically index into u_textures[].
// TODO: Turn this into a binary search instead of a large case statement.
vec4 sampled = vec4(1, 1, 1, 1);
switch (int(v_texture_id)) {
case 0: { sampled = texture(u_textures[0], v_tex_coord); } break;
case 1: { sampled = texture(u_textures[1], v_tex_coord); } break;
case 2: { sampled = texture(u_textures[2], v_tex_coord); } break;
case 3: { sampled = texture(u_textures[3], v_tex_coord); } break;
case 4: { sampled = texture(u_textures[4], v_tex_coord); } break;
case 5: { sampled = texture(u_textures[5], v_tex_coord); } break;
case 6: { sampled = texture(u_textures[6], v_tex_coord); } break;
case 7: { sampled = texture(u_textures[7], v_tex_coord); } break;
case 8: { sampled = texture(u_textures[8], v_tex_coord); } break;
case 9: { sampled = texture(u_textures[9], v_tex_coord); } break;
case 10: { sampled = texture(u_textures[10], v_tex_coord); } break;
case 11: { sampled = texture(u_textures[11], v_tex_coord); } break;
case 12: { sampled = texture(u_textures[12], v_tex_coord); } break;
case 13: { sampled = texture(u_textures[13], v_tex_coord); } break;
case 14: { sampled = texture(u_textures[14], v_tex_coord); } break;
case 15: { sampled = texture(u_textures[15], v_tex_coord); } break;
case 16: { sampled = texture(u_textures[16], v_tex_coord); } break;
case 17: { sampled = texture(u_textures[17], v_tex_coord); } break;
case 18: { sampled = texture(u_textures[18], v_tex_coord); } break;
case 19: { sampled = texture(u_textures[19], v_tex_coord); } break;
case 20: { sampled = texture(u_textures[20], v_tex_coord); } break;
case 21: { sampled = texture(u_textures[21], v_tex_coord); } break;
case 22: { sampled = texture(u_textures[22], v_tex_coord); } break;
case 23: { sampled = texture(u_textures[23], v_tex_coord); } break;
case 24: { sampled = texture(u_textures[24], v_tex_coord); } break;
case 25: { sampled = texture(u_textures[25], v_tex_coord); } break;
case 26: { sampled = texture(u_textures[26], v_tex_coord); } break;
case 27: { sampled = texture(u_textures[27], v_tex_coord); } break;
case 28: { sampled = texture(u_textures[28], v_tex_coord); } break;
case 29: { sampled = texture(u_textures[29], v_tex_coord); } break;
case 30: { sampled = texture(u_textures[30], v_tex_coord); } break;
case 31: { sampled = texture(u_textures[31], v_tex_coord); } break;
};
gl_FragColor = sampled * v_color;
}
"#;