Init
This commit is contained in:
parent
9d581bae56
commit
a80e405b3e
9
.clang-format
Normal file
9
.clang-format
Normal file
@ -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
|
||||
151
CMakeLists.txt
Normal file
151
CMakeLists.txt
Normal file
@ -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")
|
||||
62
CMakePresets.json
Normal file
62
CMakePresets.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
59
shell.html
Normal file
59
shell.html
Normal file
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PortalGame</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
background: black;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
|
||||
|
||||
<script>
|
||||
var Module = {
|
||||
preRun: [],
|
||||
postRun: [],
|
||||
canvas: (function() {
|
||||
return document.getElementById('canvas');
|
||||
})()
|
||||
};
|
||||
|
||||
// Resize canvas to fit the window and inform SDL (via Emscripten)
|
||||
function resizeCanvas() {
|
||||
const canvas = document.getElementById('canvas');
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
// // Notify Emscripten SDL2
|
||||
// if (typeof Module !== 'undefined' && Module.canvas) {
|
||||
// const evt = new CustomEvent('resize');
|
||||
// window.dispatchEvent(evt);
|
||||
// }
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
window.addEventListener('load', resizeCanvas);
|
||||
</script>
|
||||
|
||||
{{{ SCRIPT }}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
142
src/assets/animation.cpp
Normal file
142
src/assets/animation.cpp
Normal file
@ -0,0 +1,142 @@
|
||||
#include "animation.hpp"
|
||||
#include "skeleton.hpp"
|
||||
|
||||
#include "utils/files.hpp"
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const std::string& filename, const Skeleton* skeleton)
|
||||
{
|
||||
std::istringstream ifs = fs::ReadFileAsStream(filename);
|
||||
|
||||
std::shared_ptr<Animation> anim = std::make_shared<Animation>();
|
||||
|
||||
int last_frame = 0;
|
||||
std::vector<size_t> 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;
|
||||
}
|
||||
43
src/assets/animation.hpp
Normal file
43
src/assets/animation.hpp
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#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<AnimationChannel> channels_;
|
||||
std::vector<const Transform*> frame_refs_;
|
||||
std::vector<Transform> frames_;
|
||||
|
||||
public:
|
||||
Animation() = default;
|
||||
|
||||
size_t GetNumFrames() const { return num_frames_; }
|
||||
float GetTPS() const { return tps_; }
|
||||
float GetDuration() const { return static_cast<float>(num_frames_) / tps_; }
|
||||
|
||||
size_t GetNumChannels() const { return channels_.size(); }
|
||||
const AnimationChannel& GetChannel(int index) const { return channels_[index]; }
|
||||
|
||||
static std::shared_ptr<const Animation> LoadFromFile(const std::string& filename, const Skeleton* skeleton);
|
||||
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
5
src/assets/cache.cpp
Normal file
5
src/assets/cache.cpp
Normal file
@ -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_;
|
||||
79
src/assets/cache.hpp
Normal file
79
src/assets/cache.hpp
Normal file
@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "model.hpp"
|
||||
#include "skeleton.hpp"
|
||||
#include "gfx/texture.hpp"
|
||||
|
||||
namespace assets
|
||||
{
|
||||
|
||||
template <typename T>
|
||||
class Cache
|
||||
{
|
||||
public:
|
||||
using PtrType = std::shared_ptr<const T>;
|
||||
|
||||
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<std::string, std::weak_ptr<const T>> cache_;
|
||||
};
|
||||
|
||||
class TextureCache final : public Cache<gfx::Texture>
|
||||
{
|
||||
protected:
|
||||
PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); }
|
||||
};
|
||||
|
||||
class SkeletonCache final : public Cache<Skeleton>
|
||||
{
|
||||
protected:
|
||||
PtrType Load(const std::string& key) override { return Skeleton::LoadFromFile(key); }
|
||||
};
|
||||
|
||||
class ModelCache final : public Cache<Model>
|
||||
{
|
||||
protected:
|
||||
PtrType Load(const std::string& key) override { return Model::LoadFromFile(key); }
|
||||
};
|
||||
|
||||
class CacheManager
|
||||
{
|
||||
public:
|
||||
static std::shared_ptr<const gfx::Texture> GetTexture(const std::string& filename) {
|
||||
return texture_cache_.Get(filename);
|
||||
}
|
||||
|
||||
static std::shared_ptr<const Skeleton> GetSkeleton(const std::string& filename) {
|
||||
return skeleton_cache_.Get(filename);
|
||||
}
|
||||
|
||||
static std::shared_ptr<const Model> 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
|
||||
25
src/assets/cmdfile.cpp
Normal file
25
src/assets/cmdfile.cpp
Normal file
@ -0,0 +1,25 @@
|
||||
#include "cmdfile.hpp"
|
||||
|
||||
void assets::LoadCMDFile(const std::string& filename,
|
||||
const std::function<void(const std::string& command, std::istringstream& iss)>& 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);
|
||||
}
|
||||
|
||||
}
|
||||
15
src/assets/cmdfile.hpp
Normal file
15
src/assets/cmdfile.hpp
Normal file
@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils/files.hpp"
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace assets
|
||||
{
|
||||
|
||||
void LoadCMDFile(const std::string& filename,
|
||||
const std::function<void(const std::string& command, std::istringstream& iss)>& handler);
|
||||
|
||||
} // namespace assets
|
||||
49
src/assets/map.cpp
Normal file
49
src/assets/map.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
#include "map.hpp"
|
||||
#include "cache.hpp"
|
||||
#include "utils/files.hpp"
|
||||
#include "cmdfile.hpp"
|
||||
|
||||
std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string& filename)
|
||||
{
|
||||
auto map = std::make_shared<Map>();
|
||||
|
||||
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;
|
||||
}
|
||||
30
src/assets/map.hpp
Normal file
30
src/assets/map.hpp
Normal file
@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "model.hpp"
|
||||
#include "utils/transform.hpp"
|
||||
|
||||
namespace assets
|
||||
{
|
||||
|
||||
struct MapStaticObject
|
||||
{
|
||||
Transform transform;
|
||||
std::shared_ptr<const Model> model;
|
||||
glm::vec3 color = glm::vec3(1.0f);
|
||||
};
|
||||
|
||||
class Map
|
||||
{
|
||||
public:
|
||||
Map() = default;
|
||||
static std::shared_ptr<const Map> LoadFromFile(const std::string& filename);
|
||||
|
||||
private:
|
||||
std::shared_ptr<const Model> basemodel_;
|
||||
std::vector<MapStaticObject> static_objects_;
|
||||
};
|
||||
|
||||
} // namespace assets
|
||||
116
src/assets/mesh_builder.cpp
Normal file
116
src/assets/mesh_builder.cpp
Normal file
@ -0,0 +1,116 @@
|
||||
#include "mesh_builder.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
assets::MeshBuilder::MeshBuilder(gfx::MeshFlags mflags) : mflags_(mflags) {}
|
||||
|
||||
void assets::MeshBuilder::BeginSurface(gfx::SurfaceFlags sflags, const std::string& name, std::shared_ptr<const gfx::Texture> texture)
|
||||
{
|
||||
if (!mesh_)
|
||||
mesh_ = std::make_shared<Mesh>();
|
||||
|
||||
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 <class T>
|
||||
static void BufferPut(std::vector<char>& buffer, const T& val)
|
||||
{
|
||||
const char* data = reinterpret_cast<const char*>(&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<char> 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<int32_t>(vert.bones[i].bone_index));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
BufferPut(buffer, vert.bones[i].weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate VA
|
||||
std::shared_ptr<gfx::VertexArray> va =
|
||||
std::make_shared<gfx::VertexArray>(GetVertexAttrFlags(mflags_), gfx::VF_CREATE_EBO);
|
||||
va->SetVBOData(buffer.data(), buffer.size());
|
||||
va->SetIndices(reinterpret_cast<const GLuint*>(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;
|
||||
}
|
||||
}
|
||||
64
src/assets/mesh_builder.hpp
Normal file
64
src/assets/mesh_builder.hpp
Normal file
@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "gfx/surface.hpp"
|
||||
#include <cstdint>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
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<gfx::Surface> surfaces;
|
||||
std::map<std::string, size_t> surface_names;
|
||||
};
|
||||
|
||||
class MeshBuilder
|
||||
{
|
||||
public:
|
||||
MeshBuilder(gfx::MeshFlags mflags);
|
||||
|
||||
void BeginSurface(gfx::SurfaceFlags sflags, const std::string& name, std::shared_ptr<const gfx::Texture> texture);
|
||||
void AddVertex(const MeshVertex& v);
|
||||
void AddTriangle(const MeshTriangle& t);
|
||||
|
||||
void Build();
|
||||
|
||||
void SetMeshFlag(gfx::MeshFlags flag) { mflags_ |= flag; }
|
||||
|
||||
std::shared_ptr<const Mesh> GetMesh() const { return mesh_; }
|
||||
|
||||
private:
|
||||
void FinalizeSurface();
|
||||
|
||||
private:
|
||||
gfx::MeshFlags mflags_ = 0;
|
||||
std::vector<MeshVertex> verts_;
|
||||
std::vector<MeshTriangle> tris_;
|
||||
std::shared_ptr<Mesh> mesh_;
|
||||
};
|
||||
|
||||
} // namespace assets
|
||||
71
src/assets/model.cpp
Normal file
71
src/assets/model.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
#include "model.hpp"
|
||||
|
||||
#include "cmdfile.hpp"
|
||||
#include "cache.hpp"
|
||||
|
||||
std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::string& filename)
|
||||
{
|
||||
auto model = std::make_shared<Model>();
|
||||
|
||||
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<const gfx::Texture> 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;
|
||||
}
|
||||
21
src/assets/model.hpp
Normal file
21
src/assets/model.hpp
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "mesh_builder.hpp"
|
||||
|
||||
namespace assets
|
||||
{
|
||||
|
||||
class Model
|
||||
{
|
||||
public:
|
||||
Model() = default;
|
||||
static std::shared_ptr<const Model> LoadFromFile(const std::string& filename);
|
||||
|
||||
const std::shared_ptr<const Mesh>& GetMesh() const { return mesh_; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<const Mesh> mesh_;
|
||||
};
|
||||
|
||||
}
|
||||
95
src/assets/skeleton.cpp
Normal file
95
src/assets/skeleton.cpp
Normal file
@ -0,0 +1,95 @@
|
||||
#include "skeleton.hpp"
|
||||
|
||||
#include "utils/files.hpp"
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform)
|
||||
{
|
||||
int index = static_cast<int>(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<const assets::Skeleton> assets::Skeleton::LoadFromFile(const std::string& filename)
|
||||
{
|
||||
std::istringstream ifs = fs::ReadFileAsStream(filename);
|
||||
|
||||
std::shared_ptr<Skeleton> skeleton = std::make_shared<Skeleton>();
|
||||
|
||||
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<const Animation> anim = Animation::LoadFromFile("data/" + anim_filename + ".anim", skeleton.get());
|
||||
skeleton->AddAnimation(anim_name, anim);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return skeleton;
|
||||
}
|
||||
47
src/assets/skeleton.hpp
Normal file
47
src/assets/skeleton.hpp
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#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<Bone> bones_;
|
||||
std::map<std::string, int> bone_map_;
|
||||
|
||||
std::map<std::string, std::shared_ptr<const Animation>> 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<const Skeleton> LoadFromFile(const std::string& filename);
|
||||
|
||||
private:
|
||||
void AddAnimation(const std::string& name, const std::shared_ptr<const Animation>& anim) { anims_[name] = anim; }
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
50
src/client/app.cpp
Normal file
50
src/client/app.cpp
Normal file
@ -0,0 +1,50 @@
|
||||
#include "app.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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<float>(viewport_size_.x) / static_cast<float>(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()
|
||||
{
|
||||
|
||||
}
|
||||
29
src/client/app.hpp
Normal file
29
src/client/app.hpp
Normal file
@ -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();
|
||||
};
|
||||
|
||||
8
src/client/gl.hpp
Normal file
8
src/client/gl.hpp
Normal file
@ -0,0 +1,8 @@
|
||||
#if defined(EMSCRIPTEN) || defined(ANDROID)
|
||||
#define PG_GLES
|
||||
#include <GLES3/gl3.h>
|
||||
// #include <GLES3/gl2ext.h>
|
||||
#else
|
||||
#include <glad/glad.h>
|
||||
#endif
|
||||
|
||||
243
src/client/main.cpp
Normal file
243
src/client/main.cpp
Normal file
@ -0,0 +1,243 @@
|
||||
#include <SDL.h>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include "app.hpp"
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5_webgl.h>
|
||||
#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<App> 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<float>(xrel), static_cast<float>(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<App>();
|
||||
|
||||
#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;
|
||||
}
|
||||
13
src/client/utils.hpp
Normal file
13
src/client/utils.hpp
Normal file
@ -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;
|
||||
};
|
||||
54
src/collision/aabb.hpp
Normal file
54
src/collision/aabb.hpp
Normal file
@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <limits>
|
||||
|
||||
namespace collision
|
||||
{
|
||||
template <size_t dim>
|
||||
struct AABB
|
||||
{
|
||||
static constexpr float C_INFINITY = std::numeric_limits<float>::infinity();
|
||||
|
||||
using Vec = glm::vec<dim, float, glm::defaultp>;
|
||||
|
||||
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<dim>& other) const;
|
||||
|
||||
AABB<dim> Intersection(const AABB<dim>& other) const
|
||||
{
|
||||
Vec new_min = glm::max(min, other.min);
|
||||
Vec new_max = glm::min(max, other.max);
|
||||
return AABB<dim>(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>;
|
||||
}
|
||||
20
src/collision/trianglemesh.cpp
Normal file
20
src/collision/trianglemesh.cpp
Normal file
@ -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<btBvhTriangleMeshShape>(&bt_mesh_, true, true);
|
||||
|
||||
}
|
||||
27
src/collision/trianglemesh.hpp
Normal file
27
src/collision/trianglemesh.hpp
Normal file
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <span>
|
||||
#include <glm/glm.hpp>
|
||||
#include "aabb.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <btBulletCollisionCommon.h>
|
||||
|
||||
namespace collision
|
||||
{
|
||||
class TriangleMesh
|
||||
{
|
||||
btTriangleMesh bt_mesh_;
|
||||
std::unique_ptr<btBvhTriangleMeshShape> 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(); }
|
||||
};
|
||||
}
|
||||
23
src/game/player_input.hpp
Normal file
23
src/game/player_input.hpp
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
29
src/game/transform_node.hpp
Normal file
29
src/game/transform_node.hpp
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
12
src/gameview/client_session.hpp
Normal file
12
src/gameview/client_session.hpp
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
namespace game::view
|
||||
{
|
||||
|
||||
class ClientSession
|
||||
{
|
||||
public:
|
||||
private:
|
||||
};
|
||||
|
||||
} // namespace game::view
|
||||
26
src/gameview/entityview.hpp
Normal file
26
src/gameview/entityview.hpp
Normal file
@ -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
|
||||
14
src/gameview/worldview.hpp
Normal file
14
src/gameview/worldview.hpp
Normal file
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
namespace game::view
|
||||
{
|
||||
|
||||
class WorldView
|
||||
{
|
||||
public:
|
||||
|
||||
private:
|
||||
|
||||
};
|
||||
|
||||
} // namespace game::view
|
||||
29
src/gfx/buffer_object.cpp
Normal file
29
src/gfx/buffer_object.cpp
Normal file
@ -0,0 +1,29 @@
|
||||
#include <stdexcept>
|
||||
#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;
|
||||
}
|
||||
30
src/gfx/buffer_object.hpp
Normal file
30
src/gfx/buffer_object.hpp
Normal file
@ -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; }
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
31
src/gfx/draw_list.hpp
Normal file
31
src/gfx/draw_list.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#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<DrawSurfaceCmd> surfaces;
|
||||
|
||||
void AddSurface(const DrawSurfaceCmd& cmd) { surfaces.emplace_back(cmd); }
|
||||
|
||||
void Clear() { surfaces.clear(); }
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
178
src/gfx/renderer.cpp
Normal file
178
src/gfx/renderer.cpp
Normal file
@ -0,0 +1,178 @@
|
||||
#include "renderer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#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<DrawSurfaceCmd> 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<GLsizei>(num_tris * 3U), GL_UNSIGNED_INT,
|
||||
(void*)(first_tri * 3U * sizeof(GLuint)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
52
src/gfx/renderer.hpp
Normal file
52
src/gfx/renderer.hpp
Normal file
@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include "shader.hpp"
|
||||
#include "draw_list.hpp"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
struct DrawListParams
|
||||
{
|
||||
glm::mat4 view_proj;
|
||||
};
|
||||
|
||||
struct MeshShader
|
||||
{
|
||||
std::unique_ptr<Shader> 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<Shader> solid_shader_;
|
||||
|
||||
const Shader* current_shader_ = nullptr;
|
||||
|
||||
void InvalidateShaders();
|
||||
void InvalidateMeshShader(MeshShader& mshader);
|
||||
void SetupMeshShader(MeshShader& mshader, const DrawListParams& params);
|
||||
|
||||
void DrawSurfaceList(std::span<DrawSurfaceCmd> queue, const DrawListParams& params);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
103
src/gfx/shader.cpp
Normal file
103
src/gfx/shader.cpp
Normal file
@ -0,0 +1,103 @@
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#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);
|
||||
}
|
||||
52
src/gfx/shader.hpp
Normal file
52
src/gfx/shader.hpp
Normal file
@ -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<EFBFBD>t<EFBFBD> lokaci uniformy
|
||||
*
|
||||
* \param u Uniforma
|
||||
* \return Lokace
|
||||
*/
|
||||
GLint U(ShaderUniform u) const { return m_uni[u]; }
|
||||
|
||||
/**
|
||||
* \brief Vr<EFBFBD>t<EFBFBD> OpenGL n<EFBFBD>zev shaderu
|
||||
*
|
||||
* \return N<EFBFBD>zev shaderu
|
||||
*/
|
||||
GLuint GetId() const { return m_id; }
|
||||
|
||||
private:
|
||||
void SetupBindings();
|
||||
|
||||
};
|
||||
}
|
||||
4
src/gfx/shader_defs.hpp
Normal file
4
src/gfx/shader_defs.hpp
Normal file
@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define SD_MAX_LIGHTS 4
|
||||
#define SD_MAX_BONES 256
|
||||
206
src/gfx/shader_sources.cpp
Normal file
206
src/gfx/shader_sources.cpp
Normal file
@ -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];
|
||||
}
|
||||
36
src/gfx/shader_sources.hpp
Normal file
36
src/gfx/shader_sources.hpp
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#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>& shader, ShaderSource ss_vert, ShaderSource ss_frag) {
|
||||
shader = std::make_unique<Shader>(
|
||||
Get(ss_vert),
|
||||
Get(ss_frag)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
38
src/gfx/surface.hpp
Normal file
38
src/gfx/surface.hpp
Normal file
@ -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<const gfx::Texture> texture;
|
||||
std::shared_ptr<const gfx::VertexArray> 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
|
||||
87
src/gfx/texture.cpp
Normal file
87
src/gfx/texture.cpp
Normal file
@ -0,0 +1,87 @@
|
||||
#include "texture.hpp"
|
||||
#include <stdexcept>
|
||||
|
||||
#include "utils/files.hpp"
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include <stb_image.h>
|
||||
|
||||
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> 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<const unsigned char*>(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> texture;
|
||||
try {
|
||||
texture = std::make_shared<Texture>(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;
|
||||
}
|
||||
26
src/gfx/texture.hpp
Normal file
26
src/gfx/texture.hpp
Normal file
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#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<Texture> LoadFromFile(const std::string& filename);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
21
src/gfx/uniform_buffer.hpp
Normal file
21
src/gfx/uniform_buffer.hpp
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "buffer_object.hpp"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
template<class T>
|
||||
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);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
98
src/gfx/vertex_array.cpp
Normal file
98
src/gfx/vertex_array.cpp
Normal file
@ -0,0 +1,98 @@
|
||||
#include "vertex_array.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// 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<BufferObject>(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<BufferObject>(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);
|
||||
}
|
||||
62
src/gfx/vertex_array.hpp
Normal file
62
src/gfx/vertex_array.hpp
Normal file
@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
|
||||
#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<BufferObject> m_vbo;
|
||||
std::unique_ptr<BufferObject> 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; }
|
||||
};
|
||||
|
||||
}
|
||||
53
src/net/defs.hpp
Normal file
53
src/net/defs.hpp
Normal file
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "fixed_str.hpp"
|
||||
#include "quantized.hpp"
|
||||
|
||||
namespace net
|
||||
{
|
||||
|
||||
enum MessageType : uint8_t
|
||||
{
|
||||
MSG_NONE,
|
||||
|
||||
/*~~~~~~~~ Client->Server ~~~~~~~~*/
|
||||
// IN <PlayerInputFlags> <ViewYaw> <ViewPitch>
|
||||
MSG_IN,
|
||||
|
||||
/*~~~~~~~~ World ~~~~~~~~*/
|
||||
// CHWORLD <MapName>
|
||||
MSG_CHWORLD,
|
||||
|
||||
/*~~~~~~~~ Entity ~~~~~~~~*/
|
||||
// ENTSPAWN <EntNum> <EntType> data...
|
||||
MSG_ENTSPAWN,
|
||||
// ENTMSG <EntNum> data...
|
||||
MSG_ENTMSG,
|
||||
// ENTRM <EntNum>
|
||||
MSG_ENTRM,
|
||||
|
||||
/*~~~~~~~~~~~~~~~~*/
|
||||
MSG_COUNT,
|
||||
};
|
||||
|
||||
using MapName = FixedStr<32>;
|
||||
|
||||
using ViewYaw = Quantized<uint16_t, 0, 360>;
|
||||
using ViewPitch = Quantized<uint16_t, -180, 180>;
|
||||
|
||||
using EntNum = uint16_t;
|
||||
|
||||
enum EntType : uint8_t
|
||||
{
|
||||
ET_NONE,
|
||||
|
||||
ET_PAWN,
|
||||
ET_CAR,
|
||||
|
||||
ET_COUNT,
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
34
src/net/fixed_str.hpp
Normal file
34
src/net/fixed_str.hpp
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace net
|
||||
{
|
||||
|
||||
template <size_t N>
|
||||
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 <size_t N>
|
||||
using FixedStrLen = std::conditional_t<(N > 255), uint16_t, uint8_t>;
|
||||
|
||||
|
||||
} // namespace net
|
||||
121
src/net/inmessage.hpp
Normal file
121
src/net/inmessage.hpp
Normal file
@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <concepts>
|
||||
#include <stdexcept>
|
||||
|
||||
#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 <std::integral T>
|
||||
bool Read(T& value)
|
||||
{
|
||||
if (!CheckAvail(sizeof(T)))
|
||||
return false;
|
||||
|
||||
value = *(reinterpret_cast<const T*>(ptr_));
|
||||
ptr_ += sizeof(T);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
bool Read(FixedStr<N>& str)
|
||||
{
|
||||
FixedStrLen<N> len;
|
||||
|
||||
if (!Read(len) || len > N || !Read(str.str, len))
|
||||
return false;
|
||||
|
||||
str.len = len;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T, long long MinL, long long MaxL>
|
||||
bool Read(Quantized<T, MinL, MaxL>& 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<uint64_t>(p & 0b01111111) << shift;
|
||||
shift += 7;
|
||||
|
||||
|
||||
}
|
||||
|
||||
value = static_cast<int64_t>(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
|
||||
83
src/net/outmessage.hpp
Normal file
83
src/net/outmessage.hpp
Normal file
@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
#include <vector>
|
||||
|
||||
#include "fixed_str.hpp"
|
||||
#include "quantized.hpp"
|
||||
|
||||
namespace net
|
||||
{
|
||||
|
||||
class OutMessage
|
||||
{
|
||||
public:
|
||||
OutMessage(std::vector<char>& buffer) : buffer_(buffer) { buffer.clear(); }
|
||||
|
||||
template <std::integral T>
|
||||
size_t Reserve()
|
||||
{
|
||||
size_t pos = buffer_.size();
|
||||
buffer_.resize(pos + sizeof(T));
|
||||
return pos;
|
||||
}
|
||||
|
||||
template <std::integral T>
|
||||
void WriteAt(size_t pos, T value)
|
||||
{
|
||||
*(reinterpret_cast<T*>(&buffer_[pos])) = value;
|
||||
}
|
||||
|
||||
template <std::integral T>
|
||||
void Write(T value)
|
||||
{
|
||||
WriteAt(Reserve<T>(), value);
|
||||
}
|
||||
|
||||
void Write(const char* str, size_t n)
|
||||
{
|
||||
size_t pos = buffer_.size();
|
||||
buffer_.resize(pos + n);
|
||||
memcpy(&buffer_[pos], str, n);
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
void Write(const FixedStr<N>& str)
|
||||
{
|
||||
Write(static_cast<FixedStrLen<N>>(str.len));
|
||||
Write(str.str, str.len);
|
||||
}
|
||||
|
||||
template <typename T, long long MinL, long long MaxL>
|
||||
void Write(Quantized<T, MinL, MaxL> quant)
|
||||
{
|
||||
Write(quant.value);
|
||||
}
|
||||
|
||||
void WriteVarInt(int64_t value)
|
||||
{
|
||||
const bool negative = value < 0;
|
||||
uint64_t uvalue = static_cast<uint64_t>(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<char>& buffer_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
40
src/net/quantized.hpp
Normal file
40
src/net/quantized.hpp
Normal file
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <concepts>
|
||||
|
||||
namespace net
|
||||
{
|
||||
|
||||
template <std::unsigned_integral T, long long MinL = -1, long long MaxL = 1, long long DivisorL = 1>
|
||||
struct Quantized
|
||||
{
|
||||
static constexpr float Min = static_cast<float>(MinL) / static_cast<float>(DivisorL);
|
||||
static constexpr float Max = static_cast<float>(MaxL) / static_cast<float>(DivisorL);
|
||||
|
||||
static constexpr T max_int = std::numeric_limits<T>::max();
|
||||
static constexpr float range = Max - Min;
|
||||
static constexpr float scale = static_cast<float>(max_int) / range;
|
||||
static constexpr float inv_scale = range / static_cast<float>(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<T>(normalized + 0.5f); // round
|
||||
}
|
||||
|
||||
float Decode() const noexcept { return Min + static_cast<float>(value) * inv_scale; }
|
||||
|
||||
static constexpr float MaxError() noexcept { return inv_scale * 0.5f; }
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
33
src/utils/files.cpp
Normal file
33
src/utils/files.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
#include "files.hpp"
|
||||
|
||||
#include <SDL.h>
|
||||
#include <SDL_rwops.h>
|
||||
|
||||
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<size_t>(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);
|
||||
}
|
||||
9
src/utils/files.hpp
Normal file
9
src/utils/files.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
|
||||
namespace fs
|
||||
{
|
||||
std::string ReadFileAsString(const std::string& path);
|
||||
std::istringstream ReadFileAsStream(const std::string& path);
|
||||
}
|
||||
58
src/utils/transform.hpp
Normal file
58
src/utils/transform.hpp
Normal file
@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user