Add end-effector pulling (Shift + Left Button on end-effector).

- Implement CCD (Cyclic Coordinate Descent) IK (Inverse Kinematics) algorithm
- Add end-effector pulling operation, this would be useful when you want to pull a group of nodes in line to align straight or mimic curl.
master
Jeremy Hu 2018-05-24 09:44:40 +08:00
parent b45014e1b8
commit df98193091
9 changed files with 324 additions and 3 deletions

View File

@ -74,7 +74,7 @@ Mouse
+----------------------------+--------------------------------------------------------------------------+
| CTRL + ALT + LEFT BUTTON | Multiple Unselect |
+----------------------------+--------------------------------------------------------------------------+
| SHIFT + LEFT BUTTON | Rotate |
| SHIFT + LEFT BUTTON | Rotate / Pull (End Effector) |
+----------------------------+--------------------------------------------------------------------------+
| RIGHT BUTTON | Context Menu |
+----------------------------+--------------------------------------------------------------------------+

View File

@ -116,6 +116,12 @@ HEADERS += src/exportpreviewwidget.h
SOURCES += src/ambientocclusionbaker.cpp
HEADERS += src/ambientocclusionbaker.h
SOURCES += src/ccdikresolver.cpp
HEADERS += src/ccdikresolver.h
SOURCES += src/skeletonikmover.cpp
HEADERS += src/skeletonikmover.h
HEADERS += src/qtlightmapper.h
SOURCES += src/main.cpp

74
src/ccdikresolver.cpp Normal file
View File

@ -0,0 +1,74 @@
#include <QtGlobal>
#include <QMatrix4x4>
#include <QDebug>
#include "ccdikresolver.h"
CCDIKSolver::CCDIKSolver() :
m_maxRound(5),
m_distanceThreshold2(0.001 * 0.001)
{
}
void CCDIKSolver::setMaxRound(int maxRound)
{
m_maxRound = maxRound;
}
void CCDIKSolver::setDistanceThreshod(float threshold)
{
m_distanceThreshold2 = threshold * threshold;
}
int CCDIKSolver::addNodeInOrder(const QVector3D &position)
{
CCDIKNode node;
node.position = position;
int nodeCount = m_nodes.size();
m_nodes.push_back(node);
return nodeCount;
}
void CCDIKSolver::solveTo(const QVector3D &position)
{
qDebug() << "solveTo:" << position;
m_destination = position;
float lastDistance2 = 0;
for (int i = 0; i < m_maxRound; i++) {
const auto &endEffector = m_nodes[m_nodes.size() - 1];
float distance2 = (endEffector.position - m_destination).lengthSquared();
qDebug() << "Round:" << i << " distance2:" << distance2;
if (distance2 <= m_distanceThreshold2)
break;
if (lastDistance2 > 0 && distance2 >= lastDistance2)
break;
lastDistance2 = distance2;
iterate();
}
}
const QVector3D &CCDIKSolver::getNodeSolvedPosition(int index)
{
Q_ASSERT(index >= 0 && index < m_nodes.size());
return m_nodes[index].position;
}
int CCDIKSolver::getNodeCount(void)
{
return m_nodes.size();
}
void CCDIKSolver::iterate()
{
for (int i = m_nodes.size() - 2; i >= 0; i--) {
const auto &origin = m_nodes[i];
const auto &endEffector = m_nodes[m_nodes.size() - 1];
QVector3D from = endEffector.position - origin.position;
QVector3D to = m_destination - origin.position;
auto quaternion = QQuaternion::rotationTo(from, to);
for (size_t j = i + 1; j <= m_nodes.size() - 1; j++) {
auto &next = m_nodes[j];
const auto offset = next.position - origin.position;
next.position = origin.position + quaternion.rotatedVector(offset).normalized() * offset.length();
}
}
}

31
src/ccdikresolver.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef CCD_IK_SOLVER_H
#define CCD_IK_SOLVER_H
#include <vector>
#include <QVector3D>
#include <QQuaternion>
struct CCDIKNode
{
QVector3D position;
};
class CCDIKSolver
{
public:
CCDIKSolver();
void setMaxRound(int maxRound);
void setDistanceThreshod(float threshold);
int addNodeInOrder(const QVector3D &position);
void solveTo(const QVector3D &position);
const QVector3D &getNodeSolvedPosition(int index);
int getNodeCount(void);
private:
void iterate();
private:
std::vector<CCDIKNode> m_nodes;
QVector3D m_destination;
int m_maxRound;
float m_distanceThreshold2;
};
#endif

View File

