// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qquick3dskin_p.h" #include "qquick3dobject_p.h" #include "qquick3dscenemanager_p.h" #include #include #include QT_BEGIN_NAMESPACE /*! \qmltype Skin \inherits Object3D \inqmlmodule QtQuick3D \brief Defines a skinning animation. A skin defines how a model can be animated using \l {Vertex Skinning} {skeletal animation}. It contains a list of \l {Node}s and an optional list of the Inverse Bind Pose Matrices. Each \l {Node}'s transform becomes a transform of the bone with the corresponding index in the list. \qml Skin { id: skin0 joints: [ node0, node1, node2 ] inverseBindPoses: [ Qt.matrix4x4(...), Qt.matrix4x4(...), Qt.matrix4x4(...) ] } \endqml \note \l {Skeleton} and \l {Joint} will be deprecated. */ QQuick3DSkin::QQuick3DSkin(QQuick3DObject *parent) : QQuick3DObject(*(new QQuick3DObjectPrivate(QQuick3DObjectPrivate::Type::Skin)), parent) { } QQuick3DSkin::~QQuick3DSkin() { for (const auto &conn : m_jointsConnections) { disconnect(conn.first); disconnect(conn.second); } } /*! \qmlproperty List Skin::joints This property contains a list of nodes used for a hierarchy of joints. The order in the list becomes the index of the joint, which is used in the \c SkinSemantic \l {QQuick3DGeometry::addAttribute}{custom geometry attribute}. \note A value 'undefined' will be ignored and if a node which doesn't exist is described, the result is unpredictable. \sa {QQuick3DGeometry::addAttribute}, {Qt Quick 3D - Simple Skinning Example} */ QQmlListProperty QQuick3DSkin::joints() { return QQmlListProperty(this, nullptr, QQuick3DSkin::qmlAppendJoint, QQuick3DSkin::qmlJointsCount, QQuick3DSkin::qmlJointAt, QQuick3DSkin::qmlClearJoints); } #define POS4BONETRANS(x) (sizeof(float) * 16 * (x) * 2) #define POS4BONENORM(x) (sizeof(float) * 16 * ((x) * 2 + 1)) void QQuick3DSkin::qmlAppendJoint(QQmlListProperty *list, QQuick3DNode *joint) { if (joint == nullptr) return; QQuick3DSkin *self = static_cast(list->object); if (!self->m_jointsConnections.contains(joint)) { self->m_jointsConnections[joint] = std::make_pair( connect( joint, &QQuick3DNode::sceneTransformChanged, self, [self, joint]() { self->m_dirtyJoints.insert(joint); self->update(); }), connect( joint, &QQuick3DNode::destroyed, self, [self, joint]() { self->m_dirtyJoints.remove(joint); self->m_removedJoints.insert(joint); self->update(); }) ); } self->m_joints.push_back(joint); self->m_dirtyJoints.insert(joint); self->update(); } QQuick3DNode *QQuick3DSkin::qmlJointAt(QQmlListProperty *list, qsizetype index) { QQuick3DSkin *self = static_cast(list->object); return self->m_joints.at(index); } qsizetype QQuick3DSkin::qmlJointsCount(QQmlListProperty *list) { QQuick3DSkin *self = static_cast(list->object); return self->m_joints.size(); } void QQuick3DSkin::qmlClearJoints(QQmlListProperty *list) { QQuick3DSkin *self = static_cast(list->object); for (const auto &conn : self->m_jointsConnections) { disconnect(conn.first); disconnect(conn.second); } self->m_jointsConnections.clear(); self->m_joints.clear(); self->m_boneData.clear(); self->m_dirtyJoints.clear(); self->m_removedJoints.clear(); self->update(); } /*! \qmlproperty List Skin::inverseBindPoses This property contains a list of Inverse Bind Pose matrixes used for the skinning animation. Each inverseBindPose matrix means the inverse of the global transform of the corresponding node in \l {Skin::joints}, used initially. \note This property is an optional property. That is, if some or all of the matrices are not set, identity values will be used. */ QList QQuick3DSkin::inverseBindPoses() const { return m_inverseBindPoses; } void QQuick3DSkin::setInverseBindPoses(const QList &poses) { if (m_inverseBindPoses == poses) return; m_updatedByNewInverseBindPoses = qMax(poses.size(), m_inverseBindPoses.size()); m_inverseBindPoses = poses; emit inverseBindPosesChanged(); update(); } QSSGRenderGraphObject *QQuick3DSkin::updateSpatialNode(QSSGRenderGraphObject *node) { if (!node) { markAllDirty(); node = new QSSGRenderSkin(); } QQuick3DObject::updateSpatialNode(node); auto skinNode = static_cast(node); if (!m_removedJoints.empty()) { for (int i = m_joints.size() - 1; i >= 0; --i) { const auto &joint = m_joints.at(i); if (m_removedJoints.contains(joint)) { m_joints.removeAt(i); m_boneData.remove(POS4BONETRANS(i), sizeof(float) * 16 * 2); } } m_removedJoints.clear(); } if (skinNode->boneCount != quint32(m_joints.size())) { skinNode->boneCount = quint32(m_joints.size()); const int boneTexWidth = qCeil(qSqrt(skinNode->boneCount * 4 * 2)); const int textureSizeInBytes = boneTexWidth * boneTexWidth * 16; //NB: Assumes RGBA32F set above (16 bytes per color) m_boneData.resize(textureSizeInBytes); skinNode->setSize(QSize(boneTexWidth, boneTexWidth)); } if (m_updatedByNewInverseBindPoses > 0 || !m_dirtyJoints.empty()) { for (int i = 0; i < m_joints.size(); ++i) { const auto &joint = m_joints.at(i); if (i < m_updatedByNewInverseBindPoses || m_dirtyJoints.contains(joint)) { QMatrix4x4 jointGlobal = joint->sceneTransform(); if (m_inverseBindPoses.size() > i) jointGlobal *= m_inverseBindPoses.at(i); memcpy(m_boneData.data() + POS4BONETRANS(i), reinterpret_cast(jointGlobal.constData()), sizeof(float) * 16); memcpy(m_boneData.data() + POS4BONENORM(i), reinterpret_cast(QMatrix4x4(jointGlobal.normalMatrix()).constData()), sizeof(float) * 11); } } m_dirtyJoints.clear(); m_updatedByNewInverseBindPoses = 0; } skinNode->setTextureData(m_boneData); return node; } QT_END_NAMESPACE