Merge pull request #951 from HifiExperiments/pivot

Model Entities: useOriginalPivot Property
This commit is contained in:
Kalila 2021-02-10 00:42:51 -05:00 committed by GitHub
commit b826743212
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 229 additions and 112 deletions

View file

@ -248,9 +248,9 @@ private: \
* <em>Read-only.</em>
* @property {string} lodStatus - Description of the current LOD.
* <em>Read-only.</em>
* @property {string} numEntityUpdates - The number of entity updates that happened last frame.
* @property {number} numEntityUpdates - The number of entity updates that happened last frame.
* <em>Read-only.</em>
* @property {string} numNeededEntityUpdates - The total number of entity updates scheduled for last frame.
* @property {number} numNeededEntityUpdates - The total number of entity updates scheduled for last frame.
* <em>Read-only.</em>
* @property {string} timingStats - Details of the average time (ms) spent in and number of calls made to different parts of
* the code. Provided only if <code>timingExpanded</code> is <code>true</code>. Only the top 10 items are provided if
@ -547,8 +547,8 @@ class Stats : public QQuickItem {
STATS_PROPERTY(int, lodAngle, 0)
STATS_PROPERTY(int, lodTargetFramerate, 0)
STATS_PROPERTY(QString, lodStatus, QString())
STATS_PROPERTY(int, numEntityUpdates, 0)
STATS_PROPERTY(int, numNeededEntityUpdates, 0)
STATS_PROPERTY(quint64, numEntityUpdates, 0)
STATS_PROPERTY(quint64, numNeededEntityUpdates, 0)
STATS_PROPERTY(QString, timingStats, QString())
STATS_PROPERTY(QString, gameUpdateStats, QString())
STATS_PROPERTY(int, serverElements, 0)

View file

@ -33,6 +33,7 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) :
{
// SkeletonModels, and by extention Avatars, use Dual Quaternion skinning.
_useDualQuaternionSkinning = true;
_forceOffset = true;
// Avatars all cast shadow
setCanCastShadow(true);
@ -156,17 +157,13 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) {
updateAttitude(_owningAvatar->getWorldOrientation());
setBlendshapeCoefficients(_owningAvatar->getHead()->getSummedBlendshapeCoefficients());
Parent::simulate(deltaTime, fullUpdate);
if (fullUpdate) {
Parent::simulate(deltaTime, fullUpdate);
// let rig compute the model offset
glm::vec3 registrationPoint;
if (_rig.getModelRegistrationPoint(registrationPoint)) {
setOffset(registrationPoint);
}
} else {
Parent::simulate(deltaTime, fullUpdate);
}
// FIXME: This texture loading logic should probably live in Avatar, to mirror RenderableModelEntityItem,

View file

@ -136,8 +136,8 @@ public:
static bool addMaterialToAvatar(const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName);
static bool removeMaterialFromAvatar(const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName);
int getPrevNumEntityUpdates() const { return _prevNumEntityUpdates; }
int getPrevTotalNeededEntityUpdates() const { return _prevTotalNeededEntityUpdates; }
size_t getPrevNumEntityUpdates() const { return _prevNumEntityUpdates; }
size_t getPrevTotalNeededEntityUpdates() const { return _prevTotalNeededEntityUpdates; }
signals:
void enterEntity(const EntityItemID& entityItemID);
@ -253,8 +253,8 @@ private:
ReadWriteLockable _changedEntitiesGuard;
std::unordered_set<EntityItemID> _changedEntities;
int _prevNumEntityUpdates { 0 };
int _prevTotalNeededEntityUpdates { 0 };
size_t _prevNumEntityUpdates { 0 };
size_t _prevTotalNeededEntityUpdates { 0 };
std::unordered_set<EntityRendererPointer> _renderablesToUpdate;
std::unordered_map<EntityItemID, EntityRendererPointer> _entitiesInScene;

View file

@ -149,10 +149,12 @@ void RenderableModelEntityItem::updateModelBounds() {
bool overridingModelTransform = model->isOverridingModelTransformAndOffset();
glm::vec3 scaledDimensions = getScaledDimensions();
glm::vec3 registrationPoint = getRegistrationPoint();
bool needsSimulate = false;
if (!overridingModelTransform &&
(model->getScaleToFitDimensions() != scaledDimensions ||
model->getRegistrationPoint() != registrationPoint ||
!model->getIsScaledToFit() || _needsToRescaleModel)) {
model->getRegistrationPoint() != registrationPoint ||
!model->getIsScaledToFit() || _needsToRescaleModel ||
_useOriginalPivot == model->getSnapModelToRegistrationPoint())) {
// The machinery for updateModelBounds will give existing models the opportunity to fix their
// translation/rotation/scale/registration. The first two are straightforward, but the latter two
// have guards to make sure they don't happen after they've already been set. Here we reset those guards.
@ -162,9 +164,9 @@ void RenderableModelEntityItem::updateModelBounds() {
// now recalculate the bounds and registration
model->setScaleToFit(true, scaledDimensions);
model->setSnapModelToRegistrationPoint(true, registrationPoint);
model->setSnapModelToRegistrationPoint(!_useOriginalPivot, registrationPoint);
updateRenderItems = true;
model->scaleToFit();
needsSimulate = true;
_needsToRescaleModel = false;
}
@ -176,10 +178,11 @@ void RenderableModelEntityItem::updateModelBounds() {
updateRenderItems = true;
}
if (_needsInitialSimulation || _needsJointSimulation || isAnimatingSomething()) {
if (_needsInitialSimulation || _needsJointSimulation || needsSimulate || isAnimatingSomething()) {
// NOTE: on isAnimatingSomething() we need to call Model::simulate() which calls Rig::updateRig()
// TODO: there is opportunity to further optimize the isAnimatingSomething() case.
model->simulate(0.0f);
locationChanged();
_needsInitialSimulation = false;
_needsJointSimulation = false;
updateRenderItems = true;
@ -219,6 +222,16 @@ EntityItemProperties RenderableModelEntityItem::getProperties(const EntityProper
return properties;
}
glm::vec3 RenderableModelEntityItem::getPivot() const {
auto model = getModel();
auto pivot = EntityItem::getPivot();
if (!model || !model->isLoaded() || !_useOriginalPivot) {
return pivot;
}
return pivot + model->getOriginalOffset();
}
bool RenderableModelEntityItem::supportsDetailedIntersection() const {
return true;
}
@ -443,14 +456,15 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
// multiply each point by scale before handing the point-set off to the physics engine.
// also determine the extents of the collision model.
glm::vec3 registrationOffset = dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint());
glm::vec3 offset = model->getSnapModelToRegistrationPoint() ? model->getOffset() : glm::vec3(0.0f);
for (int32_t i = 0; i < pointCollection.size(); i++) {
for (int32_t j = 0; j < pointCollection[i].size(); j++) {
// back compensate for registration so we can apply that offset to the shapeInfo later
pointCollection[i][j] = scaleToFit * (pointCollection[i][j] + model->getOffset()) - registrationOffset;
pointCollection[i][j] = scaleToFit * (pointCollection[i][j] + offset) - registrationOffset;
}
}
shapeInfo.setParams(type, 0.5f * extents, getCompoundShapeURL());
adjustShapeInfoByRegistration(shapeInfo);
shapeInfo.setParams(type, 0.5f * extents, getCompoundShapeURL() + model->getSnapModelToRegistrationPoint());
adjustShapeInfoByRegistration(shapeInfo, model->getSnapModelToRegistrationPoint());
} else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) {
updateModelBounds();
model->updateGeometry();
@ -682,8 +696,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
}
}
shapeInfo.setParams(type, 0.5f * extents.size(), getModelURL());
adjustShapeInfoByRegistration(shapeInfo);
shapeInfo.setParams(type, 0.5f * extents.size(), getModelURL() + model->getSnapModelToRegistrationPoint());
adjustShapeInfoByRegistration(shapeInfo, model->getSnapModelToRegistrationPoint());
} else {
EntityItem::computeShapeInfo(shapeInfo);
}