@ -454,6 +454,7 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() :
connect(graphicsWidget, &SkeletonGraphicsWidget::addNode, m_document, &SkeletonDocument::addNode);
connect(graphicsWidget, &SkeletonGraphicsWidget::scaleNodeByAddRadius, m_document, &SkeletonDocument::scaleNodeByAddRadius);
connect(graphicsWidget, &SkeletonGraphicsWidget::moveNodeBy, m_document, &SkeletonDocument::moveNodeBy);
connect(graphicsWidget, &SkeletonGraphicsWidget::setNodeOrigin, m_document, &SkeletonDocument::setNodeOrigin);
connect(graphicsWidget, &SkeletonGraphicsWidget::removeNode, m_document, &SkeletonDocument::removeNode);
connect(graphicsWidget, &SkeletonGraphicsWidget::setEditMode, m_document, &SkeletonDocument::setEditMode);
connect(graphicsWidget, &SkeletonGraphicsWidget::removeEdge, m_document, &SkeletonDocument::removeEdge);

View File

@ -37,7 +37,9 @@ SkeletonGraphicsWidget::SkeletonGraphicsWidget(const SkeletonDocument *document)
m_mainOriginItem(nullptr),
m_sideOriginItem(nullptr),
m_hoveredOriginItem(nullptr),
m_checkedOriginItem(nullptr)
m_checkedOriginItem(nullptr),
m_ikMoveUpdateVersion(0),
m_ikMover(nullptr)
{
setRenderHint(QPainter::Antialiasing, false);
setBackgroundBrush(QBrush(QWidget::palette().color(QWidget::backgroundRole()), Qt::SolidPattern));
@ -609,7 +611,30 @@ bool SkeletonGraphicsWidget::mouseMove(QMouseEvent *event)
float byX = mouseScenePos.x() - m_lastScenePos.x();
float byY = mouseScenePos.y() - m_lastScenePos.y();
if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier)) {
rotateSelected(byX);
std::set<SkeletonGraphicsNodeItem *> nodeItems;
readMergedSkeletonNodeSetFromRangeSelection(&nodeItems);
if (nodeItems.size() == 1) {
auto &nodeItem = *nodeItems.begin();
const SkeletonNode *node = m_document->findNode(nodeItem->id());
if (node->edgeIds.size() == 1) {
const auto origin = nodeItem->origin();
byX = mouseScenePos.x() - origin.x();
byY = mouseScenePos.y() - origin.y();
byX = sceneRadiusToUnified(byX);
byY = sceneRadiusToUnified(byY);
QVector3D target = QVector3D(node->x, node->y, node->z);
if (SkeletonProfile::Main == nodeItem->profile()) {
target.setX(target.x() + byX);
target.setY(target.y() + byY);
} else {
target.setY(target.y() + byY);
target.setZ(target.z() + byX);
}
emit ikMove(nodeItem->id(), target);
}
} else {
rotateSelected(byX);
}
} else {
moveSelected(byX, byY);
}
@ -671,6 +696,8 @@ void SkeletonGraphicsWidget::moveSelected(float byX, float byY)
if (m_rangeSelectionSet.empty())
return;
m_ikMoveEndEffectorId = QUuid();
byX = sceneRadiusToUnified(byX);
byY = sceneRadiusToUnified(byY);
std::set<SkeletonGraphicsNodeItem *> nodeItems;
@ -1837,4 +1864,79 @@ void SkeletonGraphicsWidget::setItemHoveredOnAllProfiles(QGraphicsItem *item, bo
}
}
void SkeletonGraphicsWidget::ikMoveReady()
{
unsigned long long movedUpdateVersion = m_ikMover->getUpdateVersion();
if (movedUpdateVersion == m_ikMoveUpdateVersion &&
!m_ikMoveEndEffectorId.isNull()) {
emit batchChangeBegin();
for (const auto &it: m_ikMover->ikNodes()) {
emit setNodeOrigin(it.id, it.newPosition.x(), it.newPosition.y(), it.newPosition.z());
}
emit batchChangeEnd();
emit groupOperationAdded();
}
delete m_ikMover;
m_ikMover = nullptr;
if (movedUpdateVersion != m_ikMoveUpdateVersion &&
!m_ikMoveEndEffectorId.isNull()) {
ikMove(m_ikMoveEndEffectorId, m_ikMoveTarget);
}
}
void SkeletonGraphicsWidget::ikMove(QUuid endEffectorId, QVector3D target)
{
m_ikMoveEndEffectorId = endEffectorId;
m_ikMoveTarget = target;
m_ikMoveUpdateVersion++;
if (nullptr != m_ikMover) {
return;
}
QThread *thread = new QThread;
m_ikMover = new SkeletonIkMover();
m_ikMover->setUpdateVersion(m_ikMoveUpdateVersion);
m_ikMover->setTarget(m_ikMoveTarget);
QUuid nodeId = endEffectorId;
std::set<QUuid> historyNodeIds;
std::vector<std::pair<QUuid, QVector3D>> appendNodes;
for (;;) {
historyNodeIds.insert(nodeId);
const auto node = m_document->findNode(nodeId);
if (nullptr == node)
break;
appendNodes.push_back(std::make_pair(nodeId, QVector3D(node->x, node->y, node->z)));
if (node->edgeIds.size() < 1 || node->edgeIds.size() > 2)
break;
QUuid choosenNodeId;
for (const auto &edgeId: node->edgeIds) {
const auto edge = m_document->findEdge(edgeId);
if (nullptr == edge)
break;
QUuid neighborNodeId = edge->neighborOf(nodeId);
if (historyNodeIds.find(neighborNodeId) != historyNodeIds.end())
continue;
choosenNodeId = neighborNodeId;
break;
}
if (choosenNodeId.isNull())
break;
nodeId = choosenNodeId;
}
qDebug() << "ik move nodes:";
for (int i = appendNodes.size() - 1; i >= 0; i--) {
qDebug() << i << appendNodes[i].first << appendNodes[i].second;
m_ikMover->addNode(appendNodes[i].first, appendNodes[i].second);
}
qDebug() << "target:" << m_ikMoveTarget;
m_ikMover->moveToThread(thread);
connect(thread, &QThread::started, m_ikMover, &SkeletonIkMover::process);
connect(m_ikMover, &SkeletonIkMover::finished, this, &SkeletonGraphicsWidget::ikMoveReady);
connect(m_ikMover, &SkeletonIkMover::finished, thread, &QThread::quit);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}

