asbott / jamgine

Self-contained realtime graphical application engine with a simplistic design philosophy.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Introduction

My personal sandbox/portfolio.

I cannot offer you a fancy paper or fancy numbers in my CV, but I can offer you quality software in a jungle of terrible software getting worse by each passing day.

This project is meant to showcase the real fruits of my efforts and competence in software.

However, if you're not here as a recruiter, feel free to use anything you see. The goal is for everything to be self-contained with the only dependencies being part of the odin standard/vendor packages. Features are designed to be modular, so if you only want to use a single or a couple modules from this library that shouldn't be a problem. See examples.

I can be reached at charlie.malmqvist1@gmail.com.

Table of contents

Jamgine

Designed for good programmers.

This project can be summarized as a self-contained realtime graphical application engine, mainly designed for video games.

Everything is made from scratch with one temporary external dependency being shaderc. I am currently working on my own glsl to SPIR-v compiler.

Except for shaderc binaries, there are some unimplemented unix functions needed to compile on unix systems (osext/linux).

Vulkan backend is tested with a RTX 3060 Laptop GPU as well as 11th gen intel i5 integrated graphics.

Build & Run

If you want to run this project you would need to be on a x64 Windows system because I'm using precompiled binaries for shaderc. The plan is to replace shaderc with my own GLSL compiler, making this project entirely self-contained.

Everything is written with odin lang, a modern take on C-like low-level programming. If you want to compile and run any of the code it's quite simple to install odin and get started (odin-lang.org).

To build and run the project:
odin run "path/to/project" collection:jamgine="path/to/jamgine/repo"
To run with debug information, including vulkan layers, just append -debug to the command. By default odin compiles with minimal optimization. For an optimized build simply append -o:aggressive or -o:speed. If you only want to build the project without running, it's the same deal but replace odin run with odin build.

You can tell the engine to prioritize targetting some type of GPU over another with the following command-line arguments:

-prefer-integrated
-prefer-discrete
-prefer-virtual
-prefer-cpu

-force-integrated
-force-discrete
-force_virtual
-force-cpu

prefer simply boosts the "score" for the respective GPU type in the GPU selection algorithm while force guarantees to select a GPU of that type if possible.

Note on self-containedness

This project aims to be completely self-contained but it's not 100% so just yet. The one glaring problem at the moment is the shaderc dependency which currently limits us to modern x64 windows systems. I will reiterate that a self-contained glsl to spirv compiler is in the works but that's not likely to be ready any time soon. Other than that, there are dependencies on odin packages from the standard vendor library. Thanks to Odin, this is not really a big issue but the idea is to be free from these dependencies as well. Such dependencies are:

  • miniaudio
  • Vulkan loader
  • glfw

Points of interest

Note: Some implementation pages contain more information and showcases.

GPU Accelerated Particle emitter

(GIF might take a while to load)

Immediate Mode GUI

Also seen in the particle emitter showcase.

igui.begin_window("My Window");

igui.label("Hey there", color=gfx.GREEN);

@(static)
f32_value : f32;
igui.f32_drag("Float value: ", &f32_value);

if igui.button("Unset") {
    f32_value = 0;
}

igui.end_window();

Developer console

Technical points of interest

//
// 2D batch
imm.set_default_2D_camera(...);
imm.begin2d();

// Z ignored in orthographic projection
imm.push_translation({ 300, 300, 0 });
imm.push_rotation_z(app.elapsed_seconds);

// Rectangle at origin 0, 0 (we're using translation matrix) with size {100, 100}
imm.rectangle({0, 0, 0}, {100, 100});

imm.pop_transforms(2);

// Render text with default font (or pass custom font to parameter 'font')
imm.text("Some text", { x, y, 0 });

imm.flush();

Some Examples

More example are found in examples.

Hello Window

Opens a window which can be closed with the escape key.

package hello_window

import "jamgine:gfx"
import glfw "vendor:glfw"

running := true;

main :: proc() {
    gfx.init_and_open_window("Hello, Window!", width=600, height=400);

    for !gfx.should_window_close() && running {
        gfx.collect_window_events();

        // Grab the first key event if there was one such last frame
        if e := gfx.take_window_event(gfx.Window_Key_Event); e != nil {
            if e.action == glfw.PRESS && e.key == glfw.KEY_ESCAPE do running = false;
        }

        gfx.swap_buffers();
    }

    gfx.shutdown();
}

Hello Triangle

Draws a hard-coded white triangle with instanced drawing the low-level way with JustVK.

package hellow_triangle

import "jamgine:gfx"
import jvk "jamgine:gfx/justvk"
import "jamgine:lin" // Linear algebra library

import glfw "vendor:glfw"

import "core:log"

running := true;

