diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index 470bbab8b6..a0b8fba1da 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -76,3 +76,5 @@ AnimPose::operator glm::mat4() const { return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), glm::vec4(zAxis, 0.0f), glm::vec4(_trans, 1.0f)); } + + diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 27ba7a38a4..daa1c2618f 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1732,6 +1732,14 @@ glm::mat4 Rig::getJointTransform(int jointIndex) const { } } +AnimPose Rig::getJointPose(int jointIndex) const { + if (isIndexValid(jointIndex)) { + return _internalPoseSet._absolutePoses[jointIndex]; + } else { + return AnimPose::identity; + } +} + void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { const AnimPose geometryToRigPose(_geometryToRigTransform); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2b276386a0..6968d7c3af 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -164,6 +164,7 @@ public: // rig space glm::mat4 getJointTransform(int jointIndex) const; + AnimPose getJointPose(int jointIndex) const; // Start or stop animations as needed. void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState); diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 2150463065..50d40c35ac 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -116,6 +116,7 @@ public: int jointIndex; glm::mat4 inverseBindMatrix; + Transform inverseBindTransform; }; const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; @@ -225,7 +226,7 @@ public: QVector texCoords; QVector texCoords1; QVector clusterIndices; - QVector clusterWeights; + QVector clusterWeights; QVector originalIndices; QVector clusters; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 1694e31e1d..659d6dfa1e 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1675,6 +1675,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS fbxCluster.jointIndex = 0; } fbxCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform; + fbxCluster.inverseBindTransform = Transform(fbxCluster.inverseBindMatrix); extracted.mesh.clusters.append(fbxCluster); // override the bind rotation with the transform link @@ -1789,9 +1790,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } if (totalWeight > 0.0f) { const float ALMOST_HALF = 0.499f; - float weightScalingFactor = (float)(UINT8_MAX) / totalWeight; + float weightScalingFactor = (float)(UINT16_MAX) / totalWeight; for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { - extracted.mesh.clusterWeights[k] = (uint8_t)(weightScalingFactor * weightAccumulators[k] + ALMOST_HALF); + extracted.mesh.clusterWeights[k] = (uint16_t)(weightScalingFactor * weightAccumulators[k] + ALMOST_HALF); } } } diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp index b9549e2c4e..309c421052 100644 --- a/libraries/fbx/src/FBXReader_Mesh.cpp +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -624,7 +624,8 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // we need 16 bits instead of just 8 for clusterIndices clusterIndicesSize *= 2; } - const int clusterWeightsSize = fbxMesh.clusterWeights.size() * sizeof(uint8_t); + + const int clusterWeightsSize = fbxMesh.clusterWeights.size() * sizeof(uint16_t); // Normals and tangents are interleaved const int normalsOffset = 0; @@ -759,7 +760,7 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { if (clusterWeightsSize) { mesh->addAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, model::BufferView(attribBuffer, clusterWeightsOffset, clusterWeightsSize, - gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::XYZW))); + gpu::Element(gpu::VEC4, gpu::NUINT16, gpu::XYZW))); } diff --git a/libraries/gl/src/gl/GLShaders.cpp b/libraries/gl/src/gl/GLShaders.cpp index 8ef0198676..017c92b71c 100644 --- a/libraries/gl/src/gl/GLShaders.cpp +++ b/libraries/gl/src/gl/GLShaders.cpp @@ -63,12 +63,17 @@ namespace gl { } */ - qCWarning(glLogging) << "GLShader::compileShader - failed to compile the gl shader object:"; + qCCritical(glLogging) << "GLShader::compileShader - failed to compile the gl shader object:"; + int lineNumber = 0; for (auto s : srcstr) { - qCWarning(glLogging) << s; + QString str(s); + QStringList lines = str.split("\n"); + for (auto& line : lines) { + qCCritical(glLogging).noquote() << QString("%1: %2").arg(lineNumber++, 5, 10, QChar('0')).arg(line); + } } - qCWarning(glLogging) << "GLShader::compileShader - errors:"; - qCWarning(glLogging) << temp; + qCCritical(glLogging) << "GLShader::compileShader - errors:"; + qCCritical(glLogging) << temp; error = std::string(temp); delete[] temp; diff --git a/libraries/render-utils/src/CauterizedMeshPartPayload.cpp b/libraries/render-utils/src/CauterizedMeshPartPayload.cpp index 9de973480a..3d213840dd 100644 --- a/libraries/render-utils/src/CauterizedMeshPartPayload.cpp +++ b/libraries/render-utils/src/CauterizedMeshPartPayload.cpp @@ -20,16 +20,16 @@ using namespace render; CauterizedMeshPartPayload::CauterizedMeshPartPayload(ModelPointer model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {} -void CauterizedMeshPartPayload::updateClusterBuffer(const std::vector& clusterMatrices, const std::vector& cauterizedClusterMatrices) { - ModelMeshPartPayload::updateClusterBuffer(clusterMatrices); +void CauterizedMeshPartPayload::updateClusterBuffer(const std::vector& clusterTransforms, const std::vector& cauterizedClusterTransforms) { + ModelMeshPartPayload::updateClusterBuffer(clusterTransforms); - if (cauterizedClusterMatrices.size() > 1) { + if (cauterizedClusterTransforms.size() > 1) { if (!_cauterizedClusterBuffer) { - _cauterizedClusterBuffer = std::make_shared(cauterizedClusterMatrices.size() * sizeof(glm::mat4), - (const gpu::Byte*) cauterizedClusterMatrices.data()); + _cauterizedClusterBuffer = std::make_shared(cauterizedClusterTransforms.size() * sizeof(TransformType), + (const gpu::Byte*) cauterizedClusterTransforms.data()); } else { - _cauterizedClusterBuffer->setSubData(0, cauterizedClusterMatrices.size() * sizeof(glm::mat4), - (const gpu::Byte*) cauterizedClusterMatrices.data()); + _cauterizedClusterBuffer->setSubData(0, cauterizedClusterTransforms.size() * sizeof(TransformType), + (const gpu::Byte*) cauterizedClusterTransforms.data()); } } } diff --git a/libraries/render-utils/src/CauterizedMeshPartPayload.h b/libraries/render-utils/src/CauterizedMeshPartPayload.h index 44eddc6e31..2337632047 100644 --- a/libraries/render-utils/src/CauterizedMeshPartPayload.h +++ b/libraries/render-utils/src/CauterizedMeshPartPayload.h @@ -15,7 +15,13 @@ class CauterizedMeshPartPayload : public ModelMeshPartPayload { public: CauterizedMeshPartPayload(ModelPointer model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform); - void updateClusterBuffer(const std::vector& clusterMatrices, const std::vector& cauterizedClusterMatrices); +#if defined(SKIN_DQ) + using TransformType = Model::TransformDualQuaternion; +#else + using TransformType = glm::mat4; +#endif + + void updateClusterBuffer(const std::vector& clusterTransforms, const std::vector& cauterizedClusterTransforms); void updateTransformForCauterizedMesh(const Transform& renderTransform); diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index dbb82ab638..e3f26a43d8 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -9,13 +9,13 @@ #include "CauterizedModel.h" #include +#include #include "AbstractViewStateInterface.h" #include "MeshPartPayload.h" #include "CauterizedMeshPartPayload.h" #include "RenderUtilsLogging.h" - CauterizedModel::CauterizedModel(QObject* parent) : Model(parent) { } @@ -35,7 +35,7 @@ bool CauterizedModel::updateGeometry() { const FBXGeometry& fbxGeometry = getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { Model::MeshState state; - state.clusterMatrices.resize(mesh.clusters.size()); + state.clusterTransforms.resize(mesh.clusters.size()); _cauterizeMeshStates.append(state); } } @@ -109,30 +109,52 @@ void CauterizedModel::updateClusterMatrices() { const FBXMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); +#if defined(SKIN_DQ) + auto jointPose = _rig.getJointPose(cluster.jointIndex); + Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform clusterTransform; + Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + state.clusterTransforms[j] = Model::TransformDualQuaternion(clusterTransform); +#else auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); +#endif } } // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { +#if defined(SKIN_DQ) + AnimPose cauterizePose = _rig.getJointPose(geometry.neckJointIndex); + cauterizePose.scale() = glm::vec3(0.0001f, 0.0001f, 0.0001f); +#else static const glm::mat4 zeroScale( - glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), - glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), - glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), + glm::vec4(0.0001f, 0.0f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0001f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0001f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); auto cauterizeMatrix = _rig.getJointTransform(geometry.neckJointIndex) * zeroScale; - +#endif for (int i = 0; i < _cauterizeMeshStates.size(); i++) { Model::MeshState& state = _cauterizeMeshStates[i]; const FBXMesh& mesh = geometry.meshes.at(i); + for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); - if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) { - jointMatrix = cauterizeMatrix; + + if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) { + // not cauterized so just copy the value from the non-cauterized version. + state.clusterTransforms[j] = _meshStates[i].clusterTransforms[j]; + } else { +#if defined(SKIN_DQ) + Transform jointTransform(cauterizePose.rot(), cauterizePose.scale(), cauterizePose.trans()); + Transform clusterTransform; + Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + state.clusterTransforms[j] = Model::TransformDualQuaternion(clusterTransform); +#else + glm_mat4u_mul(cauterizeMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); +#endif } - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); } } } @@ -189,24 +211,38 @@ void CauterizedModel::updateRenderItems() { auto itemID = self->_modelMeshRenderItemIDs[i]; auto meshIndex = self->_modelMeshRenderItemShapes[i].meshIndex; - auto clusterMatrices(self->getMeshState(meshIndex).clusterMatrices); - auto clusterMatricesCauterized(self->getCauterizeMeshState(meshIndex).clusterMatrices); + auto clusterTransforms(self->getMeshState(meshIndex).clusterTransforms); + auto clusterTransformsCauterized(self->getCauterizeMeshState(meshIndex).clusterTransforms); bool invalidatePayloadShapeKey = self->shouldInvalidatePayloadShapeKey(meshIndex); - transaction.updateItem(itemID, [modelTransform, clusterMatrices, clusterMatricesCauterized, invalidatePayloadShapeKey, + transaction.updateItem(itemID, [modelTransform, clusterTransforms, clusterTransformsCauterized, invalidatePayloadShapeKey, isWireframe, isVisible, isLayeredInFront, isLayeredInHUD, enableCauterization](CauterizedMeshPartPayload& data) { - data.updateClusterBuffer(clusterMatrices, clusterMatricesCauterized); + data.updateClusterBuffer(clusterTransforms, clusterTransformsCauterized); Transform renderTransform = modelTransform; - if (clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(clusterMatrices[0])); + if (clusterTransforms.size() == 1) { +#if defined(SKIN_DQ) + Transform transform(clusterTransforms[0].getRotation(), + clusterTransforms[0].getScale(), + clusterTransforms[0].getTranslation()); + renderTransform = modelTransform.worldTransform(transform); +#else + renderTransform = modelTransform.worldTransform(Transform(clusterTransforms[0])); +#endif } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); renderTransform = modelTransform; - if (clusterMatricesCauterized.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(clusterMatricesCauterized[0])); + if (clusterTransformsCauterized.size() == 1) { +#if defined(SKIN_DQ) + Transform transform(clusterTransforms[0].getRotation(), + clusterTransforms[0].getScale(), + clusterTransforms[0].getTranslation()); + renderTransform = modelTransform.worldTransform(Transform(transform)); +#else + renderTransform = modelTransform.worldTransform(Transform(clusterTransformsCauterized[0])); +#endif } data.updateTransformForCauterizedMesh(renderTransform); diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index cb0a43b1e9..d6ab2ff416 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -12,6 +12,7 @@ #include "MeshPartPayload.h" #include +#include #include "DeferredLightingEffect.h" @@ -325,12 +326,20 @@ ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, in const Model::MeshState& state = model->getMeshState(_meshIndex); updateMeshPart(modelMesh, partIndex); - computeAdjustedLocalBound(state.clusterMatrices); + computeAdjustedLocalBound(state.clusterTransforms); updateTransform(transform, offsetTransform); Transform renderTransform = transform; - if (state.clusterMatrices.size() == 1) { - renderTransform = transform.worldTransform(Transform(state.clusterMatrices[0])); + if (state.clusterTransforms.size() == 1) { +#if defined(SKIN_DQ) + Transform transform(state.clusterTransforms[0].getRotation(), + state.clusterTransforms[0].getScale(), + state.clusterTransforms[0].getTranslation()); + renderTransform = transform.worldTransform(Transform(transform)); +#else + renderTransform = transform.worldTransform(Transform(state.clusterTransforms[0])); +#endif + } updateTransformForSkinnedMesh(renderTransform, transform); @@ -360,17 +369,16 @@ void ModelMeshPartPayload::notifyLocationChanged() { } - -void ModelMeshPartPayload::updateClusterBuffer(const std::vector& clusterMatrices) { +void ModelMeshPartPayload::updateClusterBuffer(const std::vector& clusterTransforms) { // Once computed the cluster matrices, update the buffer(s) - if (clusterMatrices.size() > 1) { + if (clusterTransforms.size() > 1) { if (!_clusterBuffer) { - _clusterBuffer = std::make_shared(clusterMatrices.size() * sizeof(glm::mat4), - (const gpu::Byte*) clusterMatrices.data()); + _clusterBuffer = std::make_shared(clusterTransforms.size() * sizeof(TransformType), + (const gpu::Byte*) clusterTransforms.data()); } else { - _clusterBuffer->setSubData(0, clusterMatrices.size() * sizeof(glm::mat4), - (const gpu::Byte*) clusterMatrices.data()); + _clusterBuffer->setSubData(0, clusterTransforms.size() * sizeof(TransformType), + (const gpu::Byte*) clusterTransforms.data()); } } } @@ -530,13 +538,29 @@ void ModelMeshPartPayload::render(RenderArgs* args) { args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE; } -void ModelMeshPartPayload::computeAdjustedLocalBound(const std::vector& clusterMatrices) { + +void ModelMeshPartPayload::computeAdjustedLocalBound(const std::vector& clusterTransforms) { _adjustedLocalBound = _localBound; - if (clusterMatrices.size() > 0) { - _adjustedLocalBound.transform(clusterMatrices[0]); - for (int i = 1; i < (int)clusterMatrices.size(); ++i) { + if (clusterTransforms.size() > 0) { +#if defined(SKIN_DQ) + Transform rootTransform(clusterTransforms[0].getRotation(), + clusterTransforms[0].getScale(), + clusterTransforms[0].getTranslation()); + _adjustedLocalBound.transform(rootTransform); +#else + _adjustedLocalBound.transform(clusterTransforms[0]); +#endif + + for (int i = 1; i < (int)clusterTransforms.size(); ++i) { AABox clusterBound = _localBound; - clusterBound.transform(clusterMatrices[i]); +#if defined(SKIN_DQ) + Transform transform(clusterTransforms[i].getRotation(), + clusterTransforms[i].getScale(), + clusterTransforms[i].getTranslation()); + clusterBound.transform(transform); +#else + clusterBound.transform(clusterTransforms[i]); +#endif _adjustedLocalBound += clusterBound; } } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index fb55883101..8d36395610 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -87,7 +87,14 @@ public: typedef Payload::DataPointer Pointer; void notifyLocationChanged() override; - void updateClusterBuffer(const std::vector& clusterMatrices); + +#if defined(SKIN_DQ) + using TransformType = Model::TransformDualQuaternion; +#else + using TransformType = glm::mat4; +#endif + + void updateClusterBuffer(const std::vector& clusterTransforms); void updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform); // Render Item interface @@ -104,7 +111,7 @@ public: void bindMesh(gpu::Batch& batch) override; void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override; - void computeAdjustedLocalBound(const std::vector& clusterMatrices); + void computeAdjustedLocalBound(const std::vector& clusterTransforms); gpu::BufferPointer _clusterBuffer; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 539f0421b0..9c1c579341 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -27,6 +27,7 @@ #include #include +#include #include @@ -269,16 +270,24 @@ void Model::updateRenderItems() { auto itemID = self->_modelMeshRenderItemIDs[i]; auto meshIndex = self->_modelMeshRenderItemShapes[i].meshIndex; - auto clusterMatrices(self->getMeshState(meshIndex).clusterMatrices); + auto clusterTransforms(self->getMeshState(meshIndex).clusterTransforms); bool invalidatePayloadShapeKey = self->shouldInvalidatePayloadShapeKey(meshIndex); - transaction.updateItem(itemID, [modelTransform, clusterMatrices, invalidatePayloadShapeKey, + transaction.updateItem(itemID, [modelTransform, clusterTransforms, invalidatePayloadShapeKey, isWireframe, isVisible, isLayeredInFront, isLayeredInHUD](ModelMeshPartPayload& data) { - data.updateClusterBuffer(clusterMatrices); + data.updateClusterBuffer(clusterTransforms); + Transform renderTransform = modelTransform; - if (clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(clusterMatrices[0])); + if (clusterTransforms.size() == 1) { +#if defined(SKIN_DQ) + Transform transform(clusterTransforms[0].getRotation(), + clusterTransforms[0].getScale(), + clusterTransforms[0].getTranslation()); + renderTransform = modelTransform.worldTransform(Transform(transform)); +#else + renderTransform = modelTransform.worldTransform(Transform(clusterTransforms[0])); +#endif } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); @@ -359,7 +368,7 @@ bool Model::updateGeometry() { const FBXGeometry& fbxGeometry = getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; - state.clusterMatrices.resize(mesh.clusters.size()); + state.clusterTransforms.resize(mesh.clusters.size()); _meshStates.push_back(state); // Note: we add empty buffers for meshes that lack blendshapes so we can access the buffers by index @@ -1211,7 +1220,7 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { void Model::computeMeshPartLocalBounds() { for (auto& part : _modelMeshRenderItems) { const Model::MeshState& state = _meshStates.at(part->_meshIndex); - part->computeAdjustedLocalBound(state.clusterMatrices); + part->computeAdjustedLocalBound(state.clusterTransforms); } } @@ -1222,6 +1231,7 @@ void Model::updateClusterMatrices() { if (!_needsUpdateClusterMatrices || !isLoaded()) { return; } + _needsUpdateClusterMatrices = false; const FBXGeometry& geometry = getFBXGeometry(); for (int i = 0; i < (int) _meshStates.size(); i++) { @@ -1229,8 +1239,16 @@ void Model::updateClusterMatrices() { const FBXMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); +#if defined(SKIN_DQ) + auto jointPose = _rig.getJointPose(cluster.jointIndex); + Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform clusterTransform; + Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform); + state.clusterTransforms[j] = Model::TransformDualQuaternion(clusterTransform); +#else auto jointMatrix = _rig.getJointTransform(cluster.jointIndex); - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); +#endif } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 77ed629962..623f869666 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -30,11 +30,15 @@ #include #include #include +#include #include "GeometryCache.h" #include "TextureCache.h" #include "Rig.h" +// Use dual quaternion skinning! +// Must match define in Skinning.slh +#define SKIN_DQ class AbstractViewStateInterface; class QScriptEngine; @@ -246,9 +250,46 @@ public: int getRenderInfoDrawCalls() const { return _renderInfoDrawCalls; } bool getRenderInfoHasTransparent() const { return _renderInfoHasTransparent; } + +#if defined(SKIN_DQ) + class TransformDualQuaternion { + public: + TransformDualQuaternion() {} + TransformDualQuaternion(const glm::mat4& m) { + AnimPose p(m); + _scale.x = p.scale().x; + _scale.y = p.scale().y; + _scale.z = p.scale().z; + _dq = DualQuaternion(p.rot(), p.trans()); + } + TransformDualQuaternion(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans) { + _scale.x = scale.x; + _scale.y = scale.y; + _scale.z = scale.z; + _dq = DualQuaternion(rot, trans); + } + TransformDualQuaternion(const Transform& transform) { + _scale = glm::vec4(transform.getScale(), 0.0f); + _dq = DualQuaternion(transform.getRotation(), transform.getTranslation()); + } + glm::vec3 getScale() const { return glm::vec3(_scale); } + glm::quat getRotation() const { return _dq.getRotation(); } + glm::vec3 getTranslation() const { return _dq.getTranslation(); } + glm::mat4 getMatrix() const { return createMatFromScaleQuatAndPos(getScale(), getRotation(), getTranslation()); }; + protected: + glm::vec4 _scale { 1.0f, 1.0f, 1.0f, 0.0f }; + DualQuaternion _dq; + glm::vec4 _padding; + }; +#endif + class MeshState { public: - std::vector clusterMatrices; +#if defined(SKIN_DQ) + std::vector clusterTransforms; +#else + std::vector clusterTransforms; +#endif }; const MeshState& getMeshState(int index) { return _meshStates.at(index); } diff --git a/libraries/render-utils/src/Skinning.slh b/libraries/render-utils/src/Skinning.slh index 2d1f010029..49d0df3d2c 100644 --- a/libraries/render-utils/src/Skinning.slh +++ b/libraries/render-utils/src/Skinning.slh @@ -11,6 +11,10 @@ <@if not SKINNING_SLH@> <@def SKINNING_SLH@> +// Use dual quaternion skinning +// Must match #define SKIN_DQ in Model.h +<@def SKIN_DQ@> + const int MAX_CLUSTERS = 128; const int INDICES_PER_VERTEX = 4; @@ -18,6 +22,156 @@ layout(std140) uniform skinClusterBuffer { mat4 clusterMatrices[MAX_CLUSTERS]; }; +<@if SKIN_DQ@> + +mat4 dualQuatToMat4(vec4 real, vec4 dual) { + float twoRealXSq = 2.0 * real.x * real.x; + float twoRealYSq = 2.0 * real.y * real.y; + float twoRealZSq = 2.0 * real.z * real.z; + float twoRealXY = 2.0 * real.x * real.y; + float twoRealXZ = 2.0 * real.x * real.z; + float twoRealXW = 2.0 * real.x * real.w; + float twoRealZW = 2.0 * real.z * real.w; + float twoRealYZ = 2.0 * real.y * real.z; + float twoRealYW = 2.0 * real.y * real.w; + vec4 col0 = vec4(1.0 - twoRealYSq - twoRealZSq, + twoRealXY + twoRealZW, + twoRealXZ - twoRealYW, + 0.0); + vec4 col1 = vec4(twoRealXY - twoRealZW, + 1 - twoRealXSq - twoRealZSq, + twoRealYZ + twoRealXW, + 0.0); + vec4 col2 = vec4(twoRealXZ + twoRealYW, + twoRealYZ - twoRealXW, + 1 - twoRealXSq - twoRealYSq, + 0.0); + vec4 col3 = vec4(2.0 * (-dual.w * real.x + dual.x * real.w - dual.y * real.z + dual.z * real.y), + 2.0 * (-dual.w * real.y + dual.x * real.z + dual.y * real.w - dual.z * real.x), + 2.0 * (-dual.w * real.z - dual.x * real.y + dual.y * real.x + dual.z * real.w), + 1.0); + + return mat4(col0, col1, col2, col3); +} + +// dual quaternion linear blending +void skinPosition(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, out vec4 skinnedPosition) { + + // linearly blend scale and dual quaternion components + vec3 sAccum = vec3(0.0, 0.0, 0.0); + vec4 rAccum = vec4(0.0, 0.0, 0.0, 0.0); + vec4 dAccum = vec4(0.0, 0.0, 0.0, 0.0); + vec4 polarityReference = clusterMatrices[skinClusterIndex[0]][1]; + for (int i = 0; i < INDICES_PER_VERTEX; i++) { + mat4 clusterMatrix = clusterMatrices[(skinClusterIndex[i])]; + float clusterWeight = skinClusterWeight[i]; + + vec3 scale = vec3(clusterMatrix[0]); + vec4 real = clusterMatrix[1]; + vec4 dual = clusterMatrix[2]; + + // to ensure that we rotate along the shortest arc, reverse dual quaternions with negative polarity. + float dqClusterWeight = clusterWeight; + if (dot(real, polarityReference) < 0) { + dqClusterWeight = -clusterWeight; + } + + sAccum += scale * clusterWeight; + rAccum += real * dqClusterWeight; + dAccum += dual * dqClusterWeight; + } + + // normalize dual quaternion + float norm = length(rAccum); + rAccum /= norm; + dAccum /= norm; + + // conversion from dual quaternion to 4x4 matrix. + mat4 m = dualQuatToMat4(rAccum, dAccum); + skinnedPosition = m * (vec4(sAccum, 1) * inPosition); +} + +void skinPositionNormal(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, vec3 inNormal, + out vec4 skinnedPosition, out vec3 skinnedNormal) { + + // linearly blend scale and dual quaternion components + vec3 sAccum = vec3(0.0, 0.0, 0.0); + vec4 rAccum = vec4(0.0, 0.0, 0.0, 0.0); + vec4 dAccum = vec4(0.0, 0.0, 0.0, 0.0); + vec4 polarityReference = clusterMatrices[skinClusterIndex[0]][1]; + + for (int i = 0; i < INDICES_PER_VERTEX; i++) { + mat4 clusterMatrix = clusterMatrices[(skinClusterIndex[i])]; + float clusterWeight = skinClusterWeight[i]; + + vec3 scale = vec3(clusterMatrix[0]); + vec4 real = clusterMatrix[1]; + vec4 dual = clusterMatrix[2]; + + // to ensure that we rotate along the shortest arc, reverse dual quaternions with negative polarity. + float dqClusterWeight = clusterWeight; + if (dot(real, polarityReference) < 0) { + dqClusterWeight = -clusterWeight; + } + + sAccum += scale * clusterWeight; + rAccum += real * dqClusterWeight; + dAccum += dual * dqClusterWeight; + } + + // normalize dual quaternion + float norm = length(rAccum); + rAccum /= norm; + dAccum /= norm; + + // conversion from dual quaternion to 4x4 matrix. + mat4 m = dualQuatToMat4(rAccum, dAccum); + skinnedPosition = m * (vec4(sAccum, 1) * inPosition); + skinnedNormal = vec3(m * vec4(inNormal, 0)); +} + +void skinPositionNormalTangent(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, vec3 inNormal, vec3 inTangent, + out vec4 skinnedPosition, out vec3 skinnedNormal, out vec3 skinnedTangent) { + + // linearly blend scale and dual quaternion components + vec3 sAccum = vec3(0.0, 0.0, 0.0); + vec4 rAccum = vec4(0.0, 0.0, 0.0, 0.0); + vec4 dAccum = vec4(0.0, 0.0, 0.0, 0.0); + vec4 polarityReference = clusterMatrices[skinClusterIndex[0]][1]; + + for (int i = 0; i < INDICES_PER_VERTEX; i++) { + mat4 clusterMatrix = clusterMatrices[(skinClusterIndex[i])]; + float clusterWeight = skinClusterWeight[i]; + + vec3 scale = vec3(clusterMatrix[0]); + vec4 real = clusterMatrix[1]; + vec4 dual = clusterMatrix[2]; + + // to ensure that we rotate along the shortest arc, reverse dual quaternions with negative polarity. + float dqClusterWeight = clusterWeight; + if (dot(real, polarityReference) < 0) { + dqClusterWeight = -clusterWeight; + } + + sAccum += scale * clusterWeight; + rAccum += real * dqClusterWeight; + dAccum += dual * dqClusterWeight; + } + + // normalize dual quaternion + float norm = length(rAccum); + rAccum /= norm; + dAccum /= norm; + + // conversion from dual quaternion to 4x4 matrix. + mat4 m = dualQuatToMat4(rAccum, dAccum); + skinnedPosition = m * (vec4(sAccum, 1) * inPosition); + skinnedNormal = vec3(m * vec4(inNormal, 0)); + skinnedTangent = vec3(m * vec4(inTangent, 0)); +} + +<@else@> // SKIN_DQ + void skinPosition(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, out vec4 skinnedPosition) { vec4 newPosition = vec4(0.0, 0.0, 0.0, 0.0); @@ -65,5 +219,6 @@ void skinPositionNormalTangent(ivec4 skinClusterIndex, vec4 skinClusterWeight, v skinnedTangent = newTangent.xyz; } +<@endif@> // if SKIN_DQ -<@endif@> \ No newline at end of file +<@endif@> // if not SKINNING_SLH diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index 63991f9422..0d0db7cbe3 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -52,13 +52,27 @@ void SoftAttachmentModel::updateClusterMatrices() { // TODO: cache these look-ups as an optimization int jointIndexOverride = getJointIndexOverride(cluster.jointIndex); +#if defined(SKIN_DQ) glm::mat4 jointMatrix; if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); } else { jointMatrix = _rig.getJointTransform(cluster.jointIndex); } - glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); + + glm::mat4 m; + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, m); + state.clusterTransforms[j] = Model::TransformDualQuaternion(m); +#else + glm::mat4 jointMatrix; + if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) { + jointMatrix = _rigOverride.getJointTransform(jointIndexOverride); + } else { + jointMatrix = _rig.getJointTransform(cluster.jointIndex); + } + + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterTransforms[j]); +#endif } } diff --git a/libraries/shared/src/DualQuaternion.cpp b/libraries/shared/src/DualQuaternion.cpp new file mode 100644 index 0000000000..2accbed2a9 --- /dev/null +++ b/libraries/shared/src/DualQuaternion.cpp @@ -0,0 +1,92 @@ +// +// DualQuaternion.cpp +// +// Created by Anthony J. Thibault on Dec 13th 2017. +// Copyright (c) 2017 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DualQuaternion.h" +#include "GLMHelpers.h" + +// delegating constructor +DualQuaternion::DualQuaternion() : _real(1.0f, 0.0f, 0.0f, 0.0), _dual(0.0f, 0.0f, 0.0f, 0.0f) { +} + +DualQuaternion::DualQuaternion(const glm::mat4& m) : DualQuaternion(glmExtractRotation(m), extractTranslation(m)) { +} + +DualQuaternion::DualQuaternion(const glm::quat& real, const glm::quat& dual) : _real(real), _dual(dual) { +} + +DualQuaternion::DualQuaternion(const glm::vec4& real, const glm::vec4& dual) : + _real(real.w, real.x, real.y, real.z), + _dual(dual.w, dual.x, dual.y, dual.z) { +} + +DualQuaternion::DualQuaternion(const glm::quat& rotation, const glm::vec3& translation) { + _real = rotation; + _dual = glm::quat(0.0f, 0.5f * translation.x, 0.5f * translation.y, 0.5f * translation.z) * rotation; +} + +DualQuaternion DualQuaternion::operator*(const DualQuaternion& rhs) const { + return DualQuaternion(_real * rhs._real, _real * rhs._dual + _dual * rhs._real); +} + +DualQuaternion DualQuaternion::operator*(float scalar) const { + return DualQuaternion(_real * scalar, _dual * scalar); +} + +DualQuaternion DualQuaternion::operator+(const DualQuaternion& rhs) const { + return DualQuaternion(_real + rhs._real, _dual + rhs._dual); +} + +glm::vec3 DualQuaternion::xformPoint(const glm::vec3& rhs) const { + DualQuaternion v(glm::quat(), glm::quat(0.0f, rhs.x, rhs.y, rhs.z)); + DualQuaternion dualConj(glm::conjugate(_real), -glm::conjugate(_dual)); + DualQuaternion result = *this * v * dualConj; + return vec3(result._dual.x, result._dual.y, result._dual.z); +} + +glm::quat DualQuaternion::getRotation() const { + return _real; +} + +glm::vec3 DualQuaternion::getTranslation() const { + glm::quat result = 2.0f * (_dual * glm::inverse(_real)); + return glm::vec3(result.x, result.y, result.z); +} + +glm::vec3 DualQuaternion::xformVector(const glm::vec3& rhs) const { + return _real * rhs; +} + +DualQuaternion DualQuaternion::inverse() const { + glm::quat invReal = glm::inverse(_real); + return DualQuaternion(invReal, - invReal * _dual * invReal); +} + +DualQuaternion DualQuaternion::conjugate() const { + return DualQuaternion(glm::conjugate(_real), glm::conjugate(_dual)); +} + +float DualQuaternion::length() const { + float dot = this->dot(*this); + return sqrtf(dot); +} + +DualQuaternion DualQuaternion::normalize() const { + float invLen = 1.0f / length(); + return *this * invLen; +} + +float DualQuaternion::dot(const DualQuaternion& rhs) const { + DualQuaternion result = *this * conjugate(); + return result._real.w; +} + +DualQuaternion DualQuaternion::operator-() const { + return DualQuaternion(-_real, -_dual); +} diff --git a/libraries/shared/src/DualQuaternion.h b/libraries/shared/src/DualQuaternion.h new file mode 100644 index 0000000000..709c089fdc --- /dev/null +++ b/libraries/shared/src/DualQuaternion.h @@ -0,0 +1,62 @@ +// +// DualQuaternion.h +// +// Created by Anthony J. Thibault on Dec 13th 2017. +// Copyright (c) 2017 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_DualQuaternion +#define hifi_DualQuaternion + +#include +#include +#include +#include + +class DualQuaternion { +public: + DualQuaternion(); + explicit DualQuaternion(const glm::mat4& m); + DualQuaternion(const glm::quat& real, const glm::quat& imag); + DualQuaternion(const glm::quat& rotation, const glm::vec3& translation); + DualQuaternion(const glm::vec4& real, const glm::vec4& imag); + DualQuaternion operator*(const DualQuaternion& rhs) const; + DualQuaternion operator*(float scalar) const; + DualQuaternion operator+(const DualQuaternion& rhs) const; + + const glm::quat& real() const { return _real; } + glm::quat& real() { return _real; } + + const glm::quat& dual() const { return _dual; } + glm::quat& dual() { return _dual; } + + glm::quat getRotation() const; + glm::vec3 getTranslation() const; + + glm::vec3 xformPoint(const glm::vec3& rhs) const; + glm::vec3 xformVector(const glm::vec3& rhs) const; + + DualQuaternion inverse() const; + DualQuaternion conjugate() const; + float length() const; + DualQuaternion normalize() const; + float dot(const DualQuaternion& rhs) const; + DualQuaternion operator-() const; + +protected: + friend QDebug operator<<(QDebug debug, const DualQuaternion& pose); + glm::quat _real; + glm::quat _dual; +}; + + +inline QDebug operator<<(QDebug debug, const DualQuaternion& dq) { + debug << "AnimPose, real = (" << dq._real.x << dq._real.y << dq._real.z << dq._real.w << "), dual = (" << dq._dual.x << dq._dual.y << dq._dual.z << dq._dual.w << ")"; + return debug; +} + +#endif diff --git a/libraries/shared/src/Transform.h b/libraries/shared/src/Transform.h index 7a39314f4d..90bfc1aaa6 100644 --- a/libraries/shared/src/Transform.h +++ b/libraries/shared/src/Transform.h @@ -58,7 +58,7 @@ public: _rotation(rotation), _scale(scale), _translation(translation), - _flags(FLAG_CACHE_INVALID_BITSET) // invalid cache + _flags(0xf) // FLAG_TRANSLATION | FLAG_ROTATION | FLAG_SCALING | FLAG_NON_UNIFORM { if (!isValidScale(_scale)) { _scale = Vec3(1.0f); diff --git a/tests/shared/src/DualQuaternionTests.cpp b/tests/shared/src/DualQuaternionTests.cpp new file mode 100644 index 0000000000..fe14d9d166 --- /dev/null +++ b/tests/shared/src/DualQuaternionTests.cpp @@ -0,0 +1,115 @@ +// +// DualQuaternionTests.cpp +// tests/shared/src +// +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "DualQuaternionTests.h" + +#include +#include +#include +#include + +#include <../GLMTestUtils.h> +#include <../QTestExtensions.h> + +QTEST_MAIN(DualQuaternionTests) + +static void quatComp(const glm::quat& q1, const glm::quat& q2) { + QCOMPARE_WITH_ABS_ERROR(q1.x, q2.x, EPSILON); + QCOMPARE_WITH_ABS_ERROR(q1.y, q2.y, EPSILON); + QCOMPARE_WITH_ABS_ERROR(q1.z, q2.z, EPSILON); + QCOMPARE_WITH_ABS_ERROR(q1.w, q2.w, EPSILON); +} + +void DualQuaternionTests::ctor() { + glm::quat real = angleAxis(PI / 2.0f, Vectors::UNIT_Y); + glm::quat dual(0.0f, 1.0f, 2.0f, 3.0f); + + DualQuaternion dq(real, dual); + quatComp(real, dq.real()); + quatComp(dual, dq.dual()); + + glm::quat rotation = angleAxis(PI / 3.0f, Vectors::UNIT_X); + glm::vec3 translation(1.0, 2.0f, 3.0f); + dq = DualQuaternion(rotation, translation); + quatComp(rotation, dq.getRotation()); + QCOMPARE_WITH_ABS_ERROR(translation, dq.getTranslation(), EPSILON); + + rotation = angleAxis(-2.0f * PI / 7.0f, Vectors::UNIT_Z); + translation = glm::vec3(-1.0, 12.0f, 2.0f); + glm::mat4 m = createMatFromQuatAndPos(rotation, translation); + dq = DualQuaternion(m); + quatComp(rotation, dq.getRotation()); + QCOMPARE_WITH_ABS_ERROR(translation, dq.getTranslation(), EPSILON); +} + +void DualQuaternionTests::mult() { + + glm::quat rotation = angleAxis(PI / 3.0f, Vectors::UNIT_X); + glm::vec3 translation(1.0, 2.0f, 3.0f); + glm::mat4 m1 = createMatFromQuatAndPos(rotation, translation); + DualQuaternion dq1(m1); + + rotation = angleAxis(-2.0f * PI / 7.0f, Vectors::UNIT_Z); + translation = glm::vec3(-1.0, 12.0f, 2.0f); + glm::mat4 m2 = createMatFromQuatAndPos(rotation, translation); + DualQuaternion dq2(m2); + + DualQuaternion dq3 = dq1 * dq2; + glm::mat4 m3 = m1 * m2; + + rotation = glmExtractRotation(m3); + translation = extractTranslation(m3); + + quatComp(rotation, dq3.getRotation()); + QCOMPARE_WITH_ABS_ERROR(translation, dq3.getTranslation(), EPSILON); +} + +void DualQuaternionTests::xform() { + + glm::quat rotation = angleAxis(PI / 3.0f, Vectors::UNIT_X); + glm::vec3 translation(1.0, 2.0f, 3.0f); + glm::mat4 m1 = createMatFromQuatAndPos(rotation, translation); + DualQuaternion dq1(m1); + + rotation = angleAxis(-2.0f * PI / 7.0f, Vectors::UNIT_Z); + translation = glm::vec3(-1.0, 12.0f, 2.0f); + glm::mat4 m2 = createMatFromQuatAndPos(rotation, translation); + DualQuaternion dq2(m2); + + DualQuaternion dq3 = dq1 * dq2; + glm::mat4 m3 = m1 * m2; + + glm::vec3 p(1.0f, 2.0f, 3.0f); + + glm::vec3 p1 = transformPoint(m3, p); + glm::vec3 p2 = dq3.xformPoint(p); + + QCOMPARE_WITH_ABS_ERROR(p1, p2, 0.001f); + + p1 = transformVectorFast(m3, p); + p2 = dq3.xformVector(p); + + QCOMPARE_WITH_ABS_ERROR(p1, p2, 0.001f); +} + +void DualQuaternionTests::trans() { + glm::vec3 t1 = glm::vec3(); + DualQuaternion dq1(Quaternions::IDENTITY, t1); + glm::vec3 t2 = glm::vec3(1.0f, 2.0f, 3.0f); + DualQuaternion dq2(angleAxis(PI / 3.0f, Vectors::UNIT_X), t2); + glm::vec3 t3 = glm::vec3(3.0f, 2.0f, 1.0f); + DualQuaternion dq3(angleAxis(PI / 5.0f, Vectors::UNIT_Y), t3); + + QCOMPARE_WITH_ABS_ERROR(t1, dq1.getTranslation(), 0.001f); + QCOMPARE_WITH_ABS_ERROR(t2, dq2.getTranslation(), 0.001f); + QCOMPARE_WITH_ABS_ERROR(t3, dq3.getTranslation(), 0.001f); +} diff --git a/tests/shared/src/DualQuaternionTests.h b/tests/shared/src/DualQuaternionTests.h new file mode 100644 index 0000000000..aa4b40cfd6 --- /dev/null +++ b/tests/shared/src/DualQuaternionTests.h @@ -0,0 +1,25 @@ +// +// DualQuaternionTests.h +// tests/shared/src +// +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DualQuaternionTests_h +#define hifi_DualQuaternionTests_h + +#include + +class DualQuaternionTests : public QObject { + Q_OBJECT +private slots: + void ctor(); + void mult(); + void xform(); + void trans(); +}; + +#endif // hifi_DualQuaternionTests_h