initial commit
All checks were successful
Build (Arch Linux) / build (push) Successful in 3m10s

This commit is contained in:
2025-04-16 01:58:29 +01:00
commit a8d8b9b9ab
116 changed files with 106633 additions and 0 deletions

66
src/cli/cli.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "cli/cli.hpp"
#include <cassert>
#include <cctype>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <expected>
#include <format>
#include <print>
#include <span>
#include <string_view>
using namespace std::string_view_literals;
void cli::print_usage() {
std::println("Usage: doniv-cli [-P process_name | -p process_id] -L library_path");
return;
}
std::string_view cli::cli_args_parse_err_str(cli_args_parse_err err) {
switch (err) {
case cli_args_parse_err::invalid_argument:
return "Invalid command line arguments"sv;
case cli_args_parse_err::not_enough_args:
return "Not enough command line parameters"sv;
default:
return ""sv;
}
}
std::expected<cli, cli_args_parse_err> cli::try_construct(int argc, char** argv) {
if (argc < 1) {
return std::unexpected(cli_args_parse_err::not_enough_args);
}
const std::span<char*> cli_args(argv, argc);
int root_path_arg_idx = 0;
int prefer_x11_arg_idx = 0;
for (int i = 0; i < cli_args.size(); ++i) {
const auto& arg = cli_args[i];
const int next_idx = (i + 1);
if (std::strcmp(arg, "-root") == 0) {
if (next_idx < cli_args.size()) {
root_path_arg_idx = next_idx;
}
} else if (std::strcmp(arg, "-x11") == 0) {
if (next_idx <= cli_args.size()) {
prefer_x11_arg_idx = i;
}
}
}
std::filesystem::path root_path;
bool prefer_x11 = false;
if (root_path_arg_idx) {
const auto root_path_cstr = cli_args[root_path_arg_idx];
root_path = root_path_cstr;
}
prefer_x11 = prefer_x11_arg_idx;
return cli {root_path, prefer_x11};
}

184
src/core/engine.cpp Normal file
View File

@@ -0,0 +1,184 @@
#include "core/engine.hpp"
#include "components/camera.hpp"
#include "components/lights.hpp"
#include "components/resources.hpp"
#include "core/application.hpp"
#include "flecs/addons/cpp/c_types.hpp"
#include "graphics/opengl/program.hpp"
#include "graphics/opengl/shader.hpp"
#include "logging/engine_logger.hpp"
#include "maths/transform.hpp"
#include "renderer/gl_renderer.hpp"
#include "resources/model.hpp"
#include "resources/resource_manager.hpp"
#include "utils/assert.hpp"
#include <flecs.h>
#include <stb_image.h>
#include <algorithm> // std::clamp
#include <chrono>
#include <cmath>
#include <cmath> // std::fmod
#include <filesystem>
#include <memory>
#include <utility>
using namespace kuiper;
engine::engine(std::unique_ptr<kuiper::application>&& app)
: m_application(std::move(app)),
m_window(std::make_unique<glfw_window>(m_application->get_app_spec().width,
m_application->get_app_spec().height,
m_application->get_app_spec().name,
nullptr,
nullptr)) {
return;
}
engine::~engine() {
// Should be in reverse order of subsystems initialised in Initialise()
// NOTE: Should free window ptr here?
return;
}
std::expected<void, std::error_code> engine::initialise(const cli& cli_args) noexcept {
// OpenGL expects { 0.0, 0.0 } to be the bottom-left corner of the image
stbi_set_flip_vertically_on_load(true);
// Engine working directory
if (!cli_args.root_path.empty()) {
std::filesystem::current_path(cli_args.root_path);
}
// GLFW
glfw_window::initialise_glfw(cli_args.prefer_x11);
return {};
}
void engine::start() {
auto logger = engine_logger::get();
logger.info("Kuiper Engine starting");
m_window->lock_cursor();
m_window->enable_raw_input();
m_application->initialise(this);
auto renderer = gl::renderer::init();
// Local player & camera
auto local_player = m_world.entity("Local player");
local_player.set<maths::transform_euler>({});
auto local_camera = m_world.entity("Local camera");
local_camera.set<maths::transform_euler>({});
local_camera.set<component::camera_props>({});
local_camera.add(flecs::ChildOf, local_player);
// Per-frame flecs queries
const auto renderables =
world().query_builder<maths::transform_euler, component::model_resource>().cached().build();
const auto lights = world().query_builder<maths::transform_euler, component::point_light>().cached().build();
auto last_time_point = std::chrono::high_resolution_clock::now(); // Initialised with first time point
while (!m_window->should_close()) {
const auto time_now = std::chrono::high_resolution_clock::now();
const std::chrono::duration<float> delta_time = time_now - last_time_point;
last_time_point = time_now;
// Input
m_window->poll_events();
// State
// TODO: [update world state here]
m_application->pre_render(delta_time.count());
// Rendering
{
auto rendering_start = std::chrono::high_resolution_clock::now();
lights.each([&renderer](flecs::entity e,
const maths::transform_euler& transform,
const component::point_light& light) {
renderer.add_light(transform.position, light);
});
// Render every entity with a transform & model attached
renderables.each([this, &renderer](flecs::entity e,
const maths::transform_euler& transform,
const component::model_resource& resource) {
const auto& model = m_resources.get<resource::model>(resource.id);
if (!model.has_value())
return;
renderer.draw_model(model.value(), transform);
});
// Draw to framebuffers
for (const auto& target : m_render_targets) {
renderer.render(target, m_cam_pos, m_cam_props);
}
// Draw to default framebuffer
if (m_render_to_default) {
renderer.render(m_window->back_buffer(), m_cam_pos, m_cam_props);
}
// Debug metrics
m_metrics.renderer_metrics = renderer.metrics();
// Flush the renderer state, hand over the default frame buffer to the application
renderer.flush();
std::chrono::duration<float, std::milli> render_dur =
std::chrono::high_resolution_clock::now() - rendering_start;
m_metrics.rendering_time = render_dur.count();
}
m_window->back_buffer()->bind(); // so the user application renders to the window
m_application->post_render(delta_time.count());
m_window->swap_buffers();
// End
}
m_application->shutdown();
return;
}
void engine::add_render_target(const gl::framebuffer::s_ptr& target) noexcept {
return m_render_targets.push_back(target);
}
void engine::remove_render_target(const gl::framebuffer::s_ptr& target) noexcept {
std::ignore = std::remove(m_render_targets.begin(), m_render_targets.end(), target);
return;
}
void engine::disable_default_framebuffer() noexcept {
m_render_to_default = false;
return;
}
void engine::enable_default_framebuffer() noexcept {
m_render_to_default = true;
return;
}
void engine::set_render_camera(const glm::vec3& pos, const component::camera_props& cam_props) noexcept {
m_cam_pos = pos;
m_cam_props = cam_props;
return;
}

64
src/core/entrypoint.cpp Normal file
View File

@@ -0,0 +1,64 @@
#include "core/entrypoint.hpp"
#include "cli/cli.hpp"
#include "core/application.hpp"
#include "core/engine.hpp"
#include "logging/engine_logger.hpp"
#include <cstdlib>
#include <exception>
#include <memory>
#include <utility>
// Implemented by the user
extern std::unique_ptr<kuiper::application> kuiper::create_application(int argc, char* argv[]);
int main(int argc, char* argv[]) {
return kuiper::entrypoint(argc, argv);
}
int kuiper::entrypoint(int argc, char* argv[]) {
// Initialise logging
auto logger = engine_logger::get();
// Parse command line arguments
// NOTE: We assume that command line arguments are UTF-8 encoded at this point
const auto maybe_args = cli::try_construct(argc, argv);
if (!maybe_args) {
logger.error("Invalid command like argument: {}", cli::cli_args_parse_err_str(maybe_args.error()));
return EXIT_FAILURE;
}
const auto& cli_args = maybe_args.value();
// Create instance of client application
std::unique_ptr<kuiper::application> app = kuiper::create_application(argc, argv);
if (app == nullptr) {
logger.critical("Application was nullptr");
return EXIT_FAILURE;
}
// Initialise engine & begin execution
try {
const auto initialised = engine::initialise(cli_args);
if (!initialised) {
logger.critical("Failed to initialise engine");
return EXIT_FAILURE;
}
engine engine(std::move(app));
engine.start();
} catch (const std::exception& ex) {
logger.critical("Unhandled std::exception: {}", ex.what());
return EXIT_FAILURE;
} catch (...) {
logger.critical("Unknown unhandled exception!");
return EXIT_FAILURE;
}
return EXIT_SUCCESS; // TODO: Return error exit code if the app encounters an error during execution
}

52
src/core/io.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include "core/io.hpp"
#include "errors/exceptions.hpp"
extern "C"
{
#if defined(__linux__)
#include <pwd.h>
#include <unistd.h>
#endif
}
#include <cstddef>
#include <cstdio>
#include <format>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
using namespace kuiper;
std::string io::read_text_file(const std::filesystem::path& path) noexcept {
if (path.empty() || !std::filesystem::is_regular_file(path) || !std::filesystem::exists(path)) {
return "";
}
auto input_file = std::ifstream(path, std::ios::ate);
if (!input_file.is_open()) {
return "";
}
const auto file_size = input_file.tellg();
input_file.seekg(0);
// Use std::stringstream here (?)
std::string buf(static_cast<std::size_t>(file_size) + 1, '\0');
input_file.read(&buf[0], file_size);
input_file.close();
return buf;
}
std::filesystem::path io::get_home_dir() noexcept {
#if defined(__linux__)
const auto uid = getuid();
const auto pw = getpwuid(uid);
return {pw->pw_dir};
#endif
return {};
}

