CCT, skeletons and anims init

This commit is contained in:
tovjemam 2026-02-09 17:22:49 +01:00
parent f8ef0c55d0
commit 6396f94783
29 changed files with 1744 additions and 299 deletions

View File

@ -26,6 +26,8 @@ set(COMMON_SOURCES
"src/collision/trianglemesh.hpp" "src/collision/trianglemesh.hpp"
"src/collision/trianglemesh.cpp" "src/collision/trianglemesh.cpp"
"src/game/player_input.hpp" "src/game/player_input.hpp"
"src/game/skeletoninstance.hpp"
"src/game/skeletoninstance.cpp"
"src/game/transform_node.hpp" "src/game/transform_node.hpp"
"src/game/vehicle_sync.hpp" "src/game/vehicle_sync.hpp"
"src/net/defs.hpp" "src/net/defs.hpp"
@ -67,6 +69,8 @@ set(CLIENT_ONLY_SOURCES
"src/client/gl.hpp" "src/client/gl.hpp"
"src/client/main.cpp" "src/client/main.cpp"
"src/client/utils.hpp" "src/client/utils.hpp"
"src/gameview/characterview.hpp"
"src/gameview/characterview.cpp"
"src/gameview/client_session.hpp" "src/gameview/client_session.hpp"
"src/gameview/client_session.cpp" "src/gameview/client_session.cpp"
"src/gameview/entityview.hpp" "src/gameview/entityview.hpp"
@ -102,6 +106,8 @@ set(CLIENT_ONLY_SOURCES
) )
set(SERVER_ONLY_SOURCES set(SERVER_ONLY_SOURCES
"src/game/character.hpp"
"src/game/character.cpp"
"src/game/entity.hpp" "src/game/entity.hpp"
"src/game/entity.cpp" "src/game/entity.cpp"
"src/game/game.hpp" "src/game/game.hpp"

View File

@ -1,21 +1,19 @@
#include "animation.hpp" #include "animation.hpp"
#include "skeleton.hpp" #include "skeleton.hpp"
#include "utils/files.hpp" #include "cmdfile.hpp"
#include <sstream>
#include <stdexcept> #include <stdexcept>
std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const std::string& filename, const Skeleton* skeleton) 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>(); std::shared_ptr<Animation> anim = std::make_shared<Animation>();
int last_frame = 0; int last_frame = 0;
std::vector<size_t> frame_indices; std::vector<size_t> frame_indices;
auto FillFrameRefs = [&](int end_frame) auto FillFrameRefs = [&](int end_frame) {
{
int channel_start = ((int)anim->channels_.size() - 1) * (int)anim->num_frames_; int channel_start = ((int)anim->channels_.size() - 1) * (int)anim->num_frames_;
int target_size = channel_start + end_frame; int target_size = channel_start + end_frame;
size_t num_frame_refs = frame_indices.size(); size_t num_frame_refs = frame_indices.size();
@ -34,18 +32,7 @@ std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const s
frame_indices.resize(target_size, last_frame_idx); frame_indices.resize(target_size, last_frame_idx);
}; };
std::string line; LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
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 (command == "f")
{ {
if (anim->num_frames_ == 0) if (anim->num_frames_ == 0)
@ -74,11 +61,7 @@ std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const s
last_frame = frame_index; last_frame = frame_index;
Transform t; Transform t;
iss >> t.position.x >> t.position.y >> t.position.z; ParseTransform(iss, t);
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(); size_t idx = anim->frames_.size();
anim->frames_.push_back(t); anim->frames_.push_back(t);
@ -105,7 +88,6 @@ std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const s
channel.frames = nullptr; // Will be set up later channel.frames = nullptr; // Will be set up later
last_frame = 0; last_frame = 0;
} }
else if (command == "frames") else if (command == "frames")
{ {
@ -115,7 +97,7 @@ std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const s
{ {
iss >> anim->tps_; iss >> anim->tps_;
} }
} });
if (anim->channels_.empty()) if (anim->channels_.empty())
{ {

View File

@ -1,32 +1,26 @@
#pragma once #pragma once
#include <vector>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "utils/transform.hpp" #include "utils/transform.hpp"
namespace assets namespace assets
{ {
class Skeleton; class Skeleton;
struct AnimationChannel struct AnimationChannel
{ {
int bone_index; int bone_index;
const Transform* const* frames; const Transform* const* frames;
}; };
class Animation class Animation
{ {
size_t num_frames_ = 0; public:
float tps_ = 24.0f;
std::vector<AnimationChannel> channels_;
std::vector<const Transform*> frame_refs_;
std::vector<Transform> frames_;
public:
Animation() = default; Animation() = default;
static std::shared_ptr<const Animation> LoadFromFile(const std::string& filename, const Skeleton* skeleton);
size_t GetNumFrames() const { return num_frames_; } size_t GetNumFrames() const { return num_frames_; }
float GetTPS() const { return tps_; } float GetTPS() const { return tps_; }
@ -35,9 +29,13 @@ namespace assets
size_t GetNumChannels() const { return channels_.size(); } size_t GetNumChannels() const { return channels_.size(); }
const AnimationChannel& GetChannel(int index) const { return channels_[index]; } const AnimationChannel& GetChannel(int index) const { return channels_[index]; }
static std::shared_ptr<const Animation> LoadFromFile(const std::string& filename, const Skeleton* skeleton); private:
size_t num_frames_ = 0;
float tps_ = 24.0f;
}; std::vector<AnimationChannel> channels_;
std::vector<const Transform*> frame_refs_;
std::vector<Transform> frames_;
};
} // namespace assets
}

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "utils/files.hpp" #include "utils/files.hpp"
#include "utils/transform.hpp"
#include <functional> #include <functional>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
@ -12,4 +13,12 @@ namespace assets
void LoadCMDFile(const std::string& filename, void LoadCMDFile(const std::string& filename,
const std::function<void(const std::string& command, std::istringstream& iss)>& handler); const std::function<void(const std::string& command, std::istringstream& iss)>& handler);
inline void ParseTransform(std::istringstream& iss, Transform& trans)
{
iss >> trans.position.x >> trans.position.y >> trans.position.z;
iss >> trans.rotation.x >> trans.rotation.y >> trans.rotation.z >> trans.rotation.w;
iss >> trans.scale;
}
} // namespace assets } // namespace assets

View File

@ -53,17 +53,13 @@ std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string&
glm::vec3 angles; glm::vec3 angles;
auto trans = &obj.node.local; auto& trans = obj.node.local;
ParseTransform(iss, trans);
iss >> trans->position.x >> trans->position.y >> trans->position.z;
iss >> angles.x >> angles.y >> angles.z;
trans->SetAngles(angles);
iss >> trans->scale;
obj.node.UpdateMatrix(); obj.node.UpdateMatrix();
obj.aabb.min = trans->position - glm::vec3(1.0f); obj.aabb.min = trans.position - glm::vec3(1.0f);
obj.aabb.max = trans->position + glm::vec3(1.0f); obj.aabb.max = trans.position + glm::vec3(1.0f);
std::string flag; std::string flag;
while (iss >> flag) while (iss >> flag)

View File

@ -1,77 +1,27 @@
#include "skeleton.hpp" #include "skeleton.hpp"
#include "utils/files.hpp" #include "cmdfile.hpp"
#include <sstream>
#include <stdexcept> #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::shared_ptr<const assets::Skeleton> assets::Skeleton::LoadFromFile(const std::string& filename)
{ {
std::istringstream ifs = fs::ReadFileAsStream(filename); auto skeleton = std::make_shared<Skeleton>();
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;
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
if (command == "b") if (command == "b")
{ {
Transform t; Transform t;
glm::vec3 angles;
std::string bone_name, parent_name; std::string bone_name, parent_name;
iss >> bone_name >> parent_name; iss >> bone_name >> parent_name;
iss >> t.position.x >> t.position.y >> t.position.z; ParseTransform(iss, t);
iss >> angles.x >> angles.y >> angles.z;
iss >> t.scale;
if (iss.fail()) if (iss.fail())
{ {
throw std::runtime_error("Failed to parse bone definition in file: " + filename); throw std::runtime_error("Failed to parse bone definition in file: " + filename);
} }
t.SetAngles(angles);
skeleton->AddBone(bone_name, parent_name, t); skeleton->AddBone(bone_name, parent_name, t);
} }
else if (command == "anim") else if (command == "anim")
@ -84,12 +34,45 @@ std::shared_ptr<const assets::Skeleton> assets::Skeleton::LoadFromFile(const std
throw std::runtime_error("Failed to parse animation definition in file: " + filename); 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()); std::shared_ptr<const Animation> anim =
Animation::LoadFromFile("data/" + anim_filename + ".anim", skeleton.get());
skeleton->AddAnimation(anim_name, anim); skeleton->AddAnimation(anim_name, anim);
} }
});
}
return skeleton; return skeleton;
} }
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;
}
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;
}

