Merge branch 'master' of https://github.com/highfidelity/hifi into HMDLightingBug

This commit is contained in:
Sam Gateau 2015-09-15 18:07:26 -07:00
commit 08546fcc08
42 changed files with 1609 additions and 1136 deletions

View file

@ -41,10 +41,6 @@ Agent::Agent(NLPacket& packet) :
DEFAULT_WINDOW_STARVE_THRESHOLD, DEFAULT_WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES,
DEFAULT_WINDOW_SECONDS_FOR_DESIRED_REDUCTION, false))
{
// be the parent of the script engine so it gets moved when we do
_scriptEngine.setParent(this);
_scriptEngine.setIsAgent(true);
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
DependencyManager::set<ResourceCacheSharedItems>();
@ -157,8 +153,11 @@ void Agent::run() {
qDebug() << "Downloaded script:" << scriptContents;
_scriptEngine = new ScriptEngine(scriptContents, _payload);
_scriptEngine->setParent(this); // be the parent of the script engine so it gets moved when we do
// setup an Avatar for the script to use
ScriptableAvatar scriptedAvatar(&_scriptEngine);
ScriptableAvatar scriptedAvatar(_scriptEngine);
scriptedAvatar.setForceFaceTrackerConnected(true);
// call model URL setters with empty URLs so our avatar, if user, will have the default models
@ -166,12 +165,11 @@ void Agent::run() {
scriptedAvatar.setSkeletonModelURL(QUrl());
// give this AvatarData object to the script engine
_scriptEngine.setAvatarData(&scriptedAvatar, "Avatar");
setAvatarData(&scriptedAvatar, "Avatar");
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
_scriptEngine.setAvatarHashMap(avatarHashMap.data(), "AvatarList");
_scriptEngine->registerGlobalObject("AvatarList", avatarHashMap.data());
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket");
packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar");
@ -179,33 +177,188 @@ void Agent::run() {
packetReceiver.registerListener(PacketType::AvatarBillboard, avatarHashMap.data(), "processAvatarBillboardPacket");
// register ourselves to the script engine
_scriptEngine.registerGlobalObject("Agent", this);
_scriptEngine->registerGlobalObject("Agent", this);
if (!_payload.isEmpty()) {
_scriptEngine.setParentURL(_payload);
}
// FIXME -we shouldn't be calling this directly, it's normally called by run(), not sure why
// viewers would need this called.
//_scriptEngine->init(); // must be done before we set up the viewers
_scriptEngine.init(); // must be done before we set up the viewers
_scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get<SoundCache>().data());
_scriptEngine.registerGlobalObject("SoundCache", DependencyManager::get<SoundCache>().data());
QScriptValue webSocketServerConstructorValue = _scriptEngine.newFunction(WebSocketServerClass::constructor);
_scriptEngine.globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue);
QScriptValue webSocketServerConstructorValue = _scriptEngine->newFunction(WebSocketServerClass::constructor);
_scriptEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue);
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
_scriptEngine.registerGlobalObject("EntityViewer", &_entityViewer);
_scriptEngine->registerGlobalObject("EntityViewer", &_entityViewer);
_entityViewer.setJurisdictionListener(entityScriptingInterface->getJurisdictionListener());
_entityViewer.init();
entityScriptingInterface->setEntityTree(_entityViewer.getTree());
_scriptEngine.setScriptContents(scriptContents);
_scriptEngine.run();
// wire up our additional agent related processing to the update signal
QObject::connect(_scriptEngine, &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio);
_scriptEngine->run();
setFinished(true);
// kill the avatar identity timer
delete _avatarIdentityTimer;
// delete the script engine
delete _scriptEngine;
}
void Agent::setIsAvatar(bool isAvatar) {
_isAvatar = isAvatar;
if (_isAvatar && !_avatarIdentityTimer) {
// set up the avatar timers
_avatarIdentityTimer = new QTimer(this);
_avatarBillboardTimer = new QTimer(this);
// connect our slot
connect(_avatarIdentityTimer, &QTimer::timeout, this, &Agent::sendAvatarIdentityPacket);
connect(_avatarBillboardTimer, &QTimer::timeout, this, &Agent::sendAvatarBillboardPacket);
// start the timers
_avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);
_avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS);
}
if (!_isAvatar) {
delete _avatarIdentityTimer;
_avatarIdentityTimer = NULL;
delete _avatarBillboardTimer;
_avatarBillboardTimer = NULL;
}
}
void Agent::setAvatarData(AvatarData* avatarData, const QString& objectName) {
_avatarData = avatarData;
_scriptEngine->registerGlobalObject(objectName, avatarData);
}
void Agent::sendAvatarIdentityPacket() {
if (_isAvatar && _avatarData) {
_avatarData->sendIdentityPacket();
}
}
void Agent::sendAvatarBillboardPacket() {
if (_isAvatar && _avatarData) {
_avatarData->sendBillboardPacket();
}
}
void Agent::processAgentAvatarAndAudio(float deltaTime) {
if (!_scriptEngine->isFinished() && _isAvatar && _avatarData) {
const int SCRIPT_AUDIO_BUFFER_SAMPLES = floor(((SCRIPT_DATA_CALLBACK_USECS * AudioConstants::SAMPLE_RATE)
/ (1000 * 1000)) + 0.5);
const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t);
QByteArray avatarByteArray = _avatarData->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO);
_avatarData->doneEncoding(true);
auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size());
avatarPacket->write(avatarByteArray);
auto nodeList = DependencyManager::get<NodeList>();
nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
if (_isListeningToAudioStream || _avatarSound) {
// if we have an avatar audio stream then send it out to our audio-mixer
bool silentFrame = true;
int16_t numAvailableSamples = SCRIPT_AUDIO_BUFFER_SAMPLES;
const int16_t* nextSoundOutput = NULL;
if (_avatarSound) {
const QByteArray& soundByteArray = _avatarSound->getByteArray();
nextSoundOutput = reinterpret_cast<const int16_t*>(soundByteArray.data()
+ _numAvatarSoundSentBytes);
int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > SCRIPT_AUDIO_BUFFER_BYTES
? SCRIPT_AUDIO_BUFFER_BYTES
: soundByteArray.size() - _numAvatarSoundSentBytes;
numAvailableSamples = numAvailableBytes / sizeof(int16_t);
// check if the all of the _numAvatarAudioBufferSamples to be sent are silence
for (int i = 0; i < numAvailableSamples; ++i) {
if (nextSoundOutput[i] != 0) {
silentFrame = false;
break;
}
}
_numAvatarSoundSentBytes += numAvailableBytes;
if (_numAvatarSoundSentBytes == soundByteArray.size()) {
// we're done with this sound object - so set our pointer back to NULL
// and our sent bytes back to zero
_avatarSound = NULL;
_numAvatarSoundSentBytes = 0;
}
}
auto audioPacket = NLPacket::create(silentFrame
? PacketType::SilentAudioFrame
: PacketType::MicrophoneAudioNoEcho);
// seek past the sequence number, will be packed when destination node is known
audioPacket->seek(sizeof(quint16));
if (silentFrame) {
if (!_isListeningToAudioStream) {
// if we have a silent frame and we're not listening then just send nothing and break out of here
return;
}
// write the number of silent samples so the audio-mixer can uphold timing
audioPacket->writePrimitive(SCRIPT_AUDIO_BUFFER_SAMPLES);
// use the orientation and position of this avatar for the source of this audio
audioPacket->writePrimitive(_avatarData->getPosition());
glm::quat headOrientation = _avatarData->getHeadOrientation();
audioPacket->writePrimitive(headOrientation);
}else if (nextSoundOutput) {
// assume scripted avatar audio is mono and set channel flag to zero
audioPacket->writePrimitive((quint8)0);
// use the orientation and position of this avatar for the source of this audio
audioPacket->writePrimitive(_avatarData->getPosition());
glm::quat headOrientation = _avatarData->getHeadOrientation();
audioPacket->writePrimitive(headOrientation);
// write the raw audio data
audioPacket->write(reinterpret_cast<const char*>(nextSoundOutput), numAvailableSamples * sizeof(int16_t));
}
// write audio packet to AudioMixer nodes
auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node){
// only send to nodes of type AudioMixer
if (node->getType() == NodeType::AudioMixer) {
// pack sequence number
quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++;
audioPacket->seek(0);
audioPacket->writePrimitive(sequence);
// send audio packet
nodeList->sendUnreliablePacket(*audioPacket, *node);
}
});
}
}
}
void Agent::aboutToFinish() {
_scriptEngine.stop();
_scriptEngine->stop();
_pingTimer->stop();
delete _pingTimer;

View file

@ -37,37 +37,53 @@ class Agent : public ThreadedAssignment {
public:
Agent(NLPacket& packet);
void setIsAvatar(bool isAvatar) { QMetaObject::invokeMethod(&_scriptEngine, "setIsAvatar", Q_ARG(bool, isAvatar)); }
bool isAvatar() const { return _scriptEngine.isAvatar(); }
bool isPlayingAvatarSound() const { return _scriptEngine.isPlayingAvatarSound(); }
bool isListeningToAudioStream() const { return _scriptEngine.isListeningToAudioStream(); }
void setIsListeningToAudioStream(bool isListeningToAudioStream)
{ _scriptEngine.setIsListeningToAudioStream(isListeningToAudioStream); }
void setIsAvatar(bool isAvatar);
bool isAvatar() const { return _isAvatar; }
bool isPlayingAvatarSound() const { return _avatarSound != NULL; }
bool isListeningToAudioStream() const { return _isListeningToAudioStream; }
void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; }
float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; }
virtual void aboutToFinish();
public slots:
void run();
void playAvatarSound(Sound* avatarSound) { _scriptEngine.setAvatarSound(avatarSound); }
void playAvatarSound(Sound* avatarSound) { setAvatarSound(avatarSound); }
private slots:
void handleAudioPacket(QSharedPointer<NLPacket> packet);
void handleOctreePacket(QSharedPointer<NLPacket> packet, SharedNodePointer senderNode);
void handleJurisdictionPacket(QSharedPointer<NLPacket> packet, SharedNodePointer senderNode);
void sendPingRequests();
void processAgentAvatarAndAudio(float deltaTime);
private:
ScriptEngine _scriptEngine;
ScriptEngine* _scriptEngine;
EntityEditPacketSender _entityEditSender;
EntityTreeHeadlessViewer _entityViewer;
QTimer* _pingTimer;
MixedAudioStream _receivedAudioStream;
float _lastReceivedAudioLoudness;
void setAvatarData(AvatarData* avatarData, const QString& objectName);
void setAvatarSound(Sound* avatarSound) { _avatarSound = avatarSound; }
void sendAvatarIdentityPacket();
void sendAvatarBillboardPacket();
AvatarData* _avatarData = nullptr;
bool _isListeningToAudioStream = false;
Sound* _avatarSound = nullptr;
int _numAvatarSoundSentBytes = 0;
bool _isAvatar = false;
QTimer* _avatarIdentityTimer = nullptr;
QTimer* _avatarBillboardTimer = nullptr;
QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers;
};
#endif // hifi_Agent_h

View file

@ -142,7 +142,7 @@ void EntityServer::pruneDeletedEntities() {
}
}
void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectionObject) {
bool EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectionObject) {
bool wantEditLogging = false;
readOptionBool(QString("wantEditLogging"), settingsSectionObject, wantEditLogging);
qDebug("wantEditLogging=%s", debug::valueOf(wantEditLogging));
@ -150,4 +150,6 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
tree->setWantEditLogging(wantEditLogging);
return true;
}

View file

@ -41,7 +41,7 @@ public:
virtual int sendSpecialPackets(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent);
virtual void entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode);
virtual void readAdditionalConfiguration(const QJsonObject& settingsSectionObject);
virtual bool readAdditionalConfiguration(const QJsonObject& settingsSectionObject) override;
public slots:
void pruneDeletedEntities();

View file