View File

@@ -0,0 +1,59 @@
#include "graphics/opengl/buffer.hpp"
#include <memory>
using namespace kuiper::gl;
buffer::s_ptr buffer::make(GLenum target) noexcept {
GLuint name = 0;
glGenBuffers(1, &name);
if (!name)
return nullptr;
return std::make_shared<buffer>(name, target);
}
void buffer::destroy() noexcept {
if (m_id)
glDeleteBuffers(1, &m_id);
return;
}
void buffer::bind() const noexcept {
return glBindBuffer(m_target, m_id);
}
void buffer::bind_to_location(std::uint32_t location) const noexcept {
return glBindBufferBase(m_target, location, m_id);
}
void buffer::upload(GLenum usage, std::size_t size, const void* data) noexcept {
m_size = size;
bind();
return glBufferData(m_target, static_cast<GLsizeiptr>(size), data, usage);
}
void buffer::update(std::size_t offset, std::size_t size, const void* data) noexcept {
bind();
return glBufferSubData(m_target, static_cast<GLintptr>(offset), static_cast<GLsizeiptr>(size), data);
}
void buffer::set_vertex_attrib(std::uint32_t index,
std::size_t size,
GLenum type,
std::size_t stride,
std::size_t offset,
bool normalised) noexcept {
bind();
return glVertexAttribPointer(index,
static_cast<GLint>(size),
type,
normalised,
static_cast<GLsizei>(stride),
reinterpret_cast<const void*>(offset));
}

View File

@@ -0,0 +1,118 @@
#include "graphics/opengl/framebuffer.hpp"
#include "graphics/opengl/texture.hpp"
#include "utils/assert.hpp"
#include <glad/gl.h>
#include <memory>
#include <utility>
using namespace kuiper::gl;
framebuffer::s_ptr framebuffer::make() noexcept {
GLuint name = 0;
glGenFramebuffers(1, &name);
if (!name)
return nullptr;
return std::make_shared<framebuffer>(name);
}
framebuffer::s_ptr framebuffer::make_default(std::uint32_t width, std::uint32_t height) noexcept {
if (!width || !height)
return nullptr;
auto fb = framebuffer::make();
if (!fb)
return nullptr;
// Colour
auto col_tex = texture::make();
col_tex->upload(
GL_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
col_tex->set_param(GL_TEXTURE_MIN_FILTER, GL_LINEAR);
col_tex->set_param(GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Depth & stencil
auto depth_tex = texture::make();
depth_tex->upload(GL_DEPTH24_STENCIL8,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
GL_DEPTH_STENCIL,
GL_UNSIGNED_INT_24_8,
nullptr);
depth_tex->set_param(GL_TEXTURE_MIN_FILTER, GL_LINEAR);
depth_tex->set_param(GL_TEXTURE_MAG_FILTER, GL_LINEAR);
fb->attach_colour(std::move(col_tex));
fb->attach_depth(std::move(depth_tex));
fb->bind();
const auto fb_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (fb_status != GL_FRAMEBUFFER_COMPLETE) {
return nullptr;
}
return fb;
}
void framebuffer::destroy() noexcept {
if (m_id) {
glDeleteFramebuffers(1, &m_id);
m_id = 0;
}
return;
}
void framebuffer::bind() const noexcept {
return glBindFramebuffer(GL_FRAMEBUFFER, m_id);
}
void framebuffer::resize(std::uint32_t width, std::uint32_t height) noexcept {
m_width = width;
m_height = height;
for (const auto& col_tex : m_colour_texs) {
if (col_tex == nullptr)
continue;
col_tex->resize(width, height);
}
if (m_depth_tex) {
m_depth_tex->resize(width, height);
}
return;
}
void framebuffer::attach_colour(texture::s_ptr&& tex, std::uint32_t attachment_point) noexcept {
KUIPER_ASSERT(attachment_point < framebuffer::max_colour_attachments);
m_colour_texs[attachment_point] = std::move(tex);
const auto& size = m_colour_texs[attachment_point]->dimensions();
m_width = size.x;
m_height = size.y;
bind();
return glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0 + attachment_point,
GL_TEXTURE_2D,
m_colour_texs[attachment_point]->id(),
0);
}
void framebuffer::attach_depth(texture::s_ptr&& tex) noexcept {
m_depth_tex = std::move(tex);
const auto& size = m_depth_tex->dimensions();
m_width = size.x;
m_height = size.y;
bind();
return glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, m_depth_tex->id(), 0);
}

View File

@@ -0,0 +1,149 @@
#include "graphics/opengl/program.hpp"
#include "glm/ext/vector_uint3.hpp"
#include "graphics/opengl/shader.hpp"
#include "logging/engine_logger.hpp"
#include <cstddef>
#include <glm/gtc/type_ptr.hpp>
#include <array>
using namespace kuiper::gl;
program::s_ptr program::make() noexcept {
GLuint name = glCreateProgram();
return std::make_shared<program>(name);
}
program::s_ptr program::from_shaders(std::initializer_list<shader::s_ptr> shaders) {
auto prog = program::make();
for (const auto& s : shaders) {
prog->attach_shader(s);
}
if (!prog->link()) {
std::array<char, 0x80> link_err_buf {};
GLsizei n_chars = 0;
glGetProgramInfoLog(prog->id(), link_err_buf.size() - 1 /* null terminator */, &n_chars, link_err_buf.data());
engine_logger::get().error("Failed to link program {}: {}", prog->id(), link_err_buf.data());
return nullptr;
}
return prog;
}
void program::destroy() noexcept {
if (m_id) {
glDeleteProgram(m_id);
m_id = 0;
}
return;
}
void program::use() const noexcept {
return glUseProgram(m_id);
}
void program::attach_shader(const shader::s_ptr& shader) noexcept {
return glAttachShader(m_id, shader->id());
}
bool program::link() noexcept {
glLinkProgram(m_id);
GLint link_status = GL_FALSE;
glGetProgramiv(m_id, GL_LINK_STATUS, &link_status);
return link_status == GL_TRUE;
}
// Uniforms
GLint program::uniform_location(const char* name) const noexcept {
const auto loc = glGetUniformLocation(m_id, name);
if (loc < 0)
engine_logger::get().error("glGetUniformLocation failed for uniform with name: \"{}\"", name);
return loc;
}
void program::set_uint(const char* uniform, std::uint32_t value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniform1ui(loc, value);
}
void program::set_int(const char* uniform, std::int32_t value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniform1i(loc, value);
}
void program::set_int_array(const char* uniform, const std::int32_t* value_arr, std::size_t len) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniform1iv(loc, static_cast<GLsizei>(len), value_arr);
}
void program::set_float(const char* uniform, float value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniform1f(loc, value);
return;
}
void program::set_vec3(const char* uniform, const glm::vec3& value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniform3f(loc, value.x, value.y, value.z);
return;
}
void program::set_uvec3(const char* uniform, const glm::uvec3& value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniform3ui(loc, value.x, value.y, value.z);
return;
}
void program::set_uvec2(const char* uniform, const glm::uvec2& value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniform2ui(loc, value.x, value.y);
return;
}
void program::set_mat3(const char* uniform, const glm::mat3& value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniformMatrix3fv(loc, 1, GL_FALSE, glm::value_ptr(value));
return;
}
void program::set_mat4(const char* uniform, const glm::mat4& value) noexcept {
const auto loc = uniform_location(uniform);
use();
glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(value));
return;
}

View File