main :: proc() {
    // Set up a logger so we can see messages from libraries
    context.logger = log.create_console_logger();

    // Libraries output a lot of debug messages, so you may want to
    // disable them.
    // context.logger.lowest_level = .Info;
    
    // This initializes window, JustVK and the window_surface
    gfx.init_and_open_window("Hello, Triangle!", width=600, height=400);

    vert_src :: `
        #version 450

        void main() {
            const vec2 vertices[3] = {
                vec2( -0.5,  0.5 ),
                vec2(  0.5,  0.5 ),
                vec2(  0.0, -0.5 )
            };

            vec2 pos = vertices[gl_VertexIndex % 3];

            gl_Position = vec4(pos.x, pos.y, 0.0, 1.0);
        }
    `;
    frag_src :: `
        #version 450

        layout (location = 0) out vec4 f_color;

        void main() { f_color = vec4(1.0, 1.0, 1.0, 1.0); }
    `;

    // Compile shader program
    program, program_ok := jvk.make_shader_program(vert_src, frag_src);
    assert(program_ok, "Failed compiling program"); // Compile errors are logged to stdout

    pipeline := jvk.make_pipeline(program, gfx.window_surface.render_pass);

    for !gfx.should_window_close() && running {
        gfx.collect_window_events();

        // Begin gathering draw commands targetting the window surface
        jvk.begin_draw_surface(pipeline, gfx.window_surface);

        // Clear with black
        jvk.cmd_clear(pipeline, {.COLOR}, {0.0, 0.0, 0.0, 1.0});

        // Draw one instance of 3 vertices
        jvk.cmd_draw(pipeline, 3, 1);

        // Flush draw commands
        jvk.end_draw(pipeline);

        // Grab the first key event if there was one such last frame
        if key_event := gfx.take_window_event(gfx.Window_Key_Event); key_event != nil {
            if key_event.action == glfw.PRESS && key_event.key == glfw.KEY_ESCAPE do running = false;
        }

        gfx.swap_buffers();
    }

    // Cleanup
    jvk.destroy_pipeline(pipeline);
    jvk.destroy_shader_program(program);
    gfx.shutdown();
}

Hello imm

Using imm, a more high-level immediate mode renderer with a straight-forward API.

package hello_imm

import "jamgine:gfx"
import glfw "vendor:glfw"
import "jamgine:gfx/imm"

import "core:log"

running := true;

main :: proc() {

    context.logger = log.create_console_logger();

    // We will do some 3D in this example, so enable depth testing in the window
    gfx.init_and_open_window("Hello, imm!", width=600, height=400, enable_depth_test=true);

    // Init imm & make context
    imm.init();
    imm.make_and_set_context();

    for !gfx.should_window_close() && running {
        gfx.collect_window_events();

        // Set up for 2D
        window_size := gfx.get_window_size();
        imm.set_default_2D_camera(window_size.x, window_size.y);
        imm.begin2d();

        imm.clear_target(gfx.CORNFLOWER_BLUE);

        imm.rectangle({ 100, 100, 0 }, {40, 40});

        // Rotating text
        imm.push_translation({300, 200, 0});
        imm.push_rotation_z(cast(f32)glfw.GetTime());
        imm.text("Hello, imm!", {});
        imm.pop_transforms(2);
        
        // Draw
        imm.flush();
        
        // Set up for 3D
        imm.set_default_3D_camera(window_size.x, window_size.y);
        imm.begin3d();

        imm.push_translation({4, 1, 0});
        imm.push_rotation({1, 1, 0}, cast(f32)glfw.GetTime());
        verts := imm.cube({}, { 1, 1, 1 });
        // Add some color variation so it looks more 3D
        verts[2].tint = gfx.RED;   verts[5].tint = gfx.GREEN;
        verts[8].tint = gfx.BLUE;  verts[11].tint = gfx.BLACK;
        verts[14].tint = gfx.PLUM; verts[17].tint = gfx.ORANGE;
        imm.pop_transforms(2);

        // Draw
        imm.flush();

        // Grab the first key event if there was one such last frame
        if e := gfx.take_window_event(gfx.Window_Key_Event); e != nil {
            if e.action == glfw.PRESS && e.key == glfw.KEY_ESCAPE do running = false;
        }

        gfx.swap_buffers();
    }

    // Cleanup
    imm.delete_current_context();
    imm.shutdown();
    gfx.shutdown();
}

Hello, imm_gui

package hello_imm_gui

import "jamgine:gfx"
import "jamgine:gfx/imm"
import igui "jamgine:gfx/imm/gui"

import glfw "vendor:glfw"

import "core:log"

running := true;