@ -242,7 +242,8 @@ bool OctreeQueryNode::updateCurrentViewFrustum() {
if (0.0f != getCameraAspectRatio() &&
0.0f != getCameraNearClip() &&
0.0f != getCameraFarClip()) {
0.0f != getCameraFarClip() &&
getCameraNearClip() != getCameraFarClip()) {
newestViewFrustum.setProjection(glm::perspective(
glm::radians(wideFOV), // hack
getCameraAspectRatio(),

View file

@ -887,7 +887,7 @@ bool OctreeServer::readOptionString(const QString& optionName, const QJsonObject
return optionAvailable;
}
void OctreeServer::readConfiguration() {
bool OctreeServer::readConfiguration() {
// if the assignment had a payload, read and parse that
if (getPayload().size() > 0) {
parsePayload();
@ -907,8 +907,11 @@ void OctreeServer::readConfiguration() {
loop.exec();
if (domainHandler.getSettingsObject().isEmpty()) {
qDebug() << "No settings object from domain-server.";
qDebug() << "Failed to retreive settings object from domain-server. Bailing on assignment.";
setFinished(true);
return false;
}
const QJsonObject& settingsObject = domainHandler.getSettingsObject();
QString settingsKey = getMyDomainSettingsKey();
QJsonObject settingsSectionObject = settingsObject[settingsKey].toObject();
@ -1017,7 +1020,7 @@ void OctreeServer::readConfiguration() {
packetsPerSecondTotalMax, _packetsTotalPerInterval);
readAdditionalConfiguration(settingsSectionObject);
return readAdditionalConfiguration(settingsSectionObject);
}
void OctreeServer::run() {
@ -1043,7 +1046,9 @@ void OctreeServer::run() {
commonInit(getMyLoggingServerTargetName(), getMyNodeType());
// read the configuration from either the payload or the domain server configuration
readConfiguration();
if (!readConfiguration()) {
return; // bailing on run, because readConfiguration failed
}
beforeRun(); // after payload has been processed
@ -1333,19 +1338,22 @@ void OctreeServer::sendStatsPacket() {
QJsonObject statsObject2;
statsObject2["data"] = dataObject1;
statsObject2["timing"] = timingArray1;
// Stats Object 3
QJsonObject dataArray2;
dataArray2["1. packetQueue"] = (double)_octreeInboundPacketProcessor->packetsToProcessCount();
dataArray2["2. totalPackets"] = (double)_octreeInboundPacketProcessor->getTotalPacketsProcessed();
dataArray2["3. totalElements"] = (double)_octreeInboundPacketProcessor->getTotalElementsProcessed();
QJsonObject timingArray2;
timingArray2["1. avgTransitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageTransitTimePerPacket();
timingArray2["2. avgProcessTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerPacket();
timingArray2["3. avgLockWaitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerPacket();
timingArray2["4. avgProcessTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerElement();
timingArray2["5. avgLockWaitTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerElement();
// Stats Object 3
if (_octreeInboundPacketProcessor) {
dataArray2["1. packetQueue"] = (double)_octreeInboundPacketProcessor->packetsToProcessCount();
dataArray2["2. totalPackets"] = (double)_octreeInboundPacketProcessor->getTotalPacketsProcessed();
dataArray2["3. totalElements"] = (double)_octreeInboundPacketProcessor->getTotalElementsProcessed();
timingArray2["1. avgTransitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageTransitTimePerPacket();
timingArray2["2. avgProcessTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerPacket();
timingArray2["3. avgLockWaitTimePerPacket"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerPacket();
timingArray2["4. avgProcessTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageProcessTimePerElement();
timingArray2["5. avgLockWaitTimePerElement"] = (double)_octreeInboundPacketProcessor->getAverageLockWaitTimePerElement();
}
QJsonObject statsObject3;
statsObject3["data"] = dataArray2;

View file

@ -135,8 +135,8 @@ protected:
bool readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result);
bool readOptionInt(const QString& optionName, const QJsonObject& settingsSectionObject, int& result);
bool readOptionString(const QString& optionName, const QJsonObject& settingsSectionObject, QString& result);
void readConfiguration();
virtual void readAdditionalConfiguration(const QJsonObject& settingsSectionObject) { };
bool readConfiguration();
virtual bool readAdditionalConfiguration(const QJsonObject& settingsSectionObject) { return true; };
void parsePayload();
void initHTTPManager(int port);
void resetSendingStats();

View file

@ -4,7 +4,7 @@
// Created by Eric Levin on 9/2/15
// Copyright 2015 High Fidelity, Inc.
//
// Grab's physically moveable entities with the hydra- works for either near or far objects. User can also grab a far away object and drag it towards them by pressing the "4" button on either the left or ride controller.
// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -13,6 +13,8 @@
Script.include("../libraries/utils.js");
var RADIUS_FACTOR = 4;
var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK");
var rightTriggerAction = RIGHT_HAND_CLICK;
@ -45,7 +47,7 @@ var INTERSECT_COLOR = {
blue: 10
};
var GRAB_RADIUS = 1.5;
var GRAB_RADIUS = 0.3;
var GRAB_COLOR = {
red: 250,
@ -58,12 +60,11 @@ var DISTANCE_HOLD_THRESHOLD = 0.8;
var right4Action = 18;
var left4Action = 17;
var TRACTOR_BEAM_VELOCITY_THRESHOLD = 0.5;
var RIGHT = 1;
var LEFT = 0;
var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right");
var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left");
var startTime = Date.now();
//Need to wait before calling these methods for some reason...
@ -78,56 +79,67 @@ function controller(side, triggerAction, pullAction, hand) {
this.getHandPosition = MyAvatar.getRightPalmPosition;
this.getHandRotation = MyAvatar.getRightPalmRotation;
} else {
this.getHandPosition = MyAvatar.getLeftPalmPosition;
this.getHandRotation = MyAvatar.getLeftPalmRotation;
}
this.triggerAction = triggerAction;
this.pullAction = pullAction;
this.actionID = null;
this.tractorBeamActive = false;
this.distanceHolding = false;
this.closeGrabbing = false;
this.triggerValue = 0;
this.prevTriggerValue = 0;
this.palm = 2 * side;
this.tip = 2 * side + 1;
this.pointer = Entities.addEntity({
type: "Line",
name: "pointer",
color: NO_INTERSECT_COLOR,
dimensions: {
x: 1000,
y: 1000,
z: 1000
},
visible: false,
lifetime: LIFETIME
});
this.pointer = null;
}
controller.prototype.updateLine = function() {
var handPosition = Controller.getSpatialControlPosition(this.palm);
var direction = Controller.getSpatialControlNormal(this.tip);
if (this.pointer != null) {
if (Entities.getEntityProperties(this.pointer).id != this.poitner) {
this.pointer == null;
}
}
Entities.editEntity(this.pointer, {
position: handPosition,
linePoints: [
ZERO_VEC,
Vec3.multiply(direction, LINE_LENGTH)
]
});
if (this.pointer == null) {
this.lineCreationTime = Date.now();
this.pointer = Entities.addEntity({
type: "Line",
name: "pointer",
color: NO_INTERSECT_COLOR,
dimensions: {
x: 1000,
y: 1000,
z: 1000
},
visible: true,
lifetime: LIFETIME
});
}
var handPosition = this.getHandPosition();
var direction = Quat.getUp(this.getHandRotation());
//only check if we havent already grabbed an object
if (this.distanceHolding) {
Entities.editEntity(this.pointer, {
position: handPosition,
linePoints: [ ZERO_VEC, Vec3.subtract(Entities.getEntityProperties(this.grabbedEntity).position, handPosition) ],
lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME
});
return;
}
//move origin a bit away from hand so nothing gets in way
var origin = Vec3.sum(handPosition, direction);
if (this.checkForIntersections(origin, direction)) {
Entities.editEntity(this.pointer, {
position: handPosition,
linePoints: [ ZERO_VEC, Vec3.multiply(direction, LINE_LENGTH) ],
lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME
});
if (this.checkForIntersections(handPosition, direction)) {
Entities.editEntity(this.pointer, {
color: INTERSECT_COLOR,
});
@ -144,7 +156,7 @@ controller.prototype.checkPointer = function() {
Script.setTimeout(function() {
var props = Entities.getEntityProperties(self.pointer);
Entities.editEntity(self.pointer, {
lifetime: props.age + EXTRA_TIME + LIFETIME
lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME
});
self.checkPointer();
}, POINTER_CHECK_TIME);
@ -172,28 +184,54 @@ controller.prototype.checkForIntersections = function(origin, direction) {
return false;
}
controller.prototype.attemptMove = function() {
if (this.tractorBeamActive) {
return;
}
if (this.grabbedEntity || this.distanceHolding) {
var handPosition = Controller.getSpatialControlPosition(this.palm);
var direction = Controller.getSpatialControlNormal(this.tip);
var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm));
var newPosition = Vec3.sum(handPosition, Vec3.multiply(direction, this.distanceToEntity))
this.distanceHolding = true;
if (this.actionID === null) {
this.currentObjectPosition = Entities.getEntityProperties(this.grabbedEntity).position;
this.currentObjectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation;
this.handPreviousPosition = handPosition;
this.handPreviousRotation = handRotation;
this.actionID = Entities.addAction("spring", this.grabbedEntity, {
targetPosition: newPosition,
linearTimeScale: .1
targetPosition: this.currentObjectPosition,
linearTimeScale: .1,
targetRotation: this.currentObjectRotation,
angularTimeScale: .1
});
} else {
var radius = Math.max(Vec3.distance(this.currentObjectPosition, handPosition) * RADIUS_FACTOR, 1.0);
var handMoved = Vec3.subtract(handPosition, this.handPreviousPosition);
this.handPreviousPosition = handPosition;
var superHandMoved = Vec3.multiply(handMoved, radius);
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved);
// ---------------- this tracks hand rotation
// var handChange = Quat.multiply(handRotation, Quat.inverse(this.handPreviousRotation));
// this.handPreviousRotation = handRotation;
// this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
// ----------------
// ---------------- this doubles hand rotation
var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, 2.0),
Quat.inverse(this.handPreviousRotation));
this.handPreviousRotation = handRotation;
this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
// ----------------
Entities.updateAction(this.grabbedEntity, this.actionID, {
targetPosition: newPosition
targetPosition: this.currentObjectPosition, linearTimeScale: .1,
targetRotation: this.currentObjectRotation, angularTimeScale: .1
});
}
}
}
controller.prototype.showPointer = function() {
@ -218,19 +256,10 @@ controller.prototype.letGo = function() {
this.grabbedEntity = null;
this.actionID = null;
this.distanceHolding = false;
this.tractorBeamActive = false;
this.checkForEntityArrival = false;
this.closeGrabbing = false;
}
controller.prototype.update = function() {
if (this.tractorBeamActive && this.checkForEntityArrival) {
var entityVelocity = Entities.getEntityProperties(this.grabbedEntity).velocity
if (Vec3.length(entityVelocity) < TRACTOR_BEAM_VELOCITY_THRESHOLD) {
this.letGo();
}
return;
}
this.triggerValue = Controller.getActionValue(this.triggerAction);
if (this.triggerValue > SHOW_LINE_THRESHOLD && this.prevTriggerValue < SHOW_LINE_THRESHOLD) {
//First check if an object is within close range and then run the close grabbing logic
@ -253,7 +282,6 @@ controller.prototype.update = function() {
this.attemptMove();
}
this.prevTriggerValue = this.triggerValue;
}
@ -331,27 +359,6 @@ controller.prototype.deactivateEntity = function(entity) {
setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data);
}
controller.prototype.onActionEvent = function(action, state) {
if (this.pullAction === action && state === 1) {
if (this.actionID !== null) {
var self = this;
this.tractorBeamActive = true;
//We need to wait a bit before checking for entity arrival at target destination (meaning checking for velocity being close to some
//low threshold) because otherwise we'll think the entity has arrived before its even really gotten moving!
Script.setTimeout(function() {
self.checkForEntityArrival = true;
}, 500);
var handPosition = Controller.getSpatialControlPosition(this.palm);
var direction = Vec3.normalize(Controller.getSpatialControlNormal(this.tip));
//move final destination along line a bit, so it doesnt hit avatar hand
Entities.updateAction(this.grabbedEntity, this.actionID, {
targetPosition: Vec3.sum(handPosition, Vec3.multiply(3, direction))
});
}
}
}
controller.prototype.cleanup = function() {
Entities.deleteEntity(this.pointer);
if (this.grabbedEntity) {
@ -364,13 +371,6 @@ function update() {
leftController.update();
}
function onActionEvent(action, state) {
rightController.onActionEvent(action, state);
leftController.onActionEvent(action, state);
}
function cleanup() {
rightController.cleanup();
leftController.cleanup();
@ -378,4 +378,3 @@ function cleanup() {
Script.scriptEnding.connect(cleanup);
Script.update.connect(update)
Controller.actionEvent.connect(onActionEvent);

View file

@ -2868,8 +2868,8 @@ void Application::update(float deltaTime) {
UserInputMapper::PoseValue leftHand = userInputMapper->getPoseState(UserInputMapper::LEFT_HAND);
UserInputMapper::PoseValue rightHand = userInputMapper->getPoseState(UserInputMapper::RIGHT_HAND);
Hand* hand = DependencyManager::get<AvatarManager>()->getMyAvatar()->getHand();
setPalmData(hand, leftHand, deltaTime, LEFT_HAND_INDEX);
setPalmData(hand, rightHand, deltaTime, RIGHT_HAND_INDEX);
setPalmData(hand, leftHand, deltaTime, LEFT_HAND_INDEX, userInputMapper->getActionState(UserInputMapper::LEFT_HAND_CLICK));
setPalmData(hand, rightHand, deltaTime, RIGHT_HAND_INDEX, userInputMapper->getActionState(UserInputMapper::RIGHT_HAND_CLICK));
if (Menu::getInstance()->isOptionChecked(MenuOption::HandMouseInput)) {
emulateMouse(hand, userInputMapper->getActionState(UserInputMapper::LEFT_HAND_CLICK),
userInputMapper->getActionState(UserInputMapper::SHIFT), LEFT_HAND_INDEX);
@ -4026,8 +4026,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
AvatarManager::registerMetaTypes(scriptEngine);
// hook our avatar and avatar hash map object into this script engine
scriptEngine->setAvatarData(_myAvatar, "MyAvatar"); // leave it as a MyAvatar class to expose thrust features
scriptEngine->setAvatarHashMap(DependencyManager::get<AvatarManager>().data(), "AvatarList");
scriptEngine->registerGlobalObject("MyAvatar", _myAvatar);
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());
scriptEngine->registerGlobalObject("Camera", &_myCamera);
@ -4051,9 +4051,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
scriptEngine->registerGlobalObject("Desktop", DependencyManager::get<DesktopScriptingInterface>().data());
QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", DependencyManager::get<WindowScriptingInterface>().data());
scriptEngine->registerGlobalObject("Window", DependencyManager::get<WindowScriptingInterface>().data());
scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
LocationScriptingInterface::locationSetter, windowValue);
LocationScriptingInterface::locationSetter, "Window");
// register `location` on the global object.
scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
LocationScriptingInterface::locationSetter);
@ -4083,9 +4083,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
scriptEngine->registerGlobalObject("Paths", DependencyManager::get<PathUtils>().data());
QScriptValue hmdInterface = scriptEngine->registerGlobalObject("HMD", &HMDScriptingInterface::getInstance());
scriptEngine->registerFunction(hmdInterface, "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0);
scriptEngine->registerFunction(hmdInterface, "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0);
scriptEngine->registerGlobalObject("HMD", &HMDScriptingInterface::getInstance());
scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0);
scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0);
scriptEngine->registerGlobalObject("Scene", DependencyManager::get<SceneScriptingInterface>().data());
@ -4094,31 +4094,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
#ifdef HAVE_RTMIDI
scriptEngine->registerGlobalObject("MIDI", &MIDIManager::getInstance());
#endif
// TODO: Consider moving some of this functionality into the ScriptEngine class instead. It seems wrong that this
// work is being done in the Application class when really these dependencies are more related to the ScriptEngine's
// implementation
QThread* workerThread = new QThread(this);
QString scriptEngineName = QString("Script Thread:") + scriptEngine->getFilename();
workerThread->setObjectName(scriptEngineName);
// when the worker thread is started, call our engine's run..
connect(workerThread, &QThread::started, scriptEngine, &ScriptEngine::run);
// when the thread is terminated, add both scriptEngine and thread to the deleteLater queue
connect(scriptEngine, &ScriptEngine::doneRunning, scriptEngine, &ScriptEngine::deleteLater);
connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater);
// tell the thread to stop when the script engine is done
connect(scriptEngine, &ScriptEngine::destroyed, workerThread, &QThread::quit);
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::nodeKilled, scriptEngine, &ScriptEngine::nodeKilled);
scriptEngine->moveToThread(workerThread);
// Starts an event loop, and emits workerThread->started()
workerThread->start();
}
void Application::initializeAcceptedFiles() {
@ -4264,11 +4239,14 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser
scriptEngine->setUserLoaded(isUserLoaded);
if (scriptFilename.isNull()) {
// This appears to be the script engine used by the script widget's evaluation window before the file has been saved...
// this had better be the script editor (we should de-couple so somebody who thinks they are loading a script
// doesn't just get an empty script engine)
// we can complete setup now since there isn't a script we have to load
registerScriptEngineWithApplicationServices(scriptEngine);
scriptEngine->runInThread();
} else {
// connect to the appropriate signals of this script engine
connect(scriptEngine, &ScriptEngine::scriptLoaded, this, &Application::handleScriptEngineLoaded);
@ -4290,6 +4268,7 @@ void Application::reloadScript(const QString& scriptName, bool isUserLoaded) {
loadScript(scriptName, isUserLoaded, false, false, true);
}
// FIXME - change to new version of ScriptCache loading notification
void Application::handleScriptEngineLoaded(const QString& scriptFilename) {
ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(sender());
@ -4299,8 +4278,10 @@ void Application::handleScriptEngineLoaded(const QString& scriptFilename) {
// register our application services and set it off on its own thread
registerScriptEngineWithApplicationServices(scriptEngine);
scriptEngine->runInThread();
}
// FIXME - change to new version of ScriptCache loading notification
void Application::handleScriptLoadError(const QString& scriptFilename) {
qCDebug(interfaceapp) << "Application::loadScript(), script failed to load...";
QMessageBox::warning(getWindow(), "Error Loading Script", scriptFilename + " failed to load.");
@ -4970,7 +4951,7 @@ mat4 Application::getHMDSensorPose() const {
return mat4();
}
void Application::setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index) {
void Application::setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index, float triggerValue) {
PalmData* palm;
bool foundHand = false;
for (size_t j = 0; j < hand->getNumPalms(); j++) {
@ -5040,6 +5021,7 @@ void Application::setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float
palm->setTipVelocity(glm::vec3(0.0f));
}
palm->setTipPosition(newTipPosition);
palm->setTrigger(triggerValue);
}
void Application::emulateMouse(Hand* hand, float click, float shift, int index) {

View file

@ -485,7 +485,7 @@ private:
void update(float deltaTime);
void setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index);
void setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float deltaTime, int index, float triggerValue);
void emulateMouse(Hand* hand, float click, float shift, int index);
// Various helper functions called during update()

View file

@ -1296,10 +1296,13 @@ void MyAvatar::initAnimGraph() {
// ik-avatar.json
// https://gist.github.com/hyperlogic/e58e0a24cc341ad5d060
//
// ik-avatar-hands.json
// https://gist.githubusercontent.com/hyperlogic/04a02c47eb56d8bfaebb
//
// or run a local web-server
// python -m SimpleHTTPServer&
// auto graphUrl = QUrl("http://localhost:8000/avatar.json");
auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/e58e0a24cc341ad5d060/raw/8f824da2908fd89ad1befadd1d8f5d7b3b6efa66/ik-avatar.json");
//auto graphUrl = QUrl("http://localhost:8000/avatar.json");
auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/04a02c47eb56d8bfaebb/raw/883c7ce8e75ad3f72a0d513c317fe4b74a41c3b8/ik-avatar-hands.json");
_rig->initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry());
}

View file

@ -96,6 +96,17 @@ void SkeletonModel::initJointStates(QVector<JointState> states) {
emit skeletonLoaded();
}
static const PalmData* getPalmWithIndex(Hand* hand, int index) {
const PalmData* palm = nullptr;
for (size_t j = 0; j < hand->getNumPalms(); j++) {
if (hand->getPalms()[j].getSixenseID() == index) {
palm = &(hand->getPalms()[j]);
break;
}
}
return palm;
}
const float PALM_PRIORITY = DEFAULT_PRIORITY;
// Called within Model::simulate call, below.
void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
@ -108,34 +119,59 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
const FBXGeometry& geometry = _geometry->getFBXGeometry();
Rig::HeadParameters params;
params.modelRotation = getRotation();
params.modelTranslation = getTranslation();
params.enableLean = qApp->getAvatarUpdater()->isHMDMode() && !myAvatar->getStandingHMDSensorMode();
params.leanSideways = head->getFinalLeanSideways();
params.leanForward = head->getFinalLeanForward();
params.torsoTwist = head->getTorsoTwist();
params.localHeadOrientation = head->getFinalOrientationInLocalFrame();
params.localHeadPitch = head->getFinalPitch();
params.localHeadYaw = head->getFinalYaw();
params.localHeadRoll = head->getFinalRoll();
params.isInHMD = qApp->getAvatarUpdater()->isHMDMode();
Rig::HeadParameters headParams;
headParams.modelRotation = getRotation();
headParams.modelTranslation = getTranslation();
headParams.enableLean = qApp->getAvatarUpdater()->isHMDMode() && !myAvatar->getStandingHMDSensorMode();
headParams.leanSideways = head->getFinalLeanSideways();
headParams.leanForward = head->getFinalLeanForward();
headParams.torsoTwist = head->getTorsoTwist();
headParams.localHeadOrientation = head->getFinalOrientationInLocalFrame();
headParams.localHeadPitch = head->getFinalPitch();
headParams.localHeadYaw = head->getFinalYaw();
headParams.localHeadRoll = head->getFinalRoll();
headParams.isInHMD = qApp->getAvatarUpdater()->isHMDMode();
// get HMD position from sensor space into world space, and back into model space
glm::mat4 worldToModel = glm::inverse(createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()));
glm::vec3 yAxis(0.0f, 1.0f, 0.0f);
glm::vec3 hmdPosition = glm::angleAxis((float)M_PI, yAxis) * transformPoint(worldToModel * myAvatar->getSensorToWorldMatrix(), myAvatar->getHMDSensorPosition());
params.localHeadPosition = hmdPosition;
headParams.localHeadPosition = hmdPosition;
params.worldHeadOrientation = head->getFinalOrientationInWorldFrame();
params.eyeLookAt = head->getLookAtPosition();
params.eyeSaccade = head->getSaccade();
params.leanJointIndex = geometry.leanJointIndex;
params.neckJointIndex = geometry.neckJointIndex;
params.leftEyeJointIndex = geometry.leftEyeJointIndex;
params.rightEyeJointIndex = geometry.rightEyeJointIndex;
headParams.worldHeadOrientation = head->getFinalOrientationInWorldFrame();
headParams.eyeLookAt = head->getLookAtPosition();
headParams.eyeSaccade = head->getSaccade();
headParams.leanJointIndex = geometry.leanJointIndex;
headParams.neckJointIndex = geometry.neckJointIndex;
headParams.leftEyeJointIndex = geometry.leftEyeJointIndex;
headParams.rightEyeJointIndex = geometry.rightEyeJointIndex;
_rig->updateFromHeadParameters(headParams, deltaTime);
Rig::HandParameters handParams;
const PalmData* leftPalm = getPalmWithIndex(myAvatar->getHand(), LEFT_HAND_INDEX);
if (leftPalm && leftPalm->isActive()) {
handParams.isLeftEnabled = true;
handParams.leftPosition = leftPalm->getRawPosition();
handParams.leftOrientation = leftPalm->getRawRotation();
handParams.leftTrigger = leftPalm->getTrigger();
} else {
handParams.isLeftEnabled = false;
}
const PalmData* rightPalm = getPalmWithIndex(myAvatar->getHand(), RIGHT_HAND_INDEX);
if (rightPalm && rightPalm->isActive()) {
handParams.isRightEnabled = true;
handParams.rightPosition = rightPalm->getRawPosition();
handParams.rightOrientation = rightPalm->getRawRotation();
handParams.rightTrigger = rightPalm->getTrigger();
} else {
handParams.isRightEnabled = false;
}
_rig->updateFromHandParameters(handParams, deltaTime);
_rig->updateFromHeadParameters(params);
} else {
// This is a little more work than we really want.
//

View file

@ -172,7 +172,12 @@ static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUr
qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
node->addChild(loadNode(childValue.toObject(), jsonUrl));
AnimNode::Pointer child = loadNode(childValue.toObject(), jsonUrl);
if (child) {
node->addChild(child);
} else {
return nullptr;
}
}
if ((animNodeTypeToProcessFunc(type))(node, dataObj, id, jsonUrl)) {
@ -232,13 +237,15 @@ static const char* boneSetStrings[AnimOverlay::NumBoneSets] = {
"fullBody",
"upperBody",
"lowerBody",
"rightArm",
"leftArm",
"rightArm",
"aboveTheHead",
"belowTheHead",
"headOnly",
"spineOnly",
"empty"
"empty",
"leftHand",
"rightHand"
};
static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) {

View file

@ -26,12 +26,14 @@ void AnimOverlay::buildBoneSet(BoneSet boneSet) {
case FullBodyBoneSet: buildFullBodyBoneSet(); break;
case UpperBodyBoneSet: buildUpperBodyBoneSet(); break;
case LowerBodyBoneSet: buildLowerBodyBoneSet(); break;
case RightArmBoneSet: buildRightArmBoneSet(); break;
case LeftArmBoneSet: buildLeftArmBoneSet(); break;
case RightArmBoneSet: buildRightArmBoneSet(); break;
case AboveTheHeadBoneSet: buildAboveTheHeadBoneSet(); break;
case BelowTheHeadBoneSet: buildBelowTheHeadBoneSet(); break;
case HeadOnlyBoneSet: buildHeadOnlyBoneSet(); break;
case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break;
case LeftHandBoneSet: buildLeftHandBoneSet(); break;
case RightHandBoneSet: buildRightHandBoneSet(); break;
default:
case EmptyBoneSet: buildEmptyBoneSet(); break;
}
@ -110,15 +112,6 @@ void AnimOverlay::buildLowerBodyBoneSet() {
_boneSetVec[hipsJoint] = 0.0f;
}
void AnimOverlay::buildRightArmBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int rightShoulderJoint = _skeleton->nameToJointIndex("RightShoulder");
for_each_child_joint(_skeleton, rightShoulderJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
void AnimOverlay::buildLeftArmBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
@ -128,6 +121,15 @@ void AnimOverlay::buildLeftArmBoneSet() {
});
}
void AnimOverlay::buildRightArmBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int rightShoulderJoint = _skeleton->nameToJointIndex("RightShoulder");
for_each_child_joint(_skeleton, rightShoulderJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
void AnimOverlay::buildAboveTheHeadBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
@ -168,13 +170,31 @@ void AnimOverlay::buildEmptyBoneSet() {
}
}
void AnimOverlay::buildLeftHandBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int headJoint = _skeleton->nameToJointIndex("LeftHand");
for_each_child_joint(_skeleton, headJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
void AnimOverlay::buildRightHandBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int headJoint = _skeleton->nameToJointIndex("RightHand");
for_each_child_joint(_skeleton, headJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
// for AnimDebugDraw rendering
const AnimPoseVec& AnimOverlay::getPosesInternal() const {
return _poses;
}
void AnimOverlay::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) {
_skeleton = skeleton;
AnimNode::setSkeletonInternal(skeleton);
// we have to re-build the bone set when the skeleton changes.
buildBoneSet(_boneSet);

View file

@ -28,14 +28,16 @@ public:
FullBodyBoneSet = 0,
UpperBodyBoneSet,
LowerBodyBoneSet,
RightArmBoneSet,
LeftArmBoneSet,
RightArmBoneSet,
AboveTheHeadBoneSet,
BelowTheHeadBoneSet,
HeadOnlyBoneSet,
SpineOnlyBoneSet,
EmptyBoneSet,
NumBoneSets,
LeftHandBoneSet,
RightHandBoneSet,
NumBoneSets
};
AnimOverlay(const std::string& id, BoneSet boneSet, float alpha);
@ -64,13 +66,15 @@ public:
void buildFullBodyBoneSet();
void buildUpperBodyBoneSet();
void buildLowerBodyBoneSet();
void buildRightArmBoneSet();
void buildLeftArmBoneSet();
void buildRightArmBoneSet();
void buildAboveTheHeadBoneSet();
void buildBelowTheHeadBoneSet();
void buildHeadOnlyBoneSet();
void buildSpineOnlyBoneSet();
void buildEmptyBoneSet();
void buildLeftHandBoneSet();
void buildRightHandBoneSet();
// no copies
AnimOverlay(const AnimOverlay&) = delete;

View file

@ -952,7 +952,7 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) {
return _jointStates[jointIndex].getDefaultRotationInParentFrame();
}
void Rig::updateFromHeadParameters(const HeadParameters& params) {
void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) {
if (params.enableLean) {
updateLeanJoint(params.leanJointIndex, params.leanSideways, params.leanForward, params.torsoTwist);
}
@ -1048,6 +1048,52 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm
}
}
void Rig::updateFromHandParameters(const HandParameters& params, float dt) {
if (_enableAnimGraph && _animSkeleton) {
// set leftHand grab vars
_animVars.set("isLeftHandIdle", false);
_animVars.set("isLeftHandPoint", false);
_animVars.set("isLeftHandGrab", false);
// Split the trigger range into three zones.
bool rampOut = false;
if (params.leftTrigger > 0.6666f) {
_animVars.set("isLeftHandGrab", true);
} else if (params.leftTrigger > 0.3333f) {
_animVars.set("isLeftHandPoint", true);
} else {
_animVars.set("isLeftHandIdle", true);
rampOut = true;
}
const float OVERLAY_RAMP_OUT_SPEED = 6.0f; // ramp in and out over 1/6th of a sec
_leftHandOverlayAlpha = glm::clamp(_leftHandOverlayAlpha + (rampOut ? -1.0f : 1.0f) * OVERLAY_RAMP_OUT_SPEED * dt, 0.0f, 1.0f);
_animVars.set("leftHandOverlayAlpha", _leftHandOverlayAlpha);
_animVars.set("leftHandGrabBlend", params.leftTrigger);
// set leftHand grab vars
_animVars.set("isRightHandIdle", false);
_animVars.set("isRightHandPoint", false);
_animVars.set("isRightHandGrab", false);
// Split the trigger range into three zones
rampOut = false;
if (params.rightTrigger > 0.6666f) {
_animVars.set("isRightHandGrab", true);
} else if (params.rightTrigger > 0.3333f) {
_animVars.set("isRightHandPoint", true);
} else {
_animVars.set("isRightHandIdle", true);
rampOut = true;
}
_rightHandOverlayAlpha = glm::clamp(_rightHandOverlayAlpha + (rampOut ? -1.0f : 1.0f) * OVERLAY_RAMP_OUT_SPEED * dt, 0.0f, 1.0f);
_animVars.set("rightHandOverlayAlpha", _rightHandOverlayAlpha);
_animVars.set("rightHandGrabBlend", params.rightTrigger);
}
}
void Rig::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) {
if (!_enableAnimGraph) {
return;

View file

@ -76,6 +76,17 @@ public:
void dump() const;
};
struct HandParameters {
bool isLeftEnabled;
bool isRightEnabled;
glm::vec3 leftPosition = glm::vec3();
glm::quat leftOrientation = glm::quat();
glm::vec3 rightPosition = glm::vec3();
glm::quat rightOrientation = glm::quat();
float leftTrigger = 0.0f;
float rightTrigger = 0.0f;
};
virtual ~Rig() {}
RigPointer getRigPointer() { return shared_from_this(); }
@ -168,10 +179,12 @@ public:
void setEnableAnimGraph(bool isEnabled) { _enableAnimGraph = isEnabled; }
bool getEnableAnimGraph() const { return _enableAnimGraph; }
void updateFromHeadParameters(const HeadParameters& params);
void updateFromHeadParameters(const HeadParameters& params, float dt);
void updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& modelTranslation, const glm::quat& modelRotation,
const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade = glm::vec3(0.0f));
void updateFromHandParameters(const HandParameters& params, float dt);
virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation,
float scale, float priority) = 0;
@ -215,6 +228,8 @@ public:
Move
};
RigRole _state = RigRole::Idle;
float _leftHandOverlayAlpha = 0.0f;
float _rightHandOverlayAlpha = 0.0f;
};
#endif /* defined(__hifi__Rig__) */

View file

@ -72,7 +72,7 @@ SwingTwistConstraint::SwingTwistConstraint() :
_swingLimitFunction(),
_minTwist(-PI),
_maxTwist(PI),
_lastTwistBoundary(0)
_lastTwistBoundary(LAST_CLAMP_NO_BOUNDARY)
{
}
@ -249,3 +249,6 @@ bool SwingTwistConstraint::apply(glm::quat& rotation) const {
return false;
}
void SwingTwistConstraint::clearHistory() {
_lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY;
}

View file

@ -73,6 +73,9 @@ public:
/// \return reference to SwingLimitFunction instance for unit-testing
const SwingLimitFunction& getSwingLimitFunction() const { return _swingLimitFunction; }
/// \brief exposed for unit testing
void clearHistory();
protected:
SwingLimitFunction _swingLimitFunction;
float _minTwist;

View file

@ -125,12 +125,12 @@ public:
return;
}
const int scale = (2 << ((8 * sizeof(int16_t)) - 1));
const int scale = (1 << ((8 * sizeof(int16_t)) - 1));
// de-interleave and convert int16_t to float32 (normalized to -1. ... 1.)
for (uint32_t i = 0; i < frameCount; ++i) {
for (uint32_t j = 0; j < _channelCount; ++j) {
_buffer[j][i] = ((float)(*in++)) / scale;
_buffer[j][i] = ((float)(*in++)) * (1.0f / scale);
}
}

View file

@ -49,7 +49,6 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf
OctreeRenderer(),
_wantScripts(wantScripts),
_entitiesScriptEngine(NULL),
_sandboxScriptEngine(NULL),
_lastMouseEventValid(false),
_viewState(viewState),
_scriptingServices(scriptingServices),
@ -77,23 +76,11 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf
EntityTreeRenderer::~EntityTreeRenderer() {
// NOTE: we don't need to delete _entitiesScriptEngine because it is registered with the application and has a
// signal tied to call it's deleteLater on doneRunning
if (_sandboxScriptEngine) {
// TODO: consider reworking how _sandboxScriptEngine is managed. It's treated differently than _entitiesScriptEngine
// because we don't call registerScriptEngineWithApplicationServices() for it. This implementation is confusing and
// potentially error prone because it's not a full fledged ScriptEngine that has been fully connected to the
// application. We did this so that scripts that were ill-formed could be evaluated but not execute against the
// application services. But this means it's shutdown behavior is different from other ScriptEngines
delete _sandboxScriptEngine;
_sandboxScriptEngine = NULL;
}
}
void EntityTreeRenderer::clear() {
leaveAllEntities();
foreach (const EntityItemID& entityID, _entityScripts.keys()) {
checkAndCallUnload(entityID);
}
_entityScripts.clear();
_entitiesScriptEngine->unloadAllEntityScripts();
auto scene = _viewState->getMain3DScene();
render::PendingChanges pendingChanges;
@ -113,10 +100,9 @@ void EntityTreeRenderer::init() {
if (_wantScripts) {
_entitiesScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities",
_scriptingServices->getControllerScriptingInterface(), false);
_scriptingServices->getControllerScriptingInterface());
_scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine);
_sandboxScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities Sandbox", NULL, false);
_entitiesScriptEngine->runInThread();
}
// make sure our "last avatar position" is something other than our current position, so that on our
@ -134,176 +120,6 @@ void EntityTreeRenderer::shutdown() {
_shuttingDown = true;
}
void EntityTreeRenderer::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) {
if (_waitingOnPreload.contains(url)) {
QList<EntityItemID> entityIDs = _waitingOnPreload.values(url);
_waitingOnPreload.remove(url);
foreach(EntityItemID entityID, entityIDs) {
checkAndCallPreload(entityID);
}
}
}
void EntityTreeRenderer::errorInLoadingScript(const QUrl& url) {
if (_waitingOnPreload.contains(url)) {
_waitingOnPreload.remove(url);
}
}
QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID, bool isPreload, bool reload) {
EntityItemPointer entity = std::static_pointer_cast<EntityTree>(_tree)->findEntityByEntityItemID(entityItemID);
return loadEntityScript(entity, isPreload, reload);
}
QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& urlOut,
bool& reload) {
isPending = false;
QUrl url(scriptMaybeURLorText);
// If the url is not valid, this must be script text...
// We document "direct injection" scripts as starting with "(function...", and that would never be a valid url.
// But QUrl thinks it is.
if (!url.isValid() || scriptMaybeURLorText.startsWith("(")) {
isURL = false;
return scriptMaybeURLorText;
}
isURL = true;
urlOut = url;
QString scriptContents; // assume empty
// if the scheme length is one or lower, maybe they typed in a file, let's try
const int WINDOWS_DRIVE_LETTER_SIZE = 1;
if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) {
url = QUrl::fromLocalFile(scriptMaybeURLorText);
}
// ok, let's see if it's valid... and if so, load it
if (url.isValid()) {
if (url.scheme() == "file") {
QString fileName = url.toLocalFile();
QFile scriptFile(fileName);
if (scriptFile.open(QFile::ReadOnly | QFile::Text)) {
qCDebug(entitiesrenderer) << "Loading file:" << fileName;
QTextStream in(&scriptFile);
scriptContents = in.readAll();
} else {
qCDebug(entitiesrenderer) << "ERROR Loading file:" << fileName;
}
} else {
auto scriptCache = DependencyManager::get<ScriptCache>();
if (!scriptCache->isInBadScriptList(url)) {
scriptContents = scriptCache->getScript(url, this, isPending, reload);
}
}
}
return scriptContents;
}
QScriptValue EntityTreeRenderer::loadEntityScript(EntityItemPointer entity, bool isPreload, bool reload) {
if (_shuttingDown) {
return QScriptValue(); // since we're shutting down, we don't load any more scripts
}
if (!entity) {
return QScriptValue(); // no entity...
}
// NOTE: we keep local variables for the entityID and the script because
// below in loadScriptContents() it's possible for us to execute the
// application event loop, which may cause our entity to be deleted on
// us. We don't really need access the entity after this point, can
// can accomplish all we need to here with just the script "text" and the ID.
EntityItemID entityID = entity->getEntityItemID();
QString entityScript = entity->getScript();
if (_entityScripts.contains(entityID)) {
EntityScriptDetails details = _entityScripts[entityID];
// check to make sure our script text hasn't changed on us since we last loaded it and we're not redownloading it
if (details.scriptText == entityScript && !reload) {
return details.scriptObject; // previously loaded
}
// if we got here, then we previously loaded a script, but the entity's script value
// has changed and so we need to reload it.
_entityScripts.remove(entityID);
}
if (entityScript.isEmpty()) {
return QScriptValue(); // no script
}
bool isURL = false; // loadScriptContents() will tell us if this is a URL or just text.
bool isPending = false;
QUrl url;
QString scriptContents = loadScriptContents(entityScript, isURL, isPending, url, reload);
if (isPending && isPreload && isURL) {
_waitingOnPreload.insert(url, entityID);
}
auto scriptCache = DependencyManager::get<ScriptCache>();
if (isURL && scriptCache->isInBadScriptList(url)) {
return QScriptValue(); // no script contents...
}
if (scriptContents.isEmpty()) {
return QScriptValue(); // no script contents...
}
QScriptSyntaxCheckResult syntaxCheck = QScriptEngine::checkSyntax(scriptContents);
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
qCDebug(entitiesrenderer) << "EntityTreeRenderer::loadEntityScript() entity:" << entityID;
qCDebug(entitiesrenderer) << " " << syntaxCheck.errorMessage() << ":"
<< syntaxCheck.errorLineNumber() << syntaxCheck.errorColumnNumber();
qCDebug(entitiesrenderer) << " SCRIPT:" << entityScript;
scriptCache->addScriptToBadScriptList(url);
return QScriptValue(); // invalid script
}
if (isURL) {
_entitiesScriptEngine->setParentURL(entity->getScript());
}
QScriptValue entityScriptConstructor = _sandboxScriptEngine->evaluate(scriptContents);
if (!entityScriptConstructor.isFunction()) {
qCDebug(entitiesrenderer) << "EntityTreeRenderer::loadEntityScript() entity:" << entityID;
qCDebug(entitiesrenderer) << " NOT CONSTRUCTOR";
qCDebug(entitiesrenderer) << " SCRIPT:" << entityScript;
scriptCache->addScriptToBadScriptList(url);
return QScriptValue(); // invalid script
} else {
entityScriptConstructor = _entitiesScriptEngine->evaluate(scriptContents);
}
QScriptValue entityScriptObject = entityScriptConstructor.construct();
EntityScriptDetails newDetails = { entityScript, entityScriptObject };
_entityScripts[entityID] = newDetails;
if (isURL) {
_entitiesScriptEngine->setParentURL("");
}
return entityScriptObject; // newly constructed
}
QScriptValue EntityTreeRenderer::getPreviouslyLoadedEntityScript(const EntityItemID& entityID) {
if (_entityScripts.contains(entityID)) {
EntityScriptDetails details = _entityScripts[entityID];
return details.scriptObject; // previously loaded
}
return QScriptValue(); // no script
}
void EntityTreeRenderer::setTree(OctreePointer newTree) {
OctreeRenderer::setTree(newTree);
std::static_pointer_cast<EntityTree>(_tree)->setFBXService(this);
@ -322,11 +138,7 @@ void EntityTreeRenderer::update() {
// and we want to simulate this message here as well as in mouse move
if (_lastMouseEventValid && !_currentClickingOnEntityID.isInvalidID()) {
emit holdingClickOnEntity(_currentClickingOnEntityID, _lastMouseEvent);
QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, _lastMouseEvent);
QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID);
if (currentClickingEntity.property("holdingClickOnEntity").isValid()) {
currentClickingEntity.property("holdingClickOnEntity").call(currentClickingEntity, currentClickingEntityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "holdingClickOnEntity", _lastMouseEvent);
}
}
@ -355,19 +167,14 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
});
// Note: at this point we don't need to worry about the tree being locked, because we only deal with
// EntityItemIDs from here. The loadEntityScript() method is robust against attempting to load scripts
// EntityItemIDs from here. The callEntityScriptMethod() method is robust against attempting to call scripts
// for entity IDs that no longer exist.
// for all of our previous containing entities, if they are no longer containing then send them a leave event
foreach(const EntityItemID& entityID, _currentEntitiesInside) {
if (!entitiesContainingAvatar.contains(entityID)) {
emit leaveEntity(entityID);
QScriptValueList entityArgs = createEntityArgs(entityID);
QScriptValue entityScript = loadEntityScript(entityID);
if (entityScript.property("leaveEntity").isValid()) {
entityScript.property("leaveEntity").call(entityScript, entityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
}
@ -375,11 +182,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
foreach(const EntityItemID& entityID, entitiesContainingAvatar) {
if (!_currentEntitiesInside.contains(entityID)) {
emit enterEntity(entityID);
QScriptValueList entityArgs = createEntityArgs(entityID);
QScriptValue entityScript = loadEntityScript(entityID);
if (entityScript.property("enterEntity").isValid()) {
entityScript.property("enterEntity").call(entityScript, entityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity");
}
}
_currentEntitiesInside = entitiesContainingAvatar;
@ -394,11 +197,7 @@ void EntityTreeRenderer::leaveAllEntities() {
// for all of our previous containing entities, if they are no longer containing then send them a leave event
foreach(const EntityItemID& entityID, _currentEntitiesInside) {
emit leaveEntity(entityID);
QScriptValueList entityArgs = createEntityArgs(entityID);
QScriptValue entityScript = loadEntityScript(entityID);
if (entityScript.property("leaveEntity").isValid()) {
entityScript.property("leaveEntity").call(entityScript, entityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
_currentEntitiesInside.clear();
@ -811,27 +610,6 @@ void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityS
connect(DependencyManager::get<SceneScriptingInterface>().data(), &SceneScriptingInterface::shouldRenderEntitiesChanged, this, &EntityTreeRenderer::updateEntityRenderStatus, Qt::QueuedConnection);
}
QScriptValueList EntityTreeRenderer::createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID) {
QScriptValueList args;
args << entityID.toScriptValue(_entitiesScriptEngine);
args << MouseEvent(*event, deviceID).toScriptValue(_entitiesScriptEngine);
return args;
}
QScriptValueList EntityTreeRenderer::createMouseEventArgs(const EntityItemID& entityID, const MouseEvent& mouseEvent) {
QScriptValueList args;
args << entityID.toScriptValue(_entitiesScriptEngine);
args << mouseEvent.toScriptValue(_entitiesScriptEngine);
return args;
}
QScriptValueList EntityTreeRenderer::createEntityArgs(const EntityItemID& entityID) {
QScriptValueList args;
args << entityID.toScriptValue(_entitiesScriptEngine);
return args;
}
void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int deviceID) {
// If we don't have a tree, or we're in the process of shutting down, then don't
// process these events.
@ -854,18 +632,11 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int device
}
emit mousePressOnEntity(rayPickResult, event, deviceID);
QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID);
QScriptValue entityScript = loadEntityScript(rayPickResult.entity);
if (entityScript.property("mousePressOnEntity").isValid()) {
entityScript.property("mousePressOnEntity").call(entityScript, entityScriptArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mousePressOnEntity", MouseEvent(*event, deviceID));
_currentClickingOnEntityID = rayPickResult.entityID;
emit clickDownOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID));
if (entityScript.property("clickDownOnEntity").isValid()) {
entityScript.property("clickDownOnEntity").call(entityScript, entityScriptArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "clickDownOnEntity", MouseEvent(*event, deviceID));
} else {
emit mousePressOffEntity(rayPickResult, event, deviceID);
}
@ -886,24 +657,14 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event, unsigned int devi
if (rayPickResult.intersects) {
//qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID;
emit mouseReleaseOnEntity(rayPickResult, event, deviceID);
QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID);
QScriptValue entityScript = loadEntityScript(rayPickResult.entity);
if (entityScript.property("mouseReleaseOnEntity").isValid()) {
entityScript.property("mouseReleaseOnEntity").call(entityScript, entityScriptArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseReleaseOnEntity", MouseEvent(*event, deviceID));
}
// Even if we're no longer intersecting with an entity, if we started clicking on it, and now
// we're releasing the button, then this is considered a clickOn event
if (!_currentClickingOnEntityID.isInvalidID()) {
emit clickReleaseOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID));
QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, event, deviceID);
QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID);
if (currentClickingEntity.property("clickReleaseOnEntity").isValid()) {
currentClickingEntity.property("clickReleaseOnEntity").call(currentClickingEntity, currentClickingEntityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "clickReleaseOnEntity", MouseEvent(*event, deviceID));
}
// makes it the unknown ID, we just released so we can't be clicking on anything
@ -925,17 +686,9 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI
bool precisionPicking = false; // for mouse moves we do not do precision picking
RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::TryLock, precisionPicking);
if (rayPickResult.intersects) {
//qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID;
QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID);
// load the entity script if needed...
QScriptValue entityScript = loadEntityScript(rayPickResult.entity);
if (entityScript.property("mouseMoveEvent").isValid()) {
entityScript.property("mouseMoveEvent").call(entityScript, entityScriptArgs);
}
emit mouseMoveOnEntity(rayPickResult, event, deviceID);
if (entityScript.property("mouseMoveOnEntity").isValid()) {
entityScript.property("mouseMoveOnEntity").call(entityScript, entityScriptArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveEvent", MouseEvent(*event, deviceID));
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseMoveOnEntity", MouseEvent(*event, deviceID));
// handle the hover logic...
@ -943,30 +696,19 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI
// then we need to send the hover leave.
if (!_currentHoverOverEntityID.isInvalidID() && rayPickResult.entityID != _currentHoverOverEntityID) {
emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event, deviceID));
QScriptValueList currentHoverEntityArgs = createMouseEventArgs(_currentHoverOverEntityID, event, deviceID);
QScriptValue currentHoverEntity = loadEntityScript(_currentHoverOverEntityID);
if (currentHoverEntity.property("hoverLeaveEntity").isValid()) {
currentHoverEntity.property("hoverLeaveEntity").call(currentHoverEntity, currentHoverEntityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event, deviceID));
}
// If the new hover entity does not match the previous hover entity then we are entering the new one
// this is true if the _currentHoverOverEntityID is known or unknown
if (rayPickResult.entityID != _currentHoverOverEntityID) {
emit hoverEnterEntity(rayPickResult.entityID, MouseEvent(*event, deviceID));
if (entityScript.property("hoverEnterEntity").isValid()) {
entityScript.property("hoverEnterEntity").call(entityScript, entityScriptArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverEnterEntity", MouseEvent(*event, deviceID));
}
// and finally, no matter what, if we're intersecting an entity then we're definitely hovering over it, and
// we should send our hover over event
emit hoverOverEntity(rayPickResult.entityID, MouseEvent(*event, deviceID));
if (entityScript.property("hoverOverEntity").isValid()) {
entityScript.property("hoverOverEntity").call(entityScript, entityScriptArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverOverEntity", MouseEvent(*event, deviceID));
// remember what we're hovering over
_currentHoverOverEntityID = rayPickResult.entityID;
@ -977,14 +719,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI
// send the hover leave for our previous entity
if (!_currentHoverOverEntityID.isInvalidID()) {
emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event, deviceID));
QScriptValueList currentHoverEntityArgs = createMouseEventArgs(_currentHoverOverEntityID, event, deviceID);
QScriptValue currentHoverEntity = loadEntityScript(_currentHoverOverEntityID);
if (currentHoverEntity.property("hoverLeaveEntity").isValid()) {
currentHoverEntity.property("hoverLeaveEntity").call(currentHoverEntity, currentHoverEntityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(_currentHoverOverEntityID, "hoverLeaveEntity", MouseEvent(*event, deviceID));
_currentHoverOverEntityID = UNKNOWN_ENTITY_ID; // makes it the unknown ID
}
}
@ -993,13 +728,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI
// not yet released the hold then this is still considered a holdingClickOnEntity event
if (!_currentClickingOnEntityID.isInvalidID()) {
emit holdingClickOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID));
QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, event, deviceID);
QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID);
if (currentClickingEntity.property("holdingClickOnEntity").isValid()) {
currentClickingEntity.property("holdingClickOnEntity").call(currentClickingEntity, currentClickingEntityArgs);
}
_entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "holdingClickOnEntity", MouseEvent(*event, deviceID));
}
_lastMouseEvent = MouseEvent(*event, deviceID);
_lastMouseEventValid = true;
@ -1007,9 +736,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI
void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) {
if (_tree && !_shuttingDown) {
checkAndCallUnload(entityID);
_entitiesScriptEngine->unloadEntityScript(entityID);
}
_entityScripts.remove(entityID);
// here's where we remove the entity payload from the scene
if (_entitiesInScene.contains(entityID)) {
@ -1042,28 +770,16 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) {
void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID, const bool reload) {
if (_tree && !_shuttingDown) {
checkAndCallUnload(entityID);
_entitiesScriptEngine->unloadEntityScript(entityID);
checkAndCallPreload(entityID, reload);
}
}
void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) {
if (_tree && !_shuttingDown) {
// load the entity script if needed...
QScriptValue entityScript = loadEntityScript(entityID, true, reload); // is preload!
if (entityScript.property("preload").isValid()) {
QScriptValueList entityArgs = createEntityArgs(entityID);
entityScript.property("preload").call(entityScript, entityArgs);
}
}
}
void EntityTreeRenderer::checkAndCallUnload(const EntityItemID& entityID) {
if (_tree && !_shuttingDown) {
QScriptValue entityScript = getPreviouslyLoadedEntityScript(entityID);
if (entityScript.property("unload").isValid()) {
QScriptValueList entityArgs = createEntityArgs(entityID);
entityScript.property("unload").call(entityScript, entityArgs);
EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID);
if (entity && !entity->getScript().isEmpty()) {
_entitiesScriptEngine->loadEntityScript(entityID, entity->getScript(), reload);
}
}
}
@ -1143,24 +859,9 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons
// And now the entity scripts
emit collisionWithEntity(idA, idB, collision);
QScriptValue entityScriptA = loadEntityScript(idA);
if (entityScriptA.property("collisionWithEntity").isValid()) {
QScriptValueList args;
args << idA.toScriptValue(_entitiesScriptEngine);
args << idB.toScriptValue(_entitiesScriptEngine);
args << collisionToScriptValue(_entitiesScriptEngine, collision);
entityScriptA.property("collisionWithEntity").call(entityScriptA, args);
}
_entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision);
emit collisionWithEntity(idB, idA, collision);
QScriptValue entityScriptB = loadEntityScript(idB);
if (entityScriptB.property("collisionWithEntity").isValid()) {
QScriptValueList args;
args << idB.toScriptValue(_entitiesScriptEngine);
args << idA.toScriptValue(_entitiesScriptEngine);
args << collisionToScriptValue(_entitiesScriptEngine, collision);
entityScriptB.property("collisionWithEntity").call(entityScriptA, args);
}
_entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision);
}
void EntityTreeRenderer::updateEntityRenderStatus(bool shouldRenderEntities) {

View file

@ -28,14 +28,9 @@ class Model;
class ScriptEngine;
class ZoneEntityItem;
class EntityScriptDetails {
public:
QString scriptText;
QScriptValue scriptObject;
};
// Generic client side Octree renderer class.
class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService, public ScriptUser {
class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService {
Q_OBJECT
public:
EntityTreeRenderer(bool wantScripts, AbstractViewStateInterface* viewState,
@ -87,9 +82,6 @@ public:
/// hovering over, and entering entities
void connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface);
virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents);
virtual void errorInLoadingScript(const QUrl& url);
// For Scene.shouldRenderEntities
QList<EntityItemID>& getEntitiesLastInScene() { return _entityIDsLastInScene; }
@ -137,7 +129,6 @@ private:
void applyZonePropertiesToScene(std::shared_ptr<ZoneEntityItem> zone);
void renderElementProxy(EntityTreeElementPointer entityTreeElement, RenderArgs* args);
void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false);
void checkAndCallUnload(const EntityItemID& entityID);
QList<Model*> _releasedModels;
void renderProxies(EntityItemPointer entity, RenderArgs* args);
@ -155,16 +146,6 @@ private:
bool _wantScripts;
ScriptEngine* _entitiesScriptEngine;
ScriptEngine* _sandboxScriptEngine;
QScriptValue loadEntityScript(EntityItemPointer entity, bool isPreload = false, bool reload = false);
QScriptValue loadEntityScript(const EntityItemID& entityItemID, bool isPreload = false, bool reload = false);
QScriptValue getPreviouslyLoadedEntityScript(const EntityItemID& entityItemID);
QString loadScriptContents(const QString& scriptMaybeURLorText, bool& isURL, bool& isPending, QUrl& url, bool& reload);
QScriptValueList createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID);
QScriptValueList createMouseEventArgs(const EntityItemID& entityID, const MouseEvent& mouseEvent);
QHash<EntityItemID, EntityScriptDetails> _entityScripts;
void playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree,
const EntityItemID& id, const Collision& collision);