@@ -0,0 +1,94 @@
#include "graphics/opengl/render_context.hpp"
#include "logging/engine_logger.hpp"
#include <glad/gl.h>
#include <GLFW/glfw3.h>
using namespace kuiper;
using namespace kuiper::gl;
void gl::initialise() {
int version = gladLoadGL(glfwGetProcAddress);
if (!version) {
throw std::runtime_error("Failed to load & resolve OpenGL symbols with GLAD");
}
// Debug logging
#if !defined(NDEBUG)
GLint flags = 0;
glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) {
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(gl::debug_callback, nullptr);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);
}
#endif
auto logger = engine_logger::get();
logger.debug("OpenGL version information:");
logger.debug("\tVendor: {}", (const char*) glGetString(GL_VENDOR));
logger.debug("\tRenderer: {}", (const char*) glGetString(GL_RENDERER));
logger.debug("\tVersion: {}", (const char*) glGetString(GL_VERSION));
logger.debug("\tGLSL version: {}", (const char*) glGetString(GL_SHADING_LANGUAGE_VERSION));
#if !defined(NDEBUG)
dump_hardware_info();
#endif
return;
}
void gl::dump_hardware_info() {
auto& logger = engine_logger::get();
logger.debug("OpenGL driver information:");
logger.debug("\tGL_MAX_FRAGMENT_UNIFORM_BLOCKS: {}", render_context::get<GLint>(GL_MAX_FRAGMENT_UNIFORM_BLOCKS));
logger.debug("\tGL_MAX_UNIFORM_BLOCK_SIZE: {}", render_context::get<GLint>(GL_MAX_UNIFORM_BLOCK_SIZE));
logger.debug("\tGL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: {}",
render_context::get<GLint>(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT));
logger.debug("\tGL_MAX_COLOR_ATTACHMENTS: {}", render_context::get<GLint>(GL_MAX_COLOR_ATTACHMENTS));
logger.debug("\tGL_MAX_DRAW_BUFFERS: {}", render_context::get<GLint>(GL_MAX_DRAW_BUFFERS));
logger.debug("\tGL_MAX_VERTEX_TEXTURE_IMAGE_UNITS: {}",
render_context::get<GLint>(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS));
logger.debug("\tGL_MAX_TEXTURE_IMAGE_UNITS: {}", render_context::get<GLint>(GL_MAX_TEXTURE_IMAGE_UNITS));
logger.debug("\tGL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: {}",
render_context::get<GLint>(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS));
return;
}
#if !defined(NDEBUG)
void gl::debug_callback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
const void* userParam) noexcept {
auto logger = engine_logger::get();
logger.error("OpenGL error: {}", message);
}
#endif
void render_context::bind_framebuffer(const framebuffer::s_ptr& fb) noexcept {
return fb->bind();
}
void render_context::bind_default_framebuffer() noexcept {
return glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void render_context::set_viewport_size(std::uint32_t width, std::uint32_t height) noexcept {
return glViewport(0, 0, static_cast<GLint>(width), static_cast<GLint>(height));
}
void render_context::enable_feature(GLenum feature) noexcept {
return glEnable(feature);
}
void render_context::disable_feature(GLenum feature) noexcept {
return glDisable(feature);
}

View File

@@ -0,0 +1,91 @@
#include "graphics/opengl/shader.hpp"
#include <filesystem>
#include <fstream>
#include <string_view>
#include <vector>
using namespace kuiper::gl;
shader::s_ptr shader::make(shader_type type) noexcept {
GLenum gl_type = 0;
switch (type) {
case shader_type::vertex:
gl_type = GL_VERTEX_SHADER;
break;
case shader_type::fragment:
gl_type = GL_FRAGMENT_SHADER;
break;
case shader_type::compute:
gl_type = GL_COMPUTE_SHADER;
break;
default:
return nullptr;
}
GLuint name = glCreateShader(gl_type);
if (!name)
return nullptr;
return std::make_shared<shader>(name, gl_type);
}
shader::s_ptr shader::from_path(const std::filesystem::path& shader_path, shader_type type) noexcept {
if (shader_path.empty() || !std::filesystem::is_regular_file(shader_path) || !std::filesystem::exists(shader_path))
return nullptr;
const auto file_size = std::filesystem::file_size(shader_path);
std::vector<char> read_buf(file_size + 1); // +1 for NUL terminator
std::ifstream in_file(shader_path); // TODO: std::ios::ate & tellg() trick
if (!in_file.is_open())
return nullptr;
in_file.read(read_buf.data(), file_size);
std::string_view file_ext(shader_path.extension().c_str());
if (type == shader_type::none) {
if (file_ext.compare(".vs") == 0 || file_ext.compare(".vert") == 0) {
type = shader_type::vertex;
} else if (file_ext.compare(".fs") == 0 || file_ext.compare(".frag") == 0) {
type = shader_type::fragment;
} else if (file_ext.compare(".cs") == 0 || file_ext.compare(".comp") == 0) {
type = shader_type::compute;
}
}
return shader::from_source(read_buf.data(), type);
}
shader::s_ptr shader::from_source(std::string_view source, shader_type type) noexcept {
auto shader = shader::make(type);
shader->attach_source(source);
shader->compile();
return shader;
}
void shader::destroy() noexcept {
if (m_id) {
glDeleteShader(m_id);
m_id = 0;
}
return;
}
void shader::attach_source(std::string_view source) noexcept {
const GLint n_shader_sources = 1;
const char* const s = source.data();
const GLint s_len = static_cast<GLint>(source.size());
return glShaderSource(m_id, n_shader_sources, &s, &s_len);
}
void shader::compile() noexcept {
return glCompileShader(m_id);
}

View File

@@ -0,0 +1,136 @@
#include "graphics/opengl/texture.hpp"
#include <filesystem>
#include <stb_image.h>
#include <fstream>
#include <vector>
using namespace kuiper::gl;
texture::s_ptr texture::make(texture_type type) noexcept {
GLuint name = 0;
glGenTextures(1, &name);
if (!name)
return nullptr;
GLenum gl_target = 0;
switch (type) {
case texture_type::tex_2d:
gl_target = GL_TEXTURE_2D;
break;
case texture_type::none:
return nullptr;
}
return std::make_shared<texture>(name, gl_target);
}
texture::s_ptr texture::make_placeholder() noexcept {
// FIXME: this might be cooked because it's endian-dependent
static constexpr std::array<std::uint32_t, 16> data {0xff00ffff,
0xff00ffff,
0x000000ff,
0x000000ff,
0xff00ffff,
0xff00ffff,
0x000000ff,
0x000000ff,
0x000000ff,
0x000000ff,
0xff00ffff,
0xff00ffff,
0x000000ff,
0x000000ff,
0xff00ffff,
0xff00ffff};
auto tex = texture::make();
tex->upload(GL_RGBA, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, data.data());
tex->gen_mipmaps();
return tex;
}
texture::s_ptr texture::from_image_path(const std::filesystem::path& image_path, bool flip_y) noexcept {
if (image_path.empty() || !std::filesystem::is_regular_file(image_path) || !std::filesystem::exists(image_path))
return nullptr;
const auto file_size = std::filesystem::file_size(image_path);
std::vector<std::uint8_t> read_buf(file_size);
std::ifstream in_file(image_path, std::ios::binary); // TODO: std::ios::ate & tellg() trick
if (!in_file.is_open())
return nullptr;
in_file.read((char*) read_buf.data(), file_size);
return texture::from_image_data(read_buf.data(), read_buf.size(), flip_y);
}
texture::s_ptr texture::from_image_data(const std::uint8_t* buffer, std::size_t length, bool flip_y) noexcept {
if (buffer == nullptr || length == 0)
return nullptr;
stbi_set_flip_vertically_on_load(flip_y);
int width = 0;
int height = 0;
int channels_in_file = 0;
std::uint8_t* image_data =
stbi_load_from_memory(buffer, static_cast<int>(length), &width, &height, &channels_in_file, 4);
if (!image_data)
return nullptr;
auto tex = texture::make();
tex->upload(GL_RGBA, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image_data);
stbi_image_free(image_data);
return tex;
}
void texture::destroy() noexcept {
if (m_id) {
glDeleteTextures(1, &m_id);
m_id = 0;
}
return;
}
void texture::bind() noexcept {
return glBindTexture(m_target, m_id);
}
void texture::upload(
GLint internal_fmt, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* data) noexcept {
m_width = width;
m_height = height;
m_internal_fmt = internal_fmt;
m_pixel_fmt = format;
m_pixel_type = type;
bind();
return glTexImage2D(m_target, 0, internal_fmt, width, height, 0, format, type, data);
}
void texture::resize(std::uint32_t width, std::uint32_t height) noexcept {
return upload(
m_internal_fmt, static_cast<GLsizei>(width), static_cast<GLsizei>(height), m_pixel_fmt, m_pixel_type, nullptr);
}
void texture::set_param(GLenum name, GLint param) noexcept {
bind();
return glTexParameteri(m_target, name, param);
}
void texture::gen_mipmaps() noexcept {
bind();
return glGenerateMipmap(m_target);
}

View File

@@ -0,0 +1,80 @@
#include "graphics/opengl/texture_array.hpp"
#include "graphics/opengl/texture.hpp"
using namespace kuiper::gl;
texture_array::s_ptr texture_array::make(GLenum target) noexcept {
GLuint name = 0;
glCreateTextures(target, 1, &name);
if (!name)
return nullptr;
return std::make_shared<texture_array>(name, target);
}
void texture_array::destroy() noexcept {
if (m_id) {
glDeleteTextures(1, &m_id);
m_id = 0;
}
return;
}
void texture_array::bind(std::uint32_t unit) const noexcept {
return glBindTextureUnit(unit, m_id);
}
void texture_array::set_storage(GLenum internal_fmt,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth) noexcept {
constexpr GLsizei mip_levels = 1; // only allocating storage for level 0, call gen_mipmaps()
glTextureStorage3D(m_id,
mip_levels,
internal_fmt,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLsizei>(depth));
m_internal_fmt = internal_fmt;
m_width = width;
m_height = height;
m_depth = depth;
return;
}
void texture_array::upload(std::uint32_t x_offset,
std::uint32_t y_offset,
std::uint32_t z_offset,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth,
GLenum format,
GLenum type,
const void* data,
std::uint32_t mip_level) noexcept {
return glTextureSubImage3D(m_id,
static_cast<GLint>(mip_level),
static_cast<GLint>(x_offset),
static_cast<GLint>(y_offset),
static_cast<GLint>(z_offset),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLsizei>(depth),
format,
type,
data);
}
void texture_array::gen_mipmaps() noexcept {
return glGenerateTextureMipmap(m_id);
}
void texture_array::set_param(GLenum name, GLint param) noexcept {
return glTextureParameteri(m_id, name, param);
}

View File

@@ -0,0 +1,47 @@
#include "graphics/opengl/vertex_array.hpp"
#include <glad/gl.h>
#include <memory>
using namespace kuiper::gl;
vertex_array::s_ptr vertex_array::make() noexcept {
GLuint name = 0;
glGenVertexArrays(1, &name);
if (!name)
return nullptr;
return std::make_shared<vertex_array>(name);
}
void vertex_array::destroy() noexcept {
if (m_id)
glDeleteVertexArrays(1, &m_id);
return;
}
void vertex_array::bind() const noexcept {
return glBindVertexArray(m_id);
}
void vertex_array::enable_attrib(std::uint32_t index) noexcept {
bind();
return glEnableVertexAttribArray(index);
}
void vertex_array::disable_attrib(std::uint32_t index) noexcept {
bind();
return glDisableVertexAttribArray(index);
}
void vertex_array::set_binding_divisor(std::uint32_t binding, std::uint32_t divisor) noexcept {
bind();
return glVertexBindingDivisor(binding, divisor);
}
void vertex_array::set_attrib_divisor(std::uint32_t attrib, std::uint32_t divisor) noexcept {
bind();
return glVertexAttribDivisor(attrib, divisor);
}

View File

@@ -0,0 +1,304 @@
#include "platform/glfw/glfw_window.hpp"
#include <GLFW/glfw3.h>
#include <glad/gl.h>
#include "graphics/opengl/framebuffer.hpp"
#include "graphics/opengl/render_context.hpp"
#include "logging/engine_logger.hpp"
#include "utils/assert.hpp"
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <utility>
using namespace kuiper;
glfw_window::glfw_window(
std::uint32_t width, std::uint32_t height, std::u8string_view title, GLFWmonitor* monitor, GLFWwindow* share)
: m_window_handle(create_glfw_window(width, height, title, monitor, share)),
m_default_framebuffer(std::make_shared<gl::framebuffer>(gl::framebuffer(0))) {
if (!m_window_handle) {
throw std::runtime_error("glfwCreateWindow failed, returned nullptr");
}
// Window resize callback
glfwSetFramebufferSizeCallback(m_window_handle, glfw_window::glfw_framebuffer_size_callback);
glfwSetWindowContentScaleCallback(m_window_handle, glfw_window::glfw_content_scale_callback);
// Input callbacks
glfwSetKeyCallback(m_window_handle, glfw_window::glfw_key_event_callback);
glfwSetCursorPosCallback(m_window_handle, glfw_window::glfw_cursor_pos_callback);
glfwSetScrollCallback(m_window_handle, glfw_window::glfw_scroll_callback);
glfwSetDropCallback(m_window_handle, glfw_window::glfw_dragdrop_callback);
// Make instance of this GLFWWindow available to static callbacks
glfwSetWindowUserPointer(m_window_handle, this);
// Initialise OpenGL context
glfwMakeContextCurrent(m_window_handle);
gl::initialise();
// Fixes framebuffer/viewport not being sized correctly on scalings other than 1.0 x 1.0
resize_back_buffer();
// Enable required OpenGL features
gl::render_context::enable_feature(GL_DEPTH_TEST); // depth buffer
// gl::render_context::disable_feature(GL_STENCIL_TEST); // stencil buffer
gl::render_context::enable_feature(GL_CULL_FACE); // cull backfacing triangles
gl::render_context::enable_feature(GL_BLEND); // blending
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
return;
}
glfw_window::~glfw_window() {
if (m_window_handle != nullptr) {
glfwDestroyWindow(m_window_handle);
}
return;
}
void* glfw_window::get_native_handle() const noexcept {
return m_window_handle;
}
void glfw_window::resize_back_buffer() noexcept {
const auto framebuffer_size = get_framebuffer_size();
const auto width = framebuffer_size.x;
const auto height = framebuffer_size.y;
m_default_framebuffer->resize(width, height);
gl::render_context::set_viewport_size(width, height);
return;
}
void glfw_window::initialise_glfw(bool prefer_x11) {
auto logger = engine_logger::get();
// Check for potential GLFW version mismatches & incompatibilities
logger.debug("Compiled against GLFW {}.{}.{}", GLFW_VERSION_MAJOR, GLFW_VERSION_MINOR, GLFW_VERSION_REVISION);
logger.debug("GLFW version string: {}", glfwGetVersionString());
int glfw_ver_major = 0, glfw_ver_minor = 0, glfw_ver_patch = 0;
glfwGetVersion(&glfw_ver_major, &glfw_ver_minor, &glfw_ver_patch);
if (glfw_ver_major > GLFW_VERSION_MAJOR || glfw_ver_minor < GLFW_VERSION_MINOR) {
logger.warn("Potentially incompatible GLFW loaded: {}.{}.{} (loaded) vs. {}.{}.{} (compiled-for)",
glfw_ver_major,
glfw_ver_minor,
glfw_ver_patch,
GLFW_VERSION_MAJOR,
GLFW_VERSION_MINOR,
GLFW_VERSION_REVISION);
}
// GLFW initialisation
// Set a callback for logging GLFW errors
glfwSetErrorCallback(glfw_window::glfw_error_callback);
// Initialisation hints
if (prefer_x11) {
logger.info("Hinting to GLFW that X11 should be used");
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
}
// Disable libdecor because I don't care about it, GTK, GNOME or anything else.
// Also it prints out an annoying error in the console when it fails to load it.
glfwInitHint(GLFW_WAYLAND_LIBDECOR, GLFW_WAYLAND_DISABLE_LIBDECOR);
if (!glfwInit()) {
throw std::runtime_error("Failed to initialise GLFW");
}
return;
}
GLFWwindow* glfw_window::create_glfw_window(std::uint32_t width,
std::uint32_t height,
std::u8string_view title,
GLFWmonitor* monitor,
GLFWwindow* share) noexcept {
KUIPER_ASSERT((width < std::numeric_limits<int>::max()) && (height < std::numeric_limits<int>::max()));
push_window_hints();
return glfwCreateWindow(
static_cast<int>(width), static_cast<int>(height), reinterpret_cast<const char*>(title.data()), monitor, share);
}
void glfw_window::push_window_hints() noexcept {
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_FALSE);
glfwWindowHint(GLFW_SCALE_FRAMEBUFFER, GLFW_FALSE);
#if !defined(NDEBUG)
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
#endif
return;
}
bool glfw_window::should_close() const noexcept {
return glfwWindowShouldClose(m_window_handle);
}
void glfw_window::poll_events() noexcept {
const auto old_mouse_pos = m_input_state.get_mouse_pos();
glfwPollEvents();
const auto delta = m_input_state.get_mouse_pos() - old_mouse_pos;
m_input_state.set_mouse_delta(delta);
return;
}
void glfw_window::swap_buffers() noexcept {
return glfwSwapBuffers(m_window_handle);
}
void glfw_window::set_swap_interval(std::uint32_t interval) noexcept {
return glfwSwapInterval(static_cast<int>(interval));
}
void glfw_window::bind_context() noexcept {
*(volatile unsigned long long*) nullptr = 0x1337beefcafebabe;
}
gl::framebuffer::s_ptr glfw_window::back_buffer() const noexcept {
return m_default_framebuffer;
}
bool glfw_window::is_cursor_locked() const noexcept {
return m_cursor_locked;
}
void glfw_window::lock_cursor() noexcept {
glfwSetInputMode(m_window_handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
m_cursor_locked = true;
return;
}
void glfw_window::unlock_cursor() noexcept {
glfwSetInputMode(m_window_handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
m_cursor_locked = false;
return;
}
void glfw_window::enable_raw_input() noexcept {
if (glfwRawMouseMotionSupported())
glfwSetInputMode(m_window_handle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
return;
}
void glfw_window::disable_raw_input() noexcept {
if (glfwRawMouseMotionSupported())
glfwSetInputMode(m_window_handle, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);
return;
}
glm::uvec2 glfw_window::get_size() const noexcept {
int width = 0;
int height = 0;
glfwGetWindowSize(m_window_handle, &width, &height);
// I don't understand why the width and height is a signed integer
// I'd like to see a window with negative width & height
KUIPER_ASSERT((width > 0) && (height > 0));
return {static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height)};
}
glm::uvec2 glfw_window::get_framebuffer_size() const noexcept {
int width = 0;
int height = 0;
glfwGetFramebufferSize(m_window_handle, &width, &height);
// Once again: why would the framebuffer ever have negative dimensions?
KUIPER_ASSERT((width > 0) && (height > 0));
return {static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height)};
}
glm::vec2 glfw_window::get_content_scale() const noexcept {
float xscale = 1.0f;
float yscale = 1.0f;
glfwGetWindowContentScale(m_window_handle, &xscale, &yscale);
return glm::vec2(xscale, yscale);
}
void glfw_window::set_title(std::u8string_view title) noexcept {
return glfwSetWindowTitle(m_window_handle, reinterpret_cast<const char*>(title.data()));
}
bool glfw_window::is_minimised() const noexcept {
int iconified = glfwGetWindowAttrib(m_window_handle, GLFW_ICONIFIED);
return (iconified == GLFW_TRUE);
}
// Callbacks
void glfw_window::glfw_error_callback(int error_code, const char* description) {
engine_logger::get().error("GLFW error {}: {}", error_code, description);
return;
}
void glfw_window::glfw_framebuffer_size_callback(GLFWwindow* window, int width, int height) {
KUIPER_ASSERT((width > 0) && (height > 0));
engine_logger::get().debug("Framebuffer size changed: {} x {}", width, height);
glfw_window* window_instance = static_cast<glfw_window*>(glfwGetWindowUserPointer(window));
window_instance->m_default_framebuffer->resize(width, height);
return;
}
void glfw_window::glfw_content_scale_callback(GLFWwindow* window, float xscale, float yscale) {
KUIPER_ASSERT(xscale > 0 && yscale > 0);
engine_logger::get().debug("Window content scale changed: x: {}, y: {}", xscale, yscale);
return;
}
void glfw_window::glfw_key_event_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
glfw_window* window_instance = static_cast<glfw_window*>(glfwGetWindowUserPointer(window));
window_instance->m_input_state.set_key_state(key, action);
return;
}
void glfw_window::glfw_cursor_pos_callback(GLFWwindow* window, double xpos, double ypos) {
glfw_window* window_instance = static_cast<glfw_window*>(glfwGetWindowUserPointer(window));
window_instance->m_input_state.set_mouse_pos({xpos, ypos});
return;
}
void glfw_window::glfw_scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
return;
}
void glfw_window::glfw_dragdrop_callback(GLFWwindow* window, int path_count, const char* paths[]) {
return;
}

