Fix rig skinning weights, add pose configure UI.
parent
8dcabeae72
commit
c64743f2f2
21
dust3d.pro
21
dust3d.pro
|
@ -164,6 +164,27 @@ HEADERS += src/riggenerator.h
|
||||||
SOURCES += src/meshquadify.cpp
|
SOURCES += src/meshquadify.cpp
|
||||||
HEADERS += src/meshquadify.h
|
HEADERS += src/meshquadify.h
|
||||||
|
|
||||||
|
SOURCES += src/skinnedmeshcreator.cpp
|
||||||
|
HEADERS += src/skinnedmeshcreator.h
|
||||||
|
|
||||||
|
SOURCES += src/jointnodetree.cpp
|
||||||
|
HEADERS += src/jointnodetree.h
|
||||||
|
|
||||||
|
SOURCES += src/tetrapodposer.cpp
|
||||||
|
HEADERS += src/tetrapodposer.h
|
||||||
|
|
||||||
|
SOURCES += src/poser.cpp
|
||||||
|
HEADERS += src/poser.h
|
||||||
|
|
||||||
|
SOURCES += src/posemeshcreator.cpp
|
||||||
|
HEADERS += src/posemeshcreator.h
|
||||||
|
|
||||||
|
SOURCES += src/posepreviewmanager.cpp
|
||||||
|
HEADERS += src/posepreviewmanager.h
|
||||||
|
|
||||||
|
SOURCES += src/tetrapodposeeditwidget.cpp
|
||||||
|
HEADERS += src/tetrapodposeeditwidget.h
|
||||||
|
|
||||||
SOURCES += src/main.cpp
|
SOURCES += src/main.cpp
|
||||||
|
|
||||||
HEADERS += src/version.h
|
HEADERS += src/version.h
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
#include "animationclipplayer.h"
|
|
||||||
|
|
||||||
AnimationClipPlayer::~AnimationClipPlayer()
|
|
||||||
{
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AnimationClipPlayer::updateFrameMeshes(std::vector<std::pair<float, MeshLoader *>> &frameMeshes)
|
|
||||||
{
|
|
||||||
clear();
|
|
||||||
|
|
||||||
m_frameMeshes = frameMeshes;
|
|
||||||
frameMeshes.clear();
|
|
||||||
|
|
||||||
m_currentPlayIndex = 0;
|
|
||||||
m_countForFrame.restart();
|
|
||||||
|
|
||||||
if (!m_frameMeshes.empty())
|
|
||||||
m_timerForFrame.singleShot(0, this, &AnimationClipPlayer::frameReadyToShow);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AnimationClipPlayer::clear()
|
|
||||||
{
|
|
||||||
freeFrames();
|
|
||||||
delete m_lastFrameMesh;
|
|
||||||
m_lastFrameMesh = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AnimationClipPlayer::freeFrames()
|
|
||||||
{
|
|
||||||
for (auto &it: m_frameMeshes) {
|
|
||||||
delete it.second;
|
|
||||||
}
|
|
||||||
m_frameMeshes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
MeshLoader *AnimationClipPlayer::takeFrameMesh()
|
|
||||||
{
|
|
||||||
if (m_currentPlayIndex >= (int)m_frameMeshes.size()) {
|
|
||||||
if (nullptr != m_lastFrameMesh)
|
|
||||||
return new MeshLoader(*m_lastFrameMesh);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
int millis = m_frameMeshes[m_currentPlayIndex].first * 1000 - m_countForFrame.elapsed();
|
|
||||||
if (millis > 0) {
|
|
||||||
m_timerForFrame.singleShot(millis, this, &AnimationClipPlayer::frameReadyToShow);
|
|
||||||
if (nullptr != m_lastFrameMesh)
|
|
||||||
return new MeshLoader(*m_lastFrameMesh);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
m_currentPlayIndex = (m_currentPlayIndex + 1) % m_frameMeshes.size();
|
|
||||||
m_countForFrame.restart();
|
|
||||||
|
|
||||||
MeshLoader *mesh = new MeshLoader(*m_frameMeshes[m_currentPlayIndex].second);
|
|
||||||
m_timerForFrame.singleShot(m_frameMeshes[m_currentPlayIndex].first * 1000, this, &AnimationClipPlayer::frameReadyToShow);
|
|
||||||
delete m_lastFrameMesh;
|
|
||||||
m_lastFrameMesh = new MeshLoader(*mesh);
|
|
||||||
return mesh;
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
#ifndef ANIMATION_PLAYER_H
|
|
||||||
#define ANIMATION_PLAYER_H
|
|
||||||
#include <QObject>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QTime>
|
|
||||||
#include "meshloader.h"
|
|
||||||
|
|
||||||
class AnimationClipPlayer : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
signals:
|
|
||||||
void frameReadyToShow();
|
|
||||||
public:
|
|
||||||
~AnimationClipPlayer();
|
|
||||||
MeshLoader *takeFrameMesh();
|
|
||||||
void updateFrameMeshes(std::vector<std::pair<float, MeshLoader *>> &frameMeshes);
|
|
||||||
void clear();
|
|
||||||
private:
|
|
||||||
void freeFrames();
|
|
||||||
private:
|
|
||||||
MeshLoader *m_lastFrameMesh = nullptr;
|
|
||||||
int m_currentPlayIndex = 0;
|
|
||||||
private:
|
|
||||||
std::vector<std::pair<float, MeshLoader *>> m_frameMeshes;
|
|
||||||
QTime m_countForFrame;
|
|
||||||
QTimer m_timerForFrame;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -249,7 +249,8 @@ void AutoRigger::addVerticesToWeights(const std::set<int> &vertices, int boneInd
|
||||||
{
|
{
|
||||||
for (const auto &vertexIndex: vertices) {
|
for (const auto &vertexIndex: vertices) {
|
||||||
auto &weights = m_resultWeights[vertexIndex];
|
auto &weights = m_resultWeights[vertexIndex];
|
||||||
float distance = m_verticesPositions[vertexIndex].distanceToPoint(m_resultBones[boneIndex].headPosition);
|
auto strongestPoint = (m_resultBones[boneIndex].headPosition * 3 + m_resultBones[boneIndex].tailPosition) / 4;
|
||||||
|
float distance = m_verticesPositions[vertexIndex].distanceToPoint(strongestPoint);
|
||||||
weights.addBone(boneIndex, distance);
|
weights.addBone(boneIndex, distance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,6 +308,7 @@ bool AutoRigger::rig()
|
||||||
|
|
||||||
std::set<int> headVertices;
|
std::set<int> headVertices;
|
||||||
addTrianglesToVertices(m_marks[neckIndicies->second[0]].smallGroup(), headVertices);
|
addTrianglesToVertices(m_marks[neckIndicies->second[0]].smallGroup(), headVertices);
|
||||||
|
addTrianglesToVertices(m_marks[neckIndicies->second[0]].markTriangles, headVertices);
|
||||||
QVector3D headBoneStopPosition;
|
QVector3D headBoneStopPosition;
|
||||||
if (isMainBodyVerticalAligned) {
|
if (isMainBodyVerticalAligned) {
|
||||||
QVector3D maxY = findMaxY(headVertices);
|
QVector3D maxY = findMaxY(headVertices);
|
||||||
|
@ -379,6 +381,9 @@ bool AutoRigger::rig()
|
||||||
// 2.1 Collect vertices for neck bone:
|
// 2.1 Collect vertices for neck bone:
|
||||||
std::set<int> bodyVertices;
|
std::set<int> bodyVertices;
|
||||||
addTrianglesToVertices(bodyTriangles, bodyVertices);
|
addTrianglesToVertices(bodyTriangles, bodyVertices);
|
||||||
|
addTrianglesToVertices(m_marks[leftShoulderIndicies->second[0]].markTriangles, bodyVertices);
|
||||||
|
addTrianglesToVertices(m_marks[rightShoulderIndicies->second[0]].markTriangles, bodyVertices);
|
||||||
|
|
||||||
std::set<int> bodyVerticesAfterShoulder;
|
std::set<int> bodyVerticesAfterShoulder;
|
||||||
std::set<int> neckVertices;
|
std::set<int> neckVertices;
|
||||||
{
|
{
|
||||||
|
@ -447,11 +452,9 @@ bool AutoRigger::rig()
|
||||||
{
|
{
|
||||||
std::set<int> leftArmVerticesBeforeElbow;
|
std::set<int> leftArmVerticesBeforeElbow;
|
||||||
if (isLeftArmVerticalAligned) {
|
if (isLeftArmVerticalAligned) {
|
||||||
QVector3D maxY = findMaxY(leftElbowMarkVertices);
|
splitVerticesByY(leftArmVertices, m_marks[leftElbowIndicies->second[0]].bonePosition.y(), leftArmVerticesBeforeElbow, leftArmVerticesSinceElbow);
|
||||||
splitVerticesByY(leftArmVertices, maxY.y(), leftArmVerticesBeforeElbow, leftArmVerticesSinceElbow);
|
|
||||||
} else {
|
} else {
|
||||||
QVector3D minX = findMinX(leftElbowMarkVertices);
|
splitVerticesByX(leftArmVertices, m_marks[leftElbowIndicies->second[0]].bonePosition.x(), leftArmVerticesSinceElbow, leftArmVerticesBeforeElbow);
|
||||||
splitVerticesByX(leftArmVertices, minX.x(), leftArmVerticesSinceElbow, leftArmVerticesBeforeElbow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::set<int> leftWristMarkVertices;
|
std::set<int> leftWristMarkVertices;
|
||||||
|
@ -472,11 +475,9 @@ bool AutoRigger::rig()
|
||||||
{
|
{
|
||||||
std::set<int> leftArmVerticesBeforeWrist;
|
std::set<int> leftArmVerticesBeforeWrist;
|
||||||
if (isLeftArmVerticalAligned) {
|
if (isLeftArmVerticalAligned) {
|
||||||
QVector3D maxY = findMaxY(leftWristMarkVertices);
|
splitVerticesByY(leftArmVerticesSinceElbow, m_marks[leftWristIndicies->second[0]].bonePosition.y(), leftArmVerticesBeforeWrist, leftHandVertices);
|
||||||
splitVerticesByY(leftArmVerticesSinceElbow, maxY.y(), leftArmVerticesBeforeWrist, leftHandVertices);
|
|
||||||
} else {
|
} else {
|
||||||
QVector3D minX = findMinX(leftWristMarkVertices);
|
splitVerticesByX(leftArmVerticesSinceElbow, m_marks[leftWristIndicies->second[0]].bonePosition.x(), leftHandVertices, leftArmVerticesBeforeWrist);
|
||||||
splitVerticesByX(leftArmVerticesSinceElbow, minX.x(), leftHandVertices, leftArmVerticesBeforeWrist);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -501,11 +502,9 @@ bool AutoRigger::rig()
|
||||||
{
|
{
|
||||||
std::set<int> rightArmVerticesBeforeElbow;
|
std::set<int> rightArmVerticesBeforeElbow;
|
||||||
if (isRightArmVerticalAligned) {
|
if (isRightArmVerticalAligned) {
|
||||||
QVector3D maxY = findMaxY(rightElbowMarkVertices);
|
splitVerticesByY(rightArmVertices, m_marks[rightElbowIndicies->second[0]].bonePosition.y(), rightArmVerticesBeforeElbow, rightArmVerticesSinceElbow);
|
||||||
splitVerticesByY(rightArmVertices, maxY.y(), rightArmVerticesBeforeElbow, rightArmVerticesSinceElbow);
|
|
||||||
} else {
|
} else {
|
||||||
QVector3D maxX = findMaxX(rightElbowMarkVertices);
|
splitVerticesByX(rightArmVertices, m_marks[rightElbowIndicies->second[0]].bonePosition.x(), rightArmVerticesBeforeElbow, rightArmVerticesSinceElbow);
|
||||||
splitVerticesByX(rightArmVertices, maxX.x(), rightArmVerticesBeforeElbow, rightArmVerticesSinceElbow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::set<int> rightWristMarkVertices;
|
std::set<int> rightWristMarkVertices;
|
||||||
|
@ -526,11 +525,9 @@ bool AutoRigger::rig()
|
||||||
{
|
{
|
||||||
std::set<int> rightArmVerticesBeforeWrist;
|
std::set<int> rightArmVerticesBeforeWrist;
|
||||||
if (isRightArmVerticalAligned) {
|
if (isRightArmVerticalAligned) {
|
||||||
QVector3D maxY = findMaxY(rightWristMarkVertices);
|
splitVerticesByY(rightArmVerticesSinceElbow, m_marks[rightWristIndicies->second[0]].bonePosition.y(), rightArmVerticesBeforeWrist, rightHandVertices);
|
||||||
splitVerticesByY(rightArmVerticesSinceElbow, maxY.y(), rightArmVerticesBeforeWrist, rightHandVertices);
|
|
||||||
} else {
|
} else {
|
||||||
QVector3D maxX = findMaxX(rightWristMarkVertices);
|
splitVerticesByX(rightArmVerticesSinceElbow, m_marks[rightWristIndicies->second[0]].bonePosition.x(), rightArmVerticesBeforeWrist, rightHandVertices);
|
||||||
splitVerticesByX(rightArmVerticesSinceElbow, maxX.x(), rightArmVerticesBeforeWrist, rightHandVertices);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,8 +563,7 @@ bool AutoRigger::rig()
|
||||||
std::set<int> leftFootVertices;
|
std::set<int> leftFootVertices;
|
||||||
{
|
{
|
||||||
std::set<int> leftLegVerticesBeforeAnkle;
|
std::set<int> leftLegVerticesBeforeAnkle;
|
||||||
QVector3D maxY = findMaxY(leftAnkleMarkVertices);
|
splitVerticesByY(leftLegVerticesSinceKnee, m_marks[leftAnkleIndicies->second[0]].bonePosition.y(), leftLegVerticesBeforeAnkle, leftFootVertices);
|
||||||
splitVerticesByY(leftLegVerticesSinceKnee, maxY.y(), leftLegVerticesBeforeAnkle, leftFootVertices);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4.2.1 Collect vertices for right upper leg:
|
// 4.2.1 Collect vertices for right upper leg:
|
||||||
|
@ -600,8 +596,7 @@ bool AutoRigger::rig()
|
||||||
std::set<int> rightFootVertices;
|
std::set<int> rightFootVertices;
|
||||||
{
|
{
|
||||||
std::set<int> rightLegVerticesBeforeAnkle;
|
std::set<int> rightLegVerticesBeforeAnkle;
|
||||||
QVector3D maxY = findMaxY(rightAnkleMarkVertices);
|
splitVerticesByY(rightLegVerticesSinceKnee, m_marks[rightAnkleIndicies->second[0]].bonePosition.y(), rightLegVerticesBeforeAnkle, rightFootVertices);
|
||||||
splitVerticesByY(rightLegVerticesSinceKnee, maxY.y(), rightLegVerticesBeforeAnkle, rightFootVertices);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Generate bones
|
// 5. Generate bones
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include <QVector3D>
|
#include <QVector3D>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QDebug>
|
||||||
#include "meshsplitter.h"
|
#include "meshsplitter.h"
|
||||||
#include "skeletonbonemark.h"
|
#include "skeletonbonemark.h"
|
||||||
#include "rigtype.h"
|
#include "rigtype.h"
|
||||||
|
@ -55,7 +56,7 @@ public:
|
||||||
void addBone(int boneIndex, float distance)
|
void addBone(int boneIndex, float distance)
|
||||||
{
|
{
|
||||||
if (qFuzzyIsNull(distance))
|
if (qFuzzyIsNull(distance))
|
||||||
distance = 0.01;
|
distance = 0.0001;
|
||||||
m_boneRawWeights.push_back(std::make_pair(boneIndex, 1.0 / distance));
|
m_boneRawWeights.push_back(std::make_pair(boneIndex, 1.0 / distance));
|
||||||
}
|
}
|
||||||
void finalizeWeights()
|
void finalizeWeights()
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
struct CCDIKNode
|
struct CCDIKNode
|
||||||
{
|
{
|
||||||
QVector3D position;
|
QVector3D position;
|
||||||
bool hasConstraint = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CCDIKSolver
|
class CCDIKSolver
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
#include <QtWidgets>
|
#include <QtWidgets>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include "floatnumberwidget.h"
|
#include "floatnumberwidget.h"
|
||||||
|
|
||||||
FloatNumberWidget::FloatNumberWidget(QWidget *parent) :
|
FloatNumberWidget::FloatNumberWidget(QWidget *parent, bool singleLine) :
|
||||||
QWidget(parent)
|
QWidget(parent)
|
||||||
{
|
{
|
||||||
m_slider = new QSlider(Qt::Horizontal, this);
|
m_slider = new QSlider(Qt::Horizontal, this);
|
||||||
|
@ -17,10 +19,18 @@ FloatNumberWidget::FloatNumberWidget(QWidget *parent) :
|
||||||
emit valueChanged(fvalue);
|
emit valueChanged(fvalue);
|
||||||
});
|
});
|
||||||
|
|
||||||
QBoxLayout *popupLayout = new QHBoxLayout(this);
|
QBoxLayout *layout = nullptr;
|
||||||
popupLayout->setMargin(2);
|
if (singleLine) {
|
||||||
popupLayout->addWidget(m_slider);
|
layout = new QHBoxLayout(this);
|
||||||
popupLayout->addWidget(m_label);
|
layout->setMargin(2);
|
||||||
|
layout->addWidget(m_slider);
|
||||||
|
layout->addWidget(m_label);
|
||||||
|
} else {
|
||||||
|
layout = new QVBoxLayout(this);
|
||||||
|
layout->setMargin(2);
|
||||||
|
layout->addWidget(m_label);
|
||||||
|
layout->addWidget(m_slider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatNumberWidget::updateValueLabel(float value)
|
void FloatNumberWidget::updateValueLabel(float value)
|
||||||
|
|
|
@ -9,7 +9,7 @@ class FloatNumberWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit FloatNumberWidget(QWidget *parent = nullptr);
|
explicit FloatNumberWidget(QWidget *parent = nullptr, bool singleLine=true);
|
||||||
void setRange(float min, float max);
|
void setRange(float min, float max);
|
||||||
float value() const;
|
float value() const;
|
||||||
void setItemName(const QString &name);
|
void setItemName(const QString &name);
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "gltffile.h"
|
#include "gltffile.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
#include "dust3dutil.h"
|
#include "dust3dutil.h"
|
||||||
|
#include "jointnodetree.h"
|
||||||
|
|
||||||
// Play with glTF online:
|
// Play with glTF online:
|
||||||
// https://gltf-viewer.donmccurdy.com/
|
// https://gltf-viewer.donmccurdy.com/
|
||||||
|
@ -47,14 +48,15 @@ GltfFileWriter::GltfFileWriter(MeshResultContext &resultContext,
|
||||||
int bufferViewIndex = 0;
|
int bufferViewIndex = 0;
|
||||||
int bufferViewFromOffset;
|
int bufferViewFromOffset;
|
||||||
|
|
||||||
|
JointNodeTree tailNodeTree(resultRigBones);
|
||||||
|
const auto &boneNodes = tailNodeTree.nodes();
|
||||||
|
|
||||||
m_json["asset"]["version"] = "2.0";
|
m_json["asset"]["version"] = "2.0";
|
||||||
m_json["asset"]["generator"] = APP_NAME " " APP_HUMAN_VER;
|
m_json["asset"]["generator"] = APP_NAME " " APP_HUMAN_VER;
|
||||||
m_json["scenes"][0]["nodes"] = {0};
|
m_json["scenes"][0]["nodes"] = {0};
|
||||||
|
|
||||||
if (resultRigBones && resultRigWeights && !resultRigBones->empty()) {
|
if (resultRigBones && resultRigWeights && !resultRigBones->empty()) {
|
||||||
|
|
||||||
calculateMatrices(resultRigBones);
|
|
||||||
|
|
||||||
constexpr int skeletonNodeStartIndex = 2;
|
constexpr int skeletonNodeStartIndex = 2;
|
||||||
|
|
||||||
m_json["nodes"][0]["children"] = {
|
m_json["nodes"][0]["children"] = {
|
||||||
|
@ -66,25 +68,25 @@ GltfFileWriter::GltfFileWriter(MeshResultContext &resultContext,
|
||||||
m_json["nodes"][1]["skin"] = 0;
|
m_json["nodes"][1]["skin"] = 0;
|
||||||
|
|
||||||
m_json["skins"][0]["joints"] = {};
|
m_json["skins"][0]["joints"] = {};
|
||||||
for (size_t i = 0; i < m_boneNodes.size(); i++) {
|
for (size_t i = 0; i < boneNodes.size(); i++) {
|
||||||
m_json["skins"][0]["joints"] += skeletonNodeStartIndex + i;
|
m_json["skins"][0]["joints"] += skeletonNodeStartIndex + i;
|
||||||
|
|
||||||
m_json["nodes"][skeletonNodeStartIndex + i]["name"] = m_boneNodes[i].name.toUtf8().constData();
|
m_json["nodes"][skeletonNodeStartIndex + i]["name"] = boneNodes[i].name.toUtf8().constData();
|
||||||
m_json["nodes"][skeletonNodeStartIndex + i]["translation"] = {
|
m_json["nodes"][skeletonNodeStartIndex + i]["translation"] = {
|
||||||
m_boneNodes[i].translation.x(),
|
boneNodes[i].translation.x(),
|
||||||
m_boneNodes[i].translation.y(),
|
boneNodes[i].translation.y(),
|
||||||
m_boneNodes[i].translation.z()
|
boneNodes[i].translation.z()
|
||||||
};
|
};
|
||||||
m_json["nodes"][skeletonNodeStartIndex + i]["rotation"] = {
|
m_json["nodes"][skeletonNodeStartIndex + i]["rotation"] = {
|
||||||
m_boneNodes[i].rotation.x(),
|
boneNodes[i].rotation.x(),
|
||||||
m_boneNodes[i].rotation.y(),
|
boneNodes[i].rotation.y(),
|
||||||
m_boneNodes[i].rotation.z(),
|
boneNodes[i].rotation.z(),
|
||||||
m_boneNodes[i].rotation.scalar()
|
boneNodes[i].rotation.scalar()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!m_boneNodes[i].children.empty()) {
|
if (!boneNodes[i].children.empty()) {
|
||||||
m_json["nodes"][skeletonNodeStartIndex + i]["children"] = {};
|
m_json["nodes"][skeletonNodeStartIndex + i]["children"] = {};
|
||||||
for (const auto &it: m_boneNodes[i].children) {
|
for (const auto &it: boneNodes[i].children) {
|
||||||
m_json["nodes"][skeletonNodeStartIndex + i]["children"] += skeletonNodeStartIndex + it;
|
m_json["nodes"][skeletonNodeStartIndex + i]["children"] += skeletonNodeStartIndex + it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,21 +97,21 @@ GltfFileWriter::GltfFileWriter(MeshResultContext &resultContext,
|
||||||
bufferViewFromOffset = (int)binaries.size();
|
bufferViewFromOffset = (int)binaries.size();
|
||||||
m_json["bufferViews"][bufferViewIndex]["buffer"] = 0;
|
m_json["bufferViews"][bufferViewIndex]["buffer"] = 0;
|
||||||
m_json["bufferViews"][bufferViewIndex]["byteOffset"] = bufferViewFromOffset;
|
m_json["bufferViews"][bufferViewIndex]["byteOffset"] = bufferViewFromOffset;
|
||||||
for (auto i = 0u; i < m_boneNodes.size(); i++) {
|
for (auto i = 0u; i < boneNodes.size(); i++) {
|
||||||
const float *floatArray = m_boneNodes[i].inverseBindMatrix.constData();
|
const float *floatArray = boneNodes[i].inverseBindMatrix.constData();
|
||||||
for (auto j = 0u; j < 16; j++) {
|
for (auto j = 0u; j < 16; j++) {
|
||||||
stream << (float)floatArray[j];
|
stream << (float)floatArray[j];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_json["bufferViews"][bufferViewIndex]["byteLength"] = binaries.size() - bufferViewFromOffset;
|
m_json["bufferViews"][bufferViewIndex]["byteLength"] = binaries.size() - bufferViewFromOffset;
|
||||||
Q_ASSERT((int)m_boneNodes.size() * 16 * sizeof(float) == binaries.size() - bufferViewFromOffset);
|
Q_ASSERT((int)boneNodes.size() * 16 * sizeof(float) == binaries.size() - bufferViewFromOffset);
|
||||||
alignBinaries();
|
alignBinaries();
|
||||||
if (m_enableComment)
|
if (m_enableComment)
|
||||||
m_json["accessors"][bufferViewIndex]["__comment"] = QString("/accessors/%1: mat").arg(QString::number(bufferViewIndex)).toUtf8().constData();
|
m_json["accessors"][bufferViewIndex]["__comment"] = QString("/accessors/%1: mat").arg(QString::number(bufferViewIndex)).toUtf8().constData();
|
||||||
m_json["accessors"][bufferViewIndex]["bufferView"] = bufferViewIndex;
|
m_json["accessors"][bufferViewIndex]["bufferView"] = bufferViewIndex;
|
||||||
m_json["accessors"][bufferViewIndex]["byteOffset"] = 0;
|
m_json["accessors"][bufferViewIndex]["byteOffset"] = 0;
|
||||||
m_json["accessors"][bufferViewIndex]["componentType"] = 5126;
|
m_json["accessors"][bufferViewIndex]["componentType"] = 5126;
|
||||||
m_json["accessors"][bufferViewIndex]["count"] = m_boneNodes.size();
|
m_json["accessors"][bufferViewIndex]["count"] = boneNodes.size();
|
||||||
m_json["accessors"][bufferViewIndex]["type"] = "MAT4";
|
m_json["accessors"][bufferViewIndex]["type"] = "MAT4";
|
||||||
bufferViewIndex++;
|
bufferViewIndex++;
|
||||||
} else {
|
} else {
|
||||||
|
@ -312,7 +314,7 @@ GltfFileWriter::GltfFileWriter(MeshResultContext &resultContext,
|
||||||
auto findWeight = resultRigWeights->find(oldIndex);
|
auto findWeight = resultRigWeights->find(oldIndex);
|
||||||
if (findWeight != resultRigWeights->end()) {
|
if (findWeight != resultRigWeights->end()) {
|
||||||
for (; i < MAX_WEIGHT_NUM; i++) {
|
for (; i < MAX_WEIGHT_NUM; i++) {
|
||||||
float weight = (quint16)findWeight->second.boneIndicies[i];
|
float weight = (float)findWeight->second.boneWeights[i];
|
||||||
stream << (float)weight;
|
stream << (float)weight;
|
||||||
if (m_enableComment)
|
if (m_enableComment)
|
||||||
weightList.append(QString("%1").arg(QString::number((float)weight)));
|
weightList.append(QString("%1").arg(QString::number((float)weight)));
|
||||||
|
@ -358,37 +360,3 @@ bool GltfFileWriter::save()
|
||||||
file.write(QString::fromStdString(m_json.dump(4)).toUtf8());
|
file.write(QString::fromStdString(m_json.dump(4)).toUtf8());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GltfFileWriter::calculateMatrices(const std::vector<AutoRiggerBone> *resultRigBones)
|
|
||||||
{
|
|
||||||
if (nullptr == resultRigBones)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_boneNodes.resize(resultRigBones->size());
|
|
||||||
|
|
||||||
m_boneNodes[0].parentIndex = -1;
|
|
||||||
for (decltype(resultRigBones->size()) i = 0; i < resultRigBones->size(); i++) {
|
|
||||||
const auto &bone = (*resultRigBones)[i];
|
|
||||||
auto &node = m_boneNodes[i];
|
|
||||||
node.name = bone.name;
|
|
||||||
node.position = bone.tailPosition;
|
|
||||||
node.children = bone.children;
|
|
||||||
for (const auto &childIndex: bone.children)
|
|
||||||
m_boneNodes[childIndex].parentIndex = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (decltype(resultRigBones->size()) i = 0; i < resultRigBones->size(); i++) {
|
|
||||||
const auto &bone = (*resultRigBones)[i];
|
|
||||||
QMatrix4x4 parentBindMatrix;
|
|
||||||
auto &node = m_boneNodes[i];
|
|
||||||
node.translation = bone.tailPosition - bone.headPosition;
|
|
||||||
if (node.parentIndex != -1) {
|
|
||||||
const auto &parent = m_boneNodes[node.parentIndex];
|
|
||||||
parentBindMatrix = parent.bindMatrix;
|
|
||||||
}
|
|
||||||
QMatrix4x4 translateMatrix;
|
|
||||||
translateMatrix.translate(node.translation);
|
|
||||||
node.bindMatrix = parentBindMatrix * translateMatrix;
|
|
||||||
node.inverseBindMatrix = node.bindMatrix.inverted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,18 +10,6 @@
|
||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include "skeletondocument.h"
|
#include "skeletondocument.h"
|
||||||
|
|
||||||
struct GltfNode
|
|
||||||
{
|
|
||||||
int parentIndex;
|
|
||||||
QString name;
|
|
||||||
QVector3D position;
|
|
||||||
QVector3D translation;
|
|
||||||
QQuaternion rotation;
|
|
||||||
QMatrix4x4 bindMatrix;
|
|
||||||
QMatrix4x4 inverseBindMatrix;
|
|
||||||
std::vector<int> children;
|
|
||||||
};
|
|
||||||
|
|
||||||
class GltfFileWriter : public QObject
|
class GltfFileWriter : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -33,14 +21,12 @@ public:
|
||||||
bool save();
|
bool save();
|
||||||
const QString &textureFilenameInGltf();
|
const QString &textureFilenameInGltf();
|
||||||
private:
|
private:
|
||||||
void calculateMatrices(const std::vector<AutoRiggerBone> *resultRigBones);
|
|
||||||
QString m_filename;
|
QString m_filename;
|
||||||
QString m_textureFilename;
|
QString m_textureFilename;
|
||||||
bool m_outputNormal;
|
bool m_outputNormal;
|
||||||
bool m_outputAnimation;
|
bool m_outputAnimation;
|
||||||
bool m_outputUv;
|
bool m_outputUv;
|
||||||
bool m_testOutputAsWhole;
|
bool m_testOutputAsWhole;
|
||||||
std::vector<GltfNode> m_boneNodes;
|
|
||||||
private:
|
private:
|
||||||
nlohmann::json m_json;
|
nlohmann::json m_json;
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
#include "jointnodetree.h"
|
||||||
|
|
||||||
|
const std::vector<JointNode> &JointNodeTree::nodes() const
|
||||||
|
{
|
||||||
|
return m_boneNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JointNodeTree::updateRotation(int index, QQuaternion rotation)
|
||||||
|
{
|
||||||
|
m_boneNodes[index].rotation = rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JointNodeTree::reset()
|
||||||
|
{
|
||||||
|
for (auto &node: m_boneNodes) {
|
||||||
|
node.rotation = QQuaternion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void JointNodeTree::recalculateTransformMatrices()
|
||||||
|
{
|
||||||
|
for (decltype(m_boneNodes.size()) i = 0; i < m_boneNodes.size(); i++) {
|
||||||
|
QMatrix4x4 parentTransformMatrix;
|
||||||
|
auto &node = m_boneNodes[i];
|
||||||
|
if (node.parentIndex != -1) {
|
||||||
|
const auto &parent = m_boneNodes[node.parentIndex];
|
||||||
|
parentTransformMatrix = parent.transformMatrix;
|
||||||
|
}
|
||||||
|
QMatrix4x4 translateMatrix;
|
||||||
|
translateMatrix.translate(node.translation);
|
||||||
|
QMatrix4x4 rotationMatrix;
|
||||||
|
rotationMatrix.rotate(node.rotation);
|
||||||
|
node.transformMatrix = parentTransformMatrix * translateMatrix * rotationMatrix;
|
||||||
|
}
|
||||||
|
for (decltype(m_boneNodes.size()) i = 0; i < m_boneNodes.size(); i++) {
|
||||||
|
auto &node = m_boneNodes[i];
|
||||||
|
node.transformMatrix *= node.inverseBindMatrix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JointNodeTree::JointNodeTree(const std::vector<AutoRiggerBone> *resultRigBones)
|
||||||
|
{
|
||||||
|
if (nullptr == resultRigBones || resultRigBones->empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_boneNodes.resize(resultRigBones->size());
|
||||||
|
|
||||||
|
m_boneNodes[0].parentIndex = -1;
|
||||||
|
for (decltype(resultRigBones->size()) i = 0; i < resultRigBones->size(); i++) {
|
||||||
|
const auto &bone = (*resultRigBones)[i];
|
||||||
|
auto &node = m_boneNodes[i];
|
||||||
|
node.name = bone.name;
|
||||||
|
node.position = bone.headPosition;
|
||||||
|
node.children = bone.children;
|
||||||
|
for (const auto &childIndex: bone.children)
|
||||||
|
m_boneNodes[childIndex].parentIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (decltype(resultRigBones->size()) i = 0; i < resultRigBones->size(); i++) {
|
||||||
|
QMatrix4x4 parentTransformMatrix;
|
||||||
|
auto &node = m_boneNodes[i];
|
||||||
|
if (node.parentIndex != -1) {
|
||||||
|
const auto &parent = m_boneNodes[node.parentIndex];
|
||||||
|
parentTransformMatrix = parent.transformMatrix;
|
||||||
|
node.translation = node.position - parent.position;
|
||||||
|
} else {
|
||||||
|
node.translation = node.position;
|
||||||
|
}
|
||||||
|
QMatrix4x4 translateMatrix;
|
||||||
|
translateMatrix.translate(node.translation);
|
||||||
|
node.transformMatrix = parentTransformMatrix * translateMatrix;
|
||||||
|
node.inverseBindMatrix = node.transformMatrix.inverted();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
#ifndef JOINT_NODE_TREE_H
|
||||||
|
#define JOINT_NODE_TREE_H
|
||||||
|
#include <QMatrix4x4>
|
||||||
|
#include <vector>
|
||||||
|
#include <QQuaternion>
|
||||||
|
#include "autorigger.h"
|
||||||
|
|
||||||
|
struct JointNode
|
||||||
|
{
|
||||||
|
int parentIndex;
|
||||||
|
QString name;
|
||||||
|
QVector3D position;
|
||||||
|
QVector3D translation;
|
||||||
|
QQuaternion rotation;
|
||||||
|
QMatrix4x4 transformMatrix;
|
||||||
|
QMatrix4x4 inverseBindMatrix;
|
||||||
|
std::vector<int> children;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JointNodeTree
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
const std::vector<JointNode> &nodes() const;
|
||||||
|
JointNodeTree(const std::vector<AutoRiggerBone> *resultRigBones);
|
||||||
|
void updateRotation(int index, QQuaternion rotation);
|
||||||
|
void reset();
|
||||||
|
void recalculateTransformMatrices();
|
||||||
|
private:
|
||||||
|
std::vector<JointNode> m_boneNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,47 @@
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include "posemeshcreator.h"
|
||||||
|
#include "skinnedmeshcreator.h"
|
||||||
|
|
||||||
|
PoseMeshCreator::PoseMeshCreator(const Poser &poser,
|
||||||
|
const MeshResultContext &meshResultContext,
|
||||||
|
const std::map<int, AutoRiggerVertexWeights> &resultWeights) :
|
||||||
|
m_resultNodes(poser.resultNodes()),
|
||||||
|
m_meshResultContext(meshResultContext),
|
||||||
|
m_resultWeights(resultWeights)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PoseMeshCreator::~PoseMeshCreator()
|
||||||
|
{
|
||||||
|
delete m_resultMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshLoader *PoseMeshCreator::takeResultMesh()
|
||||||
|
{
|
||||||
|
MeshLoader *resultMesh = m_resultMesh;
|
||||||
|
m_resultMesh = nullptr;
|
||||||
|
return resultMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PoseMeshCreator::createMesh()
|
||||||
|
{
|
||||||
|
SkinnedMeshCreator skinnedMeshCreator(m_meshResultContext, m_resultWeights);
|
||||||
|
|
||||||
|
std::vector<QMatrix4x4> matricies;
|
||||||
|
matricies.resize(m_resultNodes.size());
|
||||||
|
for (decltype(m_resultNodes.size()) i = 0; i < m_resultNodes.size(); i++) {
|
||||||
|
const auto &node = m_resultNodes[i];
|
||||||
|
matricies[i] = node.transformMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete m_resultMesh;
|
||||||
|
m_resultMesh = skinnedMeshCreator.createMeshFromTransform(matricies);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PoseMeshCreator::process()
|
||||||
|
{
|
||||||
|
createMesh();
|
||||||
|
|
||||||
|
this->moveToThread(QGuiApplication::instance()->thread());
|
||||||
|
emit finished();
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
#ifndef POSE_MESH_CREATOR_H
|
||||||
|
#define POSE_MESH_CREATOR_H
|
||||||
|
#include <QObject>
|
||||||
|
#include "poser.h"
|
||||||
|
#include "meshloader.h"
|
||||||
|
#include "jointnodetree.h"
|
||||||
|
#include "meshresultcontext.h"
|
||||||
|
|
||||||
|
class PoseMeshCreator : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
signals:
|
||||||
|
void finished();
|
||||||
|
public:
|
||||||
|
PoseMeshCreator(const Poser &poser,
|
||||||
|
const MeshResultContext &meshResultContext,
|
||||||
|
const std::map<int, AutoRiggerVertexWeights> &resultWeights);
|
||||||
|
~PoseMeshCreator();
|
||||||
|
void createMesh();
|
||||||
|
MeshLoader *takeResultMesh();
|
||||||
|
public slots:
|
||||||
|
void process();
|
||||||
|
private:
|
||||||
|
std::vector<JointNode> m_resultNodes;
|
||||||
|
MeshResultContext m_meshResultContext;
|
||||||
|
std::map<int, AutoRiggerVertexWeights> m_resultWeights;
|
||||||
|
MeshLoader *m_resultMesh = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,60 @@
|
||||||
|
#include <QThread>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include "posepreviewmanager.h"
|
||||||
|
|
||||||
|
PosePreviewManager::PosePreviewManager()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PosePreviewManager::~PosePreviewManager()
|
||||||
|
{
|
||||||
|
delete m_previewMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PosePreviewManager::isRendering()
|
||||||
|
{
|
||||||
|
return nullptr != m_poseMeshCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PosePreviewManager::postUpdate(const Poser &poser,
|
||||||
|
const MeshResultContext &meshResultContext,
|
||||||
|
const std::map<int, AutoRiggerVertexWeights> &resultWeights)
|
||||||
|
{
|
||||||
|
if (nullptr != m_poseMeshCreator)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
qDebug() << "Pose mesh generating..";
|
||||||
|
|
||||||
|
QThread *thread = new QThread;
|
||||||
|
m_poseMeshCreator = new PoseMeshCreator(poser, meshResultContext, resultWeights);
|
||||||
|
m_poseMeshCreator->moveToThread(thread);
|
||||||
|
connect(thread, &QThread::started, m_poseMeshCreator, &PoseMeshCreator::process);
|
||||||
|
connect(m_poseMeshCreator, &PoseMeshCreator::finished, this, &PosePreviewManager::poseMeshReady);
|
||||||
|
connect(m_poseMeshCreator, &PoseMeshCreator::finished, thread, &QThread::quit);
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshLoader *PosePreviewManager::takeResultPreviewMesh()
|
||||||
|
{
|
||||||
|
if (nullptr == m_previewMesh)
|
||||||
|
return nullptr;
|
||||||
|
return new MeshLoader(*m_previewMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PosePreviewManager::poseMeshReady()
|
||||||
|
{
|
||||||
|
delete m_previewMesh;
|
||||||
|
m_previewMesh = m_poseMeshCreator->takeResultMesh();
|
||||||
|
|
||||||
|
emit resultPreviewMeshChanged();
|
||||||
|
|
||||||
|
qDebug() << "Pose mesh generation done";
|
||||||
|
|
||||||
|
delete m_poseMeshCreator;
|
||||||
|
m_poseMeshCreator = nullptr;
|
||||||
|
|
||||||
|
emit renderDone();
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
#ifndef POSE_PREVIEW_MANAGER_H
|
||||||
|
#define POSE_PREVIEW_MANAGER_H
|
||||||
|
#include <QWidget>
|
||||||
|
#include "skeletondocument.h"
|
||||||
|
#include "poser.h"
|
||||||
|
#include "posemeshcreator.h"
|
||||||
|
#include "meshloader.h"
|
||||||
|
|
||||||
|
class PosePreviewManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
PosePreviewManager();
|
||||||
|
~PosePreviewManager();
|
||||||
|
bool isRendering();
|
||||||
|
bool postUpdate(const Poser &poser,
|
||||||
|
const MeshResultContext &meshResultContext,
|
||||||
|
const std::map<int, AutoRiggerVertexWeights> &resultWeights);
|
||||||
|
MeshLoader *takeResultPreviewMesh();
|
||||||
|
private slots:
|
||||||
|
void poseMeshReady();
|
||||||
|
signals:
|
||||||
|
void resultPreviewMeshChanged();
|
||||||
|
void renderDone();
|
||||||
|
private:
|
||||||
|
PoseMeshCreator *m_poseMeshCreator = nullptr;
|
||||||
|
MeshLoader *m_previewMesh = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,56 @@
|
||||||
|
#include <QQuaternion>
|
||||||
|
#include "poser.h"
|
||||||
|
|
||||||
|
Poser::Poser(const std::vector<AutoRiggerBone> &bones) :
|
||||||
|
m_bones(bones),
|
||||||
|
m_jointNodeTree(&bones)
|
||||||
|
{
|
||||||
|
for (decltype(m_bones.size()) i = 0; i < m_bones.size(); i++) {
|
||||||
|
m_boneNameToIndexMap[m_bones[i].name] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Poser::~Poser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int Poser::findBoneIndex(const QString &name)
|
||||||
|
{
|
||||||
|
auto findResult = m_boneNameToIndexMap.find(name);
|
||||||
|
if (findResult == m_boneNameToIndexMap.end())
|
||||||
|
return -1;
|
||||||
|
return findResult->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoRiggerBone *Poser::findBone(const QString &name)
|
||||||
|
{
|
||||||
|
auto findResult = m_boneNameToIndexMap.find(name);
|
||||||
|
if (findResult == m_boneNameToIndexMap.end())
|
||||||
|
return nullptr;
|
||||||
|
return &m_bones[findResult->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<AutoRiggerBone> &Poser::bones() const
|
||||||
|
{
|
||||||
|
return m_bones;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<JointNode> &Poser::resultNodes() const
|
||||||
|
{
|
||||||
|
return m_jointNodeTree.nodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Poser::commit()
|
||||||
|
{
|
||||||
|
m_jointNodeTree.recalculateTransformMatrices();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Poser::reset()
|
||||||
|
{
|
||||||
|
m_jointNodeTree.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<QString, std::map<QString, QString>> &Poser::parameters()
|
||||||
|
{
|
||||||
|
return m_parameters;
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
#ifndef POSER_H
|
||||||
|
#define POSER_H
|
||||||
|
#include <QObject>
|
||||||
|
#include "autorigger.h"
|
||||||
|
#include "jointnodetree.h"
|
||||||
|
#include "dust3dutil.h"
|
||||||
|
|
||||||
|
class Poser : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
Poser(const std::vector<AutoRiggerBone> &bones);
|
||||||
|
~Poser();
|
||||||
|
const AutoRiggerBone *findBone(const QString &name);
|
||||||
|
int findBoneIndex(const QString &name);
|
||||||
|
const std::vector<AutoRiggerBone> &bones() const;
|
||||||
|
const std::vector<JointNode> &resultNodes() const;
|
||||||
|
std::map<QString, std::map<QString, QString>> ¶meters();
|
||||||
|
void commit();
|
||||||
|
void reset();
|
||||||
|
protected:
|
||||||
|
std::vector<AutoRiggerBone> m_bones;
|
||||||
|
std::map<QString, int> m_boneNameToIndexMap;
|
||||||
|
JointNodeTree m_jointNodeTree;
|
||||||
|
std::map<QString, std::map<QString, QString>> m_parameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -18,6 +18,13 @@ RigGenerator::~RigGenerator()
|
||||||
delete m_resultWeights;
|
delete m_resultWeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MeshResultContext *RigGenerator::takeMeshResultContext()
|
||||||
|
{
|
||||||
|
MeshResultContext *resultContext = m_meshResultContext;
|
||||||
|
m_meshResultContext = nullptr;
|
||||||
|
return resultContext;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<AutoRiggerBone> *RigGenerator::takeResultBones()
|
std::vector<AutoRiggerBone> *RigGenerator::takeResultBones()
|
||||||
{
|
{
|
||||||
std::vector<AutoRiggerBone> *resultBones = m_resultBones;
|
std::vector<AutoRiggerBone> *resultBones = m_resultBones;
|
||||||
|
@ -114,18 +121,17 @@ void RigGenerator::process()
|
||||||
m_resultBones = new std::vector<AutoRiggerBone>;
|
m_resultBones = new std::vector<AutoRiggerBone>;
|
||||||
*m_resultBones = resultBones;
|
*m_resultBones = resultBones;
|
||||||
|
|
||||||
for (size_t vertexIndex = 0; vertexIndex < inputVerticesColors.size(); vertexIndex++) {
|
for (const auto &weightItem: resultWeights) {
|
||||||
auto findResult = resultWeights.find((int)vertexIndex);
|
size_t vertexIndex = weightItem.first;
|
||||||
|
const auto &weight = weightItem.second;
|
||||||
int blendR = 0, blendG = 0, blendB = 0;
|
int blendR = 0, blendG = 0, blendB = 0;
|
||||||
if (findResult != resultWeights.end()) {
|
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
int boneIndex = findResult->second.boneIndicies[i];
|
int boneIndex = weight.boneIndicies[i];
|
||||||
if (boneIndex > 0) {
|
if (boneIndex > 0) {
|
||||||
const auto &bone = resultBones[boneIndex];
|
const auto &bone = resultBones[boneIndex];
|
||||||
blendR += bone.color.red() * findResult->second.boneWeights[i];
|
blendR += bone.color.red() * weight.boneWeights[i];
|
||||||
blendG += bone.color.green() * findResult->second.boneWeights[i];
|
blendG += bone.color.green() * weight.boneWeights[i];
|
||||||
blendB += bone.color.blue() * findResult->second.boneWeights[i];
|
blendB += bone.color.blue() * weight.boneWeights[i];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QColor blendColor = QColor(blendR, blendG, blendB, 255);
|
QColor blendColor = QColor(blendR, blendG, blendB, 255);
|
||||||
|
@ -135,14 +141,15 @@ void RigGenerator::process()
|
||||||
|
|
||||||
// Smooth normals
|
// Smooth normals
|
||||||
|
|
||||||
std::map<int, QVector3D> vertexNormalMap;
|
std::vector<QVector3D> vertexNormalMap;
|
||||||
|
vertexNormalMap.resize(inputVerticesPositions.size());
|
||||||
for (size_t triangleIndex = 0; triangleIndex < m_meshResultContext->triangles.size(); triangleIndex++) {
|
for (size_t triangleIndex = 0; triangleIndex < m_meshResultContext->triangles.size(); triangleIndex++) {
|
||||||
const auto &sourceTriangle = m_meshResultContext->triangles[triangleIndex];
|
const auto &sourceTriangle = m_meshResultContext->triangles[triangleIndex];
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
vertexNormalMap[sourceTriangle.indicies[i]] += sourceTriangle.normal;
|
vertexNormalMap[sourceTriangle.indicies[i]] += sourceTriangle.normal;
|
||||||
}
|
}
|
||||||
for (auto &item: vertexNormalMap)
|
for (auto &item: vertexNormalMap)
|
||||||
item.second.normalize();
|
item.normalize();
|
||||||
|
|
||||||
// Create mesh for demo
|
// Create mesh for demo
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ public:
|
||||||
std::map<int, AutoRiggerVertexWeights> *takeResultWeights();
|
std::map<int, AutoRiggerVertexWeights> *takeResultWeights();
|
||||||
const std::vector<QString> &missingMarkNames();
|
const std::vector<QString> &missingMarkNames();
|
||||||
const std::vector<QString> &errorMarkNames();
|
const std::vector<QString> &errorMarkNames();
|
||||||
|
MeshResultContext *takeMeshResultContext();
|
||||||
signals:
|
signals:
|
||||||
void finished();
|
void finished();
|
||||||
public slots:
|
public slots:
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
#include "rigwidget.h"
|
#include "rigwidget.h"
|
||||||
#include "rigtype.h"
|
#include "rigtype.h"
|
||||||
#include "infolabel.h"
|
#include "infolabel.h"
|
||||||
|
#include "theme.h"
|
||||||
|
|
||||||
RigWidget::RigWidget(const SkeletonDocument *document, QWidget *parent) :
|
RigWidget::RigWidget(const SkeletonDocument *document, QWidget *parent) :
|
||||||
QWidget(parent),
|
QWidget(parent),
|
||||||
|
@ -17,7 +19,15 @@ RigWidget::RigWidget(const SkeletonDocument *document, QWidget *parent) :
|
||||||
m_rigTypeBox->addItem(RigTypeToDispName(rigType));
|
m_rigTypeBox->addItem(RigTypeToDispName(rigType));
|
||||||
}
|
}
|
||||||
|
|
||||||
formLayout->addRow(tr("Type"), m_rigTypeBox);
|
//m_poseCheckButton = new QPushButton(QChar(fa::child));
|
||||||
|
//Theme::initAwesomeButton(m_poseCheckButton);
|
||||||
|
//m_poseCheckButton->hide();
|
||||||
|
|
||||||
|
QHBoxLayout *controlsLayout = new QHBoxLayout;
|
||||||
|
controlsLayout->addWidget(m_rigTypeBox);
|
||||||
|
//controlsLayout->addWidget(m_poseCheckButton);
|
||||||
|
|
||||||
|
formLayout->addRow(tr("Type"), controlsLayout);
|
||||||
|
|
||||||
m_rigTypeBox->setCurrentIndex((int)m_document->rigType);
|
m_rigTypeBox->setCurrentIndex((int)m_document->rigType);
|
||||||
|
|
||||||
|
@ -28,6 +38,8 @@ RigWidget::RigWidget(const SkeletonDocument *document, QWidget *parent) :
|
||||||
|
|
||||||
m_rigWeightRenderWidget = new ModelWidget(this);
|
m_rigWeightRenderWidget = new ModelWidget(this);
|
||||||
m_rigWeightRenderWidget->setMinimumSize(128, 128);
|
m_rigWeightRenderWidget->setMinimumSize(128, 128);
|
||||||
|
//m_rigWeightRenderWidget->resize(256, 256);
|
||||||
|
//m_rigWeightRenderWidget->move(-64, 0);
|
||||||
m_rigWeightRenderWidget->setXRotation(0);
|
m_rigWeightRenderWidget->setXRotation(0);
|
||||||
m_rigWeightRenderWidget->setYRotation(0);
|
m_rigWeightRenderWidget->setYRotation(0);
|
||||||
m_rigWeightRenderWidget->setZRotation(0);
|
m_rigWeightRenderWidget->setZRotation(0);
|
||||||
|
@ -50,6 +62,7 @@ RigWidget::RigWidget(const SkeletonDocument *document, QWidget *parent) :
|
||||||
|
|
||||||
void RigWidget::rigTypeChanged()
|
void RigWidget::rigTypeChanged()
|
||||||
{
|
{
|
||||||
|
//m_poseCheckButton->setVisible(RigType::None != m_document->rigType);
|
||||||
m_rigTypeBox->setCurrentIndex((int)m_document->rigType);
|
m_rigTypeBox->setCurrentIndex((int)m_document->rigType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#define RIG_WIDGET_H
|
#define RIG_WIDGET_H
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QPushButton>
|
||||||
#include "skeletondocument.h"
|
#include "skeletondocument.h"
|
||||||
#include "rigtype.h"
|
#include "rigtype.h"
|
||||||
#include "modelwidget.h"
|
#include "modelwidget.h"
|
||||||
|
|
|
@ -50,7 +50,8 @@ SkeletonDocument::SkeletonDocument() :
|
||||||
m_resultRigWeightMesh(nullptr),
|
m_resultRigWeightMesh(nullptr),
|
||||||
m_resultRigBones(nullptr),
|
m_resultRigBones(nullptr),
|
||||||
m_resultRigWeights(nullptr),
|
m_resultRigWeights(nullptr),
|
||||||
m_isRigObsolete(false)
|
m_isRigObsolete(false),
|
||||||
|
m_riggedResultContext(new MeshResultContext)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2263,6 +2264,11 @@ void SkeletonDocument::rigReady()
|
||||||
m_resultRigMissingMarkNames = m_rigGenerator->missingMarkNames();
|
m_resultRigMissingMarkNames = m_rigGenerator->missingMarkNames();
|
||||||
m_resultRigErrorMarkNames = m_rigGenerator->errorMarkNames();
|
m_resultRigErrorMarkNames = m_rigGenerator->errorMarkNames();
|
||||||
|
|
||||||
|
delete m_riggedResultContext;
|
||||||
|
m_riggedResultContext = m_rigGenerator->takeMeshResultContext();
|
||||||
|
if (nullptr == m_riggedResultContext)
|
||||||
|
m_riggedResultContext = new MeshResultContext;
|
||||||
|
|
||||||
delete m_rigGenerator;
|
delete m_rigGenerator;
|
||||||
m_rigGenerator = nullptr;
|
m_rigGenerator = nullptr;
|
||||||
|
|
||||||
|
@ -2277,12 +2283,12 @@ void SkeletonDocument::rigReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<AutoRiggerBone> *SkeletonDocument::resultRigBones()
|
const std::vector<AutoRiggerBone> *SkeletonDocument::resultRigBones() const
|
||||||
{
|
{
|
||||||
return m_resultRigBones;
|
return m_resultRigBones;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::map<int, AutoRiggerVertexWeights> *SkeletonDocument::resultRigWeights()
|
const std::map<int, AutoRiggerVertexWeights> *SkeletonDocument::resultRigWeights() const
|
||||||
{
|
{
|
||||||
return m_resultRigWeights;
|
return m_resultRigWeights;
|
||||||
}
|
}
|
||||||
|
@ -2328,3 +2334,8 @@ const std::vector<QString> &SkeletonDocument::resultRigErrorMarkNames() const
|
||||||
{
|
{
|
||||||
return m_resultRigErrorMarkNames;
|
return m_resultRigErrorMarkNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MeshResultContext &SkeletonDocument::currentRiggedResultContext() const
|
||||||
|
{
|
||||||
|
return *m_riggedResultContext;
|
||||||
|
}
|
||||||
|
|
|
@ -427,8 +427,8 @@ public:
|
||||||
MeshLoader *takeResultMesh();
|
MeshLoader *takeResultMesh();
|
||||||
MeshLoader *takeResultTextureMesh();
|
MeshLoader *takeResultTextureMesh();
|
||||||
MeshLoader *takeResultRigWeightMesh();
|
MeshLoader *takeResultRigWeightMesh();
|
||||||
const std::vector<AutoRiggerBone> *resultRigBones();
|
const std::vector<AutoRiggerBone> *resultRigBones() const;
|
||||||
const std::map<int, AutoRiggerVertexWeights> *resultRigWeights();
|
const std::map<int, AutoRiggerVertexWeights> *resultRigWeights() const;
|
||||||
void updateTurnaround(const QImage &image);
|
void updateTurnaround(const QImage &image);
|
||||||
void setSharedContextWidget(QOpenGLWidget *sharedContextWidget);
|
void setSharedContextWidget(QOpenGLWidget *sharedContextWidget);
|
||||||
bool hasPastableContentInClipboard() const;
|
bool hasPastableContentInClipboard() const;
|
||||||
|
@ -445,6 +445,7 @@ public:
|
||||||
void collectComponentDescendantComponents(QUuid componentId, std::vector<QUuid> &componentIds) const;
|
void collectComponentDescendantComponents(QUuid componentId, std::vector<QUuid> &componentIds) const;
|
||||||
const std::vector<QString> &resultRigMissingMarkNames() const;
|
const std::vector<QString> &resultRigMissingMarkNames() const;
|
||||||
const std::vector<QString> &resultRigErrorMarkNames() const;
|
const std::vector<QString> &resultRigErrorMarkNames() const;
|
||||||
|
const MeshResultContext ¤tRiggedResultContext() const;
|
||||||
public slots:
|
public slots:
|
||||||
void removeNode(QUuid nodeId);
|
void removeNode(QUuid nodeId);
|
||||||
void removeEdge(QUuid edgeId);
|
void removeEdge(QUuid edgeId);
|
||||||
|
@ -563,14 +564,15 @@ private: // need initialize
|
||||||
MeshLoader *m_resultRigWeightMesh;
|
MeshLoader *m_resultRigWeightMesh;
|
||||||
std::vector<AutoRiggerBone> *m_resultRigBones;
|
std::vector<AutoRiggerBone> *m_resultRigBones;
|
||||||
std::map<int, AutoRiggerVertexWeights> *m_resultRigWeights;
|
std::map<int, AutoRiggerVertexWeights> *m_resultRigWeights;
|
||||||
|
MeshResultContext *m_riggedResultContext;
|
||||||
bool m_isRigObsolete;
|
bool m_isRigObsolete;
|
||||||
std::vector<QString> m_resultRigMissingMarkNames;
|
|
||||||
std::vector<QString> m_resultRigErrorMarkNames;
|
|
||||||
private:
|
private:
|
||||||
static unsigned long m_maxSnapshot;
|
static unsigned long m_maxSnapshot;
|
||||||
std::deque<SkeletonHistoryItem> m_undoItems;
|
std::deque<SkeletonHistoryItem> m_undoItems;
|
||||||
std::deque<SkeletonHistoryItem> m_redoItems;
|
std::deque<SkeletonHistoryItem> m_redoItems;
|
||||||
GeneratedCacheContext m_generatedCacheContext;
|
GeneratedCacheContext m_generatedCacheContext;
|
||||||
|
std::vector<QString> m_resultRigMissingMarkNames;
|
||||||
|
std::vector<QString> m_resultRigErrorMarkNames;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
#include "rigwidget.h"
|
#include "rigwidget.h"
|
||||||
#include "modelofflinerender.h"
|
#include "modelofflinerender.h"
|
||||||
#include "markiconcreator.h"
|
#include "markiconcreator.h"
|
||||||
|
#include "tetrapodposeeditwidget.h"
|
||||||
|
|
||||||
int SkeletonDocumentWindow::m_modelRenderWidgetInitialX = 16;
|
int SkeletonDocumentWindow::m_modelRenderWidgetInitialX = 16;
|
||||||
int SkeletonDocumentWindow::m_modelRenderWidgetInitialY = 16;
|
int SkeletonDocumentWindow::m_modelRenderWidgetInitialY = 16;
|
||||||
|
@ -209,7 +210,6 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() :
|
||||||
partTreeWidget->setGraphicsFunctions(graphicsWidget);
|
partTreeWidget->setGraphicsFunctions(graphicsWidget);
|
||||||
partTreeDocker->setWidget(partTreeWidget);
|
partTreeDocker->setWidget(partTreeWidget);
|
||||||
addDockWidget(Qt::RightDockWidgetArea, partTreeDocker);
|
addDockWidget(Qt::RightDockWidgetArea, partTreeDocker);
|
||||||
//partTreeDocker->hide();
|
|
||||||
|
|
||||||
QDockWidget *rigDocker = new QDockWidget(tr("Rig"), this);
|
QDockWidget *rigDocker = new QDockWidget(tr("Rig"), this);
|
||||||
rigDocker->setAllowedAreas(Qt::RightDockWidgetArea);
|
rigDocker->setAllowedAreas(Qt::RightDockWidgetArea);
|
||||||
|
@ -217,7 +217,16 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() :
|
||||||
rigDocker->setWidget(rigWidget);
|
rigDocker->setWidget(rigWidget);
|
||||||
addDockWidget(Qt::RightDockWidgetArea, rigDocker);
|
addDockWidget(Qt::RightDockWidgetArea, rigDocker);
|
||||||
|
|
||||||
|
//QDockWidget *animationDocker = new QDockWidget(tr("Animation"), this);
|
||||||
|
//animationDocker->setAllowedAreas(Qt::RightDockWidgetArea);
|
||||||
|
//TetrapodPoseEditWidget *tetrapodPoseEditWidget = new TetrapodPoseEditWidget(m_document, animationDocker);
|
||||||
|
//animationDocker->setWidget(tetrapodPoseEditWidget);
|
||||||
|
//AnimationListWidget *animationListWidget = new AnimationListWidget(m_document, animationDocker);
|
||||||
|
//animationDocker->setWidget(animationListWidget);
|
||||||
|
//addDockWidget(Qt::RightDockWidgetArea, animationDocker);
|
||||||
|
|
||||||
tabifyDockWidget(partTreeDocker, rigDocker);
|
tabifyDockWidget(partTreeDocker, rigDocker);
|
||||||
|
//tabifyDockWidget(rigDocker, animationDocker);
|
||||||
|
|
||||||
partTreeDocker->raise();
|
partTreeDocker->raise();
|
||||||
|
|
||||||
|
@ -495,6 +504,13 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() :
|
||||||
});
|
});
|
||||||
m_windowMenu->addAction(m_showRigAction);
|
m_windowMenu->addAction(m_showRigAction);
|
||||||
|
|
||||||
|
//m_showAnimationAction = new QAction(tr("Animation"), this);
|
||||||
|
//connect(m_showAnimationAction, &QAction::triggered, [=]() {
|
||||||
|
// animationDocker->show();
|
||||||
|
// animationDocker->raise();
|
||||||
|
//});
|
||||||
|
//m_windowMenu->addAction(m_showAnimationAction);
|
||||||
|
|
||||||
m_showDebugDialogAction = new QAction(tr("Debug"), this);
|
m_showDebugDialogAction = new QAction(tr("Debug"), this);
|
||||||
connect(m_showDebugDialogAction, &QAction::triggered, g_logBrowser, &LogBrowser::showDialog);
|
connect(m_showDebugDialogAction, &QAction::triggered, g_logBrowser, &LogBrowser::showDialog);
|
||||||
m_windowMenu->addAction(m_showDebugDialogAction);
|
m_windowMenu->addAction(m_showDebugDialogAction);
|
||||||
|
@ -748,6 +764,8 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() :
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//connect(m_document, &SkeletonDocument::resultRigChanged, tetrapodPoseEditWidget, &TetrapodPoseEditWidget::updatePreview);
|
||||||
|
|
||||||
connect(this, &SkeletonDocumentWindow::initialized, m_document, &SkeletonDocument::uiReady);
|
connect(this, &SkeletonDocumentWindow::initialized, m_document, &SkeletonDocument::uiReady);
|
||||||
|
|
||||||
QTimer *timer = new QTimer(this);
|
QTimer *timer = new QTimer(this);
|
||||||
|
|
|
@ -129,6 +129,7 @@ private:
|
||||||
QAction *m_showPartsListAction;
|
QAction *m_showPartsListAction;
|
||||||
QAction *m_showDebugDialogAction;
|
QAction *m_showDebugDialogAction;
|
||||||
QAction *m_showRigAction;
|
QAction *m_showRigAction;
|
||||||
|
QAction *m_showAnimationAction;
|
||||||
|
|
||||||
QMenu *m_helpMenu;
|
QMenu *m_helpMenu;
|
||||||
QAction *m_viewSourceAction;
|
QAction *m_viewSourceAction;
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
#include "skinnedmeshcreator.h"
|
||||||
|
|
||||||
|
SkinnedMeshCreator::SkinnedMeshCreator(const MeshResultContext &meshResultContext,
|
||||||
|
const std::map<int, AutoRiggerVertexWeights> &resultWeights) :
|
||||||
|
m_meshResultContext(meshResultContext),
|
||||||
|
m_resultWeights(resultWeights)
|
||||||
|
{
|
||||||
|
for (const auto &vertex: m_meshResultContext.vertices) {
|
||||||
|
m_verticesBindPositions.push_back(vertex.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_verticesBindNormals.resize(m_meshResultContext.vertices.size());
|
||||||
|
for (size_t triangleIndex = 0; triangleIndex < m_meshResultContext.triangles.size(); triangleIndex++) {
|
||||||
|
const auto &sourceTriangle = m_meshResultContext.triangles[triangleIndex];
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
m_verticesBindNormals[sourceTriangle.indicies[i]] += sourceTriangle.normal;
|
||||||
|
}
|
||||||
|
for (auto &item: m_verticesBindNormals)
|
||||||
|
item.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshLoader *SkinnedMeshCreator::createMeshFromTransform(const std::vector<QMatrix4x4> &matricies)
|
||||||
|
{
|
||||||
|
std::vector<QVector3D> transformedPositions = m_verticesBindPositions;
|
||||||
|
std::vector<QVector3D> transformedPoseNormals = m_verticesBindNormals;
|
||||||
|
|
||||||
|
if (!matricies.empty()) {
|
||||||
|
for (const auto &weightItem: m_resultWeights) {
|
||||||
|
size_t vertexIndex = weightItem.first;
|
||||||
|
const auto &weight = weightItem.second;
|
||||||
|
QMatrix4x4 mixedMatrix;
|
||||||
|
transformedPositions[vertexIndex] = QVector3D();
|
||||||
|
transformedPoseNormals[vertexIndex] = QVector3D();
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
float factor = weight.boneWeights[i];
|
||||||
|
if (factor > 0) {
|
||||||
|
transformedPositions[vertexIndex] += matricies[weight.boneIndicies[i]] * m_verticesBindPositions[vertexIndex] * factor;
|
||||||
|
transformedPoseNormals[vertexIndex] += matricies[weight.boneIndicies[i]] * m_verticesBindNormals[vertexIndex] * factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vertex *triangleVertices = new Vertex[m_meshResultContext.triangles.size() * 3];
|
||||||
|
int triangleVerticesNum = 0;
|
||||||
|
for (size_t triangleIndex = 0; triangleIndex < m_meshResultContext.triangles.size(); triangleIndex++) {
|
||||||
|
const auto &sourceTriangle = m_meshResultContext.triangles[triangleIndex];
|
||||||
|
QColor triangleColor = m_meshResultContext.triangleColors()[triangleIndex];
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
Vertex ¤tVertex = triangleVertices[triangleVerticesNum++];
|
||||||
|
const auto &sourcePosition = transformedPositions[sourceTriangle.indicies[i]];
|
||||||
|
const auto &sourceColor = triangleColor;
|
||||||
|
const auto &sourceNormal = transformedPoseNormals[sourceTriangle.indicies[i]];
|
||||||
|
currentVertex.posX = sourcePosition.x();
|
||||||
|
currentVertex.posY = sourcePosition.y();
|
||||||
|
currentVertex.posZ = sourcePosition.z();
|
||||||
|
currentVertex.texU = 0;
|
||||||
|
currentVertex.texV = 0;
|
||||||
|
currentVertex.colorR = sourceColor.redF();
|
||||||
|
currentVertex.colorG = sourceColor.greenF();
|
||||||
|
currentVertex.colorB = sourceColor.blueF();
|
||||||
|
currentVertex.normX = sourceNormal.x();
|
||||||
|
currentVertex.normY = sourceNormal.y();
|
||||||
|
currentVertex.normZ = sourceNormal.z();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MeshLoader(triangleVertices, triangleVerticesNum);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
#ifndef SKINNED_MESH_CREATOR_H
|
||||||
|
#define SKINNED_MESH_CREATOR_H
|
||||||
|
#include <QMatrix4x4>
|
||||||
|
#include <vector>
|
||||||
|
#include <QVector3D>
|
||||||
|
#include "meshloader.h"
|
||||||
|
#include "meshresultcontext.h"
|
||||||
|
#include "jointnodetree.h"
|
||||||
|
|
||||||
|
class SkinnedMeshCreator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SkinnedMeshCreator(const MeshResultContext &meshResultContext,
|
||||||
|
const std::map<int, AutoRiggerVertexWeights> &resultWeights);
|
||||||
|
MeshLoader *createMeshFromTransform(const std::vector<QMatrix4x4> &matricies);
|
||||||
|
private:
|
||||||
|
MeshResultContext m_meshResultContext;
|
||||||
|
std::map<int, AutoRiggerVertexWeights> m_resultWeights;
|
||||||
|
std::vector<QVector3D> m_verticesBindPositions;
|
||||||
|
std::vector<QVector3D> m_verticesBindNormals;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,205 @@
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QWidgetAction>
|
||||||
|
#include "theme.h"
|
||||||
|
#include "tetrapodposeeditwidget.h"
|
||||||
|
#include "floatnumberwidget.h"
|
||||||
|
|
||||||
|
TetrapodPoseEditWidget::TetrapodPoseEditWidget(const SkeletonDocument *document, QWidget *parent) :
|
||||||
|
QWidget(parent),
|
||||||
|
m_document(document)
|
||||||
|
{
|
||||||
|
m_posePreviewManager = new PosePreviewManager();
|
||||||
|
connect(m_posePreviewManager, &PosePreviewManager::renderDone, [=]() {
|
||||||
|
if (m_isPreviewDirty)
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
std::map<QString, std::tuple<QPushButton *, PopupWidgetType>> buttons;
|
||||||
|
buttons["Head"] = std::make_tuple(new QPushButton(tr("Head")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["Neck"] = std::make_tuple(new QPushButton(tr("Neck")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["RightUpperArm"] = std::make_tuple(new QPushButton(tr("UpperArm")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["RightLowerArm"] = std::make_tuple(new QPushButton(tr("LowerArm")), PopupWidgetType::Intersection);
|
||||||
|
buttons["RightHand"] = std::make_tuple(new QPushButton(tr("Hand")), PopupWidgetType::Intersection);
|
||||||
|
buttons["LeftUpperArm"] = std::make_tuple(new QPushButton(tr("UpperArm")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["LeftLowerArm"] = std::make_tuple(new QPushButton(tr("LowerArm")), PopupWidgetType::Intersection);
|
||||||
|
buttons["LeftHand"] = std::make_tuple(new QPushButton(tr("Hand")), PopupWidgetType::Intersection);
|
||||||
|
buttons["Chest"] = std::make_tuple(new QPushButton(tr("Chest")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["Spine"] = std::make_tuple(new QPushButton(tr("Spine")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["RightUpperLeg"] = std::make_tuple(new QPushButton(tr("UpperLeg")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["RightLowerLeg"] = std::make_tuple(new QPushButton(tr("LowerLeg")), PopupWidgetType::Intersection);
|
||||||
|
buttons["RightFoot"] = std::make_tuple(new QPushButton(tr("Foot")), PopupWidgetType::Intersection);
|
||||||
|
buttons["LeftUpperLeg"] = std::make_tuple(new QPushButton(tr("UpperLeg")), PopupWidgetType::PitchYawRoll);
|
||||||
|
buttons["LeftLowerLeg"] = std::make_tuple(new QPushButton(tr("LowerLeg")), PopupWidgetType::Intersection);
|
||||||
|
buttons["LeftFoot"] = std::make_tuple(new QPushButton(tr("Foot")), PopupWidgetType::Intersection);
|
||||||
|
|
||||||
|
QGridLayout *marksContainerLayout = new QGridLayout;
|
||||||
|
marksContainerLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
marksContainerLayout->setSpacing(0);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["Head"]), 0, 1);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["Neck"]), 1, 1);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["RightUpperArm"]), 1, 0);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["RightLowerArm"]), 2, 0);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["RightHand"]), 3, 0);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["LeftUpperArm"]), 1, 2);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["LeftLowerArm"]), 2, 2);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["LeftHand"]), 3, 2);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["Chest"]), 2, 1);
|
||||||
|
marksContainerLayout->addWidget(std::get<0>(buttons["Spine"]), 3, 1);
|
||||||
|
|
||||||
|
QGridLayout *lowerMarksContainerLayout = new QGridLayout;
|
||||||
|
lowerMarksContainerLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lowerMarksContainerLayout->setSpacing(0);
|
||||||
|
lowerMarksContainerLayout->addWidget(std::get<0>(buttons["RightUpperLeg"]), 0, 1);
|
||||||
|
lowerMarksContainerLayout->addWidget(std::get<0>(buttons["RightLowerLeg"]), 1, 1);
|
||||||
|
lowerMarksContainerLayout->addWidget(std::get<0>(buttons["RightFoot"]), 2, 0);
|
||||||
|
lowerMarksContainerLayout->addWidget(std::get<0>(buttons["LeftUpperLeg"]), 0, 2);
|
||||||
|
lowerMarksContainerLayout->addWidget(std::get<0>(buttons["LeftLowerLeg"]), 1, 2);
|
||||||
|
lowerMarksContainerLayout->addWidget(std::get<0>(buttons["LeftFoot"]), 2, 3);
|
||||||
|
|
||||||
|
QFont buttonFont;
|
||||||
|
buttonFont.setWeight(QFont::Light);
|
||||||
|
buttonFont.setPixelSize(7);
|
||||||
|
buttonFont.setBold(false);
|
||||||
|
for (const auto &item: buttons) {
|
||||||
|
QString boneName = item.first;
|
||||||
|
QPushButton *buttonWidget = std::get<0>(item.second);
|
||||||
|
PopupWidgetType widgetType = std::get<1>(item.second);
|
||||||
|
buttonWidget->setFont(buttonFont);
|
||||||
|
buttonWidget->setMaximumWidth(45);
|
||||||
|
connect(buttonWidget, &QPushButton::clicked, [this, boneName, widgetType]() {
|
||||||
|
emit showPopupAngleDialog(boneName, widgetType, mapFromGlobal(QCursor::pos()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QVBoxLayout *layout = new QVBoxLayout;
|
||||||
|
layout->addLayout(marksContainerLayout);
|
||||||
|
layout->addLayout(lowerMarksContainerLayout);
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
setLayout(layout);
|
||||||
|
|
||||||
|
connect(m_document, &SkeletonDocument::resultRigChanged, this, &TetrapodPoseEditWidget::updatePreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
TetrapodPoseEditWidget::~TetrapodPoseEditWidget()
|
||||||
|
{
|
||||||
|
delete m_posePreviewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetrapodPoseEditWidget::updatePreview()
|
||||||
|
{
|
||||||
|
if (m_posePreviewManager->isRendering()) {
|
||||||
|
m_isPreviewDirty = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<AutoRiggerBone> *rigBones = m_document->resultRigBones();
|
||||||
|
const std::map<int, AutoRiggerVertexWeights> *rigWeights = m_document->resultRigWeights();
|
||||||
|
|
||||||
|
m_isPreviewDirty = false;
|
||||||
|
|
||||||
|
if (nullptr == rigBones || nullptr == rigWeights) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete m_poser;
|
||||||
|
m_poser = new TetrapodPoser(*rigBones);
|
||||||
|
m_poser->parameters() = m_parameters;
|
||||||
|
m_poser->commit();
|
||||||
|
m_posePreviewManager->postUpdate(*m_poser, m_document->currentRiggedResultContext(), *rigWeights);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetrapodPoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetType popupWidgetType, QPoint pos)
|
||||||
|
{
|
||||||
|
QMenu popupMenu;
|
||||||
|
|
||||||
|
QWidget *popup = new QWidget;
|
||||||
|
|
||||||
|
QVBoxLayout *layout = new QVBoxLayout;
|
||||||
|
|
||||||
|
if (PopupWidgetType::PitchYawRoll == popupWidgetType) {
|
||||||
|
FloatNumberWidget *pitchWidget = new FloatNumberWidget;
|
||||||
|
pitchWidget->setItemName(tr("Pitch"));
|
||||||
|
pitchWidget->setRange(-180, 180);
|
||||||
|
pitchWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "pitch").toFloat());
|
||||||
|
connect(pitchWidget, &FloatNumberWidget::valueChanged, [=](float value) {
|
||||||
|
m_parameters[boneName]["pitch"] = QString::number(value);
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
QPushButton *pitchEraser = new QPushButton(QChar(fa::eraser));
|
||||||
|
Theme::initAwesomeMiniButton(pitchEraser);
|
||||||
|
connect(pitchEraser, &QPushButton::clicked, [=]() {
|
||||||
|
pitchWidget->setValue(0.0);
|
||||||
|
});
|
||||||
|
QHBoxLayout *pitchLayout = new QHBoxLayout;
|
||||||
|
pitchLayout->addWidget(pitchEraser);
|
||||||
|
pitchLayout->addWidget(pitchWidget);
|
||||||
|
layout->addLayout(pitchLayout);
|
||||||
|
|
||||||
|
FloatNumberWidget *yawWidget = new FloatNumberWidget;
|
||||||
|
yawWidget->setItemName(tr("Yaw"));
|
||||||
|
yawWidget->setRange(-180, 180);
|
||||||
|
yawWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "yaw").toFloat());
|
||||||
|
connect(yawWidget, &FloatNumberWidget::valueChanged, [=](float value) {
|
||||||
|
m_parameters[boneName]["yaw"] = QString::number(value);
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
QPushButton *yawEraser = new QPushButton(QChar(fa::eraser));
|
||||||
|
Theme::initAwesomeMiniButton(yawEraser);
|
||||||
|
connect(yawEraser, &QPushButton::clicked, [=]() {
|
||||||
|
yawWidget->setValue(0.0);
|
||||||
|
});
|
||||||
|
QHBoxLayout *yawLayout = new QHBoxLayout;
|
||||||
|
yawLayout->addWidget(yawEraser);
|
||||||
|
yawLayout->addWidget(yawWidget);
|
||||||
|
layout->addLayout(yawLayout);
|
||||||
|
|
||||||
|
FloatNumberWidget *rollWidget = new FloatNumberWidget;
|
||||||
|
rollWidget->setItemName(tr("Roll"));
|
||||||
|
rollWidget->setRange(-180, 180);
|
||||||
|
rollWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "roll").toFloat());
|
||||||
|
connect(rollWidget, &FloatNumberWidget::valueChanged, [=](float value) {
|
||||||
|
m_parameters[boneName]["roll"] = QString::number(value);
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
QPushButton *rollEraser = new QPushButton(QChar(fa::eraser));
|
||||||
|
Theme::initAwesomeMiniButton(rollEraser);
|
||||||
|
connect(rollEraser, &QPushButton::clicked, [=]() {
|
||||||
|
rollWidget->setValue(0.0);
|
||||||
|
});
|
||||||
|
QHBoxLayout *rollLayout = new QHBoxLayout;
|
||||||
|
rollLayout->addWidget(rollEraser);
|
||||||
|
rollLayout->addWidget(rollWidget);
|
||||||
|
layout->addLayout(rollLayout);
|
||||||
|
} else {
|
||||||
|
FloatNumberWidget *intersectionWidget = new FloatNumberWidget;
|
||||||
|
intersectionWidget->setItemName(tr("Intersection"));
|
||||||
|
intersectionWidget->setRange(-180, 180);
|
||||||
|
intersectionWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "intersection").toFloat());
|
||||||
|
connect(intersectionWidget, &FloatNumberWidget::valueChanged, [=](float value) {
|
||||||
|
m_parameters[boneName]["intersection"] = QString::number(value);
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
QPushButton *intersectionEraser = new QPushButton(QChar(fa::eraser));
|
||||||
|
Theme::initAwesomeMiniButton(intersectionEraser);
|
||||||
|
connect(intersectionEraser, &QPushButton::clicked, [=]() {
|
||||||
|
intersectionWidget->setValue(0.0);
|
||||||
|
});
|
||||||
|
QHBoxLayout *intersectionLayout = new QHBoxLayout;
|
||||||
|
intersectionLayout->addWidget(intersectionEraser);
|
||||||
|
intersectionLayout->addWidget(intersectionWidget);
|
||||||
|
layout->addLayout(intersectionLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
popup->setLayout(layout);
|
||||||
|
|
||||||
|
QWidgetAction action(this);
|
||||||
|
action.setDefaultWidget(popup);
|
||||||
|
|
||||||
|
popupMenu.addAction(&action);
|
||||||
|
|
||||||
|
popupMenu.exec(mapToGlobal(pos));
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
#ifndef TETRAPOD_POSE_EDIT_WIDGET_H
|
||||||
|
#define TETRAPOD_POSE_EDIT_WIDGET_H
|
||||||
|
#include <QWidget>
|
||||||
|
#include <map>
|
||||||
|
#include <QPointF>
|
||||||
|
#include "posepreviewmanager.h"
|
||||||
|
#include "tetrapodposer.h"
|
||||||
|
#include "skeletondocument.h"
|
||||||
|
|
||||||
|
enum class PopupWidgetType
|
||||||
|
{
|
||||||
|
PitchYawRoll,
|
||||||
|
Intersection
|
||||||
|
};
|
||||||
|
|
||||||
|
class TetrapodPoseEditWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
TetrapodPoseEditWidget(const SkeletonDocument *document, QWidget *parent=nullptr);
|
||||||
|
~TetrapodPoseEditWidget();
|
||||||
|
public slots:
|
||||||
|
void updatePreview();
|
||||||
|
void showPopupAngleDialog(QString boneName, PopupWidgetType popupWidgetType, QPoint pos);
|
||||||
|
private:
|
||||||
|
const SkeletonDocument *m_document = nullptr;
|
||||||
|
PosePreviewManager *m_posePreviewManager = nullptr;
|
||||||
|
TetrapodPoser *m_poser = nullptr;
|
||||||
|
bool m_isPreviewDirty = false;
|
||||||
|
std::map<QString, std::map<QString, QString>> m_parameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,66 @@
|
||||||
|
#include "tetrapodposer.h"
|
||||||
|
|
||||||
|
TetrapodPoser::TetrapodPoser(const std::vector<AutoRiggerBone> &bones) :
|
||||||
|
Poser(bones)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetrapodPoser::commit()
|
||||||
|
{
|
||||||
|
for (const auto &item: parameters()) {
|
||||||
|
int boneIndex = findBoneIndex(item.first);
|
||||||
|
if (-1 == boneIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto findPitchResult = item.second.find("pitch");
|
||||||
|
auto findYawResult = item.second.find("yaw");
|
||||||
|
auto findRollResult = item.second.find("roll");
|
||||||
|
if (findPitchResult != item.second.end() ||
|
||||||
|
findYawResult != item.second.end() ||
|
||||||
|
findRollResult != item.second.end()) {
|
||||||
|
float yawAngle = valueOfKeyInMapOrEmpty(item.second, "yaw").toFloat();
|
||||||
|
if (item.first.startsWith("Left")) {
|
||||||
|
yawAngle = -yawAngle;
|
||||||
|
}
|
||||||
|
QQuaternion rotation = QQuaternion::fromEulerAngles(valueOfKeyInMapOrEmpty(item.second, "pitch").toFloat(),
|
||||||
|
yawAngle,
|
||||||
|
valueOfKeyInMapOrEmpty(item.second, "roll").toFloat());
|
||||||
|
m_jointNodeTree.updateRotation(boneIndex, rotation);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto findIntersectionResult = item.second.find("intersection");
|
||||||
|
if (findIntersectionResult != item.second.end()) {
|
||||||
|
float intersectionAngle = valueOfKeyInMapOrEmpty(item.second, "intersection").toFloat();
|
||||||
|
const AutoRiggerBone &bone = bones()[boneIndex];
|
||||||
|
if (bone.name == "LeftHand" || bone.name == "RightHand") {
|
||||||
|
QVector3D handDirection = bone.tailPosition - bone.headPosition;
|
||||||
|
QVector3D rotateAxis = QVector3D::crossProduct(handDirection, bone.name == "RightHand" ? QVector3D(1, 0, 0) : QVector3D(-1, 0, 0)).normalized();
|
||||||
|
QQuaternion rotation = QQuaternion::fromAxisAndAngle(rotateAxis, intersectionAngle);
|
||||||
|
m_jointNodeTree.updateRotation(boneIndex, rotation);
|
||||||
|
continue;
|
||||||
|
} else if (bone.name == "LeftLowerArm" || bone.name == "RightLowerArm") {
|
||||||
|
QVector3D lowerArmDirection = bone.tailPosition - bone.headPosition;
|
||||||
|
QVector3D rotateAxis = QVector3D::crossProduct(lowerArmDirection, QVector3D(0, 0, 1)).normalized();
|
||||||
|
QQuaternion rotation = QQuaternion::fromAxisAndAngle(rotateAxis, intersectionAngle);
|
||||||
|
m_jointNodeTree.updateRotation(boneIndex, rotation);
|
||||||
|
continue;
|
||||||
|
} else if (bone.name == "LeftFoot" || bone.name == "RightFoot") {
|
||||||
|
QVector3D footDirection = bone.tailPosition - bone.headPosition;
|
||||||
|
QVector3D rotateAxis = QVector3D::crossProduct(footDirection, QVector3D(0, 1, 0)).normalized();
|
||||||
|
QQuaternion rotation = QQuaternion::fromAxisAndAngle(rotateAxis, intersectionAngle);
|
||||||
|
m_jointNodeTree.updateRotation(boneIndex, rotation);
|
||||||
|
continue;
|
||||||
|
} else if (bone.name == "LeftLowerLeg" || bone.name == "RightLowerLeg") {
|
||||||
|
QVector3D lowerLegDirection = bone.tailPosition - bone.headPosition;
|
||||||
|
QVector3D rotateAxis = QVector3D::crossProduct(lowerLegDirection, QVector3D(0, 0, -1)).normalized();
|
||||||
|
QQuaternion rotation = QQuaternion::fromAxisAndAngle(rotateAxis, intersectionAngle);
|
||||||
|
m_jointNodeTree.updateRotation(boneIndex, rotation);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Poser::commit();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
#ifndef TETRAPOD_POSER_H
|
||||||
|
#define TETRAPOD_POSER_H
|
||||||
|
#include "poser.h"
|
||||||
|
|
||||||
|
class TetrapodPoser : public Poser
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
TetrapodPoser(const std::vector<AutoRiggerBone> &bones);
|
||||||
|
public:
|
||||||
|
void commit();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in New Issue