View File

@ -1,34 +1,29 @@
#pragma once #pragma once
#include <string>
#include <vector>
#include <map> #include <map>
#include <memory> #include <memory>
#include <string>
#include <vector>
#include "utils/transform.hpp"
#include "animation.hpp" #include "animation.hpp"
#include "utils/transform.hpp"
namespace assets namespace assets
{ {
struct Bone
{ struct Bone
{
int parent_idx; int parent_idx;
std::string name; std::string name;
Transform bind_transform; Transform bind_transform;
glm::mat4 inv_bind_matrix; glm::mat4 inv_bind_matrix;
}; };
class Skeleton class Skeleton
{ {
std::vector<Bone> bones_; public:
std::map<std::string, int> bone_map_;
std::map<std::string, std::shared_ptr<const Animation>> anims_;
public:
Skeleton() = default; Skeleton() = default;
static std::shared_ptr<const Skeleton> LoadFromFile(const std::string& filename);
void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform);
int GetBoneIndex(const std::string& name) const; int GetBoneIndex(const std::string& name) const;
@ -37,11 +32,15 @@ namespace assets
const Animation* GetAnimation(const std::string& name) const; const Animation* GetAnimation(const std::string& name) const;
static std::shared_ptr<const Skeleton> LoadFromFile(const std::string& filename); private:
void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform);
private:
void AddAnimation(const std::string& name, const std::shared_ptr<const Animation>& anim) { anims_[name] = anim; } void AddAnimation(const std::string& name, const std::shared_ptr<const Animation>& anim) { anims_[name] = anim; }
};
private:
std::vector<Bone> bones_;
std::map<std::string, int> bone_map_;
} std::map<std::string, std::shared_ptr<const Animation>> anims_;
};
} // namespace assets

View File

@ -94,6 +94,25 @@ void App::Frame()
glm::mat4 camera_world = glm::inverse(view); glm::mat4 camera_world = glm::inverse(view);
audiomaster_.SetListenerOrientation(camera_world); audiomaster_.SetListenerOrientation(camera_world);
if (time_ - last_send_time_ > 0.040f)
{
net::ViewYawQ yaw_q;
net::ViewPitchQ pitch_q;
yaw_q.Encode(session_->GetYaw());
pitch_q.Encode(session_->GetPitch());
if (yaw_q.value != view_yaw_q_.value || pitch_q.value != view_pitch_q_.value)
{
auto msg = BeginMsg(net::MSG_VIEWANGLES);
msg.Write(yaw_q.value);
msg.Write(pitch_q.value);
view_yaw_q_.value = yaw_q.value;
view_pitch_q_.value = pitch_q.value;
last_send_time_ = time_;
}
}
} }
// draw chat // draw chat
@ -101,15 +120,6 @@ void App::Frame()
DrawChat(dlist_); DrawChat(dlist_);
renderer_.DrawList(dlist_, params); renderer_.DrawList(dlist_, params);
// if (time_ - last_send_time_ > 0.040f)
// {
// auto msg = BeginMsg(net::MSG_IN);
// msg.Write(input_);
// last_send_time_ = time_;
// }
} }
void App::Connected() void App::Connected()
@ -153,7 +163,7 @@ void App::MouseMove(const glm::vec2& delta)
{ {
float sensitivity = 0.002f; // Sensitivity factor for mouse movement float sensitivity = 0.002f; // Sensitivity factor for mouse movement
float delta_yaw = delta.x * sensitivity; float delta_yaw = -delta.x * sensitivity;
float delta_pitch = -delta.y * sensitivity; float delta_pitch = -delta.y * sensitivity;
if (session_) if (session_)

View File

@ -59,6 +59,8 @@ private:
glm::ivec2 viewport_size_ = {800, 600}; glm::ivec2 viewport_size_ = {800, 600};
game::PlayerInputFlags input_ = 0; game::PlayerInputFlags input_ = 0;
game::PlayerInputFlags prev_input_ = 0; game::PlayerInputFlags prev_input_ = 0;
net::ViewYawQ view_yaw_q_;
net::ViewPitchQ view_pitch_q_;
float prev_time_ = 0.0f; float prev_time_ = 0.0f;
float delta_time_ = 0.0f; float delta_time_ = 0.0f;

189
src/game/character.cpp Normal file
View File

@ -0,0 +1,189 @@
#include "character.hpp"
#include "world.hpp"
#include "net/utils.hpp"
game::Character::Character(World& world, const CharacterInfo& info)
: Super(world, net::ET_CHARACTER), shape_(info.shape), bt_shape_(shape_.radius, shape_.height),
bt_character_(&bt_ghost_, &bt_shape_, 0.3f, btVector3(0, 0, 1))
{
btTransform start_transform;
start_transform.setIdentity();
bt_ghost_.setWorldTransform(start_transform);
bt_ghost_.setCollisionShape(&bt_shape_);
bt_ghost_.setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT);
btDynamicsWorld& bt_world = world_.GetBtWorld();
static btGhostPairCallback ghostpaircb;
bt_world.getBroadphase()->getOverlappingPairCache()->setInternalGhostPairCallback(&ghostpaircb);
// bt_world.addCollisionObject(&bt_ghost_, btBroadphaseProxy::CharacterFilter,
// btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter);
bt_world.addCollisionObject(&bt_ghost_);
bt_world.addAction(&bt_character_);
}
static bool Turn(float& angle, float target, float step)
{
constexpr float PI = glm::pi<float>();
constexpr float TWO_PI = glm::two_pi<float>();
angle = glm::mod(angle, TWO_PI);
target = glm::mod(target, TWO_PI);
float diff = glm::mod(target - angle + PI, TWO_PI) - PI;
if (glm::abs(diff) <= step)
{
angle = target;
return true;
}
angle = glm::mod(angle + glm::sign(diff) * step, TWO_PI);
return false;
}
void game::Character::Update()
{
Super::Update();
auto bt_trans = bt_ghost_.getWorldTransform();
root_.local.SetBtTransform(bt_trans);
UpdateMovement();
SendUpdateMsg();
}
void game::Character::SendInitData(Player& player, net::OutMessage& msg) const
{
Super::SendInitData(player, msg);
}
void game::Character::SetInput(CharacterInputType type, bool enable)
{
if (enable)
in_ |= (1 << type);
else
in_ &= ~(1 << type);
}
// static bool SweepCapsule(btCollisionWorld& world, const btCapsuleShapeZ& shape, const glm::vec3& start,
// const glm::vec3& end, float& hit_fraction, glm::vec3& hit_normal)
// {
// btVector3 bt_start(start.x, start.y, start.z);
// btVector3 bt_end(end.x, end.y, end.z);
// static const btMatrix3x3 bt_basis(1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
// btTransform start_transform(bt_basis, bt_start);
// btTransform end_transform(bt_basis, bt_end);
// btCollisionWorld::ClosestConvexResultCallback result_callback(bt_start, bt_end);
// world.convexSweepTest(&shape, start_transform, end_transform, result_callback);
// if (result_callback.hasHit())
// {
// hit_fraction = result_callback.m_closestHitFraction;
// hit_normal = glm::vec3(result_callback.m_hitNormalWorld.x(), result_callback.m_hitNormalWorld.y(),
// result_callback.m_hitNormalWorld.z());
// return true;
// }
// return false;
// }
void game::Character::SetPosition(const glm::vec3& position)
{
auto trans = bt_ghost_.getWorldTransform();
trans.setOrigin(btVector3(position.x, position.y, position.z));
bt_ghost_.setWorldTransform(trans);
}
game::Character::~Character()
{
btDynamicsWorld& bt_world = world_.GetBtWorld();
bt_world.removeAction(&bt_character_);
bt_world.removeCollisionObject(&bt_ghost_);
}
void game::Character::UpdateMovement()
{
constexpr float dt = 1.0f / 25.0f;
glm::vec2 movedir(0.0f);
if (in_ & (1 << CIN_FORWARD))
movedir.x += 1.0f;
if (in_ & (1 << CIN_BACKWARD))
movedir.x -= 1.0f;
if (in_ & (1 << CIN_RIGHT))
movedir.y -= 1.0f;
if (in_ & (1 << CIN_LEFT))
movedir.y += 1.0f;
glm::vec3 walkdir(0.0f);
if (movedir.x != 0.0f || movedir.y != 0.0f)
{
float target_yaw = forward_yaw_ + std::atan2(movedir.y, movedir.x);
Turn(yaw_, target_yaw, 4.0f * dt);
glm::vec3 forward_dir(glm::cos(yaw_), glm::sin(yaw_), 0.0f);
walkdir = forward_dir * walk_speed_ * dt;
}
bt_character_.setWalkDirection(btVector3(walkdir.x, walkdir.y, walkdir.z));
if (in_ & (1 << CIN_JUMP) && bt_character_.canJump())
{
bt_character_.jump(btVector3(0.0f, 0.0f, 10.0f));
}
}
void game::Character::SendUpdateMsg()
{
net::PositionQ posq;
net::EncodePosition(root_.local.position, posq);
auto msg = BeginEntMsg(net::EMSG_UPDATE);
net::WritePositionQ(msg, posq);
msg.Write<net::PositiveAngleQ>(yaw_);
}
void game::Character::Move(glm::vec3& velocity, float t)
{
// glm::vec3 u = velocity * t; // Calculate the movement vector
// btCollisionWorld& bt_world = world_.GetBtWorld();
// btCapsuleShapeZ bt_shape(shape_.radius, shape_.height);
// const int MAX_ITERS = 16;
// for (size_t i = 0; i < MAX_ITERS && glm::dot(u, u) > 0.0f; ++i)
// {
// // printf("Entity::Move: Iteration %zu, u = (%f, %f, %f)\n", i, u.x, u.y, u.z);
// glm::vec3 to = position_ + u;
// float hit_fraction = 1.0f;
// glm::vec3 hit_normal;
// bool hit = SweepCapsule(bt_world, bt_shape, position_, to, hit_fraction, hit_normal);
// // Update the position based on the hit fraction
// position_ += hit_fraction * u;
// if (!hit)
// break;
// // hit_normal *= -1.0f; // Invert the normal to point outwards
// // printf("Entity::Move: Hit detected, hit_fraction = %f, hit_normal = (%f, %f, %f)\n", hit_fraction,
// // hit_normal.x, hit_normal.y, hit_normal.z);
// u -= hit_fraction * u; // Reduce the movement vector by the hit fraction
// u -= glm::dot(u, hit_normal) * hit_normal; // Reflect the velocity along the hit normal
// velocity -= glm::dot(velocity, hit_normal) * hit_normal; // Adjust the velocity
// }
}

79
src/game/character.hpp Normal file
View File

@ -0,0 +1,79 @@
#pragma once
#include "entity.hpp"
#include "BulletCollision/CollisionDispatch/btGhostObject.h"
#include "BulletDynamics/Character/btKinematicCharacterController.h"
namespace game
{
using CharacterInputFlags = uint8_t;
enum CharacterInputType
{
CIN_FORWARD,
CIN_BACKWARD,
CIN_LEFT,
CIN_RIGHT,
CIN_JUMP,
};
struct CapsuleShape
{
float radius;
float height;
CapsuleShape(float radius, float height) : radius(radius), height(height) {}
};
struct CharacterInfo
{
CapsuleShape shape = CapsuleShape(0.3f, 0.75f);
};
class Character : public Entity
{
public:
using Super = Entity;
Character(World& world, const CharacterInfo& info);
virtual void Update() override;
virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
void SetInput(CharacterInputType type, bool enable);
void SetInputs(CharacterInputFlags inputs) { in_ = inputs; }
void SetForwardYaw(float yaw) { forward_yaw_ = yaw; }
void SetPosition(const glm::vec3& position);
~Character() override;
private:
void UpdateMovement();
void SendUpdateMsg();
void Move(glm::vec3& velocity, float t);
private:
CapsuleShape shape_;
// glm::vec3 position_ = glm::vec3(0.0f);
// glm::vec3 velocity_ = glm::vec3(0.0f);
CharacterInputFlags in_ = 0;
btCapsuleShapeZ bt_shape_;
btPairCachingGhostObject bt_ghost_;
btKinematicCharacterController bt_character_;
float yaw_ = 0.0f;
float forward_yaw_ = 0.0f;
float walk_speed_ = 2.0f;
};
} // namespace game

View File

@ -94,7 +94,7 @@ game::OpenWorld::OpenWorld() : World("openworld")
// } // }
// spawn bots // spawn bots
for (size_t i = 0; i < 300; ++i) for (size_t i = 0; i < 100; ++i)
{ {
SpawnBot(); SpawnBot();
} }
@ -131,40 +131,79 @@ void game::OpenWorld::Update(int64_t delta_time)
void game::OpenWorld::PlayerJoined(Player& player) void game::OpenWorld::PlayerJoined(Player& player)
{ {
SpawnVehicle(player); // SpawnVehicle(player);
SpawnCharacter(player);
} }
void game::OpenWorld::PlayerInput(Player& player, PlayerInputType type, bool enabled) void game::OpenWorld::PlayerInput(Player& player, PlayerInputType type, bool enabled)
{ {
auto vehicle = player_vehicles_.at(&player); // auto vehicle = player_vehicles_.at(&player);
// player.SendChat("input zmenen: " + std::to_string(static_cast<int>(type)) + "=" + (enabled ? "1" : "0")); // // player.SendChat("input zmenen: " + std::to_string(static_cast<int>(type)) + "=" + (enabled ? "1" : "0"));
// switch (type)
// {
// case IN_FORWARD:
// vehicle->SetInput(VIN_FORWARD, enabled);
// break;
// case IN_BACKWARD:
// vehicle->SetInput(VIN_BACKWARD, enabled);
// break;
// case IN_LEFT:
// vehicle->SetInput(VIN_LEFT, enabled);
// break;
// case IN_RIGHT:
// vehicle->SetInput(VIN_RIGHT, enabled);
// break;
// case IN_DEBUG1:
// if (enabled)
// vehicle->SetPosition({ 100.0f, 100.0f, 5.0f });
// break;
// case IN_DEBUG2:
// if (enabled)
// SpawnVehicle(player);
// break;
// default:
// break;
// }
auto character = player_characters_.at(&player);
switch (type) switch (type)
{ {
case IN_FORWARD: case IN_FORWARD:
vehicle->SetInput(VIN_FORWARD, enabled); character->SetInput(CIN_FORWARD, enabled);
break; break;
case IN_BACKWARD: case IN_BACKWARD:
vehicle->SetInput(VIN_BACKWARD, enabled); character->SetInput(CIN_BACKWARD, enabled);
break; break;
case IN_LEFT: case IN_LEFT:
vehicle->SetInput(VIN_LEFT, enabled); character->SetInput(CIN_LEFT, enabled);
break; break;
case IN_RIGHT: case IN_RIGHT:
vehicle->SetInput(VIN_RIGHT, enabled); character->SetInput(CIN_RIGHT, enabled);
break;
case IN_JUMP:
character->SetInput(CIN_JUMP, enabled);
break; break;
case IN_DEBUG1: case IN_DEBUG1:
if (enabled) if (enabled)
vehicle->SetPosition({ 100.0f, 100.0f, 5.0f }); character->SetPosition({ 100.0f, 100.0f, 5.0f });
break; break;
case IN_DEBUG2: case IN_DEBUG2:
if (enabled) if (enabled)
SpawnVehicle(player); SpawnCharacter(player);
break; break;
default: default:
@ -172,9 +211,17 @@ void game::OpenWorld::PlayerInput(Player& player, PlayerInputType type, bool ena
} }
} }
void game::OpenWorld::PlayerViewAnglesChanged(Player& player, float yaw, float pitch)
{
auto character = player_characters_.at(&player);
std::cout << "player aiming " << yaw << " " << pitch <<std::endl;
character->SetForwardYaw(yaw);
}
void game::OpenWorld::PlayerLeft(Player& player) void game::OpenWorld::PlayerLeft(Player& player)
{ {
RemoveVehicle(player); // RemoveVehicle(player);
RemoveCharacter(player);
} }
void game::OpenWorld::RemoveVehicle(Player& player) void game::OpenWorld::RemoveVehicle(Player& player)
@ -187,6 +234,30 @@ void game::OpenWorld::RemoveVehicle(Player& player)
} }
} }
void game::OpenWorld::SpawnCharacter(Player& player)
{
RemoveCharacter(player);
CharacterInfo cinfo;
auto& character = Spawn<Character>(cinfo);
character.SetNametag("player (" + std::to_string(character.GetEntNum()) + ")");
character.SetPosition({ 100.0f, 100.0f, 5.0f });
player.SetCamera(character.GetEntNum());
player_characters_[&player] = &character;
}
void game::OpenWorld::RemoveCharacter(Player& player)
{
auto it = player_characters_.find(&player);
if (it != player_characters_.end())
{
it->second->Remove();
player_characters_.erase(it);
}
}
// static void BotThink(game::Vehicle& vehicle) // static void BotThink(game::Vehicle& vehicle)
// { // {
// int direction = rand() % 3; // 0=none, 1=forward, 2=backward // int direction = rand() % 3; // 0=none, 1=forward, 2=backward