View File

@@ -0,0 +1,423 @@
#include "renderer/gl_renderer.hpp"
#include "components/camera.hpp"
#include "core/io.hpp"
#include "graphics/opengl/buffer.hpp"
#include "graphics/opengl/render_context.hpp"
#include "graphics/opengl/shader.hpp"
#include "graphics/opengl/vertex_array.hpp"
#include "maths/aspect_ratio.hpp"
#include "resources/primitive.hpp"
#include "utils/assert.hpp"
#include "utils/colours.hpp"
// These headers are generated using bin2hpp during build
#include "clustering_cs.hpp"
#include "default_fs.hpp"
#include "default_vs.hpp"
#include "light_culling_cs.hpp"
#include <glad/gl.h>
#include <algorithm>
#include <cstdint>
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/geometric.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/mat3x3.hpp>
#include <glm/mat4x4.hpp>
#include <glm/matrix.hpp>
#include <glm/trigonometric.hpp>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <cstddef>
#include <numeric>
#include <string_view>
using namespace kuiper::gl;
glm::mat4 camera_to_view_matrix(const glm::vec3& pos, const kuiper::component::camera_props& props) noexcept;
renderer renderer::init() {
gl_state state {};
// Configuration
state.max_texture_units = render_context::get<GLint>(GL_MAX_TEXTURE_IMAGE_UNITS);
state.texture_unit_slots.resize(state.max_texture_units);
std::iota(state.texture_unit_slots.begin(), state.texture_unit_slots.end(), 0);
// Buffers
state.vertex_buffer = buffer::make(GL_ARRAY_BUFFER);
state.index_buffer = buffer::make(GL_ELEMENT_ARRAY_BUFFER);
state.vao = vertex_array::make();
state.vao->bind();
constexpr std::uint32_t pos_idx = 0;
constexpr std::uint32_t normal_idx = 1;
constexpr std::uint32_t tangent_idx = 2;
constexpr std::uint32_t uv_idx = 3;
constexpr std::uint32_t draw_id_idx = 4;
constexpr std::uint32_t model_mat_idx = 5; // mat4 occupies slot 5, 6, 7 & 8
constexpr std::uint32_t normal_mat_idx = 9;
state.vao->enable_attrib(pos_idx);
state.vao->enable_attrib(normal_idx);
state.vao->enable_attrib(tangent_idx);
state.vao->enable_attrib(uv_idx);
state.vao->enable_attrib(draw_id_idx);
state.vao->enable_attrib(model_mat_idx + 0);
state.vao->enable_attrib(model_mat_idx + 1);
state.vao->enable_attrib(model_mat_idx + 2);
state.vao->enable_attrib(model_mat_idx + 3);
state.vao->enable_attrib(normal_mat_idx + 0);
state.vao->enable_attrib(normal_mat_idx + 1);
state.vao->enable_attrib(normal_mat_idx + 2);
state.index_buffer->bind();
state.vertex_buffer->bind();
state.vertex_buffer->set_vertex_attrib(
pos_idx, glm::vec3::length(), GL_FLOAT, sizeof(renderer::vertex_t), offsetof(renderer::vertex_t, position));
state.vertex_buffer->set_vertex_attrib(normal_idx,
glm::vec3::length(),
GL_FLOAT,
sizeof(renderer::vertex_t),
offsetof(renderer::vertex_t, normal),
true);
state.vertex_buffer->set_vertex_attrib(
tangent_idx, glm::vec3::length(), GL_FLOAT, sizeof(renderer::vertex_t), offsetof(renderer::vertex_t, tangent));
state.vertex_buffer->set_vertex_attrib(
uv_idx, glm::vec2::length(), GL_FLOAT, sizeof(renderer::vertex_t), offsetof(renderer::vertex_t, uv), true);
state.matrix_buffer = buffer::make(GL_ARRAY_BUFFER);
state.matrix_buffer->bind();
// mat4 is essentially four vec4s
state.matrix_buffer->set_vertex_attrib(model_mat_idx + 0,
glm::vec4::length(),
GL_FLOAT,
sizeof(render_matrices),
offsetof(render_matrices, model_matrix) + (0 * sizeof(glm::vec4)));
state.matrix_buffer->set_vertex_attrib(model_mat_idx + 1,
glm::vec4::length(),
GL_FLOAT,
sizeof(render_matrices),
offsetof(render_matrices, model_matrix) + (1 * sizeof(glm::vec4)));
state.matrix_buffer->set_vertex_attrib(model_mat_idx + 2,
glm::vec4::length(),
GL_FLOAT,
sizeof(render_matrices),
offsetof(render_matrices, model_matrix) + (2 * sizeof(glm::vec4)));
state.matrix_buffer->set_vertex_attrib(model_mat_idx + 3,
glm::vec4::length(),
GL_FLOAT,
sizeof(render_matrices),
offsetof(render_matrices, model_matrix) + (3 * sizeof(glm::vec4)));
state.matrix_buffer->set_vertex_attrib(normal_mat_idx + 0,
glm::vec3::length(),
GL_FLOAT,
sizeof(render_matrices),
offsetof(render_matrices, normal_matrix) + (0 * sizeof(glm::vec3)));
state.matrix_buffer->set_vertex_attrib(normal_mat_idx + 1,
glm::vec3::length(),
GL_FLOAT,
sizeof(render_matrices),
offsetof(render_matrices, normal_matrix) + (1 * sizeof(glm::vec3)));
state.matrix_buffer->set_vertex_attrib(normal_mat_idx + 2,
glm::vec3::length(),
GL_FLOAT,
sizeof(render_matrices),
offsetof(render_matrices, normal_matrix) + (2 * sizeof(glm::vec3)));
state.indirect_buffer = buffer::make(GL_DRAW_INDIRECT_BUFFER);
glBindBuffer(GL_ARRAY_BUFFER, state.indirect_buffer->id());
glVertexAttribIPointer(draw_id_idx,
1,
GL_UNSIGNED_INT,
sizeof(draw_elements_indirect_cmd),
reinterpret_cast<const void*>(offsetof(draw_elements_indirect_cmd, base_instance)));
state.vao->set_attrib_divisor(pos_idx, 0); // every fragment
state.vao->set_attrib_divisor(normal_idx, 0);
state.vao->set_attrib_divisor(tangent_idx, 0);
state.vao->set_attrib_divisor(uv_idx, 0);
state.vao->set_attrib_divisor(draw_id_idx, 1); // every instance
state.vao->set_attrib_divisor(model_mat_idx + 0, 1);
state.vao->set_attrib_divisor(model_mat_idx + 1, 1);
state.vao->set_attrib_divisor(model_mat_idx + 2, 1);
state.vao->set_attrib_divisor(model_mat_idx + 3, 1);
state.vao->set_attrib_divisor(normal_mat_idx + 0, 1);
state.vao->set_attrib_divisor(normal_mat_idx + 1, 1);
state.vao->set_attrib_divisor(normal_mat_idx + 2, 1);
glBindVertexArray(0);
state.cluster_buffer = buffer::make(GL_SHADER_STORAGE_BUFFER);
state.cluster_buffer->upload(GL_STATIC_COPY, NUM_CLUSTERS * sizeof(cluster), nullptr);
state.lights_buffer = buffer::make(GL_SHADER_STORAGE_BUFFER);
// Shaders
auto vertex = shader::from_source(bin::default_vs, shader_type::vertex);
// We pre-process the fragment shader to set the correct number of texture unit slots on the system
std::string fragment_src(bin::default_fs);
constexpr std::string_view fs_search_and_replace {"const uint MAX_FRAG_TEX_UNITS = 32u;"};
fragment_src.replace(fragment_src.find(fs_search_and_replace),
fs_search_and_replace.length(),
std::format("const uint MAX_FRAG_TEX_UNITS = {}u;", state.max_texture_units));
auto fragment = shader::from_source(fragment_src, shader_type::fragment);
state.pbr_shader = program::from_shaders({vertex, fragment});
vertex->destroy();
fragment->destroy();
auto clustering_shader = shader::from_source(bin::clustering_cs, shader_type::compute);
state.clustering_shader = program::from_shaders({clustering_shader});
clustering_shader->destroy();
auto light_culling_shader = shader::from_source(bin::light_culling_cs, shader_type::compute);
state.light_culling_shader = program::from_shaders({light_culling_shader});
light_culling_shader->destroy();
return {std::move(state)};
}
void renderer::render(const framebuffer::s_ptr& framebuffer,
const glm::vec3& cam_pos,
const component::camera_props& cam_props) noexcept {
update_and_upload();
// Framebuffer
const auto& framebuffer_size = framebuffer->dimensions();
float fb_x = static_cast<float>(framebuffer_size.x);
float fb_y = static_cast<float>(framebuffer_size.y);
framebuffer->bind();
render_context::set_viewport_size(framebuffer_size.x, framebuffer_size.y);
glClearColor(colours::black.r, colours::black.g, colours::black.b, colours::black.a);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// View & projection matrices
const auto view_matrix = camera_to_view_matrix(cam_pos, cam_props);
const auto projection_matrix =
glm::perspective(cam_props.fov, maths::aspect_ratio<float>(fb_x, fb_y), NEAR_Z_DEPTH, FAR_Z_DEPTH);
// Compute shaders: clustering & light culling
constexpr glm::uvec3 grid_size_vec {NUM_CLUSTERS_X, NUM_CLUSTERS_Y, NUM_CLUSTERS_Z};
m_state.clustering_shader->use();
m_state.clustering_shader->set_float("z_near", NEAR_Z_DEPTH);
m_state.clustering_shader->set_float("z_far", FAR_Z_DEPTH);
m_state.clustering_shader->set_mat4("inverse_proj_mat", glm::inverse(projection_matrix));
m_state.clustering_shader->set_uvec3("grid_size", grid_size_vec);
m_state.clustering_shader->set_uvec2("screen_dimensions", framebuffer_size);
m_state.cluster_buffer->bind_to_location(1);
glDispatchCompute(NUM_CLUSTERS_X, NUM_CLUSTERS_Y, NUM_CLUSTERS_Z);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
m_state.light_culling_shader->use();
m_state.light_culling_shader->set_mat4("view_mat", view_matrix);
m_state.cluster_buffer->bind_to_location(1);
m_state.lights_buffer->bind_to_location(2);
glDispatchCompute(NUM_CLUSTERS / LOCAL_SIZE, 1, 1);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
// Geometry & shading
m_state.vao->bind();
m_state.pbr_shader->use();
m_state.pbr_shader->set_mat4("view_mat", view_matrix);
m_state.pbr_shader->set_mat4("view_proj_mat", projection_matrix * view_matrix);
m_state.pbr_shader->set_float("z_near", NEAR_Z_DEPTH);
m_state.pbr_shader->set_float("z_far", FAR_Z_DEPTH);
m_state.pbr_shader->set_uvec3("grid_size", grid_size_vec);
m_state.pbr_shader->set_uvec2("screen_dimensions", framebuffer_size);
m_state.pbr_shader->set_vec3("view_pos", cam_pos);
m_state.pbr_shader->set_int_array(
"packed_tex", m_state.texture_unit_slots.data(), m_state.texture_unit_slots.size());
for (std::uint32_t draw_count = 0; draw_count < m_draw_cmds.size(); draw_count += m_state.max_texture_units) {
for (std::uint32_t i = 0; i < m_state.max_texture_units && i + draw_count < m_materials.size(); ++i) {
m_materials[i + draw_count].textures->bind(i);
}
glMultiDrawElementsIndirect(
GL_TRIANGLES,
GL_UNSIGNED_INT,
reinterpret_cast<const void*>(draw_count * sizeof(draw_elements_indirect_cmd)),
static_cast<GLsizei>(
std::min(static_cast<std::uint32_t>(m_draw_cmds.size() - draw_count), m_state.max_texture_units)),
sizeof(draw_elements_indirect_cmd));
}
return;
}
void renderer::flush() noexcept {
m_draw_cmds.clear();
m_matrices.clear();
m_vertices.clear();
m_indices.clear();
m_materials.clear();
m_lights.clear();
return;
}
renderer_metrics renderer::metrics() const noexcept {
renderer_metrics metrics {};
metrics.n_vertices = m_vertices.size();
metrics.n_indices = m_indices.size();
metrics.n_draw_cmds = m_draw_cmds.size();
metrics.n_matrices = m_matrices.size();
metrics.n_materials = m_materials.size();
metrics.n_lights = m_lights.size();
return metrics;
}
void renderer::update_and_upload() noexcept {
const auto vertices_size_bytes = m_vertices.size() * sizeof(vertex);
const auto indices_size_bytes = m_indices.size() * sizeof(renderer::index_t);
const auto lights_size_bytes = m_lights.size() * sizeof(renderer::point_light);
const auto indirect_cmds_size_bytes = m_draw_cmds.size() * sizeof(draw_elements_indirect_cmd);
const auto matrices_size_bytes = m_matrices.size() * sizeof(render_matrices);
// NOTE: assuming the use of mutable buffers here
if (m_vertices.size() > m_state.vertex_buffer->size() / sizeof(vertex)) {
m_state.vertex_buffer->upload(GL_STREAM_DRAW, vertices_size_bytes, m_vertices.data());
} else {
m_state.vertex_buffer->update(0, vertices_size_bytes, m_vertices.data());
}
if (m_indices.size() > m_state.index_buffer->size() / sizeof(renderer::index_t)) {
m_state.index_buffer->upload(GL_STREAM_DRAW, indices_size_bytes, m_indices.data());
} else {
m_state.index_buffer->update(0, indices_size_bytes, m_indices.data());
}
if (m_lights.size() > m_state.lights_buffer->size() / sizeof(renderer::point_light)) {
m_state.lights_buffer->upload(GL_STREAM_DRAW, lights_size_bytes, m_lights.data());
} else {
m_state.lights_buffer->update(0, lights_size_bytes, m_lights.data());
}
if (m_draw_cmds.size() > m_state.indirect_buffer->size() / sizeof(draw_elements_indirect_cmd)) {
m_state.indirect_buffer->upload(GL_STREAM_DRAW, indirect_cmds_size_bytes, m_draw_cmds.data());
} else {
m_state.indirect_buffer->update(0, indirect_cmds_size_bytes, m_draw_cmds.data());
}
if (m_matrices.size() > m_state.matrix_buffer->size() / sizeof(render_matrices)) {
m_state.matrix_buffer->upload(GL_STREAM_DRAW, matrices_size_bytes, m_matrices.data());
} else {
m_state.matrix_buffer->update(0, matrices_size_bytes, m_matrices.data());
}
return;
}
void renderer::draw_model(const resource::model& model, const maths::transform_euler& transform) {
const auto transform_matrix = transform.to_mat4();
for (const auto& mesh : model.meshes()) {
draw_mesh(mesh, model.materials(), transform_matrix);
}
return;
}
void renderer::draw_mesh(const resource::mesh& mesh,
const std::vector<resource::material>& materials,
const glm::mat4& transform) {
// From the glTF 2.0 spec:
// "When the scale is zero on all three axes [...] implementations are free
// to optimize away rendering of the node's mesh and all of the node's
// children's meshes"
// TODO: optimise for this ^
// The `transform` parameter is the parent mesh node's local transformation matrix
const auto rel_mat = transform * mesh.transform();
for (const auto& prim : mesh.primitives()) {
draw_primitive(prim, materials[prim.material_idx], rel_mat);
}
for (const auto& submesh : mesh.submeshes()) {
draw_mesh(submesh, materials, rel_mat);
}
return;
}
void renderer::draw_primitive(const resource::primitive& primitive,
const resource::material& material,
const maths::transform_euler& transform) {
return draw_primitive(primitive, material, transform.to_mat4());
}
void renderer::draw_primitive(const resource::primitive& primitive,
const resource::material& material,
const glm::mat4& transform) {
const std::uint32_t n_vertices = primitive.vertices.size();
const std::uint32_t n_indices = primitive.indices.size();
const std::uint32_t vtx_start =
static_cast<std::uint32_t>(std::distance(std::begin(m_vertices), std::end(m_vertices)));
const std::uint32_t idx_start =
static_cast<std::uint32_t>(std::distance(std::begin(m_indices), std::end(m_indices)));
KUIPER_ASSERT(vtx_start <= (std::numeric_limits<std::uint32_t>::max() - n_vertices));
KUIPER_ASSERT(idx_start <= (std::numeric_limits<std::uint32_t>::max() - n_indices));
draw_elements_indirect_cmd cmd {};
cmd.count = n_indices;
cmd.instance_count = 1;
cmd.first_index = idx_start;
cmd.base_vertex = vtx_start;
cmd.base_instance = m_draw_cmds.size();
m_vertices.insert(std::end(m_vertices), std::begin(primitive.vertices), std::end(primitive.vertices));
m_indices.insert(std::end(m_indices), std::begin(primitive.indices), std::end(primitive.indices));
m_matrices.push_back({transform, glm::transpose(glm::inverse(glm::mat3(transform)))});
m_draw_cmds.push_back(cmd);
m_materials.push_back(material);
return;
}
void renderer::add_light(const glm::vec3& pos, const component::point_light& light) noexcept {
m_lights.emplace_back(glm::make_vec4(pos), glm::make_vec4(light.colour), light.intensity, 50.0f);
return;
}
glm::mat4 camera_to_view_matrix(const glm::vec3& pos, const kuiper::component::camera_props& props) noexcept {
static constexpr glm::vec3 up_unit_vec {0.0f, 1.0f, 0.0f};
const glm::vec3 direction = {glm::cos(props.euler.y) * glm::cos(props.euler.x),
glm::sin(props.euler.x),
glm::sin(props.euler.y) * glm::cos(props.euler.x)};
const glm::vec3 cam_front = glm::normalize(direction);
return glm::lookAt(pos, pos + cam_front, up_unit_vec);
}