View file

@ -17,21 +17,31 @@
#include <SharedUtil.h>
ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const QUrl& url) {
QUrl ResourceManager::normalizeURL(const QUrl& url) {
auto scheme = url.scheme();
if (!(scheme == URL_SCHEME_FILE ||
scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP ||
scheme == URL_SCHEME_ATP)) {
// check the degenerative file case: on windows we can often have urls of the form c:/filename
// this checks for and works around that case.
QUrl urlWithFileScheme{ URL_SCHEME_FILE + ":///" + url.toString() };
if (!urlWithFileScheme.toLocalFile().isEmpty()) {
return urlWithFileScheme;
}
}
return url;
}
ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const QUrl& url) {
auto normalizedURL = normalizeURL(url);
auto scheme = normalizedURL.scheme();
if (scheme == URL_SCHEME_FILE) {
return new FileResourceRequest(parent, url);
} else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) {
return new HTTPResourceRequest(parent, url);
} else if (scheme == URL_SCHEME_ATP) {
return new AssetResourceRequest(parent, url);
} else {
// check the degenerative file case: on windows we can often have urls of the form c:/filename
// this checks for and works around that case.
QUrl urlWithFileScheme { URL_SCHEME_FILE + ":///" + url.toString() };
if (!urlWithFileScheme.toLocalFile().isEmpty()) {
return new FileResourceRequest(parent, urlWithFileScheme);
}
}
qDebug() << "Unknown scheme (" << scheme << ") for URL: " << url.url();

