From 070108cf09b1b561613b6eea04723afbbb464507 Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Sun, 3 Mar 2024 00:10:10 -0500 Subject: Initial commit (new winit) --- src/assets/fonts/font1.png | Bin 0 -> 1508 bytes src/assets/fonts/font2.png | Bin 0 -> 1883 bytes src/assets/fonts/simple.png | Bin 0 -> 1042 bytes src/assets/shaders/common/frag.glsl | 158 ++++++++++++++++++++++ src/assets/shaders/common/vert.glsl | 30 +++++ src/assets/shaders/scale/frag.glsl | 13 ++ src/assets/shaders/scale/vert.glsl | 22 +++ src/assets/shaders/test/frag.glsl | 23 ++++ src/assets/shaders/test/vert.glsl | 4 + src/assets/shaders/text/frag.glsl | 55 ++++++++ src/assets/shaders/text/vert.glsl | 26 ++++ src/audio.rs | 96 +++++++++++++ src/context.rs | 156 +++++++++++++++++++++ src/font.rs | 82 +++++++++++ src/framebuffer.rs | 100 ++++++++++++++ src/lib.rs | 94 +++++++++++++ src/mesh.rs | 118 ++++++++++++++++ src/request.rs | 20 +++ src/shader.rs | 171 +++++++++++++++++++++++ src/state.rs | 262 ++++++++++++++++++++++++++++++++++++ src/texture.rs | 51 +++++++ src/utils.rs | 58 ++++++++ 22 files changed, 1539 insertions(+) create mode 100644 src/assets/fonts/font1.png create mode 100644 src/assets/fonts/font2.png create mode 100644 src/assets/fonts/simple.png create mode 100644 src/assets/shaders/common/frag.glsl create mode 100644 src/assets/shaders/common/vert.glsl create mode 100644 src/assets/shaders/scale/frag.glsl create mode 100644 src/assets/shaders/scale/vert.glsl create mode 100644 src/assets/shaders/test/frag.glsl create mode 100644 src/assets/shaders/test/vert.glsl create mode 100644 src/assets/shaders/text/frag.glsl create mode 100644 src/assets/shaders/text/vert.glsl create mode 100644 src/audio.rs create mode 100644 src/context.rs create mode 100644 src/font.rs create mode 100644 src/framebuffer.rs create mode 100644 src/lib.rs create mode 100644 src/mesh.rs create mode 100644 src/request.rs create mode 100644 src/shader.rs create mode 100644 src/state.rs create mode 100644 src/texture.rs create mode 100644 src/utils.rs (limited to 'src') diff --git a/src/assets/fonts/font1.png b/src/assets/fonts/font1.png new file mode 100644 index 0000000..ec06424 Binary files /dev/null and b/src/assets/fonts/font1.png differ diff --git a/src/assets/fonts/font2.png b/src/assets/fonts/font2.png new file mode 100644 index 0000000..8435cad Binary files /dev/null and b/src/assets/fonts/font2.png differ diff --git a/src/assets/fonts/simple.png b/src/assets/fonts/simple.png new file mode 100644 index 0000000..7b1d2a3 Binary files /dev/null and b/src/assets/fonts/simple.png differ diff --git a/src/assets/shaders/common/frag.glsl b/src/assets/shaders/common/frag.glsl new file mode 100644 index 0000000..908e06c --- /dev/null +++ b/src/assets/shaders/common/frag.glsl @@ -0,0 +1,158 @@ +#version 300 es +precision highp float; + +uniform vec3 camera_pos; +uniform float time; + +uniform vec3 light_ambient_color; +uniform vec3 light_dir; +uniform vec3 light_dir_color; +uniform int light_count; +uniform vec3 light_pos[5]; +uniform vec3 light_color[5]; +uniform vec2 light_attenuation[5]; +uniform highp sampler2DShadow light_shadowbuffer_dir; +uniform samplerCube light_shadowbuffer_point[5]; + +uniform int has_point_shadows; + +in vec2 vertex_texcoord; +in vec3 vertex_normal; +in vec3 vertex_fragpos; +in vec4 vertex_fragpos_shadow_dir; +in vec3 vertex_view_vector; + +out vec4 frag_color; + +mat3 compute_tbn() { + vec3 p = -vertex_view_vector; + vec3 normal = normalize(vertex_normal); + vec3 dpx = dFdx(p); + vec3 dpy = dFdy(p); + vec2 duvx = dFdx(vertex_texcoord); + vec2 duvy = dFdy(vertex_texcoord); + vec3 dpyperp = cross(dpy, normal); + vec3 dpxperp = cross(normal, dpx); + vec3 tangent = dpyperp * duvx.x + dpxperp * duvy.x; + vec3 bitangent = dpyperp * duvx.y + dpxperp * duvy.y; + float invmax = inversesqrt(max(dot(bitangent, bitangent), dot(bitangent, bitangent))); + return mat3(tangent * invmax, bitangent * invmax, normal); +} + +vec4 normal_as_color(vec3 n) { + float r = (128.0 + 127.0 * n.r) / 255.0; + float g = (128.0 + 127.0 * n.g) / 255.0; + float b = (128.0 + 127.0 * n.b) / 255.0; + return vec4(r, g, b, 1.0); +} + +vec3 dir_light(vec3 normal) { + return max(dot(normal, -normalize(light_dir)), 0.0) * light_dir_color; +} + +float dir_shadow(vec3 normal) { + vec3 proj = vertex_fragpos_shadow_dir.xyz / vertex_fragpos_shadow_dir.w; + float bias = 0.002; + // float current_depth = proj.z; + // float bias = max(0.05 * (1.0 - dot(normal, -normalize(light_dir))), 0.005); + proj.z -= bias; + proj *= 0.5; proj += 0.5; + if (proj.z > 1.0) return 0.0; + return 1.0 - texture(light_shadowbuffer_dir, proj.xyz); +} + +float point_shadow(vec3 normal, vec3 shadow_vector, float closest_depth) { + closest_depth *= 25.0; + float current_depth = length(shadow_vector); + float bias = max(0.1 * (1.0 - dot(normal, normalize(shadow_vector))), 0.005); + bias = min(bias + current_depth * 0.01, 0.2); + float shadow = current_depth - bias > closest_depth ? 1.0 : 0.0; + return shadow; +} + +vec3 point_light(vec3 normal, const int idx) { + vec3 pos = light_pos[idx]; + vec3 color = light_color[idx]; + float linear = light_attenuation[idx].x; + float quadratic = light_attenuation[idx].y; + vec3 light_vector = pos - vertex_fragpos; + float distance = length(light_vector); + float attenuation = 1.0 / (1.0 + distance * linear + distance * distance * quadratic); + + float directional = max(dot(normal.xyz, normalize(light_vector)), 0.0); + vec3 directional_light = color * directional; + + vec3 view_dir = normalize(camera_pos - vertex_fragpos); + vec3 reflect_dir = reflect(-normalize(light_vector), normalize(normal.xyz)); + float specular = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + vec3 specular_light = 0.5 * specular * color; + return (directional_light + specular_light) * attenuation; +} + +vec3 point_light_billboard(const int idx) { + vec3 pos = light_pos[idx]; + vec3 color = light_color[idx]; + float linear = light_attenuation[idx].x; + float quadratic = light_attenuation[idx].y; + vec3 light_vector = pos - vertex_fragpos; + float distance = length(light_vector); + float attenuation = 1.0 / (1.0 + distance * linear + distance * distance * quadratic); + + return color * attenuation; +} + +vec3 compute_lighting(vec3 normal) { + vec3 ambient_light = light_ambient_color; + + vec3 from_dir = dir_light(normal) * (1.0 - dir_shadow(normal)); + + vec3 shadow_vector[5]; + for (int i = 0; i < light_count; ++i) { + shadow_vector[i] = vertex_fragpos - light_pos[i]; + shadow_vector[i].x *= -1.0; + } + + // cannot only index array of samplers with a constant, hence the weird setup + #define SAMPLE_SHADOW(n) n < light_count ? texture(light_shadowbuffer_point[n], shadow_vector[n]).r : 1.0 + float shadow_depth[5]; + shadow_depth[0] = SAMPLE_SHADOW(0); + shadow_depth[1] = SAMPLE_SHADOW(1); + shadow_depth[2] = SAMPLE_SHADOW(2); + shadow_depth[3] = SAMPLE_SHADOW(3); + shadow_depth[4] = SAMPLE_SHADOW(4); + + vec3 from_points = vec3(0.0, 0.0, 0.0); + for (int i = 0; i < light_count; ++i) { + from_points += has_point_shadows != 0 + ? point_light(normal, i) * (1.0 - point_shadow(normal, shadow_vector[i], shadow_depth[i])) + : point_light(normal, i); + } + + return (ambient_light + from_dir + from_points); +} + +vec3 compute_lighting_noshadow(vec3 normal) { + vec3 ambient_light = light_ambient_color; + + vec3 from_dir = dir_light(normal); + + vec3 from_points = vec3(0.0, 0.0, 0.0); + for (int i = 0; i < light_count; ++i) { + from_points += point_light(normal, i); + } + + return (ambient_light + from_dir + from_points); +} + +vec3 compute_lighting_billboard(vec3 normal) { + vec3 ambient_light = light_ambient_color; + + vec3 from_dir = light_dir_color / 2.0; + + vec3 from_points = vec3(0.0, 0.0, 0.0); + for (int i = 0; i < light_count; ++i) { + from_points += point_light_billboard(i); + } + + return (ambient_light + from_dir + from_points); +} diff --git a/src/assets/shaders/common/vert.glsl b/src/assets/shaders/common/vert.glsl new file mode 100644 index 0000000..b8e11c5 --- /dev/null +++ b/src/assets/shaders/common/vert.glsl @@ -0,0 +1,30 @@ +#version 300 es +precision highp float; + +in vec3 vertex; +in vec3 normal; +in vec2 texcoord; + +uniform mat4 view; +uniform mat4 position; +uniform mat4 projection; +uniform mat4 normal_matrix; +uniform mat4 lightspace_matrix; +uniform vec3 camera_pos; + +out vec2 vertex_texcoord; +out vec3 vertex_normal; +out vec3 vertex_fragpos; +out vec4 vertex_fragpos_shadow_dir; +out vec3 vertex_view_vector; + +void default_main() +{ + vertex_texcoord = texcoord; + vertex_normal = (normal_matrix * vec4(normal, 1.0)).xyz; + vec3 pos = (position * vec4(vertex, 1.0)).xyz; + vertex_fragpos = pos; + vertex_fragpos_shadow_dir = lightspace_matrix * vec4(pos, 1.0); + vertex_view_vector = camera_pos - pos; + gl_Position = projection * view * vec4(pos, 1.0); +} diff --git a/src/assets/shaders/scale/frag.glsl b/src/assets/shaders/scale/frag.glsl new file mode 100644 index 0000000..2cd3c60 --- /dev/null +++ b/src/assets/shaders/scale/frag.glsl @@ -0,0 +1,13 @@ +#version 300 es +precision highp float; + +uniform sampler2D texture_data; + +in vec2 vertex_texcoord; +out vec4 frag_color; + +void main() +{ + vec4 texel = texture(texture_data, vertex_texcoord); + frag_color = texel; +} \ No newline at end of file diff --git a/src/assets/shaders/scale/vert.glsl b/src/assets/shaders/scale/vert.glsl new file mode 100644 index 0000000..e05bbb6 --- /dev/null +++ b/src/assets/shaders/scale/vert.glsl @@ -0,0 +1,22 @@ +#version 300 es +precision highp float; + +out vec2 vertex_texcoord; + +void main() { + const vec2 positions[4] = vec2[]( + vec2(-1, -1), + vec2(+1, -1), + vec2(-1, +1), + vec2(+1, +1) + ); + const vec2 coords[4] = vec2[]( + vec2(0, 0), + vec2(1, 0), + vec2(0, 1), + vec2(1, 1) + ); + + vertex_texcoord = coords[gl_VertexID]; + gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0); +} diff --git a/src/assets/shaders/test/frag.glsl b/src/assets/shaders/test/frag.glsl new file mode 100644 index 0000000..a52aa15 --- /dev/null +++ b/src/assets/shaders/test/frag.glsl @@ -0,0 +1,23 @@ +// uniform int has_normal_map; +// uniform sampler2D normal_map; + +uniform sampler2D texture_data; + +void main() +{ + vec2 inverted_texcoord = vec2(vertex_texcoord.x, 1.0 - vertex_texcoord.y); + vec4 texel = texture(texture_data, inverted_texcoord); + if (texel.a != 1.0) { + discard; + } + + // mat3 tbn = compute_tbn(); + // vec3 normal = has_normal_map != 0 + // ? normalize(tbn * (texture(normal_map, inverted_texcoord).xyz * 2.0 - 1.0)) + // : normalize(vertex_normal); + vec3 normal = normalize(vertex_normal); + + vec3 lighting = compute_lighting_noshadow(normal); + + frag_color = vec4(texel.rgb * lighting, texel.a); +} diff --git a/src/assets/shaders/test/vert.glsl b/src/assets/shaders/test/vert.glsl new file mode 100644 index 0000000..e324f7e --- /dev/null +++ b/src/assets/shaders/test/vert.glsl @@ -0,0 +1,4 @@ +void main() +{ + default_main(); +} \ No newline at end of file diff --git a/src/assets/shaders/text/frag.glsl b/src/assets/shaders/text/frag.glsl new file mode 100644 index 0000000..3a1694f --- /dev/null +++ b/src/assets/shaders/text/frag.glsl @@ -0,0 +1,55 @@ +#version 300 es +precision highp float; + +uniform sampler2D texture_data; +uniform int text_length; +uniform int text[256]; +uniform int char_width; +uniform int char_height; +uniform int font_width; +uniform int font_height; +uniform int text_width; +uniform int text_height; + +in vec2 vertex_texcoord; +out vec4 frag_color; + +void main() +{ + vec2 inverted_texcoord = vec2(vertex_texcoord.x, 1.0 - vertex_texcoord.y); + vec2 texcoord_pixels = inverted_texcoord * vec2(float(text_width), float(text_height)); + int texcoord_char_x = int(floor(texcoord_pixels.x)) / char_width; + int texcoord_char_y = int(floor(texcoord_pixels.y)) / char_height; + + int x = 0; + int y = 0; + int i = 0; + for (; i < text_length; ++i) { + if (x == texcoord_char_x && y == texcoord_char_y) { + break; + } + if (text[i] == 10) { + x = 0; + y += 1; + } else { + x += 1; + } + } + if (i == text_length || text[i] == 10) discard; + + int entry = text[i] - 32; + vec2 texcoord_base = vec2( + float(entry % (font_width / char_width)) * float(char_width), + float(entry / (font_width / char_width)) * float(char_height) + ); + // vec2 texcoord_base = vec2(8.0, 0.0); + vec2 texcoord_off = vec2( + mod(texcoord_pixels.x, float(char_width)), + mod(texcoord_pixels.y, float(char_height)) + ); + vec2 texcoord_final = (texcoord_base + texcoord_off) / vec2(float(font_width), float(font_height)); + + vec4 texel = texture(texture_data, texcoord_final); + if (texel.rgb == vec3(0.0, 0.0, 0.0)) discard; + frag_color = texel; +} \ No newline at end of file diff --git a/src/assets/shaders/text/vert.glsl b/src/assets/shaders/text/vert.glsl new file mode 100644 index 0000000..4005d75 --- /dev/null +++ b/src/assets/shaders/text/vert.glsl @@ -0,0 +1,26 @@ +#version 300 es +precision highp float; + +uniform mat4 view; +uniform mat4 position; + +out vec2 vertex_texcoord; + +void main() { + const vec2 positions[4] = vec2[]( + vec2(-1, -1), + vec2(+1, -1), + vec2(-1, +1), + vec2(+1, +1) + ); + const vec2 coords[4] = vec2[]( + vec2(0, 0), + vec2(1, 0), + vec2(0, 1), + vec2(1, 1) + ); + vec4 vertex = vec4(positions[gl_VertexID], 0.0, 1.0); + + vertex_texcoord = coords[gl_VertexID]; + gl_Position = view * position * vertex; +} diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..2b190de --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,96 @@ +use std::{cell::RefCell, collections::HashMap}; + +pub struct Context { + pub audio: web_sys::AudioContext, +} + +impl Context { + pub fn new() -> Self { + let audio = web_sys::AudioContext::new() + .expect("failed to create audio context"); + Self { + audio, + } + } +} + +pub struct Audio { + pub buffer: &'static RefCell>, + //pub source: &'static web_sys::AudioBufferSourceNode, +} + +impl Audio { + pub fn new(ctx: &Context, bytes: &[u8]) -> Self { + let sbuffer: &_ = Box::leak(Box::new(RefCell::new(None))); + let sclone: &'static RefCell> = + <&_>::clone(&sbuffer); + let ret = Audio { + buffer: sclone, + }; + let jsp = ctx.audio.decode_audio_data(&js_sys::Uint8Array::from(bytes).buffer()).expect("failed to decode audio"); + let promise = wasm_bindgen_futures::JsFuture::from(jsp); + wasm_bindgen_futures::spawn_local(async { + if let Some(data) = promise.await.ok() { + *sbuffer.borrow_mut() = Some(web_sys::AudioBuffer::from(data)); + } + () + }); + ret + } + + pub fn play(&self, ctx: &Context, looping: Option<(Option, Option)>) -> Option { + let source = ctx.audio.create_buffer_source().ok()?; + source.set_buffer((&*self.buffer.borrow()).as_ref()); + if let Some((ms, me)) = looping { + source.set_loop(true); + if let Some(s) = ms { source.set_loop_start(s) } + if let Some(e) = me { source.set_loop_end(e) } + } + source.connect_with_audio_node(&ctx.audio.destination()).ok()?; + source.start().ok()?; + Some(source) + } +} + +pub struct Assets { + pub ctx: Context, + + pub audio: HashMap, + + pub music_node: Option, +} + +impl Assets { + pub fn new(f : F) -> Self where F: Fn(&Context) -> HashMap { + let ctx = Context::new(); + + let audio = f(&ctx); + + Self { + ctx, + audio, + music_node: None, + } + } + + pub fn play_sfx(&mut self, name: &str) { + if let Some(a) = self.audio.get(name) { + a.play(&self.ctx, None); + } + } + + pub fn is_music_playing(&self) -> bool { + if let Some(ms) = &self.music_node { + ms.buffer().is_some() + } else { false } + } + + pub fn play_music(&mut self, name: &str, start: Option, end: Option) { + if let Some(s) = &self.music_node { + let _ = s.stop(); + } + if let Some(a) = self.audio.get(name) { + self.music_node = a.play(&self.ctx, Some((start, end))); + } + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..5328d4d --- /dev/null +++ b/src/context.rs @@ -0,0 +1,156 @@ +use wasm_bindgen::JsCast; +use winit::platform::web::WindowExtWebSys; +use glow::HasContext; + +#[link(wasm_import_module = "./helpers.js")] +extern { + fn js_track_resized_setup(); + fn js_poll_resized() -> bool; +} + +// pub const RENDER_WIDTH: f32 = 640.0; +// pub const RENDER_HEIGHT: f32 = 360.0; +// pub const RENDER_WIDTH: f32 = 320.0; +// pub const RENDER_HEIGHT: f32 = 180.0; +pub const RENDER_WIDTH: f32 = 240.0; +pub const RENDER_HEIGHT: f32 = 160.0; + +pub fn compute_upscale(windoww: u32, windowh: u32) -> u32 { + let mut ratio = 1; + loop { + if (RENDER_WIDTH as u32) * ratio > windoww + || (RENDER_HEIGHT as u32) * ratio > windowh + { + break; + } + ratio += 1; + } + (ratio - 1).max(1) +} + +pub struct Context { + pub window: winit::window::Window, + pub gl: glow::Context, + pub emptyvao: glow::VertexArray, + pub performance: web_sys::Performance, +} + +impl Context { + pub fn new(window: winit::window::Window) -> Self { + let gl = web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| { + let dst = doc.get_element_by_id("oubliette-parent")?; + let canvas = web_sys::Element::from(window.canvas().expect("failed to find canvas")); + dst.append_child(&canvas).ok()?; + let c = canvas.dyn_into::().ok()?; + let webgl2_context = c.get_context("webgl2").ok()?? + .dyn_into::().ok()?; + Some(glow::Context::from_webgl2_context(webgl2_context)) + }) + .expect("couldn't add canvas to document"); + unsafe { + gl.clear_color(0.1, 0.1, 0.1, 1.0); + gl.clear_depth_f32(1.0); + + gl.enable(glow::DEPTH_TEST); + gl.depth_func(glow::LEQUAL); + + gl.enable(glow::BLEND); + gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA); + + gl.enable(glow::STENCIL_TEST); + + gl.cull_face(glow::FRONT); + } + + let emptyvao = unsafe { + gl.create_vertex_array().expect("failed to initialize vao") + }; + + unsafe { js_track_resized_setup(); } + + + Self { + window, + gl, + emptyvao, + performance: web_sys::window().expect("failed to find window") + .performance().expect("failed to get performance"), + } + } + + pub fn maximize_canvas(&self) { + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| { + let inner_size = { + let browser_window = doc.default_view() + .or_else(web_sys::window) + .unwrap(); + winit::dpi::LogicalSize::new( + browser_window.inner_width().unwrap().as_f64().unwrap(), + browser_window.inner_height().unwrap().as_f64().unwrap(), + ) + }; + self.window.canvas().unwrap().set_width(inner_size.width as _); + self.window.canvas().unwrap().set_height(inner_size.height as _); + let _ = self.window.request_inner_size(inner_size); + Some(()) + }) + .expect("failed to resize canvas"); + } + + pub fn resize_necessary(&self) -> bool { + unsafe { + js_poll_resized() + } + } + + pub fn clear_color(&self, color: glam::Vec4) { + unsafe { + self.gl.clear_color(color.x, color.y, color.z, color.w); + } + } + + pub fn clear_depth(&self) { + unsafe { + self.gl.clear(glow::DEPTH_BUFFER_BIT); + } + } + + pub fn clear(&self) { + unsafe { + self.gl.stencil_mask(0xff); + self.gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT | glow::STENCIL_BUFFER_BIT); + } + } + + pub fn begin_stencil(&self) { + unsafe { + self.gl.stencil_func(glow::ALWAYS, 1, 0xff); + self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::REPLACE); + } + } + + pub fn use_stencil(&self) { + unsafe { + self.gl.stencil_func(glow::EQUAL, 1, 0xff); + self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP); + } + } + + pub fn end_stencil(&self) { + unsafe { + self.gl.stencil_func(glow::ALWAYS, 1, 0xff); + self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP); + } + } + + pub fn render_no_geometry(&self) { + unsafe { + self.gl.bind_vertex_array(Some(self.emptyvao)); + self.gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4); + } + } +} diff --git a/src/font.rs b/src/font.rs new file mode 100644 index 0000000..02d0a9f --- /dev/null +++ b/src/font.rs @@ -0,0 +1,82 @@ +use crate::{context, texture, shader}; + +const CHAR_WIDTH: i32 = 7; +const CHAR_HEIGHT: i32 = 9; +const FONT_WIDTH: i32 = 112; +const FONT_HEIGHT: i32 = 54; + +pub struct Font { + pub shader: shader::Shader, + pub font: texture::Texture, +} + +impl Font { + pub fn new(ctx: &context::Context) -> Self { + let shader = shader::Shader::new_nolib( + &ctx, + include_str!("assets/shaders/text/vert.glsl"), + include_str!("assets/shaders/text/frag.glsl"), + ); + let font = texture::Texture::new(ctx, include_bytes!("assets/fonts/simple.png")); + Self { + shader, + font, + } + } + + pub fn render_text(&self, ctx: &context::Context, pos: &glam::Vec2, text: &str) { + let mut width = 0; + let mut linewidth = 0; + let mut height = CHAR_HEIGHT; + for c in text.chars() { + if c == '\n' { + width = width.max(linewidth); + linewidth = 0; + height += CHAR_HEIGHT; + } else { + linewidth += CHAR_WIDTH; + } + } + width = width.max(linewidth); + + self.shader.bind(ctx); + let len = text.len().min(256); + self.shader.set_i32(ctx, "text_length", len as _); + let textvals: Vec = text.as_bytes().into_iter().take(len).map(|b| { + *b as i32 + }).collect(); + self.shader.set_i32_array(ctx, "text[0]", &textvals); + self.shader.set_i32(ctx, "char_width", CHAR_WIDTH as _); + self.shader.set_i32(ctx, "char_height", CHAR_HEIGHT as _); + self.shader.set_i32(ctx, "font_width", FONT_WIDTH as _); + self.shader.set_i32(ctx, "font_height", FONT_HEIGHT as _); + self.shader.set_i32(ctx, "text_width", width as _); + self.shader.set_i32(ctx, "text_height", height as _); + self.shader.set_mat4( + ctx, "view", + &glam::Mat4::from_scale( + glam::Vec3::new( + 2.0 / context::RENDER_WIDTH, + 2.0 / context::RENDER_HEIGHT, + 1.0, + ), + ), + ); + let halfwidth = width as f32 / 2.0; + let halfheight = height as f32 / 2.0; + self.shader.set_mat4( + ctx, "position", + &glam::Mat4::from_scale_rotation_translation( + glam::Vec3::new(halfwidth, halfheight, 1.0), + glam::Quat::IDENTITY, + glam::Vec3::new( + -context::RENDER_WIDTH / 2.0 + pos.x + halfwidth, + context::RENDER_HEIGHT / 2.0 - pos.y - halfheight, + 0.0, + ), + ) + ); + self.font.bind(ctx); + ctx.render_no_geometry(); + } +} diff --git a/src/framebuffer.rs b/src/framebuffer.rs new file mode 100644 index 0000000..c0ac72b --- /dev/null +++ b/src/framebuffer.rs @@ -0,0 +1,100 @@ +use glow::HasContext; + +use crate::context; + +pub struct Framebuffer { + pub tex: Option, + pub fbo: Option, + pub dims: glam::Vec2, + pub offsets: glam::Vec2, +} + +impl Framebuffer { + pub fn screen(ctx: &context::Context) -> Self { + let (windoww, windowh): (f32, f32) = ctx.window.inner_size().into(); + let ratio = context::compute_upscale(windoww as _, windowh as _) as f32; + let upscalew = context::RENDER_WIDTH * ratio; + let upscaleh = context::RENDER_HEIGHT * ratio; + let offsetx = (windoww - upscalew) / 2.0; + let offsety = (windowh - upscaleh) / 2.0; + log::info!("{} {} {} {} {} {}", windoww, windowh, upscalew, upscaleh, offsetx, offsety); + Self { + tex: None, + fbo: None, + dims: glam::Vec2::new(upscalew, upscaleh), + offsets: glam::Vec2::new(offsetx, offsety), + } + } + + pub fn new(ctx: &context::Context, dims: &glam::Vec2, offsets: &glam::Vec2) -> Self { + unsafe { + let fbo = ctx.gl.create_framebuffer() + .expect("failed to create framebuffer"); + ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo)); + + let depth_buffer = ctx.gl.create_renderbuffer() + .expect("failed to create depth buffer"); + ctx.gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth_buffer)); + ctx.gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT32F, dims.x as _, dims.y as _); + ctx.gl.framebuffer_renderbuffer(glow::FRAMEBUFFER, glow::DEPTH_ATTACHMENT, glow::RENDERBUFFER, Some(depth_buffer)); + + let stencil_buffer = ctx.gl.create_renderbuffer() + .expect("failed to create stencil buffer"); + ctx.gl.bind_renderbuffer(glow::RENDERBUFFER, Some(stencil_buffer)); + ctx.gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_STENCIL, dims.x as _, dims.y as _); + ctx.gl.framebuffer_renderbuffer(glow::FRAMEBUFFER, glow::DEPTH_STENCIL_ATTACHMENT, glow::RENDERBUFFER, Some(stencil_buffer)); + + let tex = ctx.gl.create_texture() + .expect("failed to create framebuffer texture"); + ctx.gl.bind_texture(glow::TEXTURE_2D, Some(tex)); + ctx.gl.tex_image_2d( + glow::TEXTURE_2D, + 0, + glow::RGBA as _, + dims.x as _, + dims.y as _, + 0, + glow::RGBA, + glow::UNSIGNED_BYTE, + None, + ); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as _); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as _); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as _); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as _); + ctx.gl.framebuffer_texture_2d(glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some(tex), 0); + ctx.gl.draw_buffer(glow::COLOR_ATTACHMENT0); + + let status = ctx.gl.check_framebuffer_status(glow::FRAMEBUFFER); + if status != glow::FRAMEBUFFER_COMPLETE { + panic!("error initializing framebuffer:\n{}", status); + } + + Self { + tex: Some(tex), + fbo: Some(fbo), + dims: dims.clone(), + offsets: offsets.clone(), + } + } + } + + pub fn bind_texture(&self, ctx: &context::Context) { + unsafe { + ctx.gl.active_texture(glow::TEXTURE0); + ctx.gl.bind_texture(glow::TEXTURE_2D, self.tex); + } + } + + pub fn bind(&self, ctx: &context::Context) { + unsafe { + ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, self.fbo); + ctx.gl.viewport( + self.offsets.x as _, + self.offsets.y as _, + self.dims.x as _, + self.dims.y as _, + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..df778a6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,94 @@ +use winit::platform::web::EventLoopExtWebSys; + +pub mod utils; +pub mod request; +pub mod context; +pub mod state; +pub mod framebuffer; +pub mod shader; +pub mod mesh; +pub mod texture; +pub mod font; +pub mod audio; + +pub fn run(gnew: F) where G: state::Game + 'static, F: (Fn(&context::Context) -> G) { + console_log::init_with_level(log::Level::Debug).unwrap(); + console_error_panic_hook::set_once(); + tracing_wasm::set_as_global_default(); + log::info!("HELLO COMPUTER HELLO CLONKHEAD :)"); + + let event_loop = winit::event_loop::EventLoop::new() + .expect("failed to initialize event loop"); + + let window = winit::window::WindowBuilder::new() + .with_maximized(true) + .with_decorations(false) + .build(&event_loop) + .expect("failed to initialize window"); + + let ctx = context::Context::new(window); + ctx.maximize_canvas(); + let mut game = gnew(&ctx); + let mut st = state::State::new(&ctx); + st.write_log("test"); + st.write_log("foo"); + st.write_log("bar"); + st.write_log("baz"); + + event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); + event_loop.spawn(move |event, elwt| { + match event { + winit::event::Event::WindowEvent { + event: wev, + window_id, + .. + } => match wev { + winit::event::WindowEvent::CloseRequested + if window_id == ctx.window.id() => elwt.exit(), + winit::event::WindowEvent::Resized{..} => { + ctx.maximize_canvas(); + st.handle_resize(&ctx); + }, + winit::event::WindowEvent::MouseInput { + button, + state, + .. + } => match state { + winit::event::ElementState::Pressed => { + st.mouse_pressed(&ctx, button, &mut game) + }, + winit::event::ElementState::Released => { + st.mouse_released(&ctx, button) + }, + } + winit::event::WindowEvent::KeyboardInput { + event: winit::event::KeyEvent { + physical_key: winit::keyboard::PhysicalKey::Code(key), + state, + .. + }, + .. + } => match state { + winit::event::ElementState::Pressed => { + st.key_pressed(&ctx, key) + }, + winit::event::ElementState::Released => { + st.key_released(&ctx, key) + }, + } + _ => {}, + }, + + winit::event::Event::AboutToWait => { + if ctx.resize_necessary() { + ctx.maximize_canvas(); + st.handle_resize(&ctx); + } + st.run_update(&ctx, &mut game); + st.run_render(&ctx, &mut game); + }, + + _ => {}, + } + }); +} diff --git a/src/mesh.rs b/src/mesh.rs new file mode 100644 index 0000000..2f54903 --- /dev/null +++ b/src/mesh.rs @@ -0,0 +1,118 @@ +use std::io::BufRead; + +use glow::HasContext; + +use crate::context; + +pub const ATTRIB_VERTEX: u32 = 0; +pub const ATTRIB_NORMAL: u32 = 1; +pub const ATTRIB_TEXCOORD: u32 = 2; + +pub struct Mesh { + pub vao: glow::VertexArray, + pub index_count: usize, +} + +impl Mesh { + pub fn build( + ctx: &context::Context, + vertices: &Vec, + indices: &Vec, + snormals: &Option>, + stexcoords: &Option>, + ) -> Self { + unsafe { + let vao = ctx.gl.create_vertex_array().expect("failed to initialize vao"); + ctx.gl.bind_vertex_array(Some(vao)); + + let vertices_vbo = ctx.gl.create_buffer().expect("failed to initialize vbo"); + ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertices_vbo)); + ctx.gl.buffer_data_u8_slice( + glow::ARRAY_BUFFER, + std::slice::from_raw_parts( + vertices.as_ptr() as _, + vertices.len() * std::mem::size_of::(), + ), + glow::STATIC_DRAW, + ); + ctx.gl.vertex_attrib_pointer_f32(ATTRIB_VERTEX, 3, glow::FLOAT, false, 0, 0); + ctx.gl.enable_vertex_attrib_array(ATTRIB_VERTEX); + + let indices_vbo = ctx.gl.create_buffer().expect("failed to initialize vbo"); + ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(indices_vbo)); + ctx.gl.buffer_data_u8_slice( + glow::ELEMENT_ARRAY_BUFFER, + std::slice::from_raw_parts( + indices.as_ptr() as _, + indices.len() * std::mem::size_of::(), + ), + glow::STATIC_DRAW, + ); + + if let Some(normals) = snormals { + let normals_vbo = ctx.gl.create_buffer().expect("failed to initialize vbo"); + ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(normals_vbo)); + ctx.gl.buffer_data_u8_slice( + glow::ARRAY_BUFFER, + std::slice::from_raw_parts( + normals.as_ptr() as _, + normals.len() * std::mem::size_of::(), + ), + glow::STATIC_DRAW, + ); + ctx.gl.vertex_attrib_pointer_f32(ATTRIB_NORMAL, 3, glow::FLOAT, false, 0, 0); + ctx.gl.enable_vertex_attrib_array(ATTRIB_NORMAL); + } + + if let Some(texcoords) = stexcoords { + let texcoords_vbo = ctx.gl.create_buffer().expect("failed to initialize vbo"); + ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(texcoords_vbo)); + ctx.gl.buffer_data_u8_slice( + glow::ARRAY_BUFFER, + std::slice::from_raw_parts( + texcoords.as_ptr() as _, + texcoords.len() * std::mem::size_of::(), + ), + glow::STATIC_DRAW, + ); + ctx.gl.vertex_attrib_pointer_f32(ATTRIB_TEXCOORD, 2, glow::FLOAT, false, 0, 0); + ctx.gl.enable_vertex_attrib_array(ATTRIB_TEXCOORD); + } + + Self { + vao, + index_count: indices.len(), + } + } + } + + pub fn new(ctx: &context::Context, mut bytes: &[u8]) -> Self { + let lopts = tobj::LoadOptions { + triangulate: true, + single_index: true, + ..Default::default() + }; + let (meshes, _materials) = tobj::load_obj_buf( + &mut bytes, + &lopts, + |_| Err(tobj::LoadError::GenericFailure) + ).expect("failed to load mesh"); + let mesh = meshes.into_iter().next() + .expect("failed to load mesh") + .mesh; + Self::build( + ctx, + &mesh.positions, + &mesh.indices, + &Some(mesh.normals), + &Some(mesh.texcoords), + ) + } + + pub fn render(&self, ctx: &context::Context) { + unsafe { + ctx.gl.bind_vertex_array(Some(self.vao)); + ctx.gl.draw_elements(glow::TRIANGLES, self.index_count as _, glow::UNSIGNED_INT, 0); + } + } +} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..7f66f57 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,20 @@ +use wasm_bindgen::JsCast; + +pub async fn get_store(key: &str) -> Option { + let mut opts = web_sys::RequestInit::new(); + opts.method("GET"); + opts.mode(web_sys::RequestMode::Cors); + + let url = format!("https://colonq.computer/bullfrog/api/get/{}", key); + + let request = web_sys::Request::new_with_str_and_init(&url, &opts).ok()?; + + let window = web_sys::window().unwrap(); + let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)).await.ok()?; + + assert!(resp_value.is_instance_of::()); + let resp: web_sys::Response = resp_value.dyn_into().unwrap(); + + let text = wasm_bindgen_futures::JsFuture::from(resp.text().ok()?).await.ok()?; + text.as_string() +} diff --git a/src/shader.rs b/src/shader.rs new file mode 100644 index 0000000..6bcae3c --- /dev/null +++ b/src/shader.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; + +use glow::HasContext; + +use crate::{context, mesh}; + +const COMMON_VERT: &'static str = include_str!("assets/shaders/common/vert.glsl"); +const COMMON_FRAG: &'static str = include_str!("assets/shaders/common/frag.glsl"); + +#[derive(Clone)] +pub struct Shader { + pub program: glow::Program, + pub uniforms: std::rc::Rc> +} + +impl Shader { + pub fn new_nolib(ctx: &context::Context, vsrc: &str, fsrc: &str) -> Self { + unsafe { + let program = ctx.gl.create_program() + .expect("cannot create shader program"); + + let vert = ctx.gl.create_shader(glow::VERTEX_SHADER) + .expect("cannot create shader"); + ctx.gl.shader_source(vert, &vsrc); + ctx.gl.compile_shader(vert); + if !ctx.gl.get_shader_compile_status(vert) { + panic!( + "failed to compile vertex shader:\n{}", + ctx.gl.get_shader_info_log(vert) + ); + } + ctx.gl.attach_shader(program, vert); + + let frag = ctx.gl.create_shader(glow::FRAGMENT_SHADER) + .expect("cannot create shader"); + ctx.gl.shader_source(frag, &fsrc); + ctx.gl.compile_shader(frag); + if !ctx.gl.get_shader_compile_status(frag) { + panic!( + "failed to compile fragment shader:\n{}", + ctx.gl.get_shader_info_log(frag) + ); + } + ctx.gl.attach_shader(program, frag); + + ctx.gl.bind_attrib_location(program, mesh::ATTRIB_VERTEX, "vertex"); + ctx.gl.bind_attrib_location(program, mesh::ATTRIB_NORMAL, "normal"); + ctx.gl.bind_attrib_location(program, mesh::ATTRIB_TEXCOORD, "texcoord"); + + ctx.gl.link_program(program); + if !ctx.gl.get_program_link_status(program) { + panic!( + "failed to link shader program:\n{}", + ctx.gl.get_program_info_log(program), + ); + } + + ctx.gl.detach_shader(program, vert); + ctx.gl.delete_shader(vert); + ctx.gl.detach_shader(program, frag); + ctx.gl.delete_shader(frag); + + let mut uniforms = HashMap::new(); + for index in 0..ctx.gl.get_active_uniforms(program) { + if let Some(active) = ctx.gl.get_active_uniform(program, index) { + let loc = ctx.gl.get_uniform_location(program, &active.name) + .expect(&format!("failed to get location for uniform: {}", active.name)); + uniforms.insert(active.name, loc); + } + } + + Self { + program, + uniforms: std::rc::Rc::new(uniforms), + } + } + } + + pub fn new(ctx: &context::Context, vsrcstr: &str, fsrcstr: &str) -> Self { + let vsrc = format!("{}\n{}\n", COMMON_VERT, vsrcstr); + let fsrc = format!("{}\n{}\n", COMMON_FRAG, fsrcstr); + Self::new_nolib(ctx, &vsrc, &fsrc) + } + + pub fn set_i32(&self, ctx: &context::Context, name: &str, val: i32) { + if let Some(loc) = self.uniforms.get(name) { + unsafe { ctx.gl.uniform_1_i32(Some(loc), val) } + } + } + + pub fn set_i32_array(&self, ctx: &context::Context, name: &str, val: &[i32]) { + if let Some(loc) = self.uniforms.get(name) { + unsafe { + ctx.gl.uniform_1_i32_slice(Some(loc), val) + } + } + } + + pub fn set_f32(&self, ctx: &context::Context, name: &str, val: f32) { + if let Some(loc) = self.uniforms.get(name) { + unsafe { ctx.gl.uniform_1_f32(Some(loc), val) } + } + } + + pub fn set_vec3(&self, ctx: &context::Context, name: &str, val: &glam::Vec3) { + if let Some(loc) = self.uniforms.get(name) { + unsafe { + ctx.gl.uniform_3_f32( + Some(loc), + val.x, + val.y, + val.z, + ); + } + } + } + + pub fn set_vec4(&self, ctx: &context::Context, name: &str, val: &glam::Vec4) { + if let Some(loc) = self.uniforms.get(name) { + unsafe { + ctx.gl.uniform_4_f32( + Some(loc), + val.x, + val.y, + val.z, + val.w, + ); + } + } + } + + pub fn set_mat4(&self, ctx: &context::Context, name: &str, val: &glam::Mat4) { + if let Some(loc) = self.uniforms.get(name) { + unsafe { + ctx.gl.uniform_matrix_4_f32_slice( + Some(loc), + false, + &val.to_cols_array(), + ); + } + } + } + + pub fn set_position_3d(&self, ctx: &context::Context, position: &glam::Mat4) { + self.set_mat4(&ctx, "position", &position); + self.set_mat4(&ctx, "normal_matrix", &position.inverse().transpose()); + } + + pub fn set_position_2d(&self, ctx: &context::Context, pos: &glam::Vec2, dims: &glam::Vec2) { + let halfwidth = dims.x / 2.0; + let halfheight = dims.y / 2.0; + self.set_mat4( + &ctx, "position", + &glam::Mat4::from_scale_rotation_translation( + glam::Vec3::new(halfwidth, halfheight, 1.0), + glam::Quat::IDENTITY, + glam::Vec3::new( + -context::RENDER_WIDTH / 2.0 + pos.x + halfwidth, + context::RENDER_HEIGHT / 2.0 - pos.y - halfheight, + 0.0, + ), + ) + ); + } + + pub fn bind(&self, ctx: &context::Context) { + unsafe { + ctx.gl.use_program(Some(self.program)); + } + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..bb479c6 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,262 @@ +use std::collections::HashMap; + +use crate::{context, framebuffer, shader, audio}; + +const DELTA_TIME: f64 = 1.0 / 60.0; + +pub trait Game { + fn initialize_audio(&self, ctx: &context::Context, st: &State, actx: &audio::Context) -> + HashMap; + fn finish_title(&mut self); + fn update(&mut self, ctx: &context::Context, st: &mut State) -> Option<()>; + fn render(&mut self, ctx: &context::Context, st: &mut State) -> Option<()>; +} + +pub struct Keys { + pub up: bool, + pub down: bool, + pub left: bool, + pub right: bool, + pub a: bool, + pub b: bool, + pub l: bool, + pub r: bool, + pub start: bool, + pub select: bool, +} + +impl Keys { + pub fn new() -> Self { + Self { + up: false, + down: false, + left: false, + right: false, + a: false, + b: false, + l: false, + r: false, + start: false, + select: false, + } + } +} + +pub struct State { + pub acc: f64, + pub last: f64, + pub tick: u64, + + pub keys: Keys, + + pub screen: framebuffer::Framebuffer, + pub render_framebuffer: framebuffer::Framebuffer, + pub shader_upscale: shader::Shader, + pub audio: Option, + + pub projection: glam::Mat4, + pub camera: glam::Mat4, + pub lighting: (glam::Vec3, glam::Vec3), + + pub log: Vec<(u64, String)>, +} + +pub fn now(ctx: &context::Context) -> f64 { + ctx.performance.now() / 1000.0 +} + +impl State { + pub fn new(ctx: &context::Context) -> Self { + let screen = framebuffer::Framebuffer::screen(ctx); + let render_framebuffer = framebuffer::Framebuffer::new( + ctx, + &glam::Vec2::new(context::RENDER_WIDTH, context::RENDER_HEIGHT), + &glam::Vec2::new(0.0, 0.0), + ); + let shader_upscale = shader::Shader::new_nolib( + ctx, + include_str!("assets/shaders/scale/vert.glsl"), + include_str!("assets/shaders/scale/frag.glsl"), + ); + + Self { + acc: 0.0, + last: now(ctx), + tick: 0, + keys: Keys::new(), + screen, + render_framebuffer, + shader_upscale, + audio: None, + + projection: glam::Mat4::perspective_lh( + std::f32::consts::PI / 4.0, + context::RENDER_WIDTH / context::RENDER_HEIGHT, + 0.1, + 400.0, + ), + camera: glam::Mat4::IDENTITY, + lighting: ( + glam::Vec3::new(1.0, 0.0, 0.0), + glam::Vec3::new(1.0, -1.0, 1.0), + ), + + log: Vec::new(), + } + } + + pub fn write_log(&mut self, e: &str) { + self.log.push((self.tick, e.to_owned())); + } + + pub fn handle_resize(&mut self, ctx: &context::Context) { + self.screen = framebuffer::Framebuffer::screen(ctx); + } + + pub fn move_camera( + &mut self, + _ctx: &context::Context, + camera: &glam::Mat4, + ) { + self.camera = camera.clone(); + } + + pub fn set_lighting( + &mut self, + _ctx: &context::Context, + color: &glam::Vec3, + dir: &glam::Vec3, + ) { + self.lighting = (color.clone(), dir.clone()); + } + + pub fn view(&self) -> glam::Mat4 { + self.camera.clone() + } + + pub fn bind_3d(&mut self, ctx: &context::Context, shader: &shader::Shader) { + shader.bind(ctx); + shader.set_mat4(&ctx, "projection", &self.projection); + shader.set_mat4(ctx, "view", &self.view()); + shader.set_vec3( + ctx, "light_ambient_color", + &glam::Vec3::new(1.0, 1.0, 1.0)); + shader.set_vec3( + ctx, "light_dir_color", + &self.lighting.0, + ); + shader.set_vec3( + ctx, "light_dir", + &self.lighting.1, + ); + } + + pub fn bind_2d(&mut self, ctx: &context::Context, shader: &shader::Shader) { + shader.bind(ctx); + shader.set_mat4(&ctx, "projection", &glam::Mat4::IDENTITY); + shader.set_mat4( + ctx, "view", + &glam::Mat4::from_scale( + glam::Vec3::new( + 2.0 / context::RENDER_WIDTH, + 2.0 / context::RENDER_HEIGHT, + 1.0, + ), + ), + ); + } + + pub fn mouse_pressed( + &mut self, + ctx: &context::Context, + _button: winit::event::MouseButton, + game: &mut G + ) where G: Game { + log::info!("click"); + if self.audio.is_none() { + self.audio = Some(audio::Assets::new(|actx| { + game.initialize_audio(ctx, &self, actx) + })); + game.finish_title(); + } + } + + pub fn mouse_released( + &mut self, + _ctx: &context::Context, + _button: winit::event::MouseButton, + ) { + } + + pub fn key_pressed( + &mut self, + _ctx: &context::Context, + key: winit::keyboard::KeyCode, + ) { + match key { + winit::keyboard::KeyCode::KeyW => self.keys.up = true, + winit::keyboard::KeyCode::KeyS => self.keys.down = true, + winit::keyboard::KeyCode::KeyA => self.keys.left = true, + winit::keyboard::KeyCode::KeyD => self.keys.right = true, + winit::keyboard::KeyCode::Digit1 => self.keys.a = true, + winit::keyboard::KeyCode::Digit2 => self.keys.b = true, + winit::keyboard::KeyCode::KeyQ => self.keys.l = true, + winit::keyboard::KeyCode::KeyE => self.keys.r = true, + winit::keyboard::KeyCode::Tab => self.keys.start = true, + winit::keyboard::KeyCode::Space => self.keys.select = true, + _ => {}, + } + } + + pub fn key_released( + &mut self, + _ctx: &context::Context, + key: winit::keyboard::KeyCode, + ) { + match key { + winit::keyboard::KeyCode::KeyW => self.keys.up = false, + winit::keyboard::KeyCode::KeyS => self.keys.down = false, + winit::keyboard::KeyCode::KeyA => self.keys.left = false, + winit::keyboard::KeyCode::KeyD => self.keys.right = false, + winit::keyboard::KeyCode::Digit1 => self.keys.a = false, + winit::keyboard::KeyCode::Digit2 => self.keys.b = false, + winit::keyboard::KeyCode::KeyQ => self.keys.l = false, + winit::keyboard::KeyCode::KeyE => self.keys.r = false, + winit::keyboard::KeyCode::Tab => self.keys.start = false, + winit::keyboard::KeyCode::Space => self.keys.select = false, + _ => {}, + } + } + + pub fn run_update(&mut self, ctx: &context::Context, game: &mut G) where G: Game { + let now = now(ctx); + self.acc += now - self.last; + self.last = now; + + // update, if enough time has accumulated since last update + if self.acc >= DELTA_TIME { + self.acc -= DELTA_TIME; + self.tick += 1; + game.update(ctx, self); + + // if a lot of time has elapsed (e.g. if window is unfocused and not + // running update loop), prevent "death spiral" + if self.acc >= DELTA_TIME { self.acc = 0.0 } + } + } + + pub fn run_render(&mut self, ctx: &context::Context, game: &mut G) where G: Game { + self.render_framebuffer.bind(&ctx); + ctx.clear_color(glam::Vec4::new(0.1, 0.1, 0.1, 1.0)); + ctx.clear(); + + game.render(ctx, self); + + self.screen.bind(&ctx); + ctx.clear_color(glam::Vec4::new(0.0, 0.0, 0.0, 1.0)); + ctx.clear(); + self.shader_upscale.bind(&ctx); + self.render_framebuffer.bind_texture(&ctx); + ctx.render_no_geometry(); + } +} diff --git a/src/texture.rs b/src/texture.rs new file mode 100644 index 0000000..a7c9893 --- /dev/null +++ b/src/texture.rs @@ -0,0 +1,51 @@ +use glow::HasContext; +use image::EncodableLayout; + +use crate::context; + +pub struct Texture { + pub tex: glow::Texture, +} + +impl Texture { + pub fn new(ctx: &context::Context, bytes: &[u8]) -> Self { + let rgba = image::io::Reader::new(std::io::Cursor::new(bytes)) + .with_guessed_format() + .expect("failed to guess image format") + .decode() + .expect("failed to decode image") + .into_rgba8(); + let pixels = rgba.as_bytes(); + unsafe { + let tex = ctx.gl.create_texture().expect("failed to create texture"); + ctx.gl.bind_texture(glow::TEXTURE_2D, Some(tex)); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32); + ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32); + ctx.gl.tex_image_2d( + glow::TEXTURE_2D, + 0, + glow::RGBA as i32, + rgba.width() as i32, + rgba.height() as i32, + 0, + glow::RGBA, + glow::UNSIGNED_BYTE, + Some(pixels), + ); + ctx.gl.generate_mipmap(glow::TEXTURE_2D); + + Self { + tex, + } + } + } + + pub fn bind(&self, ctx: &context::Context) { + unsafe { + ctx.gl.active_texture(glow::TEXTURE0); + ctx.gl.bind_texture(glow::TEXTURE_2D, Some(self.tex)); + } + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..57ccf13 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,58 @@ +#[derive(Clone, Debug, PartialEq)] +pub enum Cardinal { + North, + South, + West, + East, +} + +impl Cardinal { + pub fn turn_cw(&self) -> Self { + match self { + Self::North => Self::East, + Self::South => Self::West, + Self::West => Self::North, + Self::East => Self::South, + } + } + + pub fn turn_ccw(&self) -> Self { + match self { + Self::North => Self::West, + Self::South => Self::East, + Self::West => Self::South, + Self::East => Self::North, + } + } + + pub fn dir(&self) -> glam::Vec3 { + match self { + Self::North => glam::Vec3::new(0.0, 1.0, 0.0), + Self::South => glam::Vec3::new(0.0, -1.0, 0.0), + Self::West => glam::Vec3::new(-1.0, 0.0, 0.0), + Self::East => glam::Vec3::new(1.0, 0.0, 0.0), + } + } + + pub fn offsets(&self) -> (i32, i32) { + match self { + Self::North => (0, 1), + Self::South => (0, -1), + Self::West => (-1, 0), + Self::East => (1, 0), + } + } + + pub fn angle(&self) -> f32 { + match self { + Self::North => 0.0, + Self::South => std::f32::consts::PI, + Self::West => 3.0 * std::f32::consts::PI / 2.0, + Self::East => std::f32::consts::PI / 2.0, + } + } +} + +pub fn lerp(a: f32, b: f32, t: f32) -> f32 { + a + t.clamp(0.0, 1.0) * (b - a) +} -- cgit v1.2.3