102
src/resources/image.cpp Normal file
View File

@@ -0,0 +1,102 @@
#include "resources/image.hpp"
#include "utils/assert.hpp"
#include <cstddef>
#include <cstdint>
#include <stb_image.h>
#include <stb_image_resize2.h>
#include <filesystem>
#include <fstream>
#include <vector>
using namespace kuiper::resource;
image image::make_blank(std::uint32_t width, std::uint32_t height, std::uint32_t n_channels) noexcept {
const std::size_t size = width * height * n_channels;
const std::vector<std::uint8_t> pixel_data(size, 0u);
return image {width, height, n_channels, pixel_data.data(), pixel_data.size()};
}
std::expected<image, image_error> image::from_path(const std::filesystem::path& path,
std::uint8_t num_channels) noexcept {
if (path.empty() || !std::filesystem::is_regular_file(path) || !std::filesystem::exists(path))
return std::unexpected(image_error {"invalid path"});
auto input_file = std::ifstream(path, std::ios::binary | std::ios::ate);
const auto file_size = input_file.tellg();
std::vector<std::uint8_t> buf(file_size);
input_file.read((char*) buf.data(), file_size);
input_file.close();
return image::from_memory(buf.data(), buf.size(), num_channels);
}
std::expected<image, image_error> image::from_memory(const std::uint8_t* data,
std::size_t len,
std::uint8_t num_channels) noexcept {
stbi_set_flip_vertically_on_load(false);
int width = 0;
int height = 0;
int n_channels = 0;
auto decoded_image_data =
stbi_load_from_memory(data, static_cast<int>(len), &width, &height, &n_channels, num_channels);
if (!decoded_image_data)
return std::unexpected(image_error {stbi_failure_reason()});
image ret {static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(height),
static_cast<std::uint32_t>(n_channels),
decoded_image_data,
static_cast<std::uint32_t>(width * height * num_channels)};
stbi_image_free(decoded_image_data);
return ret;
}
void image::resize(std::uint32_t width, std::uint32_t height) noexcept {
std::vector<std::uint8_t> out_buf(width * height * m_channels);
stbir_pixel_layout pixel_layout {STBIR_RGBA};
switch (m_channels) {
case 4:
pixel_layout = stbir_pixel_layout::STBIR_RGBA;
break;
case 3:
pixel_layout = stbir_pixel_layout::STBIR_RGB;
break;
case 2:
pixel_layout = stbir_pixel_layout::STBIR_2CHANNEL;
break;
case 1:
pixel_layout = stbir_pixel_layout::STBIR_1CHANNEL;
break;
default:
pixel_layout = stbir_pixel_layout::STBIR_RGBA;
break;
}
if (!stbir_resize_uint8_srgb(m_data.data(),
static_cast<int>(m_width),
static_cast<int>(m_height),
0, // packed in memory
out_buf.data(),
static_cast<int>(width),
static_cast<int>(height),
0, // packed in memory
pixel_layout))
KUIPER_ASSERT(false);
m_data = out_buf;
m_width = width;
m_height = height;
return;
}

