From a80e405b3eb1990569e1e92de6684b771e908cf8 Mon Sep 17 00:00:00 2001 From: tovjemam Date: Mon, 29 Dec 2025 21:33:54 +0100 Subject: [PATCH] Init --- .clang-format | 9 ++ CMakeLists.txt | 151 ++++++++++++++++++++ CMakePresets.json | 62 ++++++++ README.md | 2 + shell.html | 59 ++++++++ src/assets/animation.cpp | 142 +++++++++++++++++++ src/assets/animation.hpp | 43 ++++++ src/assets/cache.cpp | 5 + src/assets/cache.hpp | 79 +++++++++++ src/assets/cmdfile.cpp | 25 ++++ src/assets/cmdfile.hpp | 15 ++ src/assets/map.cpp | 49 +++++++ src/assets/map.hpp | 30 ++++ src/assets/mesh_builder.cpp | 116 +++++++++++++++ src/assets/mesh_builder.hpp | 64 +++++++++ src/assets/model.cpp | 71 ++++++++++ src/assets/model.hpp | 21 +++ src/assets/skeleton.cpp | 95 +++++++++++++ src/assets/skeleton.hpp | 47 ++++++ src/client/app.cpp | 50 +++++++ src/client/app.hpp | 29 ++++ src/client/gl.hpp | 8 ++ src/client/main.cpp | 243 ++++++++++++++++++++++++++++++++ src/client/utils.hpp | 13 ++ src/collision/aabb.hpp | 54 +++++++ src/collision/trianglemesh.cpp | 20 +++ src/collision/trianglemesh.hpp | 27 ++++ src/game/player_input.hpp | 23 +++ src/game/transform_node.hpp | 29 ++++ src/gameview/client_session.hpp | 12 ++ src/gameview/entityview.hpp | 26 ++++ src/gameview/worldview.hpp | 14 ++ src/gfx/buffer_object.cpp | 29 ++++ src/gfx/buffer_object.hpp | 30 ++++ src/gfx/draw_list.hpp | 31 ++++ src/gfx/renderer.cpp | 178 +++++++++++++++++++++++ src/gfx/renderer.hpp | 52 +++++++ src/gfx/shader.cpp | 103 ++++++++++++++ src/gfx/shader.hpp | 52 +++++++ src/gfx/shader_defs.hpp | 4 + src/gfx/shader_sources.cpp | 206 +++++++++++++++++++++++++++ src/gfx/shader_sources.hpp | 36 +++++ src/gfx/surface.hpp | 38 +++++ src/gfx/texture.cpp | 87 ++++++++++++ src/gfx/texture.hpp | 26 ++++ src/gfx/uniform_buffer.hpp | 21 +++ src/gfx/vertex_array.cpp | 98 +++++++++++++ src/gfx/vertex_array.hpp | 62 ++++++++ src/net/defs.hpp | 53 +++++++ src/net/fixed_str.hpp | 34 +++++ src/net/inmessage.hpp | 121 ++++++++++++++++ src/net/outmessage.hpp | 83 +++++++++++ src/net/quantized.hpp | 40 ++++++ src/utils/files.cpp | 33 +++++ src/utils/files.hpp | 9 ++ src/utils/transform.hpp | 58 ++++++++ 56 files changed, 3117 insertions(+) create mode 100644 .clang-format create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 README.md create mode 100644 shell.html create mode 100644 src/assets/animation.cpp create mode 100644 src/assets/animation.hpp create mode 100644 src/assets/cache.cpp create mode 100644 src/assets/cache.hpp create mode 100644 src/assets/cmdfile.cpp create mode 100644 src/assets/cmdfile.hpp create mode 100644 src/assets/map.cpp create mode 100644 src/assets/map.hpp create mode 100644 src/assets/mesh_builder.cpp create mode 100644 src/assets/mesh_builder.hpp create mode 100644 src/assets/model.cpp create mode 100644 src/assets/model.hpp create mode 100644 src/assets/skeleton.cpp create mode 100644 src/assets/skeleton.hpp create mode 100644 src/client/app.cpp create mode 100644 src/client/app.hpp create mode 100644 src/client/gl.hpp create mode 100644 src/client/main.cpp create mode 100644 src/client/utils.hpp create mode 100644 src/collision/aabb.hpp create mode 100644 src/collision/trianglemesh.cpp create mode 100644 src/collision/trianglemesh.hpp create mode 100644 src/game/player_input.hpp create mode 100644 src/game/transform_node.hpp create mode 100644 src/gameview/client_session.hpp create mode 100644 src/gameview/entityview.hpp create mode 100644 src/gameview/worldview.hpp create mode 100644 src/gfx/buffer_object.cpp create mode 100644 src/gfx/buffer_object.hpp create mode 100644 src/gfx/draw_list.hpp create mode 100644 src/gfx/renderer.cpp create mode 100644 src/gfx/renderer.hpp create mode 100644 src/gfx/shader.cpp create mode 100644 src/gfx/shader.hpp create mode 100644 src/gfx/shader_defs.hpp create mode 100644 src/gfx/shader_sources.cpp create mode 100644 src/gfx/shader_sources.hpp create mode 100644 src/gfx/surface.hpp create mode 100644 src/gfx/texture.cpp create mode 100644 src/gfx/texture.hpp create mode 100644 src/gfx/uniform_buffer.hpp create mode 100644 src/gfx/vertex_array.cpp create mode 100644 src/gfx/vertex_array.hpp create mode 100644 src/net/defs.hpp create mode 100644 src/net/fixed_str.hpp create mode 100644 src/net/inmessage.hpp create mode 100644 src/net/outmessage.hpp create mode 100644 src/net/quantized.hpp create mode 100644 src/utils/files.cpp create mode 100644 src/utils/files.hpp create mode 100644 src/utils/transform.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ea1fc60 --- /dev/null +++ b/.clang-format @@ -0,0 +1,9 @@ +BasedOnStyle: Microsoft +DerivePointerAlignment: false +PointerAlignment: Left +IndentWidth: 4 # spaces per indent level +TabWidth: 4 # width of a tab character +UseTab: Never # options: Never, ForIndentation, Alwayss +AccessModifierOffset: -4 +BreakTemplateDeclarations: Yes +AllowShortFunctionsOnASingleLine: Inline diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2b23f0f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,151 @@ +cmake_minimum_required(VERSION 3.15) +project(FekalniGtacko) + +# Enable C++20 +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(SOURCES + "src/client/main.cpp" + "src/client/app.hpp" + "src/client/app.cpp" + "src/client/gl.hpp" + "src/client/utils.hpp" + "src/assets/animation.hpp" + "src/assets/animation.cpp" + "src/assets/cache.hpp" + "src/assets/cache.cpp" + "src/assets/cmdfile.hpp" + "src/assets/cmdfile.cpp" + "src/assets/model.hpp" + "src/assets/model.cpp" + "src/assets/mesh_builder.hpp" + "src/assets/mesh_builder.cpp" + "src/assets/map.hpp" + "src/assets/map.cpp" + "src/assets/skeleton.hpp" + "src/assets/skeleton.cpp" + "src/collision/trianglemesh.cpp" + # "src/game/entity.hpp" + # "src/game/entity.cpp" + # "src/game/meshinstance.hpp" + # "src/game/meshinstance.cpp" + "src/game/player_input.hpp" + # "src/game/player.hpp" + # "src/game/player.cpp" + "src/game/transform_node.hpp" + # "src/game/world.hpp" + # "src/game/world.cpp" + "src/gameview/client_session.hpp" + "src/gameview/entityview.hpp" + "src/gameview/worldview.hpp" + "src/gfx/buffer_object.cpp" + "src/gfx/buffer_object.hpp" + "src/gfx/draw_list.hpp" + "src/gfx/renderer.hpp" + "src/gfx/renderer.cpp" + "src/gfx/shader_defs.hpp" + "src/gfx/shader_sources.hpp" + "src/gfx/shader_sources.cpp" + "src/gfx/shader.hpp" + "src/gfx/shader.cpp" + "src/gfx/surface.hpp" + "src/gfx/texture.cpp" + "src/gfx/texture.hpp" + "src/gfx/uniform_buffer.hpp" + "src/gfx/vertex_array.cpp" + "src/gfx/vertex_array.hpp" + "src/net/defs.hpp" + "src/net/fixed_str.hpp" + "src/net/inmessage.hpp" + "src/net/outmessage.hpp" + "src/net/quantized.hpp" + "src/utils/files.hpp" + "src/utils/files.cpp" + "src/utils/transform.hpp" +) + + +if(ANDROID) + # Android-specific setup + set(MAIN_NAME "main") + add_library(${MAIN_NAME} SHARED ${SOURCES}) + target_link_libraries(${MAIN_NAME} PRIVATE GLESv3 log android) +else() + # Desktop build + set(MAIN_NAME "FekalniGtacko") + add_executable(${MAIN_NAME} ${SOURCES}) +endif() + + +# Include directories +target_include_directories(${MAIN_NAME} PRIVATE + "src" +) + +# Platform-specific SDL2 handling +if (CMAKE_SYSTEM_NAME STREQUAL Emscripten) + + # Emscripten provides SDL2 via its system libraries + message(STATUS "Target platform: WebAssembly (Emscripten)") + set(CMAKE_EXECUTABLE_SUFFIX ".html") # Optional: build HTML page + + target_compile_options(${MAIN_NAME} PRIVATE + "-sUSE_SDL=2" + "-sNO_DISABLE_EXCEPTION_CATCHING=1" + ) + + target_link_options(${MAIN_NAME} PRIVATE + "-sUSE_SDL=2" + "-sASYNCIFY" + "-sUSE_WEBGL2=1" + "-sNO_DISABLE_EXCEPTION_CATCHING=1" + "-sALLOW_MEMORY_GROWTH=1" + "--shell-file" "${CMAKE_SOURCE_DIR}/shell.html" + "--preload-file" "${CMAKE_SOURCE_DIR}/assets/@/" + ) + +else() + message(STATUS "Target platform: Native") + # Native platform + # find_package(SDL2 REQUIRED) + # SDL2 build options to avoid unwanted components + set(SDL_TEST OFF CACHE BOOL "" FORCE) + +if(ANDROID) + set(SDL_SHARED ON CACHE BOOL "" FORCE) + set(SDL_STATIC OFF CACHE BOOL "" FORCE) +else() + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) +endif() + + add_subdirectory(external/SDL) + target_include_directories(${MAIN_NAME} PRIVATE "external/SDL/include") + + target_link_libraries(${MAIN_NAME} PRIVATE SDL2main) + +if(ANDROID) + target_link_libraries(${MAIN_NAME} PRIVATE SDL2) +else() + target_link_libraries(${MAIN_NAME} PRIVATE SDL2-static) +endif() + + add_subdirectory(external/glad) + target_link_libraries(${MAIN_NAME} PRIVATE glad) + +endif() + +add_subdirectory(external/glm) +target_link_libraries(${MAIN_NAME} PRIVATE glm) +target_include_directories(${MAIN_NAME} PRIVATE "external/stb") + +set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE) +set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) +set(BUILD_PYBULLET OFF CACHE BOOL "" FORCE) +set(BUILD_OPENGL3_DEMOS OFF CACHE BOOL "" FORCE) + +add_subdirectory(external/bullet3) +target_link_libraries(${MAIN_NAME} PRIVATE BulletCollision LinearMath Bullet3Common) +target_include_directories(${MAIN_NAME} PRIVATE "external/bullet3/src") diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..e55a882 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 4, + "cmakeMinimumRequired": { + "major": 3, + "minor": 21, + "patch": 0 + }, + "configurePresets": [ + { + "name": "vs", + "displayName": "Visual Studio 18 2026", + "description": "Build with VS compiler using Multi-Config generator", + "generator": "Visual Studio 18 2026", + "toolset": "host=x64", + "architecture": "x64", + "binaryDir": "${sourceDir}/build/vs", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe" + } + }, + { + "name": "emscripten", + "displayName": "Emscripten Multi-Config Build", + "description": "Build with Emscripten toolchain using Multi-Config generator", + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/build/wasm", + "toolchainFile": "c:/dev/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + } + ], + "buildPresets": [ + { + "name": "vs-release", + "displayName": "VS Release", + "configurePreset": "vs", + "configuration": "Release" + }, + { + "name": "vs-debug", + "displayName": "VS Debug", + "configurePreset": "vs", + "configuration": "Debug" + }, + { + "name": "emscripten-release", + "displayName": "Emscripten Release", + "configurePreset": "emscripten", + "configuration": "Release" + }, + { + "name": "emscripten-debug", + "displayName": "Emscripten Debug", + "configurePreset": "emscripten", + "configuration": "Debug" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d00f4c --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# FekalniGtacko + diff --git a/shell.html b/shell.html new file mode 100644 index 0000000..347c9f3 --- /dev/null +++ b/shell.html @@ -0,0 +1,59 @@ + + + + + + PortalGame + + + + + + + + + + + {{{ SCRIPT }}} + + + \ No newline at end of file diff --git a/src/assets/animation.cpp b/src/assets/animation.cpp new file mode 100644 index 0000000..f44263e --- /dev/null +++ b/src/assets/animation.cpp @@ -0,0 +1,142 @@ +#include "animation.hpp" +#include "skeleton.hpp" + +#include "utils/files.hpp" +#include +#include + +std::shared_ptr assets::Animation::LoadFromFile(const std::string& filename, const Skeleton* skeleton) +{ + std::istringstream ifs = fs::ReadFileAsStream(filename); + + std::shared_ptr anim = std::make_shared(); + + int last_frame = 0; + std::vector frame_indices; + + auto FillFrameRefs = [&](int end_frame) + { + int channel_start = ((int)anim->channels_.size() - 1) * (int)anim->num_frames_; + int target_size = channel_start + end_frame; + size_t num_frame_refs = frame_indices.size(); + + if (num_frame_refs >= target_size) + { + return; // Already filled + } + + if (num_frame_refs % anim->num_frames_ == 0) + { + throw std::runtime_error("Cannot fill frames of channel that has 0 frames: " + filename); + } + + size_t last_frame_idx = frame_indices.back(); + frame_indices.resize(target_size, last_frame_idx); + }; + + std::string line; + + while (std::getline(ifs, line)) + { + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + + std::istringstream iss(line); + + std::string command; + iss >> command; + + if (command == "f") + { + if (anim->num_frames_ == 0) + { + throw std::runtime_error("Frame data specified before number of frames in animation file: " + filename); + } + + if (anim->channels_.empty()) + { + throw std::runtime_error("Frame data specified before any channels in animation file: " + filename); + } + + int frame_index; + iss >> frame_index; + + if (frame_index < 0 || frame_index >= (int)anim->num_frames_) + { + throw std::runtime_error("Frame index out of bounds in animation file: " + filename); + } + + if (frame_index < last_frame) + { + throw std::runtime_error("Frame indices must be in ascending order in animation file: " + filename); + } + + last_frame = frame_index; + + Transform t; + iss >> t.position.x >> t.position.y >> t.position.z; + glm::vec3 angles_deg; + iss >> angles_deg.x >> angles_deg.y >> angles_deg.z; + t.SetAngles(angles_deg); + iss >> t.scale; + + size_t idx = anim->frames_.size(); + anim->frames_.push_back(t); + + FillFrameRefs(frame_index); // Fill to current frame + frame_indices.push_back(idx); + } + else if (command == "ch") + { + std::string name; + iss >> name; + + int bone_index = skeleton->GetBoneIndex(name); + + if (bone_index < 0) + { + throw std::runtime_error("Bone referenced in animation not found in provided skeleton: " + name); + } + + FillFrameRefs(anim->num_frames_); // Fill to end for last channel + + AnimationChannel& channel = anim->channels_.emplace_back(); + channel.bone_index = bone_index; + channel.frames = nullptr; // Will be set up later + + last_frame = 0; + + } + else if (command == "frames") + { + iss >> anim->num_frames_; + } + else if (command == "fps") + { + iss >> anim->tps_; + } + } + + if (anim->channels_.empty()) + { + throw std::runtime_error("No channels found in animation file: " + filename); + } + + FillFrameRefs(anim->num_frames_); // Fill to end for last channel + + // Set up frame pointers + anim->frame_refs_.resize(frame_indices.size()); + for (size_t i = 0; i < frame_indices.size(); ++i) + { + anim->frame_refs_[i] = &anim->frames_[frame_indices[i]]; + } + + // Set up channel frame pointers + for (size_t i = 0; i < anim->channels_.size(); ++i) + { + AnimationChannel& channel = anim->channels_[i]; + channel.frames = &anim->frame_refs_[i * anim->num_frames_]; + } + + return anim; +} diff --git a/src/assets/animation.hpp b/src/assets/animation.hpp new file mode 100644 index 0000000..6ef01f8 --- /dev/null +++ b/src/assets/animation.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#include "utils/transform.hpp" + +namespace assets +{ + class Skeleton; + + struct AnimationChannel + { + int bone_index; + const Transform* const* frames; + }; + + class Animation + { + size_t num_frames_ = 0; + float tps_ = 24.0f; + + std::vector channels_; + std::vector frame_refs_; + std::vector frames_; + + public: + Animation() = default; + + size_t GetNumFrames() const { return num_frames_; } + float GetTPS() const { return tps_; } + float GetDuration() const { return static_cast(num_frames_) / tps_; } + + size_t GetNumChannels() const { return channels_.size(); } + const AnimationChannel& GetChannel(int index) const { return channels_[index]; } + + static std::shared_ptr LoadFromFile(const std::string& filename, const Skeleton* skeleton); + + }; + + +} \ No newline at end of file diff --git a/src/assets/cache.cpp b/src/assets/cache.cpp new file mode 100644 index 0000000..9d366d5 --- /dev/null +++ b/src/assets/cache.cpp @@ -0,0 +1,5 @@ +#include "cache.hpp" + +assets::TextureCache assets::CacheManager::texture_cache_; +assets::SkeletonCache assets::CacheManager::skeleton_cache_; +assets::ModelCache assets::CacheManager::model_cache_; diff --git a/src/assets/cache.hpp b/src/assets/cache.hpp new file mode 100644 index 0000000..ac728f6 --- /dev/null +++ b/src/assets/cache.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "model.hpp" +#include "skeleton.hpp" +#include "gfx/texture.hpp" + +namespace assets +{ + +template +class Cache +{ +public: + using PtrType = std::shared_ptr; + + PtrType Get(const std::string& key) + { + auto it = cache_.find(key); + if (it != cache_.end()) + { + if (auto ptr = it->second.lock()) + { + return ptr; // Return cached object + } + } + + PtrType obj = Load(key); + cache_[key] = obj; // Cache the loaded object + return obj; + } + +protected: + virtual PtrType Load(const std::string& key) = 0; + +private: + std::map> cache_; +}; + +class TextureCache final : public Cache +{ +protected: + PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); } +}; + +class SkeletonCache final : public Cache +{ +protected: + PtrType Load(const std::string& key) override { return Skeleton::LoadFromFile(key); } +}; + +class ModelCache final : public Cache +{ +protected: + PtrType Load(const std::string& key) override { return Model::LoadFromFile(key); } +}; + +class CacheManager +{ +public: + static std::shared_ptr GetTexture(const std::string& filename) { + return texture_cache_.Get(filename); + } + + static std::shared_ptr GetSkeleton(const std::string& filename) { + return skeleton_cache_.Get(filename); + } + + static std::shared_ptr GetModel(const std::string& filename) { + return model_cache_.Get(filename); + } + +private: + static TextureCache texture_cache_; + static SkeletonCache skeleton_cache_; + static ModelCache model_cache_; + +}; + +} // namespace assets \ No newline at end of file diff --git a/src/assets/cmdfile.cpp b/src/assets/cmdfile.cpp new file mode 100644 index 0000000..e1880bd --- /dev/null +++ b/src/assets/cmdfile.cpp @@ -0,0 +1,25 @@ +#include "cmdfile.hpp" + +void assets::LoadCMDFile(const std::string& filename, + const std::function& handler) +{ + std::istringstream file = fs::ReadFileAsStream(filename); + + std::string line; + while (std::getline(file, line)) + { + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + + // skip whitespace + line.erase(0, line.find_first_not_of(" \t")); + + std::istringstream iss(line); + + std::string command; + iss >> command; + + handler(command, iss); + } + +} diff --git a/src/assets/cmdfile.hpp b/src/assets/cmdfile.hpp new file mode 100644 index 0000000..d7999f6 --- /dev/null +++ b/src/assets/cmdfile.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "utils/files.hpp" +#include +#include +#include +#include + +namespace assets +{ + +void LoadCMDFile(const std::string& filename, + const std::function& handler); + +} // namespace assets \ No newline at end of file diff --git a/src/assets/map.cpp b/src/assets/map.cpp new file mode 100644 index 0000000..68ca82f --- /dev/null +++ b/src/assets/map.cpp @@ -0,0 +1,49 @@ +#include "map.hpp" +#include "cache.hpp" +#include "utils/files.hpp" +#include "cmdfile.hpp" + +std::shared_ptr assets::Map::LoadFromFile(const std::string& filename) +{ + auto map = std::make_shared(); + + LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) { + if (command == "basemodel") + { + std::string model_name; + iss >> model_name; + + map->basemodel_ = CacheManager::GetModel("data/" + model_name + ".mdl"); + } + else if (command == "static") + { + MapStaticObject obj; + + std::string model_name; + iss >> model_name; + + obj.model = assets::CacheManager::GetModel("data/" + model_name + ".mdl"); + + glm::vec3 angles; + + iss >> obj.transform.position.x >> obj.transform.position.y >> obj.transform.position.z; + iss >> angles.x >> angles.y >> angles.z; + obj.transform.SetAngles(angles); + iss >> obj.transform.scale; + + std::string flag; + while (iss >> flag) + { + if (flag == "+color") + { + iss >> obj.color.r >> obj.color.g >> obj.color.b; + } + } + + + map->static_objects_.push_back(std::move(obj)); + } + }); + + return map; +} diff --git a/src/assets/map.hpp b/src/assets/map.hpp new file mode 100644 index 0000000..347b53b --- /dev/null +++ b/src/assets/map.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "model.hpp" +#include "utils/transform.hpp" + +namespace assets +{ + +struct MapStaticObject +{ + Transform transform; + std::shared_ptr model; + glm::vec3 color = glm::vec3(1.0f); +}; + +class Map +{ +public: + Map() = default; + static std::shared_ptr LoadFromFile(const std::string& filename); + +private: + std::shared_ptr basemodel_; + std::vector static_objects_; +}; + +} // namespace assets \ No newline at end of file diff --git a/src/assets/mesh_builder.cpp b/src/assets/mesh_builder.cpp new file mode 100644 index 0000000..d016794 --- /dev/null +++ b/src/assets/mesh_builder.cpp @@ -0,0 +1,116 @@ +#include "mesh_builder.hpp" + +#include + +assets::MeshBuilder::MeshBuilder(gfx::MeshFlags mflags) : mflags_(mflags) {} + +void assets::MeshBuilder::BeginSurface(gfx::SurfaceFlags sflags, const std::string& name, std::shared_ptr texture) +{ + if (!mesh_) + mesh_ = std::make_shared(); + + FinalizeSurface(); + + gfx::Surface surface; + surface.sflags = sflags; + surface.texture = texture; + surface.first = tris_.size(); + mesh_->surfaces.push_back(surface); + + mesh_->surface_names[name] = mesh_->surfaces.size() - 1; +} + +void assets::MeshBuilder::AddVertex(const MeshVertex& v) +{ + verts_.push_back(v); +} + +void assets::MeshBuilder::AddTriangle(const MeshTriangle& t) +{ + if (!mesh_ || mesh_->surfaces.size() == 0) + { + throw std::runtime_error("Cannot add triangle without a surface. Call BeginSurface() first."); + } + + tris_.push_back(t); +} + +template +static void BufferPut(std::vector& buffer, const T& val) +{ + const char* data = reinterpret_cast(&val); + buffer.insert(buffer.end(), data, data + sizeof(T)); +} + +static int GetVertexAttrFlags(gfx::MeshFlags mflags) +{ + int attrs = gfx::VA_POSITION | gfx::VA_NORMAL | gfx::VA_UV; + + if (mflags & gfx::MF_LIGHTMAP_UV) + { + attrs |= gfx::VA_LIGHTMAP_UV; + } + + if (mflags & gfx::MF_SKELETAL) + { + attrs |= gfx::VA_BONE_INDICES | gfx::VA_BONE_WEIGHTS; + } + + return attrs; +} + +void assets::MeshBuilder::Build() +{ + // Finalize last surface + FinalizeSurface(); + + // Generate VBO data + std::vector buffer; + for (const MeshVertex& vert : verts_) + { + BufferPut(buffer, vert.pos); + BufferPut(buffer, vert.normal); + BufferPut(buffer, vert.uv); + + if (mflags_ & gfx::MF_LIGHTMAP_UV) + { + BufferPut(buffer, vert.lightmap_uv); + } + + if (mflags_ & gfx::MF_SKELETAL) + { + for (int i = 0; i < 4; ++i) + { + BufferPut(buffer, static_cast(vert.bones[i].bone_index)); + } + + for (int i = 0; i < 4; ++i) + { + BufferPut(buffer, vert.bones[i].weight); + } + } + } + + // populate VA + std::shared_ptr va = + std::make_shared(GetVertexAttrFlags(mflags_), gfx::VF_CREATE_EBO); + va->SetVBOData(buffer.data(), buffer.size()); + va->SetIndices(reinterpret_cast(tris_.data()), tris_.size() * 3U); + + // Assign VA to surfaces & set mesh flags + for (gfx::Surface& surface : mesh_->surfaces) + { + surface.va = va; + surface.mflags = mflags_; + } +} + +void assets::MeshBuilder::FinalizeSurface() +{ + if (mesh_->surfaces.size() > 0) + { + // finalize previous surface + gfx::Surface& prev_surface = mesh_->surfaces.back(); + prev_surface.count = tris_.size() - prev_surface.first; + } +} diff --git a/src/assets/mesh_builder.hpp b/src/assets/mesh_builder.hpp new file mode 100644 index 0000000..dc28863 --- /dev/null +++ b/src/assets/mesh_builder.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "gfx/surface.hpp" +#include +#include +#include +#include + +namespace assets +{ + +struct MeshVertexBoneInfluence +{ + int bone_index; + float weight; +}; + +struct MeshVertex +{ + glm::vec3 pos; + glm::vec3 normal; + glm::vec2 uv; + glm::vec2 lightmap_uv; + MeshVertexBoneInfluence bones[4]; +}; + +struct MeshTriangle +{ + uint32_t vert[3]; +}; + +struct Mesh +{ + gfx::MeshFlags mflags = gfx::MF_NONE; + std::vector surfaces; + std::map surface_names; +}; + +class MeshBuilder +{ +public: + MeshBuilder(gfx::MeshFlags mflags); + + void BeginSurface(gfx::SurfaceFlags sflags, const std::string& name, std::shared_ptr texture); + void AddVertex(const MeshVertex& v); + void AddTriangle(const MeshTriangle& t); + + void Build(); + + void SetMeshFlag(gfx::MeshFlags flag) { mflags_ |= flag; } + + std::shared_ptr GetMesh() const { return mesh_; } + +private: + void FinalizeSurface(); + +private: + gfx::MeshFlags mflags_ = 0; + std::vector verts_; + std::vector tris_; + std::shared_ptr mesh_; +}; + +} // namespace assets \ No newline at end of file diff --git a/src/assets/model.cpp b/src/assets/model.cpp new file mode 100644 index 0000000..75e8a80 --- /dev/null +++ b/src/assets/model.cpp @@ -0,0 +1,71 @@ +#include "model.hpp" + +#include "cmdfile.hpp" +#include "cache.hpp" + +std::shared_ptr assets::Model::LoadFromFile(const std::string& filename) +{ + auto model = std::make_shared(); + + MeshBuilder mb(gfx::MF_NONE); + + LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) { + if (command == "v") + { + MeshVertex v; + iss >> v.pos.x >> v.pos.y >> v.pos.z; + iss >> v.normal.x >> v.normal.y >> v.normal.z; + iss >> v.uv.x >> v.uv.y; + + // TODO: LUV & bone data + + mb.AddVertex(v); + } + else if (command == "f") + { + MeshTriangle t; + iss >> t.vert[0] >> t.vert[1] >> t.vert[2]; + + mb.AddTriangle(t); + } + else if (command == "surface") + { + std::string surface_name, texture_name; + gfx::SurfaceFlags sflags = gfx::SF_NONE; + + iss >> surface_name; + + // Optional flags + std::string flag; + while (iss >> flag) + { + if (flag == "+texture") + iss >> texture_name; + else if (flag == "+doublesided") + sflags |= gfx::SF_DOUBLE_SIDED; + else if (flag == "+transparent") + sflags |= gfx::SF_TRANSPARENT; + else if (flag == "+ocolor") + sflags |= gfx::SF_OBJECT_COLOR; + } + + std::shared_ptr texture; + if (!texture_name.empty()) + { + texture = CacheManager::GetTexture("data/" + surface_name + ".png"); + } + + mb.BeginSurface(sflags, surface_name, texture); + } + else + { + throw std::runtime_error("Unknown command in model file: " + command); + } + + // TODO: skeleton + }); + + + + return model; +} diff --git a/src/assets/model.hpp b/src/assets/model.hpp new file mode 100644 index 0000000..72d707e --- /dev/null +++ b/src/assets/model.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "mesh_builder.hpp" + +namespace assets +{ + +class Model +{ +public: + Model() = default; + static std::shared_ptr LoadFromFile(const std::string& filename); + + const std::shared_ptr& GetMesh() const { return mesh_; } + +private: + std::shared_ptr mesh_; +}; + +} \ No newline at end of file diff --git a/src/assets/skeleton.cpp b/src/assets/skeleton.cpp new file mode 100644 index 0000000..ab24c40 --- /dev/null +++ b/src/assets/skeleton.cpp @@ -0,0 +1,95 @@ +#include "skeleton.hpp" + +#include "utils/files.hpp" +#include +#include + +void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform) +{ + int index = static_cast(bones_.size()); + + Bone& bone = bones_.emplace_back(); + bone.name = name; + bone.parent_idx = GetBoneIndex(parent_name); + bone.bind_transform = transform; + bone.inv_bind_matrix = glm::inverse(transform.ToMatrix()); + + bone_map_[bone.name] = index; +} + +int assets::Skeleton::GetBoneIndex(const std::string& name) const +{ + auto it = bone_map_.find(name); + if (it != bone_map_.end()) { + return it->second; + } + + return -1; +} + +const assets::Animation* assets::Skeleton::GetAnimation(const std::string& name) const +{ + auto it = anims_.find(name); + if (it != anims_.end()) { + return it->second.get(); + } + return nullptr; +} + +std::shared_ptr assets::Skeleton::LoadFromFile(const std::string& filename) +{ + std::istringstream ifs = fs::ReadFileAsStream(filename); + + std::shared_ptr skeleton = std::make_shared(); + + std::string line; + + while (std::getline(ifs, line)) + { + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + + std::istringstream iss(line); + + std::string command; + iss >> command; + + if (command == "b") + { + Transform t; + glm::vec3 angles; + std::string bone_name, parent_name; + + iss >> bone_name >> parent_name; + iss >> t.position.x >> t.position.y >> t.position.z; + iss >> angles.x >> angles.y >> angles.z; + iss >> t.scale; + + if (iss.fail()) + { + throw std::runtime_error("Failed to parse bone definition in file: " + filename); + } + + t.SetAngles(angles); + + skeleton->AddBone(bone_name, parent_name, t); + } + else if (command == "anim") + { + std::string anim_name, anim_filename; + iss >> anim_name >> anim_filename; + + if (iss.fail()) + { + throw std::runtime_error("Failed to parse animation definition in file: " + filename); + } + + std::shared_ptr anim = Animation::LoadFromFile("data/" + anim_filename + ".anim", skeleton.get()); + skeleton->AddAnimation(anim_name, anim); + } + + + } + + return skeleton; +} diff --git a/src/assets/skeleton.hpp b/src/assets/skeleton.hpp new file mode 100644 index 0000000..e8ed7f2 --- /dev/null +++ b/src/assets/skeleton.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include "utils/transform.hpp" +#include "animation.hpp" + +namespace assets +{ + struct Bone + { + int parent_idx; + std::string name; + Transform bind_transform; + glm::mat4 inv_bind_matrix; + }; + + class Skeleton + { + std::vector bones_; + std::map bone_map_; + + std::map> anims_; + + public: + Skeleton() = default; + + void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform); + + int GetBoneIndex(const std::string& name) const; + + size_t GetNumBones() const { return bones_.size(); } + const Bone& GetBone(size_t idx) const { return bones_[idx]; } + + const Animation* GetAnimation(const std::string& name) const; + + static std::shared_ptr LoadFromFile(const std::string& filename); + + private: + void AddAnimation(const std::string& name, const std::shared_ptr& anim) { anims_[name] = anim; } + }; + + +} \ No newline at end of file diff --git a/src/client/app.cpp b/src/client/app.cpp new file mode 100644 index 0000000..86a1568 --- /dev/null +++ b/src/client/app.cpp @@ -0,0 +1,50 @@ +#include "app.hpp" + +#include + +App::App() +{ + std::cout << "Initializing App..." << std::endl; + + +} + +void App::Frame() +{ + float delta_time = time_ - prev_time_; + prev_time_ = time_; + + if (delta_time < 0.0f) + { + delta_time = 0.0f; // Prevent negative delta time + } + else if (delta_time > 0.1f) + { + delta_time = 0.1f; // Cap delta time to avoid large jumps + } + + // detect inputs originating in this frame + game::PlayerInputFlags new_input = input_ & ~prev_input_; + prev_input_ = input_; + + float aspect = static_cast(viewport_size_.x) / static_cast(viewport_size_.y); + + renderer_.Begin(viewport_size_.x, viewport_size_.y); + + // TODO: draw world +} + +void App::MouseMove(const glm::vec2& delta) +{ + float sensitivity = 0.002f; // Sensitivity factor for mouse movement + + float delta_yaw = delta.x * sensitivity; + float delta_pitch = -delta.y * sensitivity; + + // TODO: rotate +} + +App::~App() +{ + +} diff --git a/src/client/app.hpp b/src/client/app.hpp new file mode 100644 index 0000000..48f86bc --- /dev/null +++ b/src/client/app.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "game/player_input.hpp" +#include "gfx/renderer.hpp" + +class App +{ + float time_ = 0.0f; + glm::ivec2 viewport_size_ = { 800, 600 }; + game::PlayerInputFlags input_ = 0; + game::PlayerInputFlags prev_input_ = 0; + + float prev_time_ = 0.0f; + + gfx::Renderer renderer_; + +public: + App(); + + void Frame(); + + void SetTime(float time) { time_ = time; } + void SetViewportSize(int width, int height) { viewport_size_ = { width, height }; } + void SetInput(game::PlayerInputFlags input) { input_ = input; } + void MouseMove(const glm::vec2& delta); + + ~App(); +}; + diff --git a/src/client/gl.hpp b/src/client/gl.hpp new file mode 100644 index 0000000..2660e67 --- /dev/null +++ b/src/client/gl.hpp @@ -0,0 +1,8 @@ +#if defined(EMSCRIPTEN) || defined(ANDROID) +#define PG_GLES +#include +// #include +#else +#include +#endif + diff --git a/src/client/main.cpp b/src/client/main.cpp new file mode 100644 index 0000000..86bbd17 --- /dev/null +++ b/src/client/main.cpp @@ -0,0 +1,243 @@ +#include +#include +#include +#include "app.hpp" + +#ifdef EMSCRIPTEN +#include +#include +#endif + +#include "gl.hpp" + +static SDL_Window *s_window = nullptr; +static SDL_GLContext s_context = nullptr; +static bool s_quit = false; +static std::unique_ptr s_app; + +static void ThrowSDLError(const std::string& message) +{ + std::string error = SDL_GetError(); + throw std::runtime_error(message + ": " + error); +} + +static void InitSDL() +{ + std::cout << "Initializing SDL..." << std::endl; + if (SDL_Init(SDL_INIT_VIDEO) != 0) + { + ThrowSDLError("SDL_Init"); + } + +#ifdef PG_GLES + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); +#else + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); +#endif + + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + + std::cout << "Creating SDL window..." << std::endl; + s_window = SDL_CreateWindow("PortalGame", 100, 100, 640, 480, SDL_WINDOW_SHOWN | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); + if (!s_window) + { + ThrowSDLError("SDL_CreateWindow"); + } +} + +#ifndef PG_GLES +static void APIENTRY GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { + //if (severity == 0x826b) + // return; + // + ////std::cout << message << std::endl; + fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n", + (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""), + type, severity, message); +} + +#endif // PG_GLES + + +static void InitGL() +{ + std::cout << "Creating OpenGL context..." << std::endl; + s_context = SDL_GL_CreateContext(s_window); + if (!s_context) + { + ThrowSDLError("SDL_GL_CreateContext"); + } + + // Make context current + if (SDL_GL_MakeCurrent(s_window, s_context) != 0) + { + SDL_GL_DeleteContext(s_context); + ThrowSDLError("SDL_GL_MakeCurrent"); + } + +#ifndef PG_GLES + // Initialize GLAD + std::cout << "Initializing GLAD..." << std::endl; + if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) + { + SDL_GL_DeleteContext(s_context); + throw std::runtime_error("Failed to initialize GLAD"); + } + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageCallback(GLDebugCallback, 0); + + SDL_GL_SetSwapInterval(0); +#endif // PG_GLES + +} + +static void ShutdownGL() +{ + if (s_context) + { + SDL_GL_DeleteContext(s_context); + s_context = nullptr; + } +} + +static void ShutdownSDL() +{ + if (s_window) + { + SDL_DestroyWindow(s_window); + s_window = nullptr; + } + SDL_Quit(); +} + +static void PollEvents() +{ + SDL_Event event; + while (SDL_PollEvent(&event)) + { + switch (event.type) + { + case SDL_QUIT: + s_quit = true; + return; + + case SDL_MOUSEMOTION: + int xrel = event.motion.xrel; + int yrel = event.motion.yrel; + if (xrel != 0 || yrel != 0) + { + s_app->MouseMove(glm::vec2(static_cast(xrel), static_cast(yrel))); + } + break; + } + + } +} + +static void Frame() +{ + Uint32 current_time = SDL_GetTicks(); + s_app->SetTime(current_time / 1000.0f); // Set time in seconds + + PollEvents(); + + int width, height; + SDL_GetWindowSize(s_window, &width, &height); + s_app->SetViewportSize(width, height); + + game::PlayerInputFlags input = 0; + const uint8_t* kbd_state = SDL_GetKeyboardState(nullptr); + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_w)]) + input |= game::PI_FORWARD; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_s)]) + input |= game::PI_BACKWARD; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_a)]) + input |= game::PI_LEFT; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_d)]) + input |= game::PI_RIGHT; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_SPACE)]) + input |= game::PI_JUMP; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_LCTRL)]) + input |= game::PI_CROUCH; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_e)]) + input |= game::PI_USE; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_F3)]) + input |= game::PI_DEBUG1; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_F4)]) + input |= game::PI_DEBUG2; + + if (kbd_state[SDL_GetScancodeFromKey(SDLK_F5)]) + input |= game::PI_DEBUG3; + + int mouse_state = SDL_GetMouseState(nullptr, nullptr); + + if (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT)) + input |= game::PI_ATTACK; + + + s_app->SetInput(input); + + s_app->Frame(); + SDL_GL_SwapWindow(s_window); +} + +static void Main() { + InitSDL(); + + try + { + InitGL(); + } + catch (...) + { + ShutdownSDL(); + throw; + } + + SDL_SetRelativeMouseMode(SDL_TRUE); + + s_app = std::make_unique(); + +#ifdef EMSCRIPTEN + emscripten_set_main_loop(Frame, 0, true); +#else + while (!s_quit) + { + Frame(); + } + + s_app.reset(); + + ShutdownGL(); + ShutdownSDL(); +#endif +} + +int main(int argc, char *argv[]) +{ + try { + Main(); + } + catch (const std::exception& e) { + std::cerr << "[ERROR] " << e.what() << std::endl; + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", e.what(), nullptr); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/src/client/utils.hpp b/src/client/utils.hpp new file mode 100644 index 0000000..3a45720 --- /dev/null +++ b/src/client/utils.hpp @@ -0,0 +1,13 @@ +#pragma once + +// Trida zajistujici, ze je objekt nekopirovatelny a nepresouvatelny +class NonCopyableNonMovable { +public: + NonCopyableNonMovable() = default; + + NonCopyableNonMovable(const NonCopyableNonMovable&) = delete; + NonCopyableNonMovable& operator=(const NonCopyableNonMovable&) = delete; + + NonCopyableNonMovable(NonCopyableNonMovable&&) = delete; + NonCopyableNonMovable& operator=(NonCopyableNonMovable&&) = delete; +}; diff --git a/src/collision/aabb.hpp b/src/collision/aabb.hpp new file mode 100644 index 0000000..04aa18e --- /dev/null +++ b/src/collision/aabb.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +namespace collision +{ + template + struct AABB + { + static constexpr float C_INFINITY = std::numeric_limits::infinity(); + + using Vec = glm::vec; + + Vec min = Vec(C_INFINITY); + Vec max = Vec(-C_INFINITY); + + AABB() = default; + AABB(const Vec& min, const Vec& max) : min(min), max(max) {} + + void AddPoint(const Vec& point) + { + min = glm::min(min, point); + max = glm::max(max, point); + } + + bool CollidesWith(const AABB& other) const; + + AABB Intersection(const AABB& other) const + { + Vec new_min = glm::max(min, other.min); + Vec new_max = glm::min(max, other.max); + return AABB(new_min, new_max); + } + }; + + template <> + inline bool AABB<2>::CollidesWith(const AABB<2>& other) const + { + return (min.x <= other.max.x && max.x >= other.min.x) && + (min.y <= other.max.y && max.y >= other.min.y); + } + + template <> + inline bool AABB<3>::CollidesWith(const AABB<3>& other) const + { + return (min.x <= other.max.x && max.x >= other.min.x) && + (min.y <= other.max.y && max.y >= other.min.y) && + (min.z <= other.max.z && max.z >= other.min.z); + } + + using AABB2 = AABB<2>; + using AABB3 = AABB<3>; +} \ No newline at end of file diff --git a/src/collision/trianglemesh.cpp b/src/collision/trianglemesh.cpp new file mode 100644 index 0000000..661e7d7 --- /dev/null +++ b/src/collision/trianglemesh.cpp @@ -0,0 +1,20 @@ +#include "trianglemesh.hpp" + +collision::TriangleMesh::TriangleMesh() +{ + +} + +void collision::TriangleMesh::AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) +{ + btVector3 bt_v0(v0.x, v0.y, v0.z); + btVector3 bt_v1(v1.x, v1.y, v1.z); + btVector3 bt_v2(v2.x, v2.y, v2.z); + bt_mesh_.addTriangle(bt_v0, bt_v1, bt_v2, true); +} + +void collision::TriangleMesh::Build() +{ + bt_shape_ = std::make_unique(&bt_mesh_, true, true); + +} diff --git a/src/collision/trianglemesh.hpp b/src/collision/trianglemesh.hpp new file mode 100644 index 0000000..72cb95b --- /dev/null +++ b/src/collision/trianglemesh.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include "aabb.hpp" + +#include + +#include + +namespace collision +{ + class TriangleMesh + { + btTriangleMesh bt_mesh_; + std::unique_ptr bt_shape_; + + public: + TriangleMesh(); + + void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2); + void Build(); + + btBvhTriangleMeshShape* GetShape() { return bt_shape_.get(); } + }; +} \ No newline at end of file diff --git a/src/game/player_input.hpp b/src/game/player_input.hpp new file mode 100644 index 0000000..a5eeb89 --- /dev/null +++ b/src/game/player_input.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace game +{ + using PlayerInputFlags = uint16_t; + + enum PlayerInputFlag : PlayerInputFlags + { + PI_FORWARD = 1 << 0, + PI_BACKWARD = 1 << 1, + PI_LEFT = 1 << 2, + PI_RIGHT = 1 << 3, + PI_JUMP = 1 << 4, + PI_CROUCH = 1 << 5, + PI_USE = 1 << 6, + PI_ATTACK = 1 << 7, + PI_DEBUG1 = 1 << 8, + PI_DEBUG2 = 1 << 9, + PI_DEBUG3 = 1 << 10, + }; +} diff --git a/src/game/transform_node.hpp b/src/game/transform_node.hpp new file mode 100644 index 0000000..8c45682 --- /dev/null +++ b/src/game/transform_node.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "utils/transform.hpp" + +namespace game +{ + struct TransformNode + { + const TransformNode* parent = nullptr; + Transform local_transform; + glm::mat4 matrix = glm::mat4(1.0f); // Global + + TransformNode() + { + UpdateMatrix(); + } + + void UpdateMatrix() + { + matrix = local_transform.ToMatrix(); + + if (parent) + { + matrix = parent->matrix * matrix; + } + } + }; + +} \ No newline at end of file diff --git a/src/gameview/client_session.hpp b/src/gameview/client_session.hpp new file mode 100644 index 0000000..b73828e --- /dev/null +++ b/src/gameview/client_session.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace game::view +{ + +class ClientSession +{ +public: +private: +}; + +} // namespace game::view \ No newline at end of file diff --git a/src/gameview/entityview.hpp b/src/gameview/entityview.hpp new file mode 100644 index 0000000..5b825f9 --- /dev/null +++ b/src/gameview/entityview.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "game/transform_node.hpp" +#include "gfx/draw_list.hpp" + +class World; + +namespace game::view +{ + +class EntityView +{ +public: + EntityView(World& world) : world_(world) {} + + virtual void Draw(gfx::DrawList& dlist) {} + virtual void Update() {} + +protected: + World& world_; + TransformNode root_; + bool visible_ = false; + +}; + +} // namespace game::view \ No newline at end of file diff --git a/src/gameview/worldview.hpp b/src/gameview/worldview.hpp new file mode 100644 index 0000000..5a561e9 --- /dev/null +++ b/src/gameview/worldview.hpp @@ -0,0 +1,14 @@ +#pragma once + +namespace game::view +{ + +class WorldView +{ +public: + +private: + +}; + +} // namespace game::view \ No newline at end of file diff --git a/src/gfx/buffer_object.cpp b/src/gfx/buffer_object.cpp new file mode 100644 index 0000000..0964b96 --- /dev/null +++ b/src/gfx/buffer_object.cpp @@ -0,0 +1,29 @@ +#include +#include "buffer_object.hpp" + +gfx::BufferObject::BufferObject(GLenum target, GLenum usage) : m_target(target), m_usage(usage), m_size(0U) { + glGenBuffers(1, &m_id); +} + +gfx::BufferObject::~BufferObject() { + glDeleteBuffers(1, &m_id); +} + +void gfx::BufferObject::Bind() const { + glBindBuffer(m_target, m_id); +} + +void gfx::BufferObject::Unbind() const { + glBindBuffer(m_target, 0); +} + +void gfx::BufferObject::SetData(const void* data, size_t size) { + Bind(); + + if (size > m_size) + glBufferData(m_target, size, data, m_usage); + else + glBufferSubData(m_target, 0, size, data); + + m_size = size; +} diff --git a/src/gfx/buffer_object.hpp b/src/gfx/buffer_object.hpp new file mode 100644 index 0000000..fc2f083 --- /dev/null +++ b/src/gfx/buffer_object.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "client/gl.hpp" +#include "client/utils.hpp" + +namespace gfx +{ +/** +* \brief Wrapper pro OpenGL buffer object +*/ +class BufferObject : public NonCopyableNonMovable +{ + GLuint m_id; + GLenum m_target; + GLenum m_usage; + size_t m_size; + +public: + BufferObject(GLenum target, GLenum usage); + ~BufferObject(); + + void Bind() const; + void Unbind() const; + + void SetData(const void* data, size_t size); + + GLuint GetId() const { return m_id; } + +}; + +} diff --git a/src/gfx/draw_list.hpp b/src/gfx/draw_list.hpp new file mode 100644 index 0000000..3deaee0 --- /dev/null +++ b/src/gfx/draw_list.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "assets/map.hpp" +#include "assets/skeleton.hpp" +#include "surface.hpp" + +namespace gfx +{ + +struct DrawSurfaceCmd +{ + const Surface* surface; + const glm::mat4* matrices; // model matrix, continues in array of matrices for skeletal meshes + const glm::vec4* color; // optional tint + uint32_t first; // first triangle index + uint32_t count; // num triangles + float dist; // distance to camera - for transparnt sorting +}; + +struct DrawList +{ + std::vector surfaces; + + void AddSurface(const DrawSurfaceCmd& cmd) { surfaces.emplace_back(cmd); } + + void Clear() { surfaces.clear(); } +}; + +} // namespace gfx \ No newline at end of file diff --git a/src/gfx/renderer.cpp b/src/gfx/renderer.cpp new file mode 100644 index 0000000..99f237c --- /dev/null +++ b/src/gfx/renderer.cpp @@ -0,0 +1,178 @@ +#include "renderer.hpp" + +#include +#include + +#include +#include + +#include "client/gl.hpp" + +#include "gfx/shader_sources.hpp" + +gfx::Renderer::Renderer() +{ + ShaderSources::MakeShader(mesh_shader_.shader, SS_MESH_VERT, SS_MESH_FRAG); + ShaderSources::MakeShader(skel_mesh_shader_.shader, SS_SKEL_MESH_VERT, SS_SKEL_MESH_FRAG); + ShaderSources::MakeShader(solid_shader_, SS_SOLID_VERT, SS_SOLID_FRAG); +} + +void gfx::Renderer::Begin(size_t width, size_t height) +{ + glViewport(0, 0, width, height); +} + +void gfx::Renderer::ClearColor(const glm::vec3& color) +{ + glClearColor(color.r, color.g, color.b, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); +} + +void gfx::Renderer::ClearDepth() +{ + glClear(GL_DEPTH_BUFFER_BIT); +} + +void gfx::Renderer::DrawList(gfx::DrawList& list, const DrawListParams& params) +{ + DrawSurfaceList(list.surfaces, params); +} + +void gfx::Renderer::InvalidateShaders() +{ + InvalidateMeshShader(mesh_shader_); + InvalidateMeshShader(skel_mesh_shader_); +} + +void gfx::Renderer::InvalidateMeshShader(MeshShader& mshader) +{ + mshader.global_setup = false; + mshader.color = glm::vec4(-1.0f); // invalidate color +} + +void gfx::Renderer::SetupMeshShader(MeshShader& mshader, const DrawListParams& params) +{ + const Shader& shader = *mshader.shader; + + if (current_shader_ != &shader) + { + glUseProgram(shader.GetId()); + current_shader_ = &shader; + } + + if (mshader.global_setup) + { + return; // Global uniforms are already set up + } + + glUniformMatrix4fv(shader.U(gfx::SU_VIEW_PROJ), 1, GL_FALSE, ¶ms.view_proj[0][0]); + + mshader.global_setup = true; +} + +void gfx::Renderer::DrawSurfaceList(std::span list, const DrawListParams& params) +{ + // sort the list to minimize state changes + std::ranges::sort(list, [](const DrawSurfaceCmd& a, const DrawSurfaceCmd& b) { + const Surface* sa = a.surface; + const Surface* sb = b.surface; + + const bool trans_a = sa->sflags & SF_TRANSPARENT; + const bool trans_b = sb->sflags & SF_TRANSPARENT; + + if (trans_a != trans_b) + return trans_b; // opaque first + + if (trans_a) // both transparent + { + return a.dist > b.dist; // do not optimize transparent, sort by distance instead + } + + if (auto cmp = sa <=> sb; cmp != 0) + return cmp < 0; + + if (auto cmp = sa->texture <=> sb->texture; cmp != 0) + return cmp < 0; + + if (auto cmp = sa->va.get() <=> sb->va.get(); cmp != 0) + return cmp < 0; + }); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + + InvalidateShaders(); + + // cache to eliminate fake state changes + const gfx::Texture* last_texture = nullptr; + const gfx::VertexArray* last_vao = nullptr; + bool last_double_sided = false; + + glActiveTexture(GL_TEXTURE0); // for all future bindings + + for (const DrawSurfaceCmd& cmd : list) + { + const Surface* surface = cmd.surface; + + // mesh flags + const bool skeletal_flag = surface->mflags & MF_SKELETAL; + // surface flags + const bool double_sided_flag = surface->sflags & SF_DOUBLE_SIDED; + const bool transparent_flag = surface->sflags & SF_TRANSPARENT; + const bool object_color_flag = surface->sflags & SF_OBJECT_COLOR; + + // adjust state + if (last_double_sided != double_sided_flag) + { + if (double_sided_flag) + glDisable(GL_CULL_FACE); + else + glEnable(GL_CULL_FACE); + + last_double_sided = double_sided_flag; + } + + // select shader + MeshShader& mshader = skeletal_flag ? skel_mesh_shader_ : mesh_shader_; + SetupMeshShader(mshader, params); + + // set model matrix + glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &cmd.matrices[0][0][0]); + + // set color + glm::vec4 color = (object_color_flag && cmd.color) ? glm::vec4(*cmd.color) : glm::vec4(1.0f); + if (mshader.color != color) + { + glUniform4fv(mshader.shader->U(gfx::SU_COLOR), 1, &color[0]); + mshader.color = color; + } + + // bind texture + if (last_texture != surface->texture.get()) + { + GLuint tex_id = surface->texture ? surface->texture->GetId() : 0; + glBindTexture(GL_TEXTURE_2D, tex_id); + last_texture = surface->texture.get(); + } + + // bind VAO + if (last_vao != surface->va.get()) + { + glBindVertexArray(surface->va->GetVAOId()); + last_vao = surface->va.get(); + } + + size_t first_tri = surface->first + cmd.first; + size_t num_tris = cmd.count ? cmd.count : surface->count; + + // draw + glDrawElements(GL_TRIANGLES, static_cast(num_tris * 3U), GL_UNSIGNED_INT, + (void*)(first_tri * 3U * sizeof(GLuint))); + } + + + +} diff --git a/src/gfx/renderer.hpp b/src/gfx/renderer.hpp new file mode 100644 index 0000000..53cd8d8 --- /dev/null +++ b/src/gfx/renderer.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include "shader.hpp" +#include "draw_list.hpp" + +namespace gfx +{ + struct DrawListParams + { + glm::mat4 view_proj; + }; + + struct MeshShader + { + std::unique_ptr shader; + + // cached state to avoid redundant uniform updates which are expensive especially on WebGL + bool global_setup = false; + glm::vec4 color = glm::vec4(-1.0f); // invalid to force initial setup + }; + + class Renderer + { + public: + Renderer(); + + void Begin(size_t width, size_t height); + + void ClearColor(const glm::vec3& color); + void ClearDepth(); + + void DrawList(gfx::DrawList& list, const DrawListParams& params); + + private: + MeshShader mesh_shader_; + MeshShader skel_mesh_shader_; + std::unique_ptr solid_shader_; + + const Shader* current_shader_ = nullptr; + + void InvalidateShaders(); + void InvalidateMeshShader(MeshShader& mshader); + void SetupMeshShader(MeshShader& mshader, const DrawListParams& params); + + void DrawSurfaceList(std::span queue, const DrawListParams& params); + + }; + +} \ No newline at end of file diff --git a/src/gfx/shader.cpp b/src/gfx/shader.cpp new file mode 100644 index 0000000..be85c3e --- /dev/null +++ b/src/gfx/shader.cpp @@ -0,0 +1,103 @@ +#include +#include +#include "shader.hpp" +#define LOG_LENGTH 1024 + +// Nazvy uniformnich promennych v shaderech +static const char* const s_uni_names[] = { + "u_model", // SU_MODEL + "u_view_proj", // SU_VIEW_PROJ + "u_tex", // SU_TEX + "u_color" // SU_COLOR +}; + +// Vytvori shader z daneho zdroje +static GLuint CreateShader(const char* src, GLenum type) { + GLuint id = glCreateShader(type); + + glShaderSource(id, 1, &src, NULL); + glCompileShader(id); + + GLint is_compiled = 0; + glGetShaderiv(id, GL_COMPILE_STATUS, &is_compiled); + if (is_compiled == GL_FALSE) { + GLchar log[LOG_LENGTH]; + glGetShaderInfoLog(id, LOG_LENGTH, NULL, log); + + glDeleteShader(id); + + throw std::runtime_error(std::string("Nelze zkompilovat shader: ") + log); + } + + return id; +} + +gfx::Shader::Shader(const char* vert_src, const char* frag_src) { + m_id = glCreateProgram(); + + if (!m_id) + throw std::runtime_error("Nelze vytvorit program!"); + + GLuint vert, frag; + + // Vytvoreni vertex shaderu + vert = CreateShader(vert_src, GL_VERTEX_SHADER); + + try { + // Vyvoreni fragment shaderu + frag = CreateShader(frag_src, GL_FRAGMENT_SHADER); + } + catch (std::exception& e) { + glDeleteShader(vert); + throw e; + } + + // Pripojeni shaderu k programu + glAttachShader(m_id, vert); + glAttachShader(m_id, frag); + + // Linknuti programu + glLinkProgram(m_id); + + // Smazani shaderu + glDeleteShader(vert); + glDeleteShader(frag); + + GLint is_linked = 0; + glGetProgramiv(m_id, GL_LINK_STATUS, &is_linked); + if (is_linked == GL_FALSE) { + GLchar log[LOG_LENGTH]; + glGetProgramInfoLog(m_id, LOG_LENGTH, NULL, log); + + glDeleteProgram(m_id); + + throw std::runtime_error(std::string("Nelze linknout shader: ") + log); + } + + // Ziskani lokaci uniformnich promennych + for (size_t i = 0; i < SU_COUNT; i++) + m_uni[i] = glGetUniformLocation(m_id, s_uni_names[i]); + + SetupBindings(); +} + +gfx::Shader::~Shader() { + // Smazani programu + glDeleteProgram(m_id); +} + +void gfx::Shader::SetupBindings() +{ + glUseProgram(m_id); + + glUniform1i(m_uni[SU_TEX], 0); + + // Bones UBO + int ubo_index = glGetUniformBlockIndex(m_id, "Bones"); + if (ubo_index != GL_INVALID_INDEX) + { + glUniformBlockBinding(m_id, ubo_index, 0); // bind to binding point 0 + } + + glUseProgram(0); +} diff --git a/src/gfx/shader.hpp b/src/gfx/shader.hpp new file mode 100644 index 0000000..abf4b4c --- /dev/null +++ b/src/gfx/shader.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "client/gl.hpp" +#include "client/utils.hpp" + +namespace gfx +{ + /** + * \brief Uniformy shaderu + */ + enum ShaderUniform + { + SU_MODEL, + SU_VIEW_PROJ, + SU_TEX, + SU_COLOR, + + SU_COUNT + }; + + /** + * \brief Wrapper pro OpenGL shader program + */ + class Shader : public NonCopyableNonMovable + { + GLuint m_id; + GLint m_uni[SU_COUNT]; + + public: + Shader(const char* vert_src, const char* frag_src); + ~Shader(); + + /** + * \brief Vr�t� lokaci uniformy + * + * \param u Uniforma + * \return Lokace + */ + GLint U(ShaderUniform u) const { return m_uni[u]; } + + /** + * \brief Vr�t� OpenGL n�zev shaderu + * + * \return N�zev shaderu + */ + GLuint GetId() const { return m_id; } + + private: + void SetupBindings(); + + }; +} diff --git a/src/gfx/shader_defs.hpp b/src/gfx/shader_defs.hpp new file mode 100644 index 0000000..ea48079 --- /dev/null +++ b/src/gfx/shader_defs.hpp @@ -0,0 +1,4 @@ +#pragma once + +#define SD_MAX_LIGHTS 4 +#define SD_MAX_BONES 256 diff --git a/src/gfx/shader_sources.cpp b/src/gfx/shader_sources.cpp new file mode 100644 index 0000000..df35b23 --- /dev/null +++ b/src/gfx/shader_sources.cpp @@ -0,0 +1,206 @@ +#include "shader_sources.hpp" +#include "shader_defs.hpp" +#include "client/gl.hpp" + +#ifndef PG_GLES +#define GLSL_VERSION \ + "#version 330 core\n" \ + "\n" +#else +#define GLSL_VERSION \ + "#version 300 es\n" \ + "precision mediump float;\n" \ + "\n" +#endif + +#define STRINGIFY_HELPER(x) #x +#define STRINGIFY(x) STRINGIFY_HELPER(x) + +#define SHADER_DEFS \ + "#define MAX_LIGHTS " STRINGIFY(SD_MAX_LIGHTS) "\n" \ + "#define MAX_BONES " STRINGIFY(SD_MAX_BONES) "\n" \ + "\n" + +#define SHADER_HEADER \ + GLSL_VERSION \ + SHADER_DEFS + +#define MESH_MATRICES_GLSL R"GLSL( + uniform mat4 u_view_proj; + uniform mat4 u_model; // Transform matrix +)GLSL" + +#define LIGHT_MATRICES_GLSL R"GLSL( + uniform vec3 u_ambient_light; + uniform int u_num_lights; + uniform vec3 u_light_positions[MAX_LIGHTS]; + uniform vec4 u_light_colors_rs[MAX_LIGHTS]; // rgb = color, a = radius +)GLSL" + +#define COMPUTE_LIGHTS_GLSL R"GLSL( + vec3 ComputeLights(in vec3 sector_pos, in vec3 sector_normal) + { + vec3 color = u_ambient_light; + for (int i = 0; i < u_num_lights; ++i) { + vec3 light_pos = u_light_positions[i]; + vec3 light_color = u_light_colors_rs[i].rgb; + float light_radius = u_light_colors_rs[i].a; + + vec3 to_light = light_pos - sector_pos.xyz; + float dist2 = dot(to_light, to_light); + if (dist2 < light_radius * light_radius) { + float dist = sqrt(dist2); + float attenuation = 1.0 - (dist / light_radius); + float dot = max(dot(sector_normal, normalize(to_light)), 0.0); + color += light_color * dot * attenuation; + } + } + return color; + } +)GLSL" + +// Zdrojove kody shaderu +static const char* const s_srcs[] = { + +// SS_MESH_VERT +SHADER_HEADER +R"GLSL( +layout (location = 0) in vec3 a_pos; +layout (location = 1) in vec3 a_normal; +layout (location = 2) in vec3 a_color; +layout (location = 3) in vec2 a_uv; + +)GLSL" +MESH_MATRICES_GLSL +LIGHT_MATRICES_GLSL +COMPUTE_LIGHTS_GLSL +R"GLSL( + +out vec2 v_uv; +out vec3 v_color; + +void main() { + vec4 world_pos = u_model * vec4(a_pos, 1.0); + vec3 world_normal = normalize(mat3(u_model) * a_normal); + gl_Position = u_view_proj * world_pos; + + v_uv = vec2(a_uv.x, 1.0 - a_uv.y); + v_color = ComputeLights(world_pos.xyz, world_normal) * a_color; +} +)GLSL", + +// SS_MESH_FRAG +SHADER_HEADER +R"GLSL( +in vec2 v_uv; +in vec3 v_color; + +uniform sampler2D u_tex; + +layout (location = 0) out vec4 o_color; + +void main() { + o_color = vec4(texture(u_tex, v_uv)); + o_color.rgb *= v_color; // Apply vertex color + //o_color = vec4(1.0, 0.0, 0.0, 1.0); +} + +)GLSL", + +// SS_SKEL_MESH_VERT +SHADER_HEADER +R"GLSL( +layout (location = 0) in vec3 a_pos; +layout (location = 1) in vec3 a_normal; +layout (location = 3) in vec2 a_uv; +layout (location = 5) in ivec4 a_bone_ids; +layout (location = 6) in vec4 a_bone_weights; + +layout (std140) uniform Bones { + mat4 u_bone_matrices[MAX_BONES]; +}; + +)GLSL" +MESH_MATRICES_GLSL +LIGHT_MATRICES_GLSL +COMPUTE_LIGHTS_GLSL +R"GLSL( + +out vec2 v_uv; + +out vec3 v_color; + +void main() { + mat4 bone_transform = mat4(0.0); + for (int i = 0; i < 4; ++i) { + int bone_id = a_bone_ids[i]; + if (bone_id >= 0) { + bone_transform += u_bone_matrices[bone_id] * a_bone_weights[i]; + } + } + + vec4 world_pos = u_model * bone_transform * vec4(a_pos, 1.0); + vec3 world_normal = normalize(mat3(u_model) * mat3(bone_transform) * a_normal); + gl_Position = u_view_proj * world_pos; + + v_uv = vec2(a_uv.x, 1.0 - a_uv.y); + + v_color = ComputeLights(world_pos.xyz, world_normal); +} +)GLSL", + +// SS_SKEL_MESH_FRAG +SHADER_HEADER +R"GLSL( +in vec2 v_uv; + +in vec3 v_color; + +uniform sampler2D u_tex; + +layout (location = 0) out vec4 o_color; + +void main() { + o_color = vec4(texture(u_tex, v_uv)); + o_color.rgb *= v_color; // Apply vertex color +} + +)GLSL", + +// SS_SOLID_VERT +SHADER_HEADER +R"GLSL( +layout (location = 0) in vec3 a_pos; +layout (location = 2) in vec4 a_color; + +uniform mat4 u_view_proj; +uniform vec4 u_color; + +out vec4 v_color; + +void main() { + gl_Position = u_view_proj * vec4(a_pos, 1.0); + v_color = a_color * u_color; +} +)GLSL", + +// SS_SOLID_FRAG +SHADER_HEADER +R"GLSL( +in vec4 v_color; + +layout (location = 0) out vec4 o_color; + +void main() { + o_color = v_color; +} + +)GLSL", + + +}; + +// Vrati zdrojovy kod shaderu +const char* gfx::ShaderSources::Get(ShaderSource ss) { + return s_srcs[ss]; +} diff --git a/src/gfx/shader_sources.hpp b/src/gfx/shader_sources.hpp new file mode 100644 index 0000000..3b92885 --- /dev/null +++ b/src/gfx/shader_sources.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include "shader.hpp" + +namespace gfx +{ + enum ShaderSource + { + SS_MESH_VERT, + SS_MESH_FRAG, + + SS_SKEL_MESH_VERT, + SS_SKEL_MESH_FRAG, + + SS_SOLID_VERT, + SS_SOLID_FRAG, + + }; + + class ShaderSources + { + + public: + // Vrati zdrojovy kod shaderu + static const char* Get(ShaderSource ss); + + static void MakeShader(std::unique_ptr& shader, ShaderSource ss_vert, ShaderSource ss_frag) { + shader = std::make_unique( + Get(ss_vert), + Get(ss_frag) + ); + } + }; +} + diff --git a/src/gfx/surface.hpp b/src/gfx/surface.hpp new file mode 100644 index 0000000..44a32e7 --- /dev/null +++ b/src/gfx/surface.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "texture.hpp" +#include "vertex_array.hpp" + +namespace gfx +{ + +using MeshFlags = uint8_t; + +enum MeshFlag : MeshFlags +{ + MF_NONE = 0x00, + MF_LIGHTMAP_UV = 0x01, // unused + MF_SKELETAL = 0x02, +}; + +using SurfaceFlags = uint8_t; + +enum SurfaceFlag : SurfaceFlags +{ + SF_NONE = 0x00, + SF_DOUBLE_SIDED = 0x01, + SF_TRANSPARENT = 0x02, + SF_OBJECT_COLOR = 0x08, // use object level tint +}; + +struct Surface +{ + std::shared_ptr texture; + std::shared_ptr va; + size_t first = 0; // first triangle VA EBO + size_t count = 0; // number of triangles + MeshFlags mflags = MF_NONE; + SurfaceFlags sflags = SF_NONE; +}; + +} // namespace gfx \ No newline at end of file diff --git a/src/gfx/texture.cpp b/src/gfx/texture.cpp new file mode 100644 index 0000000..9044de9 --- /dev/null +++ b/src/gfx/texture.cpp @@ -0,0 +1,87 @@ +#include "texture.hpp" +#include + +#include "utils/files.hpp" + +#define STB_IMAGE_IMPLEMENTATION +#include + +static void GetGLFilterModes(bool linear, bool mipmaps, GLenum& filter_min, GLenum& filter_mag) { + if (linear) + { + filter_min = GL_LINEAR; + filter_mag = GL_LINEAR; + + if (mipmaps) + { + filter_min = GL_LINEAR_MIPMAP_LINEAR; + } + } + else + { + filter_min = GL_NEAREST; + filter_mag = GL_NEAREST; + + if (mipmaps) + { + // Mipmaps always linear + filter_min = GL_NEAREST_MIPMAP_LINEAR; + } + } +} + +gfx::Texture::Texture(GLuint width, GLuint height, const void* data, GLint internalformat, GLenum format, GLenum type, bool linear, bool mipmaps) { + glGenTextures(1, &m_id); + + if (!m_id) + throw std::runtime_error("Nelze vytvorit texturu!"); + + glBindTexture(GL_TEXTURE_2D, m_id); + + glTexImage2D(GL_TEXTURE_2D, 0, internalformat, width, height, 0, format, type, data); + + GLenum filter_min, filter_mag; + GetGLFilterModes(linear, mipmaps, filter_min, filter_mag); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter_min); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter_mag); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + if (mipmaps) + { + glGenerateMipmap(GL_TEXTURE_2D); + } + + glBindTexture(GL_TEXTURE_2D, 0); +} + +gfx::Texture::~Texture() { + glDeleteTextures(1, &m_id); +} + +std::shared_ptr gfx::Texture::LoadFromFile(const std::string& filename) +{ + printf("Loading texture from file: %s\n", filename.c_str()); + + std::string content = fs::ReadFileAsString(filename); + + int width, height, channels; + unsigned char* data = stbi_load_from_memory(reinterpret_cast(content.data()), content.size(), &width, &height, &channels, 4); + + if (!data) { + throw std::runtime_error("Failed to load texture from file: " + filename); + } + + std::shared_ptr texture; + try { + texture = std::make_shared(width, height, data, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, false, true); + } + catch (const std::exception& e) { + stbi_image_free(data); + throw; // Rethrow the exception after freeing the data + } + + stbi_image_free(data); + return texture; +} diff --git a/src/gfx/texture.hpp b/src/gfx/texture.hpp new file mode 100644 index 0000000..9f28ec8 --- /dev/null +++ b/src/gfx/texture.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include "client/utils.hpp" +#include "client/gl.hpp" + +namespace gfx +{ +/** + * \brief Wrapper pro OpenGL texturu + */ +class Texture : public NonCopyableNonMovable +{ + GLuint m_id; + +public: + Texture(GLuint width, GLuint height, const void* data, GLint internalformat, GLenum format, GLenum type, bool linear, bool mipmaps); + ~Texture(); + + GLuint GetId() const { return m_id; } + + static std::shared_ptr LoadFromFile(const std::string& filename); + +}; + +} \ No newline at end of file diff --git a/src/gfx/uniform_buffer.hpp b/src/gfx/uniform_buffer.hpp new file mode 100644 index 0000000..865d5d5 --- /dev/null +++ b/src/gfx/uniform_buffer.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "buffer_object.hpp" + +namespace gfx +{ + template + class UniformBuffer : public BufferObject + { + public: + UniformBuffer() : BufferObject(GL_UNIFORM_BUFFER, GL_DYNAMIC_DRAW) + { + } + + void SetData(const T* data, size_t count = 1) + { + BufferObject::SetData(data, sizeof(T) * count); + } + + }; +} \ No newline at end of file diff --git a/src/gfx/vertex_array.cpp b/src/gfx/vertex_array.cpp new file mode 100644 index 0000000..7e355e6 --- /dev/null +++ b/src/gfx/vertex_array.cpp @@ -0,0 +1,98 @@ +#include "vertex_array.hpp" + +#include + +// Struktura pro informace o jednotlivych vertex atributu +struct VertexAttribInfo { + GLuint index; + GLint size; + GLenum type; + GLboolean normalized; + size_t byte_size; +}; + +// Informace o jednotlivych vertex atributech +static const VertexAttribInfo s_ATTR_INFO[] = { + { 0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_POSITION + { 1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_NORMAL + { 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 4 }, // VA_COLOR + { 3, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_UV + { 4, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_LIGHTMAP_UV + { 5, 4, GL_INT, GL_FALSE, sizeof(int32_t) * 4 }, // VA_BONE_INDICES + { 6, 4, GL_FLOAT, GL_FALSE, sizeof(float) * 4 }, // VA_BONE_WEIGHTS +}; + +// Pocet typu vertex atributu +static const size_t s_ATTR_COUNT = 7; + +gfx::VertexArray::VertexArray(int attrs, int flags) : m_usage(GL_STATIC_DRAW), m_num_indices(0) { + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + + if (flags & VF_DYNAMIC) + m_usage = GL_DYNAMIC_DRAW; + + // vytvori buffer pro vrcholy + m_vbo = std::make_unique(GL_ARRAY_BUFFER, m_usage); + m_vbo->Bind(); + + // vypocet velikosti jednoho vrcholu + size_t stride = 0U; + for (size_t i = 0; i < s_ATTR_COUNT; i++) { + if (attrs & (1 << i)) + stride += s_ATTR_INFO[i].byte_size; + } + + // povoli a nastavi jednotlive vertex atributy + size_t offset = 0U; + for (size_t i = 0; i < s_ATTR_COUNT; i++) { + const auto& info = s_ATTR_INFO[i]; + if (attrs & (1 << i)) { + glEnableVertexAttribArray(info.index); + + if (info.type != GL_INT) + { + glVertexAttribPointer(info.index, info.size, info.type, info.normalized, stride, (const void*)offset); + } + else + { + glVertexAttribIPointer(info.index, info.size, info.type, stride, (const void*)offset); + } + + offset += info.byte_size; + } else if (i == 2) + { + // pokud neni barva, nastavime defaultni hodnotu 1.0f,1.0f,1.0f,1.0f + glDisableVertexAttribArray(info.index); + glVertexAttrib4f(info.index, 1.0f, 1.0f, 1.0f, 1.0f); + } + } + + // vytvori buffer pro indexy + if (flags & VF_CREATE_EBO) { + m_ebo = std::make_unique(GL_ELEMENT_ARRAY_BUFFER, m_usage); + m_ebo->Bind(); + } + + glBindVertexArray(0); +} + +gfx::VertexArray::~VertexArray() { + // uklid + glDeleteVertexArrays(1, &m_vao); +} + +// Nastavi data do VBO +void gfx::VertexArray::SetVBOData(const void* data, size_t size) { + glBindVertexArray(m_vao); + m_vbo->SetData(data, size); + glBindVertexArray(0); +} + +// Nastavi indexy do EBO +void gfx::VertexArray::SetIndices(const GLuint* data, size_t size) { + glBindVertexArray(m_vao); + m_ebo->SetData(data, size * sizeof(*data)); + m_num_indices = size; + glBindVertexArray(0); +} diff --git a/src/gfx/vertex_array.hpp b/src/gfx/vertex_array.hpp new file mode 100644 index 0000000..51d3e39 --- /dev/null +++ b/src/gfx/vertex_array.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +#include "client/gl.hpp" +#include "client/utils.hpp" +#include "buffer_object.hpp" + +namespace gfx +{ + // Vycet jednotlivych atributu vrcholu + enum VertexAttr + { + VA_POSITION = 1 << 0, + VA_NORMAL = 1 << 1, + VA_COLOR = 1 << 2, + VA_UV = 1 << 3, + VA_LIGHTMAP_UV = 1 << 4, + VA_BONE_INDICES = 1 << 5, + VA_BONE_WEIGHTS = 1 << 6, + }; + + // Informace pro vytvoreni VertexArraye + enum VertexArrayFlags + { + VF_CREATE_EBO = 1 << 0, + VF_DYNAMIC = 1 << 1, + }; + + // VertexArray - trida pro praci s modely + class VertexArray : public NonCopyableNonMovable + { + GLuint m_vao;// , m_vbo, m_ebo; + std::unique_ptr m_vbo; + std::unique_ptr m_ebo; + size_t m_num_indices; + GLenum m_usage; + + public: + VertexArray(int attrs, int flags); + ~VertexArray(); + + // Nastavi data do VBO + void SetVBOData(const void* data, size_t size); + + // Nastavi indexy do EBO + void SetIndices(const GLuint* data, size_t size); + + GLuint GetVAOId() const { return m_vao; } + GLuint GetVBOId() const { return m_vbo->GetId(); } + GLuint GetEBOId() const { + assert(m_ebo && "GetEBOId() ale !ebo"); + return m_ebo->GetId(); + } + + //size_t GetVBOSize() const { return m_vbo->; } + size_t GetNumIndices() const { return m_num_indices; } + }; + +} diff --git a/src/net/defs.hpp b/src/net/defs.hpp new file mode 100644 index 0000000..90414e9 --- /dev/null +++ b/src/net/defs.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include "fixed_str.hpp" +#include "quantized.hpp" + +namespace net +{ + +enum MessageType : uint8_t +{ + MSG_NONE, + + /*~~~~~~~~ Client->Server ~~~~~~~~*/ + // IN + MSG_IN, + + /*~~~~~~~~ World ~~~~~~~~*/ + // CHWORLD + MSG_CHWORLD, + + /*~~~~~~~~ Entity ~~~~~~~~*/ + // ENTSPAWN data... + MSG_ENTSPAWN, + // ENTMSG data... + MSG_ENTMSG, + // ENTRM + MSG_ENTRM, + + /*~~~~~~~~~~~~~~~~*/ + MSG_COUNT, +}; + +using MapName = FixedStr<32>; + +using ViewYaw = Quantized; +using ViewPitch = Quantized; + +using EntNum = uint16_t; + +enum EntType : uint8_t +{ + ET_NONE, + + ET_PAWN, + ET_CAR, + + ET_COUNT, +}; + +} // namespace net \ No newline at end of file diff --git a/src/net/fixed_str.hpp b/src/net/fixed_str.hpp new file mode 100644 index 0000000..7a1d78e --- /dev/null +++ b/src/net/fixed_str.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +namespace net +{ + +template +struct FixedStr +{ + size_t len = 0; + char str[N]; + + size_t MaxLen() const { return N; } + + FixedStr& operator=(const std::string& stdstr) + { + size_t putsize = std::min(N, stdstr.size()); + len = putsize; + memcpy(str, stdstr.data(), putsize); + } + + operator std::string() { return std::string(str, len); } + operator std::string_view() { return std::string_view(str, len); } +}; + +template +using FixedStrLen = std::conditional_t<(N > 255), uint16_t, uint8_t>; + + +} // namespace net \ No newline at end of file diff --git a/src/net/inmessage.hpp b/src/net/inmessage.hpp new file mode 100644 index 0000000..249e9bc --- /dev/null +++ b/src/net/inmessage.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include + +#include "fixed_str.hpp" +#include "quantized.hpp" + +namespace net +{ + +class InMessage +{ +public: + InMessage() : ptr_(nullptr), end_(nullptr) {} + InMessage(const char* ptr, size_t len) : ptr_(ptr), end_(ptr_ + len) {} + + const char* Ptr() const { return ptr_; } + const char* End() const { return end_; } + + bool CheckAvail(size_t n) + { + return ptr_ + n <= end_; + } + + bool Read(char* dest, size_t n) + { + if (!CheckAvail(n)) + return false; + + memcpy(dest, ptr_, n); + ptr_ += n; + return true; + } + + template + bool Read(T& value) + { + if (!CheckAvail(sizeof(T))) + return false; + + value = *(reinterpret_cast(ptr_)); + ptr_ += sizeof(T); + return true; + } + + template + bool Read(FixedStr& str) + { + FixedStrLen len; + + if (!Read(len) || len > N || !Read(str.str, len)) + return false; + + str.len = len; + return true; + } + + template + bool Read(Quantized& quant) + { + T value; + if (!Read(value)) + return false; + + quant.value = value; + return true; + } + + bool ReadVarInt(int64_t& value) + { + uint64_t uvalue = 0; + + uint8_t p; + if (!Read(p)) + return false; + + bool next = p & 0b10000000; + bool negative = p & 0b01000000; + uvalue |= p & 0b00111111; + + size_t shift = 6; + + while (next) + { + if (!Read(p)) + return false; + + next = p & 0b10000000; + uvalue |= static_cast(p & 0b01111111) << shift; + shift += 7; + + + } + + value = static_cast(uvalue); + if (negative) + value = -value; + + return true; + } + + bool SubMessage(InMessage& sub, size_t n) + { + if (!CheckAvail(n)) + return false; + + sub.ptr_ = ptr_; + ptr_ += n; + sub.end_ = ptr_; + return true; + } + + +private: + const char* ptr_; + const char* end_; +}; + +} // namespace net \ No newline at end of file diff --git a/src/net/outmessage.hpp b/src/net/outmessage.hpp new file mode 100644 index 0000000..789606f --- /dev/null +++ b/src/net/outmessage.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include + +#include "fixed_str.hpp" +#include "quantized.hpp" + +namespace net +{ + +class OutMessage +{ +public: + OutMessage(std::vector& buffer) : buffer_(buffer) { buffer.clear(); } + + template + size_t Reserve() + { + size_t pos = buffer_.size(); + buffer_.resize(pos + sizeof(T)); + return pos; + } + + template + void WriteAt(size_t pos, T value) + { + *(reinterpret_cast(&buffer_[pos])) = value; + } + + template + void Write(T value) + { + WriteAt(Reserve(), value); + } + + void Write(const char* str, size_t n) + { + size_t pos = buffer_.size(); + buffer_.resize(pos + n); + memcpy(&buffer_[pos], str, n); + } + + template + void Write(const FixedStr& str) + { + Write(static_cast>(str.len)); + Write(str.str, str.len); + } + + template + void Write(Quantized quant) + { + Write(quant.value); + } + + void WriteVarInt(int64_t value) + { + const bool negative = value < 0; + uint64_t uvalue = static_cast(negative ? -value : value); + + char p[10]; + p[0] = negative ? 0b11000000 : 0b10000000; + p[0] |= uvalue & 0b00111111; + uvalue >>= 6; + + size_t i; + for (i = 1; uvalue; ++i) + { + p[i] = 0b10000000 | (uvalue & 0b01111111); + uvalue >>= 7; + } + + p[i - 1] &= 0b01111111; // end mark + + Write(p, i); + } + +private: + std::vector& buffer_; +}; + +} // namespace net \ No newline at end of file diff --git a/src/net/quantized.hpp b/src/net/quantized.hpp new file mode 100644 index 0000000..976499e --- /dev/null +++ b/src/net/quantized.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace net +{ + +template +struct Quantized +{ + static constexpr float Min = static_cast(MinL) / static_cast(DivisorL); + static constexpr float Max = static_cast(MaxL) / static_cast(DivisorL); + + static constexpr T max_int = std::numeric_limits::max(); + static constexpr float range = Max - Min; + static constexpr float scale = static_cast(max_int) / range; + static constexpr float inv_scale = range / static_cast(max_int); + +public: + T value; + + Quantized(T value) : value(value) {} + Quantized(float fvalue) { Encode(value); } + + void Encode(float fvalue) noexcept + { + fvalue = std::clamp(fvalue, Min, Max); + + float normalized = (fvalue - Min) * scale; + value = static_cast(normalized + 0.5f); // round + } + + float Decode() const noexcept { return Min + static_cast(value) * inv_scale; } + + static constexpr float MaxError() noexcept { return inv_scale * 0.5f; } +}; + +} // namespace net \ No newline at end of file diff --git a/src/utils/files.cpp b/src/utils/files.cpp new file mode 100644 index 0000000..fb60acb --- /dev/null +++ b/src/utils/files.cpp @@ -0,0 +1,33 @@ +#include "files.hpp" + +#include +#include + +std::string fs::ReadFileAsString(const std::string& path) +{ + SDL_RWops *rw = SDL_RWFromFile(path.c_str(), "rb"); + + if (!rw) + { + throw std::runtime_error("Failed to open file: " + path); + } + + Sint64 res_size = SDL_RWsize(rw); + if (res_size < 0) + { + SDL_RWclose(rw); + throw std::runtime_error("Failed to get file size: " + path); + } + + std::string content; + content.resize(static_cast(res_size)); + SDL_RWread(rw, content.data(), 1, res_size); + SDL_RWclose(rw); + return content; +} + +std::istringstream fs::ReadFileAsStream(const std::string& path) +{ + std::string content = ReadFileAsString(path); + return std::istringstream(content); +} diff --git a/src/utils/files.hpp b/src/utils/files.hpp new file mode 100644 index 0000000..dc836b1 --- /dev/null +++ b/src/utils/files.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +namespace fs +{ + std::string ReadFileAsString(const std::string& path); + std::istringstream ReadFileAsStream(const std::string& path); +} \ No newline at end of file diff --git a/src/utils/transform.hpp b/src/utils/transform.hpp new file mode 100644 index 0000000..3dc16c5 --- /dev/null +++ b/src/utils/transform.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +struct Transform +{ + glm::vec3 position = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion + float scale = 1.0f; + + glm::mat4 ToMatrix() const + { + float x = rotation.x; + float y = rotation.y; + float z = rotation.z; + float w = rotation.w; + + float x2 = x + x; + float y2 = y + y; + float z2 = z + z; + float xx = x * x2; + float xy = x * y2; + float xz = x * z2; + float yy = y * y2; + float yz = y * z2; + float zz = z * z2; + float wx = w * x2; + float wy = w * y2; + float wz = w * z2; + + float sx = scale; + float sy = scale; + float sz = scale; + + return glm::mat4( + (1.0f - (yy + zz)) * sx, (xy + wz) * sx, (xz - wy) * sx, 0.0f, + (xy - wz) * sy, (1.0f - (xx + zz)) * sy, (yz + wx) * sy, 0.0f, + (xz + wy) * sz, (yz - wx) * sz, (1.0f - (xx + yy)) * sz, 0.0f, + position.x, position.y, position.z, 1.0f + ); + } + + void SetAngles(const glm::vec3& angles_deg) + { + glm::vec3 angles_rad = glm::radians(angles_deg); + rotation = glm::quat(angles_rad); + } + + static Transform Lerp(const Transform& a, const Transform& b, float t) + { + Transform result; + result.position = glm::mix(a.position, b.position, t); + result.rotation = glm::slerp(a.rotation, b.rotation, t); + result.scale = glm::mix(a.scale, b.scale, t); + return result; + } +}; \ No newline at end of file