View file

@ -24,6 +24,7 @@ const QString URL_SCHEME_ATP = "atp";
class ResourceManager {
public:
static QUrl normalizeURL(const QUrl& url);
static ResourceRequest* createResourceRequest(QObject* parent, const QUrl& url);
};

View file

@ -65,7 +65,7 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy
auto nodeList = DependencyManager::get<NodeList>();
nodeList->setOwnerType(nodeType);
_domainServerTimer = new QTimer();
_domainServerTimer = new QTimer(this);
connect(_domainServerTimer, SIGNAL(timeout()), this, SLOT(checkInWithDomainServerOrExit()));
_domainServerTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS);

View file

@ -34,7 +34,6 @@ void CongestionControl::setPacketSendPeriod(double newSendPeriod) {
}
DefaultCC::DefaultCC() :
_lastRCTime(p_high_resolution_clock::now()),
_slowStartLastAck(_sendCurrSeqNum),
_lastDecreaseMaxSeq(SequenceNumber {SequenceNumber::MAX })
{

View file

@ -108,7 +108,8 @@ public:
private:
void stopSlowStart(); // stops the slow start on loss or timeout
p_high_resolution_clock::time_point _lastRCTime; // last rate increase time
p_high_resolution_clock::time_point _lastRCTime = p_high_resolution_clock::now(); // last rate increase time
bool _slowStart { true }; // if in slow start phase
SequenceNumber _slowStartLastAck; // last ACKed seq num
bool _loss { false }; // if loss happened since last rate increase

View file

@ -28,7 +28,6 @@ using namespace udt;
using namespace std::chrono;
Connection::Connection(Socket* parentSocket, HifiSockAddr destination, std::unique_ptr<CongestionControl> congestionControl) :
_connectionStart(p_high_resolution_clock::now()),
_parentSocket(parentSocket),
_destination(destination),
_congestionControl(move(congestionControl))
@ -278,11 +277,11 @@ void Connection::sendACK(bool wasCausedBySyncTimeout) {
// pack in the receive speed and estimatedBandwidth
ackPacket->writePrimitive(packetReceiveSpeed);
ackPacket->writePrimitive(estimatedBandwidth);
// record this as the last ACK send time
lastACKSendTime = p_high_resolution_clock::now();
}
// record this as the last ACK send time
lastACKSendTime = p_high_resolution_clock::now();
// have the socket send off our packet
_parentSocket->writeBasePacket(*ackPacket, _destination);
@ -534,7 +533,8 @@ void Connection::processACK(std::unique_ptr<ControlPacket> controlPacket) {
// This will be the case if it has been longer than the sync interval OR
// it looks like they haven't received our ACK2 for this ACK
auto currentTime = p_high_resolution_clock::now();
static p_high_resolution_clock::time_point lastACK2SendTime;
static p_high_resolution_clock::time_point lastACK2SendTime =
p_high_resolution_clock::now() - std::chrono::microseconds(_synInterval);
microseconds sinceLastACK2 = duration_cast<microseconds>(currentTime - lastACK2SendTime);
@ -779,7 +779,7 @@ void Connection::resetReceiveState() {
// clear the loss list and _lastNAKTime
_lossList.clear();
_lastNAKTime = p_high_resolution_clock::time_point();
_lastNAKTime = p_high_resolution_clock::now();
// the _nakInterval need not be reset, that will happen on loss

View file

@ -114,13 +114,14 @@ private:
int _nakInterval { -1 }; // NAK timeout interval, in microseconds, set on loss
int _minNAKInterval { 100000 }; // NAK timeout interval lower bound, default of 100ms
p_high_resolution_clock::time_point _lastNAKTime;
p_high_resolution_clock::time_point _lastNAKTime = p_high_resolution_clock::now();
bool _hasReceivedHandshake { false }; // flag for receipt of handshake from server
bool _hasReceivedHandshakeACK { false }; // flag for receipt of handshake ACK from client
p_high_resolution_clock::time_point _connectionStart; // holds the time_point for creation of this connection
p_high_resolution_clock::time_point _connectionStart = p_high_resolution_clock::now(); // holds the time_point for creation of this connection
p_high_resolution_clock::time_point _lastReceiveTime; // holds the last time we received anything from sender
bool _isReceivingData { false }; // flag used for expiry of receipt portion of connection
LossList _lossList; // List of all missing packets

View file

@ -93,15 +93,16 @@ int32_t PacketTimeWindow::getEstimatedBandwidth() const {
}
void PacketTimeWindow::onPacketArrival() {
// take the current time
auto now = p_high_resolution_clock::now();
// record the interval between this packet and the last one
_packetIntervals[_currentPacketInterval++] = duration_cast<microseconds>(now - _lastPacketTime).count();
// reset the currentPacketInterval index when it wraps
if (_currentPacketInterval == _numPacketIntervals) {
_currentPacketInterval = 0;
if (_packetIntervals.size() > 0) {
// record the interval between this packet and the last one
_packetIntervals[_currentPacketInterval++] = duration_cast<microseconds>(now - _lastPacketTime).count();
// reset the currentPacketInterval index when it wraps
_currentPacketInterval %= _numPacketIntervals;
}
// remember this as the last packet arrival time
@ -120,7 +121,5 @@ void PacketTimeWindow::onProbePair2Arrival() {
_probeIntervals[_currentProbeInterval++] = duration_cast<microseconds>(now - _firstProbeTime).count();
// reset the currentProbeInterval index when it wraps
if (_currentProbeInterval == _numProbeIntervals) {
_currentProbeInterval = 0;
}
_currentProbeInterval %= _numProbeIntervals;
}

View file

@ -42,8 +42,8 @@ private:
std::vector<int> _packetIntervals; // vector of microsecond intervals between packet arrivals
std::vector<int> _probeIntervals; // vector of microsecond intervals between probe pair arrivals
p_high_resolution_clock::time_point _lastPacketTime; // the time_point when last packet arrived
p_high_resolution_clock::time_point _firstProbeTime; // the time_point when first probe in pair arrived
p_high_resolution_clock::time_point _lastPacketTime = p_high_resolution_clock::now(); // the time_point when last packet arrived
p_high_resolution_clock::time_point _firstProbeTime = p_high_resolution_clock::now(); // the time_point when first probe in pair arrived
};
}

View file

@ -279,17 +279,13 @@ void SendQueue::run() {
if (!_hasReceivedHandshakeACK) {
// we haven't received a handshake ACK from the client
// if it has been at least 100ms since we last sent a handshake, send another now
// hold the time of last send in a static
static auto lastSendHandshake = p_high_resolution_clock::time_point();
static const auto HANDSHAKE_RESEND_INTERVAL_MS = std::chrono::milliseconds(100);
// calculation the duration since the last handshake send
auto sinceLastHandshake = std::chrono::duration_cast<std::chrono::milliseconds>(p_high_resolution_clock::now()
- lastSendHandshake);
// hold the time of last send in a static
static auto lastSendHandshake = p_high_resolution_clock::now() - HANDSHAKE_RESEND_INTERVAL_MS;
if (sinceLastHandshake >= HANDSHAKE_RESEND_INTERVAL_MS) {
if (p_high_resolution_clock::now() - lastSendHandshake >= HANDSHAKE_RESEND_INTERVAL_MS) {
// it has been long enough since last handshake, send another
static auto handshakePacket = ControlPacket::create(ControlPacket::Handshake, 0);
@ -299,9 +295,7 @@ void SendQueue::run() {
}
// we wait for the ACK or the re-send interval to expire
_handshakeACKCondition.wait_until(handshakeLock,
p_high_resolution_clock::now()
+ HANDSHAKE_RESEND_INTERVAL_MS);
_handshakeACKCondition.wait_until(handshakeLock, p_high_resolution_clock::now() + HANDSHAKE_RESEND_INTERVAL_MS);
// Once we're here we've either received the handshake ACK or it's going to be time to re-send a handshake.
// Either way let's continue processing - no packets will be sent if no handshake ACK has been received.

View file

@ -45,8 +45,6 @@ class SendQueue : public QObject {
Q_OBJECT
public:
using time_point = p_high_resolution_clock::time_point;
static std::unique_ptr<SendQueue> create(Socket* socket, HifiSockAddr destination);
void queuePacket(std::unique_ptr<Packet> packet);

View file

@ -798,7 +798,7 @@ namespace render {
return payload->model->renderPart(args, payload->meshIndex, payload->partIndex, payload->transparent);
}
}
/* template <> const model::MaterialKey& shapeGetMaterialKey(const MeshPartPayload::Pointer& payload) {
return payload->model->getPartMaterial(payload->meshIndex, payload->partIndex);
}*/
@ -829,6 +829,9 @@ bool Model::addToScene(std::shared_ptr<render::Scene> scene, render::PendingChan
auto renderData = MeshPartPayload::Pointer(renderItem);
auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderData);
pendingChanges.resetItem(item, renderPayload);
pendingChanges.updateItem<MeshPartPayload>(item, [&](MeshPartPayload& data) {
data.model->_needsUpdateClusterMatrices = true;
});
_renderItems.insert(item, renderPayload);
somethingAdded = true;
}
@ -838,6 +841,9 @@ bool Model::addToScene(std::shared_ptr<render::Scene> scene, render::PendingChan
auto renderData = MeshPartPayload::Pointer(renderItem);
auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderData);
pendingChanges.resetItem(item, renderPayload);
pendingChanges.updateItem<MeshPartPayload>(item, [&](MeshPartPayload& data) {
data.model->_needsUpdateClusterMatrices = true;
});
_renderItems.insert(item, renderPayload);
somethingAdded = true;
}
@ -860,6 +866,9 @@ bool Model::addToScene(std::shared_ptr<render::Scene> scene, render::PendingChan
auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderData);
renderPayload->addStatusGetters(statusGetters);
pendingChanges.resetItem(item, renderPayload);
pendingChanges.updateItem<MeshPartPayload>(item, [&](MeshPartPayload& data) {
data.model->_needsUpdateClusterMatrices = true;
});
_renderItems.insert(item, renderPayload);
somethingAdded = true;
}
@ -870,6 +879,9 @@ bool Model::addToScene(std::shared_ptr<render::Scene> scene, render::PendingChan
auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderData);
renderPayload->addStatusGetters(statusGetters);
pendingChanges.resetItem(item, renderPayload);
pendingChanges.updateItem<MeshPartPayload>(item, [&](MeshPartPayload& data) {
data.model->_needsUpdateClusterMatrices = true;
});
_renderItems.insert(item, renderPayload);
somethingAdded = true;
}
@ -1283,6 +1295,7 @@ void Model::simulate(float deltaTime, bool fullUpdate) {
//virtual
void Model::updateRig(float deltaTime, glm::mat4 parentTransform) {
_needsUpdateClusterMatrices = true;
_rig->updateAnimations(deltaTime, parentTransform);
}
void Model::simulateInternal(float deltaTime) {
@ -1293,11 +1306,15 @@ void Model::simulateInternal(float deltaTime) {
updateRig(deltaTime, parentTransform);
}
void Model::updateClusterMatrices() {
if (!_needsUpdateClusterMatrices) {
return;
}
_needsUpdateClusterMatrices = false;
const FBXGeometry& geometry = _geometry->getFBXGeometry();
glm::mat4 zeroScale(glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
auto cauterizeMatrix = _rig->getJointTransform(geometry.neckJointIndex) * zeroScale;
glm::mat4 modelToWorld = glm::mat4_cast(_rotation);
@ -1803,13 +1820,15 @@ bool Model::initWhenReady(render::ScenePointer scene) {
segregateMeshGroups();
render::PendingChanges pendingChanges;
foreach (auto renderItem, _transparentRenderItems) {
auto item = scene->allocateID();
auto renderData = MeshPartPayload::Pointer(renderItem);
auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderData);
_renderItems.insert(item, renderPayload);
pendingChanges.resetItem(item, renderPayload);
pendingChanges.updateItem<MeshPartPayload>(item, [&](MeshPartPayload& data) {
data.model->_needsUpdateClusterMatrices = true;
});
}
foreach (auto renderItem, _opaqueRenderItems) {
@ -1818,6 +1837,9 @@ bool Model::initWhenReady(render::ScenePointer scene) {
auto renderPayload = std::make_shared<MeshPartPayload::Payload>(renderData);
_renderItems.insert(item, renderPayload);
pendingChanges.resetItem(item, renderPayload);
pendingChanges.updateItem<MeshPartPayload>(item, [&](MeshPartPayload& data) {
data.model->_needsUpdateClusterMatrices = true;
});
}
scene->enqueuePendingChanges(pendingChanges);

View file

@ -497,6 +497,7 @@ private:
QMap<render::ItemID, render::PayloadPointer> _renderItems;
bool _readyWhenAdded = false;
bool _needsReload = true;
bool _needsUpdateClusterMatrices = true;
protected:
RigPointer _rig;

View file

@ -15,6 +15,7 @@
#include <QNetworkConfiguration>
#include <QNetworkReply>
#include <QObject>
#include <QThread>
#include <assert.h>
#include <SharedUtil.h>
@ -26,7 +27,8 @@ ScriptCache::ScriptCache(QObject* parent) {
// nothing to do here...
}
QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending, bool reload) {
QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool reload) {
QUrl url = ResourceManager::normalizeURL(unnormalizedURL);
QString scriptContents;
if (_scriptCache.contains(url) && !reload) {
qCDebug(scriptengine) << "Found script in cache:" << url.toString();
@ -41,7 +43,7 @@ QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& is
if (alreadyWaiting) {
qCDebug(scriptengine) << "Already downloading script at:" << url.toString();
} else {
auto request = ResourceManager::createResourceRequest(this, url);
auto request = ResourceManager::createResourceRequest(nullptr, url);
request->setCacheEnabled(!reload);
connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptDownloaded);
request->send();
@ -50,7 +52,8 @@ QString ScriptCache::getScript(const QUrl& url, ScriptUser* scriptUser, bool& is
return scriptContents;
}
void ScriptCache::deleteScript(const QUrl& url) {
void ScriptCache::deleteScript(const QUrl& unnormalizedURL) {
QUrl url = ResourceManager::normalizeURL(unnormalizedURL);
if (_scriptCache.contains(url)) {
qCDebug(scriptengine) << "Delete script from cache:" << url.toString();
_scriptCache.remove(url);
@ -79,4 +82,63 @@ void ScriptCache::scriptDownloaded() {
req->deleteLater();
}
void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload) {
#ifdef THREAD_DEBUGGING
qCDebug(scriptengine) << "ScriptCache::getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
#endif
QUrl unnormalizedURL(scriptOrURL);
QUrl url = ResourceManager::normalizeURL(unnormalizedURL);
// attempt to determine if this is a URL to a script, or if this is actually a script itself (which is valid in the entityScript use case)
if (url.scheme().isEmpty() && scriptOrURL.simplified().replace(" ", "").contains("(function(){")) {
contentAvailable(scriptOrURL, scriptOrURL, false, true);
return;
}
if (_scriptCache.contains(url) && !forceDownload) {
qCDebug(scriptengine) << "Found script in cache:" << url.toString();
#if 1 // def THREAD_DEBUGGING
qCDebug(scriptengine) << "ScriptCache::getScriptContents() about to call contentAvailable() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
#endif
contentAvailable(url.toString(), _scriptCache[url], true, true);
} else {
bool alreadyWaiting = _contentCallbacks.contains(url);
_contentCallbacks.insert(url, contentAvailable);
if (alreadyWaiting) {
qCDebug(scriptengine) << "Already downloading script at:" << url.toString();
} else {
#ifdef THREAD_DEBUGGING
qCDebug(scriptengine) << "about to call: ResourceManager::createResourceRequest(this, url); on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
#endif
auto request = ResourceManager::createResourceRequest(nullptr, url);
request->setCacheEnabled(!forceDownload);
connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable);
request->send();
}
}
}
void ScriptCache::scriptContentAvailable() {
#ifdef THREAD_DEBUGGING
qCDebug(scriptengine) << "ScriptCache::scriptContentAvailable() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
#endif
ResourceRequest* req = qobject_cast<ResourceRequest*>(sender());
QUrl url = req->getUrl();
QList<contentAvailableCallback> allCallbacks = _contentCallbacks.values(url);
_contentCallbacks.remove(url);
bool success = req->getResult() == ResourceRequest::Success;
if (success) {
_scriptCache[url] = req->getData();
qCDebug(scriptengine) << "Done downloading script at:" << url.toString();
} else {
qCWarning(scriptengine) << "Error loading script from URL " << url;
}
foreach(contentAvailableCallback thisCallback, allCallbacks) {
thisCallback(url.toString(), _scriptCache[url], true, success);
}
req->deleteLater();
}

View file

@ -20,23 +20,33 @@ public:
virtual void errorInLoadingScript(const QUrl& url) = 0;
};
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable)>;
/// Interface for loading scripts
class ScriptCache : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
public:
QString getScript(const QUrl& url, ScriptUser* scriptUser, bool& isPending, bool redownload = false);
void deleteScript(const QUrl& url);
void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false);
QString getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool redownload = false);
void deleteScript(const QUrl& unnormalizedURL);
// FIXME - how do we remove a script from the bad script list in the case of a redownload?
void addScriptToBadScriptList(const QUrl& url) { _badScripts.insert(url); }
bool isInBadScriptList(const QUrl& url) { return _badScripts.contains(url); }
private slots:
void scriptDownloaded();
void scriptDownloaded(); // old version
void scriptContentAvailable(); // new version
private:
ScriptCache(QObject* parent = NULL);
QMultiMap<QUrl, contentAvailableCallback> _contentCallbacks;
QHash<QUrl, QString> _scriptCache;
QMultiMap<QUrl, ScriptUser*> _scriptUsers;
QSet<QUrl> _badScripts;

View file

@ -16,6 +16,7 @@
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QScriptEngine>
#include <QScriptValue>
#include <AudioConstants.h>
#include <AudioEffectOptions.h>
@ -44,6 +45,8 @@
#include "MIDIEvent.h"
Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
static int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>();
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){
QString message = "";
@ -87,17 +90,9 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam
_isFinished(false),
_isRunning(false),
_isInitialized(false),
_isAvatar(false),
_avatarIdentityTimer(NULL),
_avatarBillboardTimer(NULL),
_timerFunctionMap(),
_isListeningToAudioStream(false),
_avatarSound(NULL),
_numAvatarSoundSentBytes(0),
_wantSignals(wantSignals),
_controllerScriptingInterface(controllerScriptingInterface),
_avatarData(NULL),
_scriptName(),
_fileNameString(fileNameString),
_quatLibrary(),
_vec3Library(),
@ -122,6 +117,29 @@ ScriptEngine::~ScriptEngine() {
}
}
void ScriptEngine::runInThread() {
QThread* workerThread = new QThread(); // thread is not owned, so we need to manage the delete
QString scriptEngineName = QString("Script Thread:") + getFilename();
workerThread->setObjectName(scriptEngineName);
// when the worker thread is started, call our engine's run..
connect(workerThread, &QThread::started, this, &ScriptEngine::run);
// tell the thread to stop when the script engine is done
connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit);
// when the thread is finished, add thread to the deleteLater queue
connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater);
// when the thread is finished, add scriptEngine to the deleteLater queue
connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater);
moveToThread(workerThread);
// Starts an event loop, and emits workerThread->started()
workerThread->start();
}
QSet<ScriptEngine*> ScriptEngine::_allKnownScriptEngines;
QMutex ScriptEngine::_allScriptsMutex;
bool ScriptEngine::_stoppingAllScripts = false;
@ -180,8 +198,6 @@ void ScriptEngine::stopAllScripts(QObject* application) {
void ScriptEngine::waitTillDoneRunning() {
QString scriptName = getFilename();
// If the script never started running or finished running before we got here, we don't need to wait for it
if (_isRunning) {
@ -209,58 +225,7 @@ QString ScriptEngine::getFilename() const {
}
void ScriptEngine::setIsAvatar(bool isAvatar) {
_isAvatar = isAvatar;
if (_isAvatar && !_avatarIdentityTimer) {
// set up the avatar timers
_avatarIdentityTimer = new QTimer(this);
_avatarBillboardTimer = new QTimer(this);
// connect our slot
connect(_avatarIdentityTimer, &QTimer::timeout, this, &ScriptEngine::sendAvatarIdentityPacket);
connect(_avatarBillboardTimer, &QTimer::timeout, this, &ScriptEngine::sendAvatarBillboardPacket);
// start the timers
_avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);
_avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS);
}
if (!_isAvatar) {
delete _avatarIdentityTimer;
_avatarIdentityTimer = NULL;
delete _avatarBillboardTimer;
_avatarBillboardTimer = NULL;
}
}
void ScriptEngine::setAvatarData(AvatarData* avatarData, const QString& objectName) {
_avatarData = avatarData;
// remove the old Avatar property, if it exists
globalObject().setProperty(objectName, QScriptValue());
// give the script engine the new Avatar script property
registerGlobalObject(objectName, _avatarData);
}
void ScriptEngine::setAvatarHashMap(AvatarHashMap* avatarHashMap, const QString& objectName) {
// remove the old Avatar property, if it exists
globalObject().setProperty(objectName, QScriptValue());
// give the script engine the new avatar hash map
registerGlobalObject(objectName, avatarHashMap);
}
bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) {
if (_isRunning) {
return false;
}
_scriptContents = scriptContents;
_fileNameString = fileNameString;
return true;
}
// FIXME - switch this to the new model of ScriptCache callbacks
void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {
if (_isRunning) {
return;
@ -271,38 +236,12 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {
QUrl url(scriptURL);
// if the scheme length is one or lower, maybe they typed in a file, let's try
const int WINDOWS_DRIVE_LETTER_SIZE = 1;
if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) {
url = QUrl::fromLocalFile(_fileNameString);
}
// ok, let's see if it's valid... and if so, load it
if (url.isValid()) {
if (url.scheme() == "file") {
_fileNameString = url.toLocalFile();
QFile scriptFile(_fileNameString);
if (scriptFile.open(QFile::ReadOnly | QFile::Text)) {
qCDebug(scriptengine) << "ScriptEngine loading file:" << _fileNameString;
QTextStream in(&scriptFile);
_scriptContents = in.readAll();
if (_wantSignals) {
emit scriptLoaded(_fileNameString);
}
} else {
qCDebug(scriptengine) << "ERROR Loading file:" << _fileNameString << "line:" << __LINE__;
if (_wantSignals) {
emit errorLoadingScript(_fileNameString);
}
}
} else {
bool isPending;
auto scriptCache = DependencyManager::get<ScriptCache>();
scriptCache->getScript(url, this, isPending, reload);
}
}
bool isPending;
auto scriptCache = DependencyManager::get<ScriptCache>();
scriptCache->getScript(url, this, isPending, reload);
}
// FIXME - switch this to the new model of ScriptCache callbacks
void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) {
_scriptContents = scriptContents;
if (_wantSignals) {
@ -310,6 +249,7 @@ void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scrip
}
}
// FIXME - switch this to the new model of ScriptCache callbacks
void ScriptEngine::errorInLoadingScript(const QUrl& url) {
qCDebug(scriptengine) << "ERROR Loading file:" << url.toString() << "line:" << __LINE__;
if (_wantSignals) {
@ -383,57 +323,117 @@ void ScriptEngine::init() {
globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE)));
}
QScriptValue ScriptEngine::registerGlobalObject(const QString& name, QObject* object) {
void ScriptEngine::registerGlobalObject(const QString& name, QObject* object) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::registerGlobalObject() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name;
#endif
QMetaObject::invokeMethod(this, "registerGlobalObject",
Q_ARG(const QString&, name),
Q_ARG(QObject*, object));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name;
#endif
if (object) {
QScriptValue value = newQObject(object);
globalObject().setProperty(name, value);
return value;
}
return QScriptValue::NullValue;
}
void ScriptEngine::registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments) {
registerFunction(globalObject(), name, fun, numArguments);
void ScriptEngine::registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name;
#endif
QMetaObject::invokeMethod(this, "registerFunction",
Q_ARG(const QString&, name),
Q_ARG(QScriptEngine::FunctionSignature, functionSignature),
Q_ARG(int, numArguments));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name;
#endif
QScriptValue scriptFun = newFunction(functionSignature, numArguments);
globalObject().setProperty(name, scriptFun);
}
void ScriptEngine::registerFunction(QScriptValue parent, const QString& name, QScriptEngine::FunctionSignature fun, int numArguments) {
QScriptValue scriptFun = newFunction(fun, numArguments);
parent.setProperty(name, scriptFun);
void ScriptEngine::registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] parent:" << parent << "name:" << name;
#endif
QMetaObject::invokeMethod(this, "registerFunction",
Q_ARG(const QString&, name),
Q_ARG(QScriptEngine::FunctionSignature, functionSignature),
Q_ARG(int, numArguments));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] parent:" << parent << "name:" << name;
#endif
QScriptValue object = globalObject().property(parent);
if (object.isValid()) {
QScriptValue scriptFun = newFunction(functionSignature, numArguments);
object.setProperty(name, scriptFun);
}
}
void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter,
QScriptEngine::FunctionSignature setter, QScriptValue object) {
QScriptEngine::FunctionSignature setter, const QString& parent) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::registerGetterSetter() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
" name:" << name << "parent:" << parent;
#endif
QMetaObject::invokeMethod(this, "registerGetterSetter",
Q_ARG(const QString&, name),
Q_ARG(QScriptEngine::FunctionSignature, getter),
Q_ARG(QScriptEngine::FunctionSignature, setter),
Q_ARG(const QString&, parent));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::registerGetterSetter() called on thread [" << QThread::currentThread() << "] name:" << name << "parent:" << parent;
#endif
QScriptValue setterFunction = newFunction(setter, 1);
QScriptValue getterFunction = newFunction(getter);
if (!object.isNull()) {
object.setProperty(name, setterFunction, QScriptValue::PropertySetter);
object.setProperty(name, getterFunction, QScriptValue::PropertyGetter);
if (!parent.isNull()) {
QScriptValue object = globalObject().property(parent);
if (object.isValid()) {
object.setProperty(name, setterFunction, QScriptValue::PropertySetter);
object.setProperty(name, getterFunction, QScriptValue::PropertyGetter);
}
} else {
globalObject().setProperty(name, setterFunction, QScriptValue::PropertySetter);
globalObject().setProperty(name, getterFunction, QScriptValue::PropertyGetter);
}
}
// Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args
void ScriptEngine::generalHandler(const EntityItemID& entityID, const QString& eventName, std::function<QScriptValueList()> argGenerator) {
if (!_registeredHandlers.contains(entityID)) {
return;
}
const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID];
if (!handlersOnEntity.contains(eventName)) {
return;
}
QScriptValueList handlersForEvent = handlersOnEntity[eventName];
if (!handlersForEvent.isEmpty()) {
QScriptValueList args = argGenerator();
for (int i = 0; i < handlersForEvent.count(); ++i) {
handlersForEvent[i].call(QScriptValue(), args);
}
}
}
// Unregister the handlers for this eventName and entityID.
void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::removeEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID << " eventName:" << eventName;
#endif
QMetaObject::invokeMethod(this, "removeEventHandler",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, eventName),
Q_ARG(QScriptValue, handler));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName;
#endif
if (!_registeredHandlers.contains(entityID)) {
return;
}
@ -449,6 +449,22 @@ void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QStrin
}
// Register the handler.
void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::addEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID << " eventName:" << eventName;
#endif
QMetaObject::invokeMethod(this, "addEventHandler",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, eventName),
Q_ARG(QScriptValue, handler));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName;
#endif
if (_registeredHandlers.count() == 0) { // First time any per-entity handler has been added in this script...
// Connect up ALL the handlers to the global entities object's signals.
// (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.)
@ -503,34 +519,25 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
}
void ScriptEngine::evaluate() {
if (_stoppingAllScripts) {
return; // bail early
}
if (!_isInitialized) {
init();
}
QScriptValue result = evaluate(_scriptContents);
// TODO: why do we check this twice? It seems like the call to clearExceptions() in the lower level evaluate call
// will cause this code to never actually run...
if (hasUncaughtException()) {
int line = uncaughtExceptionLineNumber();
qCDebug(scriptengine) << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString();
if (_wantSignals) {
emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString());
}
clearExceptions();
}
}
QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileName, int lineNumber) {
if (_stoppingAllScripts) {
return QScriptValue(); // bail early
}
if (QThread::currentThread() != thread()) {
QScriptValue result;
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"program:" << program << " fileName:" << fileName << "lineNumber:" << lineNumber;
#endif
QMetaObject::invokeMethod(this, "evaluate", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QScriptValue, result),
Q_ARG(const QString&, program),
Q_ARG(const QString&, fileName),
Q_ARG(int, lineNumber));
return result;
}
_evaluatesPending++;
QScriptValue result = QScriptEngine::evaluate(program, fileName, lineNumber);
if (hasUncaughtException()) {
@ -545,18 +552,6 @@ QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileN
return result;
}
void ScriptEngine::sendAvatarIdentityPacket() {
if (_isAvatar && _avatarData) {
_avatarData->sendIdentityPacket();
}
}
void ScriptEngine::sendAvatarBillboardPacket() {
if (_isAvatar && _avatarData) {
_avatarData->sendBillboardPacket();
}
}
void ScriptEngine::run() {
// TODO: can we add a short circuit for _stoppingAllScripts here? What does it mean to not start running if
// we're in the process of stopping?
@ -608,107 +603,6 @@ void ScriptEngine::run() {
}
}
if (!_isFinished && _isAvatar && _avatarData) {
const int SCRIPT_AUDIO_BUFFER_SAMPLES = floor(((SCRIPT_DATA_CALLBACK_USECS * AudioConstants::SAMPLE_RATE)
/ (1000 * 1000)) + 0.5);
const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t);
QByteArray avatarByteArray = _avatarData->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO);
_avatarData->doneEncoding(true);
auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size());
avatarPacket->write(avatarByteArray);
nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
if (_isListeningToAudioStream || _avatarSound) {
// if we have an avatar audio stream then send it out to our audio-mixer
bool silentFrame = true;
int16_t numAvailableSamples = SCRIPT_AUDIO_BUFFER_SAMPLES;
const int16_t* nextSoundOutput = NULL;
if (_avatarSound) {
const QByteArray& soundByteArray = _avatarSound->getByteArray();
nextSoundOutput = reinterpret_cast<const int16_t*>(soundByteArray.data()
+ _numAvatarSoundSentBytes);
int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > SCRIPT_AUDIO_BUFFER_BYTES
? SCRIPT_AUDIO_BUFFER_BYTES
: soundByteArray.size() - _numAvatarSoundSentBytes;
numAvailableSamples = numAvailableBytes / sizeof(int16_t);
// check if the all of the _numAvatarAudioBufferSamples to be sent are silence
for (int i = 0; i < numAvailableSamples; ++i) {
if (nextSoundOutput[i] != 0) {
silentFrame = false;
break;
}
}
_numAvatarSoundSentBytes += numAvailableBytes;
if (_numAvatarSoundSentBytes == soundByteArray.size()) {
// we're done with this sound object - so set our pointer back to NULL
// and our sent bytes back to zero
_avatarSound = NULL;
_numAvatarSoundSentBytes = 0;
}
}
auto audioPacket = NLPacket::create(silentFrame
? PacketType::SilentAudioFrame
: PacketType::MicrophoneAudioNoEcho);
// seek past the sequence number, will be packed when destination node is known
audioPacket->seek(sizeof(quint16));
if (silentFrame) {
if (!_isListeningToAudioStream) {
// if we have a silent frame and we're not listening then just send nothing and break out of here
break;
}
// write the number of silent samples so the audio-mixer can uphold timing
audioPacket->writePrimitive(SCRIPT_AUDIO_BUFFER_SAMPLES);
// use the orientation and position of this avatar for the source of this audio
audioPacket->writePrimitive(_avatarData->getPosition());
glm::quat headOrientation = _avatarData->getHeadOrientation();
audioPacket->writePrimitive(headOrientation);
} else if (nextSoundOutput) {
// assume scripted avatar audio is mono and set channel flag to zero
audioPacket->writePrimitive((quint8) 0);
// use the orientation and position of this avatar for the source of this audio
audioPacket->writePrimitive(_avatarData->getPosition());
glm::quat headOrientation = _avatarData->getHeadOrientation();
audioPacket->writePrimitive(headOrientation);
// write the raw audio data
audioPacket->write(reinterpret_cast<const char*>(nextSoundOutput), numAvailableSamples * sizeof(int16_t));
}
// write audio packet to AudioMixer nodes
auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node){
// only send to nodes of type AudioMixer
if (node->getType() == NodeType::AudioMixer) {
// pack sequence number
quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++;
audioPacket->seek(0);
audioPacket->writePrimitive(sequence);
// send audio packet
nodeList->sendUnreliablePacket(*audioPacket, *node);
}
});
}
}
qint64 now = usecTimestampNow();
float deltaTime = (float) (now - lastUpdate) / (float) USECS_PER_SECOND;
@ -735,9 +629,6 @@ void ScriptEngine::run() {
emit scriptEnding();
}
// kill the avatar identity timer
delete _avatarIdentityTimer;
if (entityScriptingInterface->getEntityPacketSender()->serversExist()) {
// release the queue of edit entity messages.
entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages();
@ -967,6 +858,256 @@ void ScriptEngine::load(const QString& loadFile) {
}
}
void ScriptEngine::nodeKilled(SharedNodePointer node) {
_outgoingScriptAudioSequenceNumbers.remove(node->getUUID());
// Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args
void ScriptEngine::generalHandler(const EntityItemID& entityID, const QString& eventName, std::function<QScriptValueList()> argGenerator) {
if (QThread::currentThread() != thread()) {
qDebug() << "*** ERROR *** ScriptEngine::generalHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]";
assert(false);
return;
}
if (!_registeredHandlers.contains(entityID)) {
return;
}
const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID];
if (!handlersOnEntity.contains(eventName)) {
return;
}
QScriptValueList handlersForEvent = handlersOnEntity[eventName];
if (!handlersForEvent.isEmpty()) {
QScriptValueList args = argGenerator();
for (int i = 0; i < handlersForEvent.count(); ++i) {
handlersForEvent[i].call(QScriptValue(), args);
}
}
}
// since all of these operations can be asynch we will always do the actual work in the response handler
// for the download
void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::loadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID << "entityScript:" << entityScript <<"forceRedownload:" << forceRedownload;
#endif
QMetaObject::invokeMethod(this, "loadEntityScript",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, entityScript),
Q_ARG(bool, forceRedownload));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::loadEntityScript() called on correct thread [" << thread() << "] "
"entityID:" << entityID << "entityScript:" << entityScript << "forceRedownload:" << forceRedownload;
#endif
// If we've been called our known entityScripts should not know about us..
assert(!_entityScripts.contains(entityID));
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::loadEntityScript() calling scriptCache->getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
#endif
DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [=](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
#endif
this->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success);
}, forceRedownload);
}
// since all of these operations can be asynch we will always do the actual work in the response handler
// for the download
void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" << contents << "isURL:" << isURL << "success:" << success;
#endif
QMetaObject::invokeMethod(this, "entityScriptContentAvailable",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, scriptOrURL),
Q_ARG(const QString&, contents),
Q_ARG(bool, isURL),
Q_ARG(bool, success));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
#endif
auto scriptCache = DependencyManager::get<ScriptCache>();
// first check the syntax of the script contents
QScriptSyntaxCheckResult syntaxCheck = QScriptEngine::checkSyntax(contents);
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
qCDebug(scriptengine) << "ScriptEngine::loadEntityScript() entity:" << entityID;
qCDebug(scriptengine) << " " << syntaxCheck.errorMessage() << ":"
<< syntaxCheck.errorLineNumber() << syntaxCheck.errorColumnNumber();
qCDebug(scriptengine) << " SCRIPT:" << scriptOrURL;
scriptCache->addScriptToBadScriptList(scriptOrURL);
return; // done processing script
}
if (isURL) {
setParentURL(scriptOrURL);
}
QScriptEngine sandbox;
QScriptValue testConstructor = sandbox.evaluate(contents);
if (!testConstructor.isFunction()) {
qCDebug(scriptengine) << "ScriptEngine::loadEntityScript() entity:" << entityID;
qCDebug(scriptengine) << " NOT CONSTRUCTOR";
qCDebug(scriptengine) << " SCRIPT:" << scriptOrURL;
scriptCache->addScriptToBadScriptList(scriptOrURL);
return; // done processing script
}
QScriptValue entityScriptConstructor = evaluate(contents);
QScriptValue entityScriptObject = entityScriptConstructor.construct();
EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject };
_entityScripts[entityID] = newDetails;
if (isURL) {
setParentURL("");
}
// if we got this far, then call the preload method
callEntityScriptMethod(entityID, "preload");
}
void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID;
#endif
QMetaObject::invokeMethod(this, "unloadEntityScript",
Q_ARG(const EntityItemID&, entityID));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::unloadEntityScript() called on correct thread [" << thread() << "] "
"entityID:" << entityID;
#endif
if (_entityScripts.contains(entityID)) {
callEntityScriptMethod(entityID, "unload");
_entityScripts.remove(entityID);
}
}
void ScriptEngine::unloadAllEntityScripts() {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]";
#endif
QMetaObject::invokeMethod(this, "unloadAllEntityScripts");
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";
#endif
foreach(const EntityItemID& entityID, _entityScripts.keys()) {
callEntityScriptMethod(entityID, "unload");
}
_entityScripts.clear();
}
void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID << "methodName:" << methodName;
#endif
QMetaObject::invokeMethod(this, "callEntityScriptMethod",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, methodName));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] "
"entityID:" << entityID << "methodName:" << methodName;
#endif
if (_entityScripts.contains(entityID)) {
EntityScriptDetails details = _entityScripts[entityID];
QScriptValue entityScript = details.scriptObject; // previously loaded
if (entityScript.property(methodName).isFunction()) {
QScriptValueList args;
args << entityID.toScriptValue(this);
entityScript.property(methodName).call(entityScript, args);
}
}
}
void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent";
#endif
QMetaObject::invokeMethod(this, "callEntityScriptMethod",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, methodName),
Q_ARG(const MouseEvent&, event));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] "
"entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent";
#endif
if (_entityScripts.contains(entityID)) {
EntityScriptDetails details = _entityScripts[entityID];
QScriptValue entityScript = details.scriptObject; // previously loaded
if (entityScript.property(methodName).isFunction()) {
QScriptValueList args;
args << entityID.toScriptValue(this);
args << event.toScriptValue(this);
entityScript.property(methodName).call(entityScript, args);
}
}
}
void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "
"entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision";
#endif
QMetaObject::invokeMethod(this, "callEntityScriptMethod",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, methodName),
Q_ARG(const EntityItemID&, otherID),
Q_ARG(const Collision&, collision));
return;
}
#ifdef THREAD_DEBUGGING
qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] "
"entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision";
#endif
if (_entityScripts.contains(entityID)) {
EntityScriptDetails details = _entityScripts[entityID];
QScriptValue entityScript = details.scriptObject; // previously loaded
if (entityScript.property(methodName).isFunction()) {
QScriptValueList args;
args << entityID.toScriptValue(this);
args << otherID.toScriptValue(this);
args << collisionToScriptValue(this, collision);
entityScript.property(methodName).call(entityScript, args);
}
}
}

