mirror of
https://github.com/overte-org/overte.git
synced 2025-04-21 08:04:01 +02:00
Merge remote-tracking branch 'upstream/master' into 21166
This commit is contained in:
commit
82236bdcb9
35 changed files with 1047 additions and 729 deletions
|
@ -168,7 +168,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
|
|||
QList<AvatarSharedPointer> avatarList;
|
||||
std::unordered_map<AvatarSharedPointer, SharedNodePointer> avatarDataToNodes;
|
||||
|
||||
int listItem = 0;
|
||||
std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) {
|
||||
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
|
||||
|
||||
|
@ -176,7 +175,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
|
|||
// but not have yet sent data that's linked to the node. Check for that case and don't
|
||||
// consider those nodes.
|
||||
if (otherNodeData) {
|
||||
listItem++;
|
||||
AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer();
|
||||
avatarList << otherAvatar;
|
||||
avatarDataToNodes[otherAvatar] = otherNode;
|
||||
|
@ -185,8 +183,8 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
|
|||
|
||||
AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer();
|
||||
ViewFrustum cameraView = nodeData->getViewFrustom();
|
||||
std::priority_queue<AvatarPriority> sortedAvatars = AvatarData::sortAvatars(
|
||||
avatarList, cameraView,
|
||||
std::priority_queue<AvatarPriority> sortedAvatars;
|
||||
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
|
||||
|
||||
[&](AvatarSharedPointer avatar)->uint64_t{
|
||||
auto avatarNode = avatarDataToNodes[avatar];
|
||||
|
|
BIN
interface/resources/avatar/animations/sitting.fbx
Normal file
BIN
interface/resources/avatar/animations/sitting.fbx
Normal file
Binary file not shown.
BIN
interface/resources/avatar/animations/sitting_idle.fbx
Normal file
BIN
interface/resources/avatar/animations/sitting_idle.fbx
Normal file
Binary file not shown.
|
@ -335,7 +335,7 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// Per-Avatar Gain Slider
|
||||
// Per-Avatar Gain Slider
|
||||
Slider {
|
||||
id: gainSlider
|
||||
// Size
|
||||
|
@ -345,7 +345,7 @@ Item {
|
|||
anchors.verticalCenter: nameCardVUMeter.verticalCenter
|
||||
// Properties
|
||||
visible: !isMyCard && selected
|
||||
value: pal.gainSliderValueDB[uuid] ? pal.gainSliderValueDB[uuid] : 0.0
|
||||
value: Users.getAvatarGain(uuid)
|
||||
minimumValue: -60.0
|
||||
maximumValue: 20.0
|
||||
stepSize: 5
|
||||
|
@ -369,7 +369,7 @@ Item {
|
|||
mouse.accepted = false
|
||||
}
|
||||
onReleased: {
|
||||
// the above mouse.accepted seems to make this
|
||||
// the above mouse.accepted seems to make this
|
||||
// never get called, nonetheless...
|
||||
mouse.accepted = false
|
||||
}
|
||||
|
@ -393,14 +393,9 @@ Item {
|
|||
}
|
||||
|
||||
function updateGainFromQML(avatarUuid, sliderValue, isReleased) {
|
||||
if (isReleased || pal.gainSliderValueDB[avatarUuid] !== sliderValue) {
|
||||
pal.gainSliderValueDB[avatarUuid] = sliderValue;
|
||||
var data = {
|
||||
sessionId: avatarUuid,
|
||||
gain: sliderValue,
|
||||
isReleased: isReleased
|
||||
};
|
||||
pal.sendToScript({method: 'updateGain', params: data});
|
||||
Users.setAvatarGain(avatarUuid, sliderValue);
|
||||
if (isReleased) {
|
||||
UserActivityLogger.palAction("avatar_gain_changed", avatarUuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Pal.qml
|
||||
// qml/hifi
|
||||
//
|
||||
// People Action List
|
||||
// People Action List
|
||||
//
|
||||
// Created by Howard Stearns on 12/12/2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
|
@ -37,9 +37,6 @@ Rectangle {
|
|||
property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring.
|
||||
property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
|
||||
property bool iAmAdmin: false
|
||||
// Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server.
|
||||
// NOTE: if another script modifies the per-avatar gain, this value won't be accurate!
|
||||
property var gainSliderValueDB: ({});
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
|
@ -270,7 +267,7 @@ Rectangle {
|
|||
// Anchors
|
||||
anchors.left: parent.left
|
||||
}
|
||||
|
||||
|
||||
// This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now)
|
||||
// KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox
|
||||
// will appear in the "hovered" state. Hovering over the checkbox will fix it.
|
||||
|
@ -306,7 +303,7 @@ Rectangle {
|
|||
checked = Qt.binding(function() { return (model[styleData.role])})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now)
|
||||
HifiControls.Button {
|
||||
id: actionButton
|
||||
|
@ -538,7 +535,7 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case 'updateAudioLevel':
|
||||
case 'updateAudioLevel':
|
||||
for (var userId in message.params) {
|
||||
var audioLevel = message.params[userId];
|
||||
// If the userId is 0, we're updating "myData".
|
||||
|
@ -554,9 +551,8 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case 'clearLocalQMLData':
|
||||
case 'clearLocalQMLData':
|
||||
ignored = {};
|
||||
gainSliderValueDB = {};
|
||||
break;
|
||||
case 'avatarDisconnected':
|
||||
var sessionID = message.params[0];
|
||||
|
|
|
@ -351,7 +351,6 @@ void Avatar::simulate(float deltaTime, bool inView) {
|
|||
_jointDataSimulationRate.increment();
|
||||
|
||||
_skeletonModel->simulate(deltaTime, true);
|
||||
_skeletonModelSimulationRate.increment();
|
||||
|
||||
locationChanged(); // joints changed, so if there are any children, update them.
|
||||
_hasNewJointData = false;
|
||||
|
@ -367,8 +366,8 @@ void Avatar::simulate(float deltaTime, bool inView) {
|
|||
} else {
|
||||
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
|
||||
_skeletonModel->simulate(deltaTime, false);
|
||||
_skeletonModelSimulationRate.increment();
|
||||
}
|
||||
_skeletonModelSimulationRate.increment();
|
||||
}
|
||||
|
||||
// update animation for display name fade in/out
|
||||
|
|
|
@ -157,15 +157,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
lock.unlock();
|
||||
|
||||
PerformanceTimer perfTimer("otherAvatars");
|
||||
uint64_t startTime = usecTimestampNow();
|
||||
|
||||
auto avatarMap = getHashCopy();
|
||||
QList<AvatarSharedPointer> avatarList = avatarMap.values();
|
||||
ViewFrustum cameraView;
|
||||
qApp->copyDisplayViewFrustum(cameraView);
|
||||
|
||||
std::priority_queue<AvatarPriority> sortedAvatars = AvatarData::sortAvatars(
|
||||
avatarList, cameraView,
|
||||
std::priority_queue<AvatarPriority> sortedAvatars;
|
||||
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
|
||||
|
||||
[](AvatarSharedPointer avatar)->uint64_t{
|
||||
return std::static_pointer_cast<Avatar>(avatar)->getLastRenderUpdateTime();
|
||||
|
@ -194,10 +193,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
});
|
||||
|
||||
render::PendingChanges pendingChanges;
|
||||
const uint64_t RENDER_UPDATE_BUDGET = 1500; // usec
|
||||
const uint64_t MAX_UPDATE_BUDGET = 2000; // usec
|
||||
uint64_t renderExpiry = startTime + RENDER_UPDATE_BUDGET;
|
||||
uint64_t maxExpiry = startTime + MAX_UPDATE_BUDGET;
|
||||
uint64_t startTime = usecTimestampNow();
|
||||
const uint64_t UPDATE_BUDGET = 2000; // usec
|
||||
uint64_t updateExpiry = startTime + UPDATE_BUDGET;
|
||||
|
||||
int numAvatarsUpdated = 0;
|
||||
int numAVatarsNotUpdated = 0;
|
||||
|
@ -223,7 +221,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
|
||||
const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY;
|
||||
uint64_t now = usecTimestampNow();
|
||||
if (now < renderExpiry) {
|
||||
if (now < updateExpiry) {
|
||||
// we're within budget
|
||||
bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
|
||||
if (inView && avatar->hasNewJointData()) {
|
||||
|
@ -232,21 +230,13 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
avatar->simulate(deltaTime, inView);
|
||||
avatar->updateRenderItem(pendingChanges);
|
||||
avatar->setLastRenderUpdateTime(startTime);
|
||||
} else if (now < maxExpiry) {
|
||||
// we've spent most of our time budget, but we still simulate() the avatar as it if were out of view
|
||||
// --> some avatars may freeze until their priority trickles up
|
||||
bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
|
||||
if (inView && avatar->hasNewJointData()) {
|
||||
numAVatarsNotUpdated++;
|
||||
}
|
||||
avatar->simulate(deltaTime, false);
|
||||
} else {
|
||||
// we've spent ALL of our time budget --> bail on the rest of the avatar updates
|
||||
// we've spent our full time budget --> bail on the rest of the avatar updates
|
||||
// --> more avatars may freeze until their priority trickles up
|
||||
// --> some scale or fade animations may glitch
|
||||
// --> some avatar velocity measurements may be a little off
|
||||
|
||||
// HACK: no time simulate, but we will take the time to count how many were tragically missed
|
||||
// no time simulate, but we take the time to count how many were tragically missed
|
||||
bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
|
||||
if (!inView) {
|
||||
break;
|
||||
|
|
|
@ -95,12 +95,6 @@ void CauterizedModel::createCollisionRenderItemSet() {
|
|||
Model::createCollisionRenderItemSet();
|
||||
}
|
||||
|
||||
// Called within Model::simulate call, below.
|
||||
void CauterizedModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||
Model::updateRig(deltaTime, parentTransform);
|
||||
_needsUpdateClusterMatrices = true;
|
||||
}
|
||||
|
||||
void CauterizedModel::updateClusterMatrices() {
|
||||
PerformanceTimer perfTimer("CauterizedModel::updateClusterMatrices");
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ public:
|
|||
void createVisibleRenderItemSet() override;
|
||||
void createCollisionRenderItemSet() override;
|
||||
|
||||
virtual void updateRig(float deltaTime, glm::mat4 parentTransform) override;
|
||||
virtual void updateClusterMatrices() override;
|
||||
void updateRenderItems() override;
|
||||
|
||||
|
|
|
@ -260,6 +260,11 @@ QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) {
|
|||
return AvatarData::toByteArrayStateful(dataDetail);
|
||||
}
|
||||
|
||||
void MyAvatar::resetSensorsAndBody() {
|
||||
qApp->getActiveDisplayPlugin()->resetSensors();
|
||||
reset(true, false, true);
|
||||
}
|
||||
|
||||
void MyAvatar::centerBody() {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "centerBody");
|
||||
|
@ -2483,6 +2488,45 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const {
|
|||
}
|
||||
}
|
||||
|
||||
bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& orientation) {
|
||||
auto hipsIndex = getJointIndex("Hips");
|
||||
if (index != hipsIndex) {
|
||||
qWarning() << "Pinning is only supported for the hips joint at the moment.";
|
||||
return false;
|
||||
}
|
||||
|
||||
setPosition(position);
|
||||
setOrientation(orientation);
|
||||
|
||||
_rig->setMaxHipsOffsetLength(0.05f);
|
||||
|
||||
auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index);
|
||||
if (it == _pinnedJoints.end()) {
|
||||
_pinnedJoints.push_back(index);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyAvatar::clearPinOnJoint(int index) {
|
||||
auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index);
|
||||
if (it != _pinnedJoints.end()) {
|
||||
_pinnedJoints.erase(it);
|
||||
|
||||
auto hipsIndex = getJointIndex("Hips");
|
||||
if (index == hipsIndex) {
|
||||
_rig->setMaxHipsOffsetLength(FLT_MAX);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
float MyAvatar::getIKErrorOnLastSolve() const {
|
||||
return _rig->getIKErrorOnLastSolve();
|
||||
}
|
||||
|
||||
// thread-safe
|
||||
void MyAvatar::addHoldAction(AvatarActionHold* holdAction) {
|
||||
std::lock_guard<std::mutex> guard(_holdActionsMutex);
|
||||
|
|
|
@ -99,6 +99,7 @@ public:
|
|||
|
||||
void reset(bool andRecenter = false, bool andReload = true, bool andHead = true);
|
||||
|
||||
Q_INVOKABLE void resetSensorsAndBody();
|
||||
Q_INVOKABLE void centerBody(); // thread-safe
|
||||
Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe
|
||||
|
||||
|
@ -216,6 +217,11 @@ public:
|
|||
virtual void clearJointData(int index) override;
|
||||
virtual void clearJointsData() override;
|
||||
|
||||
Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation);
|
||||
Q_INVOKABLE bool clearPinOnJoint(int index);
|
||||
|
||||
Q_INVOKABLE float getIKErrorOnLastSolve() const;
|
||||
|
||||
Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString());
|
||||
Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; }
|
||||
Q_INVOKABLE QString getFullAvatarModelName() const { return _fullAvatarModelName; }
|
||||
|
@ -527,6 +533,8 @@ private:
|
|||
bool didTeleport();
|
||||
bool getIsAway() const { return _isAway; }
|
||||
void setAway(bool value);
|
||||
|
||||
std::vector<int> _pinnedJoints;
|
||||
};
|
||||
|
||||
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);
|
||||
|
|
|
@ -166,7 +166,7 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
_rig->computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState);
|
||||
|
||||
// evaluate AnimGraph animation and update jointStates.
|
||||
CauterizedModel::updateRig(deltaTime, parentTransform);
|
||||
Model::updateRig(deltaTime, parentTransform);
|
||||
|
||||
Rig::EyeParameters eyeParams;
|
||||
eyeParams.worldHeadOrientation = headParams.worldHeadOrientation;
|
||||
|
@ -179,7 +179,9 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
|
||||
_rig->updateFromEyeParameters(eyeParams);
|
||||
} else {
|
||||
CauterizedModel::updateRig(deltaTime, parentTransform);
|
||||
// no need to call Model::updateRig() because otherAvatars get their joint state
|
||||
// copied directly from AvtarData::_jointData (there are no Rig animations to blend)
|
||||
_needsUpdateClusterMatrices = true;
|
||||
|
||||
// This is a little more work than we really want.
|
||||
//
|
||||
|
|
|
@ -189,6 +189,7 @@ void AnimInverseKinematics::solveWithCyclicCoordinateDescent(const std::vector<I
|
|||
}
|
||||
}
|
||||
}
|
||||
_maxErrorOnLastSolve = maxError;
|
||||
|
||||
// finally set the relative rotation of each tip to agree with absolute target rotation
|
||||
for (auto& target: targets) {
|
||||
|
@ -268,13 +269,13 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
|
|||
|
||||
glm::quat deltaRotation;
|
||||
if (targetType == IKTarget::Type::RotationAndPosition ||
|
||||
targetType == IKTarget::Type::HipsRelativeRotationAndPosition) {
|
||||
targetType == IKTarget::Type::HipsRelativeRotationAndPosition) {
|
||||
// compute the swing that would get get tip closer
|
||||
glm::vec3 targetLine = target.getTranslation() - jointPosition;
|
||||
|
||||
const float MIN_AXIS_LENGTH = 1.0e-4f;
|
||||
RotationConstraint* constraint = getConstraint(pivotIndex);
|
||||
if (constraint && constraint->isLowerSpine()) {
|
||||
if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) {
|
||||
// for these types of targets we only allow twist at the lower-spine
|
||||
// (this prevents the hand targets from bending the spine too much and thereby driving the hips too far)
|
||||
glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans();
|
||||
|
@ -300,8 +301,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
|
|||
const float MIN_ADJUSTMENT_ANGLE = 1.0e-4f;
|
||||
if (angle > MIN_ADJUSTMENT_ANGLE) {
|
||||
// reduce angle by a fraction (for stability)
|
||||
const float FRACTION = 0.5f;
|
||||
angle *= FRACTION;
|
||||
const float STABILITY_FRACTION = 0.5f;
|
||||
angle *= STABILITY_FRACTION;
|
||||
deltaRotation = glm::angleAxis(angle, axis);
|
||||
|
||||
// The swing will re-orient the tip but there will tend to be be a non-zero delta between the tip's
|
||||
|
@ -323,7 +324,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
|
|||
glm::vec3 axis = glm::normalize(deltaRotation * leverArm);
|
||||
swingTwistDecomposition(missingRotation, axis, swingPart, twistPart);
|
||||
float dotSign = copysignf(1.0f, twistPart.w);
|
||||
deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, FRACTION)) * deltaRotation;
|
||||
const float LIMIT_LEAK_FRACTION = 0.1f;
|
||||
deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, LIMIT_LEAK_FRACTION)) * deltaRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -486,7 +488,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
|
|||
// measure new _hipsOffset for next frame
|
||||
// by looking for discrepancies between where a targeted endEffector is
|
||||
// and where it wants to be (after IK solutions are done)
|
||||
glm::vec3 newHipsOffset = Vectors::ZERO;
|
||||
|
||||
// use weighted average between HMD and other targets
|
||||
float HMD_WEIGHT = 10.0f;
|
||||
float OTHER_WEIGHT = 1.0f;
|
||||
float totalWeight = 0.0f;
|
||||
|
||||
glm::vec3 additionalHipsOffset = Vectors::ZERO;
|
||||
for (auto& target: targets) {
|
||||
int targetIndex = target.getIndex();
|
||||
if (targetIndex == _headIndex && _headIndex != -1) {
|
||||
|
@ -497,32 +505,61 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
|
|||
glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans();
|
||||
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
|
||||
const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f;
|
||||
newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under);
|
||||
additionalHipsOffset += (OTHER_WEIGHT * HEAD_OFFSET_SLAVE_FACTOR) * (under- actual);
|
||||
totalWeight += OTHER_WEIGHT;
|
||||
} else if (target.getType() == IKTarget::Type::HmdHead) {
|
||||
// we want to shift the hips to bring the head to its designated position
|
||||
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
|
||||
_hipsOffset += target.getTranslation() - actual;
|
||||
// and ignore all other targets
|
||||
newHipsOffset = _hipsOffset;
|
||||
break;
|
||||
glm::vec3 thisOffset = target.getTranslation() - actual;
|
||||
glm::vec3 futureHipsOffset = _hipsOffset + thisOffset;
|
||||
if (glm::length(glm::vec2(futureHipsOffset.x, futureHipsOffset.z)) < _maxHipsOffsetLength) {
|
||||
// it is imperative to shift the hips and bring the head to its designated position
|
||||
// so we slam newHipsOffset here and ignore all other targets
|
||||
additionalHipsOffset = futureHipsOffset - _hipsOffset;
|
||||
totalWeight = 0.0f;
|
||||
break;
|
||||
} else {
|
||||
additionalHipsOffset += HMD_WEIGHT * (target.getTranslation() - actual);
|
||||
totalWeight += HMD_WEIGHT;
|
||||
}
|
||||
}
|
||||
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
|
||||
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
|
||||
glm::vec3 targetPosition = target.getTranslation();
|
||||
newHipsOffset += targetPosition - actualPosition;
|
||||
additionalHipsOffset += OTHER_WEIGHT * (targetPosition - actualPosition);
|
||||
totalWeight += OTHER_WEIGHT;
|
||||
}
|
||||
}
|
||||
if (totalWeight > 1.0f) {
|
||||
additionalHipsOffset /= totalWeight;
|
||||
}
|
||||
|
||||
// Add downward pressure on the hips
|
||||
additionalHipsOffset *= 0.95f;
|
||||
additionalHipsOffset -= 1.0f;
|
||||
|
||||
// smooth transitions by relaxing _hipsOffset toward the new value
|
||||
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.15f;
|
||||
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f;
|
||||
float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f;
|
||||
_hipsOffset += (newHipsOffset - _hipsOffset) * tau;
|
||||
_hipsOffset += additionalHipsOffset * tau;
|
||||
|
||||
// clamp the hips offset
|
||||
float hipsOffsetLength = glm::length(_hipsOffset);
|
||||
if (hipsOffsetLength > _maxHipsOffsetLength) {
|
||||
_hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return _relativePoses;
|
||||
}
|
||||
|
||||
void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) {
|
||||
// manually adjust scale here
|
||||
const float METERS_TO_CENTIMETERS = 100.0f;
|
||||
_maxHipsOffsetLength = METERS_TO_CENTIMETERS * maxLength;
|
||||
}
|
||||
|
||||
void AnimInverseKinematics::clearIKJointLimitHistory() {
|
||||
for (auto& pair : _constraints) {
|
||||
pair.second->clearHistory();
|
||||
|
@ -740,7 +777,7 @@ void AnimInverseKinematics::initConstraints() {
|
|||
stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST);
|
||||
|
||||
std::vector<float> minDots;
|
||||
const float MAX_SPINE_SWING = PI / 14.0f;
|
||||
const float MAX_SPINE_SWING = PI / 10.0f;
|
||||
minDots.push_back(cosf(MAX_SPINE_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
if (0 == baseName.compare("Spine1", Qt::CaseSensitive)
|
||||
|
@ -776,11 +813,11 @@ void AnimInverseKinematics::initConstraints() {
|
|||
} else if (0 == baseName.compare("Head", Qt::CaseSensitive)) {
|
||||
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
|
||||
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
|
||||
const float MAX_HEAD_TWIST = PI / 9.0f;
|
||||
const float MAX_HEAD_TWIST = PI / 6.0f;
|
||||
stConstraint->setTwistLimits(-MAX_HEAD_TWIST, MAX_HEAD_TWIST);
|
||||
|
||||
std::vector<float> minDots;
|
||||
const float MAX_HEAD_SWING = PI / 10.0f;
|
||||
const float MAX_HEAD_SWING = PI / 6.0f;
|
||||
minDots.push_back(cosf(MAX_HEAD_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
|
||||
|
|
|
@ -39,6 +39,10 @@ public:
|
|||
|
||||
void clearIKJointLimitHistory();
|
||||
|
||||
void setMaxHipsOffsetLength(float maxLength);
|
||||
|
||||
float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; }
|
||||
|
||||
protected:
|
||||
void computeTargets(const AnimVariantMap& animVars, std::vector<IKTarget>& targets, const AnimPoseVec& underPoses);
|
||||
void solveWithCyclicCoordinateDescent(const std::vector<IKTarget>& targets);
|
||||
|
@ -83,6 +87,7 @@ protected:
|
|||
|
||||
// experimental data for moving hips during IK
|
||||
glm::vec3 _hipsOffset { Vectors::ZERO };
|
||||
float _maxHipsOffsetLength{ FLT_MAX };
|
||||
int _headIndex { -1 };
|
||||
int _hipsIndex { -1 };
|
||||
int _hipsParentIndex { -1 };
|
||||
|
@ -90,6 +95,8 @@ protected:
|
|||
// _maxTargetIndex is tracked to help optimize the recalculation of absolute poses
|
||||
// during the the cyclic coordinate descent algorithm
|
||||
int _maxTargetIndex { 0 };
|
||||
|
||||
float _maxErrorOnLastSolve { FLT_MAX };
|
||||
};
|
||||
|
||||
#endif // hifi_AnimInverseKinematics_h
|
||||
|
|
|
@ -319,6 +319,39 @@ void Rig::clearIKJointLimitHistory() {
|
|||
}
|
||||
}
|
||||
|
||||
void Rig::setMaxHipsOffsetLength(float maxLength) {
|
||||
_maxHipsOffsetLength = maxLength;
|
||||
|
||||
if (_animNode) {
|
||||
_animNode->traverse([&](AnimNode::Pointer node) {
|
||||
auto ikNode = std::dynamic_pointer_cast<AnimInverseKinematics>(node);
|
||||
if (ikNode) {
|
||||
ikNode->setMaxHipsOffsetLength(_maxHipsOffsetLength);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
float Rig::getMaxHipsOffsetLength() const {
|
||||
return _maxHipsOffsetLength;
|
||||
}
|
||||
|
||||
float Rig::getIKErrorOnLastSolve() const {
|
||||
float result = 0.0f;
|
||||
|
||||
if (_animNode) {
|
||||
_animNode->traverse([&](AnimNode::Pointer node) {
|
||||
auto ikNode = std::dynamic_pointer_cast<AnimInverseKinematics>(node);
|
||||
if (ikNode) {
|
||||
result = ikNode->getMaxErrorOnLastSolve();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int Rig::getJointParentIndex(int childIndex) const {
|
||||
if (_animSkeleton && isIndexValid(childIndex)) {
|
||||
return _animSkeleton->getParentIndex(childIndex);
|
||||
|
@ -1274,39 +1307,50 @@ void Rig::copyJointsIntoJointData(QVector<JointData>& jointDataVec) const {
|
|||
void Rig::copyJointsFromJointData(const QVector<JointData>& jointDataVec) {
|
||||
PerformanceTimer perfTimer("copyJoints");
|
||||
PROFILE_RANGE(simulation_animation_detail, "copyJoints");
|
||||
if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._relativePoses.size()) {
|
||||
// make a vector of rotations in absolute-geometry-frame
|
||||
const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses();
|
||||
std::vector<glm::quat> rotations;
|
||||
rotations.reserve(absoluteDefaultPoses.size());
|
||||
const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform));
|
||||
for (int i = 0; i < jointDataVec.size(); i++) {
|
||||
const JointData& data = jointDataVec.at(i);
|
||||
if (data.rotationSet) {
|
||||
// JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame
|
||||
rotations.push_back(rigToGeometryRot * data.rotation);
|
||||
} else {
|
||||
rotations.push_back(absoluteDefaultPoses[i].rot());
|
||||
}
|
||||
}
|
||||
if (!_animSkeleton) {
|
||||
return;
|
||||
}
|
||||
if (jointDataVec.size() != (int)_internalPoseSet._relativePoses.size()) {
|
||||
// animations haven't fully loaded yet.
|
||||
_internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
|
||||
}
|
||||
|
||||
// convert rotations from absolute to parent relative.
|
||||
_animSkeleton->convertAbsoluteRotationsToRelative(rotations);
|
||||
|
||||
// store new relative poses
|
||||
const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses();
|
||||
for (int i = 0; i < jointDataVec.size(); i++) {
|
||||
const JointData& data = jointDataVec.at(i);
|
||||
_internalPoseSet._relativePoses[i].scale() = Vectors::ONE;
|
||||
_internalPoseSet._relativePoses[i].rot() = rotations[i];
|
||||
if (data.translationSet) {
|
||||
// JointData translations are in scaled relative-frame so we scale back to regular relative-frame
|
||||
_internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation;
|
||||
} else {
|
||||
_internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans();
|
||||
}
|
||||
// make a vector of rotations in absolute-geometry-frame
|
||||
const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses();
|
||||
std::vector<glm::quat> rotations;
|
||||
rotations.reserve(absoluteDefaultPoses.size());
|
||||
const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform));
|
||||
for (int i = 0; i < jointDataVec.size(); i++) {
|
||||
const JointData& data = jointDataVec.at(i);
|
||||
if (data.rotationSet) {
|
||||
// JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame
|
||||
rotations.push_back(rigToGeometryRot * data.rotation);
|
||||
} else {
|
||||
rotations.push_back(absoluteDefaultPoses[i].rot());
|
||||
}
|
||||
}
|
||||
|
||||
// convert rotations from absolute to parent relative.
|
||||
_animSkeleton->convertAbsoluteRotationsToRelative(rotations);
|
||||
|
||||
// store new relative poses
|
||||
const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses();
|
||||
for (int i = 0; i < jointDataVec.size(); i++) {
|
||||
const JointData& data = jointDataVec.at(i);
|
||||
_internalPoseSet._relativePoses[i].scale() = Vectors::ONE;
|
||||
_internalPoseSet._relativePoses[i].rot() = rotations[i];
|
||||
if (data.translationSet) {
|
||||
// JointData translations are in scaled relative-frame so we scale back to regular relative-frame
|
||||
_internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation;
|
||||
} else {
|
||||
_internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans();
|
||||
}
|
||||
}
|
||||
|
||||
// build absolute poses and copy to externalPoseSet
|
||||
buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses);
|
||||
QWriteLocker writeLock(&_externalPoseSetLock);
|
||||
_externalPoseSet = _internalPoseSet;
|
||||
}
|
||||
|
||||
void Rig::computeAvatarBoundingCapsule(
|
||||
|
|
|
@ -104,6 +104,10 @@ public:
|
|||
void clearJointAnimationPriority(int index);
|
||||
|
||||
void clearIKJointLimitHistory();
|
||||
void setMaxHipsOffsetLength(float maxLength);
|
||||
float getMaxHipsOffsetLength() const;
|
||||
|
||||
float getIKErrorOnLastSolve() const;
|
||||
|
||||
int getJointParentIndex(int childIndex) const;
|
||||
|
||||
|
@ -318,6 +322,8 @@ protected:
|
|||
bool _enabledAnimations { true };
|
||||
|
||||
mutable uint32_t _jointNameWarningCount { 0 };
|
||||
float _maxHipsOffsetLength { 1.0f };
|
||||
float _maxErrorOnLastSolve { 0.0f };
|
||||
|
||||
private:
|
||||
QMap<int, StateHandler> _stateHandlers;
|
||||
|
|
|
@ -2324,61 +2324,57 @@ float AvatarData::_avatarSortCoefficientSize { 0.5f };
|
|||
float AvatarData::_avatarSortCoefficientCenter { 0.25 };
|
||||
float AvatarData::_avatarSortCoefficientAge { 1.0f };
|
||||
|
||||
std::priority_queue<AvatarPriority> AvatarData::sortAvatars(
|
||||
QList<AvatarSharedPointer> avatarList,
|
||||
const ViewFrustum& cameraView,
|
||||
std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
|
||||
std::function<float(AvatarSharedPointer)> getBoundingRadius,
|
||||
std::function<bool(AvatarSharedPointer)> shouldIgnore) {
|
||||
void AvatarData::sortAvatars(
|
||||
QList<AvatarSharedPointer> avatarList,
|
||||
const ViewFrustum& cameraView,
|
||||
std::priority_queue<AvatarPriority>& sortedAvatarsOut,
|
||||
std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
|
||||
std::function<float(AvatarSharedPointer)> getBoundingRadius,
|
||||
std::function<bool(AvatarSharedPointer)> shouldIgnore) {
|
||||
|
||||
uint64_t startTime = usecTimestampNow();
|
||||
PROFILE_RANGE(simulation, "sort");
|
||||
uint64_t now = usecTimestampNow();
|
||||
|
||||
glm::vec3 frustumCenter = cameraView.getPosition();
|
||||
const glm::vec3& forward = cameraView.getDirection();
|
||||
for (int32_t i = 0; i < avatarList.size(); ++i) {
|
||||
const auto& avatar = avatarList.at(i);
|
||||
|
||||
std::priority_queue<AvatarPriority> sortedAvatars;
|
||||
{
|
||||
PROFILE_RANGE(simulation, "sort");
|
||||
for (int32_t i = 0; i < avatarList.size(); ++i) {
|
||||
const auto& avatar = avatarList.at(i);
|
||||
|
||||
if (shouldIgnore(avatar)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// priority = weighted linear combination of:
|
||||
// (a) apparentSize
|
||||
// (b) proximity to center of view
|
||||
// (c) time since last update
|
||||
glm::vec3 avatarPosition = avatar->getPosition();
|
||||
glm::vec3 offset = avatarPosition - frustumCenter;
|
||||
float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
|
||||
|
||||
// FIXME - AvatarData has something equivolent to this
|
||||
float radius = getBoundingRadius(avatar);
|
||||
|
||||
const glm::vec3& forward = cameraView.getDirection();
|
||||
float apparentSize = 2.0f * radius / distance;
|
||||
float cosineAngle = glm::length(glm::dot(offset, forward) * forward) / distance;
|
||||
float age = (float)(startTime - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND);
|
||||
|
||||
// NOTE: we are adding values of different units to get a single measure of "priority".
|
||||
// Thus we multiply each component by a conversion "weight" that scales its units relative to the others.
|
||||
// These weights are pure magic tuning and should be hard coded in the relation below,
|
||||
// but are currently exposed for anyone who would like to explore fine tuning:
|
||||
float priority = _avatarSortCoefficientSize * apparentSize
|
||||
+ _avatarSortCoefficientCenter * cosineAngle
|
||||
+ _avatarSortCoefficientAge * age;
|
||||
|
||||
// decrement priority of avatars outside keyhole
|
||||
if (distance > cameraView.getCenterRadius()) {
|
||||
if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) {
|
||||
priority += OUT_OF_VIEW_PENALTY;
|
||||
}
|
||||
}
|
||||
sortedAvatars.push(AvatarPriority(avatar, priority));
|
||||
if (shouldIgnore(avatar)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// priority = weighted linear combination of:
|
||||
// (a) apparentSize
|
||||
// (b) proximity to center of view
|
||||
// (c) time since last update
|
||||
glm::vec3 avatarPosition = avatar->getPosition();
|
||||
glm::vec3 offset = avatarPosition - frustumCenter;
|
||||
float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
|
||||
|
||||
// FIXME - AvatarData has something equivolent to this
|
||||
float radius = getBoundingRadius(avatar);
|
||||
|
||||
float apparentSize = 2.0f * radius / distance;
|
||||
float cosineAngle = glm::dot(offset, forward) / distance;
|
||||
float age = (float)(now - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND);
|
||||
|
||||
// NOTE: we are adding values of different units to get a single measure of "priority".
|
||||
// Thus we multiply each component by a conversion "weight" that scales its units relative to the others.
|
||||
// These weights are pure magic tuning and should be hard coded in the relation below,
|
||||
// but are currently exposed for anyone who would like to explore fine tuning:
|
||||
float priority = _avatarSortCoefficientSize * apparentSize
|
||||
+ _avatarSortCoefficientCenter * cosineAngle
|
||||
+ _avatarSortCoefficientAge * age;
|
||||
|
||||
// decrement priority of avatars outside keyhole
|
||||
if (distance > cameraView.getCenterRadius()) {
|
||||
if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) {
|
||||
priority += OUT_OF_VIEW_PENALTY;
|
||||
}
|
||||
}
|
||||
sortedAvatarsOut.push(AvatarPriority(avatar, priority));
|
||||
}
|
||||
return sortedAvatars;
|
||||
}
|
||||
|
||||
QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) {
|
||||
|
|
|
@ -597,9 +597,10 @@ public:
|
|||
|
||||
static const float OUT_OF_VIEW_PENALTY;
|
||||
|
||||
static std::priority_queue<AvatarPriority> sortAvatars(
|
||||
static void sortAvatars(
|
||||
QList<AvatarSharedPointer> avatarList,
|
||||
const ViewFrustum& cameraView,
|
||||
std::priority_queue<AvatarPriority>& sortedAvatarsOut,
|
||||
std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
|
||||
std::function<float(AvatarSharedPointer)> getBoundingRadius,
|
||||
std::function<bool(AvatarSharedPointer)> shouldIgnore);
|
||||
|
|
|
@ -944,7 +944,10 @@ void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, cons
|
|||
void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload, const bool unloadFirst) {
|
||||
if (_tree && !_shuttingDown) {
|
||||
EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID);
|
||||
bool shouldLoad = entity && entity->shouldPreloadScript() && _entitiesScriptEngine;
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine;
|
||||
QString scriptUrl = entity->getScript();
|
||||
if ((unloadFirst && shouldLoad) || scriptUrl.isEmpty()) {
|
||||
_entitiesScriptEngine->unloadEntityScript(entityID);
|
||||
|
|
|
@ -266,6 +266,35 @@ void RenderablePolyVoxEntityItem::forEachVoxelValue(quint16 voxelXSize, quint16
|
|||
});
|
||||
}
|
||||
|
||||
QByteArray RenderablePolyVoxEntityItem::volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const {
|
||||
int totalSize = voxelXSize * voxelYSize * voxelZSize;
|
||||
QByteArray result = QByteArray(totalSize, '\0');
|
||||
int index = 0;
|
||||
int lowX = 0;
|
||||
int lowY = 0;
|
||||
int lowZ = 0;
|
||||
|
||||
withReadLock([&] {
|
||||
if (isEdged(_voxelSurfaceStyle)) {
|
||||
lowX++;
|
||||
lowY++;
|
||||
lowZ++;
|
||||
voxelXSize++;
|
||||
voxelYSize++;
|
||||
voxelZSize++;
|
||||
}
|
||||
|
||||
for (int z = lowZ; z < voxelZSize; z++) {
|
||||
for (int y = lowY; y < voxelYSize; y++) {
|
||||
for (int x = lowX; x < voxelXSize; x++) {
|
||||
result[index++] = _volData->getVoxelAt(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RenderablePolyVoxEntityItem::setAll(uint8_t toValue) {
|
||||
bool result = false;
|
||||
|
@ -365,12 +394,28 @@ bool RenderablePolyVoxEntityItem::setSphere(glm::vec3 centerWorldCoords, float r
|
|||
}
|
||||
|
||||
glm::mat4 vtwMatrix = voxelToWorldMatrix();
|
||||
glm::mat4 wtvMatrix = glm::inverse(vtwMatrix);
|
||||
|
||||
// This three-level for loop iterates over every voxel in the volume
|
||||
glm::vec3 dimensions = getDimensions();
|
||||
glm::vec3 voxelSize = dimensions / _voxelVolumeSize;
|
||||
float smallestDimensionSize = voxelSize.x;
|
||||
smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.y);
|
||||
smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.z);
|
||||
|
||||
glm::vec3 maxRadiusInVoxelCoords = glm::vec3(radiusWorldCoords / smallestDimensionSize);
|
||||
glm::vec3 centerInVoxelCoords = wtvMatrix * glm::vec4(centerWorldCoords, 1.0f);
|
||||
|
||||
glm::vec3 low = glm::floor(centerInVoxelCoords - maxRadiusInVoxelCoords);
|
||||
glm::vec3 high = glm::ceil(centerInVoxelCoords + maxRadiusInVoxelCoords);
|
||||
|
||||
glm::ivec3 lowI = glm::clamp(low, glm::vec3(0.0f), _voxelVolumeSize);
|
||||
glm::ivec3 highI = glm::clamp(high, glm::vec3(0.0f), _voxelVolumeSize);
|
||||
|
||||
// This three-level for loop iterates over every voxel in the volume that might be in the sphere
|
||||
withWriteLock([&] {
|
||||
for (int z = 0; z < _voxelVolumeSize.z; z++) {
|
||||
for (int y = 0; y < _voxelVolumeSize.y; y++) {
|
||||
for (int x = 0; x < _voxelVolumeSize.x; x++) {
|
||||
for (int z = lowI.z; z < highI.z; z++) {
|
||||
for (int y = lowI.y; y < highI.y; y++) {
|
||||
for (int x = lowI.x; x < highI.x; x++) {
|
||||
// Store our current position as a vector...
|
||||
glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates
|
||||
// convert to world coordinates
|
||||
|
@ -392,6 +437,59 @@ bool RenderablePolyVoxEntityItem::setSphere(glm::vec3 centerWorldCoords, float r
|
|||
return result;
|
||||
}
|
||||
|
||||
bool RenderablePolyVoxEntityItem::setCapsule(glm::vec3 startWorldCoords, glm::vec3 endWorldCoords,
|
||||
float radiusWorldCoords, uint8_t toValue) {
|
||||
bool result = false;
|
||||
if (_locked) {
|
||||
return result;
|
||||
}
|
||||
|
||||
glm::mat4 vtwMatrix = voxelToWorldMatrix();
|
||||
glm::mat4 wtvMatrix = glm::inverse(vtwMatrix);
|
||||
|
||||
glm::vec3 dimensions = getDimensions();
|
||||
glm::vec3 voxelSize = dimensions / _voxelVolumeSize;
|
||||
float smallestDimensionSize = voxelSize.x;
|
||||
smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.y);
|
||||
smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.z);
|
||||
|
||||
glm::vec3 maxRadiusInVoxelCoords = glm::vec3(radiusWorldCoords / smallestDimensionSize);
|
||||
|
||||
glm::vec3 startInVoxelCoords = wtvMatrix * glm::vec4(startWorldCoords, 1.0f);
|
||||
glm::vec3 endInVoxelCoords = wtvMatrix * glm::vec4(endWorldCoords, 1.0f);
|
||||
|
||||
glm::vec3 low = glm::min(glm::floor(startInVoxelCoords - maxRadiusInVoxelCoords),
|
||||
glm::floor(endInVoxelCoords - maxRadiusInVoxelCoords));
|
||||
glm::vec3 high = glm::max(glm::ceil(startInVoxelCoords + maxRadiusInVoxelCoords),
|
||||
glm::ceil(endInVoxelCoords + maxRadiusInVoxelCoords));
|
||||
|
||||
glm::ivec3 lowI = glm::clamp(low, glm::vec3(0.0f), _voxelVolumeSize);
|
||||
glm::ivec3 highI = glm::clamp(high, glm::vec3(0.0f), _voxelVolumeSize);
|
||||
|
||||
// This three-level for loop iterates over every voxel in the volume that might be in the capsule
|
||||
withWriteLock([&] {
|
||||
for (int z = lowI.z; z < highI.z; z++) {
|
||||
for (int y = lowI.y; y < highI.y; y++) {
|
||||
for (int x = lowI.x; x < highI.x; x++) {
|
||||
// Store our current position as a vector...
|
||||
glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates
|
||||
// convert to world coordinates
|
||||
glm::vec3 worldPos = glm::vec3(vtwMatrix * pos);
|
||||
if (pointInCapsule(worldPos, startWorldCoords, endWorldCoords, radiusWorldCoords)) {
|
||||
result |= setVoxelInternal(x, y, z, toValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result) {
|
||||
compressVolumeDataAndSendEditPacket();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
class RaycastFunctor
|
||||
{
|
||||
public:
|
||||
|
@ -501,6 +599,9 @@ PolyVox::RaycastResult RenderablePolyVoxEntityItem::doRayCast(glm::vec4 originIn
|
|||
|
||||
// virtual
|
||||
ShapeType RenderablePolyVoxEntityItem::getShapeType() const {
|
||||
if (_collisionless) {
|
||||
return SHAPE_TYPE_NONE;
|
||||
}
|
||||
return SHAPE_TYPE_COMPOUND;
|
||||
}
|
||||
|
||||
|
@ -512,6 +613,11 @@ void RenderablePolyVoxEntityItem::updateRegistrationPoint(const glm::vec3& value
|
|||
}
|
||||
|
||||
bool RenderablePolyVoxEntityItem::isReadyToComputeShape() {
|
||||
ShapeType shapeType = getShapeType();
|
||||
if (shapeType == SHAPE_TYPE_NONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// we determine if we are ready to compute the physics shape by actually doing so.
|
||||
// if _voxelDataDirty or _volDataDirty is set, don't do this yet -- wait for their
|
||||
// threads to finish before creating the collision shape.
|
||||
|
@ -524,6 +630,12 @@ bool RenderablePolyVoxEntityItem::isReadyToComputeShape() {
|
|||
}
|
||||
|
||||
void RenderablePolyVoxEntityItem::computeShapeInfo(ShapeInfo& info) {
|
||||
ShapeType shapeType = getShapeType();
|
||||
if (shapeType == SHAPE_TYPE_NONE) {
|
||||
info.setParams(getShapeType(), 0.5f * getDimensions());
|
||||
return;
|
||||
}
|
||||
|
||||
// the shape was actually computed in isReadyToComputeShape. Just hand it off, here.
|
||||
withWriteLock([&] {
|
||||
info = _shapeInfo;
|
||||
|
@ -736,7 +848,7 @@ glm::vec3 RenderablePolyVoxEntityItem::localCoordsToVoxelCoords(glm::vec3& local
|
|||
|
||||
void RenderablePolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize) {
|
||||
// This controls how many individual voxels are in the entity. This is unrelated to
|
||||
// the dimentions of the entity -- it defines the size of the arrays that hold voxel values.
|
||||
// the dimentions of the entity -- it defines the sizes of the arrays that hold voxel values.
|
||||
// In addition to setting the number of voxels, this is used in a few places for its
|
||||
// side-effect of allocating _volData to be the correct size.
|
||||
withWriteLock([&] {
|
||||
|
@ -807,7 +919,7 @@ uint8_t RenderablePolyVoxEntityItem::getVoxel(int x, int y, int z) {
|
|||
}
|
||||
|
||||
|
||||
uint8_t RenderablePolyVoxEntityItem::getVoxelInternal(int x, int y, int z) {
|
||||
uint8_t RenderablePolyVoxEntityItem::getVoxelInternal(int x, int y, int z) const {
|
||||
if (!inUserBounds(_volData, _voxelSurfaceStyle, x, y, z)) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -949,17 +1061,8 @@ void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() {
|
|||
EntityTreePointer tree = element ? element->getTree() : nullptr;
|
||||
|
||||
QtConcurrent::run([voxelXSize, voxelYSize, voxelZSize, entity, tree] {
|
||||
int rawSize = voxelXSize * voxelYSize * voxelZSize;
|
||||
QByteArray uncompressedData = QByteArray(rawSize, '\0');
|
||||
|
||||
auto polyVoxEntity = std::static_pointer_cast<RenderablePolyVoxEntityItem>(entity);
|
||||
polyVoxEntity->forEachVoxelValue(voxelXSize, voxelYSize, voxelZSize, [&] (int x, int y, int z, uint8_t uVoxelValue) {
|
||||
int uncompressedIndex =
|
||||
z * voxelYSize * voxelXSize +
|
||||
y * voxelXSize +
|
||||
x;
|
||||
uncompressedData[uncompressedIndex] = uVoxelValue;
|
||||
});
|
||||
QByteArray uncompressedData = polyVoxEntity->volDataToArray(voxelXSize, voxelYSize, voxelZSize);
|
||||
|
||||
QByteArray newVoxelData;
|
||||
QDataStream writer(&newVoxelData, QIODevice::WriteOnly | QIODevice::Truncate);
|
||||
|
@ -1174,7 +1277,9 @@ void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) {
|
|||
// this catches the payload from getMesh
|
||||
bool neighborsNeedUpdate;
|
||||
withWriteLock([&] {
|
||||
_dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS;
|
||||
if (!_collisionless) {
|
||||
_dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS;
|
||||
}
|
||||
_mesh = mesh;
|
||||
_meshDirty = true;
|
||||
_meshInitialized = true;
|
||||
|
|
|
@ -94,6 +94,8 @@ public:
|
|||
|
||||
// coords are in world-space
|
||||
virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue) override;
|
||||
virtual bool setCapsule(glm::vec3 startWorldCoords, glm::vec3 endWorldCoords,
|
||||
float radiusWorldCoords, uint8_t toValue) override;
|
||||
virtual bool setAll(uint8_t toValue) override;
|
||||
virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue) override;
|
||||
|
||||
|
@ -128,12 +130,13 @@ public:
|
|||
void setVoxelsFromData(QByteArray uncompressedData, quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize);
|
||||
void forEachVoxelValue(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize,
|
||||
std::function<void(int, int, int, uint8_t)> thunk);
|
||||
QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const;
|
||||
|
||||
void setMesh(model::MeshPointer mesh);
|
||||
void setCollisionPoints(ShapeInfo::PointCollection points, AABox box);
|
||||
PolyVox::SimpleVolume<uint8_t>* getVolData() { return _volData; }
|
||||
|
||||
uint8_t getVoxelInternal(int x, int y, int z);
|
||||
uint8_t getVoxelInternal(int x, int y, int z) const;
|
||||
bool setVoxelInternal(int x, int y, int z, uint8_t toValue);
|
||||
|
||||
void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; }); }
|
||||
|
|
|
@ -285,7 +285,7 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit
|
|||
desiredProperties = entity->getEntityProperties(params);
|
||||
desiredProperties.setHasProperty(PROP_LOCAL_POSITION);
|
||||
desiredProperties.setHasProperty(PROP_LOCAL_ROTATION);
|
||||
}
|
||||
}
|
||||
|
||||
results = entity->getProperties(desiredProperties);
|
||||
|
||||
|
@ -825,7 +825,7 @@ bool EntityScriptingInterface::setVoxels(QUuid entityID,
|
|||
|
||||
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
|
||||
if (!entity) {
|
||||
qCDebug(entities) << "EntityScriptingInterface::setVoxelSphere no entity with ID" << entityID;
|
||||
qCDebug(entities) << "EntityScriptingInterface::setVoxels no entity with ID" << entityID;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -887,24 +887,34 @@ bool EntityScriptingInterface::setVoxelSphere(QUuid entityID, const glm::vec3& c
|
|||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setSphere(center, radius, value);
|
||||
});
|
||||
return polyVoxEntity.setSphere(center, radius, value);
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID,
|
||||
const glm::vec3& start, const glm::vec3& end,
|
||||
float radius, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [start, end, radius, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setCapsule(start, end, radius, value);
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& position, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setVoxelInVolume(position, value);
|
||||
});
|
||||
return polyVoxEntity.setVoxelInVolume(position, value);
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::setAllVoxels(QUuid entityID, int value) {
|
||||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setAll(value);
|
||||
});
|
||||
return polyVoxEntity.setAll(value);
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition,
|
||||
|
@ -912,8 +922,8 @@ bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3
|
|||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return setVoxels(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) {
|
||||
return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value);
|
||||
});
|
||||
return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value);
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::setAllPoints(QUuid entityID, const QVector<glm::vec3>& points) {
|
||||
|
@ -1020,25 +1030,25 @@ QUuid EntityScriptingInterface::addAction(const QString& actionTypeString,
|
|||
auto actionFactory = DependencyManager::get<EntityActionFactoryInterface>();
|
||||
bool success = false;
|
||||
actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) {
|
||||
// create this action even if the entity doesn't have physics info. it will often be the
|
||||
// case that a script adds an action immediately after an object is created, and the physicsInfo
|
||||
// is computed asynchronously.
|
||||
// if (!entity->getPhysicsInfo()) {
|
||||
// return false;
|
||||
// }
|
||||
EntityActionType actionType = EntityActionInterface::actionTypeFromString(actionTypeString);
|
||||
if (actionType == ACTION_TYPE_NONE) {
|
||||
return false;
|
||||
}
|
||||
EntityActionPointer action = actionFactory->factory(actionType, actionID, entity, arguments);
|
||||
if (!action) {
|
||||
return false;
|
||||
}
|
||||
action->setIsMine(true);
|
||||
success = entity->addAction(simulation, action);
|
||||
entity->grabSimulationOwnership();
|
||||
return false; // Physics will cause a packet to be sent, so don't send from here.
|
||||
});
|
||||
// create this action even if the entity doesn't have physics info. it will often be the
|
||||
// case that a script adds an action immediately after an object is created, and the physicsInfo
|
||||
// is computed asynchronously.
|
||||
// if (!entity->getPhysicsInfo()) {
|
||||
// return false;
|
||||
// }
|
||||
EntityActionType actionType = EntityActionInterface::actionTypeFromString(actionTypeString);
|
||||
if (actionType == ACTION_TYPE_NONE) {
|
||||
return false;
|
||||
}
|
||||
EntityActionPointer action = actionFactory->factory(actionType, actionID, entity, arguments);
|
||||
if (!action) {
|
||||
return false;
|
||||
}
|
||||
action->setIsMine(true);
|
||||
success = entity->addAction(simulation, action);
|
||||
entity->grabSimulationOwnership();
|
||||
return false; // Physics will cause a packet to be sent, so don't send from here.
|
||||
});
|
||||
if (success) {
|
||||
return actionID;
|
||||
}
|
||||
|
@ -1050,12 +1060,12 @@ bool EntityScriptingInterface::updateAction(const QUuid& entityID, const QUuid&
|
|||
PROFILE_RANGE(script_entities, __FUNCTION__);
|
||||
|
||||
return actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) {
|
||||
bool success = entity->updateAction(simulation, actionID, arguments);
|
||||
if (success) {
|
||||
entity->grabSimulationOwnership();
|
||||
}
|
||||
return success;
|
||||
});
|
||||
bool success = entity->updateAction(simulation, actionID, arguments);
|
||||
if (success) {
|
||||
entity->grabSimulationOwnership();
|
||||
}
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::deleteAction(const QUuid& entityID, const QUuid& actionID) {
|
||||
|
@ -1063,13 +1073,13 @@ bool EntityScriptingInterface::deleteAction(const QUuid& entityID, const QUuid&
|
|||
|
||||
bool success = false;
|
||||
actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) {
|
||||
success = entity->removeAction(simulation, actionID);
|
||||
if (success) {
|
||||
// reduce from grab to poke
|
||||
entity->pokeSimulationOwnership();
|
||||
}
|
||||
return false; // Physics will cause a packet to be sent, so don't send from here.
|
||||
});
|
||||
success = entity->removeAction(simulation, actionID);
|
||||
if (success) {
|
||||
// reduce from grab to poke
|
||||
entity->pokeSimulationOwnership();
|
||||
}
|
||||
return false; // Physics will cause a packet to be sent, so don't send from here.
|
||||
});
|
||||
return success;
|
||||
}
|
||||
|
||||
|
@ -1078,10 +1088,10 @@ QVector<QUuid> EntityScriptingInterface::getActionIDs(const QUuid& entityID) {
|
|||
|
||||
QVector<QUuid> result;
|
||||
actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) {
|
||||
QList<QUuid> actionIDs = entity->getActionIDs();
|
||||
result = QVector<QUuid>::fromList(actionIDs);
|
||||
return false; // don't send an edit packet
|
||||
});
|
||||
QList<QUuid> actionIDs = entity->getActionIDs();
|
||||
result = QVector<QUuid>::fromList(actionIDs);
|
||||
return false; // don't send an edit packet
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -1090,9 +1100,9 @@ QVariantMap EntityScriptingInterface::getActionArguments(const QUuid& entityID,
|
|||
|
||||
QVariantMap result;
|
||||
actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) {
|
||||
result = entity->getActionArguments(actionID);
|
||||
return false; // don't send an edit packet
|
||||
});
|
||||
result = entity->getActionArguments(actionID);
|
||||
return false; // don't send an edit packet
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -1523,3 +1533,11 @@ QObject* EntityScriptingInterface::getWebViewRoot(const QUuid& entityID) {
|
|||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move this someplace that makes more sense...
|
||||
bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions,
|
||||
const glm::vec3& start, const glm::vec3& end, float radius) {
|
||||
glm::vec3 penetration;
|
||||
AABox aaBox(low, dimensions);
|
||||
return aaBox.findCapsulePenetration(start, end, radius, penetration);
|
||||
}
|
||||
|
|
|
@ -223,6 +223,8 @@ public slots:
|
|||
Q_INVOKABLE bool getDrawZoneBoundaries() const;
|
||||
|
||||
Q_INVOKABLE bool setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value);
|
||||
Q_INVOKABLE bool setVoxelCapsule(QUuid entityID, const glm::vec3& start, const glm::vec3& end, float radius, int value);
|
||||
|
||||
Q_INVOKABLE bool setVoxel(QUuid entityID, const glm::vec3& position, int value);
|
||||
Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value);
|
||||
Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition,
|
||||
|
@ -287,6 +289,10 @@ public slots:
|
|||
|
||||
Q_INVOKABLE QObject* getWebViewRoot(const QUuid& entityID);
|
||||
|
||||
Q_INVOKABLE bool AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions,
|
||||
const glm::vec3& start, const glm::vec3& end, float radius);
|
||||
|
||||
|
||||
signals:
|
||||
void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision);
|
||||
|
||||
|
|
|
@ -86,6 +86,8 @@ class PolyVoxEntityItem : public EntityItem {
|
|||
|
||||
// coords are in world-space
|
||||
virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue) { return false; }
|
||||
virtual bool setCapsule(glm::vec3 startWorldCoords, glm::vec3 endWorldCoords,
|
||||
float radiusWorldCoords, uint8_t toValue) { return false; }
|
||||
virtual bool setAll(uint8_t toValue) { return false; }
|
||||
virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value) { return false; }
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
|
|||
setCustomDeleter([](Dependency* dependency){
|
||||
static_cast<NodeList*>(dependency)->deleteLater();
|
||||
});
|
||||
|
||||
|
||||
auto addressManager = DependencyManager::get<AddressManager>();
|
||||
|
||||
// handle domain change signals from AddressManager
|
||||
|
@ -85,8 +85,8 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
|
|||
connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer);
|
||||
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
|
||||
// assume that we may need to send a new DS check in anytime a new keypair is generated
|
||||
|
||||
// assume that we may need to send a new DS check in anytime a new keypair is generated
|
||||
connect(accountManager.data(), &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn);
|
||||
|
||||
// clear out NodeList when login is finished
|
||||
|
@ -101,7 +101,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
|
|||
|
||||
// anytime we get a new node we may need to re-send our set of ignored node IDs to it
|
||||
connect(this, &LimitedNodeList::nodeActivated, this, &NodeList::maybeSendIgnoreSetToNode);
|
||||
|
||||
|
||||
// setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect)
|
||||
_keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable
|
||||
connect(&_keepAlivePingTimer, &QTimer::timeout, this, &NodeList::sendKeepAlivePings);
|
||||
|
@ -161,11 +161,11 @@ qint64 NodeList::sendStatsToDomainServer(QJsonObject statsObject) {
|
|||
|
||||
void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode) {
|
||||
PingType_t pingType;
|
||||
|
||||
|
||||
quint64 ourOriginalTime, othersReplyTime;
|
||||
|
||||
|
||||
message.seek(0);
|
||||
|
||||
|
||||
message.readPrimitive(&pingType);
|
||||
message.readPrimitive(&ourOriginalTime);
|
||||
message.readPrimitive(&othersReplyTime);
|
||||
|
@ -199,7 +199,7 @@ void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer&
|
|||
}
|
||||
|
||||
void NodeList::processPingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
|
||||
|
||||
// send back a reply
|
||||
auto replyPacket = constructPingReplyPacket(*message);
|
||||
const HifiSockAddr& senderSockAddr = message->getSenderSockAddr();
|
||||
|
@ -252,6 +252,11 @@ void NodeList::reset() {
|
|||
_personalMutedNodeIDs.clear();
|
||||
_personalMutedSetLock.unlock();
|
||||
|
||||
// lock and clear out set of avatarGains
|
||||
_avatarGainMapLock.lockForWrite();
|
||||
_avatarGainMap.clear();
|
||||
_avatarGainMapLock.unlock();
|
||||
|
||||
// refresh the owner UUID to the NULL UUID
|
||||
setSessionUUID(QUuid());
|
||||
|
||||
|
@ -329,7 +334,7 @@ void NodeList::sendDomainServerCheckIn() {
|
|||
}
|
||||
|
||||
auto domainPacket = NLPacket::create(domainPacketType);
|
||||
|
||||
|
||||
QDataStream packetStream(domainPacket.get());
|
||||
|
||||
if (domainPacketType == PacketType::DomainConnectRequest) {
|
||||
|
@ -488,7 +493,7 @@ void NodeList::processDomainServerPathResponse(QSharedPointer<ReceivedMessage> m
|
|||
qCDebug(networking) << "Could not read query path from DomainServerPathQueryResponse. Bailing.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
QString pathQuery = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numPathBytes);
|
||||
message->seek(message->getPosition() + numPathBytes);
|
||||
|
||||
|
@ -500,10 +505,10 @@ void NodeList::processDomainServerPathResponse(QSharedPointer<ReceivedMessage> m
|
|||
qCDebug(networking) << "Could not read resulting viewpoint from DomainServerPathQueryReponse. Bailing";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// pull the viewpoint from the packet
|
||||
QString viewpoint = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numViewpointBytes);
|
||||
|
||||
|
||||
// Hand it off to the AddressManager so it can handle it as a relative viewpoint
|
||||
if (DependencyManager::get<AddressManager>()->goToViewpointForPath(viewpoint, pathQuery)) {
|
||||
qCDebug(networking) << "Going to viewpoint" << viewpoint << "which was the lookup result for path" << pathQuery;
|
||||
|
@ -664,16 +669,16 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
|
|||
}
|
||||
|
||||
void NodeList::sendAssignment(Assignment& assignment) {
|
||||
|
||||
|
||||
PacketType assignmentPacketType = assignment.getCommand() == Assignment::CreateCommand
|
||||
? PacketType::CreateAssignment
|
||||
: PacketType::RequestAssignment;
|
||||
|
||||
auto assignmentPacket = NLPacket::create(assignmentPacketType);
|
||||
|
||||
|
||||
QDataStream packetStream(assignmentPacket.get());
|
||||
packetStream << assignment;
|
||||
|
||||
|
||||
sendPacket(std::move(assignmentPacket), _assignmentServerSocket);
|
||||
}
|
||||
|
||||
|
@ -833,7 +838,7 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) {
|
|||
_ignoredNodeIDs.insert(nodeID);
|
||||
}
|
||||
{
|
||||
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
|
||||
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
|
||||
// add this nodeID to our set of personal muted IDs
|
||||
_personalMutedNodeIDs.insert(nodeID);
|
||||
}
|
||||
|
@ -896,7 +901,7 @@ void NodeList::personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled
|
|||
|
||||
|
||||
if (muteEnabled) {
|
||||
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
|
||||
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
|
||||
// add this nodeID to our set of personal muted IDs
|
||||
_personalMutedNodeIDs.insert(nodeID);
|
||||
} else {
|
||||
|
@ -981,7 +986,7 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) {
|
|||
if (audioMixer) {
|
||||
// setup the packet
|
||||
auto setAvatarGainPacket = NLPacket::create(PacketType::PerAvatarGainSet, NUM_BYTES_RFC4122_UUID + sizeof(float), true);
|
||||
|
||||
|
||||
// write the node ID to the packet
|
||||
setAvatarGainPacket->write(nodeID.toRfc4122());
|
||||
// We need to convert the gain in dB (from the script) to an amplitude before packing it.
|
||||
|
@ -990,6 +995,9 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) {
|
|||
qCDebug(networking) << "Sending Set Avatar Gain packet UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain;
|
||||
|
||||
sendPacket(std::move(setAvatarGainPacket), *audioMixer);
|
||||
QWriteLocker{ &_avatarGainMapLock };
|
||||
_avatarGainMap[nodeID] = gain;
|
||||
|
||||
} else {
|
||||
qWarning() << "Couldn't find audio mixer to send set gain request";
|
||||
}
|
||||
|
@ -998,6 +1006,15 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) {
|
|||
}
|
||||
}
|
||||
|
||||
float NodeList::getAvatarGain(const QUuid& nodeID) {
|
||||
QReadLocker{ &_avatarGainMapLock };
|
||||
auto it = _avatarGainMap.find(nodeID);
|
||||
if (it != _avatarGainMap.cend()) {
|
||||
return it->second;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
void NodeList::kickNodeBySessionID(const QUuid& nodeID) {
|
||||
// send a request to domain-server to kick the node with the given session ID
|
||||
// the domain-server will handle the persistence of the kick (via username or IP)
|
||||
|
@ -1036,7 +1053,7 @@ void NodeList::muteNodeBySessionID(const QUuid& nodeID) {
|
|||
mutePacket->write(nodeID.toRfc4122());
|
||||
|
||||
qCDebug(networking) << "Sending packet to mute node" << uuidStringWithoutCurlyBraces(nodeID);
|
||||
|
||||
|
||||
sendPacket(std::move(mutePacket), *audioMixer);
|
||||
} else {
|
||||
qWarning() << "Couldn't find audio mixer to send node mute request";
|
||||
|
|
|
@ -68,7 +68,7 @@ public:
|
|||
|
||||
void setAssignmentServerSocket(const HifiSockAddr& serverSocket) { _assignmentServerSocket = serverSocket; }
|
||||
void sendAssignment(Assignment& assignment);
|
||||
|
||||
|
||||
void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
|
||||
|
||||
void ignoreNodesInRadius(bool enabled = true);
|
||||
|
@ -83,6 +83,7 @@ public:
|
|||
void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled);
|
||||
bool isPersonalMutingNode(const QUuid& nodeID) const;
|
||||
void setAvatarGain(const QUuid& nodeID, float gain);
|
||||
float getAvatarGain(const QUuid& nodeID);
|
||||
|
||||
void kickNodeBySessionID(const QUuid& nodeID);
|
||||
void muteNodeBySessionID(const QUuid& nodeID);
|
||||
|
@ -103,7 +104,7 @@ public slots:
|
|||
void processDomainServerPathResponse(QSharedPointer<ReceivedMessage> message);
|
||||
|
||||
void processDomainServerConnectionTokenPacket(QSharedPointer<ReceivedMessage> message);
|
||||
|
||||
|
||||
void processPingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
||||
void processPingReplyPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
||||
|
||||
|
@ -131,11 +132,11 @@ private slots:
|
|||
void handleNodePingTimeout();
|
||||
|
||||
void pingPunchForDomainServer();
|
||||
|
||||
|
||||
void sendKeepAlivePings();
|
||||
|
||||
void maybeSendIgnoreSetToNode(SharedNodePointer node);
|
||||
|
||||
|
||||
private:
|
||||
NodeList() : LimitedNodeList(INVALID_PORT, INVALID_PORT) { assert(false); } // Not implemented, needed for DependencyManager templates compile
|
||||
NodeList(char ownerType, int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT);
|
||||
|
@ -148,7 +149,7 @@ private:
|
|||
void timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode);
|
||||
|
||||
void sendDSPathQuery(const QString& newPath);
|
||||
|
||||
|
||||
void parseNodeFromPacketStream(QDataStream& packetStream);
|
||||
|
||||
void pingPunchForInactiveNode(const SharedNodePointer& node);
|
||||
|
@ -170,6 +171,8 @@ private:
|
|||
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDs;
|
||||
mutable QReadWriteLock _personalMutedSetLock;
|
||||
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _personalMutedNodeIDs;
|
||||
mutable QReadWriteLock _avatarGainMapLock;
|
||||
tbb::concurrent_unordered_map<QUuid, float, UUIDHasher> _avatarGainMap;
|
||||
|
||||
void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode);
|
||||
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true };
|
||||
|
|
|
@ -47,6 +47,10 @@ void UsersScriptingInterface::setAvatarGain(const QUuid& nodeID, float gain) {
|
|||
DependencyManager::get<NodeList>()->setAvatarGain(nodeID, gain);
|
||||
}
|
||||
|
||||
float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) {
|
||||
return DependencyManager::get<NodeList>()->getAvatarGain(nodeID);
|
||||
}
|
||||
|
||||
void UsersScriptingInterface::kick(const QUuid& nodeID) {
|
||||
// ask the NodeList to kick the user with the given session ID
|
||||
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID);
|
||||
|
@ -88,4 +92,4 @@ bool UsersScriptingInterface::getRequestsDomainListData() {
|
|||
}
|
||||
void UsersScriptingInterface::setRequestsDomainListData(bool isRequesting) {
|
||||
DependencyManager::get<NodeList>()->setRequestsDomainListData(isRequesting);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,14 @@ public slots:
|
|||
*/
|
||||
void setAvatarGain(const QUuid& nodeID, float gain);
|
||||
|
||||
/**jsdoc
|
||||
* Gets an avatar's gain for you and you only.
|
||||
* @function Users.getAvatarGain
|
||||
* @param {nodeID} nodeID The node or session ID of the user whose gain you want to get.
|
||||
* @return {float} gain (in dB)
|
||||
*/
|
||||
float getAvatarGain(const QUuid& nodeID);
|
||||
|
||||
/**jsdoc
|
||||
* Kick another user.
|
||||
* @function Users.kick
|
||||
|
|
|
@ -205,6 +205,33 @@ bool findRaySphereIntersection(const glm::vec3& origin, const glm::vec3& directi
|
|||
return true;
|
||||
}
|
||||
|
||||
bool pointInSphere(const glm::vec3& origin, const glm::vec3& center, float radius) {
|
||||
glm::vec3 relativeOrigin = origin - center;
|
||||
float c = glm::dot(relativeOrigin, relativeOrigin) - radius * radius;
|
||||
return c <= 0.0f;
|
||||
}
|
||||
|
||||
|
||||
bool pointInCapsule(const glm::vec3& origin, const glm::vec3& start, const glm::vec3& end, float radius) {
|
||||
glm::vec3 relativeOrigin = origin - start;
|
||||
glm::vec3 relativeEnd = end - start;
|
||||
float capsuleLength = glm::length(relativeEnd);
|
||||
relativeEnd /= capsuleLength;
|
||||
float originProjection = glm::dot(relativeEnd, relativeOrigin);
|
||||
glm::vec3 constant = relativeOrigin - relativeEnd * originProjection;
|
||||
float c = glm::dot(constant, constant) - radius * radius;
|
||||
if (c < 0.0f) { // starts inside cylinder
|
||||
if (originProjection < 0.0f) { // below start
|
||||
return pointInSphere(origin, start, radius);
|
||||
} else if (originProjection > capsuleLength) { // above end
|
||||
return pointInSphere(origin, end, radius);
|
||||
} else { // between start and end
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool findRayCapsuleIntersection(const glm::vec3& origin, const glm::vec3& direction,
|
||||
const glm::vec3& start, const glm::vec3& end, float radius, float& distance) {
|
||||
if (start == end) {
|
||||
|
|
|
@ -73,6 +73,9 @@ glm::vec3 addPenetrations(const glm::vec3& currentPenetration, const glm::vec3&
|
|||
bool findRaySphereIntersection(const glm::vec3& origin, const glm::vec3& direction,
|
||||
const glm::vec3& center, float radius, float& distance);
|
||||
|
||||
bool pointInSphere(const glm::vec3& origin, const glm::vec3& center, float radius);
|
||||
bool pointInCapsule(const glm::vec3& origin, const glm::vec3& start, const glm::vec3& end, float radius);
|
||||
|
||||
bool findRayCapsuleIntersection(const glm::vec3& origin, const glm::vec3& direction,
|
||||
const glm::vec3& start, const glm::vec3& end, float radius, float& distance);
|
||||
|
||||
|
|
|
@ -545,7 +545,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) {
|
|||
// HACK: when interface is launched and steam vr is NOT running, openvr will return bad HMD poses for a few frames
|
||||
// To workaround this, filter out any hmd poses that are obviously bad, i.e. beneath the floor.
|
||||
if (isBadPose(&nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking)) {
|
||||
qDebug() << "WARNING: ignoring bad hmd pose from openvr";
|
||||
// qDebug() << "WARNING: ignoring bad hmd pose from openvr";
|
||||
|
||||
// use the last known good HMD pose
|
||||
nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking = _lastGoodHMDPose;
|
||||
|
|
BIN
scripts/system/assets/models/teleport-seat.fbx
Normal file
BIN
scripts/system/assets/models/teleport-seat.fbx
Normal file
Binary file not shown.
|
@ -17,12 +17,20 @@ var NUMBER_OF_STEPS = 6;
|
|||
|
||||
var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx");
|
||||
var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx");
|
||||
var SEAT_MODEL_URL = Script.resolvePath("../assets/models/teleport-seat.fbx");
|
||||
|
||||
var TARGET_MODEL_DIMENSIONS = {
|
||||
x: 1.15,
|
||||
y: 0.5,
|
||||
z: 1.15
|
||||
};
|
||||
|
||||
var COLORS_TELEPORT_SEAT = {
|
||||
red: 255,
|
||||
green: 0,
|
||||
blue: 170
|
||||
}
|
||||
|
||||
var COLORS_TELEPORT_CAN_TELEPORT = {
|
||||
red: 97,
|
||||
green: 247,
|
||||
|
@ -35,29 +43,30 @@ var COLORS_TELEPORT_CANNOT_TELEPORT = {
|
|||
blue: 141
|
||||
};
|
||||
|
||||
var COLORS_TELEPORT_TOO_CLOSE = {
|
||||
var COLORS_TELEPORT_CANCEL = {
|
||||
red: 255,
|
||||
green: 184,
|
||||
blue: 73
|
||||
};
|
||||
|
||||
var TELEPORT_CANCEL_RANGE = 1;
|
||||
var USE_COOL_IN = true;
|
||||
var COOL_IN_DURATION = 500;
|
||||
|
||||
const handInfo = {
|
||||
right: {
|
||||
controllerInput: Controller.Standard.RightHand
|
||||
},
|
||||
left: {
|
||||
controllerInput: Controller.Standard.LeftHand
|
||||
}
|
||||
};
|
||||
|
||||
function ThumbPad(hand) {
|
||||
this.hand = hand;
|
||||
var _thisPad = this;
|
||||
|
||||
this.buttonPress = function(value) {
|
||||
_thisPad.buttonValue = value;
|
||||
if (value === 0) {
|
||||
if (activationTimeout !== null) {
|
||||
Script.clearTimeout(activationTimeout);
|
||||
activationTimeout = null;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -67,7 +76,6 @@ function Trigger(hand) {
|
|||
|
||||
this.buttonPress = function(value) {
|
||||
_this.buttonValue = value;
|
||||
|
||||
};
|
||||
|
||||
this.down = function() {
|
||||
|
@ -78,347 +86,224 @@ function Trigger(hand) {
|
|||
|
||||
var coolInTimeout = null;
|
||||
|
||||
var TELEPORTER_STATES = {
|
||||
IDLE: 'idle',
|
||||
COOL_IN: 'cool_in',
|
||||
TARGETTING_INVALID: 'targetting_invalid',
|
||||
}
|
||||
|
||||
var TARGET = {
|
||||
NONE: 'none', // Not currently targetting anything
|
||||
INVISIBLE: 'invisible', // The current target is an invvsible surface
|
||||
INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.)
|
||||
SURFACE: 'surface', // The current target is a valid surface
|
||||
SEAT: 'seat', // The current target is a seat
|
||||
}
|
||||
|
||||
function Teleporter() {
|
||||
var _this = this;
|
||||
this.intersection = null;
|
||||
this.rightOverlayLine = null;
|
||||
this.leftOverlayLine = null;
|
||||
this.targetOverlay = null;
|
||||
this.cancelOverlay = null;
|
||||
this.updateConnected = null;
|
||||
this.smoothArrivalInterval = null;
|
||||
this.teleportHand = null;
|
||||
this.tooClose = false;
|
||||
this.inCoolIn = false;
|
||||
this.active = false;
|
||||
this.state = TELEPORTER_STATES.IDLE;
|
||||
this.currentTarget = TARGET.INVALID;
|
||||
|
||||
this.initialize = function() {
|
||||
this.createMappings();
|
||||
this.overlayLines = {
|
||||
left: null,
|
||||
right: null,
|
||||
};
|
||||
this.updateConnected = null;
|
||||
this.activeHand = null;
|
||||
|
||||
this.createMappings = function() {
|
||||
teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random();
|
||||
teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName);
|
||||
this.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random();
|
||||
this.teleportMappingInternal = Controller.newMapping(this.telporterMappingInternalName);
|
||||
|
||||
Controller.enableMapping(teleporter.telporterMappingInternalName);
|
||||
// Setup overlays
|
||||
this.cancelOverlay = Overlays.addOverlay("model", {
|
||||
url: TOO_CLOSE_MODEL_URL,
|
||||
dimensions: TARGET_MODEL_DIMENSIONS,
|
||||
visible: false
|
||||
});
|
||||
this.targetOverlay = Overlays.addOverlay("model", {
|
||||
url: TARGET_MODEL_URL,
|
||||
dimensions: TARGET_MODEL_DIMENSIONS,
|
||||
visible: false
|
||||
});
|
||||
this.seatOverlay = Overlays.addOverlay("model", {
|
||||
url: SEAT_MODEL_URL,
|
||||
dimensions: TARGET_MODEL_DIMENSIONS,
|
||||
visible: false
|
||||
});
|
||||
|
||||
this.enableMappings = function() {
|
||||
Controller.enableMapping(this.telporterMappingInternalName);
|
||||
};
|
||||
|
||||
this.disableMappings = function() {
|
||||
Controller.disableMapping(teleporter.telporterMappingInternalName);
|
||||
};
|
||||
|
||||
this.enterTeleportMode = function(hand) {
|
||||
this.cleanup = function() {
|
||||
this.disableMappings();
|
||||
|
||||
Overlays.deleteOverlay(this.targetOverlay);
|
||||
this.targetOverlay = null;
|
||||
|
||||
Overlays.deleteOverlay(this.cancelOverlay);
|
||||
this.cancelOverlay = null;
|
||||
|
||||
Overlays.deleteOverlay(this.seatOverlay);
|
||||
this.seatOverlay = null;
|
||||
|
||||
this.deleteOverlayBeams();
|
||||
if (this.updateConnected === true) {
|
||||
Script.update.disconnect(this, this.update);
|
||||
}
|
||||
};
|
||||
|
||||
this.enterTeleportMode = function(hand) {
|
||||
if (inTeleportMode === true) {
|
||||
return;
|
||||
}
|
||||
if (isDisabled === 'both') {
|
||||
if (isDisabled === 'both' || isDisabled === hand) {
|
||||
return;
|
||||
}
|
||||
|
||||
inTeleportMode = true;
|
||||
this.inCoolIn = true;
|
||||
|
||||
if (coolInTimeout !== null) {
|
||||
Script.clearTimeout(coolInTimeout);
|
||||
|
||||
}
|
||||
|
||||
this.state = TELEPORTER_STATES.COOL_IN;
|
||||
coolInTimeout = Script.setTimeout(function() {
|
||||
_this.inCoolIn = false;
|
||||
if (_this.state === TELEPORTER_STATES.COOL_IN) {
|
||||
_this.state = TELEPORTER_STATES.TARGETTING;
|
||||
}
|
||||
}, COOL_IN_DURATION)
|
||||
|
||||
if (this.smoothArrivalInterval !== null) {
|
||||
Script.clearInterval(this.smoothArrivalInterval);
|
||||
}
|
||||
if (activationTimeout !== null) {
|
||||
Script.clearInterval(activationTimeout);
|
||||
}
|
||||
|
||||
this.teleportHand = hand;
|
||||
this.initialize();
|
||||
Script.update.connect(this.update);
|
||||
this.activeHand = hand;
|
||||
this.enableMappings();
|
||||
Script.update.connect(this, this.update);
|
||||
this.updateConnected = true;
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
this.createTargetOverlay = function(visible) {
|
||||
if (visible == undefined) {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
if (_this.targetOverlay !== null) {
|
||||
return;
|
||||
}
|
||||
var targetOverlayProps = {
|
||||
url: TARGET_MODEL_URL,
|
||||
dimensions: TARGET_MODEL_DIMENSIONS,
|
||||
visible: visible
|
||||
};
|
||||
|
||||
_this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps);
|
||||
|
||||
};
|
||||
|
||||
this.createCancelOverlay = function(visible) {
|
||||
if (visible == undefined) {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
if (_this.cancelOverlay !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cancelOverlayProps = {
|
||||
url: TOO_CLOSE_MODEL_URL,
|
||||
dimensions: TARGET_MODEL_DIMENSIONS,
|
||||
visible: visible
|
||||
};
|
||||
|
||||
_this.cancelOverlay = Overlays.addOverlay("model", cancelOverlayProps);
|
||||
};
|
||||
|
||||
this.deleteCancelOverlay = function() {
|
||||
if (this.cancelOverlay === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Overlays.deleteOverlay(this.cancelOverlay);
|
||||
this.cancelOverlay = null;
|
||||
}
|
||||
|
||||
this.hideCancelOverlay = function() {
|
||||
if (this.cancelOverlay === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.intersection = null;
|
||||
Overlays.editOverlay(this.cancelOverlay, { visible: false });
|
||||
}
|
||||
|
||||
this.showCancelOverlay = function() {
|
||||
if (this.cancelOverlay === null) {
|
||||
return this.createCancelOverlay();
|
||||
}
|
||||
Overlays.editOverlay(this.cancelOverlay, { visible: true });
|
||||
}
|
||||
|
||||
|
||||
this.deleteTargetOverlay = function() {
|
||||
if (this.targetOverlay === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Overlays.deleteOverlay(this.targetOverlay);
|
||||
this.intersection = null;
|
||||
this.targetOverlay = null;
|
||||
}
|
||||
|
||||
this.hideTargetOverlay = function() {
|
||||
if (this.targetOverlay === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.intersection = null;
|
||||
Overlays.editOverlay(this.targetOverlay, { visible: false });
|
||||
}
|
||||
|
||||
this.showTargetOverlay = function() {
|
||||
if (this.targetOverlay === null) {
|
||||
return this.createTargetOverlay();
|
||||
}
|
||||
Overlays.editOverlay(this.targetOverlay, { visible: true });
|
||||
}
|
||||
|
||||
this.turnOffOverlayBeams = function() {
|
||||
this.rightOverlayOff();
|
||||
this.leftOverlayOff();
|
||||
}
|
||||
|
||||
this.exitTeleportMode = function(value) {
|
||||
if (activationTimeout !== null) {
|
||||
Script.clearTimeout(activationTimeout);
|
||||
activationTimeout = null;
|
||||
}
|
||||
if (this.updateConnected === true) {
|
||||
Script.update.disconnect(this.update);
|
||||
Script.update.disconnect(this, this.update);
|
||||
}
|
||||
|
||||
this.disableMappings();
|
||||
this.turnOffOverlayBeams();
|
||||
this.deleteOverlayBeams();
|
||||
this.hideTargetOverlay();
|
||||
this.hideCancelOverlay();
|
||||
|
||||
this.updateConnected = null;
|
||||
this.inCoolIn = false;
|
||||
this.state = TELEPORTER_STATES.IDLE;
|
||||
inTeleportMode = false;
|
||||
};
|
||||
|
||||
this.update = function() {
|
||||
if (isDisabled === 'both') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (teleporter.teleportHand === 'left') {
|
||||
if (isDisabled === 'left') {
|
||||
return;
|
||||
this.deleteOverlayBeams = function() {
|
||||
for (key in this.overlayLines) {
|
||||
if (this.overlayLines[key] !== null) {
|
||||
Overlays.deleteOverlay(this.overlayLines[key]);
|
||||
this.overlayLines[key] = null;
|
||||
}
|
||||
teleporter.leftRay();
|
||||
if ((leftPad.buttonValue === 0) && inTeleportMode === true) {
|
||||
if (_this.inCoolIn === true) {
|
||||
_this.exitTeleportMode();
|
||||
_this.hideTargetOverlay();
|
||||
_this.hideCancelOverlay();
|
||||
} else {
|
||||
_this.teleport();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (isDisabled === 'right') {
|
||||
return;
|
||||
}
|
||||
teleporter.rightRay();
|
||||
if ((rightPad.buttonValue === 0) && inTeleportMode === true) {
|
||||
if (_this.inCoolIn === true) {
|
||||
_this.exitTeleportMode();
|
||||
_this.hideTargetOverlay();
|
||||
_this.hideCancelOverlay();
|
||||
} else {
|
||||
_this.teleport();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.rightRay = function() {
|
||||
var pose = Controller.getPoseValue(Controller.Standard.RightHand);
|
||||
var rightPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition();
|
||||
var rightRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) :
|
||||
Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, {
|
||||
x: 1,
|
||||
y: 0,
|
||||
z: 0
|
||||
}));
|
||||
|
||||
var rightPickRay = {
|
||||
origin: rightPosition,
|
||||
direction: Quat.getUp(rightRotation),
|
||||
};
|
||||
|
||||
this.rightPickRay = rightPickRay;
|
||||
|
||||
var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 50));
|
||||
|
||||
|
||||
var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity], true, true);
|
||||
|
||||
if (rightIntersection.intersects) {
|
||||
if (this.tooClose === true) {
|
||||
this.hideTargetOverlay();
|
||||
|
||||
this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
|
||||
if (this.cancelOverlay !== null) {
|
||||
this.updateCancelOverlay(rightIntersection);
|
||||
} else {
|
||||
this.createCancelOverlay();
|
||||
}
|
||||
} else {
|
||||
if (this.inCoolIn === true) {
|
||||
this.hideTargetOverlay();
|
||||
this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
|
||||
if (this.cancelOverlay !== null) {
|
||||
this.updateCancelOverlay(rightIntersection);
|
||||
} else {
|
||||
this.createCancelOverlay();
|
||||
}
|
||||
} else {
|
||||
this.hideCancelOverlay();
|
||||
|
||||
this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT);
|
||||
if (this.targetOverlay !== null) {
|
||||
this.updateTargetOverlay(rightIntersection);
|
||||
} else {
|
||||
this.createTargetOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
this.hideTargetOverlay();
|
||||
this.rightLineOn(rightPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT);
|
||||
}
|
||||
}
|
||||
|
||||
this.update = function() {
|
||||
if (_this.state === TELEPORTER_STATES.IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.leftRay = function() {
|
||||
var pose = Controller.getPoseValue(Controller.Standard.LeftHand);
|
||||
var leftPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition();
|
||||
var leftRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) :
|
||||
// Get current hand pose information so that we can get the direction of the teleport beam
|
||||
var pose = Controller.getPoseValue(handInfo[_this.activeHand].controllerInput);
|
||||
var handPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition();
|
||||
var handRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) :
|
||||
Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, {
|
||||
x: 1,
|
||||
y: 0,
|
||||
z: 0
|
||||
}));
|
||||
|
||||
var leftPickRay = {
|
||||
origin: leftPosition,
|
||||
direction: Quat.getUp(leftRotation),
|
||||
var pickRay = {
|
||||
origin: handPosition,
|
||||
direction: Quat.getUp(handRotation),
|
||||
};
|
||||
|
||||
this.leftPickRay = leftPickRay;
|
||||
// We do up to 2 ray picks to find a teleport location.
|
||||
// There are 2 types of teleport locations we are interested in:
|
||||
// 1. A visible floor. This can be any entity surface that points within some degree of "up"
|
||||
// 2. A seat. The seat can be visible or invisible.
|
||||
//
|
||||
// * In the first pass we pick against visible and invisible entities so that we can find invisible seats.
|
||||
// We might hit an invisible entity that is not a seat, so we need to do a second pass.
|
||||
// * In the second pass we pick against visible entities only.
|
||||
//
|
||||
var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true);
|
||||
|
||||
var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 50));
|
||||
|
||||
|
||||
var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity], true, true);
|
||||
|
||||
if (leftIntersection.intersects) {
|
||||
|
||||
if (this.tooClose === true) {
|
||||
this.hideTargetOverlay();
|
||||
this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
|
||||
if (this.cancelOverlay !== null) {
|
||||
this.updateCancelOverlay(leftIntersection);
|
||||
} else {
|
||||
this.createCancelOverlay();
|
||||
}
|
||||
} else {
|
||||
if (this.inCoolIn === true) {
|
||||
this.hideTargetOverlay();
|
||||
this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
|
||||
if (this.cancelOverlay !== null) {
|
||||
this.updateCancelOverlay(leftIntersection);
|
||||
} else {
|
||||
this.createCancelOverlay();
|
||||
}
|
||||
} else {
|
||||
this.hideCancelOverlay();
|
||||
this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT);
|
||||
|
||||
if (this.targetOverlay !== null) {
|
||||
this.updateTargetOverlay(leftIntersection);
|
||||
} else {
|
||||
this.createTargetOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
var teleportLocationType = getTeleportTargetType(intersection);
|
||||
if (teleportLocationType === TARGET.INVISIBLE) {
|
||||
intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true);
|
||||
teleportLocationType = getTeleportTargetType(intersection);
|
||||
}
|
||||
|
||||
if (teleportLocationType === TARGET.NONE) {
|
||||
this.hideTargetOverlay();
|
||||
this.leftLineOn(leftPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT);
|
||||
this.hideCancelOverlay();
|
||||
this.hideSeatOverlay();
|
||||
|
||||
var farPosition = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, 50));
|
||||
this.updateLineOverlay(_this.activeHand, pickRay.origin, farPosition, COLORS_TELEPORT_CANNOT_TELEPORT);
|
||||
} else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) {
|
||||
this.hideTargetOverlay();
|
||||
this.hideSeatOverlay();
|
||||
|
||||
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL);
|
||||
this.updateDestinationOverlay(this.cancelOverlay, intersection);
|
||||
} else if (teleportLocationType === TARGET.SURFACE) {
|
||||
if (this.state === TELEPORTER_STATES.COOL_IN) {
|
||||
this.hideTargetOverlay();
|
||||
this.hideSeatOverlay();
|
||||
|
||||
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL);
|
||||
this.updateDestinationOverlay(this.cancelOverlay, intersection);
|
||||
} else {
|
||||
this.hideCancelOverlay();
|
||||
this.hideSeatOverlay();
|
||||
|
||||
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CAN_TELEPORT);
|
||||
this.updateDestinationOverlay(this.targetOverlay, intersection);
|
||||
}
|
||||
} else if (teleportLocationType === TARGET.SEAT) {
|
||||
this.hideCancelOverlay();
|
||||
this.hideTargetOverlay();
|
||||
|
||||
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_SEAT);
|
||||
this.updateDestinationOverlay(this.seatOverlay, intersection);
|
||||
}
|
||||
|
||||
|
||||
if (((_this.activeHand == 'left' ? leftPad : rightPad).buttonValue === 0) && inTeleportMode === true) {
|
||||
this.exitTeleportMode();
|
||||
this.hideCancelOverlay();
|
||||
this.hideTargetOverlay();
|
||||
this.hideSeatOverlay();
|
||||
|
||||
if (teleportLocationType === TARGET.NONE || teleportLocationType === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) {
|
||||
// Do nothing
|
||||
} else if (teleportLocationType === TARGET.SEAT) {
|
||||
Entities.callEntityMethod(intersection.entityID, 'sit');
|
||||
} else if (teleportLocationType === TARGET.SURFACE) {
|
||||
var offset = getAvatarFootOffset();
|
||||
intersection.intersection.y += offset;
|
||||
MyAvatar.position = intersection.intersection;
|
||||
HMD.centerUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.rightLineOn = function(closePoint, farPoint, color) {
|
||||
if (this.rightOverlayLine === null) {
|
||||
this.updateLineOverlay = function(hand, closePoint, farPoint, color) {
|
||||
if (this.overlayLines[hand] === null) {
|
||||
var lineProperties = {
|
||||
start: closePoint,
|
||||
end: farPoint,
|
||||
|
@ -431,10 +316,10 @@ function Teleporter() {
|
|||
glow: 1.0
|
||||
};
|
||||
|
||||
this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties);
|
||||
this.overlayLines[hand] = Overlays.addOverlay("line3d", lineProperties);
|
||||
|
||||
} else {
|
||||
var success = Overlays.editOverlay(this.rightOverlayLine, {
|
||||
var success = Overlays.editOverlay(this.overlayLines[hand], {
|
||||
start: closePoint,
|
||||
end: farPoint,
|
||||
color: color
|
||||
|
@ -442,47 +327,19 @@ function Teleporter() {
|
|||
}
|
||||
};
|
||||
|
||||
this.leftLineOn = function(closePoint, farPoint, color) {
|
||||
if (this.leftOverlayLine === null) {
|
||||
var lineProperties = {
|
||||
ignoreRayIntersection: true,
|
||||
start: closePoint,
|
||||
end: farPoint,
|
||||
color: color,
|
||||
visible: true,
|
||||
alpha: 1,
|
||||
solid: true,
|
||||
glow: 1.0,
|
||||
drawInFront: true
|
||||
};
|
||||
|
||||
this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties);
|
||||
|
||||
} else {
|
||||
var success = Overlays.editOverlay(this.leftOverlayLine, {
|
||||
start: closePoint,
|
||||
end: farPoint,
|
||||
color: color
|
||||
});
|
||||
}
|
||||
};
|
||||
this.rightOverlayOff = function() {
|
||||
if (this.rightOverlayLine !== null) {
|
||||
Overlays.deleteOverlay(this.rightOverlayLine);
|
||||
this.rightOverlayLine = null;
|
||||
}
|
||||
this.hideCancelOverlay = function() {
|
||||
Overlays.editOverlay(this.cancelOverlay, { visible: false });
|
||||
};
|
||||
|
||||
this.leftOverlayOff = function() {
|
||||
if (this.leftOverlayLine !== null) {
|
||||
Overlays.deleteOverlay(this.leftOverlayLine);
|
||||
this.leftOverlayLine = null;
|
||||
}
|
||||
this.hideTargetOverlay = function() {
|
||||
Overlays.editOverlay(this.targetOverlay, { visible: false });
|
||||
};
|
||||
|
||||
this.updateTargetOverlay = function(intersection) {
|
||||
_this.intersection = intersection;
|
||||
this.hideSeatOverlay = function() {
|
||||
Overlays.editOverlay(this.seatOverlay, { visible: false });
|
||||
};
|
||||
|
||||
this.updateDestinationOverlay = function(overlayID, intersection) {
|
||||
var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP);
|
||||
var euler = Quat.safeEulerAngles(rotation);
|
||||
var position = {
|
||||
|
@ -491,115 +348,15 @@ function Teleporter() {
|
|||
z: intersection.intersection.z
|
||||
};
|
||||
|
||||
this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal);
|
||||
var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0);
|
||||
|
||||
Overlays.editOverlay(this.targetOverlay, {
|
||||
Overlays.editOverlay(overlayID, {
|
||||
visible: true,
|
||||
position: position,
|
||||
rotation: towardUs
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
this.updateCancelOverlay = function(intersection) {
|
||||
_this.intersection = intersection;
|
||||
|
||||
var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP);
|
||||
var euler = Quat.safeEulerAngles(rotation);
|
||||
var position = {
|
||||
x: intersection.intersection.x,
|
||||
y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2,
|
||||
z: intersection.intersection.z
|
||||
};
|
||||
|
||||
this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal);
|
||||
var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0);
|
||||
|
||||
Overlays.editOverlay(this.cancelOverlay, {
|
||||
visible: true,
|
||||
position: position,
|
||||
rotation: towardUs
|
||||
});
|
||||
};
|
||||
|
||||
this.triggerHaptics = function() {
|
||||
var hand = this.teleportHand === 'left' ? 0 : 1;
|
||||
var haptic = Controller.triggerShortHapticPulse(0.2, hand);
|
||||
};
|
||||
|
||||
this.teleport = function(value) {
|
||||
|
||||
if (value === undefined) {
|
||||
this.exitTeleportMode();
|
||||
}
|
||||
|
||||
if (this.intersection !== null) {
|
||||
if (this.tooClose === true) {
|
||||
this.exitTeleportMode();
|
||||
this.hideCancelOverlay();
|
||||
return;
|
||||
}
|
||||
var offset = getAvatarFootOffset();
|
||||
this.intersection.intersection.y += offset;
|
||||
this.exitTeleportMode();
|
||||
// Disable smooth arrival, possibly temporarily
|
||||
//this.smoothArrival();
|
||||
MyAvatar.position = _this.intersection.intersection;
|
||||
_this.hideTargetOverlay();
|
||||
_this.hideCancelOverlay();
|
||||
HMD.centerUI();
|
||||
}
|
||||
};
|
||||
|
||||
this.findMidpoint = function(start, end) {
|
||||
var xy = Vec3.sum(start, end);
|
||||
var midpoint = Vec3.multiply(0.5, xy);
|
||||
return midpoint
|
||||
};
|
||||
|
||||
this.getArrivalPoints = function(startPoint, endPoint) {
|
||||
var arrivalPoints = [];
|
||||
var i;
|
||||
var lastPoint;
|
||||
|
||||
for (i = 0; i < NUMBER_OF_STEPS; i++) {
|
||||
if (i === 0) {
|
||||
lastPoint = startPoint;
|
||||
}
|
||||
var newPoint = _this.findMidpoint(lastPoint, endPoint);
|
||||
lastPoint = newPoint;
|
||||
arrivalPoints.push(newPoint);
|
||||
}
|
||||
|
||||
arrivalPoints.push(endPoint);
|
||||
|
||||
return arrivalPoints;
|
||||
};
|
||||
|
||||
this.smoothArrival = function() {
|
||||
|
||||
_this.arrivalPoints = _this.getArrivalPoints(MyAvatar.position, _this.intersection.intersection);
|
||||
_this.smoothArrivalInterval = Script.setInterval(function() {
|
||||
if (_this.arrivalPoints.length === 0) {
|
||||
Script.clearInterval(_this.smoothArrivalInterval);
|
||||
HMD.centerUI();
|
||||
return;
|
||||
}
|
||||
var landingPoint = _this.arrivalPoints.shift();
|
||||
MyAvatar.position = landingPoint;
|
||||
|
||||
if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) {
|
||||
_this.hideTargetOverlay();
|
||||
_this.hideCancelOverlay();
|
||||
}
|
||||
|
||||
}, SMOOTH_ARRIVAL_SPACING);
|
||||
}
|
||||
|
||||
this.createTargetOverlay(false);
|
||||
this.createCancelOverlay(false);
|
||||
|
||||
}
|
||||
|
||||
//related to repositioning the avatar after you teleport
|
||||
|
@ -611,20 +368,16 @@ function getAvatarFootOffset() {
|
|||
var jointName = d.joint;
|
||||
if (jointName === "RightUpLeg") {
|
||||
upperLeg = d.translation.y;
|
||||
}
|
||||
if (jointName === "RightLeg") {
|
||||
} else if (jointName === "RightLeg") {
|
||||
lowerLeg = d.translation.y;
|
||||
}
|
||||
if (jointName === "RightFoot") {
|
||||
} else if (jointName === "RightFoot") {
|
||||
foot = d.translation.y;
|
||||
}
|
||||
if (jointName === "RightToeBase") {
|
||||
} else if (jointName === "RightToeBase") {
|
||||
toe = d.translation.y;
|
||||
}
|
||||
if (jointName === "RightToe_End") {
|
||||
} else if (jointName === "RightToe_End") {
|
||||
toeTop = d.translation.y;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var offset = upperLeg + lowerLeg + foot + toe + toeTop;
|
||||
offset = offset / 100;
|
||||
|
@ -655,7 +408,6 @@ var rightTrigger = new Trigger('right');
|
|||
|
||||
var mappingName, teleportMapping;
|
||||
|
||||
var activationTimeout = null;
|
||||
var TELEPORT_DELAY = 0;
|
||||
|
||||
function isMoving() {
|
||||
|
@ -668,17 +420,44 @@ function isMoving() {
|
|||
}
|
||||
};
|
||||
|
||||
function parseJSON(json) {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
// When determininig whether you can teleport to a location, the normal of the
|
||||
// point that is being intersected with is looked at. If this normal is more
|
||||
// than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then
|
||||
// you can't teleport there.
|
||||
var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70;
|
||||
function isValidTeleportLocation(position, surfaceNormal) {
|
||||
const MAX_ANGLE_FROM_UP_TO_TELEPORT = 70;
|
||||
function getTeleportTargetType(intersection) {
|
||||
if (!intersection.intersects) {
|
||||
return TARGET.NONE;
|
||||
}
|
||||
|
||||
var props = Entities.getEntityProperties(intersection.entityID, ['userData', 'visible']);
|
||||
var data = parseJSON(props.userData);
|
||||
if (data !== undefined && data.seat !== undefined) {
|
||||
return TARGET.SEAT;
|
||||
}
|
||||
|
||||
if (!props.visible) {
|
||||
return TARGET.INVISIBLE;
|
||||
}
|
||||
|
||||
var surfaceNormal = intersection.surfaceNormal;
|
||||
var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z);
|
||||
var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI);
|
||||
return angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
|
||||
angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
|
||||
Vec3.distance(MyAvatar.position, position) <= TELEPORT_CANCEL_RANGE;
|
||||
|
||||
if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
|
||||
angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
|
||||
Vec3.distance(MyAvatar.position, intersection.intersection) <= TELEPORT_CANCEL_RANGE) {
|
||||
return TARGET.INVALID;
|
||||
} else {
|
||||
return TARGET.SURFACE;
|
||||
}
|
||||
};
|
||||
|
||||
function registerMappings() {
|
||||
|
@ -695,20 +474,13 @@ function registerMappings() {
|
|||
if (isDisabled === 'left' || isDisabled === 'both') {
|
||||
return;
|
||||
}
|
||||
if (activationTimeout !== null) {
|
||||
return
|
||||
}
|
||||
if (leftTrigger.down()) {
|
||||
return;
|
||||
}
|
||||
if (isMoving() === true) {
|
||||
return;
|
||||
}
|
||||
activationTimeout = Script.setTimeout(function() {
|
||||
Script.clearTimeout(activationTimeout);
|
||||
activationTimeout = null;
|
||||
teleporter.enterTeleportMode('left')
|
||||
}, TELEPORT_DELAY)
|
||||
teleporter.enterTeleportMode('left')
|
||||
return;
|
||||
});
|
||||
teleportMapping.from(Controller.Standard.RightPrimaryThumb)
|
||||
|
@ -716,9 +488,6 @@ function registerMappings() {
|
|||
if (isDisabled === 'right' || isDisabled === 'both') {
|
||||
return;
|
||||
}
|
||||
if (activationTimeout !== null) {
|
||||
return
|
||||
}
|
||||
if (rightTrigger.down()) {
|
||||
return;
|
||||
}
|
||||
|
@ -726,11 +495,7 @@ function registerMappings() {
|
|||
return;
|
||||
}
|
||||
|
||||
activationTimeout = Script.setTimeout(function() {
|
||||
teleporter.enterTeleportMode('right')
|
||||
Script.clearTimeout(activationTimeout);
|
||||
activationTimeout = null;
|
||||
}, TELEPORT_DELAY)
|
||||
teleporter.enterTeleportMode('right')
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
@ -741,18 +506,11 @@ var teleporter = new Teleporter();
|
|||
|
||||
Controller.enableMapping(mappingName);
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
||||
function cleanup() {
|
||||
teleportMapping.disable();
|
||||
teleporter.disableMappings();
|
||||
teleporter.deleteTargetOverlay();
|
||||
teleporter.deleteCancelOverlay();
|
||||
teleporter.turnOffOverlayBeams();
|
||||
if (teleporter.updateConnected !== null) {
|
||||
Script.update.disconnect(teleporter.update);
|
||||
}
|
||||
teleporter.cleanup();
|
||||
}
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
||||
var isDisabled = false;
|
||||
var handleHandMessages = function(channel, message, sender) {
|
||||
|
|
|
@ -245,18 +245,6 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
|
|||
populateUserList(message.params.selected);
|
||||
UserActivityLogger.palAction("refresh", "");
|
||||
break;
|
||||
case 'updateGain':
|
||||
data = message.params;
|
||||
if (data['isReleased']) {
|
||||
// isReleased=true happens once at the end of a cycle of dragging
|
||||
// the slider about, but with same gain as last isReleased=false so
|
||||
// we don't set the gain in that case, and only here do we want to
|
||||
// send an analytic event.
|
||||
UserActivityLogger.palAction("avatar_gain_changed", data['sessionId']);
|
||||
} else {
|
||||
Users.setAvatarGain(data['sessionId'], data['gain']);
|
||||
}
|
||||
break;
|
||||
case 'displayNameUpdate':
|
||||
if (MyAvatar.displayName !== message.params) {
|
||||
MyAvatar.displayName = message.params;
|
||||
|
|
257
scripts/tutorials/entity_scripts/sit.js
Normal file
257
scripts/tutorials/entity_scripts/sit.js
Normal file
|
@ -0,0 +1,257 @@
|
|||
(function() {
|
||||
Script.include("/~/system/libraries/utils.js");
|
||||
|
||||
var SETTING_KEY = "com.highfidelity.avatar.isSitting";
|
||||
var ROLE = "fly";
|
||||
var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx";
|
||||
var ANIMATION_FPS = 30;
|
||||
var ANIMATION_FIRST_FRAME = 1;
|
||||
var ANIMATION_LAST_FRAME = 10;
|
||||
var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT'];
|
||||
var RELEASE_TIME = 500; // ms
|
||||
var RELEASE_DISTANCE = 0.2; // meters
|
||||
var MAX_IK_ERROR = 20;
|
||||
var DESKTOP_UI_CHECK_INTERVAL = 250;
|
||||
var DESKTOP_MAX_DISTANCE = 5;
|
||||
var SIT_DELAY = 25
|
||||
|
||||
this.entityID = null;
|
||||
this.timers = {};
|
||||
this.animStateHandlerID = null;
|
||||
|
||||
this.preload = function(entityID) {
|
||||
this.entityID = entityID;
|
||||
}
|
||||
this.unload = function() {
|
||||
if (MyAvatar.sessionUUID === this.getSeatUser()) {
|
||||
this.sitUp(this.entityID);
|
||||
}
|
||||
if (this.interval) {
|
||||
Script.clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
this.cleanupOverlay();
|
||||
}
|
||||
|
||||
this.setSeatUser = function(user) {
|
||||
var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData;
|
||||
userData = JSON.parse(userData);
|
||||
|
||||
if (user) {
|
||||
userData.seat.user = user;
|
||||
} else {
|
||||
delete userData.seat.user;
|
||||
}
|
||||
|
||||
Entities.editEntity(this.entityID, {
|
||||
userData: JSON.stringify(userData)
|
||||
});
|
||||
}
|
||||
this.getSeatUser = function() {
|
||||
var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]);
|
||||
var userData = JSON.parse(properties.userData);
|
||||
|
||||
if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) {
|
||||
var avatar = AvatarList.getAvatar(userData.seat.user);
|
||||
if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return userData.seat.user;
|
||||
}
|
||||
|
||||
this.checkSeatForAvatar = function() {
|
||||
var seatUser = this.getSeatUser();
|
||||
var avatarIdentifiers = AvatarList.getAvatarIdentifiers();
|
||||
for (var i in avatarIdentifiers) {
|
||||
var avatar = AvatarList.getAvatar(avatarIdentifiers[i]);
|
||||
if (avatar && avatar.sessionUUID === seatUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
this.sitDown = function() {
|
||||
if (this.checkSeatForAvatar()) {
|
||||
print("Someone is already sitting in that chair.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setSeatUser(MyAvatar.sessionUUID);
|
||||
|
||||
var previousValue = Settings.getValue(SETTING_KEY);
|
||||
Settings.setValue(SETTING_KEY, this.entityID);
|
||||
if (previousValue === "") {
|
||||
MyAvatar.characterControllerEnabled = false;
|
||||
MyAvatar.hmdLeanRecenterEnabled = false;
|
||||
MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME);
|
||||
MyAvatar.resetSensorsAndBody();
|
||||
}
|
||||
|
||||
var that = this;
|
||||
Script.setTimeout(function() {
|
||||
var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]);
|
||||
var index = MyAvatar.getJointIndex("Hips");
|
||||
MyAvatar.pinJoint(index, properties.position, properties.rotation);
|
||||
|
||||
that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) {
|
||||
return { headType: 0 };
|
||||
}, ["headType"]);
|
||||
Script.update.connect(that, that.update);
|
||||
Controller.keyPressEvent.connect(that, that.keyPressed);
|
||||
Controller.keyReleaseEvent.connect(that, that.keyReleased);
|
||||
for (var i in RELEASE_KEYS) {
|
||||
Controller.captureKeyEvents({ text: RELEASE_KEYS[i] });
|
||||
}
|
||||
}, SIT_DELAY);
|
||||
}
|
||||
|
||||
this.sitUp = function() {
|
||||
this.setSeatUser(null);
|
||||
|
||||
if (Settings.getValue(SETTING_KEY) === this.entityID) {
|
||||
MyAvatar.restoreRoleAnimation(ROLE);
|
||||
MyAvatar.characterControllerEnabled = true;
|
||||
MyAvatar.hmdLeanRecenterEnabled = true;
|
||||
|
||||
var index = MyAvatar.getJointIndex("Hips");
|
||||
MyAvatar.clearPinOnJoint(index);
|
||||
|
||||
MyAvatar.resetSensorsAndBody();
|
||||
|
||||
Script.setTimeout(function() {
|
||||
MyAvatar.bodyPitch = 0.0;
|
||||
MyAvatar.bodyRoll = 0.0;
|
||||
}, SIT_DELAY);
|
||||
|
||||
Settings.setValue(SETTING_KEY, "");
|
||||
}
|
||||
|
||||
MyAvatar.removeAnimationStateHandler(this.animStateHandlerID);
|
||||
Script.update.disconnect(this, this.update);
|
||||
Controller.keyPressEvent.disconnect(this, this.keyPressed);
|
||||
Controller.keyReleaseEvent.disconnect(this, this.keyReleased);
|
||||
for (var i in RELEASE_KEYS) {
|
||||
Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] });
|
||||
}
|
||||
}
|
||||
|
||||
this.sit = function () {
|
||||
this.sitDown();
|
||||
}
|
||||
|
||||
this.createOverlay = function() {
|
||||
var text = "Click to sit";
|
||||
var textMargin = 0.05;
|
||||
var lineHeight = 0.15;
|
||||
|
||||
this.overlay = Overlays.addOverlay("text3d", {
|
||||
position: { x: 0.0, y: 0.0, z: 0.0},
|
||||
dimensions: { x: 0.1, y: 0.1 },
|
||||
backgroundColor: { red: 0, green: 0, blue: 0 },
|
||||
color: { red: 255, green: 255, blue: 255 },
|
||||
topMargin: textMargin,
|
||||
leftMargin: textMargin,
|
||||
bottomMargin: textMargin,
|
||||
rightMargin: textMargin,
|
||||
text: text,
|
||||
lineHeight: lineHeight,
|
||||
alpha: 0.9,
|
||||
backgroundAlpha: 0.9,
|
||||
ignoreRayIntersection: true,
|
||||
visible: true,
|
||||
isFacingAvatar: true
|
||||
});
|
||||
var textSize = Overlays.textSize(this.overlay, text);
|
||||
var overlayDimensions = {
|
||||
x: textSize.width + 2 * textMargin,
|
||||
y: textSize.height + 2 * textMargin
|
||||
}
|
||||
var properties = Entities.getEntityProperties(this.entityID, ["position", "registrationPoint", "dimensions"]);
|
||||
var yOffset = (1.0 - properties.registrationPoint.y) * properties.dimensions.y + (overlayDimensions.y / 2.0);
|
||||
var overlayPosition = Vec3.sum(properties.position, { x: 0, y: yOffset, z: 0 });
|
||||
Overlays.editOverlay(this.overlay, {
|
||||
position: overlayPosition,
|
||||
dimensions: overlayDimensions
|
||||
});
|
||||
}
|
||||
this.cleanupOverlay = function() {
|
||||
if (this.overlay !== null) {
|
||||
Overlays.deleteOverlay(this.overlay);
|
||||
this.overlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.update = function(dt) {
|
||||
if (MyAvatar.sessionUUID === this.getSeatUser()) {
|
||||
var properties = Entities.getEntityProperties(this.entityID, ["position"]);
|
||||
var avatarDistance = Vec3.distance(MyAvatar.position, properties.position);
|
||||
var ikError = MyAvatar.getIKErrorOnLastSolve();
|
||||
if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) {
|
||||
print("IK error: " + ikError + ", distance from chair: " + avatarDistance);
|
||||
this.sitUp(this.entityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.keyPressed = function(event) {
|
||||
if (isInEditMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
|
||||
var that = this;
|
||||
this.timers[event.text] = Script.setTimeout(function() {
|
||||
that.sitUp();
|
||||
}, RELEASE_TIME);
|
||||
}
|
||||
}
|
||||
this.keyReleased = function(event) {
|
||||
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
|
||||
if (this.timers[event.text]) {
|
||||
Script.clearTimeout(this.timers[event.text]);
|
||||
delete this.timers[event.text];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.canSitDesktop = function() {
|
||||
var properties = Entities.getEntityProperties(this.entityID, ["position"]);
|
||||
var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position);
|
||||
return distanceFromSeat < DESKTOP_MAX_DISTANCE && !this.checkSeatForAvatar();
|
||||
}
|
||||
|
||||
this.hoverEnterEntity = function(event) {
|
||||
if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) {
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.interval = Script.setInterval(function() {
|
||||
if (that.overlay === null) {
|
||||
if (that.canSitDesktop()) {
|
||||
that.createOverlay();
|
||||
}
|
||||
} else if (!that.canSitDesktop()) {
|
||||
that.cleanupOverlay();
|
||||
}
|
||||
}, DESKTOP_UI_CHECK_INTERVAL);
|
||||
}
|
||||
this.hoverLeaveEntity = function(event) {
|
||||
if (this.interval) {
|
||||
Script.clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
this.cleanupOverlay();
|
||||
}
|
||||
|
||||
this.clickDownOnEntity = function () {
|
||||
if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) {
|
||||
return;
|
||||
}
|
||||
if (this.canSitDesktop()) {
|
||||
this.sitDown();
|
||||
}
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue