CCT, skeletons and anims init
This commit is contained in:
parent
f8ef0c55d0
commit
6396f94783
@ -26,6 +26,8 @@ set(COMMON_SOURCES
|
||||
"src/collision/trianglemesh.hpp"
|
||||
"src/collision/trianglemesh.cpp"
|
||||
"src/game/player_input.hpp"
|
||||
"src/game/skeletoninstance.hpp"
|
||||
"src/game/skeletoninstance.cpp"
|
||||
"src/game/transform_node.hpp"
|
||||
"src/game/vehicle_sync.hpp"
|
||||
"src/net/defs.hpp"
|
||||
@ -67,6 +69,8 @@ set(CLIENT_ONLY_SOURCES
|
||||
"src/client/gl.hpp"
|
||||
"src/client/main.cpp"
|
||||
"src/client/utils.hpp"
|
||||
"src/gameview/characterview.hpp"
|
||||
"src/gameview/characterview.cpp"
|
||||
"src/gameview/client_session.hpp"
|
||||
"src/gameview/client_session.cpp"
|
||||
"src/gameview/entityview.hpp"
|
||||
@ -102,6 +106,8 @@ set(CLIENT_ONLY_SOURCES
|
||||
)
|
||||
|
||||
set(SERVER_ONLY_SOURCES
|
||||
"src/game/character.hpp"
|
||||
"src/game/character.cpp"
|
||||
"src/game/entity.hpp"
|
||||
"src/game/entity.cpp"
|
||||
"src/game/game.hpp"
|
||||
|
||||
@ -1,142 +1,124 @@
|
||||
#include "animation.hpp"
|
||||
#include "skeleton.hpp"
|
||||
|
||||
#include "utils/files.hpp"
|
||||
#include <sstream>
|
||||
#include "cmdfile.hpp"
|
||||
|
||||
#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;
|
||||
std::vector<size_t> frame_indices;
|
||||
|
||||
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();
|
||||
|
||||
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 >= 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);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
size_t last_frame_idx = frame_indices.back();
|
||||
frame_indices.resize(target_size, last_frame_idx);
|
||||
};
|
||||
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
|
||||
if (command == "f")
|
||||
{
|
||||
if (anim->num_frames_ == 0)
|
||||
{
|
||||
throw std::runtime_error("Frame data specified before number of frames in animation file: " + filename);
|
||||
}
|
||||
|
||||
std::string line;
|
||||
if (anim->channels_.empty())
|
||||
{
|
||||
throw std::runtime_error("Frame data specified before any channels in animation file: " + filename);
|
||||
}
|
||||
|
||||
while (std::getline(ifs, line))
|
||||
{
|
||||
if (line.empty() || line[0] == '#') // Skip empty lines and comments
|
||||
continue;
|
||||
int frame_index;
|
||||
iss >> frame_index;
|
||||
|
||||
std::istringstream iss(line);
|
||||
if (frame_index < 0 || frame_index >= (int)anim->num_frames_)
|
||||
{
|
||||
throw std::runtime_error("Frame index out of bounds in animation file: " + filename);
|
||||
}
|
||||
|
||||
std::string command;
|
||||
iss >> command;
|
||||
if (frame_index < last_frame)
|
||||
{
|
||||
throw std::runtime_error("Frame indices must be in ascending order in animation file: " + filename);
|
||||
}
|
||||
|
||||
if (command == "f")
|
||||
{
|
||||
if (anim->num_frames_ == 0)
|
||||
{
|
||||
throw std::runtime_error("Frame data specified before number of frames in animation file: " + filename);
|
||||
}
|
||||
last_frame = frame_index;
|
||||
|
||||
if (anim->channels_.empty())
|
||||
{
|
||||
throw std::runtime_error("Frame data specified before any channels in animation file: " + filename);
|
||||
}
|
||||
Transform t;
|
||||
ParseTransform(iss, t);
|
||||
|
||||
int frame_index;
|
||||
iss >> frame_index;
|
||||
size_t idx = anim->frames_.size();
|
||||
anim->frames_.push_back(t);
|
||||
|
||||
if (frame_index < 0 || frame_index >= (int)anim->num_frames_)
|
||||
{
|
||||
throw std::runtime_error("Frame index out of bounds in animation file: " + filename);
|
||||
}
|
||||
FillFrameRefs(frame_index); // Fill to current frame
|
||||
frame_indices.push_back(idx);
|
||||
}
|
||||
else if (command == "ch")
|
||||
{
|
||||
std::string name;
|
||||
iss >> name;
|
||||
|
||||
if (frame_index < last_frame)
|
||||
{
|
||||
throw std::runtime_error("Frame indices must be in ascending order in animation file: " + filename);
|
||||
}
|
||||
int bone_index = skeleton->GetBoneIndex(name);
|
||||
|
||||
last_frame = frame_index;
|
||||
if (bone_index < 0)
|
||||
{
|
||||
throw std::runtime_error("Bone referenced in animation not found in provided skeleton: " + name);
|
||||
}
|
||||
|
||||
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;
|
||||
FillFrameRefs(anim->num_frames_); // Fill to end for last channel
|
||||
|
||||
size_t idx = anim->frames_.size();
|
||||
anim->frames_.push_back(t);
|
||||
AnimationChannel& channel = anim->channels_.emplace_back();
|
||||
channel.bone_index = bone_index;
|
||||
channel.frames = nullptr; // Will be set up later
|
||||
|
||||
FillFrameRefs(frame_index); // Fill to current frame
|
||||
frame_indices.push_back(idx);
|
||||
}
|
||||
else if (command == "ch")
|
||||
{
|
||||
std::string name;
|
||||
iss >> name;
|
||||
last_frame = 0;
|
||||
}
|
||||
else if (command == "frames")
|
||||
{
|
||||
iss >> anim->num_frames_;
|
||||
}
|
||||
else if (command == "fps")
|
||||
{
|
||||
iss >> anim->tps_;
|
||||
}
|
||||
});
|
||||
|
||||
int bone_index = skeleton->GetBoneIndex(name);
|
||||
if (anim->channels_.empty())
|
||||
{
|
||||
throw std::runtime_error("No channels found in animation file: " + filename);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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]];
|
||||
}
|
||||
|
||||
AnimationChannel& channel = anim->channels_.emplace_back();
|
||||
channel.bone_index = bone_index;
|
||||
channel.frames = nullptr; // Will be set up later
|
||||
// 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_];
|
||||
}
|
||||
|
||||
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;
|
||||
return anim;
|
||||
}
|
||||
|
||||
@ -1,43 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "utils/transform.hpp"
|
||||
|
||||
namespace assets
|
||||
{
|
||||
class Skeleton;
|
||||
class Skeleton;
|
||||
|
||||
struct AnimationChannel
|
||||
{
|
||||
int bone_index;
|
||||
const Transform* const* frames;
|
||||
};
|
||||
struct AnimationChannel
|
||||
{
|
||||
int bone_index;
|
||||
const Transform* const* frames;
|
||||
};
|
||||
|
||||
class Animation
|
||||
{
|
||||
size_t num_frames_ = 0;
|
||||
float tps_ = 24.0f;
|
||||
class Animation
|
||||
{
|
||||
public:
|
||||
Animation() = default;
|
||||
static std::shared_ptr<const Animation> LoadFromFile(const std::string& filename, const Skeleton* skeleton);
|
||||
|
||||
std::vector<AnimationChannel> channels_;
|
||||
std::vector<const Transform*> frame_refs_;
|
||||
std::vector<Transform> frames_;
|
||||
size_t GetNumFrames() const { return num_frames_; }
|
||||
float GetTPS() const { return tps_; }
|
||||
float GetDuration() const { return static_cast<float>(num_frames_) / tps_; }
|
||||
|
||||
public:
|
||||
Animation() = default;
|
||||
size_t GetNumChannels() const { return channels_.size(); }
|
||||
const AnimationChannel& GetChannel(int index) const { return channels_[index]; }
|
||||
|
||||
size_t GetNumFrames() const { return num_frames_; }
|
||||
float GetTPS() const { return tps_; }
|
||||
float GetDuration() const { return static_cast<float>(num_frames_) / tps_; }
|
||||
private:
|
||||
size_t num_frames_ = 0;
|
||||
float tps_ = 24.0f;
|
||||
|
||||
size_t GetNumChannels() const { return channels_.size(); }
|
||||
const AnimationChannel& GetChannel(int index) const { return channels_[index]; }
|
||||
std::vector<AnimationChannel> channels_;
|
||||
std::vector<const Transform*> frame_refs_;
|
||||
std::vector<Transform> frames_;
|
||||
};
|
||||
|
||||
static std::shared_ptr<const Animation> LoadFromFile(const std::string& filename, const Skeleton* skeleton);
|
||||
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
} // namespace assets
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils/files.hpp"
|
||||
#include "utils/transform.hpp"
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
@ -12,4 +13,12 @@ namespace assets
|
||||
void LoadCMDFile(const std::string& filename,
|
||||
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
|
||||
@ -53,17 +53,13 @@ std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string&
|
||||
|
||||
glm::vec3 angles;
|
||||
|
||||
auto trans = &obj.node.local;
|
||||
|
||||
iss >> trans->position.x >> trans->position.y >> trans->position.z;
|
||||
iss >> angles.x >> angles.y >> angles.z;
|
||||
trans->SetAngles(angles);
|
||||
iss >> trans->scale;
|
||||
auto& trans = obj.node.local;
|
||||
ParseTransform(iss, trans);
|
||||
|
||||
obj.node.UpdateMatrix();
|
||||
|
||||
obj.aabb.min = trans->position - glm::vec3(1.0f);
|
||||
obj.aabb.max = trans->position + glm::vec3(1.0f);
|
||||
obj.aabb.min = trans.position - glm::vec3(1.0f);
|
||||
obj.aabb.max = trans.position + glm::vec3(1.0f);
|
||||
|
||||
std::string flag;
|
||||
while (iss >> flag)
|
||||
|
||||
@ -1,95 +1,78 @@
|
||||
#include "skeleton.hpp"
|
||||
|
||||
#include "utils/files.hpp"
|
||||
#include <sstream>
|
||||
#include "cmdfile.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform)
|
||||
std::shared_ptr<const assets::Skeleton> assets::Skeleton::LoadFromFile(const std::string& filename)
|
||||
{
|
||||
int index = static_cast<int>(bones_.size());
|
||||
auto skeleton = std::make_shared<Skeleton>();
|
||||
|
||||
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());
|
||||
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
|
||||
if (command == "b")
|
||||
{
|
||||
Transform t;
|
||||
std::string bone_name, parent_name;
|
||||
|
||||
bone_map_[bone.name] = index;
|
||||
iss >> bone_name >> parent_name;
|
||||
ParseTransform(iss, t);
|
||||
|
||||
if (iss.fail())
|
||||
{
|
||||
throw std::runtime_error("Failed to parse bone definition in file: " + filename);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int assets::Skeleton::GetBoneIndex(const std::string& name) const
|
||||
{
|
||||
auto it = bone_map_.find(name);
|
||||
if (it != bone_map_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
auto it = bone_map_.find(name);
|
||||
if (it != bone_map_.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return -1;
|
||||
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;
|
||||
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)
|
||||
void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform)
|
||||
{
|
||||
std::istringstream ifs = fs::ReadFileAsStream(filename);
|
||||
int index = static_cast<int>(bones_.size());
|
||||
|
||||
std::shared_ptr<Skeleton> skeleton = std::make_shared<Skeleton>();
|
||||
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());
|
||||
|
||||
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;
|
||||
bone_map_[bone.name] = index;
|
||||
}
|
||||
@ -1,47 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "utils/transform.hpp"
|
||||
#include "animation.hpp"
|
||||
#include "utils/transform.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_;
|
||||
struct Bone
|
||||
{
|
||||
int parent_idx;
|
||||
std::string name;
|
||||
Transform bind_transform;
|
||||
glm::mat4 inv_bind_matrix;
|
||||
};
|
||||
|
||||
std::map<std::string, std::shared_ptr<const Animation>> anims_;
|
||||
class Skeleton
|
||||
{
|
||||
public:
|
||||
Skeleton() = default;
|
||||
static std::shared_ptr<const Skeleton> LoadFromFile(const std::string& filename);
|
||||
|
||||
public:
|
||||
Skeleton() = default;
|
||||
int GetBoneIndex(const std::string& name) const;
|
||||
|
||||
void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform);
|
||||
size_t GetNumBones() const { return bones_.size(); }
|
||||
const Bone& GetBone(size_t idx) const { return bones_[idx]; }
|
||||
|
||||
int GetBoneIndex(const std::string& name) const;
|
||||
const Animation* GetAnimation(const std::string& name) const;
|
||||
|
||||
size_t GetNumBones() const { return bones_.size(); }
|
||||
const Bone& GetBone(size_t idx) const { return bones_[idx]; }
|
||||
private:
|
||||
void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform);
|
||||
void AddAnimation(const std::string& name, const std::shared_ptr<const Animation>& anim) { anims_[name] = anim; }
|
||||
|
||||
const Animation* GetAnimation(const std::string& name) const;
|
||||
private:
|
||||
std::vector<Bone> bones_;
|
||||
std::map<std::string, int> bone_map_;
|
||||
|
||||
static std::shared_ptr<const Skeleton> LoadFromFile(const std::string& filename);
|
||||
std::map<std::string, std::shared_ptr<const Animation>> anims_;
|
||||
};
|
||||
|
||||
private:
|
||||
void AddAnimation(const std::string& name, const std::shared_ptr<const Animation>& anim) { anims_[name] = anim; }
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
} // namespace assets
|
||||
@ -94,6 +94,25 @@ void App::Frame()
|
||||
|
||||
glm::mat4 camera_world = glm::inverse(view);
|
||||
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
|
||||
@ -101,15 +120,6 @@ void App::Frame()
|
||||
DrawChat(dlist_);
|
||||
|
||||
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()
|
||||
@ -153,7 +163,7 @@ void App::MouseMove(const glm::vec2& delta)
|
||||
{
|
||||
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;
|
||||
|
||||
if (session_)
|
||||
|
||||
@ -59,6 +59,8 @@ private:
|
||||
glm::ivec2 viewport_size_ = {800, 600};
|
||||
game::PlayerInputFlags input_ = 0;
|
||||
game::PlayerInputFlags prev_input_ = 0;
|
||||
net::ViewYawQ view_yaw_q_;
|
||||
net::ViewPitchQ view_pitch_q_;
|
||||
|
||||
float prev_time_ = 0.0f;
|
||||
float delta_time_ = 0.0f;
|
||||
|
||||
189
src/game/character.cpp
Normal file
189
src/game/character.cpp
Normal 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
79
src/game/character.hpp
Normal 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
|
||||
@ -94,7 +94,7 @@ game::OpenWorld::OpenWorld() : World("openworld")
|
||||
// }
|
||||
|
||||
// spawn bots
|
||||
for (size_t i = 0; i < 300; ++i)
|
||||
for (size_t i = 0; i < 100; ++i)
|
||||
{
|
||||
SpawnBot();
|
||||
}
|
||||
@ -131,40 +131,79 @@ void game::OpenWorld::Update(int64_t delta_time)
|
||||
|
||||
void game::OpenWorld::PlayerJoined(Player& player)
|
||||
{
|
||||
SpawnVehicle(player);
|
||||
// SpawnVehicle(player);
|
||||
SpawnCharacter(player);
|
||||
}
|
||||
|
||||
void game::OpenWorld::PlayerInput(Player& player, PlayerInputType type, bool enabled)
|
||||
{
|
||||
auto vehicle = player_vehicles_.at(&player);
|
||||
// player.SendChat("input zmenen: " + std::to_string(static_cast<int>(type)) + "=" + (enabled ? "1" : "0"));
|
||||
// auto vehicle = player_vehicles_.at(&player);
|
||||
// // 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)
|
||||
{
|
||||
case IN_FORWARD:
|
||||
vehicle->SetInput(VIN_FORWARD, enabled);
|
||||
character->SetInput(CIN_FORWARD, enabled);
|
||||
break;
|
||||
|
||||
case IN_BACKWARD:
|
||||
vehicle->SetInput(VIN_BACKWARD, enabled);
|
||||
character->SetInput(CIN_BACKWARD, enabled);
|
||||
break;
|
||||
|
||||
case IN_LEFT:
|
||||
vehicle->SetInput(VIN_LEFT, enabled);
|
||||
character->SetInput(CIN_LEFT, enabled);
|
||||
break;
|
||||
|
||||
case IN_RIGHT:
|
||||
vehicle->SetInput(VIN_RIGHT, enabled);
|
||||
character->SetInput(CIN_RIGHT, enabled);
|
||||
break;
|
||||
|
||||
case IN_JUMP:
|
||||
character->SetInput(CIN_JUMP, enabled);
|
||||
break;
|
||||
|
||||
case IN_DEBUG1:
|
||||
if (enabled)
|
||||
vehicle->SetPosition({ 100.0f, 100.0f, 5.0f });
|
||||
character->SetPosition({ 100.0f, 100.0f, 5.0f });
|
||||
break;
|
||||
|
||||
case IN_DEBUG2:
|
||||
if (enabled)
|
||||
SpawnVehicle(player);
|
||||
SpawnCharacter(player);
|
||||
break;
|
||||
|
||||
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)
|
||||
{
|
||||
RemoveVehicle(player);
|
||||
// RemoveVehicle(player);
|
||||
RemoveCharacter(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)
|
||||
// {
|
||||
// int direction = rand() % 3; // 0=none, 1=forward, 2=backward
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "world.hpp"
|
||||
#include "vehicle.hpp"
|
||||
#include "character.hpp"
|
||||
|
||||
namespace game
|
||||
{
|
||||
@ -15,16 +16,21 @@ public:
|
||||
|
||||
virtual void PlayerJoined(Player& player) 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;
|
||||
|
||||
private:
|
||||
void SpawnVehicle(Player& player);
|
||||
void RemoveVehicle(Player& player);
|
||||
|
||||
void SpawnCharacter(Player& player);
|
||||
void RemoveCharacter(Player& player);
|
||||
|
||||
void SpawnBot();
|
||||
|
||||
private:
|
||||
std::map<Player*, Vehicle*> player_vehicles_;
|
||||
std::map<Player*, Character*> player_characters_;
|
||||
std::vector<Vehicle*> bots_;
|
||||
};
|
||||
|
||||
|
||||
@ -18,6 +18,10 @@ bool game::Player::ProcessMsg(net::MessageType type, net::InMessage& msg)
|
||||
{
|
||||
case net::MSG_IN:
|
||||
return ProcessInputMsg(msg);
|
||||
|
||||
case net::MSG_VIEWANGLES:
|
||||
return ProcessViewAnglesMsg(msg);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -187,6 +191,23 @@ bool game::Player::ProcessInputMsg(net::InMessage& msg)
|
||||
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)
|
||||
{
|
||||
if (enabled)
|
||||
@ -200,3 +221,4 @@ void game::Player::Input(PlayerInputType type, bool enabled)
|
||||
world_->PlayerInput(*this, type, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,8 @@ public:
|
||||
void SendChat(const std::string text);
|
||||
|
||||
PlayerInputFlags GetInput() const { return in_; }
|
||||
float GetViewYaw() const { return view_yaw_; }
|
||||
float GetViewPitch() const { return view_pitch_; }
|
||||
|
||||
~Player();
|
||||
|
||||
@ -46,6 +48,7 @@ private:
|
||||
|
||||
// msg handlers
|
||||
bool ProcessInputMsg(net::InMessage& msg);
|
||||
bool ProcessViewAnglesMsg(net::InMessage& msg);
|
||||
|
||||
// events
|
||||
void Input(PlayerInputType type, bool enabled);
|
||||
@ -59,6 +62,7 @@ private:
|
||||
std::set<net::EntNum> known_ents_;
|
||||
|
||||
PlayerInputFlags in_ = 0;
|
||||
float view_yaw_ = 0.0f, view_pitch_ = 0.0f;
|
||||
|
||||
net::EntNum cam_ent_ = 0;
|
||||
glm::vec3 cull_pos_ = glm::vec3(0.0f);
|
||||
|
||||
86
src/game/skeletoninstance.cpp
Normal file
86
src/game/skeletoninstance.cpp
Normal 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();
|
||||
}
|
||||
35
src/game/skeletoninstance.hpp
Normal file
35
src/game/skeletoninstance.hpp
Normal 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
|
||||
@ -35,6 +35,7 @@ public:
|
||||
// events
|
||||
virtual void PlayerJoined(Player& player) {}
|
||||
virtual void PlayerInput(Player& player, PlayerInputType type, bool enabled) {}
|
||||
virtual void PlayerViewAnglesChanged(Player& player, float yaw, float pitch) {}
|
||||
virtual void PlayerLeft(Player& player) {}
|
||||
|
||||
Entity* GetEntity(net::EntNum entnum);
|
||||
|
||||
66
src/gameview/characterview.cpp
Normal file
66
src/gameview/characterview.cpp
Normal 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;
|
||||
}
|
||||
31
src/gameview/characterview.hpp
Normal file
31
src/gameview/characterview.hpp
Normal 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_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
#include "client_session.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include "client/app.hpp"
|
||||
#include <iostream>
|
||||
// #include <glm/gtx/common.hpp>
|
||||
|
||||
game::view::ClientSession::ClientSession(App& app) : app_(app) {}
|
||||
@ -47,16 +47,18 @@ bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net:
|
||||
|
||||
void game::view::ClientSession::ProcessMouseMove(float delta_yaw, float delta_pitch)
|
||||
{
|
||||
yaw_ += delta_yaw;
|
||||
// yaw_ = glm::fmod(yaw_, 2.0f * glm::pi<float>());
|
||||
yaw_ = glm::mod(yaw_ + delta_yaw, glm::two_pi<float>());
|
||||
|
||||
pitch_ += delta_pitch;
|
||||
// Clamp pitch to avoid gimbal lock
|
||||
if (pitch_ > glm::radians(89.0f)) {
|
||||
pitch_ = glm::radians(89.0f);
|
||||
} else if (pitch_ < glm::radians(-89.0f)) {
|
||||
pitch_ = glm::radians(-89.0f);
|
||||
}
|
||||
if (pitch_ > glm::radians(89.0f))
|
||||
{
|
||||
pitch_ = glm::radians(89.0f);
|
||||
}
|
||||
else if (pitch_ < glm::radians(-89.0f))
|
||||
{
|
||||
pitch_ = glm::radians(-89.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void game::view::ClientSession::Update(const UpdateInfo& info)
|
||||
@ -76,11 +78,11 @@ void game::view::ClientSession::GetViewInfo(glm::vec3& eye, glm::mat4& view) con
|
||||
center += ent->GetRoot().local.position;
|
||||
}
|
||||
|
||||
float yaw_cos = glm::cos(yaw_);
|
||||
float yaw_sin = glm::sin(yaw_);
|
||||
float pitch_cos = glm::cos(pitch_);
|
||||
float pitch_sin = glm::sin(pitch_);
|
||||
glm::vec3 dir(yaw_sin * pitch_cos, yaw_cos * pitch_cos, pitch_sin);
|
||||
float yaw_cos = glm::cos(yaw_);
|
||||
float yaw_sin = glm::sin(yaw_);
|
||||
float pitch_cos = glm::cos(pitch_);
|
||||
float pitch_sin = glm::sin(pitch_);
|
||||
glm::vec3 dir(yaw_cos * pitch_cos, yaw_sin * pitch_cos, pitch_sin);
|
||||
|
||||
float distance = 8.0f;
|
||||
|
||||
|
||||
@ -31,6 +31,9 @@ public:
|
||||
|
||||
audio::Master& GetAudioMaster() const;
|
||||
|
||||
float GetYaw() const { return yaw_; }
|
||||
float GetPitch() const { return pitch_; }
|
||||
|
||||
private:
|
||||
// msg handlers
|
||||
bool ProcessWorldMsg(net::InMessage& msg);
|
||||
|
||||
@ -95,13 +95,9 @@ void game::view::EntityView::DrawAxes(const DrawArgs& args)
|
||||
glm::vec3 end(0.0f);
|
||||
end[i] = len;
|
||||
|
||||
gfx::DrawBeamCmd cmd;
|
||||
cmd.start = glm::vec3(root_.matrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
cmd.end = glm::vec3(root_.matrix * glm::vec4(end, 1.0f));
|
||||
cmd.color = colors[i];
|
||||
cmd.radius = 0.05f;
|
||||
//cmd.num_segments = 10;
|
||||
//cmd.max_offset = 0.1f;
|
||||
args.dlist.AddBeam(cmd);
|
||||
glm::vec3 beam_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));
|
||||
|
||||
args.dlist.AddBeam(beam_start, beam_end, colors[i], 0.05f);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "assets/cache.hpp"
|
||||
|
||||
#include "characterview.hpp"
|
||||
#include "vehicleview.hpp"
|
||||
#include "client_session.hpp"
|
||||
|
||||
@ -77,6 +78,10 @@ bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case net::ET_CHARACTER:
|
||||
entslot = std::make_unique<CharacterView>(*this, msg);
|
||||
break;
|
||||
|
||||
case net::ET_VEHICLE:
|
||||
entslot = std::make_unique<VehicleView>(*this, msg);
|
||||
break;
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
#include <vector>
|
||||
|
||||
#include "assets/skeleton.hpp"
|
||||
#include "surface.hpp"
|
||||
#include "hud.hpp"
|
||||
#include "surface.hpp"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
@ -23,10 +23,16 @@ struct DrawBeamCmd
|
||||
{
|
||||
glm::vec3 start;
|
||||
glm::vec3 end;
|
||||
uint32_t color = 0xFFFFFFFF;
|
||||
float radius = 0.1f;
|
||||
size_t num_segments = 1;
|
||||
float max_offset = 0.0f;
|
||||
uint32_t color;
|
||||
float radius;
|
||||
size_t num_segments;
|
||||
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
|
||||
@ -44,7 +50,14 @@ struct DrawList
|
||||
std::vector<DrawHudCmd> huds;
|
||||
|
||||
void AddSurface(const DrawSurfaceCmd& cmd) { surfaces.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 Clear()
|
||||
|
||||
@ -17,9 +17,12 @@ enum MessageType : uint8_t
|
||||
// ID <PlayerName>
|
||||
MSG_ID,
|
||||
|
||||
// IN <PlayerInputFlags> <ViewYawQ> <ViewPitchQ>
|
||||
// IN <u8, MSB=down/~up, 6..0=input type>
|
||||
MSG_IN,
|
||||
|
||||
// VIEWANGLES <ViewYawQ> <ViewPitchQ>
|
||||
MSG_VIEWANGLES,
|
||||
|
||||
/*~~~~~~~~ Session ~~~~~~~~*/
|
||||
// CHAT <ChatMessage>
|
||||
MSG_CHAT,
|
||||
@ -82,6 +85,7 @@ struct PositionQ
|
||||
};
|
||||
|
||||
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>;
|
||||
struct QuatQ
|
||||
|
||||
799
tools/export.py
Normal file
799
tools/export.py
Normal 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
48
tools/fnt2font.py
Normal 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])
|
||||
Loading…
x
Reference in New Issue
Block a user