390
src/resources/model.cpp Normal file
View File

@@ -0,0 +1,390 @@
#include "resources/model.hpp"
#include "errors/errors.hpp"
#include "graphics/opengl/texture.hpp"
#include "graphics/opengl/texture_array.hpp"
#include "logging/engine_logger.hpp"
#include "maths/transform.hpp"
#include "resources/image.hpp"
#include "resources/material.hpp"
#include "resources/mesh.hpp"
#include "resources/primitive.hpp"
#include "utils/assert.hpp"
#include "fastgltf/core.hpp"
#include "fastgltf/glm_element_traits.hpp"
#include "fastgltf/tools.hpp"
#include "fastgltf/types.hpp"
#include <glm/geometric.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <format>
#include <optional>
#include <utility>
#include <variant>
#include <vector>
using namespace kuiper;
resource::mesh parse_mesh_from_root_node(const fastgltf::Asset& asset, const fastgltf::Node& node);
std::vector<resource::material> parse_materials(const fastgltf::Asset& asset);
std::expected<resource::model, kuiper::error> resource::model::from_gltf_path(const std::filesystem::path& gltf_path) {
fastgltf::Parser parser {};
return model::from_gltf_path(gltf_path, parser);
}
std::expected<resource::model, kuiper::error> resource::model::from_gltf_path(const std::filesystem::path& gltf_path,
fastgltf::Parser& gltf_parser) {
if (gltf_path.empty() || !std::filesystem::is_regular_file(gltf_path) || !std::filesystem::exists(gltf_path))
return {};
auto logger = engine_logger::get();
auto data = fastgltf::GltfDataBuffer::FromPath(gltf_path);
if (data.error() != fastgltf::Error::None) {
logger.error("Failed to load glTF asset from: {}", gltf_path.filename().c_str());
return std::unexpected(error::failed_to_load);
}
const fastgltf::Options fastgltf_opts = fastgltf::Options::None | fastgltf::Options::LoadExternalBuffers |
fastgltf::Options::LoadExternalImages |
fastgltf::Options::GenerateMeshIndices;
auto asset = gltf_parser.loadGltf(data.get(), gltf_path.parent_path(), fastgltf_opts);
if (auto error = asset.error(); error != fastgltf::Error::None) {
logger.error("Failed to load glTF asset from: {}", gltf_path.filename().c_str());
return std::unexpected(error::failed_to_load);
}
// Parse the glTF scene description
if (asset->scenes.size() == 0) {
logger.error("glTF asset \"{}\" has no scenes", gltf_path.filename().c_str());
logger.error("DEBUG: asset created with \"{}\"",
asset->assetInfo.has_value() ? asset->assetInfo->generator : "[unknown]");
return std::unexpected(error::resource_invalid);
} else if (asset->scenes.size() > 1) {
logger.warn("glTF asset \"{}\" has multiple scenes. Defaulting to scene 0 ...", gltf_path.filename().c_str());
logger.warn("DEBUG: asset created with \"{}\"",
asset->assetInfo.has_value() ? asset->assetInfo->generator : "[unknown]");
}
class model out_model {};
// Collect all materials present in the model
const auto materials = parse_materials(asset.get());
// Iterate through the default scene node heirarchy
const std::size_t scene_idx = asset->defaultScene.has_value() ? asset->defaultScene.value() : 0;
const auto& scene = asset->scenes[scene_idx];
for (const std::size_t node_idx : scene.nodeIndices) {
out_model.push_mesh(parse_mesh_from_root_node(asset.get(), asset->nodes[node_idx]));
}
for (const auto& mat : materials) {
out_model.push_material(mat);
}
return out_model;
}
resource::mesh parse_mesh_from_root_node(const fastgltf::Asset& asset, const fastgltf::Node& node) {
// Node (mesh) transform
// The glTF specification notes that node transforms are relative to their parent node's transform
glm::mat4 out_matrix {};
if (std::holds_alternative<fastgltf::math::fmat4x4>(node.transform)) {
// Node contains a transformation matrix
const auto& node_trs_matrix = std::get<fastgltf::math::fmat4x4>(node.transform);
out_matrix = glm::make_mat4x4(node_trs_matrix.data());
} else if (std::holds_alternative<fastgltf::TRS>(node.transform)) {
// Node contains a TRS transform, convert to matrix
const auto& node_trs = std::get<fastgltf::TRS>(node.transform);
glm::vec3 pos {node_trs.translation.x(), node_trs.translation.y(), node_trs.translation.z()};
glm::quat rot {node_trs.rotation.w(), node_trs.rotation.x(), node_trs.rotation.y(), node_trs.rotation.z()};
glm::vec3 scale {node_trs.scale.x(), node_trs.scale.y(), node_trs.scale.z()};
static constexpr glm::mat4 identity_mat = glm::mat4(1.0f);
const glm::mat4 translation = glm::translate(identity_mat, pos);
const glm::mat4 rotation = glm::mat4_cast(rot);
const glm::mat4 scaling = glm::scale(identity_mat, scale);
out_matrix = translation * rotation * scaling;
}
// Construct the output node (mesh)
resource::mesh ret_val {out_matrix};
// Populate mesh with primitives
if (node.meshIndex.has_value()) {
const auto mesh_idx = node.meshIndex.value();
const fastgltf::Mesh& m = asset.meshes[mesh_idx];
// TODO: Split primitive parsing into separate function
for (const fastgltf::Primitive& p : m.primitives) {
KUIPER_ASSERT(p.type == fastgltf::PrimitiveType::Triangles);
resource::primitive out_prim {};
out_prim.type = primitive_type::triangles; // asserted above
// Vertex position
auto pos_attr = p.findAttribute("POSITION");
const auto& pos_accessor = asset.accessors[pos_attr->accessorIndex];
out_prim.vertices.resize(pos_accessor.count);
fastgltf::iterateAccessorWithIndex<glm::vec3>(
asset, pos_accessor, [&out_prim](const glm::vec3& pos, std::size_t idx) {
out_prim.vertices[idx].position = pos;
});
// Vertex normals
auto norm_attr = p.findAttribute("NORMAL");
const auto& norm_accessor = asset.accessors[norm_attr->accessorIndex];
if (norm_accessor.normalized) {
fastgltf::iterateAccessorWithIndex<glm::vec3>(
asset, norm_accessor, [&out_prim](const glm::vec3& norm, std::size_t idx) {
out_prim.vertices[idx].normal = norm;
});
} else {
fastgltf::iterateAccessorWithIndex<glm::vec3>(
asset, norm_accessor, [&out_prim](const glm::vec3& norm, std::size_t idx) {
out_prim.vertices[idx].normal = glm::normalize(norm);
});
}
// Vertex tangents
auto tan_attr = p.findAttribute("TANGENT");
const auto& tan_accessor = asset.accessors[tan_attr->accessorIndex];
fastgltf::iterateAccessorWithIndex<glm::vec4>(
asset, tan_accessor, [&out_prim](const glm::vec4& tan, std::size_t idx) {
out_prim.vertices[idx].tangent = tan;
});
// Indices
const auto& idx_accessor = asset.accessors[p.indicesAccessor.value()];
out_prim.indices.resize(idx_accessor.count);
fastgltf::copyFromAccessor<std::uint32_t>(asset, idx_accessor, out_prim.indices.data());
// Materials
out_prim.material_idx = p.materialIndex.value();
// Texture coordinates
const std::size_t tex_uv_idx =
asset.materials[p.materialIndex.value()].pbrData.baseColorTexture->texCoordIndex;
auto tex_uv_attr = p.findAttribute(std::format("TEXCOORD_{}", tex_uv_idx));
const auto& tex_uv_accessor = asset.accessors[tex_uv_attr->accessorIndex];
fastgltf::iterateAccessorWithIndex<glm::vec2>(
asset, tex_uv_accessor, [&out_prim](const glm::vec2& uv, std::size_t idx) {
out_prim.vertices[idx].uv = uv;
});
// Add parsed primitive to mesh
ret_val.add_primitive(out_prim);
}
}
if (!node.children.empty()) {
for (const auto child_idx : node.children) {
const auto submesh = parse_mesh_from_root_node(asset, asset.nodes[child_idx]);
ret_val.add_submesh(submesh);
}
}
return ret_val;
}
std::expected<resource::image, resource::image_error> load_material_image(const fastgltf::Asset& asset,
const fastgltf::TextureInfo& tex_info) {
const std::size_t tex_idx = tex_info.textureIndex;
const std::size_t img_idx = asset.textures[tex_idx].imageIndex.value();
const fastgltf::Image& img = asset.images[img_idx];
if (std::holds_alternative<fastgltf::sources::URI>(img.data)) {
const auto& path = std::get<fastgltf::sources::URI>(img.data);
return resource::image::from_path(path.uri.path());
} else if (std::holds_alternative<fastgltf::sources::Array>(img.data)) {
const auto& arr = std::get<fastgltf::sources::Array>(img.data);
return resource::image::from_memory((const std::uint8_t*) arr.bytes.data(), arr.bytes.size_bytes());
} else if (std::holds_alternative<fastgltf::sources::BufferView>(img.data)) {
const auto& view = std::get<fastgltf::sources::BufferView>(img.data);
const auto& buffer_view = asset.bufferViews[view.bufferViewIndex];
const auto& buffer = asset.buffers[buffer_view.bufferIndex];
if (std::holds_alternative<fastgltf::sources::Array>(buffer.data)) {
const auto& arr = std::get<fastgltf::sources::Array>(buffer.data);
return resource::image::from_memory((const std::uint8_t*) arr.bytes.data() + buffer_view.byteOffset,
arr.bytes.size_bytes());
}
}
return std::unexpected(resource::image_error {"unknown error"});
}
std::vector<resource::material> parse_materials(const fastgltf::Asset& asset) {
auto logger = engine_logger::get();
std::vector<resource::material> ret_vec(asset.materials.size());
for (std::uint32_t i = 0; i < asset.materials.size(); ++i) {
const auto& gltf_mat = asset.materials[i];
auto& ret_mat = ret_vec[i];
ret_mat.base_colour_factor = glm::make_vec4(gltf_mat.pbrData.baseColorFactor.data());
ret_mat.metallic_factor = gltf_mat.pbrData.metallicFactor;
ret_mat.roughness_factor = gltf_mat.pbrData.roughnessFactor;
// Texture parameters
GLint min_filter = GL_LINEAR_MIPMAP_NEAREST;
GLint mag_filter = GL_LINEAR;
GLint wrap_s = GL_REPEAT;
GLint wrap_t = GL_REPEAT;
// Load each of the texture images we're interested in
// 0: Albedo, 1: Normal, 2: Metallic-Roughness
std::array<resource::image, 3> imgs {};
// Base colour
if (gltf_mat.pbrData.baseColorTexture.has_value()) {
{
const auto& tex = asset.textures[gltf_mat.pbrData.baseColorTexture->textureIndex];
if (tex.samplerIndex.has_value()) {
const fastgltf::Sampler& sampler = asset.samplers[tex.samplerIndex.value()];
if (sampler.minFilter.has_value())
min_filter = std::to_underlying(sampler.minFilter.value());
if (sampler.magFilter.has_value())
mag_filter = std::to_underlying(sampler.magFilter.value());
wrap_s = std::to_underlying(sampler.wrapS);
wrap_t = std::to_underlying(sampler.wrapT);
}
}
auto base_colour_img = load_material_image(asset, gltf_mat.pbrData.baseColorTexture.value());
if (base_colour_img.has_value()) {
imgs[0] = std::move(base_colour_img.value());
} else {
logger.warn("Failed to load material's base colour texture image: {}", base_colour_img.error().message);
imgs[0] = resource::image::make_blank(64, 64, 4);
}
} else {
logger.warn("Material does not have a base colour texture");
imgs[0] = resource::image::make_blank(64, 64, 4);
}
// Normal map
if (gltf_mat.normalTexture.has_value()) {
auto normal_img = load_material_image(asset, gltf_mat.normalTexture.value());
if (normal_img.has_value()) {
imgs[1] = std::move(normal_img.value());
} else {
logger.warn("Failed to load material's normal texture image: {}", normal_img.error().message);
imgs[1] = resource::image::make_blank(64, 64, 4);
}
} else {
logger.warn("Material does not have a normal map texture");
imgs[1] = resource::image::make_blank(64, 64, 4);
}
if (gltf_mat.pbrData.metallicRoughnessTexture.has_value()) {
auto met_rough_img = load_material_image(asset, gltf_mat.pbrData.metallicRoughnessTexture.value());
if (met_rough_img.has_value()) {
imgs[2] = std::move(met_rough_img.value());
} else {
logger.warn("Failed to load material's metallic-roughness texture image: {}",
met_rough_img.error().message);
imgs[2] = resource::image::make_blank(64, 64, 4);
}
} else {
logger.warn("Material does not have a metallic-roughness texture");
imgs[2] = resource::image::make_blank(64, 64, 4);
}
// Texture 2D arrays require that each layer has the same width & height
// This is used to scale all textures in this material to the same size later on
std::uint32_t max_width = 0;
std::uint32_t max_height = 0;
for (const auto& img : imgs) {
KUIPER_ASSERT(img.width() == img.height());
if (img.width() > max_width)
max_width = img.width();
if (img.height() > max_height)
max_height = img.height();
}
for (auto& img : imgs) {
if (img.width() < max_width || img.height() < img.height())
img.resize(max_width, max_height);
}
ret_mat.textures = gl::texture_array::make(GL_TEXTURE_2D_ARRAY);
ret_mat.textures->set_storage(GL_RGBA8, max_width, max_height, 3);
ret_mat.textures->upload(
0, 0, 0, max_width, max_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, imgs[0].pixel_data().data());
ret_mat.textures->upload(
0, 0, 1, max_width, max_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, imgs[1].pixel_data().data());
ret_mat.textures->upload(
0, 0, 2, max_width, max_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, imgs[2].pixel_data().data());
ret_mat.textures->set_param(GL_TEXTURE_MIN_FILTER, min_filter);
ret_mat.textures->set_param(GL_TEXTURE_MAG_FILTER, mag_filter);
ret_mat.textures->set_param(GL_TEXTURE_WRAP_S, wrap_s);
ret_mat.textures->set_param(GL_TEXTURE_WRAP_T, wrap_t);
ret_mat.textures->gen_mipmaps();
}
return ret_vec;
}