View File

@ -2,6 +2,7 @@
#include "world.hpp" #include "world.hpp"
#include "vehicle.hpp" #include "vehicle.hpp"
#include "character.hpp"
namespace game namespace game
{ {
@ -15,16 +16,21 @@ public:
virtual void PlayerJoined(Player& player) override; virtual void PlayerJoined(Player& player) override;
virtual void PlayerInput(Player& player, PlayerInputType type, bool enabled) override; virtual void PlayerInput(Player& player, PlayerInputType type, bool enabled) override;
virtual void PlayerViewAnglesChanged(Player& player, float yaw, float pitch) override;
virtual void PlayerLeft(Player& player) override; virtual void PlayerLeft(Player& player) override;
private: private:
void SpawnVehicle(Player& player); void SpawnVehicle(Player& player);
void RemoveVehicle(Player& player); void RemoveVehicle(Player& player);
void SpawnCharacter(Player& player);
void RemoveCharacter(Player& player);
void SpawnBot(); void SpawnBot();
private: private:
std::map<Player*, Vehicle*> player_vehicles_; std::map<Player*, Vehicle*> player_vehicles_;
std::map<Player*, Character*> player_characters_;
std::vector<Vehicle*> bots_; std::vector<Vehicle*> bots_;
}; };

View File

@ -18,6 +18,10 @@ bool game::Player::ProcessMsg(net::MessageType type, net::InMessage& msg)
{ {
case net::MSG_IN: case net::MSG_IN:
return ProcessInputMsg(msg); return ProcessInputMsg(msg);
case net::MSG_VIEWANGLES:
return ProcessViewAnglesMsg(msg);
default: default:
return false; return false;
} }
@ -187,6 +191,23 @@ bool game::Player::ProcessInputMsg(net::InMessage& msg)
return true; return true;
} }
bool game::Player::ProcessViewAnglesMsg(net::InMessage& msg)
{
net::ViewYawQ yaw_q;
net::ViewPitchQ pitch_q;
if (!msg.Read(yaw_q.value) || !msg.Read(pitch_q.value))
return false;
view_yaw_ = yaw_q.Decode();
view_pitch_ = pitch_q.Decode();
if (world_)
world_->PlayerViewAnglesChanged(*this, view_yaw_, view_pitch_);
return true;
}
void game::Player::Input(PlayerInputType type, bool enabled) void game::Player::Input(PlayerInputType type, bool enabled)
{ {
if (enabled) if (enabled)
@ -200,3 +221,4 @@ void game::Player::Input(PlayerInputType type, bool enabled)
world_->PlayerInput(*this, type, enabled); world_->PlayerInput(*this, type, enabled);
} }
} }

View File

@ -31,6 +31,8 @@ public:
void SendChat(const std::string text); void SendChat(const std::string text);
PlayerInputFlags GetInput() const { return in_; } PlayerInputFlags GetInput() const { return in_; }
float GetViewYaw() const { return view_yaw_; }
float GetViewPitch() const { return view_pitch_; }
~Player(); ~Player();
@ -46,6 +48,7 @@ private:
// msg handlers // msg handlers
bool ProcessInputMsg(net::InMessage& msg); bool ProcessInputMsg(net::InMessage& msg);
bool ProcessViewAnglesMsg(net::InMessage& msg);
// events // events
void Input(PlayerInputType type, bool enabled); void Input(PlayerInputType type, bool enabled);
@ -59,6 +62,7 @@ private:
std::set<net::EntNum> known_ents_; std::set<net::EntNum> known_ents_;
PlayerInputFlags in_ = 0; PlayerInputFlags in_ = 0;
float view_yaw_ = 0.0f, view_pitch_ = 0.0f;
net::EntNum cam_ent_ = 0; net::EntNum cam_ent_ = 0;
glm::vec3 cull_pos_ = glm::vec3(0.0f); glm::vec3 cull_pos_ = glm::vec3(0.0f);

View File

@ -0,0 +1,86 @@
#include "skeletoninstance.hpp"
game::SkeletonInstance::SkeletonInstance(std::shared_ptr<const assets::Skeleton> skeleton,
const TransformNode* root_node)
: skeleton_(std::move(skeleton)), root_node_(root_node)
{
SetupBoneNodes();
}
void game::SkeletonInstance::ApplySkelAnim(const assets::Animation& anim, float time, float weight)
{
float anim_frame = time * anim.GetTPS();
if (anim_frame < 0.0f)
anim_frame = 0.0f;
size_t num_anim_frames = anim.GetNumFrames();
size_t frame_i = static_cast<size_t>(anim_frame);
size_t frame0 = frame_i % num_anim_frames;
size_t frame1 = (frame_i + 1) % num_anim_frames;
float t = anim_frame - static_cast<float>(frame_i);
for (size_t i = 0; i < anim.GetNumChannels(); ++i)
{
const assets::AnimationChannel& channel = anim.GetChannel(i);
TransformNode& node = bone_nodes_[channel.bone_index];
const Transform* t0 = channel.frames[frame0];
const Transform* t1 = channel.frames[frame1];
Transform anim_transform;
if (t0 != t1)
{
anim_transform = Transform::Lerp(*t0, *t1, t); // Frames are different, interpolate
}
else
{
anim_transform = *t0; // Frames are the same, no interpolation needed
}
if (weight < 1.0f)
{
// blend with existing transform
node.local = Transform::Lerp(node.local, anim_transform, weight);
}
else
{
node.local = anim_transform;
}
}
}
void game::SkeletonInstance::UpdateBoneMatrices()
{
for (TransformNode& node : bone_nodes_)
{
node.UpdateMatrix();
}
}
void game::SkeletonInstance::SetupBoneNodes()
{
size_t num_bones = skeleton_->GetNumBones();
bone_nodes_.resize(num_bones);
for (size_t i = 0; i < num_bones; ++i)
{
const auto& bone = skeleton_->GetBone(i);
TransformNode& node = bone_nodes_[i];
node.local = bone.bind_transform;
if (bone.parent_idx >= 0)
{
node.parent = &bone_nodes_[bone.parent_idx];
}
else
{
node.parent = root_node_;
}
}
UpdateBoneMatrices();
}

View File

@ -0,0 +1,35 @@
#pragma once
#include "assets/skeleton.hpp"
#include "transform_node.hpp"
#include "utils/defs.hpp"
namespace game
{
class SkeletonInstance
{
public:
SkeletonInstance() = default;
SkeletonInstance(std::shared_ptr<const assets::Skeleton> skeleton, const TransformNode* root_node);
const TransformNode* GetRootNode() const { return root_node_; }
const TransformNode& GetBoneNode(size_t index) const { return bone_nodes_[index]; }
void ApplySkelAnim(const assets::Animation& anim, float time, float weight);
void UpdateBoneMatrices();
const std::vector<TransformNode> GetBoneNodes() const { return bone_nodes_; }
const std::shared_ptr<const assets::Skeleton>& GetSkeleton() const { return skeleton_; }
private:
void SetupBoneNodes();
private:
std::shared_ptr<const assets::Skeleton> skeleton_;
const TransformNode* root_node_ = nullptr;
std::vector<TransformNode> bone_nodes_;
};
} // namespace game

View File

@ -35,6 +35,7 @@ public:
// events // events
virtual void PlayerJoined(Player& player) {} virtual void PlayerJoined(Player& player) {}
virtual void PlayerInput(Player& player, PlayerInputType type, bool enabled) {} virtual void PlayerInput(Player& player, PlayerInputType type, bool enabled) {}
virtual void PlayerViewAnglesChanged(Player& player, float yaw, float pitch) {}
virtual void PlayerLeft(Player& player) {} virtual void PlayerLeft(Player& player) {}
Entity* GetEntity(net::EntNum entnum); Entity* GetEntity(net::EntNum entnum);

View File