View File

@ -15,6 +15,7 @@
#include "turnaroundloader.h"
#include "theme.h"
#include "dust3dutil.h"
#include "skeletonikmover.h"
class SkeletonGraphicsOriginItem : public QGraphicsPolygonItem
{
@ -366,6 +367,7 @@ signals:
void setXlockState(bool locked);
void setYlockState(bool locked);
void setZlockState(bool locked);
void setNodeOrigin(QUuid nodeId, float x, float y, float z);
public:
SkeletonGraphicsWidget(const SkeletonDocument *document);
std::map<QUuid, std::pair<SkeletonGraphicsNodeItem *, SkeletonGraphicsNodeItem *>> nodeItemMap;
@ -431,6 +433,8 @@ public slots:
void addSelectEdge(QUuid edgeId);
void enableBackgroundBlur();
void disableBackgroundBlur();
void ikMove(QUuid endEffectorId, QVector3D target);
void ikMoveReady();
private slots:
void turnaroundImageReady();
private:
@ -475,6 +479,10 @@ private: //need initalize
SkeletonGraphicsOriginItem *m_sideOriginItem;
SkeletonGraphicsOriginItem *m_hoveredOriginItem;
SkeletonGraphicsOriginItem *m_checkedOriginItem;
unsigned long long m_ikMoveUpdateVersion;
SkeletonIkMover *m_ikMover;
QVector3D m_ikMoveTarget;
QUuid m_ikMoveEndEffectorId;
private:
std::set<QGraphicsItem *> m_rangeSelectionSet;
QPoint m_lastGlobalPos;

61
src/skeletonikmover.cpp Normal file
View File

@ -0,0 +1,61 @@
#include <QGuiApplication>
#include "skeletonikmover.h"
#include "ccdikresolver.h"
SkeletonIkMover::SkeletonIkMover() :
m_updateVersion(0)
{
}
SkeletonIkMover::~SkeletonIkMover()
{
}
void SkeletonIkMover::setTarget(const QVector3D &target)
{
m_target = target;
}
void SkeletonIkMover::setUpdateVersion(unsigned long long version)
{
m_updateVersion = version;
}
unsigned long long SkeletonIkMover::getUpdateVersion()
{
return m_updateVersion;
}
void SkeletonIkMover::addNode(QUuid id, QVector3D position)
{
SkeletonIkNode ikNode;
ikNode.id = id;
ikNode.position = ikNode.newPosition = position;
m_ikNodes.push_back(ikNode);
}
const std::vector<SkeletonIkNode> &SkeletonIkMover::ikNodes()
{
return m_ikNodes;
}
void SkeletonIkMover::process()
{
resolve();
this->moveToThread(QGuiApplication::instance()->thread());
emit finished();
}
void SkeletonIkMover::resolve()
{
CCDIKSolver solver;
for (auto i = 0u; i < m_ikNodes.size(); i++) {
solver.addNodeInOrder(m_ikNodes[i].position);
}
solver.solveTo(m_target);
for (auto i = 0u; i < m_ikNodes.size(); i++) {
m_ikNodes[i].newPosition = solver.getNodeSolvedPosition(i);
}
}

38
src/skeletonikmover.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef SKELETON_IK_MOVER_H
#define SKELETON_IK_MOVER_H
#include <QObject>
#include <vector>
#include <QVector3D>
#include <QUuid>
struct SkeletonIkNode
{
QUuid id;
QVector3D position;
QVector3D newPosition;
};
class SkeletonIkMover : public QObject
{
Q_OBJECT
public:
SkeletonIkMover();
~SkeletonIkMover();
void setTarget(const QVector3D &target);
void setUpdateVersion(unsigned long long version);
unsigned long long getUpdateVersion();
void addNode(QUuid id, QVector3D position);
const std::vector<SkeletonIkNode> &ikNodes();
signals:
void finished();
public slots:
void process();
private:
void resolve();
private:
unsigned long long m_updateVersion;
std::vector<SkeletonIkNode> m_ikNodes;
QVector3D m_target;
};
#endif