diff --git a/.gitignore b/.gitignore index c296a918af..8d537b993f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,9 @@ interface/resources/visage/* interface/external/faceplus/* !interface/external/faceplus/readme.txt +# Ignore PrioVR +interface/external/priovr/* +!interface/external/priovr/readme.txt + # Ignore interfaceCache for Linux users interface/interfaceCache/ diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index e783001228..6436ffbd75 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -33,6 +33,7 @@ link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(animation ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(script-engine ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(embedded-webserver ${TARGET_NAME} "${ROOT_DIR}") diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 9b2134ba44..e6c14d06da 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -250,6 +250,12 @@ void Agent::run() { _particleViewer.init(); _scriptEngine.getParticlesScriptingInterface()->setParticleTree(_particleViewer.getTree()); + _scriptEngine.registerGlobalObject("ModelViewer", &_modelViewer); + JurisdictionListener* modelJL = _scriptEngine.getModelsScriptingInterface()->getJurisdictionListener(); + _modelViewer.setJurisdictionListener(modelJL); + _modelViewer.init(); + _scriptEngine.getModelsScriptingInterface()->setModelTree(_modelViewer.getTree()); + _scriptEngine.setScriptContents(scriptContents); _scriptEngine.run(); setFinished(true); diff --git a/cmake/modules/FindPrioVR.cmake b/cmake/modules/FindPrioVR.cmake new file mode 100644 index 0000000000..6e78045321 --- /dev/null +++ b/cmake/modules/FindPrioVR.cmake @@ -0,0 +1,42 @@ +# Try to find the PrioVT library +# +# You must provide a PRIOVR_ROOT_DIR which contains lib and include directories +# +# Once done this will define +# +# PRIOVR_FOUND - system found PrioVR +# PRIOVR_INCLUDE_DIRS - the PrioVR include directory +# PRIOVR_LIBRARIES - Link this to use PrioVR +# +# Created on 5/12/2014 by Andrzej Kapolka +# Copyright (c) 2014 High Fidelity +# + +if (PRIOVR_LIBRARIES AND PRIOVR_INCLUDE_DIRS) + # in cache already + set(PRIOVR_FOUND TRUE) +else (PRIOVR_LIBRARIES AND PRIOVR_INCLUDE_DIRS) + find_path(PRIOVR_INCLUDE_DIRS yei_skeletal_api.h ${PRIOVR_ROOT_DIR}/include) + + if (WIN32) + find_library(PRIOVR_LIBRARIES Skeletal_API.lib ${PRIOVR_ROOT_DIR}/lib) + endif (WIN32) + + if (PRIOVR_INCLUDE_DIRS AND PRIOVR_LIBRARIES) + set(PRIOVR_FOUND TRUE) + endif (PRIOVR_INCLUDE_DIRS AND PRIOVR_LIBRARIES) + + if (PRIOVR_FOUND) + if (NOT PRIOVR_FIND_QUIETLY) + message(STATUS "Found PrioVR... ${PRIOVR_LIBRARIES}") + endif (NOT PRIOVR_FIND_QUIETLY) + else () + if (PRIOVR_FIND_REQUIRED) + message(FATAL_ERROR "Could not find PrioVR") + endif (PRIOVR_FIND_REQUIRED) + endif () + + # show the PRIOVR_INCLUDE_DIRS and PRIOVR_LIBRARIES variables only in the advanced view + mark_as_advanced(PRIOVR_INCLUDE_DIRS PRIOVR_LIBRARIES) + +endif (PRIOVR_LIBRARIES AND PRIOVR_INCLUDE_DIRS) diff --git a/examples/Test.js b/examples/Test.js new file mode 100644 index 0000000000..056ec3cbbf --- /dev/null +++ b/examples/Test.js @@ -0,0 +1,68 @@ +// +// Test.js +// examples +// +// Created by Ryan Huffman on 5/4/14 +// Copyright 2014 High Fidelity, Inc. +// +// This provides very basic unit testing functionality. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +test = function(name, func) { + print("Running test: " + name); + + var unitTest = new UnitTest(name, func); + + try { + unitTest.run(); + print(" Success: " + unitTest.numAssertions + " assertions passed"); + } catch (error) { + print(" Failure: " + error.message); + } +}; + +AssertionException = function(expected, actual, message) { + print("Creating exception"); + this.message = message + "\n: " + actual + " != " + expected; + this.name = 'AssertionException'; +}; + +UnitTest = function(name, func) { + this.numAssertions = 0; + this.func = func; +}; + +UnitTest.prototype.run = function() { + this.func(); +}; + +UnitTest.prototype.assertNotEquals = function(expected, actual, message) { + this.numAssertions++; + if (expected == actual) { + throw new AssertionException(expected, actual, message); + } +}; + +UnitTest.prototype.assertEquals = function(expected, actual, message) { + this.numAssertions++; + if (expected != actual) { + throw new AssertionException(expected, actual, message); + } +}; + +UnitTest.prototype.assertHasProperty = function(property, actual, message) { + this.numAssertions++; + if (actual[property] === undefined) { + throw new AssertionException(property, actual, message); + } +}; + +UnitTest.prototype.assertNull = function(value, message) { + this.numAssertions++; + if (value !== null) { + throw new AssertionException(value, null, message); + } +}; diff --git a/examples/animatedModelExample.js b/examples/animatedModelExample.js new file mode 100644 index 0000000000..5199eb419f --- /dev/null +++ b/examples/animatedModelExample.js @@ -0,0 +1,124 @@ +// +// animatedModelExample.js +// examples +// +// Created by Brad Hefta-Gaub on 12/31/13. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example script that demonstrates creating and editing a model +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var count = 0; +var moveUntil = 6000; +var stopAfter = moveUntil + 100; + +var pitch = 0.0; +var yaw = 0.0; +var roll = 0.0; +var rotation = Quat.fromPitchYawRollDegrees(pitch, yaw, roll) + +var originalProperties = { + position: { x: 10, + y: 0, + z: 0 }, + + radius : 1, + + color: { red: 0, + green: 255, + blue: 0 }, + + modelURL: "http://www.fungibleinsight.com/faces/beta.fst", + modelRotation: rotation, + animationURL: "http://www.fungibleinsight.com/faces/gangnam_style_2.fbx", + animationIsPlaying: true, +}; + +var modelID = Models.addModel(originalProperties); +print("Models.addModel()... modelID.creatorTokenID = " + modelID.creatorTokenID); + +var isPlaying = true; +var playPauseEveryWhile = 360; +var animationFPS = 30; +var adjustFPSEveryWhile = 120; +var resetFrameEveryWhile = 600; + +function moveModel(deltaTime) { + var somethingChanged = false; + if (count % playPauseEveryWhile == 0) { + isPlaying = !isPlaying; + print("isPlaying=" + isPlaying); + somethingChanged = true; + } + + if (count % adjustFPSEveryWhile == 0) { + if (animationFPS == 30) { + animationFPS = 10; + } else if (animationFPS == 10) { + animationFPS = 60; + } else if (animationFPS == 60) { + animationFPS = 30; + } + print("animationFPS=" + animationFPS); + isPlaying = true; + print("always start playing if we change the FPS -- isPlaying=" + isPlaying); + somethingChanged = true; + } + + if (count % resetFrameEveryWhile == 0) { + resetFrame = true; + somethingChanged = true; + } + + if (count >= moveUntil) { + + // delete it... + if (count == moveUntil) { + print("calling Models.deleteModel()"); + Models.deleteModel(modelID); + } + + // stop it... + if (count >= stopAfter) { + print("calling Script.stop()"); + Script.stop(); + } + + count++; + return; // break early + } + + count++; + + //print("modelID.creatorTokenID = " + modelID.creatorTokenID); + + if (somethingChanged) { + var newProperties = { + animationIsPlaying: isPlaying, + animationFPS: animationFPS, + }; + + if (resetFrame) { + print("resetting the frame!"); + newProperties.animationFrameIndex = 0; + resetFrame = false; + } + + Models.editModel(modelID, newProperties); + } +} + + +// register the call back so it fires before each data send +Script.update.connect(moveModel); + + +Script.scriptEnding.connect(function () { + print("cleaning up..."); + print("modelID="+ modelID.creatorTokenID + ", id:" + modelID.id); + Models.deleteModel(modelID); +}); + diff --git a/examples/editModels.js b/examples/editModels.js index 384d2f75a8..70a2e178ae 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -9,11 +9,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +Script.include("toolBars.js"); + var windowDimensions = Controller.getViewportDimensions(); +var toolIconUrl = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/tools/"; +var toolHeight = 50; +var toolWidth = 50; var LASER_WIDTH = 4; var LASER_COLOR = { red: 255, green: 0, blue: 0 }; -var LASER_LENGTH_FACTOR = 1.5; +var LASER_LENGTH_FACTOR = 5; var LEFT = 0; var RIGHT = 1; @@ -33,24 +38,7 @@ var modelURLs = [ "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx", ]; -var toolIconUrl = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/tools/"; -var numberOfTools = 1; -var toolHeight = 50; -var toolWidth = 50; -var toolVerticalSpacing = 4; -var toolsHeight = toolHeight * numberOfTools + toolVerticalSpacing * (numberOfTools - 1); -var toolsX = windowDimensions.x - 8 - toolWidth; -var toolsY = (windowDimensions.y - toolsHeight) / 2; - - -var firstModel = Overlays.addOverlay("image", { - x: 0, y: 0, width: toolWidth, height: toolHeight, - subImage: { x: 0, y: toolHeight, width: toolWidth, height: toolHeight }, - imageURL: toolIconUrl + "voxel-tool.svg", - x: toolsX, y: toolsY + ((toolHeight + toolVerticalSpacing) * 0), width: toolWidth, height: toolHeight, - visible: true, - alpha: 0.9 - }); +var toolBar; function controller(wichSide) { this.side = wichSide; @@ -206,6 +194,13 @@ function controller(wichSide) { }); } + this.hideLaser = function() { + Overlays.editOverlay(this.laser, { visible: false }); + Overlays.editOverlay(this.ball, { visible: false }); + Overlays.editOverlay(this.leftRight, { visible: false }); + Overlays.editOverlay(this.topDown, { visible: false }); + } + this.moveModel = function () { if (this.grabbing) { var newPosition = Vec3.sum(this.palmPosition, @@ -345,67 +340,272 @@ function moveModels() { rightController.moveModel(); } +var hydraConnected = false; function checkController(deltaTime) { var numberOfButtons = Controller.getNumberOfButtons(); var numberOfTriggers = Controller.getNumberOfTriggers(); var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; - - moveOverlays(); // this is expected for hydras - if (!(numberOfButtons==12 && numberOfTriggers == 2 && controllersPerTrigger == 2)) { - //print("no hydra connected?"); - return; // bail if no hydra + if (numberOfButtons==12 && numberOfTriggers == 2 && controllersPerTrigger == 2) { + if (!hydraConnected) { + hydraConnected = true; + } + + leftController.update(); + rightController.update(); + moveModels(); + } else { + if (hydraConnected) { + hydraConnected = false; + + leftController.hideLaser(); + rightController.hideLaser(); + } } - leftController.update(); - rightController.update(); - moveModels(); + moveOverlays(); +} + + + +function initToolBar() { + toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); + // New Model + newModel = toolBar.addTool({ + imageURL: toolIconUrl + "voxel-tool.svg", + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, height: toolHeight, + visible: true, + alpha: 0.9 + }); } function moveOverlays() { - windowDimensions = Controller.getViewportDimensions(); - - toolsX = windowDimensions.x - 8 - toolWidth; - toolsY = (windowDimensions.y - toolsHeight) / 2; - - Overlays.editOverlay(firstModel, { - x: toolsX, y: toolsY + ((toolHeight + toolVerticalSpacing) * 0), width: toolWidth, height: toolHeight, - }); -} - -function mousePressEvent(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); - var url; - - if (clickedOverlay == firstModel) { - url = Window.prompt("Model url", modelURLs[Math.floor(Math.random() * modelURLs.length)]); - if (url == null) { - return; } - } else { - print("Didn't click on anything"); + if (typeof(toolBar) === 'undefined') { + initToolBar(); + + } else if (windowDimensions.x == Controller.getViewportDimensions().x && + windowDimensions.y == Controller.getViewportDimensions().y) { return; } - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); - Models.addModel({ position: position, - radius: radiusDefault, - modelURL: url - }); + + windowDimensions = Controller.getViewportDimensions(); + var toolsX = windowDimensions.x - 8 - toolBar.width; + var toolsY = (windowDimensions.y - toolBar.height) / 2; + + toolBar.move(toolsX, toolsY); +} + + + +var modelSelected = false; +var selectedModelID; +var selectedModelProperties; +var mouseLastPosition; +var orientation; +var intersection; + + +var SCALE_FACTOR = 200.0; +var TRANSLATION_FACTOR = 100.0; +var ROTATION_FACTOR = 100.0; + +function rayPlaneIntersection(pickRay, point, normal) { + var d = -Vec3.dot(point, normal); + var t = -(Vec3.dot(pickRay.origin, normal) + d) / Vec3.dot(pickRay.direction, normal); + + return Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, t)); +} + +function mousePressEvent(event) { + mouseLastPosition = { x: event.x, y: event.y }; + modelSelected = false; + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + + if (newModel == toolBar.clicked(clickedOverlay)) { + var url = Window.prompt("Model url", modelURLs[Math.floor(Math.random() * modelURLs.length)]); + if (url == null) { + return; + } + + var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); + Models.addModel({ position: position, + radius: radiusDefault, + modelURL: url + }); + + } else { + var pickRay = Camera.computePickRay(event.x, event.y); + Vec3.print("[Mouse] Looking at: ", pickRay.origin); + var foundModels = Models.findModels(pickRay.origin, LASER_LENGTH_FACTOR); + for (var i = 0; i < foundModels.length; i++) { + if (!foundModels[i].isKnownID) { + var identify = Models.identifyModel(foundModels[i]); + if (!identify.isKnownID) { + print("Unknown ID " + identify.id + "(update loop)"); + continue; + } + foundModels[i] = identify; + } + + var properties = Models.getModelProperties(foundModels[i]); + print("Checking properties: " + properties.id + " " + properties.isKnownID); + + // P P - Model + // /| A - Palm + // / | d B - unit vector toward tip + // / | X - base of the perpendicular line + // A---X----->B d - distance fom axis + // x x - distance from A + // + // |X-A| = (P-A).B + // X == A + ((P-A).B)B + // d = |P-X| + + var A = pickRay.origin; + var B = Vec3.normalize(pickRay.direction); + var P = properties.position; + + var x = Vec3.dot(Vec3.subtract(P, A), B); + var X = Vec3.sum(A, Vec3.multiply(B, x)); + var d = Vec3.length(Vec3.subtract(P, X)); + + if (d < properties.radius && 0 < x && x < LASER_LENGTH_FACTOR) { + modelSelected = true; + selectedModelID = foundModels[i]; + selectedModelProperties = properties; + + selectedModelProperties.oldRadius = selectedModelProperties.radius; + selectedModelProperties.oldPosition = { + x: selectedModelProperties.position.x, + y: selectedModelProperties.position.y, + z: selectedModelProperties.position.z, + }; + selectedModelProperties.oldRotation = { + x: selectedModelProperties.modelRotation.x, + y: selectedModelProperties.modelRotation.y, + z: selectedModelProperties.modelRotation.z, + w: selectedModelProperties.modelRotation.w, + }; + + + orientation = MyAvatar.orientation; + intersection = rayPlaneIntersection(pickRay, P, Quat.getFront(orientation)); + print("Clicked on " + selectedModelID.id + " " + modelSelected); + + return; + } + } + } +} + +var oldModifier = 0; +var modifier = 0; +var wasShifted = false; +function mouseMoveEvent(event) { + if (!modelSelected) { + return; + } + + if (event.isLeftButton) { + if (event.isRightButton) { + modifier = 1; // Scale + } else { + modifier = 2; // Translate + } + } else if (event.isRightButton) { + modifier = 3; // rotate + } else { + modifier = 0; + } + + var pickRay = Camera.computePickRay(event.x, event.y); + if (wasShifted != event.isShifted || modifier != oldModifier) { + selectedModelProperties.oldRadius = selectedModelProperties.radius; + + selectedModelProperties.oldPosition = { + x: selectedModelProperties.position.x, + y: selectedModelProperties.position.y, + z: selectedModelProperties.position.z, + }; + selectedModelProperties.oldRotation = { + x: selectedModelProperties.modelRotation.x, + y: selectedModelProperties.modelRotation.y, + z: selectedModelProperties.modelRotation.z, + w: selectedModelProperties.modelRotation.w, + }; + orientation = MyAvatar.orientation; + intersection = rayPlaneIntersection(pickRay, + selectedModelProperties.oldPosition, + Quat.getFront(orientation)); + + mouseLastPosition = { x: event.x, y: event.y }; + wasShifted = event.isShifted; + oldModifier = modifier; + return; + } + + + switch (modifier) { + case 0: + return; + case 1: + // Let's Scale + selectedModelProperties.radius = (selectedModelProperties.oldRadius * + (1.0 + (mouseLastPosition.y - event.y) / SCALE_FACTOR)); + + if (selectedModelProperties.radius < 0.01) { + print("Scale too small ... bailling."); + return; + } + break; + + case 2: + // Let's translate + var newIntersection = rayPlaneIntersection(pickRay, + selectedModelProperties.oldPosition, + Quat.getFront(orientation)); + var vector = Vec3.subtract(newIntersection, intersection) + if (event.isShifted) { + var i = Vec3.dot(vector, Quat.getRight(orientation)); + var j = Vec3.dot(vector, Quat.getUp(orientation)); + vector = Vec3.sum(Vec3.multiply(Quat.getRight(orientation), i), + Vec3.multiply(Quat.getFront(orientation), j)); + } + + selectedModelProperties.position = Vec3.sum(selectedModelProperties.oldPosition, vector); + break; + case 3: + // Let's rotate + var rotation = Quat.fromVec3Degrees({ x: event.y - mouseLastPosition.y, y: event.x - mouseLastPosition.x, z: 0 }); + if (event.isShifted) { + rotation = Quat.fromVec3Degrees({ x: event.y - mouseLastPosition.y, y: 0, z: mouseLastPosition.x - event.x }); + } + + var newRotation = Quat.multiply(orientation, rotation); + newRotation = Quat.multiply(newRotation, Quat.inverse(orientation)); + + selectedModelProperties.modelRotation = Quat.multiply(newRotation, selectedModelProperties.oldRotation); + break; + } + + Models.editModel(selectedModelID, selectedModelProperties); } function scriptEnding() { leftController.cleanup(); rightController.cleanup(); - - Overlays.deleteOverlay(firstModel); + toolBar.cleanup(); } Script.scriptEnding.connect(scriptEnding); // register the call back so it fires before each data send Script.update.connect(checkController); Controller.mousePressEvent.connect(mousePressEvent); +Controller.mouseMoveEvent.connect(mouseMoveEvent); diff --git a/examples/placeModelsWithHands.js b/examples/placeModelsWithHands.js index e1ac151fe4..47b4615924 100644 --- a/examples/placeModelsWithHands.js +++ b/examples/placeModelsWithHands.js @@ -37,6 +37,7 @@ var radiusMinimum = 0.05; var radiusMaximum = 0.5; var modelURLs = [ + "http://www.fungibleinsight.com/faces/beta.fst", "https://s3-us-west-1.amazonaws.com/highfidelity-public/models/attachments/topHat.fst", "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Feisar_Ship.FBX", "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/birarda/birarda_head.fbx", @@ -48,6 +49,19 @@ var modelURLs = [ "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx", ]; +var animationURLs = [ + "http://www.fungibleinsight.com/faces/gangnam_style_2.fbx", + "", + "", + "", + "", + "", + "", + "", + "", + "", +]; + var currentModelURL = 1; var numModels = modelURLs.length; @@ -214,10 +228,19 @@ function checkControllerSide(whichSide) { modelRotation: palmRotation, modelURL: modelURLs[currentModelURL] }; + + if (animationURLs[currentModelURL] !== "") { + properties.animationURL = animationURLs[currentModelURL]; + properties.animationIsPlaying = true; + } debugPrint("modelRadius=" +modelRadius); newModel = Models.addModel(properties); + + print("just added model... newModel=" + newModel.creatorTokenID); + print("properties.animationURL=" + properties.animationURL); + if (whichSide == LEFT_PALM) { leftModelAlreadyInHand = true; leftHandModel = newModel; diff --git a/examples/streetAreaExample.js b/examples/streetAreaExample.js new file mode 100644 index 0000000000..b4efd99b70 --- /dev/null +++ b/examples/streetAreaExample.js @@ -0,0 +1,51 @@ +// +// streetAreaExample.js +// examples +// +// Created by Ryan Huffman on 5/4/14 +// Copyright 2014 High Fidelity, Inc. +// +// This is an example script showing how to load JSON data using XMLHttpRequest. +// +// URL Macro created by Thijs Wenker. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var url = "https://script.google.com/macros/s/AKfycbwIo4lmF-qUwX1Z-9eA_P-g2gse9oFhNcjVyyksGukyDDEFXgU/exec?action=listOwners&domain=alpha.highfidelity.io"; +print("Loading street data from " + url); + +var req = new XMLHttpRequest(); + +// Set response type to "json". This will tell XMLHttpRequest to parse the response data as json, so req.response can be used +// as a regular javascript object +req.responseType = 'json'; + +req.open("GET", url, false); +req.send(); + +if (req.status == 200) { + for (var domain in req.response) { + print("DOMAIN: " + domain); + var locations = req.response[domain]; + var userAreas = []; + for (var i = 0; i < locations.length; i++) { + var loc = locations[i]; + var x1 = loc[1], + x2 = loc[2], + y1 = loc[3], + y2 = loc[4]; + userAreas.push({ + username: loc[0], + area: Math.abs(x2 - x1) * Math.abs(y2 - y1), + }); + } + userAreas.sort(function(a, b) { return a.area > b.area ? -1 : (a.area < b.area ? 1 : 0) }); + for (var i = 0; i < userAreas.length; i++) { + print(userAreas[i].username + ": " + userAreas[i].area + " sq units"); + } + } +} else { + print("Error loading data: " + req.status + " " + req.statusText + ", " + req.errorCode); +} diff --git a/examples/testXMLHttpRequest.js b/examples/testXMLHttpRequest.js new file mode 100644 index 0000000000..421eb458e4 --- /dev/null +++ b/examples/testXMLHttpRequest.js @@ -0,0 +1,147 @@ +// +// testXMLHttpRequest.js +// examples +// +// Created by Ryan Huffman on 5/4/14 +// Copyright 2014 High Fidelity, Inc. +// +// XMLHttpRequest Tests +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("Test.js"); + +test("Test default request values", function(finished) { + var req = new XMLHttpRequest(); + + this.assertEquals(req.UNSENT, req.readyState, "readyState should be UNSENT"); + this.assertEquals(0, req.status, "status should be `0` by default"); + this.assertEquals("", req.statusText, "statusText should be empty string by default"); + this.assertEquals("", req.getAllResponseHeaders(), "getAllResponseHeaders() should return empty string by default"); + this.assertEquals("", req.response, "response should be empty string by default"); + this.assertEquals("", req.responseText, "responseText should be empty string by default"); + this.assertEquals("", req.responseType, "responseType should be empty string by default"); + this.assertEquals(0, req.timeout, "timeout should be `0` by default"); + this.assertEquals(0, req.errorCode, "there should be no error by default"); +}); + + +test("Test readyStates", function() { + var req = new XMLHttpRequest(); + var state = req.readyState; + + var statesVisited = [true, false, false, false, false] + + req.onreadystatechange = function() { + statesVisited[req.readyState] = true; + }; + + req.open("GET", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false); + req.send(); + for (var i = 0; i <= req.DONE; i++) { + this.assertEquals(true, statesVisited[i], i + " should be set"); + } + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); +}); + +test("Test TEXT request", function() { + var req = new XMLHttpRequest(); + var state = req.readyState; + + req.open("GET", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(200, req.status, "status should be `200`"); + this.assertEquals(0, req.errorCode); + this.assertEquals("OK", req.statusText, "statusText should be `OK`"); + this.assertNotEquals("", req.getAllResponseHeaders(), "headers should no longer be empty string"); + this.assertNull(req.getResponseHeader('invalidheader'), "invalid header should return `null`"); + this.assertEquals("GitHub.com", req.getResponseHeader('Server'), "Server header should be GitHub.com"); + this.assertEquals('{"id": 1}', req.response); + this.assertEquals('{"id": 1}', req.responseText); +}); + +test("Test JSON request", function() { + var req = new XMLHttpRequest(); + var state = req.readyState; + + req.responseType = "json"; + req.open("GET", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(200, req.status, "status should be `200`"); + this.assertEquals(0, req.errorCode); + this.assertEquals("OK", req.statusText, "statusText should be `OK`"); + this.assertNotEquals("", req.getAllResponseHeaders(), "headers should no longer be empty string"); + this.assertNull(req.getResponseHeader('invalidheader'), "invalid header should return `null`"); + this.assertEquals("GitHub.com", req.getResponseHeader('Server'), "Server header should be GitHub.com"); + this.assertHasProperty('id', req.response); + this.assertEquals(1, req.response.id); + this.assertEquals('{"id": 1}', req.responseText); +}); + +test("Test Bad URL", function() { + var req = new XMLHttpRequest(); + var state = req.readyState; + + req.open("POST", "hifi://domain/path", false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertNotEquals(0, req.errorCode); +}); + +test("Test Bad Method Error", function() { + var req = new XMLHttpRequest(); + var state = req.readyState; + + req.open("POST", "https://www.google.com", false); + + req.send("randomdata"); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(405, req.status); + this.assertEquals(202, req.errorCode); + + req.open("POST", "https://www.google.com", false) + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(405, req.status); + this.assertEquals(202, req.errorCode); +}); + +test("Test abort", function() { + var req = new XMLHttpRequest(); + var state = req.readyState; + + req.open("POST", "https://www.google.com", true) + req.send(); + req.abort(); + + this.assertEquals(0, req.status); + this.assertEquals(0, req.errorCode); +}); + +test("Test timeout", function() { + var req = new XMLHttpRequest(); + var state = req.readyState; + var timedOut = false; + + req.ontimeout = function() { + timedOut = true; + }; + + req.open("POST", "https://gist.githubusercontent.com/huffman/33cc618fec183d1bccd0/raw/test.json", false) + req.timeout = 1; + req.send(); + + this.assertEquals(true, timedOut, "request should have timed out"); + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(0, req.status, "status should be `0`"); + this.assertEquals(4, req.errorCode, "4 is the timeout error code for QNetworkReply::NetworkError"); +}); diff --git a/examples/toolBars.js b/examples/toolBars.js new file mode 100644 index 0000000000..88b07276f0 --- /dev/null +++ b/examples/toolBars.js @@ -0,0 +1,197 @@ +// +// toolBars.js +// examples +// +// Created by ClĂ©ment Brisset on 5/7/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Overlay2D = function(properties, overlay) { // overlay is an optionnal variable + if (!(typeof(properties) === 'undefined')) { + if(typeof(overlay) === 'undefined') { + overlay = Overlays.addOverlay("image", properties); + } else { + Overlays.editOverlay(overlay, properties); + } + } + + this.overlay = function() { + return overlay; + } + this.x = function() { + return properties.x; + } + this.y = function() { + return properties.y; + } + this.width = function() { + return properties.width; + } + this.height = function() { + return properties.height; + } + this.alpha = function() { + return properties.alpha; + } + this.visible = function() { + return properties.visible; + } + + + this.move = function(x, y) { + properties.x = x; + properties.y = y; + Overlays.editOverlay(overlay, { x: x, y: y }); + } + this.resize = function(width, height) { + properties.width = width; + properties.height = height; + Overlays.editOverlay(overlay, { width: width, height: height }); + } + this.setAlpha = function(alpha) { + properties.alpha = alpha; + Overlays.editOverlay(overlay, { alpha: alpha }); + } + this.show = function(doShow) { + properties.visible = doShow; + Overlays.editOverlay(overlay, { visible: doShow }); + } + + this.clicked = function(clickedOverlay) { + return (overlay == clickedOverlay ? true : false); + } + + this.cleanup = function() { + print("Cleanup"); + Overlays.deleteOverlay(overlay); + } +} + + +Tool = function(properties, selectable, selected) { // selectable and selected are optional variables. + Overlay2D.call(this, properties); + + if(typeof(selectable)==='undefined') { + selectable = false; + if(typeof(selected)==='undefined') { + selected = false; + + } + } + + this.selectable = function() { + return selectable; + } + + this.selected = function() { + return selected; + } + this.select = function(doSelect) { + selected = doSelect; + properties.subImage.y = (selected ? 2 : 1) * properties.subImage.height; + Overlays.editOverlay(this.overlay(), { subImage: properties.subImage }); + } + this.toggle = function() { + selected = !selected; + properties.subImage.y = (selected ? 2 : 1) * properties.subImage.height; + Overlays.editOverlay(this.overlay(), { subImage: properties.subImage }); + + return selected; + } + + this.select(selected); + + this.baseClicked = this.clicked; + this.clicked = function(clickedOverlay) { + if (this.baseClicked(clickedOverlay)) { + if (selectable) { + this.toggle(); + } + return true; + } + return false; + } +} +Tool.prototype = new Overlay2D; +Tool.IMAGE_HEIGHT = 50; +Tool.IMAGE_WIDTH = 50; + +ToolBar = function(x, y, direction) { + this.tools = []; + this.x = x; + this.y = y; + this.width = 0; + this.height = 0; + + + this.addTool = function(properties, selectable, selected) { + if (direction == ToolBar.HORIZONTAL) { + properties.x = this.x + this.width; + properties.y = this.y; + this.width += properties.width + ToolBar.SPACING; + this.height += Math.max(properties.height, this.height); + } else { + properties.x = this.x; + properties.y = this.y + this.height; + this.width = Math.max(properties.width, this.width); + this.height += properties.height + ToolBar.SPACING; + } + + this.tools[this.tools.length] = new Tool(properties, selectable, selected); + return ((this.tools.length) - 1); + } + + this.move = function(x, y) { + var dx = x - this.x; + var dy = y - this.y; + this.x = x; + this.y = y; + for(var tool in this.tools) { + this.tools[tool].move(this.tools[tool].x() + dx, this.tools[tool].y() + dy); + } + } + + this.setAlpha = function(alpha) { + for(var tool in this.tools) { + this.tools[tool].setAlpha(alpha); + } + } + + this.show = function(doShow) { + for(var tool in this.tools) { + this.tools[tool].show(doShow); + } + } + + this.clicked = function(clickedOverlay) { + for(var tool in this.tools) { + if (this.tools[tool].visible() && this.tools[tool].clicked(clickedOverlay)) { + return parseInt(tool); + } + } + return -1; + } + + this.numberOfTools = function() { + return this.tools.length; + } + + this.cleanup = function() { + for(var tool in this.tools) { + this.tools[tool].cleanup(); + delete this.tools[tool]; + } + + this.tools = []; + this.x = x; + this.y = y; + this.width = 0; + this.height = 0; + } +} +ToolBar.SPACING = 4; +ToolBar.VERTICAL = 0; +ToolBar.HORIZONTAL = 1; \ No newline at end of file diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 0a56109260..96c212add6 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -15,6 +15,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../cmake set(FACEPLUS_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/faceplus") set(FACESHIFT_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/faceshift") set(LIBOVR_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/oculus") +set(PRIOVR_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/priovr") set(SIXENSE_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/Sixense") set(VISAGE_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/visage") @@ -127,12 +128,14 @@ link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(avatars ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(audio ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(animation ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(script-engine ${TARGET_NAME} "${ROOT_DIR}") # find any optional libraries find_package(Faceplus) find_package(Faceshift) find_package(LibOVR) +find_package(PrioVR) find_package(Sixense) find_package(Visage) find_package(ZLIB) @@ -183,6 +186,13 @@ if (LIBOVR_FOUND AND NOT DISABLE_LIBOVR) target_link_libraries(${TARGET_NAME} "${LIBOVR_LIBRARIES}") endif (LIBOVR_FOUND AND NOT DISABLE_LIBOVR) +# and with PrioVR library +if (PRIOVR_FOUND AND NOT DISABLE_PRIOVR) + add_definitions(-DHAVE_PRIOVR) + include_directories(SYSTEM "${PRIOVR_INCLUDE_DIRS}") + target_link_libraries(${TARGET_NAME} "${PRIOVR_LIBRARIES}") +endif (PRIOVR_FOUND AND NOT DISABLE_PRIOVR) + # and with qxmpp for chat if (QXMPP_FOUND AND NOT DISABLE_QXMPP) add_definitions(-DHAVE_QXMPP -DQXMPP_STATIC) diff --git a/interface/external/priovr/readme.txt b/interface/external/priovr/readme.txt new file mode 100644 index 0000000000..202a90cf12 --- /dev/null +++ b/interface/external/priovr/readme.txt @@ -0,0 +1,16 @@ + +Instructions for adding the PrioVR driver to Interface +Andrzej Kapolka, May 12, 2014 + +1. Download and install the YEI drivers from https://www.yeitechnology.com/yei-3-space-sensor-software-suite. If using + Window 8+, follow the workaround instructions at http://forum.yeitechnology.com/viewtopic.php?f=3&t=24. + +2. Get the PrioVR skeleton API, open ts_c_api2_priovr2/visual_studio/ThreeSpace_API_2/ThreeSpace_API_2.sln + in Visual Studio, and build it. + +3. Copy ts_c_api2_priovr2/visual_studio/ThreeSpace_API_2/Skeletal_API/yei_skeletal_api.h to interface/external/priovr/include, + ts_c_api2_priovr2/visual_studio/ThreeSpace_API_2/Debug/Skeletal_API.lib to interface/external/priovr/lib, and + ts_c_api2_priovr2/visual_studio/ThreeSpace_API_2/Debug/*.dll to your path. + +4. Delete your build directory, run cmake and build, and you should be all set. + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c23804166c..ebcdcd878d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -170,7 +170,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _packetsPerSecond(0), _bytesPerSecond(0), _previousScriptLocation(), - _logger(new FileLogger(this)), _runningScriptsWidget(new RunningScriptsWidget(_window)), _runningScriptsWidgetWasVisible(false) { @@ -190,6 +189,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : setOrganizationName(applicationInfo.value("organizationName").toString()); setOrganizationDomain(applicationInfo.value("organizationDomain").toString()); + _logger = new FileLogger(this); // After setting organization name in order to get correct directory + QSettings::setDefaultFormat(QSettings::IniFormat); _myAvatar = _avatarManager.getMyAvatar(); @@ -1982,6 +1983,7 @@ void Application::update(float deltaTime) { _myAvatar->updateLookAtTargetAvatar(); updateMyAvatarLookAtPosition(); _sixenseManager.update(deltaTime); + _prioVR.update(); updateMyAvatar(deltaTime); // Sample hardware, update view frustum if needed, and send avatar data to mixer/nodes updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... _avatarManager.updateOtherAvatars(deltaTime); //loop through all the other avatars and simulate them... @@ -2751,6 +2753,9 @@ void Application::displayOverlay() { drawText(_glWidget->width() - 100, _glWidget->height() - timerBottom, 0.30f, 0.0f, 0, frameTimer, WHITE_TEXT); } + // give external parties a change to hook in + emit renderingOverlay(); + _overlays.render2D(); glPopMatrix(); @@ -3059,6 +3064,8 @@ void Application::resetSensors() { OculusManager::reset(); } + _prioVR.reset(); + QCursor::setPos(_mouseX, _mouseY); _myAvatar->reset(); diff --git a/interface/src/Application.h b/interface/src/Application.h index 174ac61c06..1b0c78e5fa 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -58,6 +58,7 @@ #include "avatar/MyAvatar.h" #include "devices/Faceplus.h" #include "devices/Faceshift.h" +#include "devices/PrioVR.h" #include "devices/SixenseManager.h" #include "devices/Visage.h" #include "models/ModelTreeRenderer.h" @@ -194,6 +195,7 @@ public: Visage* getVisage() { return &_visage; } FaceTracker* getActiveFaceTracker(); SixenseManager* getSixenseManager() { return &_sixenseManager; } + PrioVR* getPrioVR() { return &_prioVR; } BandwidthMeter* getBandwidthMeter() { return &_bandwidthMeter; } QUndoStack* getUndoStack() { return &_undoStack; } @@ -267,6 +269,9 @@ signals: /// Fired when we're rendering in-world interface elements; allows external parties to hook in. void renderingInWorldInterface(); + /// Fired when we're rendering the overlay. + void renderingOverlay(); + /// Fired when the import window is closed void importDone(); @@ -442,6 +447,7 @@ private: Visage _visage; SixenseManager _sixenseManager; + PrioVR _prioVR; Camera _myCamera; // My view onto the world Camera _viewFrustumOffsetCamera; // The camera we use to sometimes show the view frustum from an offset mode diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 20e4bcc44e..1eac264ae4 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -111,7 +111,7 @@ void MyAvatar::reset() { void MyAvatar::update(float deltaTime) { Head* head = getHead(); head->relaxLean(deltaTime); - updateFromFaceTracker(deltaTime); + updateFromTrackers(deltaTime); if (Menu::getInstance()->isOptionChecked(MenuOption::MoveWithLean)) { // Faceshift drive is enabled, set the avatar drive based on the head position moveWithLean(); @@ -234,26 +234,33 @@ void MyAvatar::simulate(float deltaTime) { } // Update avatar head rotation with sensor data -void MyAvatar::updateFromFaceTracker(float deltaTime) { +void MyAvatar::updateFromTrackers(float deltaTime) { glm::vec3 estimatedPosition, estimatedRotation; - FaceTracker* tracker = Application::getInstance()->getActiveFaceTracker(); - if (tracker) { - estimatedPosition = tracker->getHeadTranslation(); - estimatedRotation = glm::degrees(safeEulerAngles(tracker->getHeadRotation())); - - // Rotate the body if the head is turned beyond the screen - if (Menu::getInstance()->isOptionChecked(MenuOption::TurnWithHead)) { - const float TRACKER_YAW_TURN_SENSITIVITY = 0.5f; - const float TRACKER_MIN_YAW_TURN = 15.0f; - const float TRACKER_MAX_YAW_TURN = 50.0f; - if ( (fabs(estimatedRotation.y) > TRACKER_MIN_YAW_TURN) && - (fabs(estimatedRotation.y) < TRACKER_MAX_YAW_TURN) ) { - if (estimatedRotation.y > 0.0f) { - _bodyYawDelta += (estimatedRotation.y - TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY; - } else { - _bodyYawDelta += (estimatedRotation.y + TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY; - } + if (Application::getInstance()->getPrioVR()->isActive()) { + estimatedRotation = glm::degrees(safeEulerAngles(Application::getInstance()->getPrioVR()->getHeadRotation())); + estimatedRotation.x *= -1.0f; + estimatedRotation.z *= -1.0f; + + } else { + FaceTracker* tracker = Application::getInstance()->getActiveFaceTracker(); + if (tracker) { + estimatedPosition = tracker->getHeadTranslation(); + estimatedRotation = glm::degrees(safeEulerAngles(tracker->getHeadRotation())); + } + } + + // Rotate the body if the head is turned beyond the screen + if (Menu::getInstance()->isOptionChecked(MenuOption::TurnWithHead)) { + const float TRACKER_YAW_TURN_SENSITIVITY = 0.5f; + const float TRACKER_MIN_YAW_TURN = 15.0f; + const float TRACKER_MAX_YAW_TURN = 50.0f; + if ( (fabs(estimatedRotation.y) > TRACKER_MIN_YAW_TURN) && + (fabs(estimatedRotation.y) < TRACKER_MAX_YAW_TURN) ) { + if (estimatedRotation.y > 0.0f) { + _bodyYawDelta += (estimatedRotation.y - TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY; + } else { + _bodyYawDelta += (estimatedRotation.y + TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY; } } } @@ -271,6 +278,14 @@ void MyAvatar::updateFromFaceTracker(float deltaTime) { head->setDeltaYaw(estimatedRotation.y * magnifyFieldOfView); head->setDeltaRoll(estimatedRotation.z); + // the priovr can give us exact lean + if (Application::getInstance()->getPrioVR()->isActive()) { + glm::vec3 eulers = glm::degrees(safeEulerAngles(Application::getInstance()->getPrioVR()->getTorsoRotation())); + head->setLeanSideways(eulers.z); + head->setLeanForward(eulers.x); + return; + } + // Update torso lean distance based on accelerometer data const float TORSO_LENGTH = 0.5f; glm::vec3 relativePosition = estimatedPosition - glm::vec3(0.0f, -TORSO_LENGTH, 0.0f); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d446c2e895..2df74f23c2 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -38,7 +38,7 @@ public: void reset(); void update(float deltaTime); void simulate(float deltaTime); - void updateFromFaceTracker(float deltaTime); + void updateFromTrackers(float deltaTime); void moveWithLean(); void render(const glm::vec3& cameraPosition, RenderMode renderMode = NORMAL_RENDER_MODE); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 544f573eda..e48ebfa63c 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -33,14 +33,28 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { return; // only simulate for own avatar } + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + PrioVR* prioVR = Application::getInstance()->getPrioVR(); + if (prioVR->isActive()) { + for (int i = 0; i < prioVR->getJointRotations().size(); i++) { + int humanIKJointIndex = prioVR->getHumanIKJointIndices().at(i); + if (humanIKJointIndex == -1) { + continue; + } + int jointIndex = geometry.humanIKJointIndices.at(humanIKJointIndex); + if (jointIndex != -1) { + setJointRotation(jointIndex, _rotation * prioVR->getJointRotations().at(i), true); + } + } + return; + } + // find the left and rightmost active palms int leftPalmIndex, rightPalmIndex; Hand* hand = _owningAvatar->getHand(); hand->getLeftRightPalmIndices(leftPalmIndex, rightPalmIndex); - const float HAND_RESTORATION_RATE = 0.25f; - - const FBXGeometry& geometry = _geometry->getFBXGeometry(); + const float HAND_RESTORATION_RATE = 0.25f; if (leftPalmIndex == -1) { // palms are not yet set, use mouse if (_owningAvatar->getHandState() == HAND_STATE_NULL) { @@ -186,7 +200,7 @@ void SkeletonModel::updateJointState(int index) { } void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { - if (!_owningAvatar->isMyAvatar()) { + if (!_owningAvatar->isMyAvatar() || Application::getInstance()->getPrioVR()->isActive()) { return; } // get the rotation axes in joint space and use them to adjust the rotation diff --git a/interface/src/devices/PrioVR.cpp b/interface/src/devices/PrioVR.cpp new file mode 100644 index 0000000000..064e2be4b5 --- /dev/null +++ b/interface/src/devices/PrioVR.cpp @@ -0,0 +1,117 @@ +// +// PrioVR.cpp +// interface/src/devices +// +// Created by Andrzej Kapolka on 5/12/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include + +#include "Application.h" +#include "PrioVR.h" +#include "ui/TextRenderer.h" + +const unsigned int SERIAL_LIST[] = { 0x00000001, 0x00000000, 0x00000008, 0x00000009, 0x0000000A, + 0x0000000C, 0x0000000D, 0x0000000E, 0x00000004, 0x00000005, 0x00000010, 0x00000011 }; +const unsigned char AXIS_LIST[] = { 9, 43, 37, 37, 37, 13, 13, 13, 52, 52, 28, 28 }; +const int LIST_LENGTH = sizeof(SERIAL_LIST) / sizeof(SERIAL_LIST[0]); + +const char* JOINT_NAMES[] = { "Neck", "Spine", "LeftArm", "LeftForeArm", "LeftHand", "RightArm", + "RightForeArm", "RightHand", "LeftUpLeg", "LeftLeg", "RightUpLeg", "RightLeg" }; + +#ifdef HAVE_PRIOVR +static int indexOfHumanIKJoint(const char* jointName) { + for (int i = 0;; i++) { + QByteArray humanIKJoint = HUMANIK_JOINTS[i]; + if (humanIKJoint.isEmpty()) { + return -1; + } + if (humanIKJoint == jointName) { + return i; + } + } +} +#endif + +PrioVR::PrioVR() { +#ifdef HAVE_PRIOVR + char jointsDiscovered[LIST_LENGTH]; + _skeletalDevice = yei_setUpPrioVRSensors(0x00000000, const_cast(SERIAL_LIST), + const_cast(AXIS_LIST), jointsDiscovered, LIST_LENGTH, YEI_TIMESTAMP_SYSTEM); + if (!_skeletalDevice) { + return; + } + _jointRotations.resize(LIST_LENGTH); + for (int i = 0; i < LIST_LENGTH; i++) { + _humanIKJointIndices.append(jointsDiscovered[i] ? indexOfHumanIKJoint(JOINT_NAMES[i]) : -1); + } +#endif +} + +PrioVR::~PrioVR() { +#ifdef HAVE_PRIOVR + if (_skeletalDevice) { + yei_stopStreaming(_skeletalDevice); + } +#endif +} + +glm::quat PrioVR::getHeadRotation() const { + const int HEAD_ROTATION_INDEX = 0; + return _jointRotations.size() > HEAD_ROTATION_INDEX ? _jointRotations.at(HEAD_ROTATION_INDEX) : glm::quat(); +} + +glm::quat PrioVR::getTorsoRotation() const { + const int TORSO_ROTATION_INDEX = 1; + return _jointRotations.size() > TORSO_ROTATION_INDEX ? _jointRotations.at(TORSO_ROTATION_INDEX) : glm::quat(); +} + +void PrioVR::update() { +#ifdef HAVE_PRIOVR + if (!_skeletalDevice) { + return; + } + unsigned int timestamp; + yei_getLastStreamDataAll(_skeletalDevice, (char*)_jointRotations.data(), + _jointRotations.size() * sizeof(glm::quat), ×tamp); + + // convert to our expected coordinate system + for (int i = 0; i < _jointRotations.size(); i++) { + _jointRotations[i].y *= -1.0f; + _jointRotations[i].z *= -1.0f; + } +#endif +} + +void PrioVR::reset() { +#ifdef HAVE_PRIOVR + if (!_skeletalDevice) { + return; + } + connect(Application::getInstance(), SIGNAL(renderingOverlay()), SLOT(renderCalibrationCountdown())); + _calibrationCountdownStarted = QDateTime::currentDateTime(); +#endif +} + +void PrioVR::renderCalibrationCountdown() { +#ifdef HAVE_PRIOVR + const int COUNTDOWN_SECONDS = 3; + int secondsRemaining = COUNTDOWN_SECONDS - _calibrationCountdownStarted.secsTo(QDateTime::currentDateTime()); + if (secondsRemaining == 0) { + yei_tareSensors(_skeletalDevice); + Application::getInstance()->disconnect(this); + return; + } + static TextRenderer textRenderer(MONO_FONT_FAMILY, 18, QFont::Bold, false, TextRenderer::OUTLINE_EFFECT, 2); + QByteArray text = "Assume T-Pose in " + QByteArray::number(secondsRemaining) + "..."; + textRenderer.draw((Application::getInstance()->getGLWidget()->width() - textRenderer.computeWidth(text.constData())) / 2, + Application::getInstance()->getGLWidget()->height() / 2, + text); +#endif +} diff --git a/interface/src/devices/PrioVR.h b/interface/src/devices/PrioVR.h new file mode 100644 index 0000000000..9cd7bda5d4 --- /dev/null +++ b/interface/src/devices/PrioVR.h @@ -0,0 +1,62 @@ +// +// PrioVR.h +// interface/src/devices +// +// Created by Andrzej Kapolka on 5/12/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_PrioVR_h +#define hifi_PrioVR_h + +#include +#include +#include + +#include + +#ifdef HAVE_PRIOVR +extern "C" { +#include +} +#endif + +/// Handles interaction with the PrioVR skeleton tracking suit. +class PrioVR : public QObject { + Q_OBJECT + +public: + + PrioVR(); + virtual ~PrioVR(); + + bool isActive() const { return !_jointRotations.isEmpty(); } + + glm::quat getHeadRotation() const; + glm::quat getTorsoRotation() const; + + const QVector& getHumanIKJointIndices() const { return _humanIKJointIndices; } + const QVector& getJointRotations() const { return _jointRotations; } + + void update(); + void reset(); + +private slots: + + void renderCalibrationCountdown(); + +private: +#ifdef HAVE_PRIOVR + YEI_Device_Id _skeletalDevice; +#endif + + QVector _humanIKJointIndices; + QVector _jointRotations; + + QDateTime _calibrationCountdownStarted; +}; + +#endif // hifi_PrioVR_h diff --git a/interface/src/devices/Visage.cpp b/interface/src/devices/Visage.cpp index 8173519478..119d89654a 100644 --- a/interface/src/devices/Visage.cpp +++ b/interface/src/devices/Visage.cpp @@ -55,7 +55,8 @@ Visage::Visage() : Visage::~Visage() { #ifdef HAVE_VISAGE _tracker->stop(); - delete _tracker; + // deleting the tracker crashes windows; disable for now + //delete _tracker; delete _data; #endif } diff --git a/interface/src/models/ModelTreeRenderer.cpp b/interface/src/models/ModelTreeRenderer.cpp index c762182290..0f9da86887 100644 --- a/interface/src/models/ModelTreeRenderer.cpp +++ b/interface/src/models/ModelTreeRenderer.cpp @@ -20,11 +20,16 @@ ModelTreeRenderer::ModelTreeRenderer() : } ModelTreeRenderer::~ModelTreeRenderer() { - // delete the models in _modelsItemModels - foreach(Model* model, _modelsItemModels) { + // delete the models in _knownModelsItemModels + foreach(Model* model, _knownModelsItemModels) { delete model; } - _modelsItemModels.clear(); + _knownModelsItemModels.clear(); + + foreach(Model* model, _unknownModelsItemModels) { + delete model; + } + _unknownModelsItemModels.clear(); } void ModelTreeRenderer::init() { @@ -43,27 +48,38 @@ void ModelTreeRenderer::render(RenderMode renderMode) { OctreeRenderer::render(renderMode); } -Model* ModelTreeRenderer::getModel(const QString& url) { +Model* ModelTreeRenderer::getModel(const ModelItem& modelItem) { Model* model = NULL; - // if we don't already have this model then create it and initialize it - if (_modelsItemModels.find(url) == _modelsItemModels.end()) { - model = new Model(); - model->init(); - model->setURL(QUrl(url)); - _modelsItemModels[url] = model; + if (modelItem.isKnownID()) { + if (_knownModelsItemModels.find(modelItem.getID()) != _knownModelsItemModels.end()) { + model = _knownModelsItemModels[modelItem.getID()]; + } else { + model = new Model(); + model->init(); + model->setURL(QUrl(modelItem.getModelURL())); + _knownModelsItemModels[modelItem.getID()] = model; + } } else { - model = _modelsItemModels[url]; + if (_unknownModelsItemModels.find(modelItem.getCreatorTokenID()) != _unknownModelsItemModels.end()) { + model = _unknownModelsItemModels[modelItem.getCreatorTokenID()]; + } else { + model = new Model(); + model->init(); + model->setURL(QUrl(modelItem.getModelURL())); + _unknownModelsItemModels[modelItem.getCreatorTokenID()] = model; + } } return model; } void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) { + args->_elementsTouched++; // actually render it here... // we need to iterate the actual modelItems of the element ModelTreeElement* modelTreeElement = (ModelTreeElement*)element; - const QList& modelItems = modelTreeElement->getModels(); + QList& modelItems = modelTreeElement->getModels(); uint16_t numberOfModels = modelItems.size(); @@ -139,7 +155,7 @@ void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) } for (uint16_t i = 0; i < numberOfModels; i++) { - const ModelItem& modelItem = modelItems[i]; + ModelItem& modelItem = modelItems[i]; // render modelItem aspoints AABox modelBox = modelItem.getAABox(); modelBox.scale(TREE_SCALE); @@ -150,13 +166,13 @@ void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) bool drawAsModel = modelItem.hasModel(); - args->_renderedItems++; + args->_itemsRendered++; if (drawAsModel) { glPushMatrix(); const float alpha = 1.0f; - Model* model = getModel(modelItem.getModelURL()); + Model* model = getModel(modelItem); model->setScaleToFit(true, radius * 2.0f); model->setSnapModelToCenter(true); @@ -167,6 +183,21 @@ void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) // set the position model->setTranslation(position); + + // handle animations.. + if (modelItem.hasAnimation()) { + if (!modelItem.jointsMapped()) { + QStringList modelJointNames = model->getJointNames(); + modelItem.mapJoints(modelJointNames); + } + + QVector frameData = modelItem.getAnimationFrame(); + for (int i = 0; i < frameData.size(); i++) { + model->setJointState(i, true, frameData[i]); + } + } + + // make sure to simulate so everything gets set up correctly for rendering model->simulate(0.0f); // TODO: should we allow modelItems to have alpha on their models? @@ -190,6 +221,8 @@ void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) glutSolidSphere(radius, 15, 15); glPopMatrix(); } + } else { + args->_itemsOutOfView++; } } } diff --git a/interface/src/models/ModelTreeRenderer.h b/interface/src/models/ModelTreeRenderer.h index 7af5bbf317..e0b8d7d0a2 100644 --- a/interface/src/models/ModelTreeRenderer.h +++ b/interface/src/models/ModelTreeRenderer.h @@ -49,9 +49,9 @@ public: virtual void render(RenderMode renderMode = DEFAULT_RENDER_MODE); protected: - Model* getModel(const QString& url); - - QMap _modelsItemModels; + Model* getModel(const ModelItem& modelItem); + QMap _knownModelsItemModels; + QMap _unknownModelsItemModels; }; #endif // hifi_ModelTreeRenderer_h diff --git a/interface/src/particles/ParticleTreeRenderer.cpp b/interface/src/particles/ParticleTreeRenderer.cpp index 2983093564..38ef9c8516 100644 --- a/interface/src/particles/ParticleTreeRenderer.cpp +++ b/interface/src/particles/ParticleTreeRenderer.cpp @@ -76,7 +76,7 @@ void ParticleTreeRenderer::renderElement(OctreeElement* element, RenderArgs* arg bool drawAsModel = particle.hasModel(); - args->_renderedItems++; + args->_itemsRendered++; if (drawAsModel) { glPushMatrix(); diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 68eccc8f49..3d89080bb0 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -433,7 +433,8 @@ Extents Model::getMeshExtents() const { return Extents(); } const Extents& extents = _geometry->getFBXGeometry().meshExtents; - Extents scaledExtents = { extents.minimum * _scale, extents.maximum * _scale }; + glm::vec3 scale = _scale * _geometry->getFBXGeometry().fstScaled; + Extents scaledExtents = { extents.minimum * scale, extents.maximum * scale }; return scaledExtents; } @@ -441,8 +442,13 @@ Extents Model::getUnscaledMeshExtents() const { if (!isActive()) { return Extents(); } + const Extents& extents = _geometry->getFBXGeometry().meshExtents; - return extents; + + // even though our caller asked for "unscaled" we need to include any fst scaling + float scale = _geometry->getFBXGeometry().fstScaled; + Extents scaledExtents = { extents.minimum * scale, extents.maximum * scale }; + return scaledExtents; } bool Model::getJointState(int index, glm::quat& rotation) const { @@ -576,6 +582,17 @@ bool Model::getJointRotation(int jointIndex, glm::quat& rotation, bool fromBind) return true; } +QStringList Model::getJointNames() const { + if (QThread::currentThread() != thread()) { + QStringList result; + QMetaObject::invokeMethod(const_cast(this), "getJointNames", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QStringList, result)); + return result; + } + return isActive() ? _geometry->getFBXGeometry().getJointNames() : QStringList(); +} + + void Model::clearShapes() { for (int i = 0; i < _jointShapes.size(); ++i) { delete _jointShapes[i]; diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 1a469c8122..b7a42930dc 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -182,6 +182,8 @@ public: bool getJointPosition(int jointIndex, glm::vec3& position) const; bool getJointRotation(int jointIndex, glm::quat& rotation, bool fromBind = false) const; + + QStringList getJointNames() const; void clearShapes(); void rebuildShapes(); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 144415d6ee..e10197d488 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -78,6 +78,7 @@ QScriptValue WindowScriptingInterface::showPrompt(const QString& message, const promptDialog.setWindowTitle(""); promptDialog.setLabelText(message); promptDialog.setTextValue(defaultText); + promptDialog.setFixedSize(600, 200); if (promptDialog.exec() == QDialog::Accepted) { return QScriptValue(promptDialog.textValue()); diff --git a/libraries/animation/CMakeLists.txt b/libraries/animation/CMakeLists.txt new file mode 100644 index 0000000000..36088ba4bd --- /dev/null +++ b/libraries/animation/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 2.8) + +if (WIN32) + cmake_policy (SET CMP0020 NEW) +endif (WIN32) + +set(ROOT_DIR ../..) +set(MACRO_DIR "${ROOT_DIR}/cmake/macros") + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +set(TARGET_NAME animation) + +find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiLibrary.cmake) +setup_hifi_library(${TARGET_NAME}) + +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} "${ROOT_DIR}") + +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") + +# link ZLIB +find_package(ZLIB) +find_package(GnuTLS REQUIRED) + +# add a definition for ssize_t so that windows doesn't bail on gnutls.h +if (WIN32) + add_definitions(-Dssize_t=long) +endif () + +include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}" "${GNUTLS_INCLUDE_DIR}") +target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" "${GNUTLS_LIBRARY}" Qt5::Widgets) diff --git a/libraries/script-engine/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp similarity index 98% rename from libraries/script-engine/src/AnimationCache.cpp rename to libraries/animation/src/AnimationCache.cpp index 8e1493f075..ce7e4cdf36 100644 --- a/libraries/script-engine/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -36,7 +36,8 @@ QSharedPointer AnimationCache::createResource(const QUrl& url, const Q } Animation::Animation(const QUrl& url) : - Resource(url) { + Resource(url), + _isValid(false) { } class AnimationReader : public QRunnable { @@ -93,6 +94,7 @@ QVector Animation::getFrames() const { void Animation::setGeometry(const FBXGeometry& geometry) { _geometry = geometry; finishedLoading(true); + _isValid = true; } void Animation::downloadFinished(QNetworkReply* reply) { diff --git a/libraries/script-engine/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h similarity index 96% rename from libraries/script-engine/src/AnimationCache.h rename to libraries/animation/src/AnimationCache.h index 23183adf10..392443e7b5 100644 --- a/libraries/script-engine/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -53,6 +53,8 @@ public: Q_INVOKABLE QStringList getJointNames() const; Q_INVOKABLE QVector getFrames() const; + + bool isValid() const { return _isValid; } protected: @@ -63,6 +65,7 @@ protected: private: FBXGeometry _geometry; + bool _isValid; }; #endif // hifi_AnimationCache_h diff --git a/libraries/script-engine/src/AnimationObject.cpp b/libraries/animation/src/AnimationObject.cpp similarity index 100% rename from libraries/script-engine/src/AnimationObject.cpp rename to libraries/animation/src/AnimationObject.cpp diff --git a/libraries/script-engine/src/AnimationObject.h b/libraries/animation/src/AnimationObject.h similarity index 100% rename from libraries/script-engine/src/AnimationObject.h rename to libraries/animation/src/AnimationObject.h diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 1fc03ceb66..264f58f0d4 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -577,6 +577,25 @@ const char* FACESHIFT_BLENDSHAPES[] = { "" }; +const char* HUMANIK_JOINTS[] = { + "RightHand", + "RightForeArm", + "RightArm", + "Head", + "LeftArm", + "LeftForeArm", + "LeftHand", + "Spine", + "Hips", + "RightUpLeg", + "LeftUpLeg", + "RightLeg", + "LeftLeg", + "RightFoot", + "LeftFoot", + "" +}; + class FBXModel { public: QString name; @@ -1012,10 +1031,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QString jointHeadName = processID(getString(joints.value("jointHead", "jointHead"))); QString jointLeftHandName = processID(getString(joints.value("jointLeftHand", "jointLeftHand"))); QString jointRightHandName = processID(getString(joints.value("jointRightHand", "jointRightHand"))); - QVariantList jointLeftFingerNames = joints.values("jointLeftFinger"); - QVariantList jointRightFingerNames = joints.values("jointRightFinger"); - QVariantList jointLeftFingertipNames = joints.values("jointLeftFingertip"); - QVariantList jointRightFingertipNames = joints.values("jointRightFingertip"); QString jointEyeLeftID; QString jointEyeRightID; QString jointNeckID; @@ -1024,10 +1039,16 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QString jointHeadID; QString jointLeftHandID; QString jointRightHandID; - QVector jointLeftFingerIDs(jointLeftFingerNames.size()); - QVector jointRightFingerIDs(jointRightFingerNames.size()); - QVector jointLeftFingertipIDs(jointLeftFingertipNames.size()); - QVector jointRightFingertipIDs(jointRightFingertipNames.size()); + + QVector humanIKJointNames; + for (int i = 0;; i++) { + QByteArray jointName = HUMANIK_JOINTS[i]; + if (jointName.isEmpty()) { + break; + } + humanIKJointNames.append(processID(getString(joints.value(jointName, jointName)))); + } + QVector humanIKJointIDs(humanIKJointNames.size()); QVariantHash blendshapeMappings = mapping.value("bs").toHash(); QMultiHash blendshapeIndices; @@ -1091,7 +1112,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } else { name = getID(object.properties); } - int index; if (name == jointEyeLeftName || name == "EyeL" || name == "joint_Leye") { jointEyeLeftID = getID(object.properties); @@ -1115,19 +1135,12 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } else if (name == jointRightHandName) { jointRightHandID = getID(object.properties); - - } else if ((index = jointLeftFingerNames.indexOf(name)) != -1) { - jointLeftFingerIDs[index] = getID(object.properties); - - } else if ((index = jointRightFingerNames.indexOf(name)) != -1) { - jointRightFingerIDs[index] = getID(object.properties); - - } else if ((index = jointLeftFingertipNames.indexOf(name)) != -1) { - jointLeftFingertipIDs[index] = getID(object.properties); - - } else if ((index = jointRightFingertipNames.indexOf(name)) != -1) { - jointRightFingertipIDs[index] = getID(object.properties); } + int humanIKJointIndex = humanIKJointNames.indexOf(name); + if (humanIKJointIndex != -1) { + humanIKJointIDs[humanIKJointIndex] = getID(object.properties); + } + glm::vec3 translation; // NOTE: the euler angles as supplied by the FBX file are in degrees glm::vec3 rotationOffset; @@ -1352,7 +1365,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } else if (type.contains("bump") || type.contains("normal")) { bumpTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); - } else if (type.contains("specular")) { + } else if (type.contains("specular") || type.contains("reflection")) { specularTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type == "lcl rotation") { @@ -1385,6 +1398,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) // get offset transform from mapping float offsetScale = mapping.value("scale", 1.0f).toFloat(); + geometry.fstScaled = offsetScale; glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(), mapping.value("ry").toFloat(), mapping.value("rz").toFloat()))); geometry.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(), @@ -1513,11 +1527,11 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) geometry.headJointIndex = modelIDs.indexOf(jointHeadID); geometry.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID); geometry.rightHandJointIndex = modelIDs.indexOf(jointRightHandID); - geometry.leftFingerJointIndices = getIndices(jointLeftFingerIDs, modelIDs); - geometry.rightFingerJointIndices = getIndices(jointRightFingerIDs, modelIDs); - geometry.leftFingertipJointIndices = getIndices(jointLeftFingertipIDs, modelIDs); - geometry.rightFingertipJointIndices = getIndices(jointRightFingertipIDs, modelIDs); - + + foreach (const QString& id, humanIKJointIDs) { + geometry.humanIKJointIndices.append(modelIDs.indexOf(id)); + } + // extract the translation component of the neck transform if (geometry.neckJointIndex != -1) { const glm::mat4& transform = geometry.joints.at(geometry.neckJointIndex).transform; diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 51e7380181..2aabdab6fa 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -30,6 +30,9 @@ typedef QList FBXNodeList; /// The names of the blendshapes expected by Faceshift, terminated with an empty string. extern const char* FACESHIFT_BLENDSHAPES[]; +/// The names of the joints in the Maya HumanIK rig, terminated with an empty string. +extern const char* HUMANIK_JOINTS[]; + class Extents { public: /// set minimum and maximum to FLT_MAX and -FLT_MAX respectively @@ -199,11 +202,7 @@ public: int leftHandJointIndex; int rightHandJointIndex; - QVector leftFingerJointIndices; - QVector rightFingerJointIndices; - - QVector leftFingertipJointIndices; - QVector rightFingertipJointIndices; + QVector humanIKJointIndices; glm::vec3 palmDirection; @@ -212,6 +211,8 @@ public: Extents bindExtents; Extents meshExtents; + float fstScaled; + QVector animationFrames; QVector attachments; diff --git a/libraries/models/CMakeLists.txt b/libraries/models/CMakeLists.txt index 062352e50c..1e70942872 100644 --- a/libraries/models/CMakeLists.txt +++ b/libraries/models/CMakeLists.txt @@ -28,6 +28,7 @@ link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(animation ${TARGET_NAME} "${ROOT_DIR}") # for streamable link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") diff --git a/libraries/models/src/ModelItem.cpp b/libraries/models/src/ModelItem.cpp index af8500bf25..c04f9a76ae 100644 --- a/libraries/models/src/ModelItem.cpp +++ b/libraries/models/src/ModelItem.cpp @@ -85,6 +85,15 @@ ModelItem::ModelItem(const ModelItemID& modelItemID, const ModelItemProperties& _shouldDie = false; _modelURL = MODEL_DEFAULT_MODEL_URL; _modelRotation = MODEL_DEFAULT_MODEL_ROTATION; + + // animation related + _animationURL = MODEL_DEFAULT_ANIMATION_URL; + _animationIsPlaying = false; + _animationFrameIndex = 0.0f; + _animationFPS = MODEL_DEFAULT_ANIMATION_FPS; + + _jointMappingCompleted = false; + _lastAnimated = now; setProperties(properties); } @@ -110,6 +119,14 @@ void ModelItem::init(glm::vec3 position, float radius, rgbColor color, uint32_t _shouldDie = false; _modelURL = MODEL_DEFAULT_MODEL_URL; _modelRotation = MODEL_DEFAULT_MODEL_ROTATION; + + // animation related + _animationURL = MODEL_DEFAULT_ANIMATION_URL; + _animationIsPlaying = false; + _animationFrameIndex = 0.0f; + _animationFPS = MODEL_DEFAULT_ANIMATION_FPS; + _jointMappingCompleted = false; + _lastAnimated = now; } bool ModelItem::appendModelData(OctreePacketData* packetData) const { @@ -150,6 +167,31 @@ bool ModelItem::appendModelData(OctreePacketData* packetData) const { if (success) { success = packetData->appendValue(getModelRotation()); } + + // animationURL + if (success) { + uint16_t animationURLLength = _animationURL.size() + 1; // include NULL + success = packetData->appendValue(animationURLLength); + if (success) { + success = packetData->appendRawData((const unsigned char*)qPrintable(_animationURL), animationURLLength); + } + } + + // animationIsPlaying + if (success) { + success = packetData->appendValue(getAnimationIsPlaying()); + } + + // animationFrameIndex + if (success) { + success = packetData->appendValue(getAnimationFrameIndex()); + } + + // animationFPS + if (success) { + success = packetData->appendValue(getAnimationFPS()); + } + return success; } @@ -166,6 +208,7 @@ int ModelItem::expectedBytes() { } int ModelItem::readModelDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { + int bytesRead = 0; if (bytesLeftToRead >= expectedBytes()) { int clockSkew = args.sourceNode ? args.sourceNode->getClockSkewUsec() : 0; @@ -215,7 +258,7 @@ int ModelItem::readModelDataFromBuffer(const unsigned char* data, int bytesLeftT dataAt += sizeof(modelURLLength); bytesRead += sizeof(modelURLLength); QString modelURLString((const char*)dataAt); - _modelURL = modelURLString; + setModelURL(modelURLString); dataAt += modelURLLength; bytesRead += modelURLLength; @@ -224,13 +267,37 @@ int ModelItem::readModelDataFromBuffer(const unsigned char* data, int bytesLeftT dataAt += bytes; bytesRead += bytes; - //printf("ModelItem::readModelDataFromBuffer()... "); debugDump(); + if (args.bitstreamVersion >= VERSION_MODELS_HAVE_ANIMATION) { + // animationURL + uint16_t animationURLLength; + memcpy(&animationURLLength, dataAt, sizeof(animationURLLength)); + dataAt += sizeof(animationURLLength); + bytesRead += sizeof(animationURLLength); + QString animationURLString((const char*)dataAt); + setAnimationURL(animationURLString); + dataAt += animationURLLength; + bytesRead += animationURLLength; + + // animationIsPlaying + memcpy(&_animationIsPlaying, dataAt, sizeof(_animationIsPlaying)); + dataAt += sizeof(_animationIsPlaying); + bytesRead += sizeof(_animationIsPlaying); + + // animationFrameIndex + memcpy(&_animationFrameIndex, dataAt, sizeof(_animationFrameIndex)); + dataAt += sizeof(_animationFrameIndex); + bytesRead += sizeof(_animationFrameIndex); + + // animationFPS + memcpy(&_animationFPS, dataAt, sizeof(_animationFPS)); + dataAt += sizeof(_animationFPS); + bytesRead += sizeof(_animationFPS); + } } return bytesRead; } ModelItem ModelItem::fromEditPacket(const unsigned char* data, int length, int& processedBytes, ModelTree* tree, bool& valid) { - ModelItem newModelItem; // id and _lastUpdated will get set here... const unsigned char* dataAt = data; processedBytes = 0; @@ -340,12 +407,52 @@ ModelItem ModelItem::fromEditPacket(const unsigned char* data, int length, int& } // modelRotation - if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_MODEL_ROTATION) == MODEL_PACKET_CONTAINS_MODEL_ROTATION)) { + if (isNewModelItem || ((packetContainsBits & + MODEL_PACKET_CONTAINS_MODEL_ROTATION) == MODEL_PACKET_CONTAINS_MODEL_ROTATION)) { int bytes = unpackOrientationQuatFromBytes(dataAt, newModelItem._modelRotation); dataAt += bytes; processedBytes += bytes; } + // animationURL + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_ANIMATION_URL) == MODEL_PACKET_CONTAINS_ANIMATION_URL)) { + uint16_t animationURLLength; + memcpy(&animationURLLength, dataAt, sizeof(animationURLLength)); + dataAt += sizeof(animationURLLength); + processedBytes += sizeof(animationURLLength); + QString tempString((const char*)dataAt); + newModelItem._animationURL = tempString; + dataAt += animationURLLength; + processedBytes += animationURLLength; + } + + // animationIsPlaying + if (isNewModelItem || ((packetContainsBits & + MODEL_PACKET_CONTAINS_ANIMATION_PLAYING) == MODEL_PACKET_CONTAINS_ANIMATION_PLAYING)) { + + memcpy(&newModelItem._animationIsPlaying, dataAt, sizeof(newModelItem._animationIsPlaying)); + dataAt += sizeof(newModelItem._animationIsPlaying); + processedBytes += sizeof(newModelItem._animationIsPlaying); + } + + // animationFrameIndex + if (isNewModelItem || ((packetContainsBits & + MODEL_PACKET_CONTAINS_ANIMATION_FRAME) == MODEL_PACKET_CONTAINS_ANIMATION_FRAME)) { + + memcpy(&newModelItem._animationFrameIndex, dataAt, sizeof(newModelItem._animationFrameIndex)); + dataAt += sizeof(newModelItem._animationFrameIndex); + processedBytes += sizeof(newModelItem._animationFrameIndex); + } + + // animationFPS + if (isNewModelItem || ((packetContainsBits & + MODEL_PACKET_CONTAINS_ANIMATION_FPS) == MODEL_PACKET_CONTAINS_ANIMATION_FPS)) { + + memcpy(&newModelItem._animationFPS, dataAt, sizeof(newModelItem._animationFPS)); + dataAt += sizeof(newModelItem._animationFPS); + processedBytes += sizeof(newModelItem._animationFPS); + } + const bool wantDebugging = false; if (wantDebugging) { qDebug("ModelItem::fromEditPacket()..."); @@ -363,6 +470,7 @@ void ModelItem::debugDump() const { qDebug(" position:%f,%f,%f", _position.x, _position.y, _position.z); qDebug(" radius:%f", getRadius()); qDebug(" color:%d,%d,%d", _color[0], _color[1], _color[2]); + qDebug() << " modelURL:" << qPrintable(getModelURL()); } bool ModelItem::encodeModelEditMessageDetails(PacketType command, ModelItemID id, const ModelItemProperties& properties, @@ -476,6 +584,47 @@ bool ModelItem::encodeModelEditMessageDetails(PacketType command, ModelItemID id sizeOut += bytes; } + // animationURL + if (isNewModelItem || ((packetContainsBits & MODEL_PACKET_CONTAINS_ANIMATION_URL) == MODEL_PACKET_CONTAINS_ANIMATION_URL)) { + uint16_t urlLength = properties.getAnimationURL().size() + 1; + memcpy(copyAt, &urlLength, sizeof(urlLength)); + copyAt += sizeof(urlLength); + sizeOut += sizeof(urlLength); + memcpy(copyAt, qPrintable(properties.getAnimationURL()), urlLength); + copyAt += urlLength; + sizeOut += urlLength; + } + + // animationIsPlaying + if (isNewModelItem || ((packetContainsBits & + MODEL_PACKET_CONTAINS_ANIMATION_PLAYING) == MODEL_PACKET_CONTAINS_ANIMATION_PLAYING)) { + + bool animationIsPlaying = properties.getAnimationIsPlaying(); + memcpy(copyAt, &animationIsPlaying, sizeof(animationIsPlaying)); + copyAt += sizeof(animationIsPlaying); + sizeOut += sizeof(animationIsPlaying); + } + + // animationFrameIndex + if (isNewModelItem || ((packetContainsBits & + MODEL_PACKET_CONTAINS_ANIMATION_FRAME) == MODEL_PACKET_CONTAINS_ANIMATION_FRAME)) { + + float animationFrameIndex = properties.getAnimationFrameIndex(); + memcpy(copyAt, &animationFrameIndex, sizeof(animationFrameIndex)); + copyAt += sizeof(animationFrameIndex); + sizeOut += sizeof(animationFrameIndex); + } + + // animationFPS + if (isNewModelItem || ((packetContainsBits & + MODEL_PACKET_CONTAINS_ANIMATION_FPS) == MODEL_PACKET_CONTAINS_ANIMATION_FPS)) { + + float animationFPS = properties.getAnimationFPS(); + memcpy(copyAt, &animationFPS, sizeof(animationFPS)); + copyAt += sizeof(animationFPS); + sizeOut += sizeof(animationFPS); + } + bool wantDebugging = false; if (wantDebugging) { qDebug("encodeModelItemEditMessageDetails()...."); @@ -521,9 +670,106 @@ void ModelItem::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssi } } -void ModelItem::update(const quint64& now) { - _lastUpdated = now; + +QMap ModelItem::_loadedAnimations; // TODO: improve cleanup by leveraging the AnimationPointer(s) +AnimationCache ModelItem::_animationCache; + +// This class/instance will cleanup the animations once unloaded. +class ModelAnimationsBookkeeper { +public: + ~ModelAnimationsBookkeeper() { + ModelItem::cleanupLoadedAnimations(); + } +}; + +ModelAnimationsBookkeeper modelAnimationsBookkeeperInstance; + +void ModelItem::cleanupLoadedAnimations() { + foreach(AnimationPointer animation, _loadedAnimations) { + animation.clear(); + } + _loadedAnimations.clear(); +} + +Animation* ModelItem::getAnimation(const QString& url) { + AnimationPointer animation; + + // if we don't already have this model then create it and initialize it + if (_loadedAnimations.find(url) == _loadedAnimations.end()) { + animation = _animationCache.getAnimation(url); + _loadedAnimations[url] = animation; + } else { + animation = _loadedAnimations[url]; + } + return animation.data(); +} + +void ModelItem::mapJoints(const QStringList& modelJointNames) { + // if we don't have animation, or we're already joint mapped then bail early + if (!hasAnimation() || _jointMappingCompleted) { + return; + } + + Animation* myAnimation = getAnimation(_animationURL); + + if (!_jointMappingCompleted) { + QStringList animationJointNames = myAnimation->getJointNames(); + if (modelJointNames.size() > 0 && animationJointNames.size() > 0) { + _jointMapping.resize(modelJointNames.size()); + for (int i = 0; i < modelJointNames.size(); i++) { + _jointMapping[i] = animationJointNames.indexOf(modelJointNames[i]); + } + _jointMappingCompleted = true; + } + } +} + +QVector ModelItem::getAnimationFrame() { + QVector frameData; + if (hasAnimation() && _jointMappingCompleted) { + Animation* myAnimation = getAnimation(_animationURL); + QVector frames = myAnimation->getFrames(); + int animationFrameIndex = (int)std::floor(_animationFrameIndex) % frames.size(); + QVector rotations = frames[animationFrameIndex].rotations; + frameData.resize(_jointMapping.size()); + for (int j = 0; j < _jointMapping.size(); j++) { + int rotationIndex = _jointMapping[j]; + if (rotationIndex != -1 && rotationIndex < rotations.size()) { + frameData[j] = rotations[rotationIndex]; + } + } + } + return frameData; +} + +void ModelItem::update(const quint64& updateTime) { + _lastUpdated = updateTime; setShouldDie(getShouldDie()); + + quint64 now = usecTimestampNow(); + + // only advance the frame index if we're playing + if (getAnimationIsPlaying()) { + + float deltaTime = (float)(now - _lastAnimated) / (float)USECS_PER_SECOND; + + const bool wantDebugging = false; + if (wantDebugging) { + qDebug() << "ModelItem::update() now=" << now; + qDebug() << " updateTime=" << updateTime; + qDebug() << " _lastAnimated=" << _lastAnimated; + qDebug() << " deltaTime=" << deltaTime; + } + _lastAnimated = now; + _animationFrameIndex += deltaTime * _animationFPS; + + if (wantDebugging) { + qDebug() << " _animationFrameIndex=" << _animationFrameIndex; + } + + } else { + _lastAnimated = now; + } } void ModelItem::copyChangedProperties(const ModelItem& other) { @@ -547,6 +793,10 @@ ModelItemProperties::ModelItemProperties() : _shouldDie(false), _modelURL(""), _modelRotation(MODEL_DEFAULT_MODEL_ROTATION), + _animationURL(""), + _animationIsPlaying(false), + _animationFrameIndex(0.0), + _animationFPS(MODEL_DEFAULT_ANIMATION_FPS), _id(UNKNOWN_MODEL_ID), _idSet(false), @@ -558,6 +808,10 @@ ModelItemProperties::ModelItemProperties() : _shouldDieChanged(false), _modelURLChanged(false), _modelRotationChanged(false), + _animationURLChanged(false), + _animationIsPlayingChanged(false), + _animationFrameIndexChanged(false), + _animationFPSChanged(false), _defaultSettings(true) { } @@ -589,6 +843,22 @@ uint16_t ModelItemProperties::getChangedBits() const { changedBits += MODEL_PACKET_CONTAINS_MODEL_ROTATION; } + if (_animationURLChanged) { + changedBits += MODEL_PACKET_CONTAINS_ANIMATION_URL; + } + + if (_animationIsPlayingChanged) { + changedBits += MODEL_PACKET_CONTAINS_ANIMATION_PLAYING; + } + + if (_animationFrameIndexChanged) { + changedBits += MODEL_PACKET_CONTAINS_ANIMATION_FRAME; + } + + if (_animationFPSChanged) { + changedBits += MODEL_PACKET_CONTAINS_ANIMATION_FPS; + } + return changedBits; } @@ -611,6 +881,10 @@ QScriptValue ModelItemProperties::copyToScriptValue(QScriptEngine* engine) const QScriptValue modelRotation = quatToScriptValue(engine, _modelRotation); properties.setProperty("modelRotation", modelRotation); + properties.setProperty("animationURL", _animationURL); + properties.setProperty("animationIsPlaying", _animationIsPlaying); + properties.setProperty("animationFrameIndex", _animationFrameIndex); + properties.setProperty("animationFPS", _animationFPS); if (_idSet) { properties.setProperty("id", _id); @@ -707,6 +981,46 @@ void ModelItemProperties::copyFromScriptValue(const QScriptValue &object) { } } + QScriptValue animationURL = object.property("animationURL"); + if (animationURL.isValid()) { + QString newAnimationURL; + newAnimationURL = animationURL.toVariant().toString(); + if (_defaultSettings || newAnimationURL != _animationURL) { + _animationURL = newAnimationURL; + _animationURLChanged = true; + } + } + + QScriptValue animationIsPlaying = object.property("animationIsPlaying"); + if (animationIsPlaying.isValid()) { + bool newIsAnimationPlaying; + newIsAnimationPlaying = animationIsPlaying.toVariant().toBool(); + if (_defaultSettings || newIsAnimationPlaying != _animationIsPlaying) { + _animationIsPlaying = newIsAnimationPlaying; + _animationIsPlayingChanged = true; + } + } + + QScriptValue animationFrameIndex = object.property("animationFrameIndex"); + if (animationFrameIndex.isValid()) { + float newFrameIndex; + newFrameIndex = animationFrameIndex.toVariant().toFloat(); + if (_defaultSettings || newFrameIndex != _animationFrameIndex) { + _animationFrameIndex = newFrameIndex; + _animationFrameIndexChanged = true; + } + } + + QScriptValue animationFPS = object.property("animationFPS"); + if (animationFPS.isValid()) { + float newFPS; + newFPS = animationFPS.toVariant().toFloat(); + if (_defaultSettings || newFPS != _animationFPS) { + _animationFPS = newFPS; + _animationFPSChanged = true; + } + } + _lastEdited = usecTimestampNow(); } @@ -741,7 +1055,27 @@ void ModelItemProperties::copyToModelItem(ModelItem& modelItem) const { modelItem.setModelRotation(_modelRotation); somethingChanged = true; } - + + if (_animationURLChanged) { + modelItem.setAnimationURL(_animationURL); + somethingChanged = true; + } + + if (_animationIsPlayingChanged) { + modelItem.setAnimationIsPlaying(_animationIsPlaying); + somethingChanged = true; + } + + if (_animationFrameIndexChanged) { + modelItem.setAnimationFrameIndex(_animationFrameIndex); + somethingChanged = true; + } + + if (_animationFPSChanged) { + modelItem.setAnimationFPS(_animationFPS); + somethingChanged = true; + } + if (somethingChanged) { bool wantDebug = false; if (wantDebug) { @@ -761,6 +1095,10 @@ void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) { _shouldDie = modelItem.getShouldDie(); _modelURL = modelItem.getModelURL(); _modelRotation = modelItem.getModelRotation(); + _animationURL = modelItem.getAnimationURL(); + _animationIsPlaying = modelItem.getAnimationIsPlaying(); + _animationFrameIndex = modelItem.getAnimationFrameIndex(); + _animationFPS = modelItem.getAnimationFPS(); _id = modelItem.getID(); _idSet = true; @@ -772,6 +1110,10 @@ void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) { _shouldDieChanged = false; _modelURLChanged = false; _modelRotationChanged = false; + _animationURLChanged = false; + _animationIsPlayingChanged = false; + _animationFrameIndexChanged = false; + _animationFPSChanged = false; _defaultSettings = false; } diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h index 9edcf482c0..847e58e7c2 100644 --- a/libraries/models/src/ModelItem.h +++ b/libraries/models/src/ModelItem.h @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -39,14 +40,22 @@ const uint32_t UNKNOWN_MODEL_ID = 0xFFFFFFFF; const uint16_t MODEL_PACKET_CONTAINS_RADIUS = 1; const uint16_t MODEL_PACKET_CONTAINS_POSITION = 2; const uint16_t MODEL_PACKET_CONTAINS_COLOR = 4; -const uint16_t MODEL_PACKET_CONTAINS_SHOULDDIE = 512; -const uint16_t MODEL_PACKET_CONTAINS_MODEL_URL = 1024; -const uint16_t MODEL_PACKET_CONTAINS_MODEL_ROTATION = 2048; +const uint16_t MODEL_PACKET_CONTAINS_SHOULDDIE = 8; +const uint16_t MODEL_PACKET_CONTAINS_MODEL_URL = 16; +const uint16_t MODEL_PACKET_CONTAINS_MODEL_ROTATION = 32; +const uint16_t MODEL_PACKET_CONTAINS_ANIMATION_URL = 64; +const uint16_t MODEL_PACKET_CONTAINS_ANIMATION_PLAYING = 128; +const uint16_t MODEL_PACKET_CONTAINS_ANIMATION_FRAME = 256; +const uint16_t MODEL_PACKET_CONTAINS_ANIMATION_FPS = 512; const float MODEL_DEFAULT_RADIUS = 0.1f / TREE_SCALE; const float MINIMUM_MODEL_ELEMENT_SIZE = (1.0f / 100000.0f) / TREE_SCALE; // smallest size container const QString MODEL_DEFAULT_MODEL_URL(""); const glm::quat MODEL_DEFAULT_MODEL_ROTATION; +const QString MODEL_DEFAULT_ANIMATION_URL(""); +const float MODEL_DEFAULT_ANIMATION_FPS = 30.0f; + +const PacketVersion VERSION_MODELS_HAVE_ANIMATION = 1; /// A collection of properties of a model item used in the scripting API. Translates between the actual properties of a model /// and a JavaScript style hash/QScriptValue storing a set of properties. Used in scripting to set/get the complete set of @@ -69,6 +78,10 @@ public: const QString& getModelURL() const { return _modelURL; } const glm::quat& getModelRotation() const { return _modelRotation; } + const QString& getAnimationURL() const { return _animationURL; } + float getAnimationFrameIndex() const { return _animationFrameIndex; } + bool getAnimationIsPlaying() const { return _animationIsPlaying; } + float getAnimationFPS() const { return _animationFPS; } quint64 getLastEdited() const { return _lastEdited; } uint16_t getChangedBits() const; @@ -82,6 +95,10 @@ public: // model related properties void setModelURL(const QString& url) { _modelURL = url; _modelURLChanged = true; } void setModelRotation(const glm::quat& rotation) { _modelRotation = rotation; _modelRotationChanged = true; } + void setAnimationURL(const QString& url) { _animationURL = url; _animationURLChanged = true; } + void setAnimationFrameIndex(float value) { _animationFrameIndex = value; _animationFrameIndexChanged = true; } + void setAnimationIsPlaying(bool value) { _animationIsPlaying = value; _animationIsPlayingChanged = true; } + void setAnimationFPS(float value) { _animationFPS = value; _animationFPSChanged = true; } /// used by ModelScriptingInterface to return ModelItemProperties for unknown models void setIsUnknownID() { _id = UNKNOWN_MODEL_ID; _idSet = true; } @@ -97,6 +114,10 @@ private: QString _modelURL; glm::quat _modelRotation; + QString _animationURL; + bool _animationIsPlaying; + float _animationFrameIndex; + float _animationFPS; uint32_t _id; bool _idSet; @@ -109,6 +130,10 @@ private: bool _modelURLChanged; bool _modelRotationChanged; + bool _animationURLChanged; + bool _animationIsPlayingChanged; + bool _animationFrameIndexChanged; + bool _animationFPSChanged; bool _defaultSettings; }; Q_DECLARE_METATYPE(ModelItemProperties); @@ -178,6 +203,8 @@ public: bool hasModel() const { return !_modelURL.isEmpty(); } const QString& getModelURL() const { return _modelURL; } const glm::quat& getModelRotation() const { return _modelRotation; } + bool hasAnimation() const { return !_animationURL.isEmpty(); } + const QString& getAnimationURL() const { return _animationURL; } ModelItemID getModelItemID() const { return ModelItemID(getID(), getCreatorTokenID(), getID() != UNKNOWN_MODEL_ID); } ModelItemProperties getProperties() const; @@ -196,6 +223,7 @@ public: bool getShouldDie() const { return _shouldDie; } uint32_t getCreatorTokenID() const { return _creatorTokenID; } bool isNewlyCreated() const { return _newlyCreated; } + bool isKnownID() const { return getID() != UNKNOWN_MODEL_ID; } /// set position in domain scale units (0.0 - 1.0) void setPosition(const glm::vec3& value) { _position = value; } @@ -215,6 +243,10 @@ public: // model related properties void setModelURL(const QString& url) { _modelURL = url; } void setModelRotation(const glm::quat& rotation) { _modelRotation = rotation; } + void setAnimationURL(const QString& url) { _animationURL = url; } + void setAnimationFrameIndex(float value) { _animationFrameIndex = value; } + void setAnimationIsPlaying(bool value) { _animationIsPlaying = value; } + void setAnimationFPS(float value) { _animationFPS = value; } void setProperties(const ModelItemProperties& properties); @@ -239,6 +271,16 @@ public: static uint32_t getNextCreatorTokenID(); static void handleAddModelResponse(const QByteArray& packet); + void mapJoints(const QStringList& modelJointNames); + QVector getAnimationFrame(); + bool jointsMapped() const { return _jointMappingCompleted; } + + bool getAnimationIsPlaying() const { return _animationIsPlaying; } + float getAnimationFrameIndex() const { return _animationFrameIndex; } + float getAnimationFPS() const { return _animationFPS; } + + static void cleanupLoadedAnimations(); + protected: glm::vec3 _position; rgbColor _color; @@ -256,10 +298,26 @@ protected: quint64 _lastUpdated; quint64 _lastEdited; + quint64 _lastAnimated; + + QString _animationURL; + float _animationFrameIndex; // we keep this as a float and round to int only when we need the exact index + bool _animationIsPlaying; + float _animationFPS; + + bool _jointMappingCompleted; + QVector _jointMapping; + // used by the static interfaces for creator token ids static uint32_t _nextCreatorTokenID; static std::map _tokenIDsToIDs; + + + static Animation* getAnimation(const QString& url); + static QMap _loadedAnimations; + static AnimationCache _animationCache; + }; #endif // hifi_ModelItem_h diff --git a/libraries/models/src/ModelTree.cpp b/libraries/models/src/ModelTree.cpp index 45694b081d..4e92544f40 100644 --- a/libraries/models/src/ModelTree.cpp +++ b/libraries/models/src/ModelTree.cpp @@ -491,11 +491,12 @@ void ModelTree::update() { lockForWrite(); _isDirty = true; - ModelTreeUpdateArgs args = { }; + ModelTreeUpdateArgs args; recurseTreeWithOperation(updateOperation, &args); // now add back any of the particles that moved elements.... int movingModels = args._movingModels.size(); + for (int i = 0; i < movingModels; i++) { bool shouldDie = args._movingModels[i].getShouldDie(); @@ -553,7 +554,7 @@ bool ModelTree::encodeModelsDeletedSince(quint64& sinceTime, unsigned char* outp memcpy(copyAt, &numberOfIds, sizeof(numberOfIds)); copyAt += sizeof(numberOfIds); outputLength += sizeof(numberOfIds); - + // we keep a multi map of model IDs to timestamps, we only want to include the model IDs that have been // deleted since we last sent to this node _recentlyDeletedModelsLock.lockForRead(); @@ -595,7 +596,6 @@ bool ModelTree::encodeModelsDeletedSince(quint64& sinceTime, unsigned char* outp // replace the correct count for ids included memcpy(numberOfIDsAt, &numberOfIds, sizeof(numberOfIds)); - return hasMoreToSend; } diff --git a/libraries/models/src/ModelTree.h b/libraries/models/src/ModelTree.h index ac25cdc003..10ef62c0a0 100644 --- a/libraries/models/src/ModelTree.h +++ b/libraries/models/src/ModelTree.h @@ -36,6 +36,7 @@ public: // own definition. Implement these to allow your octree based server to support editing virtual bool getWantSVOfileVersions() const { return true; } virtual PacketType expectedDataPacketType() const { return PacketTypeModelData; } + virtual bool canProcessVersion(PacketVersion thisVersion) const { return true; } // we support all versions virtual bool handlesEditPacketType(PacketType packetType) const; virtual int processEditPacketData(PacketType packetType, const unsigned char* packetData, int packetLength, const unsigned char* editData, int maxLength, const SharedNodePointer& senderNode); diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp index 5c5d5100cf..2f57818044 100644 --- a/libraries/models/src/ModelTreeElement.cpp +++ b/libraries/models/src/ModelTreeElement.cpp @@ -84,14 +84,17 @@ bool ModelTreeElement::appendElementData(OctreePacketData* packetData, EncodeBit } bool ModelTreeElement::containsModelBounds(const ModelItem& model) const { - return _box.contains(model.getMinimumPoint()) && _box.contains(model.getMaximumPoint()); + glm::vec3 clampedMin = glm::clamp(model.getMinimumPoint(), 0.0f, 1.0f); + glm::vec3 clampedMax = glm::clamp(model.getMaximumPoint(), 0.0f, 1.0f); + return _box.contains(clampedMin) && _box.contains(clampedMax); } bool ModelTreeElement::bestFitModelBounds(const ModelItem& model) const { - if (_box.contains(model.getMinimumPoint()) && _box.contains(model.getMaximumPoint())) { - int childForMinimumPoint = getMyChildContainingPoint(model.getMinimumPoint()); - int childForMaximumPoint = getMyChildContainingPoint(model.getMaximumPoint()); - + glm::vec3 clampedMin = glm::clamp(model.getMinimumPoint(), 0.0f, 1.0f); + glm::vec3 clampedMax = glm::clamp(model.getMaximumPoint(), 0.0f, 1.0f); + if (_box.contains(clampedMin) && _box.contains(clampedMax)) { + int childForMinimumPoint = getMyChildContainingPoint(clampedMin); + int childForMaximumPoint = getMyChildContainingPoint(clampedMax); // If I contain both the minimum and maximum point, but two different children of mine // contain those points, then I am the best fit for that model if (childForMinimumPoint != childForMaximumPoint) { @@ -102,10 +105,16 @@ bool ModelTreeElement::bestFitModelBounds(const ModelItem& model) const { } void ModelTreeElement::update(ModelTreeUpdateArgs& args) { + args._totalElements++; // update our contained models QList::iterator modelItr = _modelItems->begin(); while(modelItr != _modelItems->end()) { ModelItem& model = (*modelItr); + args._totalItems++; + + // TODO: this _lastChanged isn't actually changing because we're not marking this element as changed. + // how do we want to handle this??? We really only want to consider an element changed when it is + // edited... not just animated... model.update(_lastChanged); // If the model wants to die, or if it's left our bounding box, then move it @@ -115,6 +124,8 @@ void ModelTreeElement::update(ModelTreeUpdateArgs& args) { // erase this model modelItr = _modelItems->erase(modelItr); + + args._movingItems++; // this element has changed so mark it... markWithChangedTime(); @@ -324,7 +335,7 @@ int ModelTreeElement::readElementDataFromBuffer(const unsigned char* data, int b dataAt += sizeof(numberOfModels); bytesLeftToRead -= (int)sizeof(numberOfModels); bytesRead += sizeof(numberOfModels); - + if (bytesLeftToRead >= (int)(numberOfModels * expectedBytesPerModel)) { for (uint16_t i = 0; i < numberOfModels; i++) { ModelItem tempModel; diff --git a/libraries/models/src/ModelTreeElement.h b/libraries/models/src/ModelTreeElement.h index ce9e2dec7e..83b745206f 100644 --- a/libraries/models/src/ModelTreeElement.h +++ b/libraries/models/src/ModelTreeElement.h @@ -23,7 +23,16 @@ class ModelTreeElement; class ModelTreeUpdateArgs { public: + ModelTreeUpdateArgs() : + _totalElements(0), + _totalItems(0), + _movingItems(0) + { } + QList _movingModels; + int _totalElements; + int _totalItems; + int _movingItems; }; class FindAndUpdateModelItemIDArgs { @@ -63,7 +72,11 @@ public: /// Should this element be considered to have content in it. This will be used in collision and ray casting methods. /// By default we assume that only leaves are actual content, but some octrees may have different semantics. - virtual bool hasContent() const { return isLeaf(); } + virtual bool hasContent() const { return hasModels(); } + + /// Should this element be considered to have detailed content in it. Specifically should it be rendered. + /// By default we assume that only leaves have detailed content, but some octrees may have different semantics. + virtual bool hasDetailedContent() const { return hasModels(); } /// Override this to break up large octree elements when an edit operation is performed on a smaller octree element. /// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the @@ -92,7 +105,7 @@ public: const QList& getModels() const { return *_modelItems; } QList& getModels() { return *_modelItems; } - bool hasModels() const { return _modelItems->size() > 0; } + bool hasModels() const { return _modelItems ? _modelItems->size() > 0 : false; } void update(ModelTreeUpdateArgs& args); void setTree(ModelTree* tree) { _myTree = tree; } diff --git a/libraries/models/src/ModelsScriptingInterface.cpp b/libraries/models/src/ModelsScriptingInterface.cpp index 446b0280a4..7625eef998 100644 --- a/libraries/models/src/ModelsScriptingInterface.cpp +++ b/libraries/models/src/ModelsScriptingInterface.cpp @@ -105,7 +105,6 @@ ModelItemID ModelsScriptingInterface::editModel(ModelItemID modelID, const Model _modelTree->updateModel(modelID, properties); _modelTree->unlock(); } - return modelID; } diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp index 0785b81581..b9eee6e0c9 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -66,6 +66,8 @@ PacketVersion versionForPacketType(PacketType type) { return 1; case PacketTypeOctreeStats: return 1; + case PacketTypeModelData: + return 1; default: return 0; } diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 5b766ecdd7..128677ff2b 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -335,10 +335,8 @@ void Octree::readBitstreamToTree(const unsigned char * bitstream, unsigned long int octalCodeBytes = bytesRequiredForCodeLength(*bitstreamAt); int theseBytesRead = 0; theseBytesRead += octalCodeBytes; - theseBytesRead += readElementData(bitstreamRootElement, bitstreamAt + octalCodeBytes, bufferSizeBytes - (bytesRead + octalCodeBytes), args); - // skip bitstream to new startPoint bitstreamAt += theseBytesRead; bytesRead += theseBytesRead; @@ -1556,6 +1554,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElement* element, bool Octree::readFromSVOFile(const char* fileName) { bool fileOk = false; + PacketVersion gotVersion = 0; std::ifstream file(fileName, std::ios::in|std::ios::binary|std::ios::ate); if(file.is_open()) { emit importSize(1.0f, 1.0f, 1.0f); @@ -1586,14 +1585,16 @@ bool Octree::readFromSVOFile(const char* fileName) { if (gotType == expectedType) { dataAt += sizeof(expectedType); dataLength -= sizeof(expectedType); - PacketVersion expectedVersion = versionForPacketType(expectedType); - PacketVersion gotVersion = *dataAt; - if (gotVersion == expectedVersion) { - dataAt += sizeof(expectedVersion); - dataLength -= sizeof(expectedVersion); + gotVersion = *dataAt; + if (canProcessVersion(gotVersion)) { + dataAt += sizeof(gotVersion); + dataLength -= sizeof(gotVersion); fileOk = true; + qDebug("SVO file version match. Expected: %d Got: %d", + versionForPacketType(expectedDataPacketType()), gotVersion); } else { - qDebug("SVO file version mismatch. Expected: %d Got: %d", expectedVersion, gotVersion); + qDebug("SVO file version mismatch. Expected: %d Got: %d", + versionForPacketType(expectedDataPacketType()), gotVersion); } } else { qDebug("SVO file type mismatch. Expected: %c Got: %c", expectedType, gotType); @@ -1602,7 +1603,8 @@ bool Octree::readFromSVOFile(const char* fileName) { fileOk = true; // assume the file is ok } if (fileOk) { - ReadBitstreamToTreeParams args(WANT_COLOR, NO_EXISTS_BITS, NULL, 0, SharedNodePointer(), wantImportProgress); + ReadBitstreamToTreeParams args(WANT_COLOR, NO_EXISTS_BITS, NULL, 0, + SharedNodePointer(), wantImportProgress, gotVersion); readBitstreamToTree(dataAt, dataLength, args); } delete[] entireFile; @@ -1615,7 +1617,6 @@ bool Octree::readFromSVOFile(const char* fileName) { } void Octree::writeToSVOFile(const char* fileName, OctreeElement* element) { - std::ofstream file(fileName, std::ios::out|std::ios::binary); if(file.is_open()) { @@ -1638,13 +1639,12 @@ void Octree::writeToSVOFile(const char* fileName, OctreeElement* element) { nodeBag.insert(_rootElement); } - static OctreePacketData packetData; + OctreePacketData packetData; int bytesWritten = 0; bool lastPacketWritten = false; while (!nodeBag.isEmpty()) { OctreeElement* subTree = nodeBag.extract(); - lockForRead(); // do tree locking down here so that we have shorter slices and less thread contention EncodeBitstreamParams params(INT_MAX, IGNORE_VIEW_FRUSTUM, WANT_COLOR, NO_EXISTS_BITS); bytesWritten = encodeTreeBitstream(subTree, &packetData, nodeBag, params); @@ -1666,7 +1666,6 @@ void Octree::writeToSVOFile(const char* fileName, OctreeElement* element) { if (!lastPacketWritten) { file.write((const char*)packetData.getFinalizedData(), packetData.getFinalizedSize()); } - } file.close(); } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 4a17cb3c1d..84212586f8 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -170,6 +170,7 @@ public: QUuid sourceUUID; SharedNodePointer sourceNode; bool wantImportProgress; + PacketVersion bitstreamVersion; ReadBitstreamToTreeParams( bool includeColor = WANT_COLOR, @@ -177,13 +178,15 @@ public: OctreeElement* destinationElement = NULL, QUuid sourceUUID = QUuid(), SharedNodePointer sourceNode = SharedNodePointer(), - bool wantImportProgress = false) : + bool wantImportProgress = false, + PacketVersion bitstreamVersion = 0) : includeColor(includeColor), includeExistsBits(includeExistsBits), destinationElement(destinationElement), sourceUUID(sourceUUID), sourceNode(sourceNode), - wantImportProgress(wantImportProgress) + wantImportProgress(wantImportProgress), + bitstreamVersion(bitstreamVersion) {} }; @@ -200,6 +203,9 @@ public: // own definition. Implement these to allow your octree based server to support editing virtual bool getWantSVOfileVersions() const { return false; } virtual PacketType expectedDataPacketType() const { return PacketTypeUnknown; } + virtual bool canProcessVersion(PacketVersion thisVersion) const { + return thisVersion == versionForPacketType(expectedDataPacketType()); } + virtual PacketVersion expectedVersion() const { return versionForPacketType(expectedDataPacketType()); } virtual bool handlesEditPacketType(PacketType packetType) const { return false; } virtual int processEditPacketData(PacketType packetType, const unsigned char* packetData, int packetLength, const unsigned char* editData, int maxLength, const SharedNodePointer& sourceNode) { return 0; } @@ -303,6 +309,7 @@ public: bool getIsViewing() const { return _isViewing; } void setIsViewing(bool isViewing) { _isViewing = isViewing; } + signals: void importSize(float x, float y, float z); diff --git a/libraries/octree/src/OctreeElement.cpp b/libraries/octree/src/OctreeElement.cpp index edba26f2a7..0462a3b53d 100644 --- a/libraries/octree/src/OctreeElement.cpp +++ b/libraries/octree/src/OctreeElement.cpp @@ -1213,7 +1213,7 @@ bool OctreeElement::calculateShouldRender(const ViewFrustum* viewFrustum, float float furthestDistance = furthestDistanceToCamera(*viewFrustum); float childBoundary = boundaryDistanceForRenderLevel(getLevel() + 1 + boundaryLevelAdjust, voxelScaleSize); bool inChildBoundary = (furthestDistance <= childBoundary); - if (isLeaf() && inChildBoundary) { + if (hasDetailedContent() && inChildBoundary) { shouldRender = true; } else { float boundary = childBoundary * 2.0f; // the boundary is always twice the distance of the child boundary diff --git a/libraries/octree/src/OctreeElement.h b/libraries/octree/src/OctreeElement.h index 42c9abad46..2485e49797 100644 --- a/libraries/octree/src/OctreeElement.h +++ b/libraries/octree/src/OctreeElement.h @@ -71,6 +71,10 @@ public: /// Should this element be considered to have content in it. This will be used in collision and ray casting methods. /// By default we assume that only leaves are actual content, but some octrees may have different semantics. virtual bool hasContent() const { return isLeaf(); } + + /// Should this element be considered to have detailed content in it. Specifically should it be rendered. + /// By default we assume that only leaves have detailed content, but some octrees may have different semantics. + virtual bool hasDetailedContent() const { return isLeaf(); } /// Override this to break up large octree elements when an edit operation is performed on a smaller octree element. /// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the diff --git a/libraries/octree/src/OctreeRenderer.cpp b/libraries/octree/src/OctreeRenderer.cpp index c1ce3cb218..e2aec8c890 100644 --- a/libraries/octree/src/OctreeRenderer.cpp +++ b/libraries/octree/src/OctreeRenderer.cpp @@ -64,6 +64,7 @@ void OctreeRenderer::processDatagram(const QByteArray& dataByteArray, const Shar unsigned int numBytesPacketHeader = numBytesForPacketHeader(dataByteArray); QUuid sourceUUID = uuidFromPacketHeader(dataByteArray); PacketType expectedType = getExpectedPacketType(); + PacketVersion expectedVersion = _tree->expectedVersion(); // TODO: would be better to read this from the packet! if(command == expectedType) { PerformanceWarning warn(showTimingDetails, "OctreeRenderer::processDatagram expected PacketType", showTimingDetails); @@ -115,7 +116,7 @@ void OctreeRenderer::processDatagram(const QByteArray& dataByteArray, const Shar if (sectionLength) { // ask the VoxelTree to read the bitstream into the tree ReadBitstreamToTreeParams args(packetIsColored ? WANT_COLOR : NO_COLOR, WANT_EXISTS_BITS, NULL, - sourceUUID, sourceNode); + sourceUUID, sourceNode, false, expectedVersion); _tree->lockForWrite(); OctreePacketData packetData(packetIsCompressed); packetData.loadFinalizedContent(dataAt, sectionLength); @@ -155,7 +156,7 @@ bool OctreeRenderer::renderOperation(OctreeElement* element, void* extraData) { } void OctreeRenderer::render(RenderMode renderMode) { - RenderArgs args = { 0, this, _viewFrustum, getSizeScale(), getBoundaryLevelAdjust(), renderMode }; + RenderArgs args = { this, _viewFrustum, getSizeScale(), getBoundaryLevelAdjust(), renderMode, 0, 0, 0 }; if (_tree) { _tree->lockForRead(); _tree->recurseTreeWithOperation(renderOperation, &args); diff --git a/libraries/octree/src/OctreeRenderer.h b/libraries/octree/src/OctreeRenderer.h index 73e26c97f6..18e68e26aa 100644 --- a/libraries/octree/src/OctreeRenderer.h +++ b/libraries/octree/src/OctreeRenderer.h @@ -71,12 +71,15 @@ protected: class RenderArgs { public: - int _renderedItems; OctreeRenderer* _renderer; ViewFrustum* _viewFrustum; float _sizeScale; int _boundaryLevelAdjust; OctreeRenderer::RenderMode _renderMode; + + int _elementsTouched; + int _itemsRendered; + int _itemsOutOfView; }; diff --git a/libraries/particles/CMakeLists.txt b/libraries/particles/CMakeLists.txt index 1cb60756a2..8cd2f30012 100644 --- a/libraries/particles/CMakeLists.txt +++ b/libraries/particles/CMakeLists.txt @@ -25,6 +25,7 @@ link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(animation ${TARGET_NAME} "${ROOT_DIR}") # link ZLIB and GnuTLS find_package(ZLIB) diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index ee918ff864..0374ad570c 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -27,6 +27,7 @@ link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}") link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_library(animation ${TARGET_NAME} "${ROOT_DIR}") # link ZLIB find_package(ZLIB) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index be97b37b46..a0ecc88a7b 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -35,6 +35,7 @@ #include "MenuItemProperties.h" #include "LocalVoxels.h" #include "ScriptEngine.h" +#include "XMLHttpRequestClass.h" VoxelsScriptingInterface ScriptEngine::_voxelsScriptingInterface; ParticlesScriptingInterface ScriptEngine::_particlesScriptingInterface; @@ -49,7 +50,12 @@ static QScriptValue soundConstructor(QScriptContext* context, QScriptEngine* eng static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ qDebug() << "script:print()<<" << context->argument(0).toString(); - engine->evaluate("Script.print('" + context->argument(0).toString() + "')"); + QString message = context->argument(0).toString() + .replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("'", "\\'"); + engine->evaluate("Script.print('" + message + "')"); return QScriptValue(); } @@ -224,6 +230,9 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(&_engine); qScriptRegisterSequenceMetaType >(&_engine); + QScriptValue xmlHttpRequestConstructorValue = _engine.newFunction(XMLHttpRequestClass::constructor); + _engine.globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); + QScriptValue printConstructorValue = _engine.newFunction(debugPrint); _engine.globalObject().setProperty("print", printConstructorValue); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 09d41e3e2e..96cc874453 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -18,13 +18,12 @@ #include #include +#include #include -#include - #include #include +#include -#include "AnimationCache.h" #include "AbstractControllerScriptingInterface.h" #include "Quat.h" #include "ScriptUUID.h" diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp new file mode 100644 index 0000000000..a81f8950fa --- /dev/null +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -0,0 +1,233 @@ +// +// XMLHttpRequestClass.cpp +// libraries/script-engine/src/ +// +// Created by Ryan Huffman on 5/2/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// This class is an implementation of the XMLHttpRequest object for scripting use. It provides a near-complete implementation +// of the class described in the Mozilla docs: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "XMLHttpRequestClass.h" + +XMLHttpRequestClass::XMLHttpRequestClass(QScriptEngine* engine) : + _engine(engine), + _async(true), + _url(), + _method(""), + _responseType(""), + _manager(this), + _request(), + _reply(NULL), + _sendData(NULL), + _rawResponseData(), + _responseData(""), + _onTimeout(QScriptValue::NullValue), + _onReadyStateChange(QScriptValue::NullValue), + _readyState(XMLHttpRequestClass::UNSENT), + _errorCode(QNetworkReply::NoError), + _timeout(0), + _timer(this), + _numRedirects(0) { + + _timer.setSingleShot(true); +} + +XMLHttpRequestClass::~XMLHttpRequestClass() { + if (_reply) { delete _reply; } + if (_sendData) { delete _sendData; } +} + +QScriptValue XMLHttpRequestClass::constructor(QScriptContext* context, QScriptEngine* engine) { + return engine->newQObject(new XMLHttpRequestClass(engine)); +} + +QScriptValue XMLHttpRequestClass::getStatus() const { + if (_reply) { + return QScriptValue(_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } + return QScriptValue(0); +} + +QString XMLHttpRequestClass::getStatusText() const { + if (_reply) { + return _reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + } + return ""; +} + +void XMLHttpRequestClass::abort() { + abortRequest(); +} + +void XMLHttpRequestClass::setRequestHeader(const QString& name, const QString& value) { + _request.setRawHeader(QByteArray(name.toLatin1()), QByteArray(value.toLatin1())); +} + +void XMLHttpRequestClass::requestMetaDataChanged() { + QVariant redirect = _reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + + // If this is a redirect, abort the current request and start a new one + if (redirect.isValid() && _numRedirects < MAXIMUM_REDIRECTS) { + _numRedirects++; + abortRequest(); + + QUrl newUrl = _url.resolved(redirect.toUrl().toString()); + _request.setUrl(newUrl); + doSend(); + } +} + +void XMLHttpRequestClass::requestDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + if (_readyState == OPENED && bytesReceived > 0) { + setReadyState(HEADERS_RECEIVED); + setReadyState(LOADING); + } +} + +QScriptValue XMLHttpRequestClass::getAllResponseHeaders() const { + if (_reply) { + QList headerList = _reply->rawHeaderPairs(); + QByteArray headers; + for (int i = 0; i < headerList.size(); i++) { + headers.append(headerList[i].first); + headers.append(": "); + headers.append(headerList[i].second); + headers.append("\n"); + } + return QString(headers.data()); + } + return QScriptValue(""); +} + +QScriptValue XMLHttpRequestClass::getResponseHeader(const QString& name) const { + if (_reply && _reply->hasRawHeader(name.toLatin1())) { + return QScriptValue(QString(_reply->rawHeader(name.toLatin1()))); + } + return QScriptValue::NullValue; +} + +void XMLHttpRequestClass::setReadyState(ReadyState readyState) { + if (readyState != _readyState) { + _readyState = readyState; + if (_onReadyStateChange.isFunction()) { + _onReadyStateChange.call(QScriptValue::NullValue); + } + } +} + +void XMLHttpRequestClass::open(const QString& method, const QString& url, bool async, const QString& username, + const QString& password) { + if (_readyState == UNSENT) { + _async = async; + _url.setUrl(url); + if (!username.isEmpty()) { + _url.setUserName(username); + } + if (!password.isEmpty()) { + _url.setPassword(password); + } + _request.setUrl(_url); + _method = method; + setReadyState(OPENED); + } +} + +void XMLHttpRequestClass::send() { + send(QString::Null()); +} + +void XMLHttpRequestClass::send(const QString& data) { + if (_readyState == OPENED && !_reply) { + if (!data.isNull()) { + _sendData = new QBuffer(this); + _sendData->setData(data.toUtf8()); + } + + doSend(); + + if (!_async) { + QEventLoop loop; + connect(this, SIGNAL(requestComplete()), &loop, SLOT(quit())); + loop.exec(); + } + } +} + +void XMLHttpRequestClass::doSend() { + _reply = _manager.sendCustomRequest(_request, _method.toLatin1(), _sendData); + + connectToReply(_reply); + + if (_timeout > 0) { + _timer.start(_timeout); + connect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout())); + } +} + +void XMLHttpRequestClass::requestTimeout() { + if (_onTimeout.isFunction()) { + _onTimeout.call(QScriptValue::NullValue); + } + abortRequest(); + _errorCode = QNetworkReply::TimeoutError; + setReadyState(DONE); + emit requestComplete(); +} + +void XMLHttpRequestClass::requestError(QNetworkReply::NetworkError code) { +} + +void XMLHttpRequestClass::requestFinished() { + disconnect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout())); + + _errorCode = _reply->error(); + if (_errorCode == QNetworkReply::NoError) { + _rawResponseData.append(_reply->readAll()); + + if (_responseType == "json") { + _responseData = _engine->evaluate("(" + QString(_rawResponseData.data()) + ")"); + if (_responseData.isError()) { + _engine->clearExceptions(); + _responseData = QScriptValue::NullValue; + } + } else if (_responseType == "arraybuffer") { + _responseData = QScriptValue(_rawResponseData.data()); + } else { + _responseData = QScriptValue(QString(_rawResponseData.data())); + } + } + setReadyState(DONE); + emit requestComplete(); +} + +void XMLHttpRequestClass::abortRequest() { + // Disconnect from signals we don't want to receive any longer. + disconnect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout())); + if (_reply) { + disconnectFromReply(_reply); + _reply->abort(); + delete _reply; + _reply = NULL; + } +} + +void XMLHttpRequestClass::connectToReply(QNetworkReply* reply) { + connect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(requestDownloadProgress(qint64, qint64))); + connect(reply, SIGNAL(metaDataChanged()), this, SLOT(requestMetaDataChanged())); +} + +void XMLHttpRequestClass::disconnectFromReply(QNetworkReply* reply) { + disconnect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); + disconnect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + disconnect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(requestDownloadProgress(qint64, qint64))); + disconnect(reply, SIGNAL(metaDataChanged()), this, SLOT(requestMetaDataChanged())); +} diff --git a/libraries/script-engine/src/XMLHttpRequestClass.h b/libraries/script-engine/src/XMLHttpRequestClass.h new file mode 100644 index 0000000000..49a952e638 --- /dev/null +++ b/libraries/script-engine/src/XMLHttpRequestClass.h @@ -0,0 +1,129 @@ +// +// XMLHttpRequestClass.h +// libraries/script-engine/src/ +// +// Created by Ryan Huffman on 5/2/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_XMLHttpRequestClass_h +#define hifi_XMLHttpRequestClass_h + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class XMLHttpRequestClass : public QObject { + Q_OBJECT + Q_PROPERTY(QScriptValue response READ getResponse) + Q_PROPERTY(QScriptValue responseText READ getResponseText) + Q_PROPERTY(QString responseType READ getResponseType WRITE setResponseType) + Q_PROPERTY(QScriptValue status READ getStatus) + Q_PROPERTY(QString statusText READ getStatusText) + Q_PROPERTY(QScriptValue readyState READ getReadyState) + Q_PROPERTY(QScriptValue errorCode READ getError) + Q_PROPERTY(int timeout READ getTimeout WRITE setTimeout) + + Q_PROPERTY(int UNSENT READ getUnsent) + Q_PROPERTY(int OPENED READ getOpened) + Q_PROPERTY(int HEADERS_RECEIVED READ getHeadersReceived) + Q_PROPERTY(int LOADING READ getLoading) + Q_PROPERTY(int DONE READ getDone) + + // Callbacks + Q_PROPERTY(QScriptValue ontimeout READ getOnTimeout WRITE setOnTimeout) + Q_PROPERTY(QScriptValue onreadystatechange READ getOnReadyStateChange WRITE setOnReadyStateChange) +public: + XMLHttpRequestClass(QScriptEngine* engine); + ~XMLHttpRequestClass(); + + static const int MAXIMUM_REDIRECTS = 5; + enum ReadyState { + UNSENT = 0, + OPENED, + HEADERS_RECEIVED, + LOADING, + DONE + }; + + int getUnsent() const { return UNSENT; }; + int getOpened() const { return OPENED; }; + int getHeadersReceived() const { return HEADERS_RECEIVED; }; + int getLoading() const { return LOADING; }; + int getDone() const { return DONE; }; + + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + + int getTimeout() const { return _timeout; } + void setTimeout(int timeout) { _timeout = timeout; } + QScriptValue getResponse() const { return _responseData; } + QScriptValue getResponseText() const { return QScriptValue(QString(_rawResponseData.data())); } + QString getResponseType() const { return _responseType; } + void setResponseType(const QString& responseType) { _responseType = responseType; } + QScriptValue getReadyState() const { return QScriptValue(_readyState); } + QScriptValue getError() const { return QScriptValue(_errorCode); } + QScriptValue getStatus() const; + QString getStatusText() const; + + QScriptValue getOnTimeout() const { return _onTimeout; } + void setOnTimeout(QScriptValue function) { _onTimeout = function; } + QScriptValue getOnReadyStateChange() const { return _onReadyStateChange; } + void setOnReadyStateChange(QScriptValue function) { _onReadyStateChange = function; } + +public slots: + void abort(); + void setRequestHeader(const QString& name, const QString& value); + void open(const QString& method, const QString& url, bool async = true, const QString& username = "", + const QString& password = ""); + void send(); + void send(const QString& data); + QScriptValue getAllResponseHeaders() const; + QScriptValue getResponseHeader(const QString& name) const; + +signals: + void requestComplete(); + +private: + void setReadyState(ReadyState readyState); + void doSend(); + void connectToReply(QNetworkReply* reply); + void disconnectFromReply(QNetworkReply* reply); + void abortRequest(); + + QScriptEngine* _engine; + bool _async; + QUrl _url; + QString _method; + QString _responseType; + QNetworkAccessManager _manager; + QNetworkRequest _request; + QNetworkReply* _reply; + QBuffer* _sendData; + QByteArray _rawResponseData; + QScriptValue _responseData; + QScriptValue _onTimeout; + QScriptValue _onReadyStateChange; + ReadyState _readyState; + QNetworkReply::NetworkError _errorCode; + int _timeout; + QTimer _timer; + int _numRedirects; + +private slots: + void requestFinished(); + void requestError(QNetworkReply::NetworkError code); + void requestMetaDataChanged(); + void requestDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void requestTimeout(); +}; + +#endif // hifi_XMLHttpRequestClass_h