rigging and animation
This commit is contained in:
parent
b85f225789
commit
cc027a2517
11 changed files with 2120 additions and 38 deletions
|
|
@ -56,6 +56,10 @@ Entity3D::~Entity3D()
|
||||||
{
|
{
|
||||||
// Cleanup cube geometry when last entity is destroyed?
|
// Cleanup cube geometry when last entity is destroyed?
|
||||||
// For now, leave it - it's shared static data
|
// For now, leave it - it's shared static data
|
||||||
|
|
||||||
|
// Clean up Python animation callback
|
||||||
|
Py_XDECREF(py_anim_callback_);
|
||||||
|
py_anim_callback_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -283,8 +287,8 @@ void Entity3D::processNextMove()
|
||||||
|
|
||||||
void Entity3D::update(float dt)
|
void Entity3D::update(float dt)
|
||||||
{
|
{
|
||||||
if (!is_animating_) return;
|
// Update movement animation
|
||||||
|
if (is_animating_) {
|
||||||
move_progress_ += dt * move_speed_;
|
move_progress_ += dt * move_speed_;
|
||||||
|
|
||||||
if (move_progress_ >= 1.0f) {
|
if (move_progress_ >= 1.0f) {
|
||||||
|
|
@ -300,6 +304,10 @@ void Entity3D::update(float dt)
|
||||||
// Interpolate position
|
// Interpolate position
|
||||||
world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_);
|
world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update skeletal animation
|
||||||
|
updateAnimation(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Entity3D::setProperty(const std::string& name, float value)
|
bool Entity3D::setProperty(const std::string& name, float value)
|
||||||
|
|
@ -386,6 +394,111 @@ bool Entity3D::hasProperty(const std::string& name) const
|
||||||
name == "sprite_index" || name == "visible";
|
name == "sprite_index" || name == "visible";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Skeletal Animation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Entity3D::setAnimClip(const std::string& name)
|
||||||
|
{
|
||||||
|
if (anim_clip_ == name) return;
|
||||||
|
|
||||||
|
anim_clip_ = name;
|
||||||
|
anim_time_ = 0.0f;
|
||||||
|
anim_paused_ = false;
|
||||||
|
|
||||||
|
// Initialize bone matrices if model has skeleton
|
||||||
|
if (model_ && model_->hasSkeleton()) {
|
||||||
|
size_t bone_count = model_->getBoneCount();
|
||||||
|
bone_matrices_.resize(bone_count);
|
||||||
|
for (auto& m : bone_matrices_) {
|
||||||
|
m = mat4::identity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Entity3D::updateAnimation(float dt)
|
||||||
|
{
|
||||||
|
// Handle auto-animate (play walk/idle based on movement state)
|
||||||
|
if (auto_animate_ && model_ && model_->hasSkeleton()) {
|
||||||
|
bool currently_moving = isMoving();
|
||||||
|
if (currently_moving != was_moving_) {
|
||||||
|
was_moving_ = currently_moving;
|
||||||
|
if (currently_moving) {
|
||||||
|
// Started moving - play walk clip
|
||||||
|
if (model_->findClip(walk_clip_)) {
|
||||||
|
setAnimClip(walk_clip_);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Stopped moving - play idle clip
|
||||||
|
if (model_->findClip(idle_clip_)) {
|
||||||
|
setAnimClip(idle_clip_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early out if no model, no skeleton, or no animation
|
||||||
|
if (!model_ || !model_->hasSkeleton()) return;
|
||||||
|
if (anim_clip_.empty() || anim_paused_) return;
|
||||||
|
|
||||||
|
const AnimationClip* clip = model_->findClip(anim_clip_);
|
||||||
|
if (!clip) return;
|
||||||
|
|
||||||
|
// Advance time
|
||||||
|
anim_time_ += dt * anim_speed_;
|
||||||
|
|
||||||
|
// Handle loop/completion
|
||||||
|
if (anim_time_ >= clip->duration) {
|
||||||
|
if (anim_loop_) {
|
||||||
|
anim_time_ = std::fmod(anim_time_, clip->duration);
|
||||||
|
} else {
|
||||||
|
anim_time_ = clip->duration;
|
||||||
|
anim_paused_ = true;
|
||||||
|
|
||||||
|
// Fire callback
|
||||||
|
if (on_anim_complete_) {
|
||||||
|
on_anim_complete_(this, anim_clip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire Python callback
|
||||||
|
if (py_anim_callback_) {
|
||||||
|
PyObject* result = PyObject_CallFunction(py_anim_callback_, "(Os)",
|
||||||
|
self, anim_clip_.c_str());
|
||||||
|
if (result) {
|
||||||
|
Py_DECREF(result);
|
||||||
|
} else {
|
||||||
|
PyErr_Print();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample animation
|
||||||
|
const Skeleton& skeleton = model_->getSkeleton();
|
||||||
|
const std::vector<mat4>& default_transforms = model_->getDefaultBoneTransforms();
|
||||||
|
|
||||||
|
std::vector<mat4> local_transforms;
|
||||||
|
clip->sample(anim_time_, skeleton.bones.size(), default_transforms, local_transforms);
|
||||||
|
|
||||||
|
// Compute global transforms
|
||||||
|
std::vector<mat4> global_transforms;
|
||||||
|
skeleton.computeGlobalTransforms(local_transforms, global_transforms);
|
||||||
|
|
||||||
|
// Compute final bone matrices (global * inverse_bind)
|
||||||
|
skeleton.computeBoneMatrices(global_transforms, bone_matrices_);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::getAnimFrame() const
|
||||||
|
{
|
||||||
|
if (!model_ || !model_->hasSkeleton()) return 0;
|
||||||
|
|
||||||
|
const AnimationClip* clip = model_->findClip(anim_clip_);
|
||||||
|
if (!clip || clip->duration <= 0) return 0;
|
||||||
|
|
||||||
|
// Approximate frame at 30fps
|
||||||
|
return static_cast<int>(anim_time_ * 30.0f);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Rendering
|
// Rendering
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -482,7 +595,13 @@ void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader)
|
||||||
// If we have a model, use it
|
// If we have a model, use it
|
||||||
if (model_) {
|
if (model_) {
|
||||||
mat4 model = getModelMatrix();
|
mat4 model = getModelMatrix();
|
||||||
|
|
||||||
|
// Use skinned rendering if model has skeleton and we have bone matrices
|
||||||
|
if (model_->hasSkeleton() && !bone_matrices_.empty()) {
|
||||||
|
model_->renderSkinned(shader, model, view, proj, bone_matrices_);
|
||||||
|
} else {
|
||||||
model_->render(shader, model, view, proj);
|
model_->render(shader, model, view, proj);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -762,6 +881,147 @@ int Entity3D::set_model(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animation property getters/setters
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_anim_clip(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyUnicode_FromString(self->data->getAnimClip().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyUnicode_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "anim_clip must be a string");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setAnimClip(PyUnicode_AsUTF8(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_anim_time(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyFloat_FromDouble(self->data->getAnimTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "anim_time must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setAnimTime((float)PyFloat_AsDouble(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_anim_speed(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyFloat_FromDouble(self->data->getAnimSpeed());
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "anim_speed must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setAnimSpeed((float)PyFloat_AsDouble(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_anim_loop(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyBool_FromLong(self->data->getAnimLoop() ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
self->data->setAnimLoop(PyObject_IsTrue(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_anim_paused(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyBool_FromLong(self->data->getAnimPaused() ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
self->data->setAnimPaused(PyObject_IsTrue(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_anim_frame(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyLong_FromLong(self->data->getAnimFrame());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_on_anim_complete(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (self->data->py_anim_callback_) {
|
||||||
|
Py_INCREF(self->data->py_anim_callback_);
|
||||||
|
return self->data->py_anim_callback_;
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
// Clear existing callback
|
||||||
|
Py_XDECREF(self->data->py_anim_callback_);
|
||||||
|
|
||||||
|
if (value == Py_None) {
|
||||||
|
self->data->py_anim_callback_ = nullptr;
|
||||||
|
} else if (PyCallable_Check(value)) {
|
||||||
|
Py_INCREF(value);
|
||||||
|
self->data->py_anim_callback_ = value;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "on_anim_complete must be callable or None");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_auto_animate(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyBool_FromLong(self->data->getAutoAnimate() ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
self->data->setAutoAnimate(PyObject_IsTrue(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_walk_clip(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyUnicode_FromString(self->data->getWalkClip().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyUnicode_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "walk_clip must be a string");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setWalkClip(PyUnicode_AsUTF8(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_idle_clip(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyUnicode_FromString(self->data->getIdleClip().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int Entity3D::set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyUnicode_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "idle_clip must be a string");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setIdleClip(PyUnicode_AsUTF8(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
|
||||||
PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
|
PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
|
@ -903,6 +1163,29 @@ PyGetSetDef Entity3D::getsetters[] = {
|
||||||
"Owning Viewport3D (read-only).", NULL},
|
"Owning Viewport3D (read-only).", NULL},
|
||||||
{"model", (getter)Entity3D::get_model, (setter)Entity3D::set_model,
|
{"model", (getter)Entity3D::get_model, (setter)Entity3D::set_model,
|
||||||
"3D model (Model3D). If None, uses placeholder cube.", NULL},
|
"3D model (Model3D). If None, uses placeholder cube.", NULL},
|
||||||
|
|
||||||
|
// Animation properties
|
||||||
|
{"anim_clip", (getter)Entity3D::get_anim_clip, (setter)Entity3D::set_anim_clip,
|
||||||
|
"Current animation clip name. Set to play an animation.", NULL},
|
||||||
|
{"anim_time", (getter)Entity3D::get_anim_time, (setter)Entity3D::set_anim_time,
|
||||||
|
"Current time position in animation (seconds).", NULL},
|
||||||
|
{"anim_speed", (getter)Entity3D::get_anim_speed, (setter)Entity3D::set_anim_speed,
|
||||||
|
"Animation playback speed multiplier. 1.0 = normal speed.", NULL},
|
||||||
|
{"anim_loop", (getter)Entity3D::get_anim_loop, (setter)Entity3D::set_anim_loop,
|
||||||
|
"Whether animation loops when it reaches the end.", NULL},
|
||||||
|
{"anim_paused", (getter)Entity3D::get_anim_paused, (setter)Entity3D::set_anim_paused,
|
||||||
|
"Whether animation playback is paused.", NULL},
|
||||||
|
{"anim_frame", (getter)Entity3D::get_anim_frame, NULL,
|
||||||
|
"Current animation frame number (read-only, approximate at 30fps).", NULL},
|
||||||
|
{"on_anim_complete", (getter)Entity3D::get_on_anim_complete, (setter)Entity3D::set_on_anim_complete,
|
||||||
|
"Callback(entity, clip_name) when non-looping animation ends.", NULL},
|
||||||
|
{"auto_animate", (getter)Entity3D::get_auto_animate, (setter)Entity3D::set_auto_animate,
|
||||||
|
"Enable auto-play of walk/idle clips based on movement.", NULL},
|
||||||
|
{"walk_clip", (getter)Entity3D::get_walk_clip, (setter)Entity3D::set_walk_clip,
|
||||||
|
"Animation clip to play when entity is moving.", NULL},
|
||||||
|
{"idle_clip", (getter)Entity3D::get_idle_clip, (setter)Entity3D::set_idle_clip,
|
||||||
|
"Animation clip to play when entity is stationary.", NULL},
|
||||||
|
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace mcrf {
|
namespace mcrf {
|
||||||
|
|
||||||
|
|
@ -158,6 +159,57 @@ public:
|
||||||
bool getProperty(const std::string& name, float& value) const;
|
bool getProperty(const std::string& name, float& value) const;
|
||||||
bool hasProperty(const std::string& name) const;
|
bool hasProperty(const std::string& name) const;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Skeletal Animation
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Get current animation clip name
|
||||||
|
const std::string& getAnimClip() const { return anim_clip_; }
|
||||||
|
|
||||||
|
/// Set animation clip by name (starts playing)
|
||||||
|
void setAnimClip(const std::string& name);
|
||||||
|
|
||||||
|
/// Get/set animation time (position in clip)
|
||||||
|
float getAnimTime() const { return anim_time_; }
|
||||||
|
void setAnimTime(float t) { anim_time_ = t; }
|
||||||
|
|
||||||
|
/// Get/set playback speed (1.0 = normal)
|
||||||
|
float getAnimSpeed() const { return anim_speed_; }
|
||||||
|
void setAnimSpeed(float s) { anim_speed_ = s; }
|
||||||
|
|
||||||
|
/// Get/set looping state
|
||||||
|
bool getAnimLoop() const { return anim_loop_; }
|
||||||
|
void setAnimLoop(bool l) { anim_loop_ = l; }
|
||||||
|
|
||||||
|
/// Get/set pause state
|
||||||
|
bool getAnimPaused() const { return anim_paused_; }
|
||||||
|
void setAnimPaused(bool p) { anim_paused_ = p; }
|
||||||
|
|
||||||
|
/// Get current animation frame (approximate)
|
||||||
|
int getAnimFrame() const;
|
||||||
|
|
||||||
|
/// Update skeletal animation (call before render)
|
||||||
|
void updateAnimation(float dt);
|
||||||
|
|
||||||
|
/// Get computed bone matrices for shader
|
||||||
|
const std::vector<mat4>& getBoneMatrices() const { return bone_matrices_; }
|
||||||
|
|
||||||
|
/// Animation complete callback type
|
||||||
|
using AnimCompleteCallback = std::function<void(Entity3D*, const std::string&)>;
|
||||||
|
|
||||||
|
/// Set animation complete callback
|
||||||
|
void setOnAnimComplete(AnimCompleteCallback cb) { on_anim_complete_ = cb; }
|
||||||
|
|
||||||
|
/// Auto-animate settings (play walk/idle based on movement)
|
||||||
|
bool getAutoAnimate() const { return auto_animate_; }
|
||||||
|
void setAutoAnimate(bool a) { auto_animate_ = a; }
|
||||||
|
|
||||||
|
const std::string& getWalkClip() const { return walk_clip_; }
|
||||||
|
void setWalkClip(const std::string& c) { walk_clip_ = c; }
|
||||||
|
|
||||||
|
const std::string& getIdleClip() const { return idle_clip_; }
|
||||||
|
void setIdleClip(const std::string& c) { idle_clip_ = c; }
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Rendering
|
// Rendering
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -193,6 +245,27 @@ public:
|
||||||
static PyObject* get_model(PyEntity3DObject* self, void* closure);
|
static PyObject* get_model(PyEntity3DObject* self, void* closure);
|
||||||
static int set_model(PyEntity3DObject* self, PyObject* value, void* closure);
|
static int set_model(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
|
||||||
|
// Animation property getters/setters
|
||||||
|
static PyObject* get_anim_clip(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_anim_clip(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_anim_time(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_anim_time(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_anim_speed(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_anim_speed(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_anim_loop(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_anim_loop(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_anim_paused(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_anim_paused(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_anim_frame(PyEntity3DObject* self, void* closure);
|
||||||
|
static PyObject* get_on_anim_complete(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_on_anim_complete(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_auto_animate(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_auto_animate(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_walk_clip(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_walk_clip(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_idle_clip(PyEntity3DObject* self, void* closure);
|
||||||
|
static int set_idle_clip(PyEntity3DObject* self, PyObject* value, void* closure);
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_teleport(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_teleport(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
@ -240,6 +313,24 @@ private:
|
||||||
float move_speed_ = 5.0f; // Cells per second
|
float move_speed_ = 5.0f; // Cells per second
|
||||||
vec3 move_start_pos_;
|
vec3 move_start_pos_;
|
||||||
|
|
||||||
|
// Skeletal animation state
|
||||||
|
std::string anim_clip_; // Current animation clip name
|
||||||
|
float anim_time_ = 0.0f; // Current time in animation
|
||||||
|
float anim_speed_ = 1.0f; // Playback speed multiplier
|
||||||
|
bool anim_loop_ = true; // Loop animation
|
||||||
|
bool anim_paused_ = false; // Pause playback
|
||||||
|
std::vector<mat4> bone_matrices_; // Computed bone matrices for shader
|
||||||
|
AnimCompleteCallback on_anim_complete_; // Callback when animation ends
|
||||||
|
|
||||||
|
// Auto-animate state
|
||||||
|
bool auto_animate_ = true; // Auto-play walk/idle based on movement
|
||||||
|
std::string walk_clip_ = "walk"; // Clip to play when moving
|
||||||
|
std::string idle_clip_ = "idle"; // Clip to play when stopped
|
||||||
|
bool was_moving_ = false; // Track movement state for auto-animate
|
||||||
|
|
||||||
|
// Python callback for animation complete
|
||||||
|
PyObject* py_anim_callback_ = nullptr;
|
||||||
|
|
||||||
// Helper to initialize voxel state
|
// Helper to initialize voxel state
|
||||||
void initVoxelState() const;
|
void initVoxelState() const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,176 @@ ModelMesh& ModelMesh::operator=(ModelMesh&& other) noexcept
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SkinnedMesh Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
SkinnedMesh::SkinnedMesh(SkinnedMesh&& other) noexcept
|
||||||
|
: vbo(other.vbo)
|
||||||
|
, ebo(other.ebo)
|
||||||
|
, vertex_count(other.vertex_count)
|
||||||
|
, index_count(other.index_count)
|
||||||
|
, material_index(other.material_index)
|
||||||
|
, is_skinned(other.is_skinned)
|
||||||
|
{
|
||||||
|
other.vbo = 0;
|
||||||
|
other.ebo = 0;
|
||||||
|
other.vertex_count = 0;
|
||||||
|
other.index_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SkinnedMesh& SkinnedMesh::operator=(SkinnedMesh&& other) noexcept
|
||||||
|
{
|
||||||
|
if (this != &other) {
|
||||||
|
vbo = other.vbo;
|
||||||
|
ebo = other.ebo;
|
||||||
|
vertex_count = other.vertex_count;
|
||||||
|
index_count = other.index_count;
|
||||||
|
material_index = other.material_index;
|
||||||
|
is_skinned = other.is_skinned;
|
||||||
|
other.vbo = 0;
|
||||||
|
other.ebo = 0;
|
||||||
|
other.vertex_count = 0;
|
||||||
|
other.index_count = 0;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AnimationChannel Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void AnimationChannel::sample(float time, vec3& trans_out, quat& rot_out, vec3& scale_out) const
|
||||||
|
{
|
||||||
|
if (times.empty()) return;
|
||||||
|
|
||||||
|
// Clamp time to animation range
|
||||||
|
float t = std::max(times.front(), std::min(time, times.back()));
|
||||||
|
|
||||||
|
// Find surrounding keyframes
|
||||||
|
size_t k0 = 0, k1 = 0;
|
||||||
|
float blend = 0.0f;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < times.size() - 1; i++) {
|
||||||
|
if (t >= times[i] && t <= times[i + 1]) {
|
||||||
|
k0 = i;
|
||||||
|
k1 = i + 1;
|
||||||
|
float dt = times[k1] - times[k0];
|
||||||
|
blend = (dt > 0.0001f) ? (t - times[k0]) / dt : 0.0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If time is past the last keyframe, use last keyframe
|
||||||
|
if (t >= times.back()) {
|
||||||
|
k0 = k1 = times.size() - 1;
|
||||||
|
blend = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate based on path type
|
||||||
|
switch (path) {
|
||||||
|
case Path::Translation:
|
||||||
|
if (!translations.empty()) {
|
||||||
|
trans_out = vec3::lerp(translations[k0], translations[k1], blend);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Path::Rotation:
|
||||||
|
if (!rotations.empty()) {
|
||||||
|
rot_out = quat::slerp(rotations[k0], rotations[k1], blend);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Path::Scale:
|
||||||
|
if (!scales.empty()) {
|
||||||
|
scale_out = vec3::lerp(scales[k0], scales[k1], blend);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AnimationClip Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void AnimationClip::sample(float time, size_t num_bones,
|
||||||
|
const std::vector<mat4>& default_transforms,
|
||||||
|
std::vector<mat4>& local_out) const
|
||||||
|
{
|
||||||
|
// Initialize with default transforms
|
||||||
|
local_out.resize(num_bones);
|
||||||
|
for (size_t i = 0; i < num_bones && i < default_transforms.size(); i++) {
|
||||||
|
local_out[i] = default_transforms[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which components have been animated per bone
|
||||||
|
struct BoneAnimState {
|
||||||
|
vec3 translation = vec3(0, 0, 0);
|
||||||
|
quat rotation;
|
||||||
|
vec3 scale = vec3(1, 1, 1);
|
||||||
|
bool has_translation = false;
|
||||||
|
bool has_rotation = false;
|
||||||
|
bool has_scale = false;
|
||||||
|
};
|
||||||
|
std::vector<BoneAnimState> bone_states(num_bones);
|
||||||
|
|
||||||
|
// Sample all channels
|
||||||
|
for (const auto& channel : channels) {
|
||||||
|
if (channel.bone_index < 0 || channel.bone_index >= static_cast<int>(num_bones)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& state = bone_states[channel.bone_index];
|
||||||
|
vec3 trans_dummy, scale_dummy;
|
||||||
|
quat rot_dummy;
|
||||||
|
|
||||||
|
channel.sample(time, trans_dummy, rot_dummy, scale_dummy);
|
||||||
|
|
||||||
|
switch (channel.path) {
|
||||||
|
case AnimationChannel::Path::Translation:
|
||||||
|
state.translation = trans_dummy;
|
||||||
|
state.has_translation = true;
|
||||||
|
break;
|
||||||
|
case AnimationChannel::Path::Rotation:
|
||||||
|
state.rotation = rot_dummy;
|
||||||
|
state.has_rotation = true;
|
||||||
|
break;
|
||||||
|
case AnimationChannel::Path::Scale:
|
||||||
|
state.scale = scale_dummy;
|
||||||
|
state.has_scale = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final local transforms for animated bones
|
||||||
|
for (size_t i = 0; i < num_bones; i++) {
|
||||||
|
const auto& state = bone_states[i];
|
||||||
|
|
||||||
|
// Only rebuild if at least one component was animated
|
||||||
|
if (state.has_translation || state.has_rotation || state.has_scale) {
|
||||||
|
// Extract default values from default transform if not animated
|
||||||
|
// (simplified: assume default is identity or use stored values)
|
||||||
|
vec3 t = state.has_translation ? state.translation : vec3(0, 0, 0);
|
||||||
|
quat r = state.has_rotation ? state.rotation : quat();
|
||||||
|
vec3 s = state.has_scale ? state.scale : vec3(1, 1, 1);
|
||||||
|
|
||||||
|
// If not fully animated, try to extract from default transform
|
||||||
|
if (!state.has_translation || !state.has_rotation || !state.has_scale) {
|
||||||
|
// For now, assume default transform contains the rest pose
|
||||||
|
// A more complete implementation would decompose default_transforms[i]
|
||||||
|
if (!state.has_translation) {
|
||||||
|
t = vec3(default_transforms[i].at(3, 0),
|
||||||
|
default_transforms[i].at(3, 1),
|
||||||
|
default_transforms[i].at(3, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose: T * R * S
|
||||||
|
local_out[i] = mat4::translate(t) * r.toMatrix() * mat4::scale(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Model3D Implementation
|
// Model3D Implementation
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -77,9 +247,13 @@ Model3D::~Model3D()
|
||||||
Model3D::Model3D(Model3D&& other) noexcept
|
Model3D::Model3D(Model3D&& other) noexcept
|
||||||
: name_(std::move(other.name_))
|
: name_(std::move(other.name_))
|
||||||
, meshes_(std::move(other.meshes_))
|
, meshes_(std::move(other.meshes_))
|
||||||
|
, skinned_meshes_(std::move(other.skinned_meshes_))
|
||||||
, bounds_min_(other.bounds_min_)
|
, bounds_min_(other.bounds_min_)
|
||||||
, bounds_max_(other.bounds_max_)
|
, bounds_max_(other.bounds_max_)
|
||||||
, has_skeleton_(other.has_skeleton_)
|
, has_skeleton_(other.has_skeleton_)
|
||||||
|
, skeleton_(std::move(other.skeleton_))
|
||||||
|
, animation_clips_(std::move(other.animation_clips_))
|
||||||
|
, default_bone_transforms_(std::move(other.default_bone_transforms_))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,9 +263,13 @@ Model3D& Model3D::operator=(Model3D&& other) noexcept
|
||||||
cleanupGPU();
|
cleanupGPU();
|
||||||
name_ = std::move(other.name_);
|
name_ = std::move(other.name_);
|
||||||
meshes_ = std::move(other.meshes_);
|
meshes_ = std::move(other.meshes_);
|
||||||
|
skinned_meshes_ = std::move(other.skinned_meshes_);
|
||||||
bounds_min_ = other.bounds_min_;
|
bounds_min_ = other.bounds_min_;
|
||||||
bounds_max_ = other.bounds_max_;
|
bounds_max_ = other.bounds_max_;
|
||||||
has_skeleton_ = other.has_skeleton_;
|
has_skeleton_ = other.has_skeleton_;
|
||||||
|
skeleton_ = std::move(other.skeleton_);
|
||||||
|
animation_clips_ = std::move(other.animation_clips_);
|
||||||
|
default_bone_transforms_ = std::move(other.default_bone_transforms_);
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +288,20 @@ void Model3D::cleanupGPU()
|
||||||
mesh.ebo = 0;
|
mesh.ebo = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (auto& mesh : skinned_meshes_) {
|
||||||
|
if (mesh.vbo) {
|
||||||
|
glDeleteBuffers(1, &mesh.vbo);
|
||||||
|
mesh.vbo = 0;
|
||||||
|
}
|
||||||
|
if (mesh.ebo) {
|
||||||
|
glDeleteBuffers(1, &mesh.ebo);
|
||||||
|
mesh.ebo = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
meshes_.clear();
|
meshes_.clear();
|
||||||
|
skinned_meshes_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Model3D::computeBounds(const std::vector<MeshVertex>& vertices)
|
void Model3D::computeBounds(const std::vector<MeshVertex>& vertices)
|
||||||
|
|
@ -184,6 +373,9 @@ int Model3D::getVertexCount() const
|
||||||
for (const auto& mesh : meshes_) {
|
for (const auto& mesh : meshes_) {
|
||||||
total += mesh.vertex_count;
|
total += mesh.vertex_count;
|
||||||
}
|
}
|
||||||
|
for (const auto& mesh : skinned_meshes_) {
|
||||||
|
total += mesh.vertex_count;
|
||||||
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,6 +389,13 @@ int Model3D::getTriangleCount() const
|
||||||
total += mesh.vertex_count / 3;
|
total += mesh.vertex_count / 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const auto& mesh : skinned_meshes_) {
|
||||||
|
if (mesh.index_count > 0) {
|
||||||
|
total += mesh.index_count / 3;
|
||||||
|
} else {
|
||||||
|
total += mesh.vertex_count / 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,6 +711,8 @@ std::shared_ptr<Model3D> Model3D::load(const std::string& path)
|
||||||
std::vector<vec3> normals;
|
std::vector<vec3> normals;
|
||||||
std::vector<vec2> texcoords;
|
std::vector<vec2> texcoords;
|
||||||
std::vector<vec4> colors;
|
std::vector<vec4> colors;
|
||||||
|
std::vector<vec4> joints; // Bone indices (as floats for shader compatibility)
|
||||||
|
std::vector<vec4> weights; // Bone weights
|
||||||
|
|
||||||
// Extract attributes
|
// Extract attributes
|
||||||
for (size_t k = 0; k < prim->attributes_count; ++k) {
|
for (size_t k = 0; k < prim->attributes_count; ++k) {
|
||||||
|
|
@ -548,6 +749,26 @@ std::shared_ptr<Model3D> Model3D::load(const std::string& path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (attr->type == cgltf_attribute_type_joints && attr->index == 0) {
|
||||||
|
// Bone indices - can be unsigned byte or unsigned short
|
||||||
|
joints.resize(accessor->count);
|
||||||
|
for (size_t v = 0; v < accessor->count; ++v) {
|
||||||
|
// Read as uint then convert to float for shader compatibility
|
||||||
|
cgltf_uint indices[4] = {0, 0, 0, 0};
|
||||||
|
cgltf_accessor_read_uint(accessor, v, indices, 4);
|
||||||
|
joints[v].x = static_cast<float>(indices[0]);
|
||||||
|
joints[v].y = static_cast<float>(indices[1]);
|
||||||
|
joints[v].z = static_cast<float>(indices[2]);
|
||||||
|
joints[v].w = static_cast<float>(indices[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attr->type == cgltf_attribute_type_weights && attr->index == 0) {
|
||||||
|
// Bone weights
|
||||||
|
weights.resize(accessor->count);
|
||||||
|
for (size_t v = 0; v < accessor->count; ++v) {
|
||||||
|
cgltf_accessor_read_float(accessor, v, &weights[v].x, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if no positions
|
// Skip if no positions
|
||||||
|
|
@ -567,7 +788,44 @@ std::shared_ptr<Model3D> Model3D::load(const std::string& path)
|
||||||
colors.resize(vertCount, vec4(1, 1, 1, 1));
|
colors.resize(vertCount, vec4(1, 1, 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interleave vertex data
|
// Extract indices
|
||||||
|
std::vector<uint32_t> indices;
|
||||||
|
if (prim->indices) {
|
||||||
|
cgltf_accessor* accessor = prim->indices;
|
||||||
|
indices.resize(accessor->count);
|
||||||
|
for (size_t idx = 0; idx < accessor->count; ++idx) {
|
||||||
|
indices[idx] = static_cast<uint32_t>(cgltf_accessor_read_index(accessor, idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a skinned mesh (has joints and weights)
|
||||||
|
bool isSkinned = !joints.empty() && !weights.empty() && model->has_skeleton_;
|
||||||
|
|
||||||
|
if (isSkinned) {
|
||||||
|
// Create skinned mesh with bone data
|
||||||
|
std::vector<SkinnedVertex> skinnedVertices;
|
||||||
|
skinnedVertices.reserve(vertCount);
|
||||||
|
for (size_t v = 0; v < vertCount; ++v) {
|
||||||
|
SkinnedVertex sv;
|
||||||
|
sv.position = positions[v];
|
||||||
|
sv.texcoord = texcoords[v];
|
||||||
|
sv.normal = normals[v];
|
||||||
|
sv.color = colors[v];
|
||||||
|
sv.bone_ids = joints[v];
|
||||||
|
sv.bone_weights = weights[v];
|
||||||
|
skinnedVertices.push_back(sv);
|
||||||
|
|
||||||
|
// Also track for bounds calculation
|
||||||
|
MeshVertex mv;
|
||||||
|
mv.position = positions[v];
|
||||||
|
mv.texcoord = texcoords[v];
|
||||||
|
mv.normal = normals[v];
|
||||||
|
mv.color = colors[v];
|
||||||
|
allVertices.push_back(mv);
|
||||||
|
}
|
||||||
|
model->skinned_meshes_.push_back(model->createSkinnedMesh(skinnedVertices, indices));
|
||||||
|
} else {
|
||||||
|
// Interleave vertex data for regular mesh
|
||||||
std::vector<MeshVertex> vertices;
|
std::vector<MeshVertex> vertices;
|
||||||
vertices.reserve(vertCount);
|
vertices.reserve(vertCount);
|
||||||
for (size_t v = 0; v < vertCount; ++v) {
|
for (size_t v = 0; v < vertCount; ++v) {
|
||||||
|
|
@ -579,29 +837,383 @@ std::shared_ptr<Model3D> Model3D::load(const std::string& path)
|
||||||
vertices.push_back(mv);
|
vertices.push_back(mv);
|
||||||
allVertices.push_back(mv);
|
allVertices.push_back(mv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract indices
|
|
||||||
std::vector<uint32_t> indices;
|
|
||||||
if (prim->indices) {
|
|
||||||
cgltf_accessor* accessor = prim->indices;
|
|
||||||
indices.resize(accessor->count);
|
|
||||||
for (size_t idx = 0; idx < accessor->count; ++idx) {
|
|
||||||
indices[idx] = static_cast<uint32_t>(cgltf_accessor_read_index(accessor, idx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create mesh
|
|
||||||
model->meshes_.push_back(createMesh(vertices, indices));
|
model->meshes_.push_back(createMesh(vertices, indices));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compute bounds from all vertices
|
// Compute bounds from all vertices
|
||||||
model->computeBounds(allVertices);
|
model->computeBounds(allVertices);
|
||||||
|
|
||||||
|
// Load skeleton and animations if present
|
||||||
|
if (model->has_skeleton_) {
|
||||||
|
model->loadSkeleton(data);
|
||||||
|
model->loadAnimations(data);
|
||||||
|
}
|
||||||
|
|
||||||
cgltf_free(data);
|
cgltf_free(data);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Skeleton Loading from glTF
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
int Model3D::findJointIndex(void* cgltf_skin_ptr, void* node_ptr)
|
||||||
|
{
|
||||||
|
cgltf_skin* skin = static_cast<cgltf_skin*>(cgltf_skin_ptr);
|
||||||
|
cgltf_node* node = static_cast<cgltf_node*>(node_ptr);
|
||||||
|
|
||||||
|
if (!skin || !node) return -1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < skin->joints_count; i++) {
|
||||||
|
if (skin->joints[i] == node) {
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Model3D::loadSkeleton(void* cgltf_data_ptr)
|
||||||
|
{
|
||||||
|
cgltf_data* data = static_cast<cgltf_data*>(cgltf_data_ptr);
|
||||||
|
if (!data || data->skins_count == 0) {
|
||||||
|
has_skeleton_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cgltf_skin* skin = &data->skins[0]; // Use first skin
|
||||||
|
|
||||||
|
// Resize skeleton
|
||||||
|
skeleton_.bones.resize(skin->joints_count);
|
||||||
|
default_bone_transforms_.resize(skin->joints_count);
|
||||||
|
|
||||||
|
// Load inverse bind matrices
|
||||||
|
if (skin->inverse_bind_matrices) {
|
||||||
|
cgltf_accessor* ibm = skin->inverse_bind_matrices;
|
||||||
|
for (size_t i = 0; i < skin->joints_count && i < ibm->count; i++) {
|
||||||
|
float mat_data[16];
|
||||||
|
cgltf_accessor_read_float(ibm, i, mat_data, 16);
|
||||||
|
|
||||||
|
// cgltf gives us column-major matrices (same as our mat4)
|
||||||
|
for (int j = 0; j < 16; j++) {
|
||||||
|
skeleton_.bones[i].inverse_bind_matrix.m[j] = mat_data[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load bone hierarchy
|
||||||
|
for (size_t i = 0; i < skin->joints_count; i++) {
|
||||||
|
cgltf_node* joint = skin->joints[i];
|
||||||
|
Bone& bone = skeleton_.bones[i];
|
||||||
|
|
||||||
|
// Name
|
||||||
|
bone.name = joint->name ? joint->name : ("bone_" + std::to_string(i));
|
||||||
|
|
||||||
|
// Find parent index
|
||||||
|
bone.parent_index = findJointIndex(skin, joint->parent);
|
||||||
|
|
||||||
|
// Track root bones
|
||||||
|
if (bone.parent_index < 0) {
|
||||||
|
skeleton_.root_bones.push_back(static_cast<int>(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local transform
|
||||||
|
if (joint->has_matrix) {
|
||||||
|
for (int j = 0; j < 16; j++) {
|
||||||
|
bone.local_transform.m[j] = joint->matrix[j];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Compose from TRS
|
||||||
|
vec3 t(0, 0, 0);
|
||||||
|
quat r;
|
||||||
|
vec3 s(1, 1, 1);
|
||||||
|
|
||||||
|
if (joint->has_translation) {
|
||||||
|
t = vec3(joint->translation[0], joint->translation[1], joint->translation[2]);
|
||||||
|
}
|
||||||
|
if (joint->has_rotation) {
|
||||||
|
r = quat(joint->rotation[0], joint->rotation[1],
|
||||||
|
joint->rotation[2], joint->rotation[3]);
|
||||||
|
}
|
||||||
|
if (joint->has_scale) {
|
||||||
|
s = vec3(joint->scale[0], joint->scale[1], joint->scale[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bone.local_transform = mat4::translate(t) * r.toMatrix() * mat4::scale(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
default_bone_transforms_[i] = bone.local_transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
has_skeleton_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Animation Loading from glTF
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Model3D::loadAnimations(void* cgltf_data_ptr)
|
||||||
|
{
|
||||||
|
cgltf_data* data = static_cast<cgltf_data*>(cgltf_data_ptr);
|
||||||
|
if (!data || data->skins_count == 0) return;
|
||||||
|
|
||||||
|
cgltf_skin* skin = &data->skins[0];
|
||||||
|
|
||||||
|
for (size_t i = 0; i < data->animations_count; i++) {
|
||||||
|
cgltf_animation* anim = &data->animations[i];
|
||||||
|
|
||||||
|
AnimationClip clip;
|
||||||
|
clip.name = anim->name ? anim->name : ("animation_" + std::to_string(i));
|
||||||
|
clip.duration = 0.0f;
|
||||||
|
|
||||||
|
for (size_t j = 0; j < anim->channels_count; j++) {
|
||||||
|
cgltf_animation_channel* chan = &anim->channels[j];
|
||||||
|
cgltf_animation_sampler* sampler = chan->sampler;
|
||||||
|
|
||||||
|
if (!sampler || !chan->target_node) continue;
|
||||||
|
|
||||||
|
AnimationChannel channel;
|
||||||
|
channel.bone_index = findJointIndex(skin, chan->target_node);
|
||||||
|
|
||||||
|
if (channel.bone_index < 0) continue; // Not a bone we're tracking
|
||||||
|
|
||||||
|
// Determine path type
|
||||||
|
switch (chan->target_path) {
|
||||||
|
case cgltf_animation_path_type_translation:
|
||||||
|
channel.path = AnimationChannel::Path::Translation;
|
||||||
|
break;
|
||||||
|
case cgltf_animation_path_type_rotation:
|
||||||
|
channel.path = AnimationChannel::Path::Rotation;
|
||||||
|
break;
|
||||||
|
case cgltf_animation_path_type_scale:
|
||||||
|
channel.path = AnimationChannel::Path::Scale;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue; // Skip unsupported paths (weights, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load keyframe times
|
||||||
|
cgltf_accessor* input = sampler->input;
|
||||||
|
if (input) {
|
||||||
|
channel.times.resize(input->count);
|
||||||
|
for (size_t k = 0; k < input->count; k++) {
|
||||||
|
cgltf_accessor_read_float(input, k, &channel.times[k], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update clip duration
|
||||||
|
if (!channel.times.empty() && channel.times.back() > clip.duration) {
|
||||||
|
clip.duration = channel.times.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load keyframe values
|
||||||
|
cgltf_accessor* output = sampler->output;
|
||||||
|
if (output) {
|
||||||
|
switch (channel.path) {
|
||||||
|
case AnimationChannel::Path::Translation:
|
||||||
|
case AnimationChannel::Path::Scale:
|
||||||
|
{
|
||||||
|
std::vector<vec3>& target = (channel.path == AnimationChannel::Path::Translation)
|
||||||
|
? channel.translations : channel.scales;
|
||||||
|
target.resize(output->count);
|
||||||
|
for (size_t k = 0; k < output->count; k++) {
|
||||||
|
float v[3];
|
||||||
|
cgltf_accessor_read_float(output, k, v, 3);
|
||||||
|
target[k] = vec3(v[0], v[1], v[2]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AnimationChannel::Path::Rotation:
|
||||||
|
{
|
||||||
|
channel.rotations.resize(output->count);
|
||||||
|
for (size_t k = 0; k < output->count; k++) {
|
||||||
|
float v[4];
|
||||||
|
cgltf_accessor_read_float(output, k, v, 4);
|
||||||
|
// glTF stores quaternions as (x, y, z, w)
|
||||||
|
channel.rotations[k] = quat(v[0], v[1], v[2], v[3]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clip.channels.push_back(std::move(channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clip.channels.empty()) {
|
||||||
|
animation_clips_.push_back(std::move(clip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Skinned Mesh Creation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
SkinnedMesh Model3D::createSkinnedMesh(const std::vector<SkinnedVertex>& vertices,
|
||||||
|
const std::vector<uint32_t>& indices)
|
||||||
|
{
|
||||||
|
SkinnedMesh mesh;
|
||||||
|
mesh.vertex_count = static_cast<int>(vertices.size());
|
||||||
|
mesh.index_count = static_cast<int>(indices.size());
|
||||||
|
mesh.is_skinned = true;
|
||||||
|
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (!gl::isGLReady()) {
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VBO
|
||||||
|
glGenBuffers(1, &mesh.vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER,
|
||||||
|
vertices.size() * sizeof(SkinnedVertex),
|
||||||
|
vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
// Create EBO if indexed
|
||||||
|
if (!indices.empty()) {
|
||||||
|
glGenBuffers(1, &mesh.ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
||||||
|
indices.size() * sizeof(uint32_t),
|
||||||
|
indices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Skinned Rendering
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Model3D::renderSkinned(unsigned int shader, const mat4& model,
|
||||||
|
const mat4& view, const mat4& projection,
|
||||||
|
const std::vector<mat4>& bone_matrices)
|
||||||
|
{
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (!gl::isGLReady()) return;
|
||||||
|
|
||||||
|
// Calculate MVP
|
||||||
|
mat4 mvp = projection * view * model;
|
||||||
|
|
||||||
|
// Set uniforms
|
||||||
|
int mvpLoc = glGetUniformLocation(shader, "u_mvp");
|
||||||
|
int modelLoc = glGetUniformLocation(shader, "u_model");
|
||||||
|
int bonesLoc = glGetUniformLocation(shader, "u_bones");
|
||||||
|
|
||||||
|
if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data());
|
||||||
|
if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data());
|
||||||
|
|
||||||
|
// Upload bone matrices (max 64 bones)
|
||||||
|
if (bonesLoc >= 0 && !bone_matrices.empty()) {
|
||||||
|
int count = std::min(static_cast<int>(bone_matrices.size()), 64);
|
||||||
|
glUniformMatrix4fv(bonesLoc, count, GL_FALSE, bone_matrices[0].data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, fall back to regular rendering for non-skinned meshes
|
||||||
|
// TODO: Add skinned mesh rendering with bone weight attributes
|
||||||
|
|
||||||
|
// Render skinned meshes
|
||||||
|
for (const auto& mesh : skinned_meshes_) {
|
||||||
|
if (mesh.vertex_count == 0) continue;
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
|
||||||
|
|
||||||
|
// Position (location 0)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, position));
|
||||||
|
|
||||||
|
// Texcoord (location 1)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, texcoord));
|
||||||
|
|
||||||
|
// Normal (location 2)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, normal));
|
||||||
|
|
||||||
|
// Color (location 3)
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, color));
|
||||||
|
|
||||||
|
// Bone IDs (location 4) - as vec4 float for GLES2 compatibility
|
||||||
|
glEnableVertexAttribArray(4);
|
||||||
|
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, bone_ids));
|
||||||
|
|
||||||
|
// Bone Weights (location 5)
|
||||||
|
glEnableVertexAttribArray(5);
|
||||||
|
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(SkinnedVertex), (void*)offsetof(SkinnedVertex, bone_weights));
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
if (mesh.index_count > 0) {
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
|
||||||
|
glDrawElements(GL_TRIANGLES, mesh.index_count, GL_UNSIGNED_INT, 0);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
} else {
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, mesh.vertex_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
glDisableVertexAttribArray(4);
|
||||||
|
glDisableVertexAttribArray(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also render regular meshes (may not have skinning)
|
||||||
|
for (const auto& mesh : meshes_) {
|
||||||
|
if (mesh.vertex_count == 0) continue;
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, position));
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, texcoord));
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, normal));
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE,
|
||||||
|
sizeof(MeshVertex), (void*)offsetof(MeshVertex, color));
|
||||||
|
|
||||||
|
if (mesh.index_count > 0) {
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
|
||||||
|
glDrawElements(GL_TRIANGLES, mesh.index_count, GL_UNSIGNED_INT, 0);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
} else {
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, mesh.vertex_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Python API Implementation
|
// Python API Implementation
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -781,6 +1393,38 @@ PyObject* Model3D::get_mesh_count(PyObject* self, void* closure)
|
||||||
return PyLong_FromLong(static_cast<long>(obj->data->getMeshCount()));
|
return PyLong_FromLong(static_cast<long>(obj->data->getMeshCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_bone_count(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
return PyLong_FromLong(0);
|
||||||
|
}
|
||||||
|
return PyLong_FromLong(static_cast<long>(obj->data->getBoneCount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Model3D::get_animation_clips(PyObject* self, void* closure)
|
||||||
|
{
|
||||||
|
PyModel3DObject* obj = (PyModel3DObject*)self;
|
||||||
|
if (!obj->data) {
|
||||||
|
return PyList_New(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto names = obj->data->getAnimationClipNames();
|
||||||
|
PyObject* list = PyList_New(names.size());
|
||||||
|
if (!list) return NULL;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < names.size(); i++) {
|
||||||
|
PyObject* name = PyUnicode_FromString(names[i].c_str());
|
||||||
|
if (!name) {
|
||||||
|
Py_DECREF(list);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
PyList_SET_ITEM(list, i, name); // Steals reference
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
// Method and property tables
|
// Method and property tables
|
||||||
PyMethodDef Model3D::methods[] = {
|
PyMethodDef Model3D::methods[] = {
|
||||||
{"cube", (PyCFunction)py_cube, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
{"cube", (PyCFunction)py_cube, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
||||||
|
|
@ -799,6 +1443,8 @@ PyGetSetDef Model3D::getsetters[] = {
|
||||||
{"bounds", get_bounds, NULL, "AABB as ((min_x, min_y, min_z), (max_x, max_y, max_z)) (read-only)", NULL},
|
{"bounds", get_bounds, NULL, "AABB as ((min_x, min_y, min_z), (max_x, max_y, max_z)) (read-only)", NULL},
|
||||||
{"name", get_name, NULL, "Model name (read-only)", NULL},
|
{"name", get_name, NULL, "Model name (read-only)", NULL},
|
||||||
{"mesh_count", get_mesh_count, NULL, "Number of submeshes (read-only)", NULL},
|
{"mesh_count", get_mesh_count, NULL, "Number of submeshes (read-only)", NULL},
|
||||||
|
{"bone_count", get_bone_count, NULL, "Number of bones in skeleton (read-only)", NULL},
|
||||||
|
{"animation_clips", get_animation_clips, NULL, "List of animation clip names (read-only)", NULL},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
199
src/3d/Model3D.h
199
src/3d/Model3D.h
|
|
@ -18,7 +18,137 @@ namespace mcrf {
|
||||||
class Shader3D;
|
class Shader3D;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// ModelMesh - Single submesh within a Model3D
|
// Bone - Single bone in a skeleton
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct Bone {
|
||||||
|
std::string name;
|
||||||
|
int parent_index = -1; // -1 for root bones
|
||||||
|
mat4 inverse_bind_matrix; // Transforms from model space to bone space
|
||||||
|
mat4 local_transform; // Default local transform (rest pose)
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Skeleton - Bone hierarchy for skeletal animation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct Skeleton {
|
||||||
|
std::vector<Bone> bones;
|
||||||
|
std::vector<int> root_bones; // Indices of bones with parent_index == -1
|
||||||
|
|
||||||
|
/// Find bone by name, returns -1 if not found
|
||||||
|
int findBone(const std::string& name) const {
|
||||||
|
for (size_t i = 0; i < bones.size(); i++) {
|
||||||
|
if (bones[i].name == name) return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute global (model-space) transforms for all bones
|
||||||
|
void computeGlobalTransforms(const std::vector<mat4>& local_transforms,
|
||||||
|
std::vector<mat4>& global_out) const {
|
||||||
|
global_out.resize(bones.size());
|
||||||
|
for (size_t i = 0; i < bones.size(); i++) {
|
||||||
|
if (bones[i].parent_index < 0) {
|
||||||
|
global_out[i] = local_transforms[i];
|
||||||
|
} else {
|
||||||
|
global_out[i] = global_out[bones[i].parent_index] * local_transforms[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute final bone matrices for shader (global * inverse_bind)
|
||||||
|
void computeBoneMatrices(const std::vector<mat4>& global_transforms,
|
||||||
|
std::vector<mat4>& matrices_out) const {
|
||||||
|
matrices_out.resize(bones.size());
|
||||||
|
for (size_t i = 0; i < bones.size(); i++) {
|
||||||
|
matrices_out[i] = global_transforms[i] * bones[i].inverse_bind_matrix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AnimationChannel - Animates a single property of a single bone
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct AnimationChannel {
|
||||||
|
int bone_index = -1;
|
||||||
|
|
||||||
|
enum class Path {
|
||||||
|
Translation,
|
||||||
|
Rotation,
|
||||||
|
Scale
|
||||||
|
} path = Path::Translation;
|
||||||
|
|
||||||
|
// Keyframe times (shared for all values in this channel)
|
||||||
|
std::vector<float> times;
|
||||||
|
|
||||||
|
// Keyframe values (only one of these is populated based on path)
|
||||||
|
std::vector<vec3> translations;
|
||||||
|
std::vector<quat> rotations;
|
||||||
|
std::vector<vec3> scales;
|
||||||
|
|
||||||
|
/// Sample the channel at a given time, returning the interpolated transform component
|
||||||
|
/// For Translation/Scale: writes to trans_out
|
||||||
|
/// For Rotation: writes to rot_out
|
||||||
|
void sample(float time, vec3& trans_out, quat& rot_out, vec3& scale_out) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AnimationClip - Named animation containing multiple channels
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct AnimationClip {
|
||||||
|
std::string name;
|
||||||
|
float duration = 0.0f;
|
||||||
|
std::vector<AnimationChannel> channels;
|
||||||
|
|
||||||
|
/// Sample the animation at a given time, producing bone local transforms
|
||||||
|
/// @param time Current time in the animation
|
||||||
|
/// @param num_bones Total number of bones (for output sizing)
|
||||||
|
/// @param default_transforms Default local transforms for bones without animation
|
||||||
|
/// @param local_out Output: interpolated local transforms for each bone
|
||||||
|
void sample(float time, size_t num_bones,
|
||||||
|
const std::vector<mat4>& default_transforms,
|
||||||
|
std::vector<mat4>& local_out) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SkinnedVertex - Vertex with bone weights for skeletal animation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct SkinnedVertex {
|
||||||
|
vec3 position;
|
||||||
|
vec2 texcoord;
|
||||||
|
vec3 normal;
|
||||||
|
vec4 color;
|
||||||
|
vec4 bone_ids; // Up to 4 bone indices (as floats for GLES2 compatibility)
|
||||||
|
vec4 bone_weights; // Corresponding weights (should sum to 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SkinnedMesh - Submesh with skinning data
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct SkinnedMesh {
|
||||||
|
unsigned int vbo = 0;
|
||||||
|
unsigned int ebo = 0;
|
||||||
|
int vertex_count = 0;
|
||||||
|
int index_count = 0;
|
||||||
|
int material_index = -1;
|
||||||
|
bool is_skinned = false; // True if this mesh has bone weights
|
||||||
|
|
||||||
|
SkinnedMesh() = default;
|
||||||
|
~SkinnedMesh() = default;
|
||||||
|
|
||||||
|
SkinnedMesh(const SkinnedMesh&) = delete;
|
||||||
|
SkinnedMesh& operator=(const SkinnedMesh&) = delete;
|
||||||
|
SkinnedMesh(SkinnedMesh&& other) noexcept;
|
||||||
|
SkinnedMesh& operator=(SkinnedMesh&& other) noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ModelMesh - Single submesh within a Model3D (legacy non-skinned)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
struct ModelMesh {
|
struct ModelMesh {
|
||||||
|
|
@ -109,8 +239,41 @@ public:
|
||||||
/// Check if model has skeletal animation data
|
/// Check if model has skeletal animation data
|
||||||
bool hasSkeleton() const { return has_skeleton_; }
|
bool hasSkeleton() const { return has_skeleton_; }
|
||||||
|
|
||||||
/// Get number of submeshes
|
/// Get number of submeshes (regular + skinned)
|
||||||
size_t getMeshCount() const { return meshes_.size(); }
|
size_t getMeshCount() const { return meshes_.size() + skinned_meshes_.size(); }
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Skeleton & Animation
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Get skeleton (may be empty if no skeleton)
|
||||||
|
const Skeleton& getSkeleton() const { return skeleton_; }
|
||||||
|
|
||||||
|
/// Get number of bones
|
||||||
|
size_t getBoneCount() const { return skeleton_.bones.size(); }
|
||||||
|
|
||||||
|
/// Get animation clips
|
||||||
|
const std::vector<AnimationClip>& getAnimationClips() const { return animation_clips_; }
|
||||||
|
|
||||||
|
/// Get animation clip names
|
||||||
|
std::vector<std::string> getAnimationClipNames() const {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const auto& clip : animation_clips_) {
|
||||||
|
names.push_back(clip.name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find animation clip by name (returns nullptr if not found)
|
||||||
|
const AnimationClip* findClip(const std::string& name) const {
|
||||||
|
for (const auto& clip : animation_clips_) {
|
||||||
|
if (clip.name == name) return &clip;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get default bone transforms (rest pose)
|
||||||
|
const std::vector<mat4>& getDefaultBoneTransforms() const { return default_bone_transforms_; }
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Rendering
|
// Rendering
|
||||||
|
|
@ -123,6 +286,15 @@ public:
|
||||||
/// @param projection Projection matrix
|
/// @param projection Projection matrix
|
||||||
void render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection);
|
void render(unsigned int shader, const mat4& model, const mat4& view, const mat4& projection);
|
||||||
|
|
||||||
|
/// Render with skeletal animation
|
||||||
|
/// @param shader Shader program handle (already bound, should be skinned shader)
|
||||||
|
/// @param model Model transformation matrix
|
||||||
|
/// @param view View matrix
|
||||||
|
/// @param projection Projection matrix
|
||||||
|
/// @param bone_matrices Final bone matrices (global * inverse_bind)
|
||||||
|
void renderSkinned(unsigned int shader, const mat4& model, const mat4& view,
|
||||||
|
const mat4& projection, const std::vector<mat4>& bone_matrices);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Python API
|
// Python API
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -142,6 +314,8 @@ public:
|
||||||
static PyObject* get_bounds(PyObject* self, void* closure);
|
static PyObject* get_bounds(PyObject* self, void* closure);
|
||||||
static PyObject* get_name(PyObject* self, void* closure);
|
static PyObject* get_name(PyObject* self, void* closure);
|
||||||
static PyObject* get_mesh_count(PyObject* self, void* closure);
|
static PyObject* get_mesh_count(PyObject* self, void* closure);
|
||||||
|
static PyObject* get_bone_count(PyObject* self, void* closure);
|
||||||
|
static PyObject* get_animation_clips(PyObject* self, void* closure);
|
||||||
|
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
|
|
@ -150,13 +324,17 @@ private:
|
||||||
// Model data
|
// Model data
|
||||||
std::string name_;
|
std::string name_;
|
||||||
std::vector<ModelMesh> meshes_;
|
std::vector<ModelMesh> meshes_;
|
||||||
|
std::vector<SkinnedMesh> skinned_meshes_; // Skinned meshes with bone weights
|
||||||
|
|
||||||
// Bounds
|
// Bounds
|
||||||
vec3 bounds_min_ = vec3(0, 0, 0);
|
vec3 bounds_min_ = vec3(0, 0, 0);
|
||||||
vec3 bounds_max_ = vec3(0, 0, 0);
|
vec3 bounds_max_ = vec3(0, 0, 0);
|
||||||
|
|
||||||
// Future: skeletal animation data
|
// Skeletal animation data
|
||||||
bool has_skeleton_ = false;
|
bool has_skeleton_ = false;
|
||||||
|
Skeleton skeleton_;
|
||||||
|
std::vector<AnimationClip> animation_clips_;
|
||||||
|
std::vector<mat4> default_bone_transforms_; // Rest pose local transforms
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
static std::string lastError_;
|
static std::string lastError_;
|
||||||
|
|
@ -169,6 +347,15 @@ private:
|
||||||
/// @return ModelMesh with GPU resources allocated
|
/// @return ModelMesh with GPU resources allocated
|
||||||
static ModelMesh createMesh(const std::vector<MeshVertex>& vertices,
|
static ModelMesh createMesh(const std::vector<MeshVertex>& vertices,
|
||||||
const std::vector<uint32_t>& indices);
|
const std::vector<uint32_t>& indices);
|
||||||
|
|
||||||
|
/// Create VBO/EBO from skinned vertex and index data
|
||||||
|
static SkinnedMesh createSkinnedMesh(const std::vector<SkinnedVertex>& vertices,
|
||||||
|
const std::vector<uint32_t>& indices);
|
||||||
|
|
||||||
|
// glTF loading helpers
|
||||||
|
void loadSkeleton(void* cgltf_data); // void* to avoid header dependency
|
||||||
|
void loadAnimations(void* cgltf_data);
|
||||||
|
int findJointIndex(void* cgltf_skin, void* node);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace mcrf
|
} // namespace mcrf
|
||||||
|
|
@ -217,7 +404,9 @@ inline PyTypeObject PyModel3DType = {
|
||||||
" triangle_count (int, read-only): Total triangles across all meshes\n"
|
" triangle_count (int, read-only): Total triangles across all meshes\n"
|
||||||
" has_skeleton (bool, read-only): Whether model has skeletal animation data\n"
|
" has_skeleton (bool, read-only): Whether model has skeletal animation data\n"
|
||||||
" bounds (tuple, read-only): AABB as ((min_x, min_y, min_z), (max_x, max_y, max_z))\n"
|
" bounds (tuple, read-only): AABB as ((min_x, min_y, min_z), (max_x, max_y, max_z))\n"
|
||||||
" mesh_count (int, read-only): Number of submeshes"
|
" mesh_count (int, read-only): Number of submeshes\n"
|
||||||
|
" bone_count (int, read-only): Number of bones in skeleton\n"
|
||||||
|
" animation_clips (list, read-only): List of animation clip names"
|
||||||
),
|
),
|
||||||
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,223 @@ void main() {
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Skinned Vertex Shaders (for skeletal animation)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const char* PS1_SKINNED_VERTEX_ES2 = R"(
|
||||||
|
// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
uniform mat4 u_bones[32];
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform bool u_enable_snap;
|
||||||
|
uniform float u_fog_start;
|
||||||
|
uniform float u_fog_end;
|
||||||
|
uniform vec3 u_light_dir;
|
||||||
|
uniform vec3 u_ambient;
|
||||||
|
|
||||||
|
attribute vec3 a_position;
|
||||||
|
attribute vec2 a_texcoord;
|
||||||
|
attribute vec3 a_normal;
|
||||||
|
attribute vec4 a_color;
|
||||||
|
attribute vec4 a_bone_ids;
|
||||||
|
attribute vec4 a_bone_weights;
|
||||||
|
|
||||||
|
varying vec4 v_color;
|
||||||
|
varying vec2 v_texcoord;
|
||||||
|
varying float v_w;
|
||||||
|
varying float v_fog;
|
||||||
|
|
||||||
|
mat4 getBoneMatrix(int index) {
|
||||||
|
if (index < 8) {
|
||||||
|
if (index < 4) {
|
||||||
|
if (index < 2) {
|
||||||
|
if (index == 0) return u_bones[0];
|
||||||
|
else return u_bones[1];
|
||||||
|
} else {
|
||||||
|
if (index == 2) return u_bones[2];
|
||||||
|
else return u_bones[3];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 6) {
|
||||||
|
if (index == 4) return u_bones[4];
|
||||||
|
else return u_bones[5];
|
||||||
|
} else {
|
||||||
|
if (index == 6) return u_bones[6];
|
||||||
|
else return u_bones[7];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (index < 16) {
|
||||||
|
if (index < 12) {
|
||||||
|
if (index < 10) {
|
||||||
|
if (index == 8) return u_bones[8];
|
||||||
|
else return u_bones[9];
|
||||||
|
} else {
|
||||||
|
if (index == 10) return u_bones[10];
|
||||||
|
else return u_bones[11];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 14) {
|
||||||
|
if (index == 12) return u_bones[12];
|
||||||
|
else return u_bones[13];
|
||||||
|
} else {
|
||||||
|
if (index == 14) return u_bones[14];
|
||||||
|
else return u_bones[15];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (index < 24) {
|
||||||
|
if (index < 20) {
|
||||||
|
if (index < 18) {
|
||||||
|
if (index == 16) return u_bones[16];
|
||||||
|
else return u_bones[17];
|
||||||
|
} else {
|
||||||
|
if (index == 18) return u_bones[18];
|
||||||
|
else return u_bones[19];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 22) {
|
||||||
|
if (index == 20) return u_bones[20];
|
||||||
|
else return u_bones[21];
|
||||||
|
} else {
|
||||||
|
if (index == 22) return u_bones[22];
|
||||||
|
else return u_bones[23];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 28) {
|
||||||
|
if (index < 26) {
|
||||||
|
if (index == 24) return u_bones[24];
|
||||||
|
else return u_bones[25];
|
||||||
|
} else {
|
||||||
|
if (index == 26) return u_bones[26];
|
||||||
|
else return u_bones[27];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 30) {
|
||||||
|
if (index == 28) return u_bones[28];
|
||||||
|
else return u_bones[29];
|
||||||
|
} else {
|
||||||
|
if (index == 30) return u_bones[30];
|
||||||
|
else return u_bones[31];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mat4(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int b0 = int(a_bone_ids.x);
|
||||||
|
int b1 = int(a_bone_ids.y);
|
||||||
|
int b2 = int(a_bone_ids.z);
|
||||||
|
int b3 = int(a_bone_ids.w);
|
||||||
|
|
||||||
|
mat4 skin_matrix =
|
||||||
|
getBoneMatrix(b0) * a_bone_weights.x +
|
||||||
|
getBoneMatrix(b1) * a_bone_weights.y +
|
||||||
|
getBoneMatrix(b2) * a_bone_weights.z +
|
||||||
|
getBoneMatrix(b3) * a_bone_weights.w;
|
||||||
|
|
||||||
|
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
|
||||||
|
vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal;
|
||||||
|
|
||||||
|
vec4 worldPos = u_model * skinned_pos;
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
if (u_enable_snap) {
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal;
|
||||||
|
worldNormal = normalize(worldNormal);
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
|
||||||
|
v_texcoord = a_texcoord * clipPos.w;
|
||||||
|
v_w = clipPos.w;
|
||||||
|
|
||||||
|
float depth = -viewPos.z;
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* PS1_SKINNED_VERTEX = R"(
|
||||||
|
#version 150 core
|
||||||
|
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
uniform mat4 u_bones[64];
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform bool u_enable_snap;
|
||||||
|
uniform float u_fog_start;
|
||||||
|
uniform float u_fog_end;
|
||||||
|
uniform vec3 u_light_dir;
|
||||||
|
uniform vec3 u_ambient;
|
||||||
|
|
||||||
|
in vec3 a_position;
|
||||||
|
in vec2 a_texcoord;
|
||||||
|
in vec3 a_normal;
|
||||||
|
in vec4 a_color;
|
||||||
|
in vec4 a_bone_ids;
|
||||||
|
in vec4 a_bone_weights;
|
||||||
|
|
||||||
|
out vec4 v_color;
|
||||||
|
noperspective out vec2 v_texcoord;
|
||||||
|
out float v_fog;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec4 bone_ids = ivec4(a_bone_ids);
|
||||||
|
|
||||||
|
mat4 skin_matrix =
|
||||||
|
u_bones[bone_ids.x] * a_bone_weights.x +
|
||||||
|
u_bones[bone_ids.y] * a_bone_weights.y +
|
||||||
|
u_bones[bone_ids.z] * a_bone_weights.z +
|
||||||
|
u_bones[bone_ids.w] * a_bone_weights.w;
|
||||||
|
|
||||||
|
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
|
||||||
|
vec3 skinned_normal = mat3(skin_matrix) * a_normal;
|
||||||
|
|
||||||
|
vec4 worldPos = u_model * skinned_pos;
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
if (u_enable_snap) {
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
vec3 worldNormal = mat3(u_model) * skinned_normal;
|
||||||
|
worldNormal = normalize(worldNormal);
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
|
||||||
|
v_texcoord = a_texcoord;
|
||||||
|
|
||||||
|
float depth = -viewPos.z;
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
} // namespace shaders
|
} // namespace shaders
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -274,6 +491,20 @@ bool Shader3D::loadPS1Shaders() {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Shader3D::loadPS1SkinnedShaders() {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// Use GLES2 skinned shaders for Emscripten/WebGL
|
||||||
|
return load(shaders::PS1_SKINNED_VERTEX_ES2, shaders::PS1_FRAGMENT_ES2);
|
||||||
|
#else
|
||||||
|
// Use desktop GL 3.2+ skinned shaders
|
||||||
|
return load(shaders::PS1_SKINNED_VERTEX, shaders::PS1_FRAGMENT);
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
bool Shader3D::load(const char* vertexSource, const char* fragmentSource) {
|
bool Shader3D::load(const char* vertexSource, const char* fragmentSource) {
|
||||||
if (!gl::isGLReady()) {
|
if (!gl::isGLReady()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ public:
|
||||||
// Automatically selects desktop vs ES2 shaders based on platform
|
// Automatically selects desktop vs ES2 shaders based on platform
|
||||||
bool loadPS1Shaders();
|
bool loadPS1Shaders();
|
||||||
|
|
||||||
|
// Load skinned (skeletal animation) shaders
|
||||||
|
bool loadPS1SkinnedShaders();
|
||||||
|
|
||||||
// Load from custom source strings
|
// Load from custom source strings
|
||||||
bool load(const char* vertexSource, const char* fragmentSource);
|
bool load(const char* vertexSource, const char* fragmentSource);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -454,16 +454,53 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) {
|
||||||
#ifdef MCRF_HAS_GL
|
#ifdef MCRF_HAS_GL
|
||||||
if (!entities_ || !shader_ || !shader_->isValid()) return;
|
if (!entities_ || !shader_ || !shader_->isValid()) return;
|
||||||
|
|
||||||
// Entity rendering uses the same shader as terrain
|
// Render non-skeletal entities first
|
||||||
shader_->bind();
|
shader_->bind();
|
||||||
|
|
||||||
for (auto& entity : *entities_) {
|
for (auto& entity : *entities_) {
|
||||||
if (entity && entity->isVisible()) {
|
if (entity && entity->isVisible()) {
|
||||||
|
auto model = entity->getModel();
|
||||||
|
if (!model || !model->hasSkeleton()) {
|
||||||
entity->render(view, proj, shader_->getProgram());
|
entity->render(view, proj, shader_->getProgram());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
shader_->unbind();
|
shader_->unbind();
|
||||||
|
|
||||||
|
// Then render skeletal entities with skinned shader
|
||||||
|
if (skinnedShader_ && skinnedShader_->isValid()) {
|
||||||
|
skinnedShader_->bind();
|
||||||
|
|
||||||
|
// Set up common uniforms for skinned shader
|
||||||
|
skinnedShader_->setUniform("u_view", view);
|
||||||
|
skinnedShader_->setUniform("u_projection", proj);
|
||||||
|
skinnedShader_->setUniform("u_resolution", vec2(static_cast<float>(internalWidth_),
|
||||||
|
static_cast<float>(internalHeight_)));
|
||||||
|
skinnedShader_->setUniform("u_enable_snap", vertexSnapEnabled_);
|
||||||
|
|
||||||
|
// Lighting
|
||||||
|
vec3 lightDir = vec3(0.5f, -0.7f, 0.5f).normalized();
|
||||||
|
skinnedShader_->setUniform("u_light_dir", lightDir);
|
||||||
|
skinnedShader_->setUniform("u_ambient", vec3(0.3f, 0.3f, 0.3f));
|
||||||
|
|
||||||
|
// Fog
|
||||||
|
skinnedShader_->setUniform("u_fog_start", fogNear_);
|
||||||
|
skinnedShader_->setUniform("u_fog_end", fogFar_);
|
||||||
|
skinnedShader_->setUniform("u_fog_color", fogColor_);
|
||||||
|
|
||||||
|
// Texture
|
||||||
|
skinnedShader_->setUniform("u_has_texture", false);
|
||||||
|
skinnedShader_->setUniform("u_enable_dither", ditheringEnabled_);
|
||||||
|
|
||||||
|
for (auto& entity : *entities_) {
|
||||||
|
if (entity && entity->isVisible()) {
|
||||||
|
auto model = entity->getModel();
|
||||||
|
if (model && model->hasSkeleton()) {
|
||||||
|
entity->render(view, proj, skinnedShader_->getProgram());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skinnedShader_->unbind();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,6 +594,12 @@ void Viewport3D::initShader() {
|
||||||
if (!shader_->loadPS1Shaders()) {
|
if (!shader_->loadPS1Shaders()) {
|
||||||
shader_.reset(); // Shader loading failed
|
shader_.reset(); // Shader loading failed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also create skinned shader for skeletal animation
|
||||||
|
skinnedShader_ = std::make_unique<Shader3D>();
|
||||||
|
if (!skinnedShader_->loadPS1SkinnedShaders()) {
|
||||||
|
skinnedShader_.reset(); // Skinned shader loading failed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Viewport3D::initTestGeometry() {
|
void Viewport3D::initTestGeometry() {
|
||||||
|
|
@ -705,6 +748,19 @@ void Viewport3D::render3DContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef MCRF_HAS_GL
|
#ifdef MCRF_HAS_GL
|
||||||
|
// Calculate delta time for animation updates
|
||||||
|
static sf::Clock frameClock;
|
||||||
|
float currentTime = frameClock.getElapsedTime().asSeconds();
|
||||||
|
float dt = firstFrame_ ? 0.016f : (currentTime - lastFrameTime_);
|
||||||
|
lastFrameTime_ = currentTime;
|
||||||
|
firstFrame_ = false;
|
||||||
|
|
||||||
|
// Cap delta time to avoid huge jumps (e.g., after window minimize)
|
||||||
|
if (dt > 0.1f) dt = 0.016f;
|
||||||
|
|
||||||
|
// Update entity animations
|
||||||
|
updateEntities(dt);
|
||||||
|
|
||||||
// Save GL state
|
// Save GL state
|
||||||
gl::pushState();
|
gl::pushState();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,10 @@ private:
|
||||||
float testRotation_ = 0.0f;
|
float testRotation_ = 0.0f;
|
||||||
bool renderTestCube_ = true; // Set to false when layers are added
|
bool renderTestCube_ = true; // Set to false when layers are added
|
||||||
|
|
||||||
|
// Animation timing
|
||||||
|
float lastFrameTime_ = 0.0f;
|
||||||
|
bool firstFrame_ = true;
|
||||||
|
|
||||||
// Mesh layers for terrain, static geometry
|
// Mesh layers for terrain, static geometry
|
||||||
std::vector<std::shared_ptr<MeshLayer>> meshLayers_;
|
std::vector<std::shared_ptr<MeshLayer>> meshLayers_;
|
||||||
|
|
||||||
|
|
@ -304,6 +308,7 @@ private:
|
||||||
|
|
||||||
// Shader for PS1-style rendering
|
// Shader for PS1-style rendering
|
||||||
std::unique_ptr<Shader3D> shader_;
|
std::unique_ptr<Shader3D> shader_;
|
||||||
|
std::unique_ptr<Shader3D> skinnedShader_; // For skeletal animation
|
||||||
|
|
||||||
// Test geometry VBO (cube)
|
// Test geometry VBO (cube)
|
||||||
unsigned int testVBO_ = 0;
|
unsigned int testVBO_ = 0;
|
||||||
|
|
|
||||||
108
src/3d/shaders/ps1_skinned_vertex.glsl
Normal file
108
src/3d/shaders/ps1_skinned_vertex.glsl
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// PS1-style skinned vertex shader for OpenGL 3.2+
|
||||||
|
// Implements skeletal animation, vertex snapping, Gouraud shading, and fog
|
||||||
|
|
||||||
|
#version 150 core
|
||||||
|
|
||||||
|
// Uniforms - transform matrices
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
|
||||||
|
// Uniforms - skeletal animation (max 64 bones)
|
||||||
|
uniform mat4 u_bones[64];
|
||||||
|
|
||||||
|
// Uniforms - PS1 effects
|
||||||
|
uniform vec2 u_resolution; // Internal render resolution for vertex snapping
|
||||||
|
uniform bool u_enable_snap; // Enable vertex snapping to pixel grid
|
||||||
|
uniform float u_fog_start; // Fog start distance
|
||||||
|
uniform float u_fog_end; // Fog end distance
|
||||||
|
|
||||||
|
// Uniforms - lighting
|
||||||
|
uniform vec3 u_light_dir; // Directional light direction (normalized)
|
||||||
|
uniform vec3 u_ambient; // Ambient light color
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
in vec3 a_position;
|
||||||
|
in vec2 a_texcoord;
|
||||||
|
in vec3 a_normal;
|
||||||
|
in vec4 a_color;
|
||||||
|
in vec4 a_bone_ids; // Up to 4 bone indices (as float for compatibility)
|
||||||
|
in vec4 a_bone_weights; // Corresponding weights
|
||||||
|
|
||||||
|
// Varyings - passed to fragment shader
|
||||||
|
out vec4 v_color; // Gouraud-shaded vertex color
|
||||||
|
noperspective out vec2 v_texcoord; // Texture coordinates (affine interpolation!)
|
||||||
|
out float v_fog; // Fog factor (0 = no fog, 1 = full fog)
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// =========================================================================
|
||||||
|
// Skeletal Animation: Vertex Skinning
|
||||||
|
// Transform vertex and normal by weighted bone matrices
|
||||||
|
// =========================================================================
|
||||||
|
ivec4 bone_ids = ivec4(a_bone_ids); // Convert to integer indices
|
||||||
|
|
||||||
|
// Compute skinned position and normal
|
||||||
|
mat4 skin_matrix =
|
||||||
|
u_bones[bone_ids.x] * a_bone_weights.x +
|
||||||
|
u_bones[bone_ids.y] * a_bone_weights.y +
|
||||||
|
u_bones[bone_ids.z] * a_bone_weights.z +
|
||||||
|
u_bones[bone_ids.w] * a_bone_weights.w;
|
||||||
|
|
||||||
|
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
|
||||||
|
vec3 skinned_normal = mat3(skin_matrix) * a_normal;
|
||||||
|
|
||||||
|
// Transform vertex to clip space
|
||||||
|
vec4 worldPos = u_model * skinned_pos;
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Vertex Snapping
|
||||||
|
// The PS1 had limited precision for vertex positions, causing vertices
|
||||||
|
// to "snap" to a grid, creating the characteristic jittery look.
|
||||||
|
// =========================================================================
|
||||||
|
if (u_enable_snap) {
|
||||||
|
// Convert to NDC
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
|
||||||
|
// Snap to pixel grid based on render resolution
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
|
||||||
|
// Convert back to clip space
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Gouraud Shading
|
||||||
|
// Per-vertex lighting was used on PS1 due to hardware limitations.
|
||||||
|
// This creates characteristic flat-shaded polygons.
|
||||||
|
// =========================================================================
|
||||||
|
vec3 worldNormal = mat3(u_model) * skinned_normal;
|
||||||
|
worldNormal = normalize(worldNormal);
|
||||||
|
|
||||||
|
// Simple directional light + ambient
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
|
||||||
|
// Apply lighting to vertex color
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Affine Texture Mapping
|
||||||
|
// Using 'noperspective' qualifier disables perspective-correct interpolation
|
||||||
|
// This creates the characteristic texture warping on large polygons
|
||||||
|
// =========================================================================
|
||||||
|
v_texcoord = a_texcoord;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Fog Distance Calculation
|
||||||
|
// Calculate linear fog factor based on view-space depth
|
||||||
|
// =========================================================================
|
||||||
|
float depth = -viewPos.z; // View space depth (positive)
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
195
src/3d/shaders/ps1_skinned_vertex_es2.glsl
Normal file
195
src/3d/shaders/ps1_skinned_vertex_es2.glsl
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// PS1-style skinned vertex shader for OpenGL ES 2.0 / WebGL 1.0
|
||||||
|
// Implements skeletal animation, vertex snapping, Gouraud shading, and fog
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
// Uniforms - transform matrices
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
|
||||||
|
// Uniforms - skeletal animation (max 64 bones)
|
||||||
|
// GLES2 doesn't guarantee support for arrays > 128 vec4s in vertex shaders
|
||||||
|
// 64 bones * 4 vec4s = 256 vec4s, so we use 32 bones for safety
|
||||||
|
uniform mat4 u_bones[32];
|
||||||
|
|
||||||
|
// Uniforms - PS1 effects
|
||||||
|
uniform vec2 u_resolution; // Internal render resolution for vertex snapping
|
||||||
|
uniform bool u_enable_snap; // Enable vertex snapping to pixel grid
|
||||||
|
uniform float u_fog_start; // Fog start distance
|
||||||
|
uniform float u_fog_end; // Fog end distance
|
||||||
|
|
||||||
|
// Uniforms - lighting
|
||||||
|
uniform vec3 u_light_dir; // Directional light direction (normalized)
|
||||||
|
uniform vec3 u_ambient; // Ambient light color
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
attribute vec3 a_position;
|
||||||
|
attribute vec2 a_texcoord;
|
||||||
|
attribute vec3 a_normal;
|
||||||
|
attribute vec4 a_color;
|
||||||
|
attribute vec4 a_bone_ids; // Up to 4 bone indices (as floats)
|
||||||
|
attribute vec4 a_bone_weights; // Corresponding weights
|
||||||
|
|
||||||
|
// Varyings - passed to fragment shader
|
||||||
|
varying vec4 v_color; // Gouraud-shaded vertex color
|
||||||
|
varying vec2 v_texcoord; // Texture coordinates (multiplied by w for affine trick)
|
||||||
|
varying float v_w; // Clip space w for affine mapping restoration
|
||||||
|
varying float v_fog; // Fog factor (0 = no fog, 1 = full fog)
|
||||||
|
|
||||||
|
// Helper to get bone matrix by index (GLES2 doesn't support dynamic array indexing well)
|
||||||
|
mat4 getBoneMatrix(int index) {
|
||||||
|
// GLES2 workaround: use if-chain for dynamic indexing
|
||||||
|
if (index < 8) {
|
||||||
|
if (index < 4) {
|
||||||
|
if (index < 2) {
|
||||||
|
if (index == 0) return u_bones[0];
|
||||||
|
else return u_bones[1];
|
||||||
|
} else {
|
||||||
|
if (index == 2) return u_bones[2];
|
||||||
|
else return u_bones[3];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 6) {
|
||||||
|
if (index == 4) return u_bones[4];
|
||||||
|
else return u_bones[5];
|
||||||
|
} else {
|
||||||
|
if (index == 6) return u_bones[6];
|
||||||
|
else return u_bones[7];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (index < 16) {
|
||||||
|
if (index < 12) {
|
||||||
|
if (index < 10) {
|
||||||
|
if (index == 8) return u_bones[8];
|
||||||
|
else return u_bones[9];
|
||||||
|
} else {
|
||||||
|
if (index == 10) return u_bones[10];
|
||||||
|
else return u_bones[11];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 14) {
|
||||||
|
if (index == 12) return u_bones[12];
|
||||||
|
else return u_bones[13];
|
||||||
|
} else {
|
||||||
|
if (index == 14) return u_bones[14];
|
||||||
|
else return u_bones[15];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (index < 24) {
|
||||||
|
if (index < 20) {
|
||||||
|
if (index < 18) {
|
||||||
|
if (index == 16) return u_bones[16];
|
||||||
|
else return u_bones[17];
|
||||||
|
} else {
|
||||||
|
if (index == 18) return u_bones[18];
|
||||||
|
else return u_bones[19];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 22) {
|
||||||
|
if (index == 20) return u_bones[20];
|
||||||
|
else return u_bones[21];
|
||||||
|
} else {
|
||||||
|
if (index == 22) return u_bones[22];
|
||||||
|
else return u_bones[23];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 28) {
|
||||||
|
if (index < 26) {
|
||||||
|
if (index == 24) return u_bones[24];
|
||||||
|
else return u_bones[25];
|
||||||
|
} else {
|
||||||
|
if (index == 26) return u_bones[26];
|
||||||
|
else return u_bones[27];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 30) {
|
||||||
|
if (index == 28) return u_bones[28];
|
||||||
|
else return u_bones[29];
|
||||||
|
} else {
|
||||||
|
if (index == 30) return u_bones[30];
|
||||||
|
else return u_bones[31];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mat4(1.0); // Identity fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// =========================================================================
|
||||||
|
// Skeletal Animation: Vertex Skinning
|
||||||
|
// Transform vertex and normal by weighted bone matrices
|
||||||
|
// =========================================================================
|
||||||
|
int b0 = int(a_bone_ids.x);
|
||||||
|
int b1 = int(a_bone_ids.y);
|
||||||
|
int b2 = int(a_bone_ids.z);
|
||||||
|
int b3 = int(a_bone_ids.w);
|
||||||
|
|
||||||
|
// Compute skinned position and normal
|
||||||
|
mat4 skin_matrix =
|
||||||
|
getBoneMatrix(b0) * a_bone_weights.x +
|
||||||
|
getBoneMatrix(b1) * a_bone_weights.y +
|
||||||
|
getBoneMatrix(b2) * a_bone_weights.z +
|
||||||
|
getBoneMatrix(b3) * a_bone_weights.w;
|
||||||
|
|
||||||
|
vec4 skinned_pos = skin_matrix * vec4(a_position, 1.0);
|
||||||
|
vec3 skinned_normal = mat3(skin_matrix[0].xyz, skin_matrix[1].xyz, skin_matrix[2].xyz) * a_normal;
|
||||||
|
|
||||||
|
// Transform vertex to clip space
|
||||||
|
vec4 worldPos = u_model * skinned_pos;
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Vertex Snapping
|
||||||
|
// The PS1 had limited precision for vertex positions, causing vertices
|
||||||
|
// to "snap" to a grid, creating the characteristic jittery look.
|
||||||
|
// =========================================================================
|
||||||
|
if (u_enable_snap) {
|
||||||
|
// Convert to NDC
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
|
||||||
|
// Snap to pixel grid based on render resolution
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
|
||||||
|
// Convert back to clip space
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Gouraud Shading
|
||||||
|
// Per-vertex lighting was used on PS1 due to hardware limitations.
|
||||||
|
// This creates characteristic flat-shaded polygons.
|
||||||
|
// =========================================================================
|
||||||
|
vec3 worldNormal = mat3(u_model[0].xyz, u_model[1].xyz, u_model[2].xyz) * skinned_normal;
|
||||||
|
worldNormal = normalize(worldNormal);
|
||||||
|
|
||||||
|
// Simple directional light + ambient
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
|
||||||
|
// Apply lighting to vertex color
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Affine Texture Mapping Trick
|
||||||
|
// GLES2 doesn't have 'noperspective' interpolation, so we manually
|
||||||
|
// multiply texcoords by w here and divide by w in fragment shader.
|
||||||
|
// This creates the characteristic texture warping on large polygons.
|
||||||
|
// =========================================================================
|
||||||
|
v_texcoord = a_texcoord * clipPos.w;
|
||||||
|
v_w = clipPos.w;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Fog Distance Calculation
|
||||||
|
// Calculate linear fog factor based on view-space depth
|
||||||
|
// =========================================================================
|
||||||
|
float depth = -viewPos.z; // View space depth (positive)
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
275
tests/demo/screens/skeletal_animation_demo.py
Normal file
275
tests/demo/screens/skeletal_animation_demo.py
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
# skeletal_animation_demo.py - 3D Skeletal Animation Demo Screen
|
||||||
|
# Demonstrates Entity3D animation with real animated glTF models
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
DEMO_NAME = "3D Skeletal Animation"
|
||||||
|
DEMO_DESCRIPTION = """Entity3D Animation API with real skeletal models"""
|
||||||
|
|
||||||
|
# Create demo scene
|
||||||
|
scene = mcrfpy.Scene("skeletal_animation_demo")
|
||||||
|
|
||||||
|
# Dark background frame
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Skeletal Animation Demo", pos=(20, 10))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
# Create the 3D viewport
|
||||||
|
viewport = mcrfpy.Viewport3D(
|
||||||
|
pos=(50, 50),
|
||||||
|
size=(600, 500),
|
||||||
|
render_resolution=(600, 500),
|
||||||
|
fov=60.0,
|
||||||
|
camera_pos=(0.0, 2.0, 5.0),
|
||||||
|
camera_target=(0.0, 1.0, 0.0),
|
||||||
|
bg_color=mcrfpy.Color(30, 30, 50)
|
||||||
|
)
|
||||||
|
scene.children.append(viewport)
|
||||||
|
|
||||||
|
# Set up navigation grid
|
||||||
|
GRID_SIZE = 16
|
||||||
|
viewport.set_grid_size(GRID_SIZE, GRID_SIZE)
|
||||||
|
|
||||||
|
# Build a simple flat floor
|
||||||
|
hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
hm.normalize(0.0, 0.0)
|
||||||
|
viewport.apply_heightmap(hm, 0.0)
|
||||||
|
viewport.build_terrain(
|
||||||
|
layer_name="floor",
|
||||||
|
heightmap=hm,
|
||||||
|
y_scale=0.0,
|
||||||
|
cell_size=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply floor colors (dark gray)
|
||||||
|
r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
for y in range(GRID_SIZE):
|
||||||
|
for x in range(GRID_SIZE):
|
||||||
|
checker = ((x + y) % 2) * 0.1 + 0.15
|
||||||
|
r_map[x, y] = checker
|
||||||
|
g_map[x, y] = checker
|
||||||
|
b_map[x, y] = checker + 0.05
|
||||||
|
viewport.apply_terrain_colors("floor", r_map, g_map, b_map)
|
||||||
|
|
||||||
|
# Load animated models
|
||||||
|
animated_entity = None
|
||||||
|
model_info = "No animated model"
|
||||||
|
|
||||||
|
# Try to load CesiumMan (humanoid with walk animation)
|
||||||
|
try:
|
||||||
|
model = mcrfpy.Model3D("../assets/models/CesiumMan.glb")
|
||||||
|
if model.has_skeleton:
|
||||||
|
animated_entity = mcrfpy.Entity3D(pos=(8, 8), scale=1.0, color=mcrfpy.Color(200, 180, 150))
|
||||||
|
animated_entity.model = model
|
||||||
|
viewport.entities.append(animated_entity)
|
||||||
|
|
||||||
|
# Set up animation
|
||||||
|
clips = model.animation_clips
|
||||||
|
if clips:
|
||||||
|
animated_entity.anim_clip = clips[0]
|
||||||
|
animated_entity.anim_loop = True
|
||||||
|
animated_entity.anim_speed = 1.0
|
||||||
|
|
||||||
|
model_info = f"CesiumMan: {model.bone_count} bones, {model.vertex_count} verts"
|
||||||
|
print(f"Loaded {model_info}")
|
||||||
|
print(f"Animation clips: {clips}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load CesiumMan: {e}")
|
||||||
|
|
||||||
|
# Also try RiggedSimple as a second model
|
||||||
|
try:
|
||||||
|
model2 = mcrfpy.Model3D("../assets/models/RiggedSimple.glb")
|
||||||
|
if model2.has_skeleton:
|
||||||
|
entity2 = mcrfpy.Entity3D(pos=(10, 8), scale=0.5, color=mcrfpy.Color(100, 200, 255))
|
||||||
|
entity2.model = model2
|
||||||
|
viewport.entities.append(entity2)
|
||||||
|
|
||||||
|
clips = model2.animation_clips
|
||||||
|
if clips:
|
||||||
|
entity2.anim_clip = clips[0]
|
||||||
|
entity2.anim_loop = True
|
||||||
|
entity2.anim_speed = 1.5
|
||||||
|
|
||||||
|
print(f"Loaded RiggedSimple: {model2.bone_count} bones")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load RiggedSimple: {e}")
|
||||||
|
|
||||||
|
# Info panel on the right
|
||||||
|
info_panel = mcrfpy.Frame(pos=(670, 50), size=(330, 500),
|
||||||
|
fill_color=mcrfpy.Color(30, 30, 40),
|
||||||
|
outline_color=mcrfpy.Color(80, 80, 100),
|
||||||
|
outline=2.0)
|
||||||
|
scene.children.append(info_panel)
|
||||||
|
|
||||||
|
# Panel title
|
||||||
|
panel_title = mcrfpy.Caption(text="Animation Properties", pos=(690, 60))
|
||||||
|
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
|
||||||
|
scene.children.append(panel_title)
|
||||||
|
|
||||||
|
# Status labels (will be updated by timer)
|
||||||
|
status_labels = []
|
||||||
|
y_offset = 90
|
||||||
|
|
||||||
|
label_texts = [
|
||||||
|
"Model: loading...",
|
||||||
|
"anim_clip: ",
|
||||||
|
"anim_time: 0.00",
|
||||||
|
"anim_speed: 1.00",
|
||||||
|
"anim_loop: True",
|
||||||
|
"anim_paused: False",
|
||||||
|
"anim_frame: 0",
|
||||||
|
]
|
||||||
|
|
||||||
|
for text in label_texts:
|
||||||
|
label = mcrfpy.Caption(text=text, pos=(690, y_offset))
|
||||||
|
label.fill_color = mcrfpy.Color(150, 200, 150)
|
||||||
|
scene.children.append(label)
|
||||||
|
status_labels.append(label)
|
||||||
|
y_offset += 25
|
||||||
|
|
||||||
|
# Set initial model info
|
||||||
|
status_labels[0].text = f"Model: {model_info}"
|
||||||
|
|
||||||
|
# Controls section
|
||||||
|
y_offset += 20
|
||||||
|
controls_title = mcrfpy.Caption(text="Controls:", pos=(690, y_offset))
|
||||||
|
controls_title.fill_color = mcrfpy.Color(255, 255, 200)
|
||||||
|
scene.children.append(controls_title)
|
||||||
|
y_offset += 25
|
||||||
|
|
||||||
|
controls = [
|
||||||
|
"[SPACE] Toggle pause",
|
||||||
|
"[L] Toggle loop",
|
||||||
|
"[+/-] Adjust speed",
|
||||||
|
"[R] Reset time",
|
||||||
|
"[1-3] Camera presets",
|
||||||
|
]
|
||||||
|
|
||||||
|
for ctrl in controls:
|
||||||
|
cap = mcrfpy.Caption(text=ctrl, pos=(690, y_offset))
|
||||||
|
cap.fill_color = mcrfpy.Color(180, 180, 150)
|
||||||
|
scene.children.append(cap)
|
||||||
|
y_offset += 20
|
||||||
|
|
||||||
|
# Auto-animate section
|
||||||
|
y_offset += 20
|
||||||
|
auto_title = mcrfpy.Caption(text="Auto-Animate:", pos=(690, y_offset))
|
||||||
|
auto_title.fill_color = mcrfpy.Color(255, 200, 200)
|
||||||
|
scene.children.append(auto_title)
|
||||||
|
y_offset += 25
|
||||||
|
|
||||||
|
auto_labels = []
|
||||||
|
auto_texts = [
|
||||||
|
"auto_animate: True",
|
||||||
|
"walk_clip: 'walk'",
|
||||||
|
"idle_clip: 'idle'",
|
||||||
|
]
|
||||||
|
|
||||||
|
for text in auto_texts:
|
||||||
|
cap = mcrfpy.Caption(text=text, pos=(690, y_offset))
|
||||||
|
cap.fill_color = mcrfpy.Color(180, 160, 160)
|
||||||
|
scene.children.append(cap)
|
||||||
|
auto_labels.append(cap)
|
||||||
|
y_offset += 20
|
||||||
|
|
||||||
|
# Instructions at bottom
|
||||||
|
status = mcrfpy.Caption(text="Status: Animation playing", pos=(20, 570))
|
||||||
|
status.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
scene.children.append(status)
|
||||||
|
|
||||||
|
# Camera presets
|
||||||
|
camera_presets = [
|
||||||
|
((0.0, 2.0, 5.0), (0.0, 1.0, 0.0), "Front view"),
|
||||||
|
((5.0, 3.0, 0.0), (0.0, 1.0, 0.0), "Side view"),
|
||||||
|
((0.0, 6.0, 0.1), (0.0, 0.0, 0.0), "Top-down view"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Update function - updates display and entity rotation
|
||||||
|
def update(timer, runtime):
|
||||||
|
if animated_entity:
|
||||||
|
# Update status display
|
||||||
|
status_labels[1].text = f"anim_clip: '{animated_entity.anim_clip}'"
|
||||||
|
status_labels[2].text = f"anim_time: {animated_entity.anim_time:.2f}"
|
||||||
|
status_labels[3].text = f"anim_speed: {animated_entity.anim_speed:.2f}"
|
||||||
|
status_labels[4].text = f"anim_loop: {animated_entity.anim_loop}"
|
||||||
|
status_labels[5].text = f"anim_paused: {animated_entity.anim_paused}"
|
||||||
|
status_labels[6].text = f"anim_frame: {animated_entity.anim_frame}"
|
||||||
|
|
||||||
|
auto_labels[0].text = f"auto_animate: {animated_entity.auto_animate}"
|
||||||
|
auto_labels[1].text = f"walk_clip: '{animated_entity.walk_clip}'"
|
||||||
|
auto_labels[2].text = f"idle_clip: '{animated_entity.idle_clip}'"
|
||||||
|
|
||||||
|
# Key handler
|
||||||
|
def on_key(key, state):
|
||||||
|
if state != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
|
||||||
|
if animated_entity:
|
||||||
|
if key == mcrfpy.Key.SPACE:
|
||||||
|
animated_entity.anim_paused = not animated_entity.anim_paused
|
||||||
|
status.text = f"Status: {'Paused' if animated_entity.anim_paused else 'Playing'}"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.L:
|
||||||
|
animated_entity.anim_loop = not animated_entity.anim_loop
|
||||||
|
status.text = f"Status: Loop {'ON' if animated_entity.anim_loop else 'OFF'}"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.EQUAL or key == mcrfpy.Key.ADD:
|
||||||
|
animated_entity.anim_speed = min(animated_entity.anim_speed + 0.25, 4.0)
|
||||||
|
status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.HYPHEN or key == mcrfpy.Key.SUBTRACT:
|
||||||
|
animated_entity.anim_speed = max(animated_entity.anim_speed - 0.25, 0.0)
|
||||||
|
status.text = f"Status: Speed {animated_entity.anim_speed:.2f}x"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.R:
|
||||||
|
animated_entity.anim_time = 0.0
|
||||||
|
status.text = "Status: Animation reset"
|
||||||
|
|
||||||
|
# Camera presets
|
||||||
|
if key == mcrfpy.Key.NUM_1:
|
||||||
|
pos, target, name = camera_presets[0]
|
||||||
|
viewport.camera_pos = pos
|
||||||
|
viewport.camera_target = target
|
||||||
|
status.text = f"Camera: {name}"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.NUM_2:
|
||||||
|
pos, target, name = camera_presets[1]
|
||||||
|
viewport.camera_pos = pos
|
||||||
|
viewport.camera_target = target
|
||||||
|
status.text = f"Camera: {name}"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.NUM_3:
|
||||||
|
pos, target, name = camera_presets[2]
|
||||||
|
viewport.camera_pos = pos
|
||||||
|
viewport.camera_target = target
|
||||||
|
status.text = f"Camera: {name}"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.ESCAPE:
|
||||||
|
mcrfpy.exit()
|
||||||
|
|
||||||
|
# Set up scene
|
||||||
|
scene.on_key = on_key
|
||||||
|
|
||||||
|
# Create timer for updates
|
||||||
|
timer = mcrfpy.Timer("anim_update", update, 16)
|
||||||
|
|
||||||
|
# Activate scene
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Skeletal Animation Demo loaded!")
|
||||||
|
print("Controls:")
|
||||||
|
print(" [Space] Toggle pause")
|
||||||
|
print(" [L] Toggle loop")
|
||||||
|
print(" [+/-] Adjust speed")
|
||||||
|
print(" [R] Reset time")
|
||||||
|
print(" [1-3] Camera presets")
|
||||||
|
print(" [ESC] Quit")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue