diff --git a/res/shader/fullscreen.fs b/res/shader/fullscreen.fs index d7549f8..2848c84 100644 --- a/res/shader/fullscreen.fs +++ b/res/shader/fullscreen.fs @@ -11,5 +11,7 @@ void main() coord = vec2(vTex.x, 1.0 - vTex.y); - FragColor = vec4(coord.x, coord.y, 0.0, 1.0); + vec4 col = texture(tex, coord); + + FragColor = vec4(col.xyz, 1.0); } diff --git a/res/shader/sprite.fs b/res/shader/sprite.fs new file mode 100644 index 0000000..7164a40 --- /dev/null +++ b/res/shader/sprite.fs @@ -0,0 +1,17 @@ +#version 330 +layout(location = 0) out vec3 color; + +in vec2 vTex; + +uniform sampler2D tex; + +void main() +{ + vec2 coord = vTex; + + coord = vec2(vTex.x, 1.0 - vTex.y); + + vec4 col = texture(tex, coord); + + color = col.xyz; +} diff --git a/res/shader/sprite.vs b/res/shader/sprite.vs new file mode 100644 index 0000000..3651f26 --- /dev/null +++ b/res/shader/sprite.vs @@ -0,0 +1,13 @@ +#version 330 +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec2 aTex; + +out vec2 vTex; + +uniform mat4 tform; + +void main() +{ + vTex = aTex; + gl_Position = tform * vec4(aPos.x, aPos.y, aPos.z, 1.0); +} diff --git a/res/sprite/test.png b/res/sprite/test.png new file mode 100644 index 0000000..97a20fc Binary files /dev/null and b/res/sprite/test.png differ diff --git a/src/engine/base.nim b/src/engine/base.nim index c799b0f..1233c87 100644 --- a/src/engine/base.nim +++ b/src/engine/base.nim @@ -1,49 +1,63 @@ import nimgl/[glfw, opengl] -import graphics/sprite -import graphics/renderer as rnd +import os +include base/renderer +include base/scene_manager var should_quit* = false var renderer*: Renderer = nil +var dt*: float32 -var update_fnc*: proc(dt: float, w: GLFWWindow) = nil +var update_fnc*: proc() = nil var render_fnc*: proc() = nil -var quit_fnc*: proc(w: GLFWWindow) = nil +var quit_fnc*: proc() = nil -proc launch_game*() = - assert glfwInit() +assert glfwInit() - glfwWindowHint(GLFWContextVersionMajor, 3) - glfwWindowHint(GLFWContextVersionMinor, 3) - glfwWindowHint(GLFWOpenglForwardCompat, GLFW_TRUE) # Used for Mac - glfwWindowHint(GLFWOpenglProfile, GLFW_OPENGL_CORE_PROFILE) - glfwWindowHint(GLFWResizable, GLFW_FALSE) +glfwWindowHint(GLFWContextVersionMajor, 3) +glfwWindowHint(GLFWContextVersionMinor, 3) +glfwWindowHint(GLFWOpenglForwardCompat, GLFW_TRUE) # Used for Mac +glfwWindowHint(GLFWOpenglProfile, GLFW_OPENGL_CORE_PROFILE) +glfwWindowHint(GLFWResizable, GLFW_FALSE) - let w: GLFWWindow = glfwCreateWindow(800, 600, "Minijam 110") - if w == nil: - quit(-1) - - w.makeContextCurrent() +let glfw_window*: GLFWWindow = glfwCreateWindow(800, 600, "Minijam 110") +if glfw_window == nil: + quit(-1) + +glfw_window.makeContextCurrent() + +# Active V-sync, useful to avoid FPS being in the thousands! +glfwSwapInterval(1) + +assert glInit() - assert glInit() +renderer = create_renderer("res/shader/fullscreen", 800, 600) - renderer = create_renderer("res/shader/fullscreen") +proc launch_game*() = + assert scene_stack.len > 0, "Remember to add a scene before starting the game" + + var last_time = glfwGetTime() - var dt = 0.0 # Launch the main loop while not should_quit: glfwPollEvents() - update_fnc(dt, w) + update_fnc() + scene_manager_update() glClearColor(0.0, 0.0, 0.0, 1.0) glClear(GL_COLOR_BUFFER_BIT) - renderer.render() + renderer.before_render() if render_fnc != nil: render_fnc() + scene_manager_render(renderer) + renderer.render() - w.swapBuffers() + glfw_window.swapBuffers() + var new_time = glfwGetTime() + dt = new_time - last_time + last_time = new_time - if quit_fnc != nil: quit_fnc(w) + if quit_fnc != nil: quit_fnc() - w.destroyWindow() + glfw_window.destroyWindow() glfwTerminate() \ No newline at end of file diff --git a/src/engine/base/renderer.nim b/src/engine/base/renderer.nim new file mode 100644 index 0000000..78827f5 --- /dev/null +++ b/src/engine/base/renderer.nim @@ -0,0 +1,116 @@ +# Do not "import" as it will cause issues with the global "renderer"! +# (it's already included by base) + +# Implements generic, layered drawing for drawables, binding +# only the most basic uniforms in the shaders: +# tform: camera + object transformation +# It renders to a framebuffer to allow fullscreen effects +# Layering is achieved by the use of the depth buffer! +# Drawables must implement: +# -> .position (returns Vec2f, position of the drawable), (optional if using draw_at) +# -> .shader (returns a shader to be used for the draw) +# -> .do_draw (calls the appropiate OpenGL for actually drawing the object, including additional shader stuff) +# -> .layer (returns int, the layer of the object) +# Keep layers below LAYER_CAP, as its used for scene overlaying + +import ../graphics/camera +import ../graphics/shader +import ../graphics/gl_rectangle +import glm +import nimgl/opengl + +const LAYER_CAP = 100000 + +type Renderer* = ref object + camera*: Camera + fullscreen_shader*: Shader + fbuffer, rnd_texture, depth_buffer: GLuint + # All drawable calls while layer_bias is enabled get increased by LAYER_CAP + layer_bias*: bool + width*, height*: int + +proc resize(renderer: var Renderer, width: int, height: int) = + glViewport(0, 0, width.GLsizei, height.GLsizei) + + renderer.width = width + renderer.height = height + + # Remove the old buffers if present + if renderer.fbuffer != 0: + glDeleteFramebuffers(1, addr renderer.fbuffer) + glDeleteTextures(1, addr renderer.rnd_texture) + glDeleteRenderbuffers(1, addr renderer.depth_buffer) + + glGenFramebuffers(1, addr renderer.fbuffer) + glBindFramebuffer(GL_FRAMEBUFFER, renderer.fbuffer) + + glGenTextures(1, addr renderer.rnd_texture) + glBindTexture(GL_TEXTURE_2D, renderer.rnd_texture) + + glTexImage2D(GL_TEXTURE_2D, 0'i32, GL_RGB.GLint, width.GLsizei, height.GLsizei, 0.GLint, GL_RGB, GL_UNSIGNED_BYTE, nil) + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST.GLint) + + glGenRenderbuffers(1, addr renderer.depth_buffer) + glBindRenderbuffer(GL_RENDERBUFFER, renderer.depth_buffer) + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width.GLsizei, height.GLsizei) + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderer.depth_buffer) + + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderer.rnd_texture, 0) + var dbuffers = [GL_COLOR_ATTACHMENT_0] + glDrawBuffers(1, addr dbuffers[0]) + + + +proc create_renderer*(shader_path: string, width: int, height: int): Renderer = + let shader = load_shader(shader_path) + let camera = create_camera() + var rnd = Renderer(camera: camera, fullscreen_shader: shader) + rnd.resize(width, height) + return rnd + +# Render stuff to the framebuffer after this call +proc before_render*(renderer: Renderer) = + glBindFramebuffer(GL_FRAMEBUFFER, renderer.fbuffer) + +# Stop rendering stuff to the framebuffer after this call +proc render*(renderer: Renderer) = + + # Draw the fullscreen rectangle, binding texture 0 + glBindFramebuffer(GL_FRAMEBUFFER, 0); + renderer.fullscreen_shader.use() + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, renderer.rnd_texture) + renderer.fullscreen_shader.set_int("tex", 0) + + var tform = translate(mat4f(), -1.0, -1.0, 0.0).scale(2.0, 2.0, 2.0) + renderer.fullscreen_shader.set_mat4("tform", tform) + draw_rectangle() + +proc draw*[T](renderer: Renderer, drawable: T) = + drawable.shader.use() + + let pos = drawable.position + var layer = drawable.layer + assert layer < LAYER_CAP + if renderer.layer_bias: + layer += LAYER_CAP + var tform = renderer.camera.get_transform_matrix() + tform = tform.translate(pos.x, pos.y, layer.toFloat()) + + drawable.shader.set_mat4("tform", tform) + drawable.do_draw() + +proc draw_at*[T](renderer: Renderer, drawable: T, pos: Vec2f) = + drawable.shader.use() + + var layer = drawable.layer + assert layer < LAYER_CAP + if renderer.layer_bias: + layer += LAYER_CAP + var tform = renderer.camera.get_transform_matrix() + tform = tform.translate(pos.x, pos.y, layer.toFloat()) + + drawable.shader.set_mat4("tform", tform) + drawable.do_draw() \ No newline at end of file diff --git a/src/engine/base/scene_manager.nim b/src/engine/base/scene_manager.nim new file mode 100644 index 0000000..8c3ceee --- /dev/null +++ b/src/engine/base/scene_manager.nim @@ -0,0 +1,49 @@ +# As renderer, do not manually import, as this is an integral part of the engine + +# Implements a basic scene manager based on OOP. +# Scenes may implement the following functions: +# -> init(): called on load of the scene +# -> update(): called every frame the scene is active +# -> render(): called every frame the scene is active / overlaid with rendering +# -> unload(): called on unloading of the scene +# Scenes may be overlaid, in such a way that the bottom scene renders but does not update +# (Useful for menus. Only one scene may be overlaid on top of another) +# Bottom scenes are always below top scenes via the adding of a big number to the layer of upper scene +# Scene travel works in a stack fashion + +type Scene* = ref object of RootObj + +method init(this: Scene) {.base.} = return +method update(this: Scene) {.base.} = return +method render(this: Scene) {.base.} = return +method unload(this: Scene) {.base.} = return + +var scene_stack = newSeq[Scene]() +var overlaid = false + +# Loads a new scene, optionally as an overlay +proc load_scene*(scene: Scene, overlay: bool = false) = + scene_stack.add(scene) + if overlay: + assert scene_stack.len >= 2 + assert overlaid == false + overlaid = true + +# Goes to previous scene +proc leave_scene*() = + scene_stack.setLen(scene_stack.len - 1) + if overlaid: + overlaid = false + +proc is_overlaid(): bool = return overlaid + +proc scene_manager_update*() = + scene_stack[^1].update() + +proc scene_manager_render*(renderer: Renderer) = + + scene_stack[^1].render() + if overlaid: + renderer.layer_bias = true + scene_stack[^2].render() + renderer.layer_bias = false \ No newline at end of file diff --git a/src/engine/graphics/camera.nim b/src/engine/graphics/camera.nim index 989c38e..174ed8d 100644 --- a/src/engine/graphics/camera.nim +++ b/src/engine/graphics/camera.nim @@ -3,15 +3,15 @@ import glm type Camera* = ref object - center: Vec2f + center*: Vec2f # Scale is pixels / unit, rotation in radians - scale, rotation: float + scale*, rotation*: float proc create_camera*(): Camera = return Camera(center: vec2f(0, 0), scale: 1.0, rotation: 0.0) # Obtains a transform matrix such that the world is rendered proc get_transform_matrix*(cam: Camera): Mat4f = - result = result.translate(vec3f(cam.center, 0.0)) + return mat4f().translate(vec3f(-cam.center, 0.0)) .rotate(cam.rotation, 0, 0, 1) .scale(cam.scale, cam.scale, cam.scale) \ No newline at end of file diff --git a/src/engine/graphics/renderer.nim b/src/engine/graphics/renderer.nim deleted file mode 100644 index eaf0b43..0000000 --- a/src/engine/graphics/renderer.nim +++ /dev/null @@ -1,23 +0,0 @@ -# Implements generic, layered drawing for drawables, binding -# only the most basic uniforms in the shaders: -# -import camera -import shader -import gl_rectangle -import glm - -type Renderer* = ref object - camera*: Camera - fullscreen_shader*: Shader - -proc create_renderer*(shader_path: string): Renderer = - let shader = load_shader(shader_path) - let camera = create_camera() - return Renderer(camera: camera, fullscreen_shader: shader) - -proc render*(renderer: Renderer) = - renderer.fullscreen_shader.use() - var tform = translate(mat4f(), -1.0, -1.0, 0.0).scale(2.0, 2.0, 2.0) - renderer.fullscreen_shader.set_mat4("tform", tform) - draw_rectangle() - diff --git a/src/engine/graphics/shader.nim b/src/engine/graphics/shader.nim index 2e92b70..34c49c3 100644 --- a/src/engine/graphics/shader.nim +++ b/src/engine/graphics/shader.nim @@ -61,4 +61,19 @@ proc use*(shader: Shader) = glUseProgram(shader.gl) proc set_mat4*(shader: Shader, name: cstring, mat: var Mat4f) = - glUniformMatrix4fv(glGetUniformLocation(shader.gl, name), 1.GLsizei, false, mat.caddr) \ No newline at end of file + glUniformMatrix4fv(glGetUniformLocation(shader.gl, name), 1.GLsizei, false, mat.caddr) + +proc set_int*(shader: Shader, name: cstring, val: int32) = + glUniform1i(glGetUniformLocation(shader.gl, name), val) + +proc set_vec3*(shader: Shader, name: cstring, val: Vec3f) = + glUniform3f(glGetUniformLocation(shader.gl, name), val.x, val.y, val.z) + +proc set_vec3*(shader: Shader, name: cstring, x: float32, y: float32, z: float32) = + glUniform3f(glGetUniformLocation(shader.gl, name), x, y, z) + +proc set_vec4*(shader: Shader, name: cstring, val: Vec4f) = + glUniform4f(glGetUniformLocation(shader.gl, name), val.x, val.y, val.z, val.w) + +proc set_vec4*(shader: Shader, name: cstring, x: float32, y: float32, z: float32, w: float32) = + glUniform4f(glGetUniformLocation(shader.gl, name), x, y, z, w) \ No newline at end of file diff --git a/src/engine/graphics/sprite.nim b/src/engine/graphics/sprite.nim index 3c99bef..422ed04 100644 --- a/src/engine/graphics/sprite.nim +++ b/src/engine/graphics/sprite.nim @@ -1,21 +1,26 @@ import stb_image/read as stbi import nimgl/opengl import glm +import gl_rectangle +import shader - -# An sprite allows displaying of an image, optionally clipped and colored +# An sprite allows displaying of an image, optionally clipped, colored and flipped type Sprite* = ref object texture_id: GLuint + shader*: Shader texture_width*, texture_height*: int # In UV coordinates clip*: Vec4f # Tint color color*: Vec4f - + flip_h*, flip_v*: bool + layer*: int + # Position, to be used with the renderer + position*: Vec2f proc create_sprite*(image: string): Sprite = var width, height, nCh : int - var data = stbi.load(image, width, height, nCh, 4) + var data = stbi.load(image, width, height, nCh, 0) var tex: Gluint glGenTextures(1, addr tex) @@ -28,12 +33,22 @@ proc create_sprite*(image: string): Sprite = # The OpenGL bindings are a bit annoying with the types of enums! glTexImage2D(GL_TEXTURE_2D, 0'i32, GL_RGBA.GLint, width.GLsizei, - height.GLsizei, 0.GLint, GL_RGBA, GL_UNSIGNED_BYTE, addr data) + height.GLsizei, 0.GLint, GL_RGBA, GL_UNSIGNED_BYTE, addr data[0]) return Sprite(texture_id: tex, texture_width: width, texture_height: height, - clip: vec4f(0, 0, 1, 1), color: vec4f(1.0, 1.0, 1.0, 1.0)) - - - + clip: vec4f(0, 0, 1, 1), color: vec4f(1.0, 1.0, 1.0, 1.0), + shader: load_shader("res/shader/sprite")) + + +proc do_draw*(sprite: Sprite) = + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, sprite.texture_id) + # Shader is already used by renderer draw + sprite.shader.set_int("tex", 0) + sprite.shader.set_int("flip_h", if sprite.flip_h: 1 else: 0) + sprite.shader.set_int("flip_v", if sprite.flip_v: 1 else: 0) + sprite.shader.set_vec4("tint", sprite.color) + sprite.shader.set_vec4("clip", sprite.clip) + draw_rectangle() # An animated sprite uses sprite to display an image that has animation \ No newline at end of file diff --git a/src/main.nim b/src/main.nim index d5387af..c7f3021 100644 --- a/src/main.nim +++ b/src/main.nim @@ -1,12 +1,24 @@ import nimgl/[glfw, opengl] -import "engine/base.nim" +import engine/base +import engine/graphics/sprite +import engine/graphics/camera +import glm +type MenuScene = ref object of Scene -proc update(dt: float, w: GLFWWindow) = - if w.windowShouldClose: - should_quit = true +let scene = MenuScene() +method update(this: MenuScene) = + return + +method render(this: MenuScene) = + return +load_scene(scene) + +proc update() = + if glfw_window.windowShouldClose: + should_quit = true update_fnc = update