main :: proc() {
    context.logger = log.create_console_logger();

    gfx.init_and_open_window("Hello, imm_gui!", width=600, height=400, enable_depth_test=true);

    // Init imm & make context. This is used in imm_gui rendering.
    imm.init();
    imm.make_and_set_context();

    igui.make_and_set_gui_context();

    last_time := glfw.GetTime();
    for !gfx.should_window_close() && running {

        now := glfw.GetTime();
        delta_seconds := f32(now - last_time);
        last_time = now;

        gfx.collect_window_events();

        // Set 2D camera (will be used in imm_gui) and clear window-
        window_size := gfx.get_window_size();
        imm.set_default_2D_camera(window_size.x, window_size.y)
        imm.begin2d();
        imm.clear_target(gfx.CORNFLOWER_BLUE);
        imm.flush();

        igui.new_frame();

        igui.begin_window("Hello, gui window");

        igui.label("Hello, label!");

        @(static)
        value : f32;
        igui.f32_drag("Hello, float widget!", &value);

        igui.end_window();
        
        igui.update(delta_seconds);
        
        igui.draw();
        
        gfx.swap_buffers();

        // Grab the first key event if there was one such last frame
        if e := gfx.take_window_event(gfx.Window_Key_Event); e != nil {
            if e.action == glfw.PRESS && e.key == glfw.KEY_ESCAPE do running = false;
        }
    }

    // Cleanup
    igui.destroy_current_context();
    imm.delete_current_context();
    imm.shutdown();
    gfx.shutdown();
}

Hello Console

Integrates the developer console into the app.

package hello_console

import "jamgine:gfx"
import "jamgine:gfx/imm"
import "jamgine:console"
import glfw "vendor:glfw"

import "core:fmt"

running := true;

main :: proc() {
    // Use the console for logging
    context.logger = console.create_console_logger();

    gfx.init_and_open_window("Hello, console!", width=600, height=400, enable_depth_test=true);

    // Init imm & make context. This is used in console  rendering.
    imm.init();
    imm.make_and_set_context();

    console.init(imm.get_current_context());

    // Bind functions directly to command names
    console.bind_command("exit", proc() {
        running = false;
    });
    console.bind_command("print_hello", proc(something : string) -> string {
        return fmt.tprintf("Hello, %s!", something);
    });

    last_time := glfw.GetTime();
    for !gfx.should_window_close() && running {

        now := glfw.GetTime();
        delta_seconds := f32(now - last_time);
        last_time = now;

        // Set 2D view & clear window
        window_size := gfx.get_window_size();
        imm.set_default_2D_camera(window_size.x, window_size.y);
        imm.begin2d();
        imm.clear_target(gfx.CORNFLOWER_BLUE);
        imm.flush();

        gfx.collect_window_events();

        // Input is handled here, so where you call this matters.
        console.update(delta_seconds);
        // For example if we want the console to handle input before
        // app handles input, we would call the app input handler after.
        // input.update();

        console.draw();
        
        gfx.swap_buffers();

        // Grab the first key event if there was one such last frame
        if e := gfx.take_window_event(gfx.Window_Key_Event); e != nil {
            if e.action == glfw.PRESS && e.key == glfw.KEY_ESCAPE do running = false;
        }
    }

    // Cleanup
    console.shutdown();
    imm.delete_current_context();
    imm.shutdown();
    gfx.shutdown();
}

Hello Everything

Configures a jamgine app which sets up all main features for you.

package hello_everything

import "jamgine:gfx"
import "jamgine:gfx/imm"
import igui "jamgine:gfx/imm/gui"
import "jamgine:app"
import "jamgine:input"
import "jamgine:lin"

import "vendor:glfw"

import "core:log"

main :: proc() {

    app.init_proc     = init;
    app.shutdown_proc = shutdown;
    app.sim_proc      = simulate_app;
    app.draw_proc     = draw_app;

    app.config.enable_depth_test = true;

    app.run();
}

init :: proc() -> bool {
    /* Initialize stuff */
    return true;
}
shutdown :: proc() -> bool {
    /* Free stuff */
    return true;
}

// Called once each frame
simulate_app :: proc() -> bool {

    if input.is_key_just_pressed(glfw.KEY_F) {
        log.error("You pressed F!!!");
    }

    igui.begin_window("A window");
    igui.button("Hey there");
    igui.end_window();

    return true;
}

// Called at the end of each frame right before swapping window buffers
// but before overlay things like the console or imm_gui.
draw_app :: proc() -> bool {

    // Here imm has default 2D camera set by default

    imm.begin2d();
    imm.clear_target(gfx.CORNFLOWER_BLUE);

    imm.push_translation(lin.v3(gfx.get_window_size()/2));
    imm.push_rotation_z(app.elapsed_seconds);
    imm.rectangle({}, {100, 100});
    imm.pop_transforms(2);

    imm.flush();

    return true;
}

Upcoming

  • Audio playback library (miniaudio backend) (See Audio Branch)
  • Basic 3D models (v1)
    • .obj loading
    • Mesh animation
  • Basic 3D rendering techniques
    • Instanced, static/dynamic
    • blinn-phong, basic casters
    • PBR?
    • Shadowmapping
    • Light & shadow baking
  • Asset catalougues
    • Hot reloading
  • Emitter optimizations
  • Demo/Showcase FPS game
  • Self-contained GLSL to spv compiler
  • Technical improvements
    • Fast global allocator
    • Window system win32 backend
    • WASAPI backend
    • Fallback backends glfw/miniaudio
    • Unix support

About

Self-contained realtime graphical application engine with a simplistic design philosophy.

License:MIT License


Languages

Language:Odin 98.1%Language:CMake 1.9%