@ -0,0 +1,66 @@
#include "characterview.hpp"
#include "assets/cache.hpp"
#include "net/utils.hpp"
game::view::CharacterView::CharacterView(WorldView& world, net::InMessage& msg) : EntityView(world, msg)
{
sk_ = SkeletonInstance(assets::CacheManager::GetSkeleton("data/human.sk"), &root_);
}
bool game::view::CharacterView::ProcessMsg(net::EntMsgType type, net::InMessage& msg)
{
switch (type)
{
case net::EMSG_UPDATE:
return ProcessUpdateMsg(msg);
default:
return Super::ProcessMsg(type, msg);
}
}
void game::view::CharacterView::Update(const UpdateInfo& info)
{
auto anim = sk_.GetSkeleton()->GetAnimation("walk");
sk_.ApplySkelAnim(*anim, info.time, 1.0f);
root_.UpdateMatrix();
sk_.UpdateBoneMatrices();
}
void game::view::CharacterView::Draw(const DrawArgs& args)
{
Super::Draw(args);
glm::vec3 start = root_.local.position;
glm::vec3 end = start + glm::vec3(0.0f, 0.0f, 1.5f);
args.dlist.AddBeam(start, end, 0xFF777777, 0.1f);
start = root_.local.position;
end = start + glm::vec3(glm::cos(yaw_), glm::sin(yaw_), 0.0f) * 0.5f;
args.dlist.AddBeam(start, end, 0xFF007700, 0.05f);
// draw bones debug
const auto& bone_nodes = sk_.GetBoneNodes();
for (const auto& bone_node : bone_nodes)
{
if (!bone_node.parent)
continue;
glm::vec3 p0 = bone_node.parent->matrix[3];
glm::vec3 p1 = bone_node.matrix[3];
args.dlist.AddBeam(p0, p1, 0xFF00EEEE, 0.01f);
}
}
bool game::view::CharacterView::ProcessUpdateMsg(net::InMessage& msg)
{
net::PositionQ posq;
if (!net::ReadPositionQ(msg, posq) || !msg.Read<net::PositiveAngleQ>(yaw_))
return false;
net::DecodePosition(posq, root_.local.position);
return true;
}

View File

@ -0,0 +1,31 @@
#pragma once
#include "entityview.hpp"
#include "game/skeletoninstance.hpp"
namespace game::view
{
class CharacterView : public EntityView
{
public:
using Super = EntityView;
CharacterView(WorldView& world, net::InMessage& msg);
DELETE_COPY_MOVE(CharacterView)
virtual bool ProcessMsg(net::EntMsgType type, net::InMessage& msg) override;
virtual void Update(const UpdateInfo& info) override;
virtual void Draw(const DrawArgs& args) override;
private:
bool ProcessUpdateMsg(net::InMessage& msg);
private:
float yaw_ = 0.0f;
SkeletonInstance sk_;
};
}

View File

@ -1,7 +1,7 @@
#include "client_session.hpp" #include "client_session.hpp"
#include <iostream>
#include "client/app.hpp" #include "client/app.hpp"
#include <iostream>
// #include <glm/gtx/common.hpp> // #include <glm/gtx/common.hpp>
game::view::ClientSession::ClientSession(App& app) : app_(app) {} game::view::ClientSession::ClientSession(App& app) : app_(app) {}
@ -47,14 +47,16 @@ bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net:
void game::view::ClientSession::ProcessMouseMove(float delta_yaw, float delta_pitch) void game::view::ClientSession::ProcessMouseMove(float delta_yaw, float delta_pitch)
{ {
yaw_ += delta_yaw; yaw_ = glm::mod(yaw_ + delta_yaw, glm::two_pi<float>());
// yaw_ = glm::fmod(yaw_, 2.0f * glm::pi<float>());
pitch_ += delta_pitch; pitch_ += delta_pitch;
// Clamp pitch to avoid gimbal lock // Clamp pitch to avoid gimbal lock
if (pitch_ > glm::radians(89.0f)) { if (pitch_ > glm::radians(89.0f))
{
pitch_ = glm::radians(89.0f); pitch_ = glm::radians(89.0f);
} else if (pitch_ < glm::radians(-89.0f)) { }
else if (pitch_ < glm::radians(-89.0f))
{
pitch_ = glm::radians(-89.0f); pitch_ = glm::radians(-89.0f);
} }
} }
@ -80,7 +82,7 @@ void game::view::ClientSession::GetViewInfo(glm::vec3& eye, glm::mat4& view) con
float yaw_sin = glm::sin(yaw_); float yaw_sin = glm::sin(yaw_);
float pitch_cos = glm::cos(pitch_); float pitch_cos = glm::cos(pitch_);
float pitch_sin = glm::sin(pitch_); float pitch_sin = glm::sin(pitch_);
glm::vec3 dir(yaw_sin * pitch_cos, yaw_cos * pitch_cos, pitch_sin); glm::vec3 dir(yaw_cos * pitch_cos, yaw_sin * pitch_cos, pitch_sin);
float distance = 8.0f; float distance = 8.0f;

View File

@ -31,6 +31,9 @@ public:
audio::Master& GetAudioMaster() const; audio::Master& GetAudioMaster() const;
float GetYaw() const { return yaw_; }
float GetPitch() const { return pitch_; }
private: private:
// msg handlers // msg handlers
bool ProcessWorldMsg(net::InMessage& msg); bool ProcessWorldMsg(net::InMessage& msg);

View File

@ -95,13 +95,9 @@ void game::view::EntityView::DrawAxes(const DrawArgs& args)
glm::vec3 end(0.0f); glm::vec3 end(0.0f);
end[i] = len; end[i] = len;
gfx::DrawBeamCmd cmd; glm::vec3 beam_start = glm::vec3(root_.matrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
cmd.start = glm::vec3(root_.matrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); glm::vec3 beam_end = glm::vec3(root_.matrix * glm::vec4(end, 1.0f));
cmd.end = glm::vec3(root_.matrix * glm::vec4(end, 1.0f));
cmd.color = colors[i]; args.dlist.AddBeam(beam_start, beam_end, colors[i], 0.05f);
cmd.radius = 0.05f;
//cmd.num_segments = 10;
//cmd.max_offset = 0.1f;
args.dlist.AddBeam(cmd);
} }
} }

View File

