summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2024-03-03 00:10:10 -0500
committerLLLL Colonq <llll@colonq>2024-03-03 00:10:10 -0500
commit070108cf09b1b561613b6eea04723afbbb464507 (patch)
tree01503df63f16ef6ef4ab4a37b24305286bb2477c /src
Initial commit (new winit)
Diffstat (limited to 'src')
-rw-r--r--src/assets/fonts/font1.pngbin0 -> 1508 bytes
-rw-r--r--src/assets/fonts/font2.pngbin0 -> 1883 bytes
-rw-r--r--src/assets/fonts/simple.pngbin0 -> 1042 bytes
-rw-r--r--src/assets/shaders/common/frag.glsl158
-rw-r--r--src/assets/shaders/common/vert.glsl30
-rw-r--r--src/assets/shaders/scale/frag.glsl13
-rw-r--r--src/assets/shaders/scale/vert.glsl22
-rw-r--r--src/assets/shaders/test/frag.glsl23
-rw-r--r--src/assets/shaders/test/vert.glsl4
-rw-r--r--src/assets/shaders/text/frag.glsl55
-rw-r--r--src/assets/shaders/text/vert.glsl26
-rw-r--r--src/audio.rs96
-rw-r--r--src/context.rs156
-rw-r--r--src/font.rs82
-rw-r--r--src/framebuffer.rs100
-rw-r--r--src/lib.rs94
-rw-r--r--src/mesh.rs118
-rw-r--r--src/request.rs20
-rw-r--r--src/shader.rs171
-rw-r--r--src/state.rs262
-rw-r--r--src/texture.rs51
-rw-r--r--src/utils.rs58
22 files changed, 1539 insertions, 0 deletions
diff --git a/src/assets/fonts/font1.png b/src/assets/fonts/font1.png
new file mode 100644
index 0000000..ec06424
--- /dev/null
+++ b/src/assets/fonts/font1.png
Binary files differ
diff --git a/src/assets/fonts/font2.png b/src/assets/fonts/font2.png
new file mode 100644
index 0000000..8435cad
--- /dev/null
+++ b/src/assets/fonts/font2.png
Binary files differ
diff --git a/src/assets/fonts/simple.png b/src/assets/fonts/simple.png
new file mode 100644
index 0000000..7b1d2a3
--- /dev/null
+++ b/src/assets/fonts/simple.png
Binary files 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<Option<web_sys::AudioBuffer>>,
+ //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<Option<web_sys::AudioBuffer>> =
+ <&_>::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<f64>, Option<f64>)>) -> Option<web_sys::AudioBufferSourceNode> {
+ 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<String, Audio>,
+
+ pub music_node: Option<web_sys::AudioBufferSourceNode>,
+}
+
+impl Assets {
+ pub fn new<F>(f : F) -> Self where F: Fn(&Context) -> HashMap<String, Audio> {
+ 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<f64>, end: Option<f64>) {
+ 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::<web_sys::HtmlCanvasElement>().ok()?;
+ let webgl2_context = c.get_context("webgl2").ok()??
+ .dyn_into::<web_sys::WebGl2RenderingContext>().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<i32> = 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<glow::Texture>,
+ pub fbo: Option<glow::Framebuffer>,
+ 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<F, G>(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<f32>,
+ indices: &Vec<u32>,
+ snormals: &Option<Vec<f32>>,
+ stexcoords: &Option<Vec<f32>>,
+ ) -> 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::<f32>(),
+ ),
+ 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::<f32>(),
+ ),
+ 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::<f32>(),
+ ),
+ 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::<f32>(),
+ ),
+ 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<String> {
+ 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::<web_sys::Response>());
+ 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<HashMap<String, glow::UniformLocation>>
+}
+
+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<String, audio::Audio>;
+ 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<audio::Assets>,
+
+ 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<G>(
+ &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<G>(&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<G>(&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)
+}