View File

@@ -0,0 +1,7 @@
#include "resources/resource_index.hpp"
using namespace kuiper;
resource::index resource::index::from_file(const std::filesystem::path& index_path) noexcept {
return {};
}

View File

@@ -0,0 +1,39 @@
#include "resources/resource_manager.hpp"
#include "resources/model.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
using namespace kuiper;
static constexpr std::array<std::string_view, 2> GLTF_FILE_EXTENSIONS {".gltf", ".glb"};
std::optional<resource_metadata> resource_manager::import_from_path(const std::filesystem::path& path) {
if (!std::filesystem::exists(path) || !std::filesystem::is_regular_file(path)) {
return std::nullopt;
}
std::string file_ext = path.extension().string();
std::transform(file_ext.begin(), file_ext.end(), file_ext.begin(), [](const auto c) {
return std::tolower(c);
});
const auto model_ext_it =
std::find_if(GLTF_FILE_EXTENSIONS.begin(), GLTF_FILE_EXTENSIONS.end(), [&file_ext](const auto s) {
return file_ext == s;
});
if (model_ext_it != GLTF_FILE_EXTENSIONS.end()) {
auto maybe_model = resource::model::from_gltf_path(path);
if (maybe_model)
add<resource::model>(std::move(maybe_model.value()), path.relative_path().string());
}
return std::optional<resource_metadata>();
}