@ -2,6 +2,7 @@
#include "assets/cache.hpp" #include "assets/cache.hpp"
#include "characterview.hpp"
#include "vehicleview.hpp" #include "vehicleview.hpp"
#include "client_session.hpp" #include "client_session.hpp"
@ -77,6 +78,10 @@ bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg)
{ {
switch (type) switch (type)
{ {
case net::ET_CHARACTER:
entslot = std::make_unique<CharacterView>(*this, msg);
break;
case net::ET_VEHICLE: case net::ET_VEHICLE:
entslot = std::make_unique<VehicleView>(*this, msg); entslot = std::make_unique<VehicleView>(*this, msg);
break; break;

View File

@ -3,8 +3,8 @@
#include <vector> #include <vector>
#include "assets/skeleton.hpp" #include "assets/skeleton.hpp"
#include "surface.hpp"
#include "hud.hpp" #include "hud.hpp"
#include "surface.hpp"
namespace gfx namespace gfx
{ {
@ -23,10 +23,16 @@ struct DrawBeamCmd
{ {
glm::vec3 start; glm::vec3 start;
glm::vec3 end; glm::vec3 end;
uint32_t color = 0xFFFFFFFF; uint32_t color;
float radius = 0.1f; float radius;
size_t num_segments = 1; size_t num_segments;
float max_offset = 0.0f; float max_offset;
DrawBeamCmd(const glm::vec3& start, const glm::vec3& end, uint32_t color, float radius,
size_t num_segments, float max_offset)
: start(start), end(end), color(color), radius(radius), num_segments(num_segments), max_offset(max_offset)
{
}
}; };
struct DrawHudCmd struct DrawHudCmd
@ -44,7 +50,14 @@ struct DrawList
std::vector<DrawHudCmd> huds; std::vector<DrawHudCmd> huds;
void AddSurface(const DrawSurfaceCmd& cmd) { surfaces.emplace_back(cmd); } void AddSurface(const DrawSurfaceCmd& cmd) { surfaces.emplace_back(cmd); }
void AddBeam(const DrawBeamCmd& cmd) { beams.emplace_back(cmd); } void AddBeam(const DrawBeamCmd& cmd) { beams.emplace_back(cmd); }
void AddBeam(const glm::vec3& start, const glm::vec3& end, uint32_t color = 0xFFFFFFFF, float radius = 0.1f,
size_t num_segments = 1, float max_offset = 0.0f)
{
beams.emplace_back(start, end, color, radius, num_segments, max_offset);
}
void AddHUD(const DrawHudCmd& cmd) { huds.emplace_back(cmd); } void AddHUD(const DrawHudCmd& cmd) { huds.emplace_back(cmd); }
void Clear() void Clear()

View File

@ -17,9 +17,12 @@ enum MessageType : uint8_t
// ID <PlayerName> // ID <PlayerName>
MSG_ID, MSG_ID,
// IN <PlayerInputFlags> <ViewYawQ> <ViewPitchQ> // IN <u8, MSB=down/~up, 6..0=input type>
MSG_IN, MSG_IN,
// VIEWANGLES <ViewYawQ> <ViewPitchQ>
MSG_VIEWANGLES,
/*~~~~~~~~ Session ~~~~~~~~*/ /*~~~~~~~~ Session ~~~~~~~~*/
// CHAT <ChatMessage> // CHAT <ChatMessage>
MSG_CHAT, MSG_CHAT,
@ -82,6 +85,7 @@ struct PositionQ
}; };
using AngleQ = Quantized<uint16_t, -PI_N, PI_N, PI_D>; using AngleQ = Quantized<uint16_t, -PI_N, PI_N, PI_D>;
using PositiveAngleQ = Quantized<uint16_t, 0, PI_N * 2, PI_D>;
using QuatElemQ = Quantized<uint16_t, -1, 1, 1>; using QuatElemQ = Quantized<uint16_t, -1, 1, 1>;
struct QuatQ struct QuatQ

799
tools/export.py Normal file
View File

@ -0,0 +1,799 @@
from __future__ import annotations
from dataclasses import dataclass
import bpy
import os
import re
import math
CHUNK_SIZE = 250.0
@dataclass
class Vec3:
x: float
y: float
z: float
def __add__(self, other):
return Vec3(self.x + other.x, self.y + other.y, self.z + other.z)
def __mul__(self, scalar: float):
return Vec3(self.x * scalar, self.y * scalar, self.z * scalar)
def dot(self, other):
return self.x*other.x + self.y*other.y + self.z*other.z
def cross(self, other):
return Vec3(
self.y*other.z - self.z*other.y,
self.z*other.x - self.x*other.z,
self.x*other.y - self.y*other.x
)
@staticmethod
def min(a: Vec3, b: Vec3):
return Vec3(
a.x if a.x < b.x else b.x,
a.y if a.y < b.y else b.y,
a.z if a.z < b.z else b.z
)
@staticmethod
def max(a: Vec3, b: Vec3):
return Vec3(
a.x if a.x > b.x else b.x,
a.y if a.y > b.y else b.y,
a.z if a.z > b.z else b.z
)
def norm(self):
return math.sqrt(self.dot(self))
class Vertex:
position: tuple[float, float, float]
normal: tuple[float, float, float]
uv: tuple[float, float]
color: tuple[float, float, float] | None
def __init__(self, position, normal, uv, color=None):
self.position = tuple(round(x, 6) for x in position)
self.normal = tuple(round(x, 6) for x in normal)
self.uv = tuple(round(x, 6) for x in uv)
self.color = tuple(round(c, 3) for c in color) if color else None
def __hash__(self):
return hash((self.position, self.normal, self.uv, self.color))
def __eq__(self, other):
return (self.position, self.normal, self.uv, self.color) == (other.position, other.normal, other.uv, other.color)
class Surface:
tris: list[tuple[int, int, int]]
texture: str
twosided: bool
ocolor: bool
blend: str|None
def __init__(self, name: str):
self.tris = []
self.texture = name
self.twosided = False
self.ocolor = False
self.blend = None
class Model:
vertices: list[Vertex]
vertex_map: dict[Vertex, int]
materials: dict[str, Surface]
make_col_trimesh: bool
make_convex_hull: bool
def __init__(self):
self.vertices = []
self.vertex_map = {}
self.materials = {}
self.make_col_trimesh = False
self.make_convex_hull = False
def add_vertex(self, vertex: Vertex) -> int:
if vertex in self.vertex_map:
return self.vertex_map[vertex]
index = len(self.vertices)
self.vertices.append(vertex)
self.vertex_map[vertex] = index
return index
def add_triangle(self, material_name: str, v1: int, v2: int, v3: int):
# if material_name not in self.materials:
# self.materials[material_name] = Surface(material_name)
self.materials[material_name].tris.append((v1, v2, v3))
class Transform:
position: tuple[float, float, float]
rotation: tuple[float, float, float, float] # quat xyzw
scale: float
def __init__(self, position, rotation, scale):
self.position = tuple(round(x, 6) for x in position)
self.rotation = tuple(round(x, 6) for x in rotation)
self.scale = round(scale, 6)
class GraphNode:
pos: tuple[float, float, float]
def __init__(self, pos):
self.pos = tuple(round(x, 6) for x in pos)
class GraphEdge:
nodes: tuple[int, int]
def __init__(self, node1, node2):
self.nodes = (node1, node2)
class Graph:
name: str
nodes: list[GraphNode]
edges: list[GraphEdge]
def __init__(self, name: str):
self.name = name
self.nodes = []
self.edges = []
def add_node(self, node: GraphNode) -> int:
index = len(self.nodes)
self.nodes.append(node)
return index
def add_edge(self, node1_index: int, node2_index: int):
edge = GraphEdge(node1_index, node2_index)
self.edges.append(edge)
class Chunk:
coord: tuple[int, int] # x,y
shifted_coord: tuple[int, int]
aabb_min: Vec3
aabb_max: Vec3
static_objects: list[tuple[str, Transform]]
surface_ranges: list[tuple[str, int, int]] # name, first, count
def __init__(self, coord: tuple[int, int]):
self.coord = coord
self.shifted_coord = (0, 0)
self.static_objects = []
self.surface_ranges = []
self.aabb_min = Vec3(math.inf, math.inf, math.inf)
self.aabb_max = Vec3(-math.inf, -math.inf, -math.inf)
def extend_aabb(self, pos: Vec3):
self.aabb_min = Vec3.min(self.aabb_min, pos)
self.aabb_max = Vec3.max(self.aabb_max, pos)
class Map:
basemodel: Model
basemodel_name: str
static_objects: list[tuple[str, Transform]]
graphs: list[Graph]
chunks: dict[tuple[int, int], Chunk]
max_chunk: tuple[int, int]
def __init__(self):
self.basemodel = Model()
self.basemodel_name = ""
self.static_objects = []
self.graphs = []
self.chunks = None
self.max_chunk = (0, 0)
def create_chunks(self):
self.chunks = {}
def get_chunk(coord: tuple[int,int]) -> Chunk:
if not coord in self.chunks:
chunk = Chunk(coord)
self.chunks[coord] = chunk
return chunk
return self.chunks[coord]
def pos_to_chunk(pos: Vec3) -> tuple[int,int]:
return int(math.floor(pos.x / CHUNK_SIZE)), int(math.floor(pos.y / CHUNK_SIZE))
def tri_to_chunk(tri: tuple[int, int, int]) -> tuple[int, int]:
p0, p1, p2 = [Vec3(*self.basemodel.vertices[i].position) for i in tri]
return pos_to_chunk((p0 + p1 + p2) * (1/3))
# surfaces
for name, surface in self.basemodel.materials.items():
tris = [(tri, tri_to_chunk(tri)) for tri in surface.tris]
tris.sort(key=lambda x: x[1]) #sort by chunk coord
cur_chunk: Chunk|None = None
start = 0
i = 0
def finish_cur_chunk():
if cur_chunk is None or i - start < 1:
return
count = i - start
cur_chunk.surface_ranges.append((name, start, count))
# extend aabb wtih tris
for j in range(start, i):
p0, p1, p2 = [Vec3(*self.basemodel.vertices[k].position) for k in tris[j][0]]
cur_chunk.extend_aabb(p0)
cur_chunk.extend_aabb(p1)
cur_chunk.extend_aabb(p2)
surface.tris.clear()
for tri_coord in tris:
tri, coord = tri_coord
surface.tris.append(tri)
if cur_chunk is None or coord != cur_chunk.coord:
finish_cur_chunk()
cur_chunk = get_chunk(coord)
start = i
i += 1
finish_cur_chunk()
# objects
for obj in self.static_objects:
name, trans = obj
pos = Vec3(*trans.position)
chunk = get_chunk(pos_to_chunk(pos))
chunk.extend_aabb(pos)
chunk.static_objects.append(obj)
# min/max
min_chunk = (100000, 100000)
max_chunk = (-100000, -100000)
for coord in self.chunks:
min_chunk = (
min_chunk[0] if min_chunk[0] < coord[0] else coord[0],
min_chunk[1] if min_chunk[1] < coord[1] else coord[1]
)
max_chunk = (
max_chunk[0] if max_chunk[0] > coord[0] else coord[0],
max_chunk[1] if max_chunk[1] > coord[1] else coord[1]
)
for coord, chunk in self.chunks.items():
chunk.shifted_coord = (
coord[0] - min_chunk[0],
coord[1] - min_chunk[1],
)
self.max_chunk = (
max_chunk[0] - min_chunk[0],
max_chunk[1] - min_chunk[1],
)
class Wheel:
type: str
model_name: str
position: tuple[float, float, float]
radius: float
class Vehicle:
basemodel_name: str
wheels: list[Wheel] # type, model, transform
def __init__(self):
self.wheels = []
class Bone:
name: str
parent: str|None
trans: Transform
def __init__(self, name):
self.name = name
class AnimChannel:
name: str
frames: list[tuple[int, Transform]]
def __init__(self, name):
self.name = name
self.frames = []
class Animation:
name: str
fps: float
frames: int
channels: list[AnimChannel]
def __init__(self, name: str, fps: float, frames: int):
self.name = name
self.fps = fps
self.frames = frames
self.channels = []
class Skeleton:
name: str
bones: list[Bone]
bone_set: set[str]
anims: list[Animation]
def __init__(self, name, armature):
self.name = name
self.bones = []
self.bone_set = set()
self.armature = armature
self.anims = []
class Exporter:
skeletons: dict[str, Skeleton]
def __init__(self):
self.blend_dir = os.path.dirname(bpy.data.filepath)
self.out_path = os.path.join(self.blend_dir, "export")
self.skeletons = {}
@staticmethod
def add_mesh_to_model(obj, model: Model):
if obj.type != "MESH":
print("warning: tried to export object that is not a mesh")
return
print(f" processing mesh: {obj.name}")
mesh = obj.data
mesh.calc_loop_triangles()
# Prepare UV layers
uv_layer = mesh.uv_layers.active.data
for tri in mesh.loop_triangles:
face_indices = []
material_index = tri.material_index
# Get the material from the object's material slots
if material_index < len(obj.material_slots):
material = obj.material_slots[material_index].material
material_name = material.name if material else "Unknown"
else:
material_name = "Unknown"
mat_type, mat_name, mat_params = Exporter.extract_name(material_name)
if mat_type != "MAT":
continue # skip non material
for loop_index in tri.loops:
loop = mesh.loops[loop_index]
vertex = mesh.vertices[loop.vertex_index]
pos = tuple(c for c in vertex.co)
normal = tuple(n for n in loop.normal)
uv = tuple(c for c in uv_layer[loop_index].uv)
# get color from named attribute
color = None
color_attr = mesh.color_attributes.get("vertex_color")
if color_attr:
color_data = color_attr.data[loop_index]
color = tuple(c for c in color_data.color[:3]) # ignore alpha
vert = Vertex(position=pos, normal=normal, uv=uv, color=color)
vert_index = model.add_vertex(vert)
face_indices.append(vert_index)
if mat_name not in model.materials:
surface = Surface(mat_name)
surface.twosided = "2S" in mat_params
surface.ocolor = "OCOLOR" in mat_params
blend = mat_params.get("BLEND")
if isinstance(blend, str):
surface.blend = blend
model.materials[mat_name] = surface
model.add_triangle(mat_name, *face_indices)
@staticmethod
def add_mesh_to_graph(obj, graph: Graph):
mesh = obj.data
# Ensure we have access to the 'G_flip' attribute
flip_layer = None
if "G_flip" in mesh.attributes:
flip_layer = mesh.attributes["G_flip"]
# Add nodes
for v in mesh.vertices:
pos = tuple(c for c in v.co)
graph.add_node(GraphNode(pos))
# Add edges
for e in mesh.edges:
v1, v2 = e.vertices[0], e.vertices[1]
if flip_layer and flip_layer.data[e.index].value:
graph.add_edge(v2, v1)
else:
graph.add_edge(v1, v2)
def rad2deg(self, rad: float) -> float:
return rad * (180.0 / 3.141592653589793)
def transform_str(self, transform: Transform):
# rx, ry, rz = transform.rotation
# dx, dy, dz = self.rad2deg(rx), self.rad2deg(ry), self.rad2deg(rz)
t, r, s = transform.position, transform.rotation, transform.scale
return f"{t[0]:.6f} {t[1]:.6f} {t[2]:.6f} {r[0]:.6f} {r[1]:.6f} {r[2]:.6f} {r[3]:.6f} {s:.6f}"
def export_mdl(self, model: Model, filepath: str):
with open(filepath, "w") as f:
if model.make_col_trimesh:
f.write("makecoltrimesh\n")
if model.make_convex_hull:
f.write("makeconvexhull\n")
for v in model.vertices:
color_str = f" {v.color[0]} {v.color[1]} {v.color[2]}" if v.color else ""
f.write(f"v {v.position[0]} {v.position[1]} {v.position[2]} {v.normal[0]} {v.normal[1]} {v.normal[2]} {v.uv[0]} {v.uv[1]}{color_str}\n")
for mat_name, surface in model.materials.items():
f.write(f"surface {mat_name} +texture {surface.texture}")
if surface.twosided:
f.write(" +2sided")
if surface.ocolor:
f.write(" +ocolor")
if surface.blend is not None:
f.write(f" +blend {surface.blend}")
f.write("\n")
for tri in surface.tris:
f.write(f"f {tri[0]} {tri[1]} {tri[2]}\n")
def export_map(self, map: Map, filepath: str):
with open(filepath, "w") as f:
f.write(f"basemodel {map.basemodel_name}\n")
# graphs
for graph in map.graphs:
f.write(f"graph {graph.name}\n")
for node in graph.nodes:
f.write(f"n {node.pos[0]} {node.pos[1]} {node.pos[2]}\n")
for edge in graph.edges:
f.write(f"e {edge.nodes[0]} {edge.nodes[1]}\n")
# # static
# for obj_name, transform in map.static_objects:
# f.write(f"static {obj_name} {Exporter.transform_str(transform)}\n")
f.write(f"chunks {map.max_chunk[0]} {map.max_chunk[1]}\n")
chunks_sorted = [c[1] for c in map.chunks.items()]
chunks_sorted.sort(key=lambda c: c.shifted_coord)
for chunk in chunks_sorted:
f.write(f"chunk {chunk.shifted_coord[0]} {chunk.shifted_coord[1]} {chunk.aabb_min.x} {chunk.aabb_min.y} {chunk.aabb_min.z} {chunk.aabb_max.x} {chunk.aabb_max.y} {chunk.aabb_max.z}\n")
for name, first, count in chunk.surface_ranges:
f.write(f"surface {name} {first} {count}\n")
for obj_name, transform in chunk.static_objects:
f.write(f"static {obj_name} {self.transform_str(transform)}\n")
def export_veh(self, veh: Vehicle, filepath: str):
with open(filepath, "w") as f:
f.write(f"basemodel {veh.basemodel_name}\n")
for wheel in veh.wheels:
f.write(f"wheel {wheel.type} {wheel.model_name} {wheel.position[0]} {wheel.position[1]} {wheel.position[2]} {wheel.radius}\n")
def export_sk(self, sk: Skeleton, filepath: str):
with open(filepath, "w") as f:
for bone in sk.bones:
parent_str = bone.parent if bone.parent is not None else "NONE"
f.write(f"b {bone.name} {parent_str} {self.transform_str(bone.trans)}\n")
for anim in sk.anims:
f.write(f"anim {anim.name} {sk.name}_{anim.name}\n")
def export_anim(self, anim: Animation, filepath: str):
with open(filepath, 'w') as f:
f.write(f"frames {anim.frames}\n")
f.write(f"fps {anim.fps}\n")
for chan in anim.channels:
f.write(f"ch {chan.name}\n")
for idx, trans in chan.frames:
f.write(f"f {idx} {self.transform_str(trans)}\n")
@staticmethod
def extract_name(fullname: str) -> tuple[str|None, str, dict[str, str|bool]]:
match = re.match(rf"^(\w+)\/([\w]+)\/?([^\.]*)", fullname)
if not match:
return None, fullname, {}
type, name, all_params_str = match.group(1), match.group(2), match.group(3)
# extract params
params_str = all_params_str.split("/")
params: dict[str, str|bool] = {}
for str_param in params_str:
if str_param.find("=") == -1:
params[str_param] = True
continue
k, v = str_param.split("=")
params[k] = v
return type, name, params
def get_obj_transform(self, obj) -> Transform:
mat = obj.matrix_world
# position = obj.location
# rotation = (obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w)
# scale = obj.scale[0] # assume uniform scale
# return Transform(position, rotation, scale)
return Transform(*self.matrix_decompose(mat))
def process_MDL(self, col, name, params):
print(f"exporting MDL: {name}")
model = Model()
if "C" in params:
Cs = params["C"].split(",")
for C in Cs:
if C == "tri":
model.make_col_trimesh = True
elif C == "convex":
model.make_convex_hull = True
for obj in col.objects:
type, _, _ = self.extract_name(obj.name)
if type == "M":
self.add_mesh_to_model(obj, model)
mdl_filepath = os.path.join(self.out_path, f"{name}.mdl")
self.export_mdl(model, mdl_filepath)
def process_MAP(self, col, name, params):
print(f"exporting MAP: {name}")
map = Map()
map.basemodel_name = name
map.basemodel.make_col_trimesh = True
def proc_col(col):
for obj in col.objects:
type, obj_name, _ = self.extract_name(obj.name)
if type == "M":
self.add_mesh_to_model(obj, map.basemodel)
elif type == "OBJ":
transform = self.get_obj_transform(obj)
map.static_objects.append((obj_name, transform))
elif type == "GRAPH":
graph = Graph(obj_name)
self.add_mesh_to_graph(obj, graph)
map.graphs.append(graph)
for child_col in col.children:
proc_col(child_col)
proc_col(col)
map.create_chunks()
mdl_filepath = os.path.join(self.out_path, f"{name}.mdl")
self.export_mdl(map.basemodel, mdl_filepath)
map_filepath = os.path.join(self.out_path, f"{name}.map")
self.export_map(map, map_filepath)
def process_VEH(self, col, name, params):
print(f"exporting VEH: {name}")
veh = Vehicle()
veh.basemodel_name = name
for obj in col.objects:
type, obj_name, params = self.extract_name(obj.name)
if type == "WHEEL":
wheel_type = params["W"] if "W" in params else ""
radius = float(params["R"].replace(",", ".")) if "R" in params else 0.5
transform = self.get_obj_transform(obj)
wheel = Wheel()
wheel.type = wheel_type
wheel.model_name = obj_name
wheel.position = transform.position
wheel.radius = radius
veh.wheels.append(wheel)
veh.wheels.sort(key=lambda w: w.type)
veh_filepath = os.path.join(self.out_path, f"{name}.veh")
self.export_veh(veh, veh_filepath)
def get_armature_keep_bones(self, armature):
keep_bones = set()
for bone in armature.data.bones:
#bone_name = bone.name
if bone.use_deform: # or is_tag:
keep_bones.add(bone)
while bone.parent:
bone = bone.parent
keep_bones.add(bone)
return keep_bones
def matrix_decompose(self, matrix):
t, r, s = matrix.decompose()
return (t.x, t.y, t.z), (r.x, r.y, r.z, r.w), s.x
def process_SK(self, obj, name, params):
if obj.type != "ARMATURE":
print("SK object not armature!")
return
sk = Skeleton(name, obj)
keep_bones = self.get_armature_keep_bones(obj)
keep_bones_str = ",".join([bone.name for bone in keep_bones])
print(f"Keep bones: {keep_bones_str}")
print(f"Num bones: {len(keep_bones)}")
for bone in obj.data.bones:
if not bone in keep_bones:
continue
xbone = Bone(bone.name)
parent = bone.parent
xbone.parent = parent.name if parent else None
bind_matrix = bone.matrix_local
xbone.trans = Transform(*self.matrix_decompose(bind_matrix))
sk.bones.append(xbone)
sk.bone_set.add(xbone.name)
self.skeletons[name] = sk
def process_A(self, action, name, params):
if not "_" in name:
print(f"{name}: required format skeleton_animname")
return
sk_name, anim_name = name.split("_", 1)
if not sk_name in self.skeletons:
print(f"{anim_name}: unknown anim skeleton {sk_name}")
return
sk = self.skeletons[sk_name]
original_action = sk.armature.animation_data.action
original_frame = bpy.context.scene.frame_current
sk.armature.animation_data.action = action
bone_frames = {bonename: [] for bonename in sk.bone_set}
_, end = map(int, action.frame_range)
fps = bpy.context.scene.render.fps
def vectors_similar(v1, v2, threshold):
return (v1 - v2).length < threshold
def quats_similar(q1, q2, threshold=1e-4):
return q1.rotation_difference(q2).angle < threshold
def frames_similar(f1, f2, threshold=0.00001):
_, t1, r1, s1, _ = f1
_, t2, r2, s2, _ = f2
if not vectors_similar(t1, t2, threshold):
return False
if not quats_similar(r1, r2):
return False
if not vectors_similar(s1, s2, threshold):
return False
return True
for frame in range(0, end):
bpy.context.scene.frame_set(frame)
bpy.context.view_layer.update()
for bonename in sk.bone_set:
pose_bone = sk.armature.pose.bones.get(bonename)
if not pose_bone:
continue
matrix = (pose_bone.parent.matrix.inverted() @ pose_bone.matrix) if pose_bone.parent else pose_bone.matrix.copy()
translation, rotation, scale = matrix.decompose()
current_frame = (frame, translation, rotation, scale, matrix)
frame_list = bone_frames[bonename]
if len(frame_list) > 0:
last_frame = frame_list[-1]
if frames_similar(last_frame, current_frame):
continue
frame_list.append(current_frame)
anim = Animation(anim_name, fps, end)
for bone_name, frames in bone_frames.items():
chan = AnimChannel(bone_name)
for frame in frames:
idx, _, _, _, matrix = frame
chan.frames.append((idx, Transform(*self.matrix_decompose(matrix))))
anim.channels.append(chan)
anim.channels.sort(key=lambda ch: ch.name)
bpy.context.scene.frame_set(original_frame)
sk.armature.animation_data.action = original_action
sk.anims.append(anim)
def run(self):
print("=== EXPORT STARTED ===")
os.makedirs(self.out_path, exist_ok=True)
# collections
for col in bpy.context.scene.collection.children:
type, name, params = self.extract_name(col.name)
if type == "MDL":
self.process_MDL(col, name, params)
elif type == "MAP":
self.process_MAP(col, name, params)
elif type == "VEH":
self.process_VEH(col, name, params)
# skeletons
for obj in bpy.context.scene.collection.objects:
type, name, params = self.extract_name(obj.name)
if type == "SK":
self.process_SK(obj, name, params)
# animations
for action in bpy.data.actions:
type, name, params = self.extract_name(action.name)
if type == "A":
self.process_A(action, name, params)
# export skeletons & anims
for name, sk in self.skeletons.items():
sk.anims.sort(key=lambda a: a.name)
for anim in sk.anims:
self.export_anim(anim, os.path.join(self.out_path, f"{name}_{anim.name}.anim"))
self.export_sk(sk, os.path.join(self.out_path, f"{name}.sk"))
Exporter().run()

48
tools/fnt2font.py Normal file
View File

@ -0,0 +1,48 @@
import xml.etree.ElementTree as ET
import sys
from pathlib import Path
def convert_bmfont_xml(xml_path, output_path):
tree = ET.parse(xml_path)
root = tree.getroot()
# --- Read font info ---
info = root.find("info")
common = root.find("common")
page = root.find("pages/page")
font_size = info.attrib.get("size", "0")
tex_width = common.attrib.get("scaleW", "0")
tex_height = common.attrib.get("scaleH", "0")
texture_name = Path(page.attrib["file"]).stem
lines = []
lines.append(f"texture {texture_name} {tex_width} {tex_height}")
lines.append(f"size {font_size}")
# --- Glyphs ---
chars = root.find("chars")
for ch in chars.findall("char"):
code = ch.attrib["id"]
x = ch.attrib["x"]
y = ch.attrib["y"]
w = ch.attrib["width"]
h = ch.attrib["height"]
xoff = ch.attrib["xoffset"]
yoff = ch.attrib["yoffset"]
xadv = ch.attrib["xadvance"]
lines.append(f"c {code} {x} {y} {w} {h} {xoff} {yoff} {xadv}")
# --- Write output ---
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"Written: {output_path}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python fnt2font.py input.fnt output.txt")
sys.exit(1)
convert_bmfont_xml(sys.argv[1], sys.argv[2])