mirror of
https://github.com/JulianGro/overte.git
synced 2025-08-16 18:25:13 +02:00
Working on skeleton loading, added support for FBX text format (exported by
Blender).
This commit is contained in:
parent
e63923774f
commit
9ba3c3719f
9 changed files with 287 additions and 23 deletions
|
@ -805,6 +805,11 @@ void Menu::editPreferences() {
|
|||
faceURLEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH);
|
||||
form->addRow("Face URL:", faceURLEdit);
|
||||
|
||||
QString skeletonURLString = applicationInstance->getProfile()->getSkeletonModelURL().toString();
|
||||
QLineEdit* skeletonURLEdit = new QLineEdit(skeletonURLString);
|
||||
skeletonURLEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH);
|
||||
form->addRow("Skeleton URL:", skeletonURLEdit);
|
||||
|
||||
QSlider* pupilDilation = new QSlider(Qt::Horizontal);
|
||||
pupilDilation->setValue(applicationInstance->getAvatar()->getHead().getPupilDilation() * pupilDilation->maximum());
|
||||
form->addRow("Pupil Dilation:", pupilDilation);
|
||||
|
|
|
@ -84,6 +84,7 @@ void Avatar::sendAvatarURLsMessage(const QUrl& voxelURL) {
|
|||
Avatar::Avatar(Node* owningNode) :
|
||||
AvatarData(owningNode),
|
||||
_head(this),
|
||||
_body(this),
|
||||
_hand(this),
|
||||
_ballSpringsInitialized(false),
|
||||
_bodyYawDelta(0.0f),
|
||||
|
@ -420,6 +421,7 @@ void Avatar::simulate(float deltaTime, Transmitter* transmitter) {
|
|||
_head.setPosition(_bodyBall[ BODY_BALL_HEAD_BASE ].position);
|
||||
_head.setSkinColor(glm::vec3(SKIN_COLOR[0], SKIN_COLOR[1], SKIN_COLOR[2]));
|
||||
_head.simulate(deltaTime, false);
|
||||
_body.simulate(deltaTime);
|
||||
_hand.simulate(deltaTime, false);
|
||||
|
||||
// use speed and angular velocity to determine walking vs. standing
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include "AvatarTouch.h"
|
||||
#include "AvatarVoxelSystem.h"
|
||||
#include "Balls.h"
|
||||
#include "Body.h"
|
||||
#include "Hand.h"
|
||||
#include "Head.h"
|
||||
#include "InterfaceConfig.h"
|
||||
|
@ -150,8 +151,9 @@ public:
|
|||
const glm::vec3& getHeadJointPosition() const { return _skeleton.joint[ AVATAR_JOINT_HEAD_BASE ].position; }
|
||||
float getScale() const { return _scale; }
|
||||
const glm::vec3& getVelocity() const { return _velocity; }
|
||||
Head& getHead() {return _head; }
|
||||
Hand& getHand() {return _hand; }
|
||||
Head& getHead() { return _head; }
|
||||
Body& getBody() { return _body; }
|
||||
Hand& getHand() { return _hand; }
|
||||
glm::quat getOrientation() const;
|
||||
glm::quat getWorldAlignedOrientation() const;
|
||||
AvatarVoxelSystem* getVoxels() { return &_voxels; }
|
||||
|
@ -196,6 +198,7 @@ protected:
|
|||
};
|
||||
|
||||
Head _head;
|
||||
Body _body;
|
||||
Hand _hand;
|
||||
Skeleton _skeleton;
|
||||
bool _ballSpringsInitialized;
|
||||
|
|
66
interface/src/avatar/Body.cpp
Normal file
66
interface/src/avatar/Body.cpp
Normal file
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// Body.cpp
|
||||
// interface
|
||||
//
|
||||
// Created by Andrzej Kapolka on 10/17/13.
|
||||
// Copyright (c) 2013 High Fidelity, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#include <glm/gtx/transform.hpp>
|
||||
|
||||
#include "Application.h"
|
||||
#include "Body.h"
|
||||
|
||||
Body::Body(Avatar* owningAvatar) : _owningAvatar(owningAvatar) {
|
||||
// we may have been created in the network thread, but we live in the main thread
|
||||
moveToThread(Application::getInstance()->thread());
|
||||
}
|
||||
|
||||
void Body::simulate(float deltaTime) {
|
||||
if (!isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set up joint states on first simulate after load
|
||||
const FBXGeometry& geometry = _skeletonGeometry->getFBXGeometry();
|
||||
if (_jointStates.isEmpty()) {
|
||||
foreach (const FBXJoint& joint, geometry.joints) {
|
||||
JointState state;
|
||||
state.rotation = joint.rotation;
|
||||
_jointStates.append(state);
|
||||
}
|
||||
}
|
||||
|
||||
glm::quat orientation = _owningAvatar->getOrientation();
|
||||
const float MODEL_SCALE = 0.0006f;
|
||||
glm::vec3 scale = glm::vec3(-1.0f, 1.0f, -1.0f) * _owningAvatar->getScale() * MODEL_SCALE;
|
||||
glm::mat4 baseTransform = glm::translate(_owningAvatar->getPosition()) * glm::mat4_cast(orientation) * glm::scale(scale);
|
||||
|
||||
// update the world space transforms for all joints
|
||||
for (int i = 0; i < _jointStates.size(); i++) {
|
||||
JointState& state = _jointStates[i];
|
||||
const FBXJoint& joint = geometry.joints.at(i);
|
||||
if (joint.parentIndex == -1) {
|
||||
state.transform = baseTransform * geometry.offset * joint.preRotation *
|
||||
glm::mat4_cast(state.rotation) * joint.postRotation;
|
||||
|
||||
} else {
|
||||
state.transform = _jointStates[joint.parentIndex].transform * joint.preRotation *
|
||||
glm::mat4_cast(state.rotation) * joint.postRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Body::render(float alpha) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void Body::setSkeletonModelURL(const QUrl& url) {
|
||||
// don't recreate the geometry if it's the same URL
|
||||
if (_skeletonModelURL == url) {
|
||||
return;
|
||||
}
|
||||
_skeletonModelURL = url;
|
||||
|
||||
_skeletonGeometry = Application::getInstance()->getGeometryCache()->getGeometry(url);
|
||||
}
|
50
interface/src/avatar/Body.h
Normal file
50
interface/src/avatar/Body.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// Body.h
|
||||
// interface
|
||||
//
|
||||
// Created by Andrzej Kapolka on 10/17/13.
|
||||
// Copyright (c) 2013 High Fidelity, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef __interface__Body__
|
||||
#define __interface__Body__
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include "renderer/GeometryCache.h"
|
||||
|
||||
/// An avatar body with an arbitrary skeleton.
|
||||
class Body : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
Body(Avatar* owningAvatar);
|
||||
|
||||
bool isActive() const { return _skeletonGeometry && _skeletonGeometry->isLoaded(); }
|
||||
|
||||
void simulate(float deltaTime);
|
||||
bool render(float alpha);
|
||||
|
||||
Q_INVOKABLE void setSkeletonModelURL(const QUrl& url);
|
||||
const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; }
|
||||
|
||||
private:
|
||||
|
||||
Avatar* _owningAvatar;
|
||||
|
||||
QUrl _skeletonModelURL;
|
||||
|
||||
QSharedPointer<NetworkGeometry> _skeletonGeometry;
|
||||
|
||||
class JointState {
|
||||
public:
|
||||
glm::quat rotation;
|
||||
glm::mat4 transform;
|
||||
};
|
||||
|
||||
QVector<JointState> _jointStates;
|
||||
};
|
||||
|
||||
#endif /* defined(__interface__Body__) */
|
|
@ -49,6 +49,14 @@ void Profile::setFaceModelURL(const QUrl& faceModelURL) {
|
|||
Q_ARG(QUrl, _faceModelURL));
|
||||
}
|
||||
|
||||
void Profile::setSkeletonModelURL(const QUrl& skeletonModelURL) {
|
||||
_skeletonModelURL = skeletonModelURL;
|
||||
|
||||
QMetaObject::invokeMethod(&Application::getInstance()->getAvatar()->getBody(),
|
||||
"setSkeletonModelURL",
|
||||
Q_ARG(QUrl, _skeletonModelURL));
|
||||
}
|
||||
|
||||
void Profile::updateDomain(const QString& domain) {
|
||||
if (_lastDomain != domain) {
|
||||
_lastDomain = domain;
|
||||
|
@ -91,6 +99,7 @@ void Profile::saveData(QSettings* settings) {
|
|||
settings->setValue("username", _username);
|
||||
settings->setValue("UUID", _uuid);
|
||||
settings->setValue("faceModelURL", _faceModelURL);
|
||||
settings->setValue("skeletonModelURL", _skeletonModelURL);
|
||||
|
||||
settings->endGroup();
|
||||
}
|
||||
|
@ -101,6 +110,7 @@ void Profile::loadData(QSettings* settings) {
|
|||
_username = settings->value("username").toString();
|
||||
this->setUUID(settings->value("UUID").toUuid());
|
||||
_faceModelURL = settings->value("faceModelURL").toUrl();
|
||||
_skeletonModelURL = settings->value("skeletonModelURL").toUrl();
|
||||
|
||||
settings->endGroup();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ public:
|
|||
void setFaceModelURL(const QUrl& faceModelURL);
|
||||
const QUrl& getFaceModelURL() const { return _faceModelURL; }
|
||||
|
||||
void setSkeletonModelURL(const QUrl& skeletonModelURL);
|
||||
const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; }
|
||||
|
||||
void updateDomain(const QString& domain);
|
||||
void updatePosition(const glm::vec3 position);
|
||||
|
||||
|
@ -43,6 +46,7 @@ private:
|
|||
QString _lastDomain;
|
||||
glm::vec3 _lastPosition;
|
||||
QUrl _faceModelURL;
|
||||
QUrl _skeletonModelURL;
|
||||
};
|
||||
|
||||
#endif /* defined(__hifi__Profile__) */
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <QBuffer>
|
||||
#include <QDataStream>
|
||||
#include <QIODevice>
|
||||
#include <QTextStream>
|
||||
#include <QtDebug>
|
||||
#include <QtEndian>
|
||||
|
||||
|
@ -19,7 +20,7 @@
|
|||
|
||||
using namespace std;
|
||||
|
||||
template<class T> QVariant readArray(QDataStream& in) {
|
||||
template<class T> QVariant readBinaryArray(QDataStream& in) {
|
||||
quint32 arrayLength;
|
||||
quint32 encoding;
|
||||
quint32 compressedLength;
|
||||
|
@ -54,7 +55,7 @@ template<class T> QVariant readArray(QDataStream& in) {
|
|||
return QVariant::fromValue(values);
|
||||
}
|
||||
|
||||
QVariant parseFBXProperty(QDataStream& in) {
|
||||
QVariant parseBinaryFBXProperty(QDataStream& in) {
|
||||
char ch;
|
||||
in.device()->getChar(&ch);
|
||||
switch (ch) {
|
||||
|
@ -89,19 +90,19 @@ QVariant parseFBXProperty(QDataStream& in) {
|
|||
return QVariant::fromValue(value);
|
||||
}
|
||||
case 'f': {
|
||||
return readArray<float>(in);
|
||||
return readBinaryArray<float>(in);
|
||||
}
|
||||
case 'd': {
|
||||
return readArray<double>(in);
|
||||
return readBinaryArray<double>(in);
|
||||
}
|
||||
case 'l': {
|
||||
return readArray<qint64>(in);
|
||||
return readBinaryArray<qint64>(in);
|
||||
}
|
||||
case 'i': {
|
||||
return readArray<qint32>(in);
|
||||
return readBinaryArray<qint32>(in);
|
||||
}
|
||||
case 'b': {
|
||||
return readArray<bool>(in);
|
||||
return readBinaryArray<bool>(in);
|
||||
}
|
||||
case 'S':
|
||||
case 'R': {
|
||||
|
@ -114,7 +115,7 @@ QVariant parseFBXProperty(QDataStream& in) {
|
|||
}
|
||||
}
|
||||
|
||||
FBXNode parseFBXNode(QDataStream& in) {
|
||||
FBXNode parseBinaryFBXNode(QDataStream& in) {
|
||||
quint32 endOffset;
|
||||
quint32 propertyCount;
|
||||
quint32 propertyListLength;
|
||||
|
@ -134,11 +135,11 @@ FBXNode parseFBXNode(QDataStream& in) {
|
|||
node.name = in.device()->read(nameLength);
|
||||
|
||||
for (int i = 0; i < propertyCount; i++) {
|
||||
node.properties.append(parseFBXProperty(in));
|
||||
node.properties.append(parseBinaryFBXProperty(in));
|
||||
}
|
||||
|
||||
while (endOffset > in.device()->pos()) {
|
||||
FBXNode child = parseFBXNode(in);
|
||||
FBXNode child = parseBinaryFBXNode(in);
|
||||
if (child.name.isNull()) {
|
||||
return node;
|
||||
|
||||
|
@ -150,28 +151,149 @@ FBXNode parseFBXNode(QDataStream& in) {
|
|||
return node;
|
||||
}
|
||||
|
||||
class Tokenizer {
|
||||
public:
|
||||
|
||||
Tokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { }
|
||||
|
||||
enum SpecialToken { DATUM_TOKEN = 0x100 };
|
||||
|
||||
int nextToken();
|
||||
const QByteArray& getDatum() const { return _datum; }
|
||||
|
||||
void pushBackToken(int token) { _pushedBackToken = token; }
|
||||
|
||||
private:
|
||||
|
||||
QIODevice* _device;
|
||||
QByteArray _datum;
|
||||
int _pushedBackToken;
|
||||
};
|
||||
|
||||
int Tokenizer::nextToken() {
|
||||
if (_pushedBackToken != -1) {
|
||||
int token = _pushedBackToken;
|
||||
_pushedBackToken = -1;
|
||||
return token;
|
||||
}
|
||||
|
||||
char ch;
|
||||
while (_device->getChar(&ch)) {
|
||||
if (QChar(ch).isSpace()) {
|
||||
continue; // skip whitespace
|
||||
}
|
||||
switch (ch) {
|
||||
case ';':
|
||||
_device->readLine(); // skip the comment
|
||||
break;
|
||||
|
||||
case ':':
|
||||
case '{':
|
||||
case '}':
|
||||
case ',':
|
||||
return ch; // special punctuation
|
||||
|
||||
case '\"':
|
||||
_datum = "";
|
||||
while (_device->getChar(&ch)) {
|
||||
if (ch == '\"') { // end on closing quote
|
||||
break;
|
||||
}
|
||||
if (ch == '\\') { // handle escaped quotes
|
||||
if (_device->getChar(&ch) && ch != '\"') {
|
||||
_datum.append('\\');
|
||||
}
|
||||
}
|
||||
_datum.append(ch);
|
||||
}
|
||||
return DATUM_TOKEN;
|
||||
|
||||
default:
|
||||
_datum = "";
|
||||
_datum.append(ch);
|
||||
while (_device->getChar(&ch)) {
|
||||
if (QChar(ch).isSpace() || ch == ';' || ch == ':' || ch == '{' || ch == '}' || ch == ',' || ch == '\"') {
|
||||
_device->ungetChar(ch); // read until we encounter a special character, then replace it
|
||||
break;
|
||||
}
|
||||
_datum.append(ch);
|
||||
}
|
||||
return DATUM_TOKEN;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
FBXNode parseTextFBXNode(Tokenizer& tokenizer) {
|
||||
FBXNode node;
|
||||
|
||||
if (tokenizer.nextToken() != Tokenizer::DATUM_TOKEN) {
|
||||
return node;
|
||||
}
|
||||
node.name = tokenizer.getDatum();
|
||||
|
||||
if (tokenizer.nextToken() != ':') {
|
||||
return node;
|
||||
}
|
||||
|
||||
int token;
|
||||
bool expectingDatum = true;
|
||||
while ((token = tokenizer.nextToken()) != -1) {
|
||||
if (token == '{') {
|
||||
for (FBXNode child = parseTextFBXNode(tokenizer); !child.name.isNull(); child = parseTextFBXNode(tokenizer)) {
|
||||
node.children.append(child);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
if (token == ',') {
|
||||
expectingDatum = true;
|
||||
|
||||
} else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) {
|
||||
node.properties.append(tokenizer.getDatum());
|
||||
expectingDatum = false;
|
||||
|
||||
} else {
|
||||
tokenizer.pushBackToken(token);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
FBXNode parseFBX(QIODevice* device) {
|
||||
// verify the prolog
|
||||
const QByteArray BINARY_PROLOG = "Kaydara FBX Binary ";
|
||||
if (device->peek(BINARY_PROLOG.size()) != BINARY_PROLOG) {
|
||||
// parse as a text file
|
||||
FBXNode top;
|
||||
Tokenizer tokenizer(device);
|
||||
while (device->bytesAvailable()) {
|
||||
FBXNode next = parseTextFBXNode(tokenizer);
|
||||
if (next.name.isNull()) {
|
||||
return top;
|
||||
|
||||
} else {
|
||||
top.children.append(next);
|
||||
}
|
||||
}
|
||||
return top;
|
||||
}
|
||||
QDataStream in(device);
|
||||
in.setByteOrder(QDataStream::LittleEndian);
|
||||
in.setVersion(QDataStream::Qt_4_5); // for single/double precision switch
|
||||
|
||||
// see http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ for an explanation
|
||||
// of the FBX format
|
||||
|
||||
// verify the prolog
|
||||
const QByteArray EXPECTED_PROLOG = "Kaydara FBX Binary ";
|
||||
if (device->read(EXPECTED_PROLOG.size()) != EXPECTED_PROLOG) {
|
||||
throw QString("Invalid header.");
|
||||
}
|
||||
// of the FBX binary format
|
||||
|
||||
// skip the rest of the header
|
||||
const int HEADER_SIZE = 27;
|
||||
in.skipRawData(HEADER_SIZE - EXPECTED_PROLOG.size());
|
||||
in.skipRawData(HEADER_SIZE);
|
||||
|
||||
// parse the top-level node
|
||||
FBXNode top;
|
||||
while (device->bytesAvailable()) {
|
||||
FBXNode next = parseFBXNode(in);
|
||||
FBXNode next = parseBinaryFBXNode(in);
|
||||
if (next.name.isNull()) {
|
||||
return top;
|
||||
|
||||
|
@ -750,7 +872,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
|
|||
joint.transform = geometry.joints.at(joint.parentIndex).transform *
|
||||
model.preRotation * glm::mat4_cast(model.rotation) * model.postRotation;
|
||||
}
|
||||
geometry.joints.append(joint);
|
||||
geometry.joints.append(joint);
|
||||
geometry.jointIndices.insert(model.name, geometry.joints.size() - 1);
|
||||
}
|
||||
|
||||
// find our special joints
|
||||
|
|
|
@ -99,6 +99,7 @@ class FBXGeometry {
|
|||
public:
|
||||
|
||||
QVector<FBXJoint> joints;
|
||||
QHash<QString, int> jointIndices;
|
||||
|
||||
QVector<FBXMesh> meshes;
|
||||
|
||||
|
|
Loading…
Reference in a new issue