View File

@@ -0,0 +1,64 @@
#include "GLFW/glfw3.h"
#include "window/input_system.hpp"
using namespace kuiper;
void input_system::set_key_state(int key, int state) {
m_key_states[key].state = state;
// Call any action handlers
if (state == GLFW_PRESS) {
for (const auto& f : m_action_listeners[m_action_keymap[key]]) {
f();
}
}
return;
}
int input_system::get_key_state(int key) const {
return m_key_states[key].state;
}
bool input_system::is_key_pressed(int key) const {
return m_key_states[key].state == GLFW_PRESS;
}
bool input_system::is_key_released(int key) const {
return m_key_states[key].state == GLFW_RELEASE;
}
bool input_system::is_key_held(int key) const {
return m_key_states[key].state == GLFW_REPEAT;
}
bool input_system::is_key_down(int key) const {
return m_key_states[key].state == GLFW_PRESS || m_key_states[key].state == GLFW_REPEAT;
}
// Mouse
void input_system::set_mouse_pos(const glm::vec2& pos) {
m_mouse_state.delta_pos = m_old_mouse_pos - pos;
m_mouse_state.pos = pos;
return;
}
void input_system::set_mouse_delta(const glm::vec2& pos) {
m_mouse_state.delta_pos = pos;
return;
}
glm::vec2 input_system::get_mouse_pos() const {
return m_mouse_state.pos;
}
glm::vec2 input_system::get_mouse_delta() const {
return m_mouse_state.delta_pos;
}
void input_system::subscribe_to_action(action_type type, const action_fn_t& callback) {
m_action_listeners[type].push_back(callback);
}