View file

@ -42,7 +42,7 @@ protected:
void setModel(const ModelPointer& model);
ModelPointer getModel() const;
bool _needsInitialSimulation{ true };
bool _needsInitialSimulation { true };
private:
ModelPointer _model;
};
@ -63,6 +63,7 @@ public:
virtual EntityItemProperties getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const override;
void updateModelBounds();
glm::vec3 getPivot() const override;
virtual bool supportsDetailedIntersection() const override;
virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
OctreeElementPointer& element, float& distance,

View file

@ -723,8 +723,10 @@ ShapeType RenderablePolyVoxEntityItem::getShapeType() const {
}
void RenderablePolyVoxEntityItem::setRegistrationPoint(const glm::vec3& value) {
if (value != _registrationPoint) {
_shapeReady = false;
if (value != getRegistrationPoint()) {
withWriteLock([&] {
_shapeReady = false;
});
EntityItem::setRegistrationPoint(value);
startUpdates();
}

View file

@ -1607,8 +1607,13 @@ void EntityItem::recordCreationTime() {
const Transform EntityItem::getTransformToCenter(bool& success) const {
Transform result = getTransform(success);
if (getRegistrationPoint() != ENTITY_ITEM_HALF_VEC3) { // If it is not already centered, translate to center
result.postTranslate((ENTITY_ITEM_HALF_VEC3 - getRegistrationPoint()) * getScaledDimensions()); // Position to center
glm::vec3 pivot = getPivot();
if (pivot != ENTITY_ITEM_ZERO_VEC3) {
result.postTranslate(pivot);
}
glm::vec3 registrationPoint = getRegistrationPoint();
if (registrationPoint != ENTITY_ITEM_HALF_VEC3) { // If it is not already centered, translate to center
result.postTranslate((ENTITY_ITEM_HALF_VEC3 - registrationPoint) * getScaledDimensions()); // Position to center
}
return result;
}
@ -1623,15 +1628,16 @@ AACube EntityItem::getMaximumAACube(bool& success) const {
_recalcMaxAACube = false;
// we want to compute the furthestExtent that an entity can extend out from its "position"
// to do this we compute the max of these two vec3s: registration and 1-registration
// and then scale by dimensions
glm::vec3 maxExtents = getScaledDimensions() * glm::max(_registrationPoint, glm::vec3(1.0f) - _registrationPoint);
// and then scale by dimensions and add the absolute value of the pivot
glm::vec3 registrationPoint = getRegistrationPoint();
glm::vec3 maxExtents = getScaledDimensions() * glm::max(registrationPoint, glm::vec3(1.0f) - registrationPoint);
// there exists a sphere that contains maxExtents for all rotations
float radius = glm::length(maxExtents);
// put a cube around the sphere
// TODO? replace _maxAACube with _boundingSphereRadius
glm::vec3 minimumCorner = centerOfRotation - glm::vec3(radius, radius, radius);
glm::vec3 minimumCorner = (centerOfRotation + getWorldOrientation() * getPivot()) - glm::vec3(radius, radius, radius);
_maxAACube = AACube(minimumCorner, radius * 2.0f);
}
} else {
@ -1650,9 +1656,12 @@ AACube EntityItem::getMinimumAACube(bool& success) const {
if (success) {
_recalcMinAACube = false;
glm::vec3 dimensions = getScaledDimensions();
glm::vec3 unrotatedMinRelativeToEntity = - (dimensions * _registrationPoint);
glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (glm::vec3(1.0f, 1.0f, 1.0f) - _registrationPoint);
glm::vec3 registrationPoint = getRegistrationPoint();
glm::vec3 pivot = getPivot();
glm::vec3 unrotatedMinRelativeToEntity = -(dimensions * registrationPoint);
glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (ENTITY_ITEM_ONE_VEC3 - registrationPoint);
Extents extents = { unrotatedMinRelativeToEntity, unrotatedMaxRelativeToEntity };
extents.shiftBy(pivot);
extents.rotate(getWorldOrientation());
// shift the extents to be relative to the position/registration point
@ -1680,9 +1689,12 @@ AABox EntityItem::getAABox(bool& success) const {
if (success) {
_recalcAABox = false;
glm::vec3 dimensions = getScaledDimensions();
glm::vec3 unrotatedMinRelativeToEntity = - (dimensions * _registrationPoint);
glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (glm::vec3(1.0f, 1.0f, 1.0f) - _registrationPoint);
glm::vec3 registrationPoint = getRegistrationPoint();
glm::vec3 pivot = getPivot();
glm::vec3 unrotatedMinRelativeToEntity = -(dimensions * registrationPoint);
glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (ENTITY_ITEM_ONE_VEC3 - registrationPoint);
Extents extents = { unrotatedMinRelativeToEntity, unrotatedMaxRelativeToEntity };
extents.shiftBy(pivot);
extents.rotate(getWorldOrientation());
// shift the extents to be relative to the position/registration point
@ -1722,12 +1734,22 @@ float EntityItem::getRadius() const {
return 0.5f * glm::length(getScaledDimensions());
}
void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info) const {
if (_registrationPoint != ENTITY_ITEM_DEFAULT_REGISTRATION_POINT) {
glm::mat4 scale = glm::scale(getScaledDimensions());
glm::mat4 registration = scale * glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint());
glm::vec3 regTransVec = glm::vec3(registration[3]); // extract position component from matrix
info.setOffset(regTransVec);
void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info, bool includePivot) const {
glm::vec3 offset;
glm::vec3 registrationPoint = getRegistrationPoint();
if (registrationPoint != ENTITY_ITEM_DEFAULT_REGISTRATION_POINT) {
offset += (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - registrationPoint) * getScaledDimensions();
}
if (includePivot) {
glm::vec3 pivot = getPivot();
if (pivot != ENTITY_ITEM_ZERO_VEC3) {
offset += pivot;
}
}
if (offset != ENTITY_ITEM_ZERO_VEC3) {
info.setOffset(offset);
}
}
@ -1739,7 +1761,7 @@ bool EntityItem::contains(const glm::vec3& point) const {
// anything with shapeType == SPHERE must collide as a bounding sphere in the world-frame regardless of dimensions
// therefore we must do math using an unscaled localPoint relative to sphere center
glm::vec3 dimensions = getScaledDimensions();
glm::vec3 localPoint = point - (getWorldPosition() + getWorldOrientation() * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint())));
glm::vec3 localPoint = point - (getWorldPosition() + getWorldOrientation() * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()) + getPivot()));
const float HALF_SQUARED = 0.25f;
return glm::length2(localPoint) < HALF_SQUARED * glm::length2(dimensions);
}
@ -1797,11 +1819,16 @@ float EntityItem::getVolumeEstimate() const {
}
void EntityItem::setRegistrationPoint(const glm::vec3& value) {
if (value != _registrationPoint) {
withWriteLock([&] {
bool changed = false;
withWriteLock([&] {
if (value != _registrationPoint) {
_registrationPoint = glm::clamp(value, glm::vec3(ENTITY_ITEM_MIN_REGISTRATION_POINT),
glm::vec3(ENTITY_ITEM_MAX_REGISTRATION_POINT));
});
changed = true;
}
});
if (changed) {
dimensionsChanged(); // Registration Point affects the bounding box
markDirtyFlags(Simulation::DIRTY_SHAPE);
}
@ -2892,11 +2919,9 @@ QString EntityItem::getCollisionSoundURL() const {
}
glm::vec3 EntityItem::getRegistrationPoint() const {
glm::vec3 result;
withReadLock([&] {
result = _registrationPoint;
return resultWithReadLock<glm::vec3>([&] {
return _registrationPoint;
});
return result;
}
float EntityItem::getAngularDamping() const {

View file

@ -204,6 +204,7 @@ public:
virtual glm::vec3 getScaledDimensions() const;
virtual void setScaledDimensions(const glm::vec3& value);
virtual glm::vec3 getRaycastDimensions() const { return getScaledDimensions(); }
virtual glm::vec3 getPivot() const { return glm::vec3(0.0f); } // pivot offset for positioning, mainly for model entities
glm::vec3 getUnscaledDimensions() const;
virtual void setUnscaledDimensions(const glm::vec3& value);
@ -403,7 +404,7 @@ public:
// TODO: get rid of users of getRadius()...
float getRadius() const;
virtual void adjustShapeInfoByRegistration(ShapeInfo& info) const;
virtual void adjustShapeInfoByRegistration(ShapeInfo& info, bool includePivot = true) const;
virtual bool contains(const glm::vec3& point) const;
virtual bool isReadyToComputeShape() const { return !isDead(); }

View file

@ -538,6 +538,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
CHECK_PROPERTY_CHANGE(PROP_RELAY_PARENT_JOINTS, relayParentJoints);
CHECK_PROPERTY_CHANGE(PROP_GROUP_CULLED, groupCulled);
CHECK_PROPERTY_CHANGE(PROP_BLENDSHAPE_COEFFICIENTS, blendshapeCoefficients);
CHECK_PROPERTY_CHANGE(PROP_USE_ORIGINAL_PIVOT, useOriginalPivot);
changedProperties += _animation.getChangedProperties();
// Light
@ -1002,6 +1003,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
* @property {string} blendshapeCoefficients - A JSON string of a map of blendshape names to values. Only stores set values.
* When editing this property, only coefficients that you are editing will change; it will not explicitly reset other
* coefficients.
* @property {boolean} useOriginalPivot=false - If <code>false</code>, the model will be centered based on its content,
* ignoring any offset in the model itself. If <code>true</code>, the model will respect its original offset. Currently,
* only pivots relative to <code>{x: 0, y: 0, z: 0}</code> are supported.
* @property {string} textures="" - A JSON string of texture name, URL pairs used when rendering the model in place of the
* model's original textures. Use a texture name from the <code>originalTextures</code> property to override that texture.
* Only the texture names and URLs to be overridden need be specified; original textures are used where there are no
@ -1733,6 +1737,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RELAY_PARENT_JOINTS, relayParentJoints);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GROUP_CULLED, groupCulled);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_BLENDSHAPE_COEFFICIENTS, blendshapeCoefficients);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USE_ORIGINAL_PIVOT, useOriginalPivot);
if (!psuedoPropertyFlagsButDesiredEmpty) {
_animation.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
}
@ -2142,6 +2147,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
COPY_PROPERTY_FROM_QSCRIPTVALUE(relayParentJoints, bool, setRelayParentJoints);
COPY_PROPERTY_FROM_QSCRIPTVALUE(groupCulled, bool, setGroupCulled);
COPY_PROPERTY_FROM_QSCRIPTVALUE(blendshapeCoefficients, QString, setBlendshapeCoefficients);
COPY_PROPERTY_FROM_QSCRIPTVALUE(useOriginalPivot, bool, setUseOriginalPivot);
_animation.copyFromScriptValue(object, _defaultSettings);
// Light
@ -2434,6 +2440,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
COPY_PROPERTY_IF_CHANGED(relayParentJoints);
COPY_PROPERTY_IF_CHANGED(groupCulled);
COPY_PROPERTY_IF_CHANGED(blendshapeCoefficients);
COPY_PROPERTY_IF_CHANGED(useOriginalPivot);
_animation.merge(other._animation);
// Light
@ -2790,6 +2797,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
ADD_PROPERTY_TO_MAP(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool);
ADD_PROPERTY_TO_MAP(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool);
ADD_PROPERTY_TO_MAP(PROP_BLENDSHAPE_COEFFICIENTS, BlendshapeCoefficients, blendshapeCoefficients, QString);
ADD_PROPERTY_TO_MAP(PROP_USE_ORIGINAL_PIVOT, UseOriginalPivot, useOriginalPivot, bool);
{ // Animation
ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_URL, Animation, animation, URL, url);
ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_ALLOW_TRANSLATION, Animation, animation, AllowTranslation, allowTranslation);
@ -3231,6 +3239,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, properties.getRelayParentJoints());
APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, properties.getGroupCulled());
APPEND_ENTITY_PROPERTY(PROP_BLENDSHAPE_COEFFICIENTS, properties.getBlendshapeCoefficients());
APPEND_ENTITY_PROPERTY(PROP_USE_ORIGINAL_PIVOT, properties.getUseOriginalPivot());
_staticAnimation.setProperties(properties);
_staticAnimation.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState);
@ -3720,6 +3729,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GROUP_CULLED, bool, setGroupCulled);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_BLENDSHAPE_COEFFICIENTS, QString, setBlendshapeCoefficients);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_USE_ORIGINAL_PIVOT, bool, setUseOriginalPivot);
properties.getAnimation().decodeFromEditPacket(propertyFlags, dataAt, processedBytes);
}
@ -4129,6 +4139,7 @@ void EntityItemProperties::markAllChanged() {
_relayParentJointsChanged = true;
_groupCulledChanged = true;
_blendshapeCoefficientsChanged = true;
_useOriginalPivotChanged = true;
_animation.markAllChanged();
// Light
@ -4701,6 +4712,9 @@ QList<QString> EntityItemProperties::listChangedProperties() {
if (blendshapeCoefficientsChanged()) {
out += "blendshapeCoefficients";
}
if (useOriginalPivotChanged()) {
out += "useOriginalPivot";
}
getAnimation().listChangedProperties(out);
// Light

View file

@ -302,6 +302,7 @@ public:
DEFINE_PROPERTY(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool, ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS);
DEFINE_PROPERTY_REF(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool, false);
DEFINE_PROPERTY_REF(PROP_BLENDSHAPE_COEFFICIENTS, BlendshapeCoefficients, blendshapeCoefficients, QString, "");
DEFINE_PROPERTY_REF(PROP_USE_ORIGINAL_PIVOT, UseOriginalPivot, useOriginalPivot, bool, false);
DEFINE_PROPERTY_GROUP(Animation, animation, AnimationPropertyGroup);
// Light

View file

@ -218,16 +218,17 @@ enum EntityPropertyList {
PROP_RELAY_PARENT_JOINTS = PROP_DERIVED_6,
PROP_GROUP_CULLED = PROP_DERIVED_7,
PROP_BLENDSHAPE_COEFFICIENTS = PROP_DERIVED_8,
PROP_USE_ORIGINAL_PIVOT = PROP_DERIVED_9,
// Animation
PROP_ANIMATION_URL = PROP_DERIVED_9,
PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_10,
PROP_ANIMATION_FPS = PROP_DERIVED_11,
PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_12,
PROP_ANIMATION_PLAYING = PROP_DERIVED_13,
PROP_ANIMATION_LOOP = PROP_DERIVED_14,
PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_15,
PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_16,
PROP_ANIMATION_HOLD = PROP_DERIVED_17,
PROP_ANIMATION_URL = PROP_DERIVED_10,
PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_11,
PROP_ANIMATION_FPS = PROP_DERIVED_12,
PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_13,
PROP_ANIMATION_PLAYING = PROP_DERIVED_14,
PROP_ANIMATION_LOOP = PROP_DERIVED_15,
PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_16,
PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_17,
PROP_ANIMATION_HOLD = PROP_DERIVED_18,
// Light
PROP_IS_SPOTLIGHT = PROP_DERIVED_0,

View file

@ -205,10 +205,7 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori
// (this is faster and more likely to cull results than the filter check below so we do it first)
bool success;
AABox entityBox = entity->getAABox(success);
if (!success) {
return;
}
if (!entityBox.rayHitsBoundingSphere(origin, direction)) {
if (!success || !entityBox.rayHitsBoundingSphere(origin, direction)) {
return;
}
@ -226,7 +223,7 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori
glm::vec3 dimensions = entity->getRaycastDimensions();
glm::vec3 registrationPoint = entity->getRegistrationPoint();
glm::vec3 corner = -(dimensions * registrationPoint);
glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot();
AABox entityFrameBox(corner, dimensions);
@ -277,11 +274,12 @@ bool EntityTreeElement::findSpherePenetration(const glm::vec3& center, float rad
bool result = false;
withReadLock([&] {
foreach(EntityItemPointer entity, _entityItems) {
glm::vec3 entityCenter = entity->getWorldPosition();
bool success;
glm::vec3 entityCenter = entity->getCenterPosition(success);
float entityRadius = entity->getRadius();
// don't penetrate yourself
if (entityCenter == center && entityRadius == radius) {
if (!success || (entityCenter == center && entityRadius == radius)) {
return;
}
@ -349,15 +347,12 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3
// (this is faster and more likely to cull results than the filter check below so we do it first)
bool success;
AABox entityBox = entity->getAABox(success);
if (!success) {
return;
}
// Instead of checking parabolaInstersectsBoundingSphere here, we are just going to check if the plane
// defined by the parabola slices the sphere. The solution to parabolaIntersectsBoundingSphere is cubic,
// the solution to which is more computationally expensive than the quadratic AABox::findParabolaIntersection
// below
if (!entityBox.parabolaPlaneIntersectsBoundingSphere(origin, velocity, acceleration, normal)) {
if (!success || !entityBox.parabolaPlaneIntersectsBoundingSphere(origin, velocity, acceleration, normal)) {
return;
}
@ -375,7 +370,7 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3
glm::vec3 dimensions = entity->getRaycastDimensions();
glm::vec3 registrationPoint = entity->getRegistrationPoint();
glm::vec3 corner = -(dimensions * registrationPoint);
glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot();
AABox entityFrameBox(corner, dimensions);
@ -445,7 +440,6 @@ void EntityTreeElement::evalEntitiesInSphere(const glm::vec3& position, float ra
bool success;
AABox entityBox = entity->getAABox(success);
// if the sphere doesn't intersect with our world frame AABox, we don't need to consider the more complex case
glm::vec3 penetration;
if (success && entityBox.findSpherePenetration(position, radius, penetration)) {
@ -464,10 +458,9 @@ void EntityTreeElement::evalEntitiesInSphere(const glm::vec3& position, float ra
float entityTrueRadius = dimensions.x / 2.0f;
bool success;
if (findSphereSpherePenetration(position, radius, entity->getCenterPosition(success), entityTrueRadius, penetration)) {
if (success) {
foundEntities.push_back(entity->getID());
}
glm::vec3 center = entity->getCenterPosition(success);
if (success && findSphereSpherePenetration(position, radius, center, entityTrueRadius, penetration)) {
foundEntities.push_back(entity->getID());
}
} else {
// determine the worldToEntityMatrix that doesn't include scale because
@ -478,7 +471,7 @@ void EntityTreeElement::evalEntitiesInSphere(const glm::vec3& position, float ra
glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix);
glm::vec3 registrationPoint = entity->getRegistrationPoint();
glm::vec3 corner = -(dimensions * registrationPoint);
glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot();
AABox entityFrameBox(corner, dimensions);
@ -499,7 +492,6 @@ void EntityTreeElement::evalEntitiesInSphereWithType(const glm::vec3& position,
bool success;
AABox entityBox = entity->getAABox(success);
// if the sphere doesn't intersect with our world frame AABox, we don't need to consider the more complex case
glm::vec3 penetration;
if (success && entityBox.findSpherePenetration(position, radius, penetration)) {
@ -518,10 +510,9 @@ void EntityTreeElement::evalEntitiesInSphereWithType(const glm::vec3& position,
float entityTrueRadius = dimensions.x / 2.0f;
bool success;
if (findSphereSpherePenetration(position, radius, entity->getCenterPosition(success), entityTrueRadius, penetration)) {
if (success) {
foundEntities.push_back(entity->getID());
}
glm::vec3 center = entity->getCenterPosition(success);
if (success && findSphereSpherePenetration(position, radius, center, entityTrueRadius, penetration)) {
foundEntities.push_back(entity->getID());
}
} else {
// determine the worldToEntityMatrix that doesn't include scale because
@ -532,7 +523,7 @@ void EntityTreeElement::evalEntitiesInSphereWithType(const glm::vec3& position,
glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix);
glm::vec3 registrationPoint = entity->getRegistrationPoint();
glm::vec3 corner = -(dimensions * registrationPoint);
glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot();
AABox entityFrameBox(corner, dimensions);
@ -575,12 +566,11 @@ void EntityTreeElement::evalEntitiesInSphereWithName(const glm::vec3& position,
// NOTE: entity->getRadius() doesn't return the true radius, it returns the radius of the
// maximum bounding sphere, which is actually larger than our actual radius
float entityTrueRadius = dimensions.x / 2.0f;
bool success;
if (findSphereSpherePenetration(position, radius, entity->getCenterPosition(success), entityTrueRadius, penetration)) {
if (success) {
foundEntities.push_back(entity->getID());
}
glm::vec3 center = entity->getCenterPosition(success);
if (success && findSphereSpherePenetration(position, radius, center, entityTrueRadius, penetration)) {
foundEntities.push_back(entity->getID());
}
} else {
// determine the worldToEntityMatrix that doesn't include scale because
@ -591,7 +581,7 @@ void EntityTreeElement::evalEntitiesInSphereWithName(const glm::vec3& position,
glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix);
glm::vec3 registrationPoint = entity->getRegistrationPoint();
glm::vec3 corner = -(dimensions * registrationPoint);
glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot();
AABox entityFrameBox(corner, dimensions);
@ -612,6 +602,7 @@ void EntityTreeElement::evalEntitiesInCube(const AACube& cube, PickFilter search
bool success;
AABox entityBox = entity->getAABox(success);
// FIXME - handle entity->getShapeType() == SHAPE_TYPE_SPHERE case better
// FIXME - consider allowing the entity to determine penetration so that
// entities could presumably dull actuall hull testing if they wanted to
@ -642,6 +633,7 @@ void EntityTreeElement::evalEntitiesInBox(const AABox& box, PickFilter searchFil
bool success;
AABox entityBox = entity->getAABox(success);
// FIXME - handle entity->getShapeType() == SHAPE_TYPE_SPHERE case better
// FIXME - consider allowing the entity to determine penetration so that
// entities could presumably dull actuall hull testing if they wanted to
@ -680,7 +672,7 @@ void EntityTreeElement::evalEntitiesInFrustum(const ViewFrustum& frustum, PickFi
});
}
void EntityTreeElement::getEntities(EntityItemFilter& filter, QVector<EntityItemPointer>& foundEntities) {
void EntityTreeElement::getEntities(EntityItemFilter& filter, QVector<EntityItemPointer>& foundEntities) {
forEachEntity([&](EntityItemPointer entity) {
if (filter(entity)) {
foundEntities.push_back(entity);

View file

@ -73,6 +73,7 @@ EntityItemProperties ModelEntityItem::getProperties(const EntityPropertyFlags& d
COPY_ENTITY_PROPERTY_TO_PROPERTIES(relayParentJoints, getRelayParentJoints);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(groupCulled, getGroupCulled);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(blendshapeCoefficients, getBlendshapeCoefficients);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(useOriginalPivot, getUseOriginalPivot);
withReadLock([&] {
_animationProperties.getProperties(properties);
});
@ -96,6 +97,7 @@ bool ModelEntityItem::setSubClassProperties(const EntityItemProperties& properti
SET_ENTITY_PROPERTY_FROM_PROPERTIES(relayParentJoints, setRelayParentJoints);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(groupCulled, setGroupCulled);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(blendshapeCoefficients, setBlendshapeCoefficients);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(useOriginalPivot, setUseOriginalPivot);
withWriteLock([&] {
AnimationPropertyGroup animationProperties = _animationProperties;
@ -130,6 +132,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
READ_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints);
READ_ENTITY_PROPERTY(PROP_GROUP_CULLED, bool, setGroupCulled);
READ_ENTITY_PROPERTY(PROP_BLENDSHAPE_COEFFICIENTS, QString, setBlendshapeCoefficients);
READ_ENTITY_PROPERTY(PROP_USE_ORIGINAL_PIVOT, bool, setUseOriginalPivot);
// grab a local copy of _animationProperties to avoid multiple locks
int bytesFromAnimation;
@ -169,6 +172,7 @@ EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams&
requestedProperties += PROP_RELAY_PARENT_JOINTS;
requestedProperties += PROP_GROUP_CULLED;
requestedProperties += PROP_BLENDSHAPE_COEFFICIENTS;
requestedProperties += PROP_USE_ORIGINAL_PIVOT;
requestedProperties += _animationProperties.getEntityProperties(params);
return requestedProperties;
@ -198,6 +202,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit
APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, getRelayParentJoints());
APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, getGroupCulled());
APPEND_ENTITY_PROPERTY(PROP_BLENDSHAPE_COEFFICIENTS, getBlendshapeCoefficients());
APPEND_ENTITY_PROPERTY(PROP_USE_ORIGINAL_PIVOT, getUseOriginalPivot());
withReadLock([&] {
_animationProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties,
@ -251,6 +256,7 @@ void ModelEntityItem::debugDump() const {
qCDebug(entities) << " model URL:" << getModelURL();
qCDebug(entities) << " compound shape URL:" << getCompoundShapeURL();
qCDebug(entities) << " blendshapeCoefficients:" << getBlendshapeCoefficients();
qCDebug(entities) << " useOrigialPivot:" << getUseOriginalPivot();
}
void ModelEntityItem::setShapeType(ShapeType type) {
@ -713,3 +719,25 @@ QVector<float> ModelEntityItem::getBlendshapeCoefficientVector() {
return _blendshapeCoefficientsVector;
});
}
void ModelEntityItem::setUseOriginalPivot(bool value) {
bool changed = false;
withWriteLock([&] {
if (_useOriginalPivot != value) {
_needsRenderUpdate = true;
_useOriginalPivot = value;
changed = true;
}
});
if (changed) {
markDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS);
locationChanged();
}
}
bool ModelEntityItem::getUseOriginalPivot() const {
return resultWithReadLock<bool>([&] {
return _useOriginalPivot;
});
}

View file

@ -119,6 +119,9 @@ public:
bool blendshapesChanged() const { return _blendshapesChanged; }
QVector<float> getBlendshapeCoefficientVector();
bool getUseOriginalPivot() const;
void setUseOriginalPivot(bool useOriginalPivot);
private:
void setAnimationSettings(const QString& value); // only called for old bitstream format
bool applyNewAnimationProperties(AnimationPropertyGroup newProperties);
@ -152,6 +155,7 @@ protected:
bool _relayParentJoints;
bool _groupCulled { false };
QVariantMap _blendshapeCoefficientsMap;
bool _useOriginalPivot { false };
ThreadSafeValueCache<QString> _compoundShapeURL;

View file

@ -104,7 +104,7 @@ bool HFMModel::convexHullContains(const glm::vec3& point) const {
auto checkEachPrimitive = [=](HFMMesh& mesh, QVector<int> indices, int primitiveSize) -> bool {
// Check whether the point is "behind" all the primitives.
// But first must transform from model-frame into mesh-frame
glm::vec3 transformedPoint = glm::vec3(glm::inverse(mesh.modelTransform) * glm::vec4(point, 1.0f));
glm::vec3 transformedPoint = glm::vec3(glm::inverse(offset * mesh.modelTransform) * glm::vec4(point, 1.0f));
int verticesSize = mesh.vertices.size();
for (int j = 0;
j < indices.size() - 2; // -2 in case the vertices aren't the right size -- we access j + 2 below

View file

@ -283,6 +283,7 @@ enum class EntityVersion : PacketVersion {
ZoneOcclusion,
ModelBlendshapes,
TransparentWeb,
UseOriginalPivot,
// Add new versions above here
NUM_PACKET_TYPE,

View file

@ -343,7 +343,11 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g
}
// extents is the entity relative, scaled, centered extents of the entity
glm::mat4 modelToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation);
glm::mat4 transRot = createMatFromQuatAndPos(_rotation, _translation);
glm::mat4 modelToWorldMatrix = transRot;
if (!_snapModelToRegistrationPoint) {
modelToWorldMatrix = modelToWorldMatrix * glm::translate(getOriginalOffset());
}
glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix);
Extents modelExtents = getMeshExtents(); // NOTE: unrotated
@ -375,8 +379,12 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g
calculateTriangleSets(hfmModel);
}
glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset);
glm::mat4 meshToWorldMatrix = modelToWorldMatrix * meshToModelMatrix;
glm::mat4 meshToWorldMatrix = transRot;
if (_snapModelToRegistrationPoint || _forceOffset) {
meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(_offset));
} else {
meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(getNaturalDimensions() * (0.5f - _registrationPoint)));
}
glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix);
glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f));
@ -498,7 +506,11 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co
}
// extents is the entity relative, scaled, centered extents of the entity
glm::mat4 modelToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation);
glm::mat4 transRot = createMatFromQuatAndPos(_rotation, _translation);
glm::mat4 modelToWorldMatrix = transRot;
if (!_snapModelToRegistrationPoint) {
modelToWorldMatrix = modelToWorldMatrix * glm::translate(getOriginalOffset());
}
glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix);
Extents modelExtents = getMeshExtents(); // NOTE: unrotated
@ -531,8 +543,12 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co
calculateTriangleSets(hfmModel);
}
glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset);
glm::mat4 meshToWorldMatrix = modelToWorldMatrix * meshToModelMatrix;
glm::mat4 meshToWorldMatrix = transRot;
if (_snapModelToRegistrationPoint || _forceOffset) {
meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(_offset));
} else {
meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(getNaturalDimensions() * (0.5f - _registrationPoint)));
}
glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix);
glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f));
@ -1182,17 +1198,8 @@ glm::vec3 Model::getNaturalDimensions() const {
}
Extents Model::getMeshExtents() const {
if (!isLoaded()) {
return Extents();
}
const Extents& extents = getHFMModel().meshExtents;
// even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which
// is captured in the offset matrix
glm::vec3 minimum = glm::vec3(getHFMModel().offset * glm::vec4(extents.minimum, 1.0f));
glm::vec3 maximum = glm::vec3(getHFMModel().offset * glm::vec4(extents.maximum, 1.0f));
Extents scaledExtents = { minimum * _scale, maximum * _scale };
return scaledExtents;
Extents extents = getUnscaledMeshExtents();
return { extents.minimum * _scale, extents.maximum * _scale };
}
Extents Model::getUnscaledMeshExtents() const {
@ -1417,6 +1424,15 @@ void Model::snapToRegistrationPoint() {
_snappedToRegistrationPoint = true;
}
glm::vec3 Model::getOriginalOffset() const {
Extents modelMeshExtents = getUnscaledMeshExtents();
glm::vec3 dimensions = (modelMeshExtents.maximum - modelMeshExtents.minimum);
glm::vec3 offset = modelMeshExtents.minimum + (0.5f * dimensions);
glm::mat4 transform = glm::scale(_scale) * glm::translate(offset);
return transform[3];
}
void Model::setUseDualQuaternionSkinning(bool value) {
_useDualQuaternionSkinning = value;
}
@ -1437,7 +1453,8 @@ void Model::simulate(float deltaTime, bool fullUpdate) {
snapToRegistrationPoint();
}
// update the world space transforms for all joints
glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset);
glm::mat4 parentTransform = glm::scale(_scale) * ((_snapModelToRegistrationPoint || _forceOffset) ?
glm::translate(_offset) : glm::translate(getNaturalDimensions() * (0.5f - _registrationPoint)));
updateRig(deltaTime, parentTransform);
}
}

View file

@ -167,6 +167,7 @@ public:
void setSnapModelToRegistrationPoint(bool snapModelToRegistrationPoint, const glm::vec3& registrationPoint);
bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; }
bool getSnappedToRegistrationPoint() { return _snappedToRegistrationPoint; }
virtual void simulate(float deltaTime, bool fullUpdate = true);
virtual void updateClusterMatrices();
@ -203,6 +204,7 @@ public:
void setOffset(const glm::vec3& offset);
const glm::vec3& getOffset() const { return _offset; }
glm::vec3 getOriginalOffset() const;
void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f, bool forceRescale = false);
void setScaleToFit(bool scaleToFit, const glm::vec3& dimensions, bool forceRescale = false);
@ -348,6 +350,7 @@ public:
virtual bool replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer model, int meshIndex, int partIndex) override;
void scaleToFit();
void snapToRegistrationPoint();
bool getUseDualQuaternionSkinning() const { return _useDualQuaternionSkinning; }
void setUseDualQuaternionSkinning(bool value);
@ -409,14 +412,14 @@ protected:
bool _snapModelToRegistrationPoint; /// is the model's offset automatically adjusted to a registration point in model space
bool _snappedToRegistrationPoint; /// are we currently snapped to a registration point
glm::vec3 _registrationPoint = glm::vec3(0.5f); /// the point in model space our center is snapped to
glm::vec3 _registrationPoint { glm::vec3(0.5f) }; /// the point in model space our center is snapped to
bool _forceOffset { false };
std::vector<MeshState> _meshStates;
virtual void initJointStates();
void setScaleInternal(const glm::vec3& scale);
void snapToRegistrationPoint();
virtual void updateRig(float deltaTime, glm::mat4 parentTransform);

View file

@ -601,6 +601,9 @@
"groupCulled": {
"tooltip": "If false, individual pieces of the entity may be culled by the render engine. If true, either the entire entity will be culled, or it won't at all."
},
"useOriginalPivot": {
"tooltip": "If false, the model will be centered based on its content, ignoring any offset in the model itself. If true, the model will respect its original offset."
},
"webColor": {
"tooltip": "The tint of the web entity."
},

View file

@ -741,6 +741,7 @@ var toolBar = (function () {
grabbable: result.grabbable
},
dynamic: dynamic,
useOriginalPivot: result.useOriginalPivot
});
}
}

View file

@ -614,6 +614,11 @@ const GROUPS = [
propertyID: "compoundShapeURL",
hideIfCertified: true,
},
{
label: "Use Original Pivot",
type: "bool",
propertyID: "useOriginalPivot",
},
{
label: "Animation",
type: "string",

View file

@ -120,6 +120,12 @@ Rectangle {
height: 600
spacing: 10
CheckBox {
id: useOriginalPivot
text: qsTr("Use Original Pivot")
checked: true
}
CheckBox {
id: grabbable
text: qsTr("Grabbable")
@ -219,7 +225,8 @@ Rectangle {
url: modelURL.text,
dynamic: dynamic.checked,
collisionShapeIndex: collisionType.currentIndex,
grabbable: grabbable.checked
grabbable: grabbable.checked,
useOriginalPivot: useOriginalPivot.checked
}
});
}