mirror of
https://github.com/overte-org/overte.git
synced 2025-04-21 06:44:06 +02:00
Merge branch 'rig' of https://github.com/howard-stearns/hifi into rig
This commit is contained in:
commit
68aec1fd36
7 changed files with 98 additions and 107 deletions
|
@ -968,12 +968,8 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
|
|||
Avatar::setSkeletonModelURL(skeletonModelURL);
|
||||
render::ScenePointer scene = Application::getInstance()->getMain3DScene();
|
||||
_billboardValid = false;
|
||||
|
||||
if (_useFullAvatar) {
|
||||
_skeletonModel.setVisibleInScene(_prevShouldDrawHead, scene);
|
||||
} else {
|
||||
_skeletonModel.setVisibleInScene(true, scene);
|
||||
}
|
||||
_skeletonModel.setVisibleInScene(true, scene);
|
||||
_headBoneSet.clear();
|
||||
}
|
||||
|
||||
void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName) {
|
||||
|
@ -1183,17 +1179,45 @@ void MyAvatar::setVisibleInSceneIfReady(Model* model, render::ScenePointer scene
|
|||
}
|
||||
}
|
||||
|
||||
void MyAvatar::initHeadBones() {
|
||||
int neckJointIndex = -1;
|
||||
if (_skeletonModel.getGeometry()) {
|
||||
neckJointIndex = _skeletonModel.getGeometry()->getFBXGeometry().neckJointIndex;
|
||||
}
|
||||
if (neckJointIndex == -1) {
|
||||
return;
|
||||
}
|
||||
_headBoneSet.clear();
|
||||
std::queue<int> q;
|
||||
q.push(neckJointIndex);
|
||||
_headBoneSet.insert(neckJointIndex);
|
||||
|
||||
// fbxJoints only hold links to parents not children, so we have to do a bit of extra work here.
|
||||
while (q.size() > 0) {
|
||||
int jointIndex = q.front();
|
||||
for (int i = 0; i < _skeletonModel.getJointStateCount(); i++) {
|
||||
if (jointIndex == _skeletonModel.getParentJointIndex(i)) {
|
||||
_headBoneSet.insert(i);
|
||||
q.push(i);
|
||||
}
|
||||
}
|
||||
q.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void MyAvatar::preRender(RenderArgs* renderArgs) {
|
||||
|
||||
render::ScenePointer scene = Application::getInstance()->getMain3DScene();
|
||||
const bool shouldDrawHead = shouldRenderHead(renderArgs);
|
||||
|
||||
_skeletonModel.initWhenReady(scene);
|
||||
if (_skeletonModel.initWhenReady(scene)) {
|
||||
initHeadBones();
|
||||
_skeletonModel.setCauterizeBoneSet(_headBoneSet);
|
||||
}
|
||||
|
||||
if (shouldDrawHead != _prevShouldDrawHead) {
|
||||
if (_useFullAvatar) {
|
||||
_skeletonModel.setVisibleInScene(true, scene);
|
||||
_rig->setFirstPerson(!shouldDrawHead);
|
||||
_skeletonModel.setCauterizeBones(!shouldDrawHead);
|
||||
} else {
|
||||
getHead()->getFaceModel().setVisibleInScene(shouldDrawHead, scene);
|
||||
}
|
||||
|
|
|
@ -273,7 +273,8 @@ private:
|
|||
void updatePosition(float deltaTime);
|
||||
void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency);
|
||||
void maybeUpdateBillboard();
|
||||
|
||||
void initHeadBones();
|
||||
|
||||
// Avatar Preferences
|
||||
bool _useFullAvatar = false;
|
||||
QUrl _fullAvatarURLFromPreferences;
|
||||
|
@ -286,6 +287,7 @@ private:
|
|||
|
||||
RigPointer _rig;
|
||||
bool _prevShouldDrawHead;
|
||||
std::unordered_set<int> _headBoneSet;
|
||||
};
|
||||
|
||||
#endif // hifi_MyAvatar_h
|
||||
|
|
|
@ -51,7 +51,7 @@ SkeletonModel::~SkeletonModel() {
|
|||
void SkeletonModel::initJointStates(QVector<JointState> states) {
|
||||
const FBXGeometry& geometry = _geometry->getFBXGeometry();
|
||||
glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset;
|
||||
_boundingRadius = _rig->initJointStates(states, parentTransform, geometry.neckJointIndex);
|
||||
_boundingRadius = _rig->initJointStates(states, parentTransform);
|
||||
|
||||
// Determine the default eye position for avatar scale = 1.0
|
||||
int headJointIndex = _geometry->getFBXGeometry().headJointIndex;
|
||||
|
@ -175,14 +175,6 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) {
|
|||
restoreRightHandPosition(HAND_RESTORATION_RATE, PALM_PRIORITY);
|
||||
}
|
||||
}
|
||||
|
||||
// if (_isFirstPerson) {
|
||||
// cauterizeHead();
|
||||
// updateClusterMatrices();
|
||||
// }
|
||||
if (_rig->getJointsAreDirty()) {
|
||||
updateClusterMatrices();
|
||||
}
|
||||
}
|
||||
|
||||
void SkeletonModel::renderIKConstraints(gpu::Batch& batch) {
|
||||
|
|
|
@ -124,9 +124,8 @@ void Rig::deleteAnimations() {
|
|||
_animationHandles.clear();
|
||||
}
|
||||
|
||||
float Rig::initJointStates(QVector<JointState> states, glm::mat4 parentTransform, int neckJointIndex) {
|
||||
float Rig::initJointStates(QVector<JointState> states, glm::mat4 parentTransform) {
|
||||
_jointStates = states;
|
||||
_neckJointIndex = neckJointIndex;
|
||||
initJointTransforms(parentTransform);
|
||||
|
||||
int numStates = _jointStates.size();
|
||||
|
@ -142,8 +141,6 @@ float Rig::initJointStates(QVector<JointState> states, glm::mat4 parentTransform
|
|||
_jointStates[i].slaveVisibleTransform();
|
||||
}
|
||||
|
||||
initHeadBones();
|
||||
|
||||
return radius;
|
||||
}
|
||||
|
||||
|
@ -195,8 +192,7 @@ JointState Rig::getJointState(int jointIndex) const {
|
|||
if (jointIndex == -1 || jointIndex >= _jointStates.size()) {
|
||||
return JointState();
|
||||
}
|
||||
// return _jointStates[jointIndex];
|
||||
return maybeCauterizeHead(jointIndex);
|
||||
return _jointStates[jointIndex];
|
||||
}
|
||||
|
||||
bool Rig::getJointStateRotation(int index, glm::quat& rotation) const {
|
||||
|
@ -270,8 +266,7 @@ bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position,
|
|||
return false;
|
||||
}
|
||||
// position is in world-frame
|
||||
// position = translation + rotation * _jointStates[jointIndex].getPosition();
|
||||
position = translation + rotation * maybeCauterizeHead(jointIndex).getPosition();
|
||||
position = translation + rotation * _jointStates[jointIndex].getPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -280,7 +275,7 @@ bool Rig::getJointPosition(int jointIndex, glm::vec3& position) const {
|
|||
return false;
|
||||
}
|
||||
// position is in model-frame
|
||||
position = extractTranslation(maybeCauterizeHead(jointIndex).getTransform());
|
||||
position = extractTranslation(_jointStates[jointIndex].getTransform());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -288,7 +283,7 @@ bool Rig::getJointRotationInWorldFrame(int jointIndex, glm::quat& result, const
|
|||
if (jointIndex == -1 || jointIndex >= _jointStates.size()) {
|
||||
return false;
|
||||
}
|
||||
result = rotation * maybeCauterizeHead(jointIndex).getRotation();
|
||||
result = rotation * _jointStates[jointIndex].getRotation();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -296,7 +291,7 @@ bool Rig::getJointRotation(int jointIndex, glm::quat& rotation) const {
|
|||
if (jointIndex == -1 || jointIndex >= _jointStates.size()) {
|
||||
return false;
|
||||
}
|
||||
rotation = maybeCauterizeHead(jointIndex).getRotation();
|
||||
rotation = _jointStates[jointIndex].getRotation();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -304,7 +299,7 @@ bool Rig::getJointCombinedRotation(int jointIndex, glm::quat& result, const glm:
|
|||
if (jointIndex == -1 || jointIndex >= _jointStates.size()) {
|
||||
return false;
|
||||
}
|
||||
result = rotation * maybeCauterizeHead(jointIndex).getRotation();
|
||||
result = rotation * _jointStates[jointIndex].getRotation();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -315,7 +310,7 @@ bool Rig::getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& positio
|
|||
return false;
|
||||
}
|
||||
// position is in world-frame
|
||||
position = translation + rotation * maybeCauterizeHead(jointIndex).getVisiblePosition();
|
||||
position = translation + rotation * _jointStates[jointIndex].getVisiblePosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -323,7 +318,7 @@ bool Rig::getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& result,
|
|||
if (jointIndex == -1 || jointIndex >= _jointStates.size()) {
|
||||
return false;
|
||||
}
|
||||
result = rotation * maybeCauterizeHead(jointIndex).getVisibleRotation();
|
||||
result = rotation * _jointStates[jointIndex].getVisibleRotation();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -331,14 +326,14 @@ glm::mat4 Rig::getJointTransform(int jointIndex) const {
|
|||
if (jointIndex == -1 || jointIndex >= _jointStates.size()) {
|
||||
return glm::mat4();
|
||||
}
|
||||
return maybeCauterizeHead(jointIndex).getTransform();
|
||||
return _jointStates[jointIndex].getTransform();
|
||||
}
|
||||
|
||||
glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const {
|
||||
if (jointIndex == -1 || jointIndex >= _jointStates.size()) {
|
||||
return glm::mat4();
|
||||
}
|
||||
return maybeCauterizeHead(jointIndex).getVisibleTransform();
|
||||
return _jointStates[jointIndex].getVisibleTransform();
|
||||
}
|
||||
|
||||
void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) {
|
||||
|
@ -624,7 +619,7 @@ glm::vec3 Rig::getJointDefaultTranslationInConstrainedFrame(int jointIndex) {
|
|||
if (jointIndex == -1 || _jointStates.isEmpty()) {
|
||||
return glm::vec3();
|
||||
}
|
||||
return maybeCauterizeHead(jointIndex).getDefaultTranslationInConstrainedFrame();
|
||||
return _jointStates[jointIndex].getDefaultTranslationInConstrainedFrame();
|
||||
}
|
||||
|
||||
glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain) {
|
||||
|
@ -669,53 +664,5 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) {
|
|||
if (jointIndex == -1 || _jointStates.isEmpty()) {
|
||||
return glm::quat();
|
||||
}
|
||||
return maybeCauterizeHead(jointIndex).getDefaultRotationInParentFrame();
|
||||
}
|
||||
|
||||
void Rig::initHeadBones() {
|
||||
if (_neckJointIndex == -1) {
|
||||
return;
|
||||
}
|
||||
_headBones.clear();
|
||||
std::queue<int> q;
|
||||
q.push(_neckJointIndex);
|
||||
_headBones.push_back(_neckJointIndex);
|
||||
|
||||
// fbxJoints only hold links to parents not children, so we have to do a bit of extra work here.
|
||||
while (q.size() > 0) {
|
||||
int jointIndex = q.front();
|
||||
for (int i = 0; i < _jointStates.size(); i++) {
|
||||
const FBXJoint& fbxJoint = _jointStates[i].getFBXJoint();
|
||||
if (jointIndex == fbxJoint.parentIndex) {
|
||||
_headBones.push_back(i);
|
||||
q.push(i);
|
||||
}
|
||||
}
|
||||
q.pop();
|
||||
}
|
||||
}
|
||||
|
||||
JointState Rig::maybeCauterizeHead(int jointIndex) const {
|
||||
// if (_headBones.contains(jointIndex)) {
|
||||
// XXX fix this... make _headBones a hash? add a flag to JointState?
|
||||
if (_neckJointIndex != -1 &&
|
||||
_isFirstPerson &&
|
||||
std::find(_headBones.begin(), _headBones.end(), jointIndex) != _headBones.end()) {
|
||||
glm::vec4 trans = _jointStates[jointIndex].getTransform()[3];
|
||||
glm::vec4 zero(0, 0, 0, 0);
|
||||
glm::mat4 newXform(zero, zero, zero, trans);
|
||||
JointState jointStateCopy = _jointStates[jointIndex];
|
||||
jointStateCopy.setTransform(newXform);
|
||||
jointStateCopy.setVisibleTransform(newXform);
|
||||
return jointStateCopy;
|
||||
} else {
|
||||
return _jointStates[jointIndex];
|
||||
}
|
||||
}
|
||||
|
||||
void Rig::setFirstPerson(bool isFirstPerson) {
|
||||
if (_isFirstPerson != isFirstPerson) {
|
||||
_isFirstPerson = isFirstPerson;
|
||||
_jointsAreDirty = true;
|
||||
}
|
||||
return _jointStates[jointIndex].getDefaultRotationInParentFrame();
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ public:
|
|||
float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f,
|
||||
float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList(), bool startAutomatically = false);
|
||||
|
||||
float initJointStates(QVector<JointState> states, glm::mat4 parentTransform, int neckJointIndex);
|
||||
float initJointStates(QVector<JointState> states, glm::mat4 parentTransform);
|
||||
bool jointStatesEmpty() { return _jointStates.isEmpty(); };
|
||||
int getJointStateCount() const { return _jointStates.size(); }
|
||||
int indexOfJoint(const QString& jointName) ;
|
||||
|
@ -131,10 +131,6 @@ public:
|
|||
virtual void updateJointState(int index, glm::mat4 parentTransform) = 0;
|
||||
virtual void updateFaceJointState(int index, glm::mat4 parentTransform) = 0;
|
||||
|
||||
virtual void setFirstPerson(bool isFirstPerson);
|
||||
virtual bool getIsFirstPerson() const { return _isFirstPerson; }
|
||||
|
||||
bool getJointsAreDirty() { return _jointsAreDirty; }
|
||||
void setEnableRig(bool isEnabled) { _enableRig = isEnabled; }
|
||||
|
||||
protected:
|
||||
|
@ -143,13 +139,6 @@ public:
|
|||
QList<AnimationHandlePointer> _animationHandles;
|
||||
QList<AnimationHandlePointer> _runningAnimations;
|
||||
|
||||
JointState maybeCauterizeHead(int jointIndex) const;
|
||||
void initHeadBones();
|
||||
bool _isFirstPerson = false;
|
||||
std::vector<int> _headBones;
|
||||
bool _jointsAreDirty = false;
|
||||
int _neckJointIndex = -1;
|
||||
|
||||
bool _enableRig;
|
||||
bool _isWalking;
|
||||
bool _isTurning;
|
||||
|
|
|
@ -67,6 +67,7 @@ Model::Model(RigPointer rig, QObject* parent) :
|
|||
_snapModelToRegistrationPoint(false),
|
||||
_snappedToRegistrationPoint(false),
|
||||
_showTrueJointTransforms(true),
|
||||
_cauterizeBones(false),
|
||||
_lodDistance(0.0f),
|
||||
_pupilDilation(0.0f),
|
||||
_url("http://invalid.com"),
|
||||
|
@ -452,6 +453,7 @@ bool Model::updateGeometry() {
|
|||
foreach (const FBXMesh& mesh, fbxGeometry.meshes) {
|
||||
MeshState state;
|
||||
state.clusterMatrices.resize(mesh.clusters.size());
|
||||
state.cauterizedClusterMatrices.resize(mesh.clusters.size());
|
||||
_meshStates.append(state);
|
||||
|
||||
auto buffer = std::make_shared<gpu::Buffer>();
|
||||
|
@ -472,7 +474,7 @@ bool Model::updateGeometry() {
|
|||
void Model::initJointStates(QVector<JointState> states) {
|
||||
const FBXGeometry& geometry = _geometry->getFBXGeometry();
|
||||
glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset;
|
||||
_boundingRadius = _rig->initJointStates(states, parentTransform, geometry.neckJointIndex);
|
||||
_boundingRadius = _rig->initJointStates(states, parentTransform);
|
||||
}
|
||||
|
||||
bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance,
|
||||
|
@ -1337,6 +1339,12 @@ void Model::simulateInternal(float deltaTime) {
|
|||
glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset;
|
||||
updateRig(deltaTime, parentTransform);
|
||||
|
||||
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.0f, 0.0f, 0.0f, 1.0f));
|
||||
auto cauterizeMatrix = _rig->getJointTransform(geometry.neckJointIndex) * zeroScale;
|
||||
|
||||
glm::mat4 modelToWorld = glm::mat4_cast(_rotation);
|
||||
for (int i = 0; i < _meshStates.size(); i++) {
|
||||
MeshState& state = _meshStates[i];
|
||||
|
@ -1344,14 +1352,30 @@ void Model::simulateInternal(float deltaTime) {
|
|||
if (_showTrueJointTransforms) {
|
||||
for (int j = 0; j < mesh.clusters.size(); j++) {
|
||||
const FBXCluster& cluster = mesh.clusters.at(j);
|
||||
state.clusterMatrices[j] =
|
||||
modelToWorld * _rig->getJointTransform(cluster.jointIndex) * cluster.inverseBindMatrix;
|
||||
auto jointMatrix =_rig->getJointTransform(cluster.jointIndex);
|
||||
state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix;
|
||||
|
||||
// as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty.
|
||||
if (!_cauterizeBoneSet.empty()) {
|
||||
if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) {
|
||||
jointMatrix = cauterizeMatrix;
|
||||
}
|
||||
state.cauterizedClusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int j = 0; j < mesh.clusters.size(); j++) {
|
||||
const FBXCluster& cluster = mesh.clusters.at(j);
|
||||
state.clusterMatrices[j] =
|
||||
modelToWorld * _rig->getJointVisibleTransform(cluster.jointIndex) * cluster.inverseBindMatrix;
|
||||
auto jointMatrix = _rig->getJointVisibleTransform(cluster.jointIndex);
|
||||
state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix;
|
||||
|
||||
// as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty.
|
||||
if (!_cauterizeBoneSet.empty()) {
|
||||
if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) {
|
||||
jointMatrix = cauterizeMatrix;
|
||||
}
|
||||
state.cauterizedClusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1611,8 +1635,11 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran
|
|||
}
|
||||
|
||||
if (isSkinned) {
|
||||
batch._glUniformMatrix4fv(locations->clusterMatrices, state.clusterMatrices.size(), false,
|
||||
(const float*)state.clusterMatrices.constData());
|
||||
const float* bones = (const float*)state.clusterMatrices.constData();
|
||||
if (_cauterizeBones) {
|
||||
bones = (const float*)state.cauterizedClusterMatrices.constData();
|
||||
}
|
||||
batch._glUniformMatrix4fv(locations->clusterMatrices, state.clusterMatrices.size(), false, bones);
|
||||
_transforms[0] = Transform();
|
||||
_transforms[0].preTranslate(_translation);
|
||||
} else {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <QMutex>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <functional>
|
||||
|
||||
#include <AABox.h>
|
||||
|
@ -172,6 +173,9 @@ public:
|
|||
/// \return true if joint exists
|
||||
bool getJointRotation(int jointIndex, glm::quat& rotation) const;
|
||||
|
||||
/// Returns the index of the parent of the indexed joint, or -1 if not found.
|
||||
int getParentJointIndex(int jointIndex) const;
|
||||
|
||||
void inverseKinematics(int jointIndex, glm::vec3 position, const glm::quat& rotation, float priority);
|
||||
|
||||
/// Returns the extents of the model in its bind pose.
|
||||
|
@ -187,6 +191,12 @@ public:
|
|||
bool getIsScaledToFit() const { return _scaledToFit; } /// is model scaled to fit
|
||||
const glm::vec3& getScaleToFitDimensions() const { return _scaleToFitDimensions; } /// the dimensions model is scaled to
|
||||
|
||||
void setCauterizeBones(bool flag) { _cauterizeBones = flag; }
|
||||
bool getCauterizeBones() const { return _cauterizeBones; }
|
||||
|
||||
const std::unordered_set<int>& getCauterizeBoneSet() const { return _cauterizeBoneSet; }
|
||||
void setCauterizeBoneSet(const std::unordered_set<int>& boneSet) { _cauterizeBoneSet = boneSet; }
|
||||
|
||||
protected:
|
||||
|
||||
void setPupilDilation(float dilation) { _pupilDilation = dilation; }
|
||||
|
@ -218,9 +228,6 @@ protected:
|
|||
/// Clear the joint states
|
||||
void clearJointState(int index);
|
||||
|
||||
/// Returns the index of the parent of the indexed joint, or -1 if not found.
|
||||
int getParentJointIndex(int jointIndex) const;
|
||||
|
||||
/// Returns the index of the last free ancestor of the indexed joint, or -1 if not found.
|
||||
int getLastFreeJointIndex(int jointIndex) const;
|
||||
|
||||
|
@ -255,9 +262,12 @@ protected:
|
|||
class MeshState {
|
||||
public:
|
||||
QVector<glm::mat4> clusterMatrices;
|
||||
QVector<glm::mat4> cauterizedClusterMatrices;
|
||||
};
|
||||
|
||||
QVector<MeshState> _meshStates;
|
||||
std::unordered_set<int> _cauterizeBoneSet;
|
||||
bool _cauterizeBones;
|
||||
|
||||
// returns 'true' if needs fullUpdate after geometry change
|
||||
bool updateGeometry();
|
||||
|
|
Loading…
Reference in a new issue