View file

@ -40,6 +40,12 @@ const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1
typedef QHash<QString, QScriptValueList> RegisteredEventHandlers;
class EntityScriptDetails {
public:
QString scriptText;
QScriptValue scriptObject;
};
class ScriptEngine : public QScriptEngine, public ScriptUser {
Q_OBJECT
public:
@ -50,79 +56,86 @@ public:
~ScriptEngine();
ArrayBufferClass* getArrayBufferClass() { return _arrayBufferClass; }
/// run the script in a dedicated thread. This will have the side effect of evalulating
/// the current script contents and calling run(). Callers will likely want to register the script with external
/// services before calling this.
void runInThread();
/// run the script in the callers thread, exit when stop() is called.
void run();
/// sets the script contents, will return false if failed, will fail if script is already running
bool setScriptContents(const QString& scriptContents, const QString& fileNameString = QString(""));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// NOTE - these are NOT intended to be public interfaces available to scripts, the are only Q_INVOKABLE so we can
// properly ensure they are only called on the correct thread
const QString& getScriptName() const { return _scriptName; }
/// registers a global object by name
Q_INVOKABLE void registerGlobalObject(const QString& name, QObject* object);
QScriptValue registerGlobalObject(const QString& name, QObject* object); /// registers a global object by name
void registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter,
QScriptEngine::FunctionSignature setter, QScriptValue object = QScriptValue::NullValue);
void registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments = -1);
void registerFunction(QScriptValue parent, const QString& name, QScriptEngine::FunctionSignature fun,
/// registers a global getter/setter
Q_INVOKABLE void registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter,
QScriptEngine::FunctionSignature setter, const QString& parent = QString(""));
/// register a global function
Q_INVOKABLE void registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments = -1);
/// register a function as a method on a previously registered global object
Q_INVOKABLE void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature fun,
int numArguments = -1);
Q_INVOKABLE void setIsAvatar(bool isAvatar);
bool isAvatar() const { return _isAvatar; }
/// evaluate some code in the context of the ScriptEngine and return the result
Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName = QString(), int lineNumber = 1); // this is also used by the script tool widget
void setAvatarData(AvatarData* avatarData, const QString& objectName);
void setAvatarHashMap(AvatarHashMap* avatarHashMap, const QString& objectName);
bool isListeningToAudioStream() const { return _isListeningToAudioStream; }
void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; }
void setAvatarSound(Sound* avatarSound) { _avatarSound = avatarSound; }
bool isPlayingAvatarSound() const { return _avatarSound != NULL; }
void init();
void run(); /// runs continuously until Agent.stop() is called
void evaluate(); /// initializes the engine, and evaluates the script, but then returns control to caller
void timerFired();
bool hasScript() const { return !_scriptContents.isEmpty(); }
bool isFinished() const { return _isFinished; }
bool isRunning() const { return _isRunning; }
bool evaluatePending() const { return _evaluatesPending > 0; }
void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; }
bool isUserLoaded() const { return _isUserLoaded; }
void setIsAgent(bool isAgent) { _isAgent = isAgent; }
void setParentURL(const QString& parentURL) { _parentURL = parentURL; }
QString getFilename() const;
static void stopAllScripts(QObject* application);
void waitTillDoneRunning();
virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents);
virtual void errorInLoadingScript(const QUrl& url);
/// if the script engine is not already running, this will download the URL and start the process of seting it up
/// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed
/// to scripts. we may not need this to be invokable
void loadURL(const QUrl& scriptURL, bool reload);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// NOTE - these are intended to be public interfaces available to scripts
Q_INVOKABLE void addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler);
Q_INVOKABLE void removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler);
public slots:
void loadURL(const QUrl& scriptURL, bool reload);
void stop();
Q_INVOKABLE void load(const QString& loadfile);
Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue());
Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue());
QScriptValue evaluate(const QString& program, const QString& fileName = QString(), int lineNumber = 1);
QObject* setInterval(const QScriptValue& function, int intervalMS);
QObject* setTimeout(const QScriptValue& function, int timeoutMS);
void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue());
void include(const QString& includeFile, QScriptValue callback = QScriptValue());
void load(const QString& loadfile);
void print(const QString& message);
QUrl resolvePath(const QString& path) const;
Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS);
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
Q_INVOKABLE void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
Q_INVOKABLE void print(const QString& message);
Q_INVOKABLE QUrl resolvePath(const QString& path) const;
void nodeKilled(SharedNodePointer node);
// Entity Script Related methods
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload = false); // will call the preload method once loaded
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method
Q_INVOKABLE void unloadAllEntityScripts();
Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName);
Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event);
Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts
Q_INVOKABLE void stop();
bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget
bool isRunning() const { return _isRunning; } // used by ScriptWidget
static void stopAllScripts(QObject* application); // used by Application on shutdown
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// NOTE - These are the callback implementations for ScriptUser the get called by ScriptCache when the contents
// of a script are available.
virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents);
virtual void errorInLoadingScript(const QUrl& url);
// These are currently used by Application to track if a script is user loaded or not. Consider finding a solution
// inside of Application so that the ScriptEngine class is not polluted by this notion
void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; }
bool isUserLoaded() const { return _isUserLoaded; }
// NOTE - this is used by the TypedArray implemetation. we need to review this for thread safety
ArrayBufferClass* getArrayBufferClass() { return _arrayBufferClass; }
signals:
void scriptLoaded(const QString& scriptFilename);
@ -146,28 +159,24 @@ protected:
bool _isRunning;
int _evaluatesPending = 0;
bool _isInitialized;
bool _isAvatar;
QTimer* _avatarIdentityTimer;
QTimer* _avatarBillboardTimer;
QHash<QTimer*, QScriptValue> _timerFunctionMap;
bool _isListeningToAudioStream;
Sound* _avatarSound;
int _numAvatarSoundSentBytes;
bool _isAgent = false;
QSet<QUrl> _includedURLs;
bool _wantSignals = true;
QHash<EntityItemID, EntityScriptDetails> _entityScripts;
private:
void init();
QString getFilename() const;
void waitTillDoneRunning();
bool evaluatePending() const { return _evaluatesPending > 0; }
void timerFired();
void stopAllTimers();
void sendAvatarIdentityPacket();
void sendAvatarBillboardPacket();
void setParentURL(const QString& parentURL) { _parentURL = parentURL; }
QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot);
void stopTimer(QTimer* timer);
AbstractControllerScriptingInterface* _controllerScriptingInterface;
AvatarData* _avatarData;
QString _scriptName;
QString _fileNameString;
Quat _quatLibrary;
Vec3 _vec3Library;
@ -177,15 +186,15 @@ private:
ArrayBufferClass* _arrayBufferClass;
QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers;
QHash<EntityItemID, RegisteredEventHandlers> _registeredHandlers;
void generalHandler(const EntityItemID& entityID, const QString& eventName, std::function<QScriptValueList()> argGenerator);
Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success);
private:
static QSet<ScriptEngine*> _allKnownScriptEngines;
static QMutex _allScriptsMutex;
static bool _stoppingAllScripts;
static bool _doneRunningThisScript;
};
#endif // hifi_ScriptEngine_h

View file

@ -35,7 +35,6 @@ void makeTestFBXJoints(std::vector<FBXJoint>& fbxJoints) {
joint.freeLineage.clear();
joint.parentIndex = -1;
joint.distanceToParent = 1.0f;
joint.boneRadius = 1.0f;
joint.translation = origin; // T
joint.preTransform = glm::mat4(); // Roff * Rp
@ -96,11 +95,11 @@ void makeTestFBXJoints(std::vector<FBXJoint>& fbxJoints) {
void AnimInverseKinematicsTests::testSingleChain() {
std::vector<FBXJoint> fbxJoints;
AnimPose offset;
makeTestFBXJoints(fbxJoints, offset);
makeTestFBXJoints(fbxJoints);
// create a skeleton and doll
AnimSkeleton* skeleton = new AnimSkeleton(fbxJoints);
AnimPose offset;
AnimSkeleton* skeleton = new AnimSkeleton(fbxJoints, offset);
AnimSkeleton::Pointer skeletonPtr(skeleton);
AnimInverseKinematics ikDoll("doll");
ikDoll.setSkeleton(skeletonPtr);
@ -130,10 +129,13 @@ void AnimInverseKinematicsTests::testSingleChain() {
//
// A------>B------>C------>D
//
int indexD = 3;
glm::vec3 targetPosition(2.0f, 1.0f, 0.0f);
glm::quat targetRotation = glm::angleAxis(PI / 2.0f, zAxis);
ikDoll.updateTarget(indexD, targetPosition, targetRotation);
AnimVariantMap varMap;
varMap.set("positionD", targetPosition);
varMap.set("rotationD", targetRotation);
ikDoll.setTargetVars("D", "positionD", "rotationD");
AnimNode::Triggers triggers;
// the IK solution should be:
//
@ -143,7 +145,7 @@ void AnimInverseKinematicsTests::testSingleChain() {
// A------>B------>C
//
float dt = 1.0f;
ikDoll.evaluate(dt);
ikDoll.evaluate(varMap, dt, triggers);
// verify absolute results
// NOTE: since we expect this solution to converge very quickly (one loop)
@ -204,17 +206,20 @@ void AnimInverseKinematicsTests::testSingleChain() {
// |
// A------>B t
//
int indexD = 3;
glm::vec3 targetPosition(3.0f, 0.0f, 0.0f);
glm::quat targetRotation = identity;
ikDoll.updateTarget(indexD, targetPosition, targetRotation);
AnimVariantMap varMap;
varMap.set("positionD", targetPosition);
varMap.set("rotationD", targetRotation);
ikDoll.setTargetVars("D", "positionD", "rotationD");
AnimNode::Triggers triggers;
// the IK solution should be:
//
// A------>B------>C------>D
//
float dt = 1.0f;
ikDoll.evaluate(dt);
ikDoll.evaluate(varMap, dt, triggers);
// verify absolute results
// NOTE: the IK algorithm doesn't converge very fast for full-reach targets,

View file

@ -193,6 +193,7 @@ void RotationConstraintTests::testSwingTwistConstraint() {
glm::quat inputRotation = swingRotation * twistRotation * referenceRotation;
glm::quat outputRotation = inputRotation;
shoulder.clearHistory();
bool updated = shoulder.apply(outputRotation);
QVERIFY(updated == false);
QCOMPARE_WITH_ABS_ERROR(inputRotation, outputRotation, EPSILON);
@ -223,6 +224,7 @@ void RotationConstraintTests::testSwingTwistConstraint() {
glm::quat inputRotation = swingRotation * twistRotation * referenceRotation;
glm::quat outputRotation = inputRotation;
shoulder.clearHistory();
bool updated = shoulder.apply(outputRotation);
QVERIFY(updated == true);
@ -257,6 +259,7 @@ void RotationConstraintTests::testSwingTwistConstraint() {
glm::quat inputRotation = swingRotation * twistRotation * referenceRotation;
glm::quat outputRotation = inputRotation;
shoulder.clearHistory();
bool updated = shoulder.apply(outputRotation);
QVERIFY(updated == true);
@ -291,6 +294,7 @@ void RotationConstraintTests::testSwingTwistConstraint() {
glm::quat inputRotation = swingRotation * twistRotation * referenceRotation;
glm::quat outputRotation = inputRotation;
shoulder.clearHistory();
bool updated = shoulder.apply(outputRotation);
QVERIFY(updated == true);

View file

@ -52,190 +52,426 @@
"children": []
},
{
"id": "mainStateMachine",
"type": "stateMachine",
"id": "rightHandOverlay",
"type": "overlay",
"data": {
"currentState": "idle",
"states": [
{
"id": "idle",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "walkFwd",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "walkBwd",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "strafeRight",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "strafeLeft",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "turnRight",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotTurning", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "turnLeft",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotTurning", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" }
]
}
]
"alpha": 1.0,
"boneSet": "rightHand",
"alphaVar": "rightHandOverlayAlpha"
},
"children": [
{
"id": "idle",
"type": "clip",
"id": "rightHandStateMachine",
"type": "stateMachine",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx",
"startFrame": 0.0,
"endFrame": 90.0,
"timeScale": 1.0,
"loopFlag": true
"currentState": "rightHandIdle",
"states": [
{
"id": "rightHandIdle",
"interpTarget": 3,
"interpDuration": 3,
"transitions": [
{ "var": "isRightHandPoint", "state": "rightHandPointIntro" },
{ "var": "isRightHandGrab", "state": "rightHandGrab" }
]
},
{
"id": "rightHandPointIntro",
"interpTarget": 3,
"interpDuration": 3,
"transitions": [
{ "var": "isRightHandIdle", "state": "rightHandIdle" },
{ "var": "isRightHandPointIntroOnDone", "state": "rightHandPointHold" },
{ "var": "isRightHandGrab", "state": "rightHandGrab" }
]
},
{
"id": "rightHandPointHold",
"interpTarget": 3,
"interpDuration": 3,
"transitions": [
{ "var": "isRightHandIdle", "state": "rightHandPointOutro" },
{ "var": "isRightHandGrab", "state": "rightHandGrab" }
]
},
{
"id": "rightHandPointOutro",
"interpTarget": 3,
"interpDuration": 3,
"transitions": [
{ "var": "isRightHandPointOutroOnDone", "state": "rightHandIdle" },
{ "var": "isRightHandGrab", "state": "rightHandGrab" },
{ "var": "isRightHandPoint", "state": "rightHandPointHold" }
]
},
{
"id": "rightHandGrab",
"interpTarget": 3,
"interpDuration": 3,
"transitions": [
{ "var": "isRightHandIdle", "state": "rightHandIdle" },
{ "var": "isRightHandPoint_DISABLED", "state": "rightHandPointHold" }
]
}
]
},
"children": []
"children": [
{
"id": "rightHandIdle",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx",
"startFrame": 0.0,
"endFrame": 0.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "rightHandPointHold",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx",
"startFrame": 12.0,
"endFrame": 12.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "rightHandPointIntro",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx",
"startFrame": 0.0,
"endFrame": 12.0,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "rightHandPointOutro",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx",
"startFrame": 0.0,
"endFrame": 65.0,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "rightHandGrab",
"type": "blendLinear",
"data": {
"alpha": 0.0,
"alphaVar": "rightHandGrabBlend"
},
"children": [
{
"id": "rightHandOpen",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/hand_anims/right_hand_point.fbx",
"startFrame": 0.0,
"endFrame": 0.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "rightHandClose",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/right_hand_anim.fbx",
"startFrame": 15.0,
"endFrame": 15.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
}
]
}
]
},
{
"id": "walkFwd",
"type": "clip",
"id": "leftHandOverlay",
"type": "overlay",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx",
"startFrame": 0.0,
"endFrame": 35.0,
"timeScale": 1.0,
"loopFlag": true,
"timeScaleVar": "walkTimeScale"
"alpha": 1.0,
"boneSet": "leftHand",
"alphaVar" : "leftHandOverlay"
},
"children": []
},
{
"id": "walkBwd",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx",
"startFrame": 0.0,
"endFrame": 37.0,
"timeScale": 1.0,
"loopFlag": true,
"timeScaleVar": "walkTimeScale"
},
"children": []
},
{
"id": "turnLeft",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx",
"startFrame": 0.0,
"endFrame": 28.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "turnRight",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx",
"startFrame": 0.0,
"endFrame": 30.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "strafeLeft",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx",
"startFrame": 0.0,
"endFrame": 31.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "strafeRight",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx",
"startFrame": 0.0,
"endFrame": 31.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
"children": [
{
"id": "leftHandStateMachine",
"type": "stateMachine",
"data": {
"currentState": "leftHandIdle",
"states": [
{
"id": "leftHandIdle",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isLeftHandPoint", "state": "leftHandPoint" },
{ "var": "isLeftHandGrab", "state": "leftHandGrab" }
]
},
{
"id": "leftHandPoint",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isLeftHandIdle", "state": "leftHandIdle" },
{ "var": "isLeftHandGrab", "state": "leftHandGrab" }
]
},
{
"id": "leftHandGrab",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isLeftHandIdle", "state": "leftHandIdle" },
{ "var": "isLeftHandPoint", "state": "leftHandPoint" }
]
}
]
},
"children": [
{
"id": "leftHandIdle",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/left_hand_anim.fbx",
"startFrame": 30.0,
"endFrame": 30.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "leftHandPoint",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/left_hand_anim.fbx",
"startFrame": 0.0,
"endFrame": 0.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "leftHandGrab",
"type": "clip",
"data": {
"url": "http://hifi-public.s3.amazonaws.com/ozan/anim/squeeze_hands/left_hand_anim.fbx",
"startFrame": 15.0,
"endFrame": 15.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
}
]
},
{
"id": "mainStateMachine",
"type": "stateMachine",
"data": {
"currentState": "idle",
"states": [
{
"id": "idle",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "walkFwd",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "walkBwd",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "strafeRight",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "strafeLeft",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "turnRight",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotTurning", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "turnLeft",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotTurning", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" }
]
}
]
},
"children": [
{
"id": "idle",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx",
"startFrame": 0.0,
"endFrame": 90.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "walkFwd",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx",
"startFrame": 0.0,
"endFrame": 35.0,
"timeScale": 1.0,
"loopFlag": true,
"timeScaleVar": "walkTimeScale"
},
"children": []
},
{
"id": "walkBwd",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx",
"startFrame": 0.0,
"endFrame": 37.0,
"timeScale": 1.0,
"loopFlag": true,
"timeScaleVar": "walkTimeScale"
},
"children": []
},
{
"id": "turnLeft",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx",
"startFrame": 0.0,
"endFrame": 28.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "turnRight",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx",
"startFrame": 0.0,
"endFrame": 30.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "strafeLeft",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx",
"startFrame": 0.0,
"endFrame": 31.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "strafeRight",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx",
"startFrame": 0.0,
"endFrame": 31.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
}
]
}
]
}
]
}