diff --git a/CMakeLists.txt b/CMakeLists.txt index 2dbc89b75f..28ff00cc96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,10 +44,17 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) -# targets not supported on windows -if (NOT WIN32) - add_subdirectory(animation-server) -endif () +set(HIFI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries") + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/") + +set(MACRO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macros") + +file(GLOB HIFI_CUSTOM_MACROS "cmake/macros/*.cmake") +foreach(CUSTOM_MACRO ${HIFI_CUSTOM_MACROS}) + include(${CUSTOM_MACRO}) +endforeach() # targets on all platforms add_subdirectory(assignment-client) diff --git a/animation-server/CMakeLists.txt b/animation-server/CMakeLists.txt deleted file mode 100644 index 116ee0e942..0000000000 --- a/animation-server/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ -if (WIN32) - cmake_policy (SET CMP0020 NEW) -endif (WIN32) - -set(TARGET_NAME animation-server) - -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/") - -find_package(Qt5 COMPONENTS Script) -include_directories(SYSTEM "${Qt5Script_INCLUDE_DIRS}") - -# set up the external glm library -include("${MACRO_DIR}/IncludeGLM.cmake") -include_glm(${TARGET_NAME} "${ROOT_DIR}") - -include("${MACRO_DIR}/SetupHifiProject.cmake") -setup_hifi_project(${TARGET_NAME} TRUE) - -# link in the shared library -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") - -# link in the hifi octree library -link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") - -# link in the hifi voxels library -link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") - -# link the hifi networking library -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () diff --git a/animation-server/src/AnimationServer.cpp b/animation-server/src/AnimationServer.cpp deleted file mode 100644 index 32a95b48ea..0000000000 --- a/animation-server/src/AnimationServer.cpp +++ /dev/null @@ -1,852 +0,0 @@ -// -// AnimationServer.cpp -// animation-server/src -// -// Created by Stephen Birarda on 12/5/2013. -// Copyright 2013 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 -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "AnimationServer.h" - -bool shouldShowPacketsPerSecond = false; // do we want to debug packets per second -bool includeBillboard = true; -bool includeBorderTracer = true; -bool includeMovingBug = true; -bool includeBlinkingVoxel = false; -bool includeDanceFloor = true; -bool buildStreet = false; -bool nonThreadedPacketSender = false; -int packetsPerSecond = PacketSender::DEFAULT_PACKETS_PER_SECOND; -bool waitForVoxelServer = true; - - -const int ANIMATION_LISTEN_PORT = 40107; -int ANIMATE_FPS = 60; -double ANIMATE_FPS_IN_MILLISECONDS = 1000.0/ANIMATE_FPS; // determines FPS from our desired FPS -quint64 ANIMATE_VOXELS_INTERVAL_USECS = (ANIMATE_FPS_IN_MILLISECONDS * 1000); // converts from milliseconds to usecs - - -int PROCESSING_FPS = 60; -double PROCESSING_FPS_IN_MILLISECONDS = 1000.0/PROCESSING_FPS; // determines FPS from our desired FPS -int FUDGE_USECS = 650; // a little bit of fudge to actually do some processing -quint64 PROCESSING_INTERVAL_USECS = (PROCESSING_FPS_IN_MILLISECONDS * 1000) - FUDGE_USECS; // converts from milliseconds to usecs - -bool wantLocalDomain = false; - -unsigned long packetsSent = 0; -unsigned long bytesSent = 0; - -JurisdictionListener* jurisdictionListener = NULL; -VoxelEditPacketSender* voxelEditPacketSender = NULL; -pthread_t animateVoxelThread; - -glm::vec3 rotatePoint(glm::vec3 point, float angle) { - // First, create the quaternion based on this angle of rotation - glm::quat q(glm::vec3(0, -angle, 0)); - - // Next, create a rotation matrix from that quaternion - glm::mat4 rotation = glm::mat4_cast(q); - - // Transform the original vectors by the rotation matrix to get the new vectors - glm::vec4 quatPoint(point.x, point.y, point.z, 0); - glm::vec4 newPoint = quatPoint * rotation; - - return glm::vec3(newPoint.x, newPoint.y, newPoint.z); -} - - -const float BUG_VOXEL_SIZE = 0.0625f / TREE_SCALE; -glm::vec3 bugPosition = glm::vec3(BUG_VOXEL_SIZE * 20.0, BUG_VOXEL_SIZE * 30.0, BUG_VOXEL_SIZE * 20.0); -glm::vec3 bugDirection = glm::vec3(0, 0, 1); -const int VOXELS_PER_BUG = 18; -glm::vec3 bugPathCenter = glm::vec3(0.25f,0.15f,0.25f); // glm::vec3(BUG_VOXEL_SIZE * 150.0, BUG_VOXEL_SIZE * 30.0, BUG_VOXEL_SIZE * 150.0); -float bugPathRadius = 0.2f; //BUG_VOXEL_SIZE * 140.0; -float bugPathTheta = 0.0f * RADIANS_PER_DEGREE; -float bugRotation = 0.0f * RADIANS_PER_DEGREE; -float bugAngleDelta = 0.2f * RADIANS_PER_DEGREE; -bool moveBugInLine = false; - -class BugPart { -public: - glm::vec3 partLocation; - unsigned char partColor[3]; - - BugPart(const glm::vec3& location, unsigned char red, unsigned char green, unsigned char blue ) { - partLocation = location; - partColor[0] = red; - partColor[1] = green; - partColor[2] = blue; - } -}; - -const BugPart bugParts[VOXELS_PER_BUG] = { - - // tail - BugPart(glm::vec3( 0, 0, -3), 51, 51, 153) , - BugPart(glm::vec3( 0, 0, -2), 51, 51, 153) , - BugPart(glm::vec3( 0, 0, -1), 51, 51, 153) , - - // body - BugPart(glm::vec3( 0, 0, 0), 255, 200, 0) , - BugPart(glm::vec3( 0, 0, 1), 255, 200, 0) , - - // head - BugPart(glm::vec3( 0, 0, 2), 200, 0, 0) , - - // eyes - BugPart(glm::vec3( 1, 0, 3), 64, 64, 64) , - BugPart(glm::vec3(-1, 0, 3), 64, 64, 64) , - - // wings - BugPart(glm::vec3( 3, 1, 1), 0, 153, 0) , - BugPart(glm::vec3( 2, 1, 1), 0, 153, 0) , - BugPart(glm::vec3( 1, 0, 1), 0, 153, 0) , - BugPart(glm::vec3(-1, 0, 1), 0, 153, 0) , - BugPart(glm::vec3(-2, 1, 1), 0, 153, 0) , - BugPart(glm::vec3(-3, 1, 1), 0, 153, 0) , - - - BugPart(glm::vec3( 2, -1, 0), 153, 200, 0) , - BugPart(glm::vec3( 1, -1, 0), 153, 200, 0) , - BugPart(glm::vec3(-1, -1, 0), 153, 200, 0) , - BugPart(glm::vec3(-2, -1, 0), 153, 200, 0) , -}; - -static void renderMovingBug() { - VoxelDetail details[VOXELS_PER_BUG]; - - // Generate voxels for where bug used to be - for (int i = 0; i < VOXELS_PER_BUG; i++) { - details[i].s = BUG_VOXEL_SIZE; - - glm::vec3 partAt = bugParts[i].partLocation * BUG_VOXEL_SIZE * (bugDirection.x < 0 ? -1.0f : 1.0f); - glm::vec3 rotatedPartAt = rotatePoint(partAt, bugRotation); - glm::vec3 offsetPartAt = rotatedPartAt + bugPosition; - - details[i].x = offsetPartAt.x; - details[i].y = offsetPartAt.y; - details[i].z = offsetPartAt.z; - - details[i].red = bugParts[i].partColor[0]; - details[i].green = bugParts[i].partColor[1]; - details[i].blue = bugParts[i].partColor[2]; - } - - // send the "erase message" first... - PacketType message = PacketTypeVoxelErase; - ::voxelEditPacketSender->queueVoxelEditMessages(message, VOXELS_PER_BUG, (VoxelDetail*)&details); - - // Move the bug... - if (moveBugInLine) { - bugPosition.x += (bugDirection.x * BUG_VOXEL_SIZE); - bugPosition.y += (bugDirection.y * BUG_VOXEL_SIZE); - bugPosition.z += (bugDirection.z * BUG_VOXEL_SIZE); - - // Check boundaries - if (bugPosition.z > 1.0f) { - bugDirection.z = -1.f; - } - if (bugPosition.z < BUG_VOXEL_SIZE) { - bugDirection.z = 1.f; - } - } else { - - //qDebug("bugPathCenter=(%f,%f,%f)", bugPathCenter.x, bugPathCenter.y, bugPathCenter.z); - - bugPathTheta += bugAngleDelta; // move slightly - bugRotation -= bugAngleDelta; // rotate slightly - - // If we loop past end of circle, just reset back into normal range - if (bugPathTheta > TWO_PI) { - bugPathTheta = 0.f; - bugRotation = 0.f; - } - - float x = bugPathCenter.x + bugPathRadius * cos(bugPathTheta); - float z = bugPathCenter.z + bugPathRadius * sin(bugPathTheta); - float y = bugPathCenter.y; - - bugPosition = glm::vec3(x, y, z); - //qDebug("bugPathTheta=%f", bugPathTheta); - //qDebug("bugRotation=%f", bugRotation); - } - - //qDebug("bugPosition=(%f,%f,%f)", bugPosition.x, bugPosition.y, bugPosition.z); - //qDebug("bugDirection=(%f,%f,%f)", bugDirection.x, bugDirection.y, bugDirection.z); - // would be nice to add some randomness here... - - // Generate voxels for where bug is going to - for (int i = 0; i < VOXELS_PER_BUG; i++) { - details[i].s = BUG_VOXEL_SIZE; - - glm::vec3 partAt = bugParts[i].partLocation * BUG_VOXEL_SIZE * (bugDirection.x < 0 ? -1.0f : 1.0f); - glm::vec3 rotatedPartAt = rotatePoint(partAt, bugRotation); - glm::vec3 offsetPartAt = rotatedPartAt + bugPosition; - - details[i].x = offsetPartAt.x; - details[i].y = offsetPartAt.y; - details[i].z = offsetPartAt.z; - - details[i].red = bugParts[i].partColor[0]; - details[i].green = bugParts[i].partColor[1]; - details[i].blue = bugParts[i].partColor[2]; - } - - // send the "create message" ... - message = PacketTypeVoxelSetDestructive; - ::voxelEditPacketSender->queueVoxelEditMessages(message, VOXELS_PER_BUG, (VoxelDetail*)&details); -} - - -float intensity = 0.5f; -float intensityIncrement = 0.1f; -const float MAX_INTENSITY = 1.0f; -const float MIN_INTENSITY = 0.5f; -const float BEACON_SIZE = 0.25f / TREE_SCALE; // approximately 1/4th meter - -static void sendVoxelBlinkMessage() { - VoxelDetail detail; - detail.s = BEACON_SIZE; - - glm::vec3 position = glm::vec3(0.f, 0.f, detail.s); - - detail.x = detail.s * floor(position.x / detail.s); - detail.y = detail.s * floor(position.y / detail.s); - detail.z = detail.s * floor(position.z / detail.s); - - ::intensity += ::intensityIncrement; - if (::intensity >= MAX_INTENSITY) { - ::intensity = MAX_INTENSITY; - ::intensityIncrement = -::intensityIncrement; - } - if (::intensity <= MIN_INTENSITY) { - ::intensity = MIN_INTENSITY; - ::intensityIncrement = -::intensityIncrement; - } - - detail.red = 255 * ::intensity; - detail.green = 0 * ::intensity; - detail.blue = 0 * ::intensity; - - PacketType message = PacketTypeVoxelSetDestructive; - - ::voxelEditPacketSender->sendVoxelEditMessage(message, detail); -} - -bool stringOfLightsInitialized = false; -int currentLight = 0; -int lightMovementDirection = 1; -const int SEGMENT_COUNT = 4; -const int LIGHTS_PER_SEGMENT = 80; -const int LIGHT_COUNT = LIGHTS_PER_SEGMENT * SEGMENT_COUNT; -glm::vec3 stringOfLights[LIGHT_COUNT]; -unsigned char offColor[3] = { 240, 240, 240 }; -unsigned char onColor[3] = { 0, 255, 255 }; -const float STRING_OF_LIGHTS_SIZE = 0.125f / TREE_SCALE; // approximately 1/8th meter - -static void sendBlinkingStringOfLights() { - PacketType message = PacketTypeVoxelSetDestructive; // we're a bully! - float lightScale = STRING_OF_LIGHTS_SIZE; - static VoxelDetail details[LIGHTS_PER_SEGMENT]; - - // first initialized the string of lights if needed... - if (!stringOfLightsInitialized) { - for (int segment = 0; segment < SEGMENT_COUNT; segment++) { - for (int indexInSegment = 0; indexInSegment < LIGHTS_PER_SEGMENT; indexInSegment++) { - - int i = (segment * LIGHTS_PER_SEGMENT) + indexInSegment; - - // four different segments on sides of initial platform - switch (segment) { - case 0: - // along x axis - stringOfLights[i] = glm::vec3(indexInSegment * lightScale, 0, 0); - break; - case 1: - // parallel to Z axis at outer X edge - stringOfLights[i] = glm::vec3(LIGHTS_PER_SEGMENT * lightScale, 0, indexInSegment * lightScale); - break; - case 2: - // parallel to X axis at outer Z edge - stringOfLights[i] = glm::vec3((LIGHTS_PER_SEGMENT-indexInSegment) * lightScale, 0, - LIGHTS_PER_SEGMENT * lightScale); - break; - case 3: - // on Z axis - stringOfLights[i] = glm::vec3(0, 0, (LIGHTS_PER_SEGMENT-indexInSegment) * lightScale); - break; - } - - details[indexInSegment].s = STRING_OF_LIGHTS_SIZE; - details[indexInSegment].x = stringOfLights[i].x; - details[indexInSegment].y = stringOfLights[i].y; - details[indexInSegment].z = stringOfLights[i].z; - - details[indexInSegment].red = offColor[0]; - details[indexInSegment].green = offColor[1]; - details[indexInSegment].blue = offColor[2]; - } - - ::voxelEditPacketSender->queueVoxelEditMessages(message, LIGHTS_PER_SEGMENT, (VoxelDetail*)&details); - } - stringOfLightsInitialized = true; - } else { - // turn off current light - details[0].x = stringOfLights[currentLight].x; - details[0].y = stringOfLights[currentLight].y; - details[0].z = stringOfLights[currentLight].z; - details[0].red = offColor[0]; - details[0].green = offColor[1]; - details[0].blue = offColor[2]; - - // move current light... - // if we're at the end of our string, then change direction - if (currentLight == LIGHT_COUNT-1) { - lightMovementDirection = -1; - } - if (currentLight == 0) { - lightMovementDirection = 1; - } - currentLight += lightMovementDirection; - - // turn on new current light - details[1].x = stringOfLights[currentLight].x; - details[1].y = stringOfLights[currentLight].y; - details[1].z = stringOfLights[currentLight].z; - details[1].red = onColor[0]; - details[1].green = onColor[1]; - details[1].blue = onColor[2]; - - // send both changes in same message - ::voxelEditPacketSender->queueVoxelEditMessages(message, 2, (VoxelDetail*)&details); - } -} - -bool danceFloorInitialized = false; -const float DANCE_FLOOR_LIGHT_SIZE = 1.0f / TREE_SCALE; // approximately 1 meter -const int DANCE_FLOOR_LENGTH = 10; -const int DANCE_FLOOR_WIDTH = 10; -glm::vec3 danceFloorPosition(100.0f / TREE_SCALE, 30.0f / TREE_SCALE, 10.0f / TREE_SCALE); -glm::vec3 danceFloorLights[DANCE_FLOOR_LENGTH][DANCE_FLOOR_WIDTH]; -unsigned char danceFloorOffColor[3] = { 240, 240, 240 }; -const int DANCE_FLOOR_COLORS = 6; - -unsigned char danceFloorOnColorA[DANCE_FLOOR_COLORS][3] = { - { 255, 0, 0 }, { 0, 255, 0 }, { 0, 0, 255 }, - { 0, 191, 255 }, { 0, 250, 154 }, { 255, 69, 0 }, -}; -unsigned char danceFloorOnColorB[DANCE_FLOOR_COLORS][3] = { - { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 } , - { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 } -}; -float danceFloorGradient = 0.5f; -const float BEATS_PER_MINUTE = 118.0f; -const float SECONDS_PER_MINUTE = 60.0f; -const float FRAMES_PER_BEAT = (SECONDS_PER_MINUTE * ANIMATE_FPS) / BEATS_PER_MINUTE; -float danceFloorGradientIncrement = 1.0f / FRAMES_PER_BEAT; -const float DANCE_FLOOR_MAX_GRADIENT = 1.0f; -const float DANCE_FLOOR_MIN_GRADIENT = 0.0f; -const int DANCE_FLOOR_VOXELS_PER_PACKET = 100; -int danceFloorColors[DANCE_FLOOR_WIDTH][DANCE_FLOOR_LENGTH]; - -void sendDanceFloor() { - PacketType message = PacketTypeVoxelSetDestructive; // we're a bully! - float lightScale = DANCE_FLOOR_LIGHT_SIZE; - static VoxelDetail details[DANCE_FLOOR_VOXELS_PER_PACKET]; - - // first initialized the billboard of lights if needed... - if (!::danceFloorInitialized) { - for (int i = 0; i < DANCE_FLOOR_WIDTH; i++) { - for (int j = 0; j < DANCE_FLOOR_LENGTH; j++) { - - int randomColorIndex = randIntInRange(-DANCE_FLOOR_COLORS, DANCE_FLOOR_COLORS); - ::danceFloorColors[i][j] = randomColorIndex; - ::danceFloorLights[i][j] = ::danceFloorPosition + - glm::vec3(i * DANCE_FLOOR_LIGHT_SIZE, 0, j * DANCE_FLOOR_LIGHT_SIZE); - } - } - ::danceFloorInitialized = true; - } - - ::danceFloorGradient += ::danceFloorGradientIncrement; - - if (::danceFloorGradient >= DANCE_FLOOR_MAX_GRADIENT) { - ::danceFloorGradient = DANCE_FLOOR_MAX_GRADIENT; - ::danceFloorGradientIncrement = -::danceFloorGradientIncrement; - } - if (::danceFloorGradient <= DANCE_FLOOR_MIN_GRADIENT) { - ::danceFloorGradient = DANCE_FLOOR_MIN_GRADIENT; - ::danceFloorGradientIncrement = -::danceFloorGradientIncrement; - } - - for (int i = 0; i < DANCE_FLOOR_LENGTH; i++) { - for (int j = 0; j < DANCE_FLOOR_WIDTH; j++) { - - int nthVoxel = ((i * DANCE_FLOOR_WIDTH) + j); - int item = nthVoxel % DANCE_FLOOR_VOXELS_PER_PACKET; - - ::danceFloorLights[i][j] = ::danceFloorPosition + - glm::vec3(i * DANCE_FLOOR_LIGHT_SIZE, 0, j * DANCE_FLOOR_LIGHT_SIZE); - - details[item].s = lightScale; - details[item].x = ::danceFloorLights[i][j].x; - details[item].y = ::danceFloorLights[i][j].y; - details[item].z = ::danceFloorLights[i][j].z; - - if (danceFloorColors[i][j] > 0) { - int color = danceFloorColors[i][j] - 1; - details[item].red = (::danceFloorOnColorA[color][0] + - ((::danceFloorOnColorB[color][0] - ::danceFloorOnColorA[color][0]) - * ::danceFloorGradient)); - details[item].green = (::danceFloorOnColorA[color][1] + - ((::danceFloorOnColorB[color][1] - ::danceFloorOnColorA[color][1]) - * ::danceFloorGradient)); - details[item].blue = (::danceFloorOnColorA[color][2] + - ((::danceFloorOnColorB[color][2] - ::danceFloorOnColorA[color][2]) - * ::danceFloorGradient)); - } else if (::danceFloorColors[i][j] < 0) { - int color = -(::danceFloorColors[i][j] + 1); - details[item].red = (::danceFloorOnColorB[color][0] + - ((::danceFloorOnColorA[color][0] - ::danceFloorOnColorB[color][0]) - * ::danceFloorGradient)); - details[item].green = (::danceFloorOnColorB[color][1] + - ((::danceFloorOnColorA[color][1] - ::danceFloorOnColorB[color][1]) - * ::danceFloorGradient)); - details[item].blue = (::danceFloorOnColorB[color][2] + - ((::danceFloorOnColorA[color][2] - ::danceFloorOnColorB[color][2]) - * ::danceFloorGradient)); - } else { - int color = 0; - details[item].red = (::danceFloorOnColorB[color][0] + - ((::danceFloorOnColorA[color][0] - ::danceFloorOnColorB[color][0]) - * ::danceFloorGradient)); - details[item].green = (::danceFloorOnColorB[color][1] + - ((::danceFloorOnColorA[color][1] - ::danceFloorOnColorB[color][1]) - * ::danceFloorGradient)); - details[item].blue = (::danceFloorOnColorB[color][2] + - ((::danceFloorOnColorA[color][2] - ::danceFloorOnColorB[color][2]) - * ::danceFloorGradient)); - } - - if (item == DANCE_FLOOR_VOXELS_PER_PACKET - 1) { - ::voxelEditPacketSender->queueVoxelEditMessages(message, DANCE_FLOOR_VOXELS_PER_PACKET, (VoxelDetail*)&details); - } - } - } -} - -bool billboardInitialized = false; -const int BILLBOARD_HEIGHT = 9; -const int BILLBOARD_WIDTH = 45; -glm::vec3 billboardPosition((0.125f / TREE_SCALE),(0.125f / TREE_SCALE),0); -glm::vec3 billboardLights[BILLBOARD_HEIGHT][BILLBOARD_WIDTH]; -unsigned char billboardOffColor[3] = { 240, 240, 240 }; -unsigned char billboardOnColorA[3] = { 0, 0, 255 }; -unsigned char billboardOnColorB[3] = { 0, 255, 0 }; -float billboardGradient = 0.5f; -float billboardGradientIncrement = 0.01f; -const float BILLBOARD_MAX_GRADIENT = 1.0f; -const float BILLBOARD_MIN_GRADIENT = 0.0f; -const float BILLBOARD_LIGHT_SIZE = 0.125f / TREE_SCALE; // approximately 1/8 meter per light -const int VOXELS_PER_PACKET = 81; - -// top to bottom... -bool billboardMessage[BILLBOARD_HEIGHT][BILLBOARD_WIDTH] = { - { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }, - { 0,0,1,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,1,1,1,1,0,0,0,0,0,1,0,1,1,1,0,1,0,1,0,0,1,0,0,0,0,0,0 }, - { 0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1,0,1,0,1,0,0,0,1,1,1,0,0,0,0,0 }, - { 0,0,1,1,1,1,0,1,0,1,1,1,0,1,1,1,0,0,1,1,1,0,0,0,0,1,1,1,0,1,1,1,0,1,0,1,0,0,1,0,0,1,0,1,0 }, - { 0,0,1,0,0,1,0,1,0,1,0,1,0,1,0,1,0,0,1,0,0,0,0,1,0,1,0,1,0,1,0,0,0,1,0,1,0,0,1,0,0,1,0,1,0 }, - { 0,0,1,0,0,1,0,1,0,1,1,1,0,1,0,1,0,0,1,0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,0,1,0,0,1,0,0,1,1,1,0 }, - { 0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0 }, - { 0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0 }, - { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 } -}; - -static void sendBillboard() { - PacketType message = PacketTypeVoxelSetDestructive; // we're a bully! - float lightScale = BILLBOARD_LIGHT_SIZE; - static VoxelDetail details[VOXELS_PER_PACKET]; - - // first initialized the billboard of lights if needed... - if (!billboardInitialized) { - for (int i = 0; i < BILLBOARD_HEIGHT; i++) { - for (int j = 0; j < BILLBOARD_WIDTH; j++) { - - billboardLights[i][j] = billboardPosition + glm::vec3(j * lightScale, (float)((BILLBOARD_HEIGHT - i) * lightScale), 0); - } - } - billboardInitialized = true; - } - - ::billboardGradient += ::billboardGradientIncrement; - - if (::billboardGradient >= BILLBOARD_MAX_GRADIENT) { - ::billboardGradient = BILLBOARD_MAX_GRADIENT; - ::billboardGradientIncrement = -::billboardGradientIncrement; - } - if (::billboardGradient <= BILLBOARD_MIN_GRADIENT) { - ::billboardGradient = BILLBOARD_MIN_GRADIENT; - ::billboardGradientIncrement = -::billboardGradientIncrement; - } - - for (int i = 0; i < BILLBOARD_HEIGHT; i++) { - for (int j = 0; j < BILLBOARD_WIDTH; j++) { - - int nthVoxel = ((i * BILLBOARD_WIDTH) + j); - int item = nthVoxel % VOXELS_PER_PACKET; - - billboardLights[i][j] = billboardPosition + glm::vec3(j * lightScale, (float)((BILLBOARD_HEIGHT - i) * lightScale), 0); - - details[item].s = lightScale; - details[item].x = billboardLights[i][j].x; - details[item].y = billboardLights[i][j].y; - details[item].z = billboardLights[i][j].z; - - if (billboardMessage[i][j]) { - details[item].red = (billboardOnColorA[0] + ((billboardOnColorB[0] - billboardOnColorA[0]) * ::billboardGradient)); - details[item].green = (billboardOnColorA[1] + ((billboardOnColorB[1] - billboardOnColorA[1]) * ::billboardGradient)); - details[item].blue = (billboardOnColorA[2] + ((billboardOnColorB[2] - billboardOnColorA[2]) * ::billboardGradient)); - } else { - details[item].red = billboardOffColor[0]; - details[item].green = billboardOffColor[1]; - details[item].blue = billboardOffColor[2]; - } - - if (item == VOXELS_PER_PACKET - 1) { - ::voxelEditPacketSender->queueVoxelEditMessages(message, VOXELS_PER_PACKET, (VoxelDetail*)&details); - } - } - } -} - -bool roadInitialized = false; -const int BRICKS_ACROSS_ROAD = 32; -const float ROAD_BRICK_SIZE = 0.125f/TREE_SCALE; //(ROAD_WIDTH_METERS / TREE_SCALE) / BRICKS_ACROSS_ROAD; // in voxel units -const int ROAD_LENGTH = 1.0f / ROAD_BRICK_SIZE; // in bricks -const int ROAD_WIDTH = BRICKS_ACROSS_ROAD; // in bricks -glm::vec3 roadPosition(0.5f - (ROAD_BRICK_SIZE * BRICKS_ACROSS_ROAD), 0.0f, 0.0f); -const int BRICKS_PER_PACKET = 32; // guessing - -void doBuildStreet() { - if (roadInitialized) { - return; - } - - PacketType message = PacketTypeVoxelSetDestructive; // we're a bully! - static VoxelDetail details[BRICKS_PER_PACKET]; - - for (int z = 0; z < ROAD_LENGTH; z++) { - for (int x = 0; x < ROAD_WIDTH; x++) { - - int nthVoxel = ((z * ROAD_WIDTH) + x); - int item = nthVoxel % BRICKS_PER_PACKET; - - glm::vec3 brick = roadPosition + glm::vec3(x * ROAD_BRICK_SIZE, 0, z * ROAD_BRICK_SIZE); - - details[item].s = ROAD_BRICK_SIZE; - details[item].x = brick.x; - details[item].y = brick.y; - details[item].z = brick.z; - - unsigned char randomTone = randIntInRange(118,138); - details[item].red = randomTone; - details[item].green = randomTone; - details[item].blue = randomTone; - - if (item == BRICKS_PER_PACKET - 1) { - ::voxelEditPacketSender->queueVoxelEditMessages(message, BRICKS_PER_PACKET, (VoxelDetail*)&details); - } - } - } - roadInitialized = true; -} - - -double start = 0; - - -void* animateVoxels(void* args) { - - quint64 lastAnimateTime = 0; - quint64 lastProcessTime = 0; - int processesPerAnimate = 0; - - bool firstTime = true; - - qDebug() << "Setting PPS to " << ::packetsPerSecond; - ::voxelEditPacketSender->setPacketsPerSecond(::packetsPerSecond); - - qDebug() << "PPS set to " << ::voxelEditPacketSender->getPacketsPerSecond(); - - while (true) { - - // If we're asked to wait for voxel servers, and there isn't one available yet, then - // let the voxelEditPacketSender process and move on. - if (::waitForVoxelServer && !::voxelEditPacketSender->voxelServersExist()) { - if (::nonThreadedPacketSender) { - ::voxelEditPacketSender->process(); - } - } else { - if (firstTime) { - lastAnimateTime = usecTimestampNow(); - firstTime = false; - } - lastProcessTime = usecTimestampNow(); - - // The while loop will be running at PROCESSING_FPS, but we only want to call these animation functions at - // ANIMATE_FPS. So we check out last animate time and only call these if we've elapsed that time. - quint64 now = usecTimestampNow(); - quint64 animationElapsed = now - lastAnimateTime; - int withinAnimationTarget = ANIMATE_VOXELS_INTERVAL_USECS - animationElapsed; - const int CLOSE_ENOUGH_TO_ANIMATE = 2000; // approximately 2 ms - - int animateLoopsPerAnimate = 0; - while (withinAnimationTarget < CLOSE_ENOUGH_TO_ANIMATE) { - processesPerAnimate = 0; - animateLoopsPerAnimate++; - - lastAnimateTime = now; - // some animations - //sendVoxelBlinkMessage(); - - if (::includeBillboard) { - sendBillboard(); - } - if (::includeBorderTracer) { - sendBlinkingStringOfLights(); - } - if (::includeMovingBug) { - renderMovingBug(); - } - if (::includeBlinkingVoxel) { - sendVoxelBlinkMessage(); - } - if (::includeDanceFloor) { - sendDanceFloor(); - } - - if (::buildStreet) { - doBuildStreet(); - } - - if (animationElapsed > ANIMATE_VOXELS_INTERVAL_USECS) { - animationElapsed -= ANIMATE_VOXELS_INTERVAL_USECS; // credit ourselves one animation frame - } else { - animationElapsed = 0; - } - withinAnimationTarget = ANIMATE_VOXELS_INTERVAL_USECS - animationElapsed; - - ::voxelEditPacketSender->releaseQueuedMessages(); - } - - if (::nonThreadedPacketSender) { - ::voxelEditPacketSender->process(); - } - processesPerAnimate++; - } - // dynamically sleep until we need to fire off the next set of voxels - quint64 usecToSleep = ::PROCESSING_INTERVAL_USECS - (usecTimestampNow() - lastProcessTime); - if (usecToSleep > ::PROCESSING_INTERVAL_USECS) { - usecToSleep = ::PROCESSING_INTERVAL_USECS; - } - - if (usecToSleep > 0) { - usleep(usecToSleep); - } - } - - pthread_exit(0); -} - -AnimationServer::AnimationServer(int &argc, char **argv) : - QCoreApplication(argc, argv) -{ - ::start = usecTimestampNow(); - - NodeList* nodeList = NodeList::createInstance(NodeType::AnimationServer, ANIMATION_LISTEN_PORT); - setvbuf(stdout, NULL, _IOLBF, 0); - - // Handle Local Domain testing with the --local command line - const char* NON_THREADED_PACKETSENDER = "--NonThreadedPacketSender"; - ::nonThreadedPacketSender = cmdOptionExists(argc, (const char**) argv, NON_THREADED_PACKETSENDER); - qDebug("nonThreadedPacketSender=%s", debug::valueOf(::nonThreadedPacketSender)); - - // Handle Local Domain testing with the --local command line - const char* NO_BILLBOARD = "--NoBillboard"; - ::includeBillboard = !cmdOptionExists(argc, (const char**) argv, NO_BILLBOARD); - qDebug("includeBillboard=%s", debug::valueOf(::includeBillboard)); - - const char* NO_BORDER_TRACER = "--NoBorderTracer"; - ::includeBorderTracer = !cmdOptionExists(argc, (const char**) argv, NO_BORDER_TRACER); - qDebug("includeBorderTracer=%s", debug::valueOf(::includeBorderTracer)); - - const char* NO_MOVING_BUG = "--NoMovingBug"; - ::includeMovingBug = !cmdOptionExists(argc, (const char**) argv, NO_MOVING_BUG); - qDebug("includeMovingBug=%s", debug::valueOf(::includeMovingBug)); - - const char* INCLUDE_BLINKING_VOXEL = "--includeBlinkingVoxel"; - ::includeBlinkingVoxel = cmdOptionExists(argc, (const char**) argv, INCLUDE_BLINKING_VOXEL); - qDebug("includeBlinkingVoxel=%s", debug::valueOf(::includeBlinkingVoxel)); - - const char* NO_DANCE_FLOOR = "--NoDanceFloor"; - ::includeDanceFloor = !cmdOptionExists(argc, (const char**) argv, NO_DANCE_FLOOR); - qDebug("includeDanceFloor=%s", debug::valueOf(::includeDanceFloor)); - - const char* BUILD_STREET = "--BuildStreet"; - ::buildStreet = cmdOptionExists(argc, (const char**) argv, BUILD_STREET); - qDebug("buildStreet=%s", debug::valueOf(::buildStreet)); - - // Handle Local Domain testing with the --local command line - const char* showPPS = "--showPPS"; - ::shouldShowPacketsPerSecond = cmdOptionExists(argc, (const char**) argv, showPPS); - - // Handle Local Domain testing with the --local command line - const char* local = "--local"; - ::wantLocalDomain = cmdOptionExists(argc, (const char**) argv,local); - if (::wantLocalDomain) { - qDebug("Local Domain MODE!"); - nodeList->getDomainHandler().setIPToLocalhost(); - } - - const char* domainHostname = getCmdOption(argc, (const char**) argv, "--domain"); - if (domainHostname) { - NodeList::getInstance()->getDomainHandler().setHostname(domainHostname); - } - - const char* packetsPerSecondCommand = getCmdOption(argc, (const char**) argv, "--pps"); - if (packetsPerSecondCommand) { - ::packetsPerSecond = atoi(packetsPerSecondCommand); - } - qDebug("packetsPerSecond=%d",packetsPerSecond); - - const char* animateFPSCommand = getCmdOption(argc, (const char**) argv, "--AnimateFPS"); - const char* animateIntervalCommand = getCmdOption(argc, (const char**) argv, "--AnimateInterval"); - if (animateFPSCommand || animateIntervalCommand) { - if (animateIntervalCommand) { - ::ANIMATE_FPS_IN_MILLISECONDS = atoi(animateIntervalCommand); - ::ANIMATE_VOXELS_INTERVAL_USECS = (ANIMATE_FPS_IN_MILLISECONDS * 1000.0); // converts from milliseconds to usecs - ::ANIMATE_FPS = PacketSender::USECS_PER_SECOND / ::ANIMATE_VOXELS_INTERVAL_USECS; - } else { - ::ANIMATE_FPS = atoi(animateFPSCommand); - ::ANIMATE_FPS_IN_MILLISECONDS = 1000.0/ANIMATE_FPS; // determines FPS from our desired FPS - ::ANIMATE_VOXELS_INTERVAL_USECS = (ANIMATE_FPS_IN_MILLISECONDS * 1000.0); // converts from milliseconds to usecs - } - } - qDebug("ANIMATE_FPS=%d",ANIMATE_FPS); - qDebug("ANIMATE_VOXELS_INTERVAL_USECS=%llu",ANIMATE_VOXELS_INTERVAL_USECS); - - const char* processingFPSCommand = getCmdOption(argc, (const char**) argv, "--ProcessingFPS"); - const char* processingIntervalCommand = getCmdOption(argc, (const char**) argv, "--ProcessingInterval"); - if (processingFPSCommand || processingIntervalCommand) { - if (processingIntervalCommand) { - ::PROCESSING_FPS_IN_MILLISECONDS = atoi(processingIntervalCommand); - ::PROCESSING_INTERVAL_USECS = ::PROCESSING_FPS_IN_MILLISECONDS * 1000.0; - ::PROCESSING_FPS = PacketSender::USECS_PER_SECOND / ::PROCESSING_INTERVAL_USECS; - } else { - ::PROCESSING_FPS = atoi(processingFPSCommand); - ::PROCESSING_FPS_IN_MILLISECONDS = 1000.0/PROCESSING_FPS; // determines FPS from our desired FPS - ::PROCESSING_INTERVAL_USECS = (PROCESSING_FPS_IN_MILLISECONDS * 1000.0) - FUDGE_USECS; // converts from milliseconds to usecs - } - } - qDebug("PROCESSING_FPS=%d",PROCESSING_FPS); - qDebug("PROCESSING_INTERVAL_USECS=%llu",PROCESSING_INTERVAL_USECS); - - nodeList->linkedDataCreateCallback = NULL; // do we need a callback? - - // Create our JurisdictionListener so we'll know where to send edit packets - ::jurisdictionListener = new JurisdictionListener(); - if (::jurisdictionListener) { - ::jurisdictionListener->initialize(true); - } - - // Create out VoxelEditPacketSender - ::voxelEditPacketSender = new VoxelEditPacketSender; - ::voxelEditPacketSender->initialize(!::nonThreadedPacketSender); - - if (::jurisdictionListener) { - ::voxelEditPacketSender->setVoxelServerJurisdictions(::jurisdictionListener->getJurisdictions()); - } - if (::nonThreadedPacketSender) { - ::voxelEditPacketSender->setProcessCallIntervalHint(PROCESSING_INTERVAL_USECS); - } - - srand((unsigned)time(0)); - - - pthread_create(&::animateVoxelThread, NULL, animateVoxels, NULL); - - NodeList::getInstance()->addNodeTypeToInterestSet(NodeType::VoxelServer); - - QTimer* domainServerTimer = new QTimer(this); - connect(domainServerTimer, SIGNAL(timeout()), nodeList, SLOT(sendDomainServerCheckIn())); - domainServerTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - - QTimer* silentNodeTimer = new QTimer(this); - connect(silentNodeTimer, SIGNAL(timeout()), nodeList, SLOT(removeSilentNodes())); - silentNodeTimer->start(NODE_SILENCE_THRESHOLD_MSECS); - - connect(&nodeList->getNodeSocket(), SIGNAL(readyRead()), SLOT(readPendingDatagrams())); -} - -void AnimationServer::readPendingDatagrams() { - NodeList* nodeList = NodeList::getInstance(); - - static QByteArray receivedPacket; - static HifiSockAddr nodeSockAddr; - - // Nodes sending messages to us... - while (nodeList->getNodeSocket().hasPendingDatagrams()) { - receivedPacket.resize(nodeList->getNodeSocket().pendingDatagramSize()); - nodeList->getNodeSocket().readDatagram(receivedPacket.data(), receivedPacket.size(), - nodeSockAddr.getAddressPointer(), nodeSockAddr.getPortPointer()); - if (nodeList->packetVersionAndHashMatch(receivedPacket)) { - if (packetTypeForPacket(receivedPacket) == PacketTypeJurisdiction) { - int headerBytes = numBytesForPacketHeader(receivedPacket); - // PacketType_JURISDICTION, first byte is the node type... - if (receivedPacket.data()[headerBytes] == NodeType::VoxelServer && ::jurisdictionListener) { - - SharedNodePointer matchedNode = NodeList::getInstance()->sendingNodeForPacket(receivedPacket); - if (matchedNode) { - ::jurisdictionListener->queueReceivedPacket(matchedNode, receivedPacket); - } - } - } - NodeList::getInstance()->processNodeData(nodeSockAddr, receivedPacket); - } - } -} - -AnimationServer::~AnimationServer() { - pthread_join(animateVoxelThread, NULL); - - if (::jurisdictionListener) { - ::jurisdictionListener->terminate(); - delete ::jurisdictionListener; - } - - if (::voxelEditPacketSender) { - ::voxelEditPacketSender->terminate(); - delete ::voxelEditPacketSender; - } -} diff --git a/animation-server/src/AnimationServer.h b/animation-server/src/AnimationServer.h deleted file mode 100644 index 58f05c32c5..0000000000 --- a/animation-server/src/AnimationServer.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// AnimationServer.h -// animation-server/src -// -// Created by Stephen Birarda on 12/5/2013. -// Copyright 2013 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_AnimationServer_h -#define hifi_AnimationServer_h - -#include - -class AnimationServer : public QCoreApplication { - Q_OBJECT -public: - AnimationServer(int &argc, char **argv); - ~AnimationServer(); -private slots: - void readPendingDatagrams(); -}; - - -#endif // hifi_AnimationServer_h diff --git a/animation-server/src/main.cpp b/animation-server/src/main.cpp deleted file mode 100644 index 8acf3b8db2..0000000000 --- a/animation-server/src/main.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// -// main.cpp -// animation-server/src -// -// Created by Brad Hefta-Gaub on 05/16/2013. -// Copyright 2012 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 "AnimationServer.h" - -int main(int argc, char * argv[]) { - AnimationServer animationServer(argc, argv); - return animationServer.exec(); -} diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 5ca021b175..972c6ac220 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -1,47 +1,17 @@ set(TARGET_NAME assignment-client) -set(ROOT_DIR ..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") +setup_hifi_project(Core Gui Network Script Widgets) -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/modules/") - -find_package(Qt5 COMPONENTS Network Script Widgets) - -include("${MACRO_DIR}/SetupHifiProject.cmake") -setup_hifi_project(${TARGET_NAME} TRUE) - -# include glm -include("${MACRO_DIR}/IncludeGLM.cmake") -include_glm(${TARGET_NAME} "${ROOT_DIR}") +include_glm() # link in the shared libraries -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(audio ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(avatars ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") -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(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}") +link_hifi_libraries( + audio avatars octree voxels fbx particles models metavoxels + networking animation shared script-engine embedded-webserver +) if (UNIX) - target_link_libraries(${TARGET_NAME} ${CMAKE_DL_LIBS}) + list(APPEND ${TARGET_NAME}_LIBRARIES_TO_LINK ${CMAKE_DL_LIBS}) endif (UNIX) -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) - -target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +link_shared_dependencies() \ No newline at end of file diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index dbb1620252..09496f0179 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -115,7 +115,7 @@ void Agent::readPendingDatagrams() { sourceNode->setLastHeardMicrostamp(usecTimestampNow()); QByteArray mutablePacket = receivedPacket; - ssize_t messageLength = mutablePacket.size(); + int messageLength = mutablePacket.size(); if (datagramPacketType == PacketTypeOctreeStats) { diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index c1d75af8ba..5f4c3827f2 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ #include "AudioRingBuffer.h" #include "AudioMixerClientData.h" +#include "AudioMixerDatagramProcessor.h" #include "AvatarAudioStream.h" #include "InjectedAudioStream.h" @@ -71,6 +73,8 @@ bool AudioMixer::_useDynamicJitterBuffers = false; int AudioMixer::_staticDesiredJitterBufferFrames = 0; int AudioMixer::_maxFramesOverDesired = 0; +bool AudioMixer::_printStreamStats = false; + AudioMixer::AudioMixer(const QByteArray& packet) : ThreadedAssignment(packet), _trailingSleepRatio(1.0f), @@ -295,38 +299,34 @@ void AudioMixer::prepareMixForListeningNode(Node* node) { } } -void AudioMixer::readPendingDatagrams() { - QByteArray receivedPacket; - HifiSockAddr senderSockAddr; +void AudioMixer::readPendingDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr) { NodeList* nodeList = NodeList::getInstance(); - while (readAvailableDatagram(receivedPacket, senderSockAddr)) { - if (nodeList->packetVersionAndHashMatch(receivedPacket)) { - // pull any new audio data from nodes off of the network stack - PacketType mixerPacketType = packetTypeForPacket(receivedPacket); - if (mixerPacketType == PacketTypeMicrophoneAudioNoEcho - || mixerPacketType == PacketTypeMicrophoneAudioWithEcho - || mixerPacketType == PacketTypeInjectAudio - || mixerPacketType == PacketTypeSilentAudioFrame - || mixerPacketType == PacketTypeAudioStreamStats) { - - nodeList->findNodeAndUpdateWithDataFromPacket(receivedPacket); - } else if (mixerPacketType == PacketTypeMuteEnvironment) { - QByteArray packet = receivedPacket; - populatePacketHeader(packet, PacketTypeMuteEnvironment); - - foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { - if (node->getType() == NodeType::Agent && node->getActiveSocket() && node->getLinkedData() && node != nodeList->sendingNodeForPacket(receivedPacket)) { - nodeList->writeDatagram(packet, packet.size(), node); - } + if (nodeList->packetVersionAndHashMatch(receivedPacket)) { + // pull any new audio data from nodes off of the network stack + PacketType mixerPacketType = packetTypeForPacket(receivedPacket); + if (mixerPacketType == PacketTypeMicrophoneAudioNoEcho + || mixerPacketType == PacketTypeMicrophoneAudioWithEcho + || mixerPacketType == PacketTypeInjectAudio + || mixerPacketType == PacketTypeSilentAudioFrame + || mixerPacketType == PacketTypeAudioStreamStats) { + + nodeList->findNodeAndUpdateWithDataFromPacket(receivedPacket); + } else if (mixerPacketType == PacketTypeMuteEnvironment) { + QByteArray packet = receivedPacket; + populatePacketHeader(packet, PacketTypeMuteEnvironment); + + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { + if (node->getType() == NodeType::Agent && node->getActiveSocket() && node->getLinkedData() && node != nodeList->sendingNodeForPacket(receivedPacket)) { + nodeList->writeDatagram(packet, packet.size(), node); } - - } else { - // let processNodeData handle it. - nodeList->processNodeData(senderSockAddr, receivedPacket); } + + } else { + // let processNodeData handle it. + nodeList->processNodeData(senderSockAddr, receivedPacket); } - } + } } void AudioMixer::sendStatsPacket() { @@ -390,46 +390,58 @@ void AudioMixer::run() { ThreadedAssignment::commonInit(AUDIO_MIXER_LOGGING_TARGET_NAME, NodeType::AudioMixer); NodeList* nodeList = NodeList::getInstance(); - + + // we do not want this event loop to be the handler for UDP datagrams, so disconnect + disconnect(&nodeList->getNodeSocket(), 0, this, 0); + + // setup a QThread with us as parent that will house the AudioMixerDatagramProcessor + _datagramProcessingThread = new QThread(this); + + // create an AudioMixerDatagramProcessor and move it to that thread + AudioMixerDatagramProcessor* datagramProcessor = new AudioMixerDatagramProcessor(nodeList->getNodeSocket(), thread()); + datagramProcessor->moveToThread(_datagramProcessingThread); + + // remove the NodeList as the parent of the node socket + nodeList->getNodeSocket().setParent(NULL); + nodeList->getNodeSocket().moveToThread(_datagramProcessingThread); + + // let the datagram processor handle readyRead from node socket + connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, + datagramProcessor, &AudioMixerDatagramProcessor::readPendingDatagrams); + + // connect to the datagram processing thread signal that tells us we have to handle a packet + connect(datagramProcessor, &AudioMixerDatagramProcessor::packetRequiresProcessing, this, &AudioMixer::readPendingDatagram); + + // delete the datagram processor and the associated thread when the QThread quits + connect(_datagramProcessingThread, &QThread::finished, datagramProcessor, &QObject::deleteLater); + connect(datagramProcessor, &QObject::destroyed, _datagramProcessingThread, &QThread::deleteLater); + + // start the datagram processing thread + _datagramProcessingThread->start(); + nodeList->addNodeTypeToInterestSet(NodeType::Agent); nodeList->linkedDataCreateCallback = attachNewNodeDataToNode; - // setup a NetworkAccessManager to ask the domain-server for our settings - NetworkAccessManager& networkManager = NetworkAccessManager::getInstance(); + // wait until we have the domain-server settings, otherwise we bail + DomainHandler& domainHandler = nodeList->getDomainHandler(); - QUrl settingsJSONURL; - settingsJSONURL.setScheme("http"); - settingsJSONURL.setHost(nodeList->getDomainHandler().getHostname()); - settingsJSONURL.setPort(DOMAIN_SERVER_HTTP_PORT); - settingsJSONURL.setPath("/settings.json"); - settingsJSONURL.setQuery(QString("type=%1").arg(_type)); + qDebug() << "Waiting for domain settings from domain-server."; - QNetworkReply *reply = NULL; + // block until we get the settingsRequestComplete signal + QEventLoop loop; + connect(&domainHandler, &DomainHandler::settingsReceived, &loop, &QEventLoop::quit); + connect(&domainHandler, &DomainHandler::settingsReceiveFail, &loop, &QEventLoop::quit); + domainHandler.requestDomainSettings(); + loop.exec(); - int failedAttempts = 0; - const int MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS = 5; - - qDebug() << "Requesting settings for assignment from domain-server at" << settingsJSONURL.toString(); - - while (!reply || reply->error() != QNetworkReply::NoError) { - reply = networkManager.get(QNetworkRequest(settingsJSONURL)); - - QEventLoop loop; - QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - - loop.exec(); - - ++failedAttempts; - - if (failedAttempts == MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS) { - qDebug() << "Failed to get settings from domain-server. Bailing on assignment."; - setFinished(true); - return; - } + if (domainHandler.getSettingsObject().isEmpty()) { + qDebug() << "Failed to retreive settings object from domain-server. Bailing on assignment."; + setFinished(true); + return; } - QJsonObject settingsObject = QJsonDocument::fromJson(reply->readAll()).object(); + const QJsonObject& settingsObject = domainHandler.getSettingsObject(); // check the settings object to see if we have anything we can parse out const QString AUDIO_GROUP_KEY = "audio"; @@ -464,7 +476,11 @@ void AudioMixer::run() { } qDebug() << "Max frames over desired:" << _maxFramesOverDesired; - + const QString PRINT_STREAM_STATS_JSON_KEY = "H-print-stream-stats"; + _printStreamStats = audioGroupObject[PRINT_STREAM_STATS_JSON_KEY].toBool(); + if (_printStreamStats) { + qDebug() << "Stream stats will be printed to stdout"; + } const QString UNATTENUATED_ZONE_KEY = "D-unattenuated-zone"; @@ -563,6 +579,7 @@ void AudioMixer::run() { sendAudioStreamStats = true; } + bool streamStatsPrinted = false; foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getLinkedData()) { AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData(); @@ -597,12 +614,21 @@ void AudioMixer::run() { // send an audio stream stats packet if it's time if (sendAudioStreamStats) { nodeData->sendAudioStreamStatsPackets(node); + + if (_printStreamStats) { + printf("\nStats for agent %s:\n", node->getUUID().toString().toLatin1().data()); + nodeData->printUpstreamDownstreamStats(); + streamStatsPrinted = true; + } } ++_sumListeners; } } } + if (streamStatsPrinted) { + printf("\n----------------------------------------------------------------\n"); + } ++_numStatFrames; diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 83769a4209..2a4b93149c 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -33,7 +33,8 @@ public slots: /// threaded run of assignment void run(); - void readPendingDatagrams(); + void readPendingDatagrams() { }; // this will not be called since our datagram processing thread will handle + void readPendingDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr); void sendStatsPacket(); @@ -66,6 +67,8 @@ private: static int _staticDesiredJitterBufferFrames; static int _maxFramesOverDesired; + static bool _printStreamStats; + quint64 _lastSendAudioStreamStatsTime; }; diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 54c723243c..9a8a85c3d1 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -22,7 +22,8 @@ AudioMixerClientData::AudioMixerClientData() : _audioStreams(), - _outgoingMixedAudioSequenceNumber(0) + _outgoingMixedAudioSequenceNumber(0), + _downstreamAudioStreamStats() { } @@ -104,7 +105,7 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend(AABox* checkSourceZone, A QHash::ConstIterator i; for (i = _audioStreams.constBegin(); i != _audioStreams.constEnd(); i++) { PositionalAudioStream* stream = i.value(); - if (stream->popFrames(1)) { + if (stream->popFrames(1, true) > 0) { // this is a ring buffer that is ready to go // calculate the trailing avg loudness for the next frame @@ -263,3 +264,45 @@ QString AudioMixerClientData::getAudioStreamStatsString() const { } return result; } + +void AudioMixerClientData::printUpstreamDownstreamStats() const { + // print the upstream (mic stream) stats if the mic stream exists + if (_audioStreams.contains(QUuid())) { + printf("Upstream:\n"); + printAudioStreamStats(_audioStreams.value(QUuid())->getAudioStreamStats()); + } + // print the downstream stats if they contain valid info + if (_downstreamAudioStreamStats._packetStreamStats._received > 0) { + printf("Downstream:\n"); + printAudioStreamStats(_downstreamAudioStreamStats); + } +} + +void AudioMixerClientData::printAudioStreamStats(const AudioStreamStats& streamStats) const { + printf(" Packet loss | overall: %5.2f%% (%d lost), last_30s: %5.2f%% (%d lost)\n", + streamStats._packetStreamStats.getLostRate() * 100.0f, + streamStats._packetStreamStats._lost, + streamStats._packetStreamWindowStats.getLostRate() * 100.0f, + streamStats._packetStreamWindowStats._lost); + + printf(" Ringbuffer frames | desired: %u, avg_available(10s): %u, available: %u\n", + streamStats._desiredJitterBufferFrames, + streamStats._framesAvailableAverage, + streamStats._framesAvailable); + + printf(" Ringbuffer stats | starves: %u, prev_starve_lasted: %u, frames_dropped: %u, overflows: %u\n", + streamStats._starveCount, + streamStats._consecutiveNotMixedCount, + streamStats._framesDropped, + streamStats._overflowCount); + + printf(" Inter-packet timegaps (overall) | min: %9s, max: %9s, avg: %9s\n", + formatUsecTime(streamStats._timeGapMin).toLatin1().data(), + formatUsecTime(streamStats._timeGapMax).toLatin1().data(), + formatUsecTime(streamStats._timeGapAverage).toLatin1().data()); + + printf(" Inter-packet timegaps (last 30s) | min: %9s, max: %9s, avg: %9s\n", + formatUsecTime(streamStats._timeGapWindowMin).toLatin1().data(), + formatUsecTime(streamStats._timeGapWindowMax).toLatin1().data(), + formatUsecTime(streamStats._timeGapWindowAverage).toLatin1().data()); +} diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 80f3f9e3ca..0ce4ecc36a 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -38,6 +38,11 @@ public: void incrementOutgoingMixedAudioSequenceNumber() { _outgoingMixedAudioSequenceNumber++; } quint16 getOutgoingSequenceNumber() const { return _outgoingMixedAudioSequenceNumber; } + void printUpstreamDownstreamStats() const; + +private: + void printAudioStreamStats(const AudioStreamStats& streamStats) const; + private: QHash _audioStreams; // mic stream stored under key of null UUID diff --git a/assignment-client/src/audio/AudioMixerDatagramProcessor.cpp b/assignment-client/src/audio/AudioMixerDatagramProcessor.cpp new file mode 100644 index 0000000000..73a4e01844 --- /dev/null +++ b/assignment-client/src/audio/AudioMixerDatagramProcessor.cpp @@ -0,0 +1,47 @@ +// +// AudioMixerDatagramProcessor.cpp +// assignment-client/src +// +// Created by Stephen Birarda on 2014-08-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 + +#include "AudioMixerDatagramProcessor.h" + +AudioMixerDatagramProcessor::AudioMixerDatagramProcessor(QUdpSocket& nodeSocket, QThread* previousNodeSocketThread) : + _nodeSocket(nodeSocket), + _previousNodeSocketThread(previousNodeSocketThread) +{ + +} + +AudioMixerDatagramProcessor::~AudioMixerDatagramProcessor() { + // return the node socket to its previous thread + _nodeSocket.moveToThread(_previousNodeSocketThread); +} + +void AudioMixerDatagramProcessor::readPendingDatagrams() { + + HifiSockAddr senderSockAddr; + static QByteArray incomingPacket; + + // read everything that is available + while (_nodeSocket.hasPendingDatagrams()) { + incomingPacket.resize(_nodeSocket.pendingDatagramSize()); + + // just get this packet off the stack + _nodeSocket.readDatagram(incomingPacket.data(), incomingPacket.size(), + senderSockAddr.getAddressPointer(), senderSockAddr.getPortPointer()); + + // emit the signal to tell AudioMixer it needs to process a packet + emit packetRequiresProcessing(incomingPacket, senderSockAddr); + } +} diff --git a/assignment-client/src/audio/AudioMixerDatagramProcessor.h b/assignment-client/src/audio/AudioMixerDatagramProcessor.h new file mode 100644 index 0000000000..94233a1373 --- /dev/null +++ b/assignment-client/src/audio/AudioMixerDatagramProcessor.h @@ -0,0 +1,32 @@ +// +// AudioMixerDatagramProcessor.h +// assignment-client/src +// +// Created by Stephen Birarda on 2014-08-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_AudioMixerDatagramProcessor_h +#define hifi_AudioMixerDatagramProcessor_h + +#include +#include + +class AudioMixerDatagramProcessor : public QObject { + Q_OBJECT +public: + AudioMixerDatagramProcessor(QUdpSocket& nodeSocket, QThread* previousNodeSocketThread); + ~AudioMixerDatagramProcessor(); +public slots: + void readPendingDatagrams(); +signals: + void packetRequiresProcessing(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr); +private: + QUdpSocket& _nodeSocket; + QThread* _previousNodeSocketThread; +}; + +#endif // hifi_AudioMixerDatagramProcessor_h \ No newline at end of file diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 150e364ed7..0f721ac98c 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -11,6 +11,8 @@ #include +#include + #include "ScriptableAvatar.h" ScriptableAvatar::ScriptableAvatar(ScriptEngine* scriptEngine) : _scriptEngine(scriptEngine), _animation(NULL) { diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 551bed795d..02fb26c09f 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -9,11 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include #include #include + +#include #include #include #include @@ -244,6 +247,10 @@ OctreeServer::OctreeServer(const QByteArray& packet) : _averageLoopTime.updateAverage(0); qDebug() << "Octree server starting... [" << this << "]"; + + // make sure the AccountManager has an Auth URL for payment redemptions + + AccountManager::getInstance().setAuthURL(DEFAULT_NODE_AUTH_URL); } OctreeServer::~OctreeServer() { @@ -857,6 +864,8 @@ void OctreeServer::readPendingDatagrams() { } } else if (packetType == PacketTypeJurisdictionRequest) { _jurisdictionSender->queueReceivedPacket(matchingNode, receivedPacket); + } else if (packetType == PacketTypeSignedTransactionPayment) { + handleSignedTransactionPayment(packetType, receivedPacket); } else if (_octreeInboundPacketProcessor && getOctree()->handlesEditPacketType(packetType)) { _octreeInboundPacketProcessor->queueReceivedPacket(matchingNode, receivedPacket); } else { @@ -1204,6 +1213,51 @@ QString OctreeServer::getStatusLink() { return result; } +void OctreeServer::handleSignedTransactionPayment(PacketType packetType, const QByteArray& datagram) { + // for now we're not verifying that this is actual payment for any octree edits + // just use the AccountManager to send it up to the data server and have it redeemed + AccountManager& accountManager = AccountManager::getInstance(); + + const int NUM_BYTES_SIGNED_TRANSACTION_BINARY_MESSAGE = 72; + const int NUM_BYTES_SIGNED_TRANSACTION_BINARY_SIGNATURE = 256; + + int numBytesPacketHeader = numBytesForPacketHeaderGivenPacketType(packetType); + + // pull out the transaction message in binary + QByteArray messageHex = datagram.mid(numBytesPacketHeader, NUM_BYTES_SIGNED_TRANSACTION_BINARY_MESSAGE).toHex(); + // pull out the binary signed message digest + QByteArray signatureHex = datagram.mid(numBytesPacketHeader + NUM_BYTES_SIGNED_TRANSACTION_BINARY_MESSAGE, + NUM_BYTES_SIGNED_TRANSACTION_BINARY_SIGNATURE).toHex(); + + // setup the QJSONObject we are posting + QJsonObject postObject; + + const QString TRANSACTION_OBJECT_MESSAGE_KEY = "message"; + const QString TRANSACTION_OBJECT_SIGNATURE_KEY = "signature"; + const QString POST_OBJECT_TRANSACTION_KEY = "transaction"; + + QJsonObject transactionObject; + transactionObject.insert(TRANSACTION_OBJECT_MESSAGE_KEY, QString(messageHex)); + transactionObject.insert(TRANSACTION_OBJECT_SIGNATURE_KEY, QString(signatureHex)); + + postObject.insert(POST_OBJECT_TRANSACTION_KEY, transactionObject); + + // setup our callback params + JSONCallbackParameters callbackParameters; + callbackParameters.jsonCallbackReceiver = this; + callbackParameters.jsonCallbackMethod = "handleSignedTransactionPaymentResponse"; + + accountManager.unauthenticatedRequest("/api/v1/transactions/redeem", QNetworkAccessManager::PostOperation, + callbackParameters, QJsonDocument(postObject).toJson()); + +} + +void OctreeServer::handleSignedTransactionPaymentResponse(const QJsonObject& jsonObject) { + // pull the ID to debug the transaction + QString transactionIDString = jsonObject["data"].toObject()["transaction"].toObject()["id"].toString(); + qDebug() << "Redeemed transaction with ID" << transactionIDString << "successfully."; +} + void OctreeServer::sendStatsPacket() { // TODO: we have too many stats to fit in a single MTU... so for now, we break it into multiple JSON objects and // send them separately. What we really should do is change the NodeList::sendStatsToDomainServer() to handle the diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 76b39c5771..c904b3d86c 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -127,6 +127,8 @@ public slots: void nodeAdded(SharedNodePointer node); void nodeKilled(SharedNodePointer node); void sendStatsPacket(); + + void handleSignedTransactionPaymentResponse(const QJsonObject& jsonObject); protected: void parsePayload(); @@ -137,6 +139,8 @@ protected: QString getConfiguration(); QString getStatusLink(); + void handleSignedTransactionPayment(PacketType packetType, const QByteArray& datagram); + int _argc; const char** _argv; char** _parsedArgV; diff --git a/cmake/macros/AutoMTC.cmake b/cmake/macros/AutoMTC.cmake index 6f0216de7f..4d433e7b69 100644 --- a/cmake/macros/AutoMTC.cmake +++ b/cmake/macros/AutoMTC.cmake @@ -8,8 +8,8 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -macro(AUTO_MTC TARGET ROOT_DIR) - set(AUTOMTC_SRC ${TARGET}_automtc.cpp) +macro(AUTO_MTC) + set(AUTOMTC_SRC ${TARGET_NAME}_automtc.cpp) file(GLOB INCLUDE_FILES src/*.h) diff --git a/cmake/macros/IncludeGLM.cmake b/cmake/macros/IncludeGLM.cmake index a31324993e..e2fa981a3b 100644 --- a/cmake/macros/IncludeGLM.cmake +++ b/cmake/macros/IncludeGLM.cmake @@ -7,7 +7,7 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -macro(INCLUDE_GLM TARGET ROOT_DIR) +macro(INCLUDE_GLM) find_package(GLM REQUIRED) include_directories("${GLM_INCLUDE_DIRS}") @@ -16,4 +16,4 @@ macro(INCLUDE_GLM TARGET ROOT_DIR) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${GLM_INCLUDE_DIRS}") endif () -endmacro(INCLUDE_GLM _target _root_dir) \ No newline at end of file +endmacro(INCLUDE_GLM) \ No newline at end of file diff --git a/cmake/macros/IncludeHifiLibraryHeaders.cmake b/cmake/macros/IncludeHifiLibraryHeaders.cmake new file mode 100644 index 0000000000..913d1e1181 --- /dev/null +++ b/cmake/macros/IncludeHifiLibraryHeaders.cmake @@ -0,0 +1,14 @@ +# +# IncludeHifiLibraryHeaders.cmake +# cmake/macros +# +# Copyright 2014 High Fidelity, Inc. +# Created by Stephen Birarda on August 8, 2014 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(include_hifi_library_headers LIBRARY) + include_directories("${HIFI_LIBRARY_DIR}/${LIBRARY}/src") +endmacro(include_hifi_library_headers _library _root_dir) \ No newline at end of file diff --git a/cmake/macros/LinkHifiLibraries.cmake b/cmake/macros/LinkHifiLibraries.cmake new file mode 100644 index 0000000000..9d73963fea --- /dev/null +++ b/cmake/macros/LinkHifiLibraries.cmake @@ -0,0 +1,34 @@ +# +# LinkHifiLibrary.cmake +# +# Copyright 2013 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 +# + +macro(LINK_HIFI_LIBRARIES) + + file(RELATIVE_PATH RELATIVE_LIBRARY_DIR_PATH ${CMAKE_CURRENT_SOURCE_DIR} "${HIFI_LIBRARY_DIR}") + + set(LIBRARIES_TO_LINK ${ARGN}) + + foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK}) + if (NOT TARGET ${HIFI_LIBRARY}) + add_subdirectory("${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}" "${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}") + endif () + + include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src") + + add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY}) + + # link the actual library - it is static so don't bubble it up + target_link_libraries(${TARGET_NAME} ${HIFI_LIBRARY}) + + # ask the library what its dynamic dependencies are and link them + get_target_property(LINKED_TARGET_DEPENDENCY_LIBRARIES ${HIFI_LIBRARY} DEPENDENCY_LIBRARIES) + list(APPEND ${TARGET_NAME}_LIBRARIES_TO_LINK ${LINKED_TARGET_DEPENDENCY_LIBRARIES}) + + endforeach() + +endmacro(LINK_HIFI_LIBRARIES) \ No newline at end of file diff --git a/cmake/macros/LinkHifiLibrary.cmake b/cmake/macros/LinkHifiLibrary.cmake deleted file mode 100644 index 6300e50c34..0000000000 --- a/cmake/macros/LinkHifiLibrary.cmake +++ /dev/null @@ -1,27 +0,0 @@ -# -# LinkHifiLibrary.cmake -# -# Copyright 2013 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 -# - -macro(LINK_HIFI_LIBRARY LIBRARY TARGET ROOT_DIR) - - if (NOT TARGET ${LIBRARY}) - add_subdirectory("${ROOT_DIR}/libraries/${LIBRARY}" "${ROOT_DIR}/libraries/${LIBRARY}") - endif () - - include_directories("${ROOT_DIR}/libraries/${LIBRARY}/src") - - add_dependencies(${TARGET} ${LIBRARY}) - target_link_libraries(${TARGET} ${LIBRARY}) - - if (APPLE) - # currently the "shared" library requires CoreServices - # link in required OS X framework - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework CoreServices") - endif () - -endmacro(LINK_HIFI_LIBRARY _library _target _root_dir) \ No newline at end of file diff --git a/cmake/macros/LinkSharedDependencies.cmake b/cmake/macros/LinkSharedDependencies.cmake new file mode 100644 index 0000000000..ae478ca530 --- /dev/null +++ b/cmake/macros/LinkSharedDependencies.cmake @@ -0,0 +1,25 @@ +# +# LinkSharedDependencies.cmake +# cmake/macros +# +# Copyright 2014 High Fidelity, Inc. +# Created by Stephen Birarda on August 8, 2014 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(LINK_SHARED_DEPENDENCIES) + if (${TARGET_NAME}_LIBRARIES_TO_LINK) + list(REMOVE_DUPLICATES ${TARGET_NAME}_LIBRARIES_TO_LINK) + + # link these libraries to our target + target_link_libraries(${TARGET_NAME} ${${TARGET_NAME}_LIBRARIES_TO_LINK}) + endif () + + # we've already linked our Qt modules, but we need to bubble them up to parents + list(APPEND ${TARGET_NAME}_LIBRARIES_TO_LINK "${${TARGET}_QT_MODULES_TO_LINK}") + + # set the property on this target so it can be retreived by targets linking to us + set_target_properties(${TARGET_NAME} PROPERTIES DEPENDENCY_LIBRARIES "${${TARGET}_LIBRARIES_TO_LINK}") +endmacro(LINK_SHARED_DEPENDENCIES) \ No newline at end of file diff --git a/cmake/macros/SetupHifiLibrary.cmake b/cmake/macros/SetupHifiLibrary.cmake index ff4ae3b4f3..7bb85f68f5 100644 --- a/cmake/macros/SetupHifiLibrary.cmake +++ b/cmake/macros/SetupHifiLibrary.cmake @@ -7,20 +7,27 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -macro(SETUP_HIFI_LIBRARY TARGET) +macro(SETUP_HIFI_LIBRARY) - project(${TARGET}) + project(${TARGET_NAME}) # grab the implemenation and header files file(GLOB LIB_SRCS src/*.h src/*.cpp) - set(LIB_SRCS ${LIB_SRCS} ${WRAPPED_SRCS}) + set(LIB_SRCS ${LIB_SRCS}) # create a library and set the property so it can be referenced later - add_library(${TARGET} ${LIB_SRCS} ${ARGN}) + add_library(${TARGET_NAME} ${LIB_SRCS} ${AUTOMTC_SRC}) - find_package(Qt5Core REQUIRED) - qt5_use_modules(${TARGET} Core) - - target_link_libraries(${TARGET} ${QT_LIBRARIES}) - -endmacro(SETUP_HIFI_LIBRARY _target) \ No newline at end of file + set(QT_MODULES_TO_LINK ${ARGN}) + list(APPEND QT_MODULES_TO_LINK Core) + + find_package(Qt5 COMPONENTS ${QT_MODULES_TO_LINK} REQUIRED) + + foreach(QT_MODULE ${QT_MODULES_TO_LINK}) + get_target_property(QT_LIBRARY_LOCATION Qt5::${QT_MODULE} LOCATION) + + # add the actual path to the Qt module to our LIBRARIES_TO_LINK variable + target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) + list(APPEND ${TARGET_NAME}_QT_MODULES_TO_LINK ${QT_LIBRARY_LOCATION}) + endforeach() +endmacro(SETUP_HIFI_LIBRARY) \ No newline at end of file diff --git a/cmake/macros/SetupHifiProject.cmake b/cmake/macros/SetupHifiProject.cmake index ec731859d4..d21e2c11bb 100644 --- a/cmake/macros/SetupHifiProject.cmake +++ b/cmake/macros/SetupHifiProject.cmake @@ -7,8 +7,8 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -macro(SETUP_HIFI_PROJECT TARGET INCLUDE_QT) - project(${TARGET}) +macro(SETUP_HIFI_PROJECT) + project(${TARGET_NAME}) # grab the implemenation and header files file(GLOB TARGET_SRCS src/*) @@ -23,12 +23,19 @@ macro(SETUP_HIFI_PROJECT TARGET INCLUDE_QT) endforeach() # add the executable, include additional optional sources - add_executable(${TARGET} ${TARGET_SRCS} ${ARGN}) + add_executable(${TARGET_NAME} ${TARGET_SRCS} "${AUTOMTC_SRC}") - if (${INCLUDE_QT}) - find_package(Qt5Core REQUIRED) - qt5_use_modules(${TARGET} Core) - endif () + set(QT_MODULES_TO_LINK ${ARGN}) + list(APPEND QT_MODULES_TO_LINK Core) + + find_package(Qt5 COMPONENTS ${QT_MODULES_TO_LINK} REQUIRED) + + foreach(QT_MODULE ${QT_MODULES_TO_LINK}) + target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) + + # add the actual path to the Qt module to our LIBRARIES_TO_LINK variable + get_target_property(QT_LIBRARY_LOCATION Qt5::${QT_MODULE} LOCATION) + list(APPEND ${TARGET_NAME}_QT_MODULES_TO_LINK ${QT_LIBRARY_LOCATION}) + endforeach() - target_link_libraries(${TARGET} ${QT_LIBRARIES}) endmacro() \ No newline at end of file diff --git a/cmake/modules/FindATL.cmake b/cmake/modules/FindATL.cmake new file mode 100644 index 0000000000..f95b0267eb --- /dev/null +++ b/cmake/modules/FindATL.cmake @@ -0,0 +1,29 @@ +# +# FindATL.cmake +# +# Try to find the ATL library needed to use the LibOVR +# +# Once done this will define +# +# ATL_FOUND - system found ATL +# ATL_LIBRARIES - Link this to use ATL +# +# Created on 8/13/2013 by Stephen Birarda +# 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 +# + +if (WIN32) + find_library(ATL_LIBRARY_RELEASE atls PATH_SUFFIXES "7600.16385.1/lib/ATL/i386" HINTS "C:\\WinDDK") + find_library(ATL_LIBRARY_DEBUG atlsd PATH_SUFFIXES "7600.16385.1/lib/ATL/i386" HINTS "C:\\WinDDK") + + include(SelectLibraryConfigurations) + select_library_configurations(ATL) +endif () + +set(ATL_LIBRARIES "${ATL_LIBRARY}") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(ATL DEFAULT_MSG ATL_LIBRARIES) \ No newline at end of file diff --git a/cmake/modules/FindFaceplus.cmake b/cmake/modules/FindFaceplus.cmake index 1050493c69..e97fce3edb 100644 --- a/cmake/modules/FindFaceplus.cmake +++ b/cmake/modules/FindFaceplus.cmake @@ -19,6 +19,6 @@ if (WIN32) endif (WIN32) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(FACEPLUS DEFAULT_MSG FACEPLUS_INCLUDE_DIRS FACEPLUS_LIBRARIES) +find_package_handle_standard_args(Faceplus DEFAULT_MSG FACEPLUS_INCLUDE_DIRS FACEPLUS_LIBRARIES) mark_as_advanced(FACEPLUS_INCLUDE_DIRS FACEPLUS_LIBRARIES) \ No newline at end of file diff --git a/cmake/modules/FindFaceshift.cmake b/cmake/modules/FindFaceshift.cmake index 2641475fa3..0dcbcbb7dd 100644 --- a/cmake/modules/FindFaceshift.cmake +++ b/cmake/modules/FindFaceshift.cmake @@ -40,6 +40,6 @@ select_library_configurations(FACESHIFT) set(FACESHIFT_LIBRARIES ${FACESHIFT_LIBRARY}) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(FACESHIFT DEFAULT_MSG FACESHIFT_INCLUDE_DIRS FACESHIFT_LIBRARIES) +find_package_handle_standard_args(Faceshift DEFAULT_MSG FACESHIFT_INCLUDE_DIRS FACESHIFT_LIBRARIES) mark_as_advanced(FACESHIFT_INCLUDE_DIRS FACESHIFT_LIBRARIES FACESHIFT_SEARCH_DIRS) \ No newline at end of file diff --git a/cmake/modules/FindLeapMotion.cmake b/cmake/modules/FindLeapMotion.cmake index a64fc22e48..cb49ceb597 100644 --- a/cmake/modules/FindLeapMotion.cmake +++ b/cmake/modules/FindLeapMotion.cmake @@ -30,6 +30,6 @@ select_library_configurations(LEAPMOTION) set(LEAPMOTION_LIBRARIES "${LEAPMOTION_LIBRARY}") include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(LEAPMOTION DEFAULT_MSG LEAPMOTION_INCLUDE_DIRS LEAPMOTION_LIBRARIES) +find_package_handle_standard_args(LeapMotion DEFAULT_MSG LEAPMOTION_INCLUDE_DIRS LEAPMOTION_LIBRARIES) mark_as_advanced(LEAPMOTION_INCLUDE_DIRS LEAPMOTION_LIBRARIES LEAPMOTION_SEARCH_DIRS) diff --git a/cmake/modules/FindLibOVR.cmake b/cmake/modules/FindLibOVR.cmake index e90afe29e4..a1d75add3f 100644 --- a/cmake/modules/FindLibOVR.cmake +++ b/cmake/modules/FindLibOVR.cmake @@ -19,16 +19,18 @@ # include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") -hifi_library_search_hints("oculus") +hifi_library_search_hints("libovr") -find_path(LIBOVR_INCLUDE_DIRS OVR.h PATH_SUFFIXES Include HINTS ${OCULUS_SEARCH_DIRS}) -find_path(LIBOVR_SRC_DIR Util_Render_Stereo.h PATH_SUFFIXES Src/Util HINTS ${OCULUS_SEARCH_DIRS}) +find_path(LIBOVR_INCLUDE_DIRS OVR.h PATH_SUFFIXES Include HINTS ${LIBOVR_SEARCH_DIRS}) +find_path(LIBOVR_SRC_DIR Util_Render_Stereo.h PATH_SUFFIXES Src/Util HINTS ${LIBOVR_SEARCH_DIRS}) include(SelectLibraryConfigurations) if (APPLE) - find_library(LIBOVR_LIBRARY_DEBUG NAMES ovr PATH_SUFFIXES Lib/MacOS/Debug HINTS ${OCULUS_SEARCH_DIRS}) - find_library(LIBOVR_LIBRARY_RELEASE NAMES ovr PATH_SUFFIXES Lib/MacOS/Release HINTS ${OCULUS_SEARCH_DIRS}) + find_library(LIBOVR_LIBRARY_DEBUG NAMES ovr PATH_SUFFIXES Lib/Mac/Debug HINTS ${LIBOVR_SEARCH_DIRS}) + find_library(LIBOVR_LIBRARY_RELEASE NAMES ovr PATH_SUFFIXES Lib/Mac/Release HINTS ${LIBOVR_SEARCH_DIRS}) + find_library(ApplicationServices ApplicationServices) + find_library(IOKit IOKit) elseif (UNIX) find_library(UDEV_LIBRARY_RELEASE udev /usr/lib/x86_64-linux-gnu/) find_library(XINERAMA_LIBRARY_RELEASE Xinerama /usr/lib/x86_64-linux-gnu/) @@ -39,29 +41,36 @@ elseif (UNIX) set(LINUX_ARCH_DIR "x86_64") endif() - find_library(LIBOVR_LIBRARY_DEBUG NAMES ovr PATH_SUFFIXES Lib/Linux/Debug/${LINUX_ARCH_DIR} HINTS ${OCULUS_SEARCH_DIRS}) - find_library(LIBOVR_LIBRARY_RELEASE NAMES ovr PATH_SUFFIXES Lib/Linux/Release/${LINUX_ARCH_DIR} HINTS ${OCULUS_SEARCH_DIRS}) + find_library(LIBOVR_LIBRARY_DEBUG NAMES ovr PATH_SUFFIXES Lib/Linux/Debug/${LINUX_ARCH_DIR} HINTS ${LIBOVR_SEARCH_DIRS}) + find_library(LIBOVR_LIBRARY_RELEASE NAMES ovr PATH_SUFFIXES Lib/Linux/Release/${LINUX_ARCH_DIR} HINTS ${LIBOVR_SEARCH_DIRS}) select_library_configurations(UDEV) select_library_configurations(XINERAMA) elseif (WIN32) - find_library(LIBOVR_LIBRARY_DEBUG NAMES libovrd PATH_SUFFIXES Lib/Win32/VS2010 HINTS ${OCULUS_SEARCH_DIRS}) - find_library(LIBOVR_LIBRARY_RELEASE NAMES libovr PATH_SUFFIXES Lib/Win32/VS2010 HINTS ${OCULUS_SEARCH_DIRS}) + find_library(LIBOVR_LIBRARY_DEBUG NAMES libovrd PATH_SUFFIXES Lib/Win32/VS2010 HINTS ${LIBOVR_SEARCH_DIRS}) + find_library(LIBOVR_LIBRARY_RELEASE NAMES libovr PATH_SUFFIXES Lib/Win32/VS2010 HINTS ${LIBOVR_SEARCH_DIRS}) + find_package(ATL) endif () select_library_configurations(LIBOVR) -set(LIBOVR_LIBRARIES "${LIBOVR_LIBRARY}") +set(LIBOVR_LIBRARIES ${LIBOVR_LIBRARY}) -if (UNIX AND NOT APPLE) - set(LIBOVR_LIBRARIES "${LIBOVR_LIBRARIES}" "${UDEV_LIBRARY}" "${XINERAMA_LIBRARY}") +list(APPEND LIBOVR_ARGS_LIST LIBOVR_INCLUDE_DIRS LIBOVR_SRC_DIR LIBOVR_LIBRARY) + +if (APPLE) + list(APPEND LIBOVR_LIBRARIES ${IOKit} ${ApplicationServices}) + list(APPEND LIBOVR_ARGS_LIST IOKit ApplicationServices) +elseif (UNIX) + list(APPEND LIBOVR_LIBRARIES "${UDEV_LIBRARY}" "${XINERAMA_LIBRARY}") + list(APPEND LIBOVR_ARGS_LIST UDEV_LIBRARY XINERAMA_LIBRARY) +elseif (WIN32) + list(APPEND LIBOVR_LIBRARIES ${ATL_LIBRARIES}) + list(APPEND LIBOVR_ARGS_LIST ATL_LIBRARIES) endif () include(FindPackageHandleStandardArgs) -if (UNIX AND NOT APPLE) - find_package_handle_standard_args(LIBOVR DEFAULT_MSG LIBOVR_INCLUDE_DIRS LIBOVR_SRC_DIR LIBOVR_LIBRARIES UDEV_LIBRARY XINERAMA_LIBRARY) -else () - find_package_handle_standard_args(LIBOVR DEFAULT_MSG LIBOVR_INCLUDE_DIRS LIBOVR_SRC_DIR LIBOVR_LIBRARIES) -endif () -mark_as_advanced(LIBOVR_INCLUDE_DIRS LIBOVR_LIBRARIES OCULUS_SEARCH_DIRS) +find_package_handle_standard_args(LibOVR DEFAULT_MSG ${LIBOVR_ARGS_LIST}) + +mark_as_advanced(LIBOVR_INCLUDE_DIRS LIBOVR_LIBRARIES LIBOVR_SEARCH_DIRS) diff --git a/cmake/modules/FindPrioVR.cmake b/cmake/modules/FindPrioVR.cmake index af2c0aca52..691ba85689 100644 --- a/cmake/modules/FindPrioVR.cmake +++ b/cmake/modules/FindPrioVR.cmake @@ -19,6 +19,6 @@ if (WIN32) endif (WIN32) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(PRIOVR DEFAULT_MSG PRIOVR_INCLUDE_DIRS PRIOVR_LIBRARIES) +find_package_handle_standard_args(PrioVR DEFAULT_MSG PRIOVR_INCLUDE_DIRS PRIOVR_LIBRARIES) mark_as_advanced(PRIOVR_INCLUDE_DIRS PRIOVR_LIBRARIES) \ No newline at end of file diff --git a/cmake/modules/FindQxmpp.cmake b/cmake/modules/FindQxmpp.cmake index e5b6e6506a..8fbc63e9dc 100644 --- a/cmake/modules/FindQxmpp.cmake +++ b/cmake/modules/FindQxmpp.cmake @@ -26,12 +26,14 @@ find_path(QXMPP_INCLUDE_DIRS QXmppClient.h PATH_SUFFIXES include/qxmpp HINTS ${Q find_library(QXMPP_LIBRARY_RELEASE NAMES qxmpp PATH_SUFFIXES lib HINTS ${QXMPP_SEARCH_DIRS}) find_library(QXMPP_LIBRARY_DEBUG NAMES qxmpp_d PATH_SUFFIXES lib HINTS ${QXMPP_SEARCH_DIRS}) +find_package(Qt5 COMPONENTS Xml REQUIRED) + include(SelectLibraryConfigurations) select_library_configurations(QXMPP) -set(QXMPP_LIBRARIES "${QXMPP_LIBRARY}") +set(QXMPP_LIBRARIES "${QXMPP_LIBRARY}" Qt5::Xml) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(QXMPP DEFAULT_MSG QXMPP_INCLUDE_DIRS QXMPP_LIBRARIES) +find_package_handle_standard_args(QXmpp DEFAULT_MSG QXMPP_INCLUDE_DIRS QXMPP_LIBRARIES QXMPP_LIBRARY) mark_as_advanced(QXMPP_INCLUDE_DIRS QXMPP_LIBRARIES QXMPP_SEARCH_DIRS) \ No newline at end of file diff --git a/cmake/modules/FindRtMidi.cmake b/cmake/modules/FindRtMidi.cmake index 93f1cc69cc..3b57c76f25 100644 --- a/cmake/modules/FindRtMidi.cmake +++ b/cmake/modules/FindRtMidi.cmake @@ -25,6 +25,6 @@ find_path(RTMIDI_INCLUDE_DIRS RtMidi.h PATH_SUFFIXES include HINTS ${RTMIDI_SEAR find_library(RTMIDI_LIBRARIES NAMES rtmidi PATH_SUFFIXES lib HINTS ${RTMIDI_SEARCH_DIRS}) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(RTMIDI DEFAULT_MSG RTMIDI_INCLUDE_DIRS RTMIDI_LIBRARIES) +find_package_handle_standard_args(RtMidi DEFAULT_MSG RTMIDI_INCLUDE_DIRS RTMIDI_LIBRARIES) mark_as_advanced(RTMIDI_INCLUDE_DIRS RTMIDI_LIBRARIES RTMIDI_SEARCH_DIRS) \ No newline at end of file diff --git a/cmake/modules/FindSixense.cmake b/cmake/modules/FindSixense.cmake index a24131698b..f772c42e41 100644 --- a/cmake/modules/FindSixense.cmake +++ b/cmake/modules/FindSixense.cmake @@ -40,6 +40,6 @@ select_library_configurations(SIXENSE) set(SIXENSE_LIBRARIES "${SIXENSE_LIBRARY}") include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(SIXENSE DEFAULT_MSG SIXENSE_INCLUDE_DIRS SIXENSE_LIBRARIES) +find_package_handle_standard_args(Sixense DEFAULT_MSG SIXENSE_INCLUDE_DIRS SIXENSE_LIBRARIES) mark_as_advanced(SIXENSE_LIBRARIES SIXENSE_INCLUDE_DIRS SIXENSE_SEARCH_DIRS) diff --git a/cmake/modules/FindVisage.cmake b/cmake/modules/FindVisage.cmake index b461926604..96176a2648 100644 --- a/cmake/modules/FindVisage.cmake +++ b/cmake/modules/FindVisage.cmake @@ -30,8 +30,11 @@ if (APPLE) find_library(VISAGE_CORE_LIBRARY NAME vscore PATH_SUFFIXES lib HINTS ${VISAGE_SEARCH_DIRS}) find_library(VISAGE_VISION_LIBRARY NAME vsvision PATH_SUFFIXES lib HINTS ${VISAGE_SEARCH_DIRS}) - find_library(VISAGE_OPENCV_LIBRARY NAME OpenCV PATH_SUFFIXES dependencies/OpenCV_MacOSX/lib ${VISAGE_SEARCH_DIRS}) + find_library(VISAGE_OPENCV_LIBRARY NAME OpenCV PATH_SUFFIXES dependencies/OpenCV_MacOSX/lib HINTS ${VISAGE_SEARCH_DIRS}) + find_library(AppKit AppKit) + find_library(QuartzCore QuartzCore) + find_library(QTKit QTKit) elseif (WIN32) find_path(VISAGE_XML_INCLUDE_DIR libxml/xmlreader.h PATH_SUFFIXES dependencies/libxml2/include HINTS ${VISAGE_SEARCH_DIRS}) find_path(VISAGE_OPENCV_INCLUDE_DIR opencv/cv.h PATH_SUFFIXES dependencies/OpenCV/include HINTS ${VISAGE_SEARCH_DIRS}) @@ -43,16 +46,22 @@ elseif (WIN32) endif () include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(VISAGE DEFAULT_MSG - VISAGE_BASE_INCLUDE_DIR VISAGE_XML_INCLUDE_DIR VISAGE_OPENCV_INCLUDE_DIR VISAGE_OPENCV2_INCLUDE_DIR - VISAGE_CORE_LIBRARY VISAGE_VISION_LIBRARY VISAGE_OPENCV_LIBRARY -) + +list(APPEND VISAGE_ARGS_LIST VISAGE_BASE_INCLUDE_DIR VISAGE_XML_INCLUDE_DIR + VISAGE_OPENCV_INCLUDE_DIR VISAGE_OPENCV2_INCLUDE_DIR + VISAGE_CORE_LIBRARY VISAGE_VISION_LIBRARY VISAGE_OPENCV_LIBRARY) + +if (APPLE) + list(APPEND VISAGE_ARGS_LIST QuartzCore AppKit QTKit) +endif () + +find_package_handle_standard_args(Visage DEFAULT_MSG ${VISAGE_ARGS_LIST}) set(VISAGE_INCLUDE_DIRS "${VISAGE_XML_INCLUDE_DIR}" "${VISAGE_OPENCV_INCLUDE_DIR}" "${VISAGE_OPENCV2_INCLUDE_DIR}" "${VISAGE_BASE_INCLUDE_DIR}") set(VISAGE_LIBRARIES "${VISAGE_CORE_LIBRARY}" "${VISAGE_VISION_LIBRARY}" "${VISAGE_OPENCV_LIBRARY}") -mark_as_advanced( - VISAGE_INCLUDE_DIRS VISAGE_LIBRARIES - VISAGE_BASE_INCLUDE_DIR VISAGE_XML_INCLUDE_DIR VISAGE_OPENCV_INCLUDE_DIR VISAGE_OPENCV2_INCLUDE_DIR - VISAGE_CORE_LIBRARY VISAGE_VISION_LIBRARY VISAGE_OPENCV_LIBRARY VISAGE_SEARCH_DIRS -) +if (APPLE) + list(APPEND VISAGE_LIBRARIES ${QuartzCore} ${AppKit} ${QTKit}) +endif () + +mark_as_advanced(VISAGE_INCLUDE_DIRS VISAGE_LIBRARIES) diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index 6ee794f7c6..0fb9d25b5f 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -1,24 +1,7 @@ -if (WIN32) - cmake_policy (SET CMP0020 NEW) -endif (WIN32) - set(TARGET_NAME domain-server) -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 up the external glm library -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") - -find_package(Qt5Network REQUIRED) - -include(${MACRO_DIR}/SetupHifiProject.cmake) - -setup_hifi_project(${TARGET_NAME} TRUE) +# setup the project and link required Qt modules +setup_hifi_project(Network) # remove and then copy the files for the webserver add_custom_command(TARGET ${TARGET_NAME} POST_BUILD @@ -29,20 +12,7 @@ add_custom_command(TARGET ${TARGET_NAME} POST_BUILD "${PROJECT_SOURCE_DIR}/resources/web" $/resources/web) -# link the shared hifi library -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(embedded-webserver ${TARGET_NAME} "${ROOT_DIR}") +# link the shared hifi libraries +link_hifi_libraries(embedded-webserver networking shared) -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) - -# link QtNetwork -target_link_libraries(${TARGET_NAME} Qt5::Network) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +link_shared_dependencies() \ No newline at end of file diff --git a/domain-server/resources/web/footer.html b/domain-server/resources/web/footer.html index d1a3fc29e8..aa6821a3b9 100644 --- a/domain-server/resources/web/footer.html +++ b/domain-server/resources/web/footer.html @@ -1,3 +1,4 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/domain-server/resources/web/header.html b/domain-server/resources/web/header.html index 2be603b00e..a2aebc1189 100644 --- a/domain-server/resources/web/header.html +++ b/domain-server/resources/web/header.html @@ -8,4 +8,33 @@ +
\ No newline at end of file diff --git a/domain-server/resources/web/index.shtml b/domain-server/resources/web/index.shtml index f0315a113f..2f83172d4a 100644 --- a/domain-server/resources/web/index.shtml +++ b/domain-server/resources/web/index.shtml @@ -10,6 +10,7 @@ Type UUID Pool + Username Public Local Uptime (s) @@ -24,8 +25,9 @@ <%- node.type %> <%- node.uuid %> <%- node.pool %> - <%- node.public.ip %><%- node.public.port %> - <%- node.local.ip %><%- node.local.port %> + <%- node.username %> + <%- node.public.ip %>:<%- node.public.port %> + <%- node.local.ip %>:<%- node.local.port %> <%- ((Date.now() - node.wake_timestamp) / 1000).toLocaleString() %> <%- (typeof node.pending_credits == 'number' ? node.pending_credits.toLocaleString() : 'N/A') %> diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js new file mode 100644 index 0000000000..3f78d8f466 --- /dev/null +++ b/domain-server/resources/web/js/domain-server.js @@ -0,0 +1,10 @@ +$(document).ready(function(){ + var url = window.location; + // Will only work if string in href matches with location + $('ul.nav a[href="'+ url +'"]').parent().addClass('active'); + + // Will also work for relative and absolute hrefs + $('ul.nav a').filter(function() { + return this.href == url; + }).parent().addClass('active'); +}); \ No newline at end of file diff --git a/domain-server/resources/web/settings/describe.json b/domain-server/resources/web/settings/describe.json index f4920a7b50..788a3ad551 100644 --- a/domain-server/resources/web/settings/describe.json +++ b/domain-server/resources/web/settings/describe.json @@ -21,6 +21,12 @@ "placeholder": "10", "default": "10" }, + "H-print-stream-stats": { + "type": "checkbox", + "label": "Print Stream Stats:", + "help": "If enabled, audio upstream and downstream stats of each agent will be printed each second to stdout", + "default": false + }, "D-unattenuated-zone": { "label": "Unattenuated Zone", "help": "Boxes for source and listener (corner x, corner y, corner z, size x, size y, size z, corner x, corner y, corner z, size x, size y, size z)", diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 3bb669b32e..73e3cdbea2 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -16,16 +16,22 @@ <% var setting_id = group_key + "." + setting_key %>
- <% if(setting.type) %> - <% if (setting.type === "checkbox") { %> - <% var checked_box = (values[group_key] || {})[setting_key] || setting.default %> - > - <% } else { %> + <% if (setting.type === "checkbox") { %> + <% var checked_box = (values[group_key] || {})[setting_key] || setting.default %> + > + <% } else { %> + <% if (setting.input_addon) { %> +
+
<%- setting.input_addon %>
+ <% } %> + + <% if (setting.input_addon) { %> +
+ <% } %> <% } %> -

<%- setting.help %>

diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 366a5016f9..a76d3a916d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -42,12 +42,17 @@ DomainServer::DomainServer(int argc, char* argv[]) : _hostname(), _networkReplyUUIDMap(), _sessionAuthenticationHash(), + _webAuthenticationStateSet(), + _cookieSessionHash(), _settingsManager() { setOrganizationName("High Fidelity"); setOrganizationDomain("highfidelity.io"); setApplicationName("domain-server"); QSettings::setDefaultFormat(QSettings::IniFormat); + + qRegisterMetaType("DomainServerWebSessionData"); + qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); _argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments()); @@ -57,6 +62,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : qDebug() << "Setting up LimitedNodeList and assignments."; setupNodeListAndAssignments(); + + loadExistingSessionsFromSettings(); } } @@ -426,11 +433,14 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock } } + + QString connectedUsername; if (!isAssignment && !_oauthProviderURL.isEmpty() && _argumentVariantMap.contains(ALLOWED_ROLES_CONFIG_KEY)) { // this is an Agent, and we require authentication so we can compare the user's roles to our list of allowed ones if (_sessionAuthenticationHash.contains(packetUUID)) { - if (!_sessionAuthenticationHash.value(packetUUID)) { + connectedUsername = _sessionAuthenticationHash.take(packetUUID); + if (connectedUsername.isEmpty()) { // we've decided this is a user that isn't allowed in, return out // TODO: provide information to the user so they know why they can't connect return; @@ -473,6 +483,9 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock // now that we've pulled the wallet UUID and added the node to our list, delete the pending assignee data delete pendingAssigneeData; } + + // if we have a username from an OAuth connect request, set it on the DomainServerNodeData + nodeData->setUsername(connectedUsername); nodeData->setSendingSockAddr(senderSockAddr); @@ -848,7 +861,7 @@ QJsonObject DomainServer::jsonForSocket(const HifiSockAddr& socket) { QJsonObject socketJSON; socketJSON["ip"] = socket.getAddress().toString(); - socketJSON["port"] = ntohs(socket.getPort()); + socketJSON["port"] = socket.getPort(); return socketJSON; } @@ -860,6 +873,7 @@ const char JSON_KEY_LOCAL_SOCKET[] = "local"; const char JSON_KEY_POOL[] = "pool"; const char JSON_KEY_PENDING_CREDITS[] = "pending_credits"; const char JSON_KEY_WAKE_TIMESTAMP[] = "wake_timestamp"; +const char JSON_KEY_USERNAME[] = "username"; QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { QJsonObject nodeJson; @@ -884,6 +898,10 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { // if the node has pool information, add it DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); + + // add the node username, if it exists + nodeJson[JSON_KEY_USERNAME] = nodeData->getUsername(); + SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID()); if (matchingAssignment) { nodeJson[JSON_KEY_POOL] = matchingAssignment->getPool(); @@ -921,7 +939,52 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_NODES = "/nodes"; const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; - + + // allow sub-handlers to handle requests that do not require authentication + if (_settingsManager.handlePublicHTTPRequest(connection, url)) { + return true; + } + + // check if this is a request for a scripted assignment (with a temp unique UUID) + const QString ASSIGNMENT_REGEX_STRING = QString("\\%1\\/(%2)\\/?$").arg(URI_ASSIGNMENT).arg(UUID_REGEX_STRING); + QRegExp assignmentRegex(ASSIGNMENT_REGEX_STRING); + + if (connection->requestOperation() == QNetworkAccessManager::GetOperation + && assignmentRegex.indexIn(url.path()) != -1) { + QUuid matchingUUID = QUuid(assignmentRegex.cap(1)); + + SharedAssignmentPointer matchingAssignment = _allAssignments.value(matchingUUID); + if (!matchingAssignment) { + // check if we have a pending assignment that matches this temp UUID, and it is a scripted assignment + PendingAssignedNodeData* pendingData = _pendingAssignedNodes.value(matchingUUID); + if (pendingData) { + matchingAssignment = _allAssignments.value(pendingData->getAssignmentUUID()); + + if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { + // we have a matching assignment and it is for the right type, have the HTTP manager handle it + // via correct URL for the script so the client can download + + QUrl scriptURL = url; + scriptURL.setPath(URI_ASSIGNMENT + "/" + + uuidStringWithoutCurlyBraces(pendingData->getAssignmentUUID())); + + // have the HTTPManager serve the appropriate script file + return _httpManager.handleHTTPRequest(connection, scriptURL); + } + } + } + + // request not handled + return false; + } + + // all requests below require a cookie to prove authentication so check that first + if (!isAuthenticatedRequest(connection, url)) { + // this is not an authenticated request + // return true from the handler since it was handled with a 401 or re-direct to auth + return true; + } + if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { if (url.path() == "/assignments.json") { // user is asking for json list of assignments @@ -1038,38 +1101,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return false; } - - // check if this is a request for a scripted assignment (with a temp unique UUID) - const QString ASSIGNMENT_REGEX_STRING = QString("\\%1\\/(%2)\\/?$").arg(URI_ASSIGNMENT).arg(UUID_REGEX_STRING); - QRegExp assignmentRegex(ASSIGNMENT_REGEX_STRING); - - if (assignmentRegex.indexIn(url.path()) != -1) { - QUuid matchingUUID = QUuid(assignmentRegex.cap(1)); - - SharedAssignmentPointer matchingAssignment = _allAssignments.value(matchingUUID); - if (!matchingAssignment) { - // check if we have a pending assignment that matches this temp UUID, and it is a scripted assignment - PendingAssignedNodeData* pendingData = _pendingAssignedNodes.value(matchingUUID); - if (pendingData) { - matchingAssignment = _allAssignments.value(pendingData->getAssignmentUUID()); - - if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { - // we have a matching assignment and it is for the right type, have the HTTP manager handle it - // via correct URL for the script so the client can download - - QUrl scriptURL = url; - scriptURL.setPath(URI_ASSIGNMENT + "/" - + uuidStringWithoutCurlyBraces(pendingData->getAssignmentUUID())); - - // have the HTTPManager serve the appropriate script file - return _httpManager.handleHTTPRequest(connection, scriptURL); - } - } - } - - // request not handled - return false; - } } } else if (connection->requestOperation() == QNetworkAccessManager::PostOperation) { if (url.path() == URI_ASSIGNMENT) { @@ -1162,9 +1193,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url } // didn't process the request, let our DomainServerSettingsManager or HTTPManager handle - return _settingsManager.handleHTTPRequest(connection, url); + return _settingsManager.handleAuthenticatedHTTPRequest(connection, url); } +const QString HIFI_SESSION_COOKIE_KEY = "DS_WEB_SESSION_UUID"; + bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url) { const QString URI_OAUTH = "/oauth"; qDebug() << "HTTPS request received at" << url.toString(); @@ -1178,7 +1211,6 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u const QString STATE_QUERY_KEY = "state"; QUuid stateUUID = QUuid(codeURLQuery.queryItemValue(STATE_QUERY_KEY)); - if (!authorizationCode.isEmpty() && !stateUUID.isNull()) { // fire off a request with this code and state to get an access token for the user @@ -1193,15 +1225,45 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u QNetworkRequest tokenRequest(tokenRequestUrl); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - + QNetworkReply* tokenReply = NetworkAccessManager::getInstance().post(tokenRequest, tokenPostBody.toLocal8Bit()); - - qDebug() << "Requesting a token for user with session UUID" << uuidStringWithoutCurlyBraces(stateUUID); - - // insert this to our pending token replies so we can associate the returned access token with the right UUID - _networkReplyUUIDMap.insert(tokenReply, stateUUID); - - connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::handleTokenRequestFinished); + + if (_webAuthenticationStateSet.remove(stateUUID)) { + // this is a web user who wants to auth to access web interface + // we hold the response back to them until we get their profile information + // and can decide if they are let in or not + + QEventLoop loop; + connect(tokenReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + + // start the loop for the token request + loop.exec(); + + QNetworkReply* profileReply = profileRequestGivenTokenReply(tokenReply); + + // stop the loop once the profileReply is complete + connect(profileReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + + // restart the loop for the profile request + loop.exec(); + + // call helper method to get cookieHeaders + Headers cookieHeaders = setupCookieHeadersFromProfileReply(profileReply); + + connection->respond(HTTPConnection::StatusCode302, QByteArray(), + HTTPConnection::DefaultContentType, cookieHeaders); + + // we've redirected the user back to our homepage + return true; + + } else { + qDebug() << "Requesting a token for user with session UUID" << uuidStringWithoutCurlyBraces(stateUUID); + + // insert this to our pending token replies so we can associate the returned access token with the right UUID + _networkReplyUUIDMap.insert(tokenReply, stateUUID); + + connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::handleTokenRequestFinished); + } } // respond with a 200 code indicating that login is complete @@ -1213,6 +1275,137 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u } } +bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url) { + + const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie"; + const QString ADMIN_USERS_CONFIG_KEY = "admin-users"; + const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles"; + const QString BASIC_AUTH_CONFIG_KEY = "basic-auth"; + + const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; + + if (!_oauthProviderURL.isEmpty() + && (_argumentVariantMap.contains(ADMIN_USERS_CONFIG_KEY) || _argumentVariantMap.contains(ADMIN_ROLES_CONFIG_KEY))) { + QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY); + + const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; + QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); + + QUuid cookieUUID; + if (cookieString.indexOf(cookieUUIDRegex) != -1) { + cookieUUID = cookieUUIDRegex.cap(1); + } + + if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) { + qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." + << "These cannot be combined - using OAuth for authentication."; + } + + if (!cookieUUID.isNull() && _cookieSessionHash.contains(cookieUUID)) { + // pull the QJSONObject for the user with this cookie UUID + DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID); + QString profileUsername = sessionData.getUsername(); + + if (_argumentVariantMap.value(ADMIN_USERS_CONFIG_KEY).toJsonValue().toArray().contains(profileUsername)) { + // this is an authenticated user + return true; + } + + // loop the roles of this user and see if they are in the admin-roles array + QJsonArray adminRolesArray = _argumentVariantMap.value(ADMIN_ROLES_CONFIG_KEY).toJsonValue().toArray(); + + if (!adminRolesArray.isEmpty()) { + foreach(const QString& userRole, sessionData.getRoles()) { + if (adminRolesArray.contains(userRole)) { + // this user has a role that allows them to administer the domain-server + return true; + } + } + } + + connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY); + + // the user does not have allowed username or role, return 401 + return false; + } else { + // re-direct this user to OAuth page + + // generate a random state UUID to use + QUuid stateUUID = QUuid::createUuid(); + + // add it to the set so we can handle the callback from the OAuth provider + _webAuthenticationStateSet.insert(stateUUID); + + QUrl oauthRedirectURL = oauthAuthorizationURL(stateUUID); + + Headers redirectHeaders; + redirectHeaders.insert("Location", oauthRedirectURL.toEncoded()); + + connection->respond(HTTPConnection::StatusCode302, + QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders); + + // we don't know about this user yet, so they are not yet authenticated + return false; + } + } else if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) { + // config file contains username and password combinations for basic auth + const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; + + // check if a username and password have been provided with the request + QString basicAuthString = connection->requestHeaders().value(BASIC_AUTH_HEADER_KEY); + + if (!basicAuthString.isEmpty()) { + QStringList splitAuthString = basicAuthString.split(' '); + QString base64String = splitAuthString.size() == 2 ? splitAuthString[1] : ""; + QString credentialString = QByteArray::fromBase64(base64String.toLocal8Bit()); + + if (!credentialString.isEmpty()) { + QStringList credentialList = credentialString.split(':'); + if (credentialList.size() == 2) { + QString username = credentialList[0]; + QString password = credentialList[1]; + + // we've pulled a username and password - now check if there is a match in our basic auth hash + QJsonObject basicAuthObject = _argumentVariantMap.value(BASIC_AUTH_CONFIG_KEY).toJsonValue().toObject(); + + if (basicAuthObject.contains(username)) { + const QString BASIC_AUTH_USER_PASSWORD_KEY = "password"; + QJsonObject userObject = basicAuthObject.value(username).toObject(); + + if (userObject.contains(BASIC_AUTH_USER_PASSWORD_KEY) + && userObject.value(BASIC_AUTH_USER_PASSWORD_KEY).toString() == password) { + // this is username / password match - let this user in + return true; + } + } + } + } + } + + // basic HTTP auth being used but no username and password are present + // or the username and password are not correct + // send back a 401 and ask for basic auth + + const QByteArray HTTP_AUTH_REQUEST_HEADER_KEY = "WWW-Authenticate"; + static QString HTTP_AUTH_REALM_STRING = QString("Basic realm='%1 %2'") + .arg(_hostname.isEmpty() ? "localhost" : _hostname) + .arg("domain-server"); + + Headers basicAuthHeader; + basicAuthHeader.insert(HTTP_AUTH_REQUEST_HEADER_KEY, HTTP_AUTH_REALM_STRING.toUtf8()); + + connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY, + HTTPConnection::DefaultContentType, basicAuthHeader); + + // not authenticated, bubble up false + return false; + + } else { + // we don't have an OAuth URL + admin roles/usernames, so all users are authenticated + return true; + } +} + const QString OAUTH_JSON_ACCESS_TOKEN_KEY = "access_token"; void DomainServer::handleTokenRequestFinished() { @@ -1220,20 +1413,11 @@ void DomainServer::handleTokenRequestFinished() { QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply); if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) { - // pull the access token from the returned JSON and store it with the matching session UUID - QJsonDocument returnedJSON = QJsonDocument::fromJson(networkReply->readAll()); - QString accessToken = returnedJSON.object()[OAUTH_JSON_ACCESS_TOKEN_KEY].toString(); - - qDebug() << "Received access token for user with UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID); - - // fire off a request to get this user's identity so we can see if we will let them in - QUrl profileURL = _oauthProviderURL; - profileURL.setPath("/api/v1/users/profile"); - profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken)); - - QNetworkReply* profileReply = NetworkAccessManager::getInstance().get(QNetworkRequest(profileURL)); - - qDebug() << "Requesting access token for user with session UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID); + + qDebug() << "Received access token for user with UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID) + << "-" << "requesting profile."; + + QNetworkReply* profileReply = profileRequestGivenTokenReply(networkReply); connect(profileReply, &QNetworkReply::finished, this, &DomainServer::handleProfileRequestFinished); @@ -1241,6 +1425,19 @@ void DomainServer::handleTokenRequestFinished() { } } +QNetworkReply* DomainServer::profileRequestGivenTokenReply(QNetworkReply* tokenReply) { + // pull the access token from the returned JSON and store it with the matching session UUID + QJsonDocument returnedJSON = QJsonDocument::fromJson(tokenReply->readAll()); + QString accessToken = returnedJSON.object()[OAUTH_JSON_ACCESS_TOKEN_KEY].toString(); + + // fire off a request to get this user's identity so we can see if we will let them in + QUrl profileURL = _oauthProviderURL; + profileURL.setPath("/api/v1/users/profile"); + profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken)); + + return NetworkAccessManager::getInstance().get(QNetworkRequest(profileURL)); +} + void DomainServer::handleProfileRequestFinished() { QNetworkReply* networkReply = reinterpret_cast(sender()); QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply); @@ -1254,26 +1451,83 @@ void DomainServer::handleProfileRequestFinished() { QJsonArray allowedRolesArray = _argumentVariantMap.value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray(); - bool shouldAllowUserToConnect = false; + QString connectableUsername; + QString profileUsername = profileJSON.object()["data"].toObject()["user"].toObject()["username"].toString(); foreach(const QJsonValue& roleValue, userRolesArray) { if (allowedRolesArray.contains(roleValue)) { // the user has a role that lets them in // set the bool to true and break - shouldAllowUserToConnect = true; + connectableUsername = profileUsername; break; } } - - qDebug() << "Confirmed authentication state for user" << uuidStringWithoutCurlyBraces(matchingSessionUUID) - << "-" << shouldAllowUserToConnect; + + if (connectableUsername.isEmpty()) { + qDebug() << "User" << profileUsername << "with session UUID" + << uuidStringWithoutCurlyBraces(matchingSessionUUID) + << "does not have an allowable role. Refusing connection."; + } else { + qDebug() << "User" << profileUsername << "with session UUID" + << uuidStringWithoutCurlyBraces(matchingSessionUUID) + << "has an allowable role. Can connect."; + } // insert this UUID and a flag that indicates if they are allowed to connect - _sessionAuthenticationHash.insert(matchingSessionUUID, shouldAllowUserToConnect); + _sessionAuthenticationHash.insert(matchingSessionUUID, connectableUsername); } } } + +const QString DS_SETTINGS_SESSIONS_GROUP = "web-sessions"; + +Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileReply) { + Headers cookieHeaders; + + // create a UUID for this cookie + QUuid cookieUUID = QUuid::createUuid(); + + QJsonDocument profileDocument = QJsonDocument::fromJson(profileReply->readAll()); + QJsonObject userObject = profileDocument.object()["data"].toObject()["user"].toObject(); + + // add the profile to our in-memory data structure so we know who the user is when they send us their cookie + DomainServerWebSessionData sessionData(userObject); + _cookieSessionHash.insert(cookieUUID, sessionData); + + // persist the cookie to settings file so we can get it back on DS relaunch + QSettings localSettings; + localSettings.beginGroup(DS_SETTINGS_SESSIONS_GROUP); + QVariant sessionVariant = QVariant::fromValue(sessionData); + localSettings.setValue(cookieUUID.toString(), QVariant::fromValue(sessionData)); + + // setup expiry for cookie to 1 month from today + QDateTime cookieExpiry = QDateTime::currentDateTimeUtc().addMonths(1); + + QString cookieString = HIFI_SESSION_COOKIE_KEY + "=" + uuidStringWithoutCurlyBraces(cookieUUID.toString()); + cookieString += "; expires=" + cookieExpiry.toString("ddd, dd MMM yyyy HH:mm:ss") + " GMT"; + cookieString += "; domain=" + _hostname + "; path=/"; + + cookieHeaders.insert("Set-Cookie", cookieString.toUtf8()); + + // redirect the user back to the homepage so they can present their cookie and be authenticated + QString redirectString = "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()); + cookieHeaders.insert("Location", redirectString.toUtf8()); + + return cookieHeaders; +} + +void DomainServer::loadExistingSessionsFromSettings() { + // read data for existing web sessions into memory so existing sessions can be leveraged + QSettings domainServerSettings; + domainServerSettings.beginGroup(DS_SETTINGS_SESSIONS_GROUP); + + foreach(const QString& uuidKey, domainServerSettings.childKeys()) { + _cookieSessionHash.insert(QUuid(uuidKey), domainServerSettings.value(uuidKey).value()); + qDebug() << "Pulled web session from settings - cookie UUID is" << uuidKey; + } +} + void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment) { QUuid oldUUID = assignment->getUUID(); assignment->resetUUID(); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index cc44bd95a8..666994c818 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -25,6 +25,7 @@ #include #include "DomainServerSettingsManager.h" +#include "DomainServerWebSessionData.h" #include "WalletTransaction.h" #include "PendingAssignedNodeData.h" @@ -85,8 +86,14 @@ private: QUrl oauthRedirectURL(); QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid()); + bool isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url); + void handleTokenRequestFinished(); + QNetworkReply* profileRequestGivenTokenReply(QNetworkReply* tokenReply); void handleProfileRequestFinished(); + Headers setupCookieHeadersFromProfileReply(QNetworkReply* profileReply); + + void loadExistingSessionsFromSettings(); QJsonObject jsonForSocket(const HifiSockAddr& socket); QJsonObject jsonObjectForNode(const SharedNodePointer& node); @@ -108,7 +115,10 @@ private: QString _oauthClientSecret; QString _hostname; QMap _networkReplyUUIDMap; - QHash _sessionAuthenticationHash; + QHash _sessionAuthenticationHash; + + QSet _webAuthenticationStateSet; + QHash _cookieSessionHash; DomainServerSettingsManager _settingsManager; }; diff --git a/domain-server/src/DomainServerNodeData.cpp b/domain-server/src/DomainServerNodeData.cpp index 68903cc106..fd95ea9a67 100644 --- a/domain-server/src/DomainServerNodeData.cpp +++ b/domain-server/src/DomainServerNodeData.cpp @@ -21,6 +21,7 @@ DomainServerNodeData::DomainServerNodeData() : _sessionSecretHash(), _assignmentUUID(), _walletUUID(), + _username(), _paymentIntervalTimer(), _statsJSONObject(), _sendingSockAddr(), diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index a7d7233874..366ee8c730 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -35,6 +35,9 @@ public: void setWalletUUID(const QUuid& walletUUID) { _walletUUID = walletUUID; } const QUuid& getWalletUUID() const { return _walletUUID; } + void setUsername(const QString& username) { _username = username; } + const QString& getUsername() const { return _username; } + QElapsedTimer& getPaymentIntervalTimer() { return _paymentIntervalTimer; } void setSendingSockAddr(const HifiSockAddr& sendingSockAddr) { _sendingSockAddr = sendingSockAddr; } @@ -50,6 +53,7 @@ private: QHash _sessionSecretHash; QUuid _assignmentUUID; QUuid _walletUUID; + QString _username; QElapsedTimer _paymentIntervalTimer; QJsonObject _statsJSONObject; HifiSockAddr _sendingSockAddr; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index f9996aa0e7..9c741b2a3b 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -47,24 +47,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() : const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; -bool DomainServerSettingsManager::handleHTTPRequest(HTTPConnection* connection, const QUrl &url) { - if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == "/settings.json") { - // this is a POST operation to change one or more settings - QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); - QJsonObject postedObject = postedDocument.object(); - - // we recurse one level deep below each group for the appropriate setting - recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionObject); - - // store whatever the current _settingsMap is to file - persistToFile(); - - // return success to the caller - QString jsonSuccess = "{\"status\": \"success\"}"; - connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); - - return true; - } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == "/settings.json") { +bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) { + if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == "/settings.json") { // this is a GET operation for our settings // check if there is a query parameter for settings affecting a particular type of assignment @@ -135,6 +119,30 @@ bool DomainServerSettingsManager::handleHTTPRequest(HTTPConnection* connection, return false; } +bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) { + if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == "/settings.json") { + // this is a POST operation to change one or more settings + QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); + QJsonObject postedObject = postedDocument.object(); + + // we recurse one level deep below each group for the appropriate setting + recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionObject); + + // store whatever the current _settingsMap is to file + persistToFile(); + + // return success to the caller + QString jsonSuccess = "{\"status\": \"success\"}"; + connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); + + return true; + } + + return false; +} + +const QString SETTING_DESCRIPTION_TYPE_KEY = "type"; + void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant, QJsonObject descriptionObject) { @@ -145,7 +153,17 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ // we don't continue if this key is not present in our descriptionObject if (descriptionObject.contains(key)) { if (rootValue.isString()) { - settingsVariant[key] = rootValue.toString(); + if (rootValue.toString().isEmpty()) { + // this is an empty value, clear it in settings variant so the default is sent + settingsVariant.remove(key); + } else { + if (descriptionObject[key].toObject().contains(SETTING_DESCRIPTION_TYPE_KEY)) { + // for now this means that this is a double, so set it as a double + settingsVariant[key] = rootValue.toString().toDouble(); + } else { + settingsVariant[key] = rootValue.toString(); + } + } } else if (rootValue.isBool()) { settingsVariant[key] = rootValue.toBool(); } else if (rootValue.isObject()) { @@ -158,9 +176,16 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ settingsVariant[key] = QVariantMap(); } + QVariantMap& thisMap = *reinterpret_cast(settingsVariant[key].data()); + recurseJSONObjectAndOverwriteSettings(rootValue.toObject(), - *reinterpret_cast(settingsVariant[key].data()), + thisMap, nextDescriptionObject[DESCRIPTION_SETTINGS_KEY].toObject()); + + if (thisMap.isEmpty()) { + // we've cleared all of the settings below this value, so remove this one too + settingsVariant.remove(key); + } } } } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 8b80cad280..26bfe57ab4 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -16,11 +16,12 @@ #include -class DomainServerSettingsManager : public QObject, HTTPRequestHandler { +class DomainServerSettingsManager : public QObject { Q_OBJECT public: DomainServerSettingsManager(); - bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url); + bool handlePublicHTTPRequest(HTTPConnection* connection, const QUrl& url); + bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url); QByteArray getJSONSettingsMap() const; private: diff --git a/domain-server/src/DomainServerWebSessionData.cpp b/domain-server/src/DomainServerWebSessionData.cpp new file mode 100644 index 0000000000..ee32a17570 --- /dev/null +++ b/domain-server/src/DomainServerWebSessionData.cpp @@ -0,0 +1,63 @@ +// +// DomainServerWebSessionData.cpp +// domain-server/src +// +// Created by Stephen Birarda on 2014-07-21. +// 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 + +#include "DomainServerWebSessionData.h" + +DomainServerWebSessionData::DomainServerWebSessionData() : + _username(), + _roles() +{ + +} + +DomainServerWebSessionData::DomainServerWebSessionData(const QJsonObject& userObject) : + _roles() +{ + _username = userObject["username"].toString(); + + // pull each of the roles and throw them into our set + foreach(const QJsonValue& rolesValue, userObject["roles"].toArray()) { + _roles.insert(rolesValue.toString()); + } +} + +DomainServerWebSessionData::DomainServerWebSessionData(const DomainServerWebSessionData& otherSessionData) { + _username = otherSessionData._username; + _roles = otherSessionData._roles; +} + +DomainServerWebSessionData& DomainServerWebSessionData::operator=(const DomainServerWebSessionData& otherSessionData) { + DomainServerWebSessionData temp(otherSessionData); + swap(temp); + return *this; +} + +void DomainServerWebSessionData::swap(DomainServerWebSessionData& otherSessionData) { + using std::swap; + + swap(_username, otherSessionData._username); + swap(_roles, otherSessionData._roles); +} + +QDataStream& operator<<(QDataStream &out, const DomainServerWebSessionData& session) { + out << session._username << session._roles; + return out; +} + +QDataStream& operator>>(QDataStream &in, DomainServerWebSessionData& session) { + in >> session._username >> session._roles; + return in; +} + diff --git a/domain-server/src/DomainServerWebSessionData.h b/domain-server/src/DomainServerWebSessionData.h new file mode 100644 index 0000000000..450bc13a9b --- /dev/null +++ b/domain-server/src/DomainServerWebSessionData.h @@ -0,0 +1,41 @@ +// +// DomainServerWebSessionData.h +// domain-server/src +// +// Created by Stephen Birarda on 2014-07-21. +// 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_DomainServerWebSessionData_h +#define hifi_DomainServerWebSessionData_h + +#include +#include + +class DomainServerWebSessionData : public QObject { + Q_OBJECT +public: + DomainServerWebSessionData(); + DomainServerWebSessionData(const QJsonObject& userObject); + DomainServerWebSessionData(const DomainServerWebSessionData& otherSessionData); + DomainServerWebSessionData& operator=(const DomainServerWebSessionData& otherSessionData); + + const QString& getUsername() const { return _username; } + const QSet& getRoles() const { return _roles; } + + friend QDataStream& operator<<(QDataStream &out, const DomainServerWebSessionData& session); + friend QDataStream& operator>>(QDataStream &in, DomainServerWebSessionData& session); + +private: + void swap(DomainServerWebSessionData& otherSessionData); + + QString _username; + QSet _roles; +}; + +Q_DECLARE_METATYPE(DomainServerWebSessionData) + +#endif // hifi_DomainServerWebSessionData_h \ No newline at end of file diff --git a/examples/Test.js b/examples/Test.js index 36dee7bd90..612c56d10b 100644 --- a/examples/Test.js +++ b/examples/Test.js @@ -59,6 +59,13 @@ UnitTest.prototype.assertEquals = function(expected, actual, message) { } }; +UnitTest.prototype.assertContains = function (expected, actual, message) { + this.numAssertions++; + if (actual.indexOf(expected) == -1) { + throw new AssertionException(expected, actual, message); + } +}; + UnitTest.prototype.assertHasProperty = function(property, actual, message) { this.numAssertions++; if (actual[property] === undefined) { diff --git a/examples/bot_procedural.js. b/examples/bot_procedural.js similarity index 100% rename from examples/bot_procedural.js. rename to examples/bot_procedural.js diff --git a/examples/clipboardExample.js b/examples/clipboardExample.js index e6db44054f..0b6371e2b7 100644 --- a/examples/clipboardExample.js +++ b/examples/clipboardExample.js @@ -24,6 +24,7 @@ function setupMenus() { Menu.removeMenuItem("Edit", "Paste"); Menu.removeMenuItem("Edit", "Delete"); Menu.removeMenuItem("Edit", "Nudge"); + Menu.removeMenuItem("Edit", "Replace from File"); Menu.removeMenuItem("File", "Export Voxels"); Menu.removeMenuItem("File", "Import Voxels"); @@ -32,6 +33,7 @@ function setupMenus() { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Copy", shortcutKey: "CTRL+C", afterItem: "Cut" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Paste", shortcutKey: "CTRL+V", afterItem: "Copy" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Nudge", shortcutKey: "CTRL+N", afterItem: "Paste" }); + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Replace from File", shortcutKey: "CTRL+R", afterItem: "Nudge" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Delete", shortcutKeyEvent: { text: "backspace" }, afterItem: "Nudge" }); Menu.addMenuItem({ menuName: "File", menuItemName: "Export Voxels", shortcutKey: "CTRL+E", afterItem: "Voxels" }); Menu.addMenuItem({ menuName: "File", menuItemName: "Import Voxels", shortcutKey: "CTRL+I", afterItem: "Export Voxels" }); @@ -60,7 +62,6 @@ function menuItemEvent(menuItem) { print("deleting..."); Clipboard.deleteVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); } - if (menuItem == "Export Voxels") { print("export"); Clipboard.exportVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); @@ -73,6 +74,12 @@ function menuItemEvent(menuItem) { print("nudge"); Clipboard.nudgeVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s, { x: -1, y: 0, z: 0 }); } + if (menuItem == "Replace from File") { + var filename = Window.browse("Select file to load replacement", "", "Voxel Files (*.png *.svo *.schematic)"); + if (filename) { + Clipboard.importVoxel(filename, selectedVoxel); + } + } } var selectCube = Overlays.addOverlay("cube", { diff --git a/examples/editModels.js b/examples/editModels.js index d430800748..411431791c 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -9,12 +9,12 @@ // // If using the hydras : // grab grab models with the triggers, you can then move the models around or scale them with both hands. -// You can switch mode using the bumpers so that you can move models roud more easily. +// You can switch mode using the bumpers so that you can move models around more easily. // // If using the mouse : // - left click lets you move the model in the plane facing you. -// If pressing shift, it will move on the horizontale plane it's in. -// - right click lets you rotate the model. z and x give you access to more axix of rotation while shift allows for finer control. +// If pressing shift, it will move on the horizontal plane it's in. +// - right click lets you rotate the model. z and x give access to more axes of rotation while shift provides finer control. // - left + right click lets you scale the model. // - you can press r while holding the model to reset its rotation // @@ -39,30 +39,1671 @@ var MAX_ANGULAR_SIZE = 45; var LEFT = 0; var RIGHT = 1; - var SPAWN_DISTANCE = 1; -var radiusDefault = 0.10; +var DEFAULT_RADIUS = 0.10; var modelURLs = [ - "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", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/newInvader16x16-large-purple.svo", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/minotaur/mino_full.fbx", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Combat_tank_V01.FBX", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/orc.fbx", - "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx", - ]; - -var toolBar; + "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", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/newInvader16x16-large-purple.svo", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/minotaur/mino_full.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Combat_tank_V01.FBX", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/orc.fbx", + "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx" + ]; var jointList = MyAvatar.getJointNames(); var mode = 0; +var isActive = false; + + +if (typeof String.prototype.fileName !== "function") { + String.prototype.fileName = function () { + return this.replace(/^(.*[\/\\])*/, ""); + }; +} + +if (typeof String.prototype.fileBase !== "function") { + String.prototype.fileBase = function () { + var filename = this.fileName(); + return filename.slice(0, filename.indexOf(".")); + }; +} + +if (typeof String.prototype.fileType !== "function") { + String.prototype.fileType = function () { + return this.slice(this.lastIndexOf(".") + 1); + }; +} + +if (typeof String.prototype.path !== "function") { + String.prototype.path = function () { + return this.replace(/[\\\/][^\\\/]*$/, ""); + }; +} + +if (typeof String.prototype.regExpEscape !== "function") { + String.prototype.regExpEscape = function () { + return this.replace(/([$\^.+*?|\\\/{}()\[\]])/g, '\\$1'); + }; +} + +if (typeof String.prototype.toArrayBuffer !== "function") { + String.prototype.toArrayBuffer = function () { + var length, + buffer, + view, + charCode, + charCodes, + i; + + charCodes = []; + + length = this.length; + for (i = 0; i < length; i += 1) { + charCode = this.charCodeAt(i); + if (charCode <= 255) { + charCodes.push(charCode); + } else { + charCodes.push(charCode / 256); + charCodes.push(charCode % 256); + } + } + + length = charCodes.length; + buffer = new ArrayBuffer(length); + view = new Uint8Array(buffer); + for (i = 0; i < length; i += 1) { + view[i] = charCodes[i]; + } + + return buffer; + }; +} + +if (typeof DataView.prototype.indexOf !== "function") { + DataView.prototype.indexOf = function (searchString, position) { + var searchLength = searchString.length, + byteArrayLength = this.byteLength, + maxSearchIndex = byteArrayLength - searchLength, + searchCharCodes = [], + found, + i, + j; + + searchCharCodes[searchLength] = 0; + for (j = 0; j < searchLength; j += 1) { + searchCharCodes[j] = searchString.charCodeAt(j); + } + + i = position; + found = false; + while (i < maxSearchIndex && !found) { + j = 0; + while (j < searchLength && this.getUint8(i + j) === searchCharCodes[j]) { + j += 1; + } + found = (j === searchLength); + i += 1; + } + + return found ? i - 1 : -1; + }; +} + +if (typeof DataView.prototype.string !== "function") { + DataView.prototype.string = function (start, length) { + var charCodes = [], + end, + i; + + if (start === undefined) { + start = 0; + } + if (length === undefined) { + length = this.length; + } + + end = start + length; + for (i = start; i < end; i += 1) { + charCodes.push(this.getUint8(i)); + } + + return String.fromCharCode.apply(String, charCodes); + }; +} + +var progressDialog = (function () { + var that = {}, + progressBackground, + progressMessage, + cancelButton, + displayed = false, + backgroundWidth = 300, + backgroundHeight = 100, + messageHeight = 32, + cancelWidth = 70, + cancelHeight = 32, + textColor = { red: 255, green: 255, blue: 255 }, + textBackground = { red: 52, green: 52, blue: 52 }, + backgroundUrl = toolIconUrl + "progress-background.svg", + windowDimensions; + + progressBackground = Overlays.addOverlay("image", { + width: backgroundWidth, + height: backgroundHeight, + imageURL: backgroundUrl, + alpha: 0.9, + visible: false + }); + + progressMessage = Overlays.addOverlay("text", { + width: backgroundWidth - 40, + height: messageHeight, + text: "", + textColor: textColor, + backgroundColor: textBackground, + alpha: 0.9, + visible: false + }); + + cancelButton = Overlays.addOverlay("text", { + width: cancelWidth, + height: cancelHeight, + text: "Cancel", + textColor: textColor, + backgroundColor: textBackground, + alpha: 0.9, + visible: false + }); + + function move() { + var progressX, + progressY; + + if (displayed) { + + if (windowDimensions.x === Window.innerWidth && windowDimensions.y === Window.innerHeight) { + return; + } + windowDimensions.x = Window.innerWidth; + windowDimensions.y = Window.innerHeight; + + progressX = (windowDimensions.x - backgroundWidth) / 2; // Center. + progressY = windowDimensions.y / 2 - backgroundHeight; // A little up from center. + + Overlays.editOverlay(progressBackground, { x: progressX, y: progressY }); + Overlays.editOverlay(progressMessage, { x: progressX + 20, y: progressY + 15 }); + Overlays.editOverlay(cancelButton, { + x: progressX + backgroundWidth - cancelWidth - 20, + y: progressY + backgroundHeight - cancelHeight - 15 + }); + } + } + that.move = move; + + that.onCancel = undefined; + + function open(message) { + if (!displayed) { + windowDimensions = { x: 0, y : 0 }; + displayed = true; + move(); + Overlays.editOverlay(progressBackground, { visible: true }); + Overlays.editOverlay(progressMessage, { visible: true, text: message }); + Overlays.editOverlay(cancelButton, { visible: true }); + } else { + throw new Error("open() called on progressDialog when already open"); + } + } + that.open = open; + + function isOpen() { + return displayed; + } + that.isOpen = isOpen; + + function update(message) { + if (displayed) { + Overlays.editOverlay(progressMessage, { text: message }); + } else { + throw new Error("update() called on progressDialog when not open"); + } + } + that.update = update; + + function close() { + if (displayed) { + Overlays.editOverlay(cancelButton, { visible: false }); + Overlays.editOverlay(progressMessage, { visible: false }); + Overlays.editOverlay(progressBackground, { visible: false }); + displayed = false; + } else { + throw new Error("close() called on progressDialog when not open"); + } + } + that.close = close; + + function mousePressEvent(event) { + if (Overlays.getOverlayAtPoint({ x: event.x, y: event.y }) === cancelButton) { + if (typeof this.onCancel === "function") { + close(); + this.onCancel(); + } + return true; + } + return false; + } + that.mousePressEvent = mousePressEvent; + + function cleanup() { + Overlays.deleteOverlay(cancelButton); + Overlays.deleteOverlay(progressMessage); + Overlays.deleteOverlay(progressBackground); + } + that.cleanup = cleanup; + + return that; +}()); + +var httpMultiPart = (function () { + var that = {}, + parts, + byteLength, + boundaryString, + crlf; + + function clear() { + boundaryString = "--boundary_" + String(Uuid.generate()).slice(1, 36) + "="; + parts = []; + byteLength = 0; + crlf = ""; + } + that.clear = clear; + + function boundary() { + return boundaryString.slice(2); + } + that.boundary = boundary; + + function length() { + return byteLength; + } + that.length = length; + + function add(object) { + // - name, string + // - name, buffer + var buffer, + string, + stringBuffer, + compressedBuffer; + + if (object.name === undefined) { + + throw new Error("Item to add to HttpMultiPart must have a name"); + + } else if (object.string !== undefined) { + //--= + //Content-Disposition: form-data; name="model_name" + // + // + + string = crlf + boundaryString + "\r\n" + + "Content-Disposition: form-data; name=\"" + object.name + "\"\r\n" + + "\r\n" + + object.string; + buffer = string.toArrayBuffer(); + + } else if (object.buffer !== undefined) { + //--= + //Content-Disposition: form-data; name="fbx"; filename="" + //Content-Type: application/octet-stream + // + // + + string = crlf + boundaryString + "\r\n" + + "Content-Disposition: form-data; name=\"" + object.name + + "\"; filename=\"" + object.buffer.filename + "\"\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n"; + stringBuffer = string.toArrayBuffer(); + + compressedBuffer = object.buffer.buffer.compress(); + buffer = new Uint8Array(stringBuffer.byteLength + compressedBuffer.byteLength); + buffer.set(new Uint8Array(stringBuffer)); + buffer.set(new Uint8Array(compressedBuffer), stringBuffer.byteLength); + + } else { + + throw new Error("Item to add to HttpMultiPart not recognized"); + } + + byteLength += buffer.byteLength; + parts.push(buffer); + + crlf = "\r\n"; + + return true; + } + that.add = add; + + function response() { + var buffer, + index, + str, + i; + + str = crlf + boundaryString + "--\r\n"; + buffer = str.toArrayBuffer(); + byteLength += buffer.byteLength; + parts.push(buffer); + + buffer = new Uint8Array(byteLength); + index = 0; + for (i = 0; i < parts.length; i += 1) { + buffer.set(new Uint8Array(parts[i]), index); + index += parts[i].byteLength; + } + + return buffer; + } + that.response = response; + + clear(); + + return that; +}()); + +var modelUploader = (function () { + var that = {}, + modelFile, + modelName, + modelURL, + modelCallback, + isProcessing, + fstBuffer, + fbxBuffer, + //svoBuffer, + mapping, + geometry, + API_URL = "https://data.highfidelity.io/api/v1/models", + MODEL_URL = "http://public.highfidelity.io/models/content", + NAME_FIELD = "name", + SCALE_FIELD = "scale", + FILENAME_FIELD = "filename", + TEXDIR_FIELD = "texdir", + MAX_TEXTURE_SIZE = 1024; + + function info(message) { + if (progressDialog.isOpen()) { + progressDialog.update(message); + } else { + progressDialog.open(message); + } + print(message); + } + + function error(message) { + if (progressDialog.isOpen()) { + progressDialog.close(); + } + print(message); + Window.alert(message); + } + + function randomChar(length) { + var characters = "0123457689abcdefghijklmnopqrstuvwxyz", + string = "", + i; + + for (i = 0; i < length; i += 1) { + string += characters[Math.floor(Math.random() * 36)]; + } + + return string; + } + + function resetDataObjects() { + fstBuffer = null; + fbxBuffer = null; + //svoBuffer = null; + mapping = {}; + geometry = {}; + geometry.textures = []; + geometry.embedded = []; + } + + function readFile(filename) { + var url = "file:///" + filename, + req = new XMLHttpRequest(); + + req.open("GET", url, false); + req.responseType = "arraybuffer"; + req.send(); + if (req.status !== 200) { + error("Could not read file: " + filename + " : " + req.statusText); + return null; + } + + return { + filename: filename.fileName(), + buffer: req.response + }; + } + + function readMapping(buffer) { + var dv = new DataView(buffer.buffer), + lines, + line, + tokens, + i, + name, + value, + remainder, + existing; + + mapping = {}; // { name : value | name : { value : [remainder] } } + lines = dv.string(0, dv.byteLength).split(/\r\n|\r|\n/); + for (i = 0; i < lines.length; i += 1) { + line = lines[i].trim(); + if (line.length > 0 && line[0] !== "#") { + tokens = line.split(/\s*=\s*/); + if (tokens.length > 1) { + name = tokens[0]; + value = tokens[1]; + if (tokens.length > 2) { + remainder = tokens.slice(2, tokens.length).join(" = "); + } else { + remainder = null; + } + if (tokens.length === 2 && mapping[name] === undefined) { + mapping[name] = value; + } else { + if (mapping[name] === undefined) { + mapping[name] = {}; + + } else if (typeof mapping[name] !== "object") { + existing = mapping[name]; + mapping[name] = { existing : null }; + } + + if (mapping[name][value] === undefined) { + mapping[name][value] = []; + } + mapping[name][value].push(remainder); + } + } + } + } + } + + function writeMapping(buffer) { + var name, + value, + remainder, + i, + string = ""; + + for (name in mapping) { + if (mapping.hasOwnProperty(name)) { + if (typeof mapping[name] === "object") { + for (value in mapping[name]) { + if (mapping[name].hasOwnProperty(value)) { + remainder = mapping[name][value]; + if (remainder === null) { + string += (name + " = " + value + "\n"); + } else { + for (i = 0; i < remainder.length; i += 1) { + string += (name + " = " + value + " = " + remainder[i] + "\n"); + } + } + } + } + } else { + string += (name + " = " + mapping[name] + "\n"); + } + } + } + + buffer.buffer = string.toArrayBuffer(); + } + + function readGeometry(fbxBuffer) { + var textures, + view, + index, + EOF, + previousNodeFilename; + + // Reference: + // http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ + + textures = {}; + view = new DataView(fbxBuffer.buffer); + EOF = false; + + function parseBinaryFBX() { + var endOffset, + numProperties, + propertyListLength, + nameLength, + name, + filename; + + endOffset = view.getUint32(index, true); + numProperties = view.getUint32(index + 4, true); + propertyListLength = view.getUint32(index + 8, true); + nameLength = view.getUint8(index + 12); + index += 13; + + if (endOffset === 0) { + return; + } + if (endOffset < index || endOffset > view.byteLength) { + EOF = true; + return; + } + + name = view.string(index, nameLength).toLowerCase(); + index += nameLength; + + if (name === "content" && previousNodeFilename !== "") { + // Blender 2.71 exporter "embeds" external textures as empty binary blobs so ignore these + if (propertyListLength > 5) { + geometry.embedded.push(previousNodeFilename); + } + } + + if (name === "relativefilename") { + filename = view.string(index + 5, view.getUint32(index + 1, true)).fileName(); + if (!textures.hasOwnProperty(filename)) { + textures[filename] = ""; + geometry.textures.push(filename); + } + previousNodeFilename = filename; + } else { + previousNodeFilename = ""; + } + + index += (propertyListLength); + + while (index < endOffset && !EOF) { + parseBinaryFBX(); + } + } + + function readTextFBX() { + var line, + view, + viewLength, + charCode, + charCodes, + numCharCodes, + filename, + relativeFilename = "", + MAX_CHAR_CODES = 250; + + view = new Uint8Array(fbxBuffer.buffer); + viewLength = view.byteLength; + charCodes = []; + numCharCodes = 0; + + for (index = 0; index < viewLength; index += 1) { + charCode = view[index]; + if (charCode !== 9 && charCode !== 32) { + if (charCode === 10) { // EOL. Can ignore EOF. + line = String.fromCharCode.apply(String, charCodes).toLowerCase(); + // For embedded textures, "Content:" line immediately follows "RelativeFilename:" line. + if (line.slice(0, 8) === "content:" && relativeFilename !== "") { + geometry.embedded.push(relativeFilename); + } + if (line.slice(0, 17) === "relativefilename:") { + filename = line.slice(line.indexOf("\""), line.lastIndexOf("\"") - line.length).fileName(); + if (!textures.hasOwnProperty(filename)) { + textures[filename] = ""; + geometry.textures.push(filename); + } + relativeFilename = filename; + } else { + relativeFilename = ""; + } + charCodes = []; + numCharCodes = 0; + } else { + if (numCharCodes < MAX_CHAR_CODES) { // Only interested in start of line + charCodes.push(charCode); + numCharCodes += 1; + } + } + } + } + } + + if (view.string(0, 18) === "Kaydara FBX Binary") { + previousNodeFilename = ""; + + index = 27; + while (index < view.byteLength - 39 && !EOF) { + parseBinaryFBX(); + } + + } else { + + readTextFBX(); + + } + } + + function readModel() { + var fbxFilename, + //svoFilename, + fileType; + + info("Reading model file"); + print("Model file: " + modelFile); + + if (modelFile.toLowerCase().fileType() === "fst") { + fstBuffer = readFile(modelFile); + if (fstBuffer === null) { + return false; + } + readMapping(fstBuffer); + fileType = mapping[FILENAME_FIELD].toLowerCase().fileType(); + if (mapping.hasOwnProperty(FILENAME_FIELD)) { + if (fileType === "fbx") { + fbxFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; + //} else if (fileType === "svo") { + // svoFilename = modelFile.path() + "\\" + mapping[FILENAME_FIELD]; + } else { + error("Unrecognized model type in FST file!"); + return false; + } + } else { + error("Model file name not found in FST file!"); + return false; + } + } else { + fstBuffer = { + filename: "Interface." + randomChar(6), // Simulate avatar model uploading behaviour + buffer: null + }; + + if (modelFile.toLowerCase().fileType() === "fbx") { + fbxFilename = modelFile; + mapping[FILENAME_FIELD] = modelFile.fileName(); + + //} else if (modelFile.toLowerCase().fileType() === "svo") { + // svoFilename = modelFile; + // mapping[FILENAME_FIELD] = modelFile.fileName(); + + } else { + error("Unrecognized file type: " + modelFile); + return false; + } + } + + if (!isProcessing) { return false; } + + if (fbxFilename) { + fbxBuffer = readFile(fbxFilename); + if (fbxBuffer === null) { + return false; + } + + if (!isProcessing) { return false; } + + readGeometry(fbxBuffer); + } + + //if (svoFilename) { + // svoBuffer = readFile(svoFilename); + // if (svoBuffer === null) { + // return false; + // } + //} + + // Add any missing basic mappings + if (!mapping.hasOwnProperty(NAME_FIELD)) { + mapping[NAME_FIELD] = modelFile.fileName().fileBase(); + } + if (!mapping.hasOwnProperty(TEXDIR_FIELD)) { + mapping[TEXDIR_FIELD] = "."; + } + if (!mapping.hasOwnProperty(SCALE_FIELD)) { + mapping[SCALE_FIELD] = 1.0; + } + + return true; + } + + function setProperties() { + var form = [], + directory, + displayAs, + validateAs; + + progressDialog.close(); + print("Setting model properties"); + + form.push({ label: "Name:", value: mapping[NAME_FIELD] }); + + directory = modelFile.path() + "/" + mapping[TEXDIR_FIELD]; + displayAs = new RegExp("^" + modelFile.path().regExpEscape() + "[\\\\\\\/](.*)"); + validateAs = new RegExp("^" + modelFile.path().regExpEscape() + "([\\\\\\\/].*)?"); + + form.push({ + label: "Texture directory:", + directory: modelFile.path() + "/" + mapping[TEXDIR_FIELD], + title: "Choose Texture Directory", + displayAs: displayAs, + validateAs: validateAs, + errorMessage: "Texture directory must be subdirectory of the model directory." + }); + + form.push({ button: "Cancel" }); + + if (!Window.form("Set Model Properties", form)) { + print("User cancelled uploading model"); + return false; + } + + mapping[NAME_FIELD] = form[0].value; + mapping[TEXDIR_FIELD] = form[1].directory.slice(modelFile.path().length + 1); + if (mapping[TEXDIR_FIELD] === "") { + mapping[TEXDIR_FIELD] = "."; + } + + writeMapping(fstBuffer); + + return true; + } + + function createHttpMessage(callback) { + var multiparts = [], + lodCount, + lodFile, + lodBuffer, + textureBuffer, + textureSourceFormat, + textureTargetFormat, + embeddedTextures, + i; + + info("Preparing to send model"); + + // Model name + if (mapping.hasOwnProperty(NAME_FIELD)) { + multiparts.push({ + name : "model_name", + string : mapping[NAME_FIELD] + }); + } else { + error("Model name is missing"); + httpMultiPart.clear(); + return; + } + + // FST file + if (fstBuffer) { + multiparts.push({ + name : "fst", + buffer: fstBuffer + }); + } + + // FBX file + if (fbxBuffer) { + multiparts.push({ + name : "fbx", + buffer: fbxBuffer + }); + } + + // SVO file + //if (svoBuffer) { + // multiparts.push({ + // name : "svo", + // buffer: svoBuffer + // }); + //} + + // LOD files + lodCount = 0; + for (lodFile in mapping.lod) { + if (mapping.lod.hasOwnProperty(lodFile)) { + lodBuffer = readFile(modelFile.path() + "\/" + lodFile); + if (lodBuffer === null) { + return; + } + multiparts.push({ + name: "lod" + lodCount, + buffer: lodBuffer + }); + lodCount += 1; + } + if (!isProcessing) { return; } + } + + // Textures + embeddedTextures = "|" + geometry.embedded.join("|") + "|"; + for (i = 0; i < geometry.textures.length; i += 1) { + if (embeddedTextures.indexOf("|" + geometry.textures[i].fileName() + "|") === -1) { + textureBuffer = readFile(modelFile.path() + "\/" + + (mapping[TEXDIR_FIELD] !== "." ? mapping[TEXDIR_FIELD] + "\/" : "") + + geometry.textures[i]); + if (textureBuffer === null) { + return; + } + + textureSourceFormat = geometry.textures[i].fileType().toLowerCase(); + textureTargetFormat = (textureSourceFormat === "jpg" ? "jpg" : "png"); + textureBuffer.buffer = + textureBuffer.buffer.recodeImage(textureSourceFormat, textureTargetFormat, MAX_TEXTURE_SIZE); + textureBuffer.filename = textureBuffer.filename.slice(0, -textureSourceFormat.length) + textureTargetFormat; + + multiparts.push({ + name: "texture" + i, + buffer: textureBuffer + }); + } + + if (!isProcessing) { return; } + } + + // Model category + multiparts.push({ + name : "model_category", + string : "content" + }); + + // Create HTTP message + httpMultiPart.clear(); + Script.setTimeout(function addMultipart() { + var multipart = multiparts.shift(); + httpMultiPart.add(multipart); + + if (!isProcessing) { return; } + + if (multiparts.length > 0) { + Script.setTimeout(addMultipart, 25); + } else { + callback(); + } + }, 25); + } + + function sendToHighFidelity() { + var req, + uploadedChecks, + HTTP_GET_TIMEOUT = 60, // 1 minute + HTTP_SEND_TIMEOUT = 900, // 15 minutes + UPLOADED_CHECKS = 30, + CHECK_UPLOADED_TIMEOUT = 1, // 1 second + handleCheckUploadedResponses, + handleUploadModelResponses, + handleRequestUploadResponses; + + function uploadTimedOut() { + error("Model upload failed: Internet request timed out!"); + } + + //function debugResponse() { + // print("req.errorCode = " + req.errorCode); + // print("req.readyState = " + req.readyState); + // print("req.status = " + req.status); + // print("req.statusText = " + req.statusText); + // print("req.responseType = " + req.responseType); + // print("req.responseText = " + req.responseText); + // print("req.response = " + req.response); + // print("req.getAllResponseHeaders() = " + req.getAllResponseHeaders()); + //} + + function checkUploaded() { + if (!isProcessing) { return; } + + info("Checking uploaded model"); + + req = new XMLHttpRequest(); + req.open("HEAD", modelURL, true); + req.timeout = HTTP_GET_TIMEOUT * 1000; + req.onreadystatechange = handleCheckUploadedResponses; + req.ontimeout = uploadTimedOut; + req.send(); + } + + handleCheckUploadedResponses = function () { + //debugResponse(); + if (req.readyState === req.DONE) { + if (req.status === 200) { + // Note: Unlike avatar models, for content models we don't need to refresh texture cache. + print("Model uploaded: " + modelURL); + progressDialog.close(); + if (Window.confirm("Your model has been uploaded as: " + modelURL + "\nDo you want to rez it?")) { + modelCallback(modelURL); + } + } else if (req.status === 404) { + if (uploadedChecks > 0) { + uploadedChecks -= 1; + Script.setTimeout(checkUploaded, CHECK_UPLOADED_TIMEOUT * 1000); + } else { + print("Error: " + req.status + " " + req.statusText); + error("We could not verify that your model was successfully uploaded but it may have been at: " + + modelURL); + } + } else { + print("Error: " + req.status + " " + req.statusText); + error("There was a problem with your upload, please try again later."); + } + } + }; + + function uploadModel(method) { + var url; + + if (!isProcessing) { return; } + + req = new XMLHttpRequest(); + if (method === "PUT") { + url = API_URL + "\/" + modelName; + req.open("PUT", url, true); //print("PUT " + url); + } else { + url = API_URL; + req.open("POST", url, true); //print("POST " + url); + } + req.setRequestHeader("Content-Type", "multipart/form-data; boundary=\"" + httpMultiPart.boundary() + "\""); + req.timeout = HTTP_SEND_TIMEOUT * 1000; + req.onreadystatechange = handleUploadModelResponses; + req.ontimeout = uploadTimedOut; + req.send(httpMultiPart.response().buffer); + } + + handleUploadModelResponses = function () { + //debugResponse(); + if (req.readyState === req.DONE) { + if (req.status === 200) { + uploadedChecks = UPLOADED_CHECKS; + checkUploaded(); + } else { + print("Error: " + req.status + " " + req.statusText); + error("There was a problem with your upload, please try again later."); + } + } + }; + + function requestUpload() { + var url; + + if (!isProcessing) { return; } + + url = API_URL + "\/" + modelName; // XMLHttpRequest automatically handles authorization of API requests. + req = new XMLHttpRequest(); + req.open("GET", url, true); //print("GET " + url); + req.responseType = "json"; + req.timeout = HTTP_GET_TIMEOUT * 1000; + req.onreadystatechange = handleRequestUploadResponses; + req.ontimeout = uploadTimedOut; + req.send(); + } + + handleRequestUploadResponses = function () { + var response; + + //debugResponse(); + if (req.readyState === req.DONE) { + if (req.status === 200) { + if (req.responseType === "json") { + response = JSON.parse(req.responseText); + if (response.status === "success") { + if (response.exists === false) { + uploadModel("POST"); + } else if (response.can_update === true) { + uploadModel("PUT"); + } else { + error("This model file already exists and is owned by someone else!"); + } + return; + } + } + } else { + print("Error: " + req.status + " " + req.statusText); + } + error("Model upload failed! Something went wrong at the data server."); + } + }; + + info("Sending model to High Fidelity"); + + requestUpload(); + } + + that.upload = function (file, callback) { + + modelFile = file; + modelCallback = callback; + + isProcessing = true; + + progressDialog.onCancel = function () { + print("User cancelled uploading model"); + isProcessing = false; + }; + + resetDataObjects(); + + if (readModel()) { + if (setProperties()) { + modelName = mapping[NAME_FIELD]; + modelURL = MODEL_URL + "\/" + mapping[NAME_FIELD] + ".fst"; // All models are uploaded as an FST + + createHttpMessage(sendToHighFidelity); + } + } + + resetDataObjects(); + }; + + return that; +}()); + +var toolBar = (function () { + var that = {}, + toolBar, + activeButton, + newModelButton, + browseModelsButton, + loadURLMenuItem, + loadFileMenuItem, + menuItemWidth = 90, + menuItemOffset, + menuItemHeight, + menuItemMargin = 5, + menuTextColor = { red: 255, green: 255, blue: 255 }, + menuBackgoundColor = { red: 18, green: 66, blue: 66 }; + + function initialize() { + toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); + + activeButton = toolBar.addTool({ + imageURL: toolIconUrl + "models-tool.svg", + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + alpha: 0.9, + visible: true + }, true, false); + + newModelButton = toolBar.addTool({ + imageURL: toolIconUrl + "add-model-tool.svg", + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + alpha: 0.9, + visible: true + }, true, false); + + browseModelsButton = toolBar.addTool({ + imageURL: toolIconUrl + "list-icon.png", + width: toolWidth, + height: toolHeight, + alpha: 0.7, + visible: true + }); + + menuItemOffset = toolBar.height / 3 + 2; + menuItemHeight = Tool.IMAGE_HEIGHT / 2 - 2; + + loadURLMenuItem = Overlays.addOverlay("text", { + x: newModelButton.x - menuItemWidth, + y: newModelButton.y + menuItemOffset, + width: menuItemWidth, + height: menuItemHeight, + backgroundColor: menuBackgoundColor, + topMargin: menuItemMargin, + text: "Model URL", + alpha: 0.9, + visible: false + }); + + loadFileMenuItem = Overlays.addOverlay("text", { + x: newModelButton.x - menuItemWidth, + y: newModelButton.y + menuItemOffset + menuItemHeight, + width: menuItemWidth, + height: menuItemHeight, + backgroundColor: menuBackgoundColor, + topMargin: menuItemMargin, + text: "Model File", + alpha: 0.9, + visible: false + }); + } + + function toggleNewModelButton(active) { + if (active === undefined) { + active = !toolBar.toolSelected(newModelButton); + } + toolBar.selectTool(newModelButton, active); + + Overlays.editOverlay(loadURLMenuItem, { visible: active }); + Overlays.editOverlay(loadFileMenuItem, { visible: active }); + } + + function addModel(url) { + var position; + + position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); + + if (position.x > 0 && position.y > 0 && position.z > 0) { + Models.addModel({ + position: position, + radius: DEFAULT_RADIUS, + modelURL: url + }); + print("Model added: " + url); + } else { + print("Can't add model: Model would be out of bounds."); + } + } + + that.move = function () { + var newViewPort, + toolsX, + toolsY; + + newViewPort = Controller.getViewportDimensions(); + + if (toolBar === undefined) { + initialize(); + + } else if (windowDimensions.x === newViewPort.x && + windowDimensions.y === newViewPort.y) { + return; + } + + windowDimensions = newViewPort; + toolsX = windowDimensions.x - 8 - toolBar.width; + toolsY = (windowDimensions.y - toolBar.height) / 2; + + toolBar.move(toolsX, toolsY); + + Overlays.editOverlay(loadURLMenuItem, { x: toolsX - menuItemWidth, y: toolsY + menuItemOffset }); + Overlays.editOverlay(loadFileMenuItem, { x: toolsX - menuItemWidth, y: toolsY + menuItemOffset + menuItemHeight }); + }; + + that.mousePressEvent = function (event) { + var clickedOverlay, + url, + file; + + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + if (activeButton === toolBar.clicked(clickedOverlay)) { + isActive = !isActive; + return true; + } + + if (newModelButton === toolBar.clicked(clickedOverlay)) { + toggleNewModelButton(); + return true; + } + + if (clickedOverlay === loadURLMenuItem) { + toggleNewModelButton(false); + url = Window.prompt("Model URL", modelURLs[Math.floor(Math.random() * modelURLs.length)]); + if (url !== null && url !== "") { + addModel(url); + } + return true; + } + + if (clickedOverlay === loadFileMenuItem) { + toggleNewModelButton(false); + file = Window.browse("Select your model file ...", + Settings.getValue("LastModelUploadLocation").path(), + "Model files (*.fst *.fbx)"); + //"Model files (*.fst *.fbx *.svo)"); + if (file !== null) { + Settings.setValue("LastModelUploadLocation", file); + modelUploader.upload(file, addModel); + } + return true; + } + + if (browseModelsButton === toolBar.clicked(clickedOverlay)) { + toggleNewModelButton(false); + url = Window.s3Browse(".*(fbx|FBX)"); + if (url !== null && url !== "") { + addModel(url); + } + return true; + } + + return false; + }; + + that.cleanup = function () { + toolBar.cleanup(); + Overlays.deleteOverlay(loadURLMenuItem); + Overlays.deleteOverlay(loadFileMenuItem); + }; + + return that; +}()); + + +var exportMenu = null; + +var ExportMenu = function (opts) { + var self = this; + + var windowDimensions = Controller.getViewportDimensions(); + var pos = { x: windowDimensions.x / 2, y: windowDimensions.y - 100 }; + + this._onClose = opts.onClose || function () { }; + this._position = { x: 0.0, y: 0.0, z: 0.0 }; + this._scale = 1.0; + + var minScale = 1; + var maxScale = 32768; + var titleWidth = 120; + var locationWidth = 100; + var scaleWidth = 144; + var exportWidth = 100; + var cancelWidth = 100; + var margin = 4; + var height = 30; + var outerHeight = height + (2 * margin); + var buttonColor = { red: 128, green: 128, blue: 128 }; + + var SCALE_MINUS = scaleWidth * 40.0 / 100.0; + var SCALE_PLUS = scaleWidth * 63.0 / 100.0; + + var fullWidth = locationWidth + scaleWidth + exportWidth + cancelWidth + (2 * margin); + var offset = fullWidth / 2; + pos.x -= offset; + + var background = Overlays.addOverlay("text", { + x: pos.x, + y: pos.y, + opacity: 1, + width: fullWidth, + height: outerHeight, + backgroundColor: { red: 200, green: 200, blue: 200 }, + text: "", + }); + + var titleText = Overlays.addOverlay("text", { + x: pos.x, + y: pos.y - height, + font: { size: 14 }, + width: titleWidth, + height: height, + backgroundColor: { red: 255, green: 255, blue: 255 }, + color: { red: 255, green: 255, blue: 255 }, + text: "Export Models" + }); + + var locationButton = Overlays.addOverlay("text", { + x: pos.x + margin, + y: pos.y + margin, + width: locationWidth, + height: height, + color: { red: 255, green: 255, blue: 255 }, + text: "0, 0, 0", + }); + var scaleOverlay = Overlays.addOverlay("image", { + x: pos.x + margin + locationWidth, + y: pos.y + margin, + width: scaleWidth, + height: height, + subImage: { x: 0, y: 3, width: 144, height: height }, + imageURL: toolIconUrl + "voxel-size-selector.svg", + alpha: 0.9, + }); + var scaleViewWidth = 40; + var scaleView = Overlays.addOverlay("text", { + x: pos.x + margin + locationWidth + SCALE_MINUS, + y: pos.y + margin, + width: scaleViewWidth, + height: height, + alpha: 0.0, + color: { red: 255, green: 255, blue: 255 }, + text: "1" + }); + var exportButton = Overlays.addOverlay("text", { + x: pos.x + margin + locationWidth + scaleWidth, + y: pos.y + margin, + width: exportWidth, + height: height, + color: { red: 0, green: 255, blue: 255 }, + text: "Export" + }); + var cancelButton = Overlays.addOverlay("text", { + x: pos.x + margin + locationWidth + scaleWidth + exportWidth, + y: pos.y + margin, + width: cancelWidth, + height: height, + color: { red: 255, green: 255, blue: 255 }, + text: "Cancel" + }); + + var voxelPreview = Overlays.addOverlay("cube", { + position: { x: 0, y: 0, z: 0 }, + size: this._scale, + color: { red: 255, green: 255, blue: 0 }, + alpha: 1, + solid: false, + visible: true, + lineWidth: 4 + }); + + this.parsePosition = function (str) { + var parts = str.split(','); + if (parts.length == 3) { + var x = parseFloat(parts[0]); + var y = parseFloat(parts[1]); + var z = parseFloat(parts[2]); + if (isFinite(x) && isFinite(y) && isFinite(z)) { + return { x: x, y: y, z: z }; + } + } + return null; + }; + + this.showPositionPrompt = function () { + var positionStr = self._position.x + ", " + self._position.y + ", " + self._position.z; + while (1) { + positionStr = Window.prompt("Position to export form:", positionStr); + if (positionStr == null) { + break; + } + var position = self.parsePosition(positionStr); + if (position != null) { + self.setPosition(position.x, position.y, position.z); + break; + } + Window.alert("The position you entered was invalid."); + } + }; + + this.setScale = function (scale) { + self._scale = Math.min(maxScale, Math.max(minScale, scale)); + Overlays.editOverlay(scaleView, { text: self._scale }); + Overlays.editOverlay(voxelPreview, { size: self._scale }); + } + + this.decreaseScale = function () { + self.setScale(self._scale /= 2); + } + + this.increaseScale = function () { + self.setScale(self._scale *= 2); + } + + this.exportModels = function () { + var x = self._position.x; + var y = self._position.y; + var z = self._position.z; + var s = self._scale; + var filename = "models__" + Window.location.hostname + "__" + x + "_" + y + "_" + z + "_" + s + "__.svo"; + filename = Window.save("Select where to save", filename, "*.svo") + if (filename) { + var success = Clipboard.exportModels(filename, x, y, z, s); + if (!success) { + Window.alert("Export failed: no models found in selected area."); + } + } + self.close(); + }; + + this.getPosition = function () { + return self._position; + }; + + this.setPosition = function (x, y, z) { + self._position = { x: x, y: y, z: z }; + var positionStr = x + ", " + y + ", " + z; + Overlays.editOverlay(locationButton, { text: positionStr }); + Overlays.editOverlay(voxelPreview, { position: self._position }); + + }; + + this.mouseReleaseEvent = function (event) { + var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + if (clickedOverlay == locationButton) { + self.showPositionPrompt(); + } else if (clickedOverlay == exportButton) { + self.exportModels(); + } else if (clickedOverlay == cancelButton) { + self.close(); + } else if (clickedOverlay == scaleOverlay) { + var x = event.x - pos.x - margin - locationWidth; + print(x); + if (x < SCALE_MINUS) { + self.decreaseScale(); + } else if (x > SCALE_PLUS) { + self.increaseScale(); + } + } + }; + + this.close = function () { + this.cleanup(); + this._onClose(); + }; + + this.cleanup = function () { + Overlays.deleteOverlay(background); + Overlays.deleteOverlay(titleText); + Overlays.deleteOverlay(locationButton); + Overlays.deleteOverlay(exportButton); + Overlays.deleteOverlay(cancelButton); + Overlays.deleteOverlay(voxelPreview); + Overlays.deleteOverlay(scaleOverlay); + Overlays.deleteOverlay(scaleView); + }; + + print("CONNECTING!"); + Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent); +}; + +var ModelImporter = function (opts) { + var self = this; + + var height = 30; + var margin = 4; + var outerHeight = height + (2 * margin); + var titleWidth = 120; + var cancelWidth = 100; + var fullWidth = titleWidth + cancelWidth + (2 * margin); + + var localModels = Overlays.addOverlay("localmodels", { + position: { x: 1, y: 1, z: 1 }, + scale: 1, + visible: false + }); + var importScale = 1; + var importBoundaries = Overlays.addOverlay("cube", { + position: { x: 0, y: 0, z: 0 }, + size: 1, + color: { red: 128, blue: 128, green: 128 }, + lineWidth: 4, + solid: false, + visible: false + }); + + var pos = { x: windowDimensions.x / 2 - (fullWidth / 2), y: windowDimensions.y - 100 }; + + var background = Overlays.addOverlay("text", { + x: pos.x, + y: pos.y, + opacity: 1, + width: fullWidth, + height: outerHeight, + backgroundColor: { red: 200, green: 200, blue: 200 }, + visible: false, + text: "", + }); + + var titleText = Overlays.addOverlay("text", { + x: pos.x + margin, + y: pos.y + margin, + font: { size: 14 }, + width: titleWidth, + height: height, + backgroundColor: { red: 255, green: 255, blue: 255 }, + color: { red: 255, green: 255, blue: 255 }, + visible: false, + text: "Import Models" + }); + var cancelButton = Overlays.addOverlay("text", { + x: pos.x + margin + titleWidth, + y: pos.y + margin, + width: cancelWidth, + height: height, + color: { red: 255, green: 255, blue: 255 }, + visible: false, + text: "Close" + }); + this._importing = false; + + this.setImportVisible = function (visible) { + Overlays.editOverlay(importBoundaries, { visible: visible }); + Overlays.editOverlay(localModels, { visible: visible }); + Overlays.editOverlay(cancelButton, { visible: visible }); + Overlays.editOverlay(titleText, { visible: visible }); + Overlays.editOverlay(background, { visible: visible }); + }; + + var importPosition = { x: 0, y: 0, z: 0 }; + this.moveImport = function (position) { + importPosition = position; + Overlays.editOverlay(localModels, { + position: { x: importPosition.x, y: importPosition.y, z: importPosition.z } + }); + Overlays.editOverlay(importBoundaries, { + position: { x: importPosition.x, y: importPosition.y, z: importPosition.z } + }); + } + + this.mouseMoveEvent = function (event) { + if (self._importing) { + var pickRay = Camera.computePickRay(event.x, event.y); + var intersection = Voxels.findRayIntersection(pickRay); + + var distance = 2;// * self._scale; + + if (false) {//intersection.intersects) { + var intersectionDistance = Vec3.length(Vec3.subtract(pickRay.origin, intersection.intersection)); + if (intersectionDistance < distance) { + distance = intersectionDistance * 0.99; + } + + } + + var targetPosition = { + x: pickRay.origin.x + (pickRay.direction.x * distance), + y: pickRay.origin.y + (pickRay.direction.y * distance), + z: pickRay.origin.z + (pickRay.direction.z * distance) + }; + + if (targetPosition.x < 0) targetPosition.x = 0; + if (targetPosition.y < 0) targetPosition.y = 0; + if (targetPosition.z < 0) targetPosition.z = 0; + + var nudgeFactor = 1; + var newPosition = { + x: Math.floor(targetPosition.x / nudgeFactor) * nudgeFactor, + y: Math.floor(targetPosition.y / nudgeFactor) * nudgeFactor, + z: Math.floor(targetPosition.z / nudgeFactor) * nudgeFactor + } + + self.moveImport(newPosition); + } + } + + this.mouseReleaseEvent = function (event) { + var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + if (clickedOverlay == cancelButton) { + self._importing = false; + self.setImportVisible(false); + } + }; + + // Would prefer to use {4} for the coords, but it would only capture the last digit. + var fileRegex = /__(.+)__(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)_(\d+(?:\.\d+)?)__/; + this.doImport = function () { + if (!self._importing) { + var filename = Window.browse("Select models to import", "", "*.svo") + if (filename) { + parts = fileRegex.exec(filename); + if (parts == null) { + Window.alert("The file you selected does not contain source domain or location information"); + } else { + var hostname = parts[1]; + var x = parts[2]; + var y = parts[3]; + var z = parts[4]; + var s = parts[5]; + importScale = s; + if (hostname != location.hostname) { + if (!Window.confirm(("These models were not originally exported from this domain. Continue?"))) { + return; + } + } else { + if (Window.confirm(("Would you like to import back to the source location?"))) { + var success = Clipboard.importModels(filename); + if (success) { + Clipboard.pasteModels(x, y, z, 1); + } else { + Window.alert("There was an error importing the model file."); + } + return; + } + } + } + var success = Clipboard.importModels(filename); + if (success) { + self._importing = true; + self.setImportVisible(true); + Overlays.editOverlay(importBoundaries, { size: s }); + } else { + Window.alert("There was an error importing the model file."); + } + } + } + } + + this.paste = function () { + if (self._importing) { + // self._importing = false; + // self.setImportVisible(false); + Clipboard.pasteModels(importPosition.x, importPosition.y, importPosition.z, 1); + } + } + + this.cleanup = function () { + Overlays.deleteOverlay(localModels); + Overlays.deleteOverlay(importBoundaries); + Overlays.deleteOverlay(cancelButton); + Overlays.deleteOverlay(titleText); + Overlays.deleteOverlay(background); + } + + Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent); + Controller.mouseMoveEvent.connect(this.mouseMoveEvent); +}; + +var modelImporter = new ModelImporter(); + function isLocked(properties) { // special case to lock the ground plane model in hq. - if (location.hostname == "hq.highfidelity.io" && + if (location.hostname == "hq.highfidelity.io" && properties.modelURL == "https://s3-us-west-1.amazonaws.com/highfidelity-public/ozan/Terrain_Reduce_forAlpha.fbx") { return true; } @@ -76,105 +1717,105 @@ function controller(wichSide) { this.tip = 2 * wichSide + 1; this.trigger = wichSide; this.bumper = 6 * wichSide + 5; - + this.oldPalmPosition = Controller.getSpatialControlPosition(this.palm); this.palmPosition = Controller.getSpatialControlPosition(this.palm); - + this.oldTipPosition = Controller.getSpatialControlPosition(this.tip); this.tipPosition = Controller.getSpatialControlPosition(this.tip); - + this.oldUp = Controller.getSpatialControlNormal(this.palm); this.up = this.oldUp; - + this.oldFront = Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition)); this.front = this.oldFront; - + this.oldRight = Vec3.cross(this.front, this.up); this.right = this.oldRight; - + this.oldRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); this.rotation = this.oldRotation; - + this.triggerValue = Controller.getTriggerValue(this.trigger); this.bumperValue = Controller.isButtonPressed(this.bumper); - + this.pressed = false; // is trigger pressed this.pressing = false; // is trigger being pressed (is pressed now but wasn't previously) - + this.grabbing = false; this.modelID = { isKnownID: false }; this.modelURL = ""; this.oldModelRotation; this.oldModelPosition; this.oldModelRadius; - + this.positionAtGrab; this.rotationAtGrab; this.modelPositionAtGrab; this.modelRotationAtGrab; - + this.jointsIntersectingFromStart = []; - + this.laser = Overlays.addOverlay("line3d", { - position: { x: 0, y: 0, z: 0 }, - end: { x: 0, y: 0, z: 0 }, - color: LASER_COLOR, - alpha: 1, - visible: false, - lineWidth: LASER_WIDTH, - anchor: "MyAvatar" - }); - + position: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 0, z: 0 }, + color: LASER_COLOR, + alpha: 1, + visible: false, + lineWidth: LASER_WIDTH, + anchor: "MyAvatar" + }); + this.guideScale = 0.02; this.ball = Overlays.addOverlay("sphere", { - position: { x: 0, y: 0, z: 0 }, - size: this.guideScale, - solid: true, - color: { red: 0, green: 255, blue: 0 }, - alpha: 1, - visible: false, - anchor: "MyAvatar" - }); + position: { x: 0, y: 0, z: 0 }, + size: this.guideScale, + solid: true, + color: { red: 0, green: 255, blue: 0 }, + alpha: 1, + visible: false, + anchor: "MyAvatar" + }); this.leftRight = Overlays.addOverlay("line3d", { - position: { x: 0, y: 0, z: 0 }, - end: { x: 0, y: 0, z: 0 }, - color: { red: 0, green: 0, blue: 255 }, - alpha: 1, - visible: false, - lineWidth: LASER_WIDTH, - anchor: "MyAvatar" - }); + position: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 0, z: 0 }, + color: { red: 0, green: 0, blue: 255 }, + alpha: 1, + visible: false, + lineWidth: LASER_WIDTH, + anchor: "MyAvatar" + }); this.topDown = Overlays.addOverlay("line3d", { - position: { x: 0, y: 0, z: 0 }, - end: { x: 0, y: 0, z: 0 }, - color: { red: 0, green: 0, blue: 255 }, - alpha: 1, - visible: false, - lineWidth: LASER_WIDTH, - anchor: "MyAvatar" - }); - + position: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 0, z: 0 }, + color: { red: 0, green: 0, blue: 255 }, + alpha: 1, + visible: false, + lineWidth: LASER_WIDTH, + anchor: "MyAvatar" + }); + + - this.grab = function (modelID, properties) { if (isLocked(properties)) { print("Model locked " + modelID.id); } else { print("Grabbing " + modelID.id); - + this.grabbing = true; this.modelID = modelID; this.modelURL = properties.modelURL; - + this.oldModelPosition = properties.position; this.oldModelRotation = properties.modelRotation; this.oldModelRadius = properties.radius; - + this.positionAtGrab = this.palmPosition; this.rotationAtGrab = this.rotation; this.modelPositionAtGrab = properties.position; this.modelRotationAtGrab = properties.modelRotation; - + this.jointsIntersectingFromStart = []; for (var i = 0; i < jointList.length; i++) { var distance = Vec3.distance(MyAvatar.getJointPosition(jointList[i]), this.oldModelPosition); @@ -185,11 +1826,11 @@ function controller(wichSide) { this.showLaser(false); } } - + this.release = function () { if (this.grabbing) { jointList = MyAvatar.getJointNames(); - + var closestJointIndex = -1; var closestJointDistance = 10; for (var i = 0; i < jointList.length; i++) { @@ -199,14 +1840,14 @@ function controller(wichSide) { closestJointIndex = i; } } - + if (closestJointIndex != -1) { print("closestJoint: " + jointList[closestJointIndex]); print("closestJointDistance (attach max distance): " + closestJointDistance + " (" + this.oldModelRadius + ")"); } - + if (closestJointDistance < this.oldModelRadius) { - + if (this.jointsIntersectingFromStart.indexOf(closestJointIndex) != -1 || (leftController.grabbing && rightController.grabbing && leftController.modelID.id == rightController.modelID.id)) { @@ -215,26 +1856,26 @@ function controller(wichSide) { print("Attaching to " + jointList[closestJointIndex]); var jointPosition = MyAvatar.getJointPosition(jointList[closestJointIndex]); var jointRotation = MyAvatar.getJointCombinedRotation(jointList[closestJointIndex]); - + var attachmentOffset = Vec3.subtract(this.oldModelPosition, jointPosition); attachmentOffset = Vec3.multiplyQbyV(Quat.inverse(jointRotation), attachmentOffset); var attachmentRotation = Quat.multiply(Quat.inverse(jointRotation), this.oldModelRotation); - + MyAvatar.attach(this.modelURL, jointList[closestJointIndex], attachmentOffset, attachmentRotation, 2.0 * this.oldModelRadius, true, false); - + Models.deleteModel(this.modelID); } } } - + this.grabbing = false; this.modelID.isKnownID = false; this.jointsIntersectingFromStart = []; this.showLaser(true); } - + this.checkTrigger = function () { if (this.triggerValue > 0.9) { if (this.pressed) { @@ -254,8 +1895,8 @@ function controller(wichSide) { if (isLocked(properties)) { return { valid: false }; } - - + + // P P - Model // /| A - Palm // / | d B - unit vector toward tip @@ -266,67 +1907,67 @@ function controller(wichSide) { // |X-A| = (P-A).B // X == A + ((P-A).B)B // d = |P-X| - + var A = this.palmPosition; var B = this.front; var P = properties.position; - + var x = Vec3.dot(Vec3.subtract(P, A), B); var y = Vec3.dot(Vec3.subtract(P, A), this.up); var z = Vec3.dot(Vec3.subtract(P, A), this.right); var X = Vec3.sum(A, Vec3.multiply(B, x)); var d = Vec3.length(Vec3.subtract(P, X)); - + var angularSize = 2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14; if (0 < x && angularSize > MIN_ANGULAR_SIZE) { if (angularSize > MAX_ANGULAR_SIZE) { print("Angular size too big: " + 2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14); return { valid: false }; } - + return { valid: true, x: x, y: y, z: z }; } return { valid: false }; } - + this.glowedIntersectingModel = { isKnownID: false }; this.moveLaser = function () { // the overlays here are anchored to the avatar, which means they are specified in the avatar's local frame - + var inverseRotation = Quat.inverse(MyAvatar.orientation); var startPosition = Vec3.multiplyQbyV(inverseRotation, Vec3.subtract(this.palmPosition, MyAvatar.position)); var direction = Vec3.multiplyQbyV(inverseRotation, Vec3.subtract(this.tipPosition, this.palmPosition)); var distance = Vec3.length(direction); direction = Vec3.multiply(direction, LASER_LENGTH_FACTOR / distance); var endPosition = Vec3.sum(startPosition, direction); - + Overlays.editOverlay(this.laser, { - position: startPosition, - end: endPosition - }); - - + position: startPosition, + end: endPosition + }); + + Overlays.editOverlay(this.ball, { - position: endPosition - }); + position: endPosition + }); Overlays.editOverlay(this.leftRight, { - position: Vec3.sum(endPosition, Vec3.multiply(this.right, 2 * this.guideScale)), - end: Vec3.sum(endPosition, Vec3.multiply(this.right, -2 * this.guideScale)) - }); - Overlays.editOverlay(this.topDown, {position: Vec3.sum(endPosition, Vec3.multiply(this.up, 2 * this.guideScale)), - end: Vec3.sum(endPosition, Vec3.multiply(this.up, -2 * this.guideScale)) - }); + position: Vec3.sum(endPosition, Vec3.multiply(this.right, 2 * this.guideScale)), + end: Vec3.sum(endPosition, Vec3.multiply(this.right, -2 * this.guideScale)) + }); + Overlays.editOverlay(this.topDown, { position: Vec3.sum(endPosition, Vec3.multiply(this.up, 2 * this.guideScale)), + end: Vec3.sum(endPosition, Vec3.multiply(this.up, -2 * this.guideScale)) + }); this.showLaser(!this.grabbing || mode == 0); - + if (this.glowedIntersectingModel.isKnownID) { Models.editModel(this.glowedIntersectingModel, { glowLevel: 0.0 }); this.glowedIntersectingModel.isKnownID = false; } if (!this.grabbing) { var intersection = Models.findRayIntersection({ - origin: this.palmPosition, - direction: this.front - }); + origin: this.palmPosition, + direction: this.front + }); var angularSize = 2 * Math.atan(intersection.modelProperties.radius / Vec3.distance(Camera.getPosition(), intersection.modelProperties.position)) * 180 / 3.14; if (intersection.accurate && intersection.modelID.isKnownID && angularSize > MIN_ANGULAR_SIZE && angularSize < MAX_ANGULAR_SIZE) { this.glowedIntersectingModel = intersection.modelID; @@ -334,27 +1975,27 @@ function controller(wichSide) { } } } - - this.showLaser = function(show) { + + this.showLaser = function (show) { Overlays.editOverlay(this.laser, { visible: show }); Overlays.editOverlay(this.ball, { visible: show }); Overlays.editOverlay(this.leftRight, { visible: show }); Overlays.editOverlay(this.topDown, { visible: show }); } - + this.moveModel = function () { if (this.grabbing) { if (!this.modelID.isKnownID) { print("Unknown grabbed ID " + this.modelID.id + ", isKnown: " + this.modelID.isKnownID); - this.modelID = Models.findRayIntersection({ - origin: this.palmPosition, - direction: this.front - }).modelID; + this.modelID = Models.findRayIntersection({ + origin: this.palmPosition, + direction: this.front + }).modelID; print("Identified ID " + this.modelID.id + ", isKnown: " + this.modelID.isKnownID); } var newPosition; var newRotation; - + switch (mode) { case 0: newPosition = Vec3.sum(this.palmPosition, @@ -363,8 +2004,8 @@ function controller(wichSide) { Vec3.multiply(this.up, this.y)); newPosition = Vec3.sum(newPosition, Vec3.multiply(this.right, this.z)); - - + + newRotation = Quat.multiply(this.rotation, Quat.inverse(this.oldRotation)); newRotation = Quat.multiply(newRotation, @@ -373,11 +2014,11 @@ function controller(wichSide) { case 1: var forward = Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -1 }); var d = Vec3.dot(forward, MyAvatar.position); - + var factor1 = Vec3.dot(forward, this.positionAtGrab) - d; var factor2 = Vec3.dot(forward, this.modelPositionAtGrab) - d; var vector = Vec3.subtract(this.palmPosition, this.positionAtGrab); - + if (factor2 < 0) { factor2 = 0; } @@ -385,26 +2026,26 @@ function controller(wichSide) { factor1 = 1; factor2 = 1; } - + newPosition = Vec3.sum(this.modelPositionAtGrab, Vec3.multiply(vector, factor2 / factor1)); - + newRotation = Quat.multiply(this.rotation, Quat.inverse(this.rotationAtGrab)); newRotation = Quat.multiply(newRotation, this.modelRotationAtGrab); break; } - + Models.editModel(this.modelID, { - position: newPosition, - modelRotation: newRotation - }); - + position: newPosition, + modelRotation: newRotation + }); + this.oldModelRotation = newRotation; this.oldModelPosition = newPosition; - + var indicesToRemove = []; for (var i = 0; i < this.jointsIntersectingFromStart.length; ++i) { var distance = Vec3.distance(MyAvatar.getJointPosition(this.jointsIntersectingFromStart[i]), this.oldModelPosition); @@ -418,27 +2059,27 @@ function controller(wichSide) { } } } - + this.update = function () { this.oldPalmPosition = this.palmPosition; this.oldTipPosition = this.tipPosition; this.palmPosition = Controller.getSpatialControlPosition(this.palm); this.tipPosition = Controller.getSpatialControlPosition(this.tip); - + this.oldUp = this.up; this.up = Vec3.normalize(Controller.getSpatialControlNormal(this.palm)); - + this.oldFront = this.front; this.front = Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition)); - + this.oldRight = this.right; this.right = Vec3.normalize(Vec3.cross(this.front, this.up)); - + this.oldRotation = this.rotation; this.rotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm)); - + this.triggerValue = Controller.getTriggerValue(this.trigger); - + var bumperValue = Controller.isButtonPressed(this.bumper); if (bumperValue && !this.bumperValue) { if (mode == 0) { @@ -452,56 +2093,56 @@ function controller(wichSide) { } } this.bumperValue = bumperValue; - - + + this.checkTrigger(); - + this.moveLaser(); - + if (!this.pressed && this.grabbing) { // release if trigger not pressed anymore. this.release(); } - + if (this.pressing) { // Checking for attachments intersecting var attachments = MyAvatar.getAttachmentData(); var attachmentIndex = -1; var attachmentX = LASER_LENGTH_FACTOR; - + var newModel; var newProperties; - + for (var i = 0; i < attachments.length; ++i) { var position = Vec3.sum(MyAvatar.getJointPosition(attachments[i].jointName), Vec3.multiplyQbyV(MyAvatar.getJointCombinedRotation(attachments[i].jointName), attachments[i].translation)); var scale = attachments[i].scale; - + var A = this.palmPosition; var B = this.front; var P = 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 < scale / 2.0 && 0 < x && x < attachmentX) { attachmentIndex = i; attachmentX = d; } } - + if (attachmentIndex != -1) { print("Detaching: " + attachments[attachmentIndex].modelURL); MyAvatar.detachOne(attachments[attachmentIndex].modelURL, attachments[attachmentIndex].jointName); - + newProperties = { - position: Vec3.sum(MyAvatar.getJointPosition(attachments[attachmentIndex].jointName), - Vec3.multiplyQbyV(MyAvatar.getJointCombinedRotation(attachments[attachmentIndex].jointName), attachments[attachmentIndex].translation)), - modelRotation: Quat.multiply(MyAvatar.getJointCombinedRotation(attachments[attachmentIndex].jointName), - attachments[attachmentIndex].rotation), - radius: attachments[attachmentIndex].scale / 2.0, - modelURL: attachments[attachmentIndex].modelURL + position: Vec3.sum(MyAvatar.getJointPosition(attachments[attachmentIndex].jointName), + Vec3.multiplyQbyV(MyAvatar.getJointCombinedRotation(attachments[attachmentIndex].jointName), attachments[attachmentIndex].translation)), + modelRotation: Quat.multiply(MyAvatar.getJointCombinedRotation(attachments[attachmentIndex].jointName), + attachments[attachmentIndex].rotation), + radius: attachments[attachmentIndex].scale / 2.0, + modelURL: attachments[attachmentIndex].modelURL }; newModel = Models.addModel(newProperties); } else { @@ -511,13 +2152,13 @@ function controller(wichSide) { var pickRay = { origin: this.palmPosition, direction: Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition)) }; var foundIntersection = Models.findRayIntersection(pickRay); - - if(!foundIntersection.accurate) { + + if (!foundIntersection.accurate) { print("No accurate intersection"); return; } newModel = foundIntersection.modelID; - + if (!newModel.isKnownID) { var identify = Models.identifyModel(newModel); if (!identify.isKnownID) { @@ -528,10 +2169,10 @@ function controller(wichSide) { } newProperties = Models.getModelProperties(newModel); } - - + + print("foundModel.modelURL=" + newProperties.modelURL); - + if (isLocked(newProperties)) { print("Model locked " + newProperties.id); } else { @@ -539,9 +2180,9 @@ function controller(wichSide) { if (!check.valid) { return; } - + this.grab(newModel, newProperties); - + this.x = check.x; this.y = check.y; this.z = check.z; @@ -566,32 +2207,32 @@ function moveModels() { var newPosition = leftController.oldModelPosition; var rotation = leftController.oldModelRotation; var ratio = 1; - - + + switch (mode) { case 0: var oldLeftPoint = Vec3.sum(leftController.oldPalmPosition, Vec3.multiply(leftController.oldFront, leftController.x)); var oldRightPoint = Vec3.sum(rightController.oldPalmPosition, Vec3.multiply(rightController.oldFront, rightController.x)); - + var oldMiddle = Vec3.multiply(Vec3.sum(oldLeftPoint, oldRightPoint), 0.5); var oldLength = Vec3.length(Vec3.subtract(oldLeftPoint, oldRightPoint)); - - + + var leftPoint = Vec3.sum(leftController.palmPosition, Vec3.multiply(leftController.front, leftController.x)); var rightPoint = Vec3.sum(rightController.palmPosition, Vec3.multiply(rightController.front, rightController.x)); - + var middle = Vec3.multiply(Vec3.sum(leftPoint, rightPoint), 0.5); var length = Vec3.length(Vec3.subtract(leftPoint, rightPoint)); - - + + ratio = length / oldLength; newPosition = Vec3.sum(middle, Vec3.multiply(Vec3.subtract(leftController.oldModelPosition, oldMiddle), ratio)); - break; + break; case 1: var u = Vec3.normalize(Vec3.subtract(rightController.oldPalmPosition, leftController.oldPalmPosition)); var v = Vec3.normalize(Vec3.subtract(rightController.palmPosition, leftController.palmPosition)); - + var cos_theta = Vec3.dot(u, v); if (cos_theta > 1) { cos_theta = 1; @@ -599,41 +2240,41 @@ function moveModels() { var angle = Math.acos(cos_theta) / Math.PI * 180; if (angle < 0.1) { return; - + } var w = Vec3.normalize(Vec3.cross(u, v)); - + rotation = Quat.multiply(Quat.angleAxis(angle, w), leftController.oldModelRotation); - - + + leftController.positionAtGrab = leftController.palmPosition; leftController.rotationAtGrab = leftController.rotation; leftController.modelPositionAtGrab = leftController.oldModelPosition; leftController.modelRotationAtGrab = rotation; - + rightController.positionAtGrab = rightController.palmPosition; rightController.rotationAtGrab = rightController.rotation; rightController.modelPositionAtGrab = rightController.oldModelPosition; rightController.modelRotationAtGrab = rotation; break; } - + Models.editModel(leftController.modelID, { - position: newPosition, - modelRotation: rotation, - radius: leftController.oldModelRadius * ratio - }); - + position: newPosition, + modelRotation: rotation, + radius: leftController.oldModelRadius * ratio + }); + leftController.oldModelPosition = newPosition; leftController.oldModelRotation = rotation; leftController.oldModelRadius *= ratio; - + rightController.oldModelPosition = newPosition; rightController.oldModelRotation = rotation; rightController.oldModelRadius *= ratio; return; } - + leftController.moveModel(); rightController.moveModel(); } @@ -644,68 +2285,34 @@ function checkController(deltaTime) { var numberOfTriggers = Controller.getNumberOfTriggers(); var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; - + + if (!isActive) { + // So that we hide the lasers bellow and keep updating the overlays position + numberOfButtons = 0; + } + // this is expected for hydras - if (numberOfButtons==12 && numberOfTriggers == 2 && controllersPerTrigger == 2) { + if (numberOfButtons == 12 && numberOfTriggers == 2 && controllersPerTrigger == 2) { if (!hydraConnected) { hydraConnected = true; } - + leftController.update(); rightController.update(); moveModels(); } else { if (hydraConnected) { hydraConnected = false; - + leftController.showLaser(false); rightController.showLaser(false); } } - - moveOverlays(); -} -var newModel; -var browser; -function initToolBar() { - toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); - // New Model - newModel = toolBar.addTool({ - imageURL: toolIconUrl + "add-model-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 - }); - browser = toolBar.addTool({ - imageURL: toolIconUrl + "list-icon.png", - width: toolWidth, height: toolHeight, - visible: true, - alpha: 0.7 - }); -} -function moveOverlays() { - var newViewPort = Controller.getViewportDimensions(); - - if (typeof(toolBar) === 'undefined') { - initToolBar(); - - } else if (windowDimensions.x == newViewPort.x && - windowDimensions.y == newViewPort.y) { - return; - } - - - windowDimensions = newViewPort; - var toolsX = windowDimensions.x - 8 - toolBar.width; - var toolsY = (windowDimensions.y - toolBar.height) / 2; - - toolBar.move(toolsX, toolsY); + toolBar.move(); + progressDialog.move(); } - - var modelSelected = false; var selectedModelID; var selectedModelProperties; @@ -719,7 +2326,7 @@ var SCALE_FACTOR = 200.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)); } @@ -727,25 +2334,25 @@ function Tooltip() { this.x = 285; this.y = 115; this.width = 500; - this.height = 145 ; + this.height = 145; this.margin = 5; this.decimals = 3; - + this.textOverlay = Overlays.addOverlay("text", { - x: this.x, - y: this.y, - width: this.width, - height: this.height, - margin: this.margin, - text: "", - color: { red: 128, green: 128, blue: 128 }, - alpha: 0.2, - visible: false - }); - this.show = function(doShow) { + x: this.x, + y: this.y, + width: this.width, + height: this.height, + margin: this.margin, + text: "", + color: { red: 128, green: 128, blue: 128 }, + alpha: 0.2, + visible: false + }); + this.show = function (doShow) { Overlays.editOverlay(this.textOverlay, { visible: doShow }); } - this.updateText = function(properties) { + this.updateText = function (properties) { var angles = Quat.safeEulerAngles(properties.modelRotation); var text = "Model Properties:\n" text += "X: " + properties.position.x.toFixed(this.decimals) + "\n" @@ -771,8 +2378,8 @@ function Tooltip() { Overlays.editOverlay(this.textOverlay, { text: text }); } - - this.cleanup = function() { + + this.cleanup = function () { Overlays.deleteOverlay(this.textOverlay); } } @@ -782,55 +2389,29 @@ function mousePressEvent(event) { if (event.isAlt) { return; } - + 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 || url == "") { - return; - } - - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); - - if (position.x > 0 && position.y > 0 && position.z > 0) { - Models.addModel({ position: position, - radius: radiusDefault, - modelURL: url - }); - } else { - print("Can't create model: Model would be out of bounds."); - } - - } else if (browser == toolBar.clicked(clickedOverlay)) { - var url = Window.s3Browse(".*(fbx|FBX)"); - if (url == null || url == "") { - return; - } - - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); - - if (position.x > 0 && position.y > 0 && position.z > 0) { - Models.addModel({ position: position, - radius: radiusDefault, - modelURL: url - }); - } else { - print("Can't create model: Model would be out of bounds."); - } - + var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + if (toolBar.mousePressEvent(event) || progressDialog.mousePressEvent(event)) { + // Event handled; do nothing. + return; } else { + // If we aren't active and didn't click on an overlay: quit + if (!isActive) { + return; + } + var pickRay = Camera.computePickRay(event.x, event.y); Vec3.print("[Mouse] Looking at: ", pickRay.origin); var foundIntersection = Models.findRayIntersection(pickRay); - - if(!foundIntersection.accurate) { + + if (!foundIntersection.accurate) { return; } var foundModel = foundIntersection.modelID; - + if (!foundModel.isKnownID) { var identify = Models.identifyModel(foundModel); if (!identify.isKnownID) { @@ -839,7 +2420,7 @@ function mousePressEvent(event) { } foundModel = identify; } - + var properties = Models.getModelProperties(foundModel); if (isLocked(properties)) { print("Model locked " + properties.id); @@ -855,22 +2436,22 @@ function mousePressEvent(event) { // |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)); - + var angularSize = 2 * Math.atan(properties.radius / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14; if (0 < x && angularSize > MIN_ANGULAR_SIZE) { if (angularSize < MAX_ANGULAR_SIZE) { modelSelected = true; selectedModelID = foundModel; selectedModelProperties = properties; - + orientation = MyAvatar.orientation; intersection = rayPlaneIntersection(pickRay, P, Quat.getFront(orientation)); } else { @@ -879,23 +2460,23 @@ function mousePressEvent(event) { } } } - + if (modelSelected) { selectedModelProperties.oldRadius = selectedModelProperties.radius; selectedModelProperties.oldPosition = { - x: selectedModelProperties.position.x, - y: selectedModelProperties.position.y, - z: selectedModelProperties.position.z, + 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, + x: selectedModelProperties.modelRotation.x, + y: selectedModelProperties.modelRotation.y, + z: selectedModelProperties.modelRotation.z, + w: selectedModelProperties.modelRotation.w, }; selectedModelProperties.glowLevel = 0.0; - - print("Clicked on " + selectedModelID.id + " " + modelSelected); + + print("Clicked on " + selectedModelID.id + " " + modelSelected); tooltip.updateText(selectedModelProperties); tooltip.show(true); } @@ -905,22 +2486,22 @@ var glowedModelID = { id: -1, isKnownID: false }; var oldModifier = 0; var modifier = 0; var wasShifted = false; -function mouseMoveEvent(event) { - if (event.isAlt) { +function mouseMoveEvent(event) { + if (event.isAlt || !isActive) { return; } - + var pickRay = Camera.computePickRay(event.x, event.y); - + if (!modelSelected) { var modelIntersection = Models.findRayIntersection(pickRay); if (modelIntersection.accurate) { - if(glowedModelID.isKnownID && glowedModelID.id != modelIntersection.modelID.id) { + if (glowedModelID.isKnownID && glowedModelID.id != modelIntersection.modelID.id) { Models.editModel(glowedModelID, { glowLevel: 0.0 }); glowedModelID.id = -1; glowedModelID.isKnownID = false; } - + var angularSize = 2 * Math.atan(modelIntersection.modelProperties.radius / Vec3.distance(Camera.getPosition(), modelIntersection.modelProperties.position)) * 180 / 3.14; if (modelIntersection.modelID.isKnownID && angularSize > MIN_ANGULAR_SIZE && angularSize < MAX_ANGULAR_SIZE) { Models.editModel(modelIntersection.modelID, { glowLevel: 0.25 }); @@ -929,7 +2510,7 @@ function mouseMoveEvent(event) { } return; } - + if (event.isLeftButton) { if (event.isRightButton) { modifier = 1; // Scale @@ -944,30 +2525,30 @@ function mouseMoveEvent(event) { 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, + 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, + 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; @@ -975,13 +2556,13 @@ function mouseMoveEvent(event) { // 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, @@ -994,7 +2575,7 @@ function mouseMoveEvent(event) { 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: @@ -1008,10 +2589,10 @@ function mouseMoveEvent(event) { mouseLastPosition.y = event.y; somethingChanged = false; } - - + + var pixelPerDegrees = windowDimensions.y / (1 * 360); // the entire height of the window allow you to make 2 full rotations - + //compute delta in pixel var cameraForward = Quat.getFront(Camera.getOrientation()); var rotationAxis = (!zIsPressed && xIsPressed) ? { x: 1, y: 0, z: 0 } : @@ -1023,42 +2604,42 @@ function mouseMoveEvent(event) { .x, y: mouseLastPosition.y - event.y, z: 0 }; var transformedMouseDelta = Vec3.multiplyQbyV(Camera.getOrientation(), mouseDelta); var delta = Math.floor(Vec3.dot(transformedMouseDelta, Vec3.normalize(orthogonalAxis)) / pixelPerDegrees); - + var STEP = 15; if (!event.isShifted) { delta = Math.round(delta / STEP) * STEP; } - + var rotation = Quat.fromVec3Degrees({ - x: (!zIsPressed && xIsPressed) ? delta : 0, // x is pressed - y: (!zIsPressed && !xIsPressed) ? delta : 0, // neither is pressed - z: (zIsPressed && !xIsPressed) ? delta : 0 // z is pressed - }); + x: (!zIsPressed && xIsPressed) ? delta : 0, // x is pressed + y: (!zIsPressed && !xIsPressed) ? delta : 0, // neither is pressed + z: (zIsPressed && !xIsPressed) ? delta : 0 // z is pressed + }); rotation = Quat.multiply(selectedModelProperties.oldRotation, rotation); - + selectedModelProperties.modelRotation.x = rotation.x; selectedModelProperties.modelRotation.y = rotation.y; selectedModelProperties.modelRotation.z = rotation.z; selectedModelProperties.modelRotation.w = rotation.w; break; } - + Models.editModel(selectedModelID, selectedModelProperties); tooltip.updateText(selectedModelProperties); } function mouseReleaseEvent(event) { - if (event.isAlt) { + if (event.isAlt || !isActive) { return; } - + if (modelSelected) { tooltip.show(false); } - + modelSelected = false; - + glowedModelID.id = -1; glowedModelID.isKnownID = false; } @@ -1069,18 +2650,24 @@ function mouseReleaseEvent(event) { var modelMenuAddedDelete = false; function setupModelMenus() { print("setupModelMenus()"); - // add our menuitems + // adj our menuitems Menu.addMenuItem({ menuName: "Edit", menuItemName: "Models", isSeparator: true, beforeItem: "Physics" }); - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Edit Properties...", + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Edit Properties...", shortcutKeyEvent: { text: "`" }, afterItem: "Models" }); - if (!Menu.menuItemExists("Edit","Delete")) { + if (!Menu.menuItemExists("Edit", "Delete")) { print("no delete... adding ours"); - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Delete", + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Delete", shortcutKeyEvent: { text: "backspace" }, afterItem: "Models" }); modelMenuAddedDelete = true; } else { print("delete exists... don't add ours"); } + + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Paste Models", shortcutKey: "CTRL+META+V", afterItem: "Edit Properties..." }); + + Menu.addMenuItem({ menuName: "File", menuItemName: "Models", isSeparator: true, beforeItem: "Settings" }); + Menu.addMenuItem({ menuName: "File", menuItemName: "Export Models", shortcutKey: "CTRL+META+E", afterItem: "Models" }); + Menu.addMenuItem({ menuName: "File", menuItemName: "Import Models", shortcutKey: "CTRL+META+I", afterItem: "Export Models" }); } function cleanupModelMenus() { @@ -1090,14 +2677,25 @@ function cleanupModelMenus() { // delete our menuitems Menu.removeMenuItem("Edit", "Delete"); } + + Menu.removeMenuItem("Edit", "Paste Models"); + + Menu.removeSeparator("File", "Models"); + Menu.removeMenuItem("File", "Export Models"); + Menu.removeMenuItem("File", "Import Models"); } function scriptEnding() { leftController.cleanup(); rightController.cleanup(); + progressDialog.cleanup(); toolBar.cleanup(); cleanupModelMenus(); tooltip.cleanup(); + modelImporter.cleanup(); + if (exportMenu) { + exportMenu.close(); + } } Script.scriptEnding.connect(scriptEnding); @@ -1109,19 +2707,19 @@ Controller.mouseReleaseEvent.connect(mouseReleaseEvent); setupModelMenus(); -function handeMenuEvent(menuItem){ +function handeMenuEvent(menuItem) { print("menuItemEvent() in JS... menuItem=" + menuItem); if (menuItem == "Delete") { if (leftController.grabbing) { - print(" Delete Model.... leftController.modelID="+ leftController.modelID); + print(" Delete Model.... leftController.modelID=" + leftController.modelID); Models.deleteModel(leftController.modelID); leftController.grabbing = false; } else if (rightController.grabbing) { - print(" Delete Model.... rightController.modelID="+ rightController.modelID); + print(" Delete Model.... rightController.modelID=" + rightController.modelID); Models.deleteModel(rightController.modelID); rightController.grabbing = false; } else if (modelSelected) { - print(" Delete Model.... selectedModelID="+ selectedModelID); + print(" Delete Model.... selectedModelID=" + selectedModelID); Models.deleteModel(selectedModelID); modelSelected = false; } else { @@ -1130,13 +2728,13 @@ function handeMenuEvent(menuItem){ } else if (menuItem == "Edit Properties...") { var editModelID = -1; if (leftController.grabbing) { - print(" Edit Properties.... leftController.modelID="+ leftController.modelID); + print(" Edit Properties.... leftController.modelID=" + leftController.modelID); editModelID = leftController.modelID; } else if (rightController.grabbing) { - print(" Edit Properties.... rightController.modelID="+ rightController.modelID); + print(" Edit Properties.... rightController.modelID=" + rightController.modelID); editModelID = rightController.modelID; } else if (modelSelected) { - print(" Edit Properties.... selectedModelID="+ selectedModelID); + print(" Edit Properties.... selectedModelID=" + selectedModelID); editModelID = selectedModelID; } else { print(" Edit Properties.... not holding..."); @@ -1156,25 +2754,40 @@ function handeMenuEvent(menuItem){ array.push({ label: "Yaw:", value: angles.y.toFixed(decimals) }); array.push({ label: "Roll:", value: angles.z.toFixed(decimals) }); array.push({ label: "Scale:", value: 2 * selectedModelProperties.radius.toFixed(decimals) }); - - var propertyName = Window.form("Edit Properties", array); - modelSelected = false; - - var index = 0; - selectedModelProperties.modelURL = array[index++].value; - selectedModelProperties.animationURL = array[index++].value; - selectedModelProperties.animationIsPlaying = array[index++].value; - selectedModelProperties.position.x = array[index++].value; - selectedModelProperties.position.y = array[index++].value; - selectedModelProperties.position.z = array[index++].value; - angles.x = array[index++].value; - angles.y = array[index++].value; - angles.z = array[index++].value; - selectedModelProperties.modelRotation = Quat.fromVec3Degrees(angles); - selectedModelProperties.radius = array[index++].value / 2; + array.push({ button: "Cancel" }); - Models.editModel(selectedModelID, selectedModelProperties); + var index = 0; + if (Window.form("Edit Properties", array)) { + selectedModelProperties.modelURL = array[index++].value; + selectedModelProperties.animationURL = array[index++].value; + selectedModelProperties.animationIsPlaying = array[index++].value; + selectedModelProperties.position.x = array[index++].value; + selectedModelProperties.position.y = array[index++].value; + selectedModelProperties.position.z = array[index++].value; + angles.x = array[index++].value; + angles.y = array[index++].value; + angles.z = array[index++].value; + selectedModelProperties.modelRotation = Quat.fromVec3Degrees(angles); + selectedModelProperties.radius = array[index++].value / 2; + print(selectedModelProperties.radius); + + Models.editModel(selectedModelID, selectedModelProperties); + } + + modelSelected = false; } + } else if (menuItem == "Paste Models") { + modelImporter.paste(); + } else if (menuItem == "Export Models") { + if (!exportMenu) { + exportMenu = new ExportMenu({ + onClose: function () { + exportMenu = null; + } + }); + } + } else if (menuItem == "Import Models") { + modelImporter.doImport(); } tooltip.show(false); } @@ -1186,7 +2799,7 @@ Menu.menuItemEvent.connect(handeMenuEvent); var zIsPressed = false; var xIsPressed = false; var somethingChanged = false; -Controller.keyPressEvent.connect(function(event) { +Controller.keyPressEvent.connect(function (event) { if ((event.text == "z" || event.text == "Z") && !zIsPressed) { zIsPressed = true; somethingChanged = true; @@ -1195,7 +2808,7 @@ Controller.keyPressEvent.connect(function(event) { xIsPressed = true; somethingChanged = true; } - + // resets model orientation when holding with mouse if (event.text == "r" && modelSelected) { selectedModelProperties.modelRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }); @@ -1205,7 +2818,7 @@ Controller.keyPressEvent.connect(function(event) { } }); -Controller.keyReleaseEvent.connect(function(event) { +Controller.keyReleaseEvent.connect(function (event) { if (event.text == "z" || event.text == "Z") { zIsPressed = false; somethingChanged = true; @@ -1221,4 +2834,4 @@ Controller.keyReleaseEvent.connect(function(event) { if (event.text == "BACKSPACE") { handeMenuEvent("Delete"); } -}); \ No newline at end of file +}); diff --git a/examples/editVoxels.js b/examples/editVoxels.js index 77cec87b15..1ed3dcc0c3 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -752,7 +752,7 @@ function calculateVoxelFromIntersection(intersection, operation) { highlightAt.z = z + zFightingSizeAdjust; voxelSize -= 2 * zFightingSizeAdjust; if (wantAddAdjust) { - resultVoxel.y += voxelSize; + resultVoxel.y += resultVoxel.s; } resultVoxel.bottomRight = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; @@ -782,7 +782,7 @@ function calculateVoxelFromIntersection(intersection, operation) { highlightAt.z = z + voxelSize + zFightingSizeAdjust; voxelSize -= 2 * zFightingSizeAdjust; if (wantAddAdjust) { - resultVoxel.z += voxelSize; + resultVoxel.z += resultVoxel.s; } resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; diff --git a/examples/rockPaperScissorsCells.js b/examples/rockPaperScissorsCells.js new file mode 100644 index 0000000000..2a9cb00a0b --- /dev/null +++ b/examples/rockPaperScissorsCells.js @@ -0,0 +1,272 @@ +// rockPaperScissorsCells.js +// examples +// +// Created by Ben Arnold on 7/16/14. +// Copyright 2014 High Fidelity, Inc. +// +// This sample script creates a voxel wall that simulates the Rock Paper Scissors cellular +// automata. http://www.gamedev.net/blog/844/entry-2249737-another-cellular-automaton-video/ +// If multiple instances of this script are run, they will combine into a larger wall. +// NOTE: You must run each instance one at a time. If they all start at once there are race conditions. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var NUMBER_OF_CELLS_EACH_DIMENSION = 48; +var NUMBER_OF_CELLS_REGION_EACH_DIMESION = 16; +var REGIONS_EACH_DIMENSION = NUMBER_OF_CELLS_EACH_DIMENSION / NUMBER_OF_CELLS_REGION_EACH_DIMESION; + +var isLocal = false; + +var currentCells = []; +var nextCells = []; + +var cornerPosition = {x: 100, y: 0, z: 0 } +var position = {x: 0, y: 0, z: 0 }; + +var METER_LENGTH = 1; +var cellScale = (NUMBER_OF_CELLS_EACH_DIMENSION * METER_LENGTH) / NUMBER_OF_CELLS_EACH_DIMENSION; + +var viewerPosition = {x: cornerPosition.x + (NUMBER_OF_CELLS_EACH_DIMENSION / 2) * cellScale, y: cornerPosition.y + (NUMBER_OF_CELLS_EACH_DIMENSION / 2) * cellScale, z: cornerPosition.z }; + +viewerPosition.z += 50; +var yaw = 0; +var orientation = Quat.fromPitchYawRollDegrees(0, yaw, 0); + +//Feel free to add new cell types here. It can be more than three. +var cellTypes = []; +cellTypes[0] = { r: 255, g: 0, b: 0 }; +cellTypes[1] = { r: 0, g: 255, b: 0 }; +cellTypes[2] = { r: 0, g:0, b: 255 }; +cellTypes[3] = { r: 0, g: 255, b: 255 }; + + +//Check for free region for AC +var regionMarkerX = -1; +var regionMarkerY = -1; +var regionMarkerI = -1; +var regionMarkerJ = -1; + +var regionMarkerColor = {r: 254, g: 0, b: 253}; + +function setRegionToColor(startX, startY, width, height, color) { + for (var i = startY; i < startY + height; i++) { + for (var j = startX; j < startX + width; j++) { + + currentCells[i][j] = { changed: true, type: color }; + + // put the same value in the nextCells array for first board draw + nextCells[i][j] = { changed: true, type: color }; + } + } +} + +function init() { + + for (var i = 0; i < REGIONS_EACH_DIMENSION; i++) { + for (var j = 0; j < REGIONS_EACH_DIMENSION; j++) { + var x = cornerPosition.x + (j) * cellScale; + var y = cornerPosition.y + (i + NUMBER_OF_CELLS_EACH_DIMENSION) * cellScale; + var z = cornerPosition.z; + var voxel = Voxels.getVoxelAt(x, y, z, cellScale); + if (voxel.x != x || voxel.y != y || voxel.z != z || voxel.s != cellScale || + voxel.red != regionMarkerColor.r || voxel.green != regionMarkerColor.g || voxel.blue != regionMarkerColor.b) { + regionMarkerX = x; + regionMarkerY = y; + regionMarkerI = i; + regionMarkerJ = j; + i = REGIONS_EACH_DIMENSION; //force quit loop + break; + } + } + } + + //Didnt find an open spot, end script + if (regionMarkerX == -1) { + Script.stop(); + } + + position.x = cornerPosition.x + regionMarkerJ * NUMBER_OF_CELLS_REGION_EACH_DIMESION; + position.y = cornerPosition.y + regionMarkerI * NUMBER_OF_CELLS_REGION_EACH_DIMESION; + position.z = cornerPosition.z; + + Voxels.setVoxel(regionMarkerX, regionMarkerY, cornerPosition.z, cellScale, regionMarkerColor.r, regionMarkerColor.g, regionMarkerColor.b); + + for (var i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) { + // create the array to hold this row + currentCells[i] = []; + + // create the array to hold this row in the nextCells array + nextCells[i] = []; + } + + var width = NUMBER_OF_CELLS_REGION_EACH_DIMESION / 2; + setRegionToColor(0, 0, width, width, 0); + setRegionToColor(0, width, width, width, 1); + setRegionToColor(width, width, width, width, 2); + setRegionToColor(width, 0, width, width, 3); +} + +function updateCells() { + var i = 0; + var j = 0; + var cell; + var y = 0; + var x = 0; + + for (i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) { + for (j = 0; j < NUMBER_OF_CELLS_REGION_EACH_DIMESION; j++) { + + cell = currentCells[i][j]; + + var r = Math.floor(Math.random() * 8); + + switch (r){ + case 0: + y = i - 1; + x = j - 1; + break; + case 1: + y = i; + x = j-1; + break; + case 2: + y = i + 1; + x = j - 1; + break; + case 3: + y = i + 1; + x = j; + break; + case 4: + y = i + 1; + x = j + 1; + break; + case 5: + y = i; + x = j + 1; + break; + case 6: + y = i - 1; + x = j + 1; + break; + case 7: + y = i - 1; + x = j; + break; + default: + continue; + + } + + //check the voxel grid instead of local array when on the edge + if (x == -1 || x == NUMBER_OF_CELLS_REGION_EACH_DIMESION || + y == -1 || y == NUMBER_OF_CELLS_REGION_EACH_DIMESION) { + + var voxel = Voxels.getVoxelAt(position.x + x * cellScale, position.y + y * cellScale, position.z, cellScale); + var predatorCellType = ((cell.type + 1) % cellTypes.length); + var predatorCellColor = cellTypes[predatorCellType]; + if (voxel.red == predatorCellColor.r && voxel.green == predatorCellColor.g && voxel.blue == predatorCellColor.b) { + nextCells[i][j].type = predatorCellType; + nextCells[i][j].changed = true; + } + } else { + + if (currentCells[y][x].type == ((cell.type + 1) % cellTypes.length)) { + nextCells[i][j].type = currentCells[y][x].type; + nextCells[i][j].changed = true; + } else { + //indicate no update + nextCells[i][j].changed = false; + } + } + } + } + + for (i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) { + for (j = 0; j < NUMBER_OF_CELLS_REGION_EACH_DIMESION; j++) { + if (nextCells[i][j].changed == true) { + // there has been a change to this cell, change the value in the currentCells array + currentCells[i][j].type = nextCells[i][j].type; + currentCells[i][j].changed = true; + } + } + } +} + +function sendNextCells() { + for (var i = 0; i < NUMBER_OF_CELLS_REGION_EACH_DIMESION; i++) { + for (var j = 0; j < NUMBER_OF_CELLS_REGION_EACH_DIMESION; j++) { + if (nextCells[i][j].changed == true) { + // there has been a change to the state of this cell, send it + + // find the x and y position for this voxel, z = 0 + var x = j * cellScale; + var y = i * cellScale; + var type = nextCells[i][j].type; + + // queue a packet to add a voxel for the new cell + Voxels.setVoxel(position.x + x, position.y + y, position.z, cellScale, cellTypes[type].r, cellTypes[type].g, cellTypes[type].b); + } + } + } +} + +var sentFirstBoard = false; +var voxelViewerInit = false; + +var UPDATES_PER_SECOND = 6.0; +var frameIndex = 1.0; +var oldFrameIndex = 0; + +var framesToWait = UPDATES_PER_SECOND; + +function step(deltaTime) { + + if (isLocal == false) { + if (voxelViewerInit == false) { + VoxelViewer.setPosition(viewerPosition); + VoxelViewer.setOrientation(orientation); + voxelViewerInit = true; + } + VoxelViewer.queryOctree(); + } + + frameIndex += deltaTime * UPDATES_PER_SECOND; + if (Math.floor(frameIndex) == oldFrameIndex) { + return; + } + oldFrameIndex++; + + if (frameIndex <= framesToWait) { + return; + } + + if (sentFirstBoard) { + // we've already sent the first full board, perform a step in time + updateCells(); + } else { + // this will be our first board send + sentFirstBoard = true; + init(); + } + + if (isLocal == false) { + VoxelViewer.queryOctree(); + } + sendNextCells(); +} + +function scriptEnding() { + Voxels.eraseVoxel(regionMarkerX, regionMarkerY, position.z, cellScale); +} + +Script.scriptEnding.connect(scriptEnding); + +Script.update.connect(step); +Voxels.setPacketsPerSecond(2000); + +// test for local... +Menu.isOptionChecked("Voxels"); +isLocal = true; // will only get here on local client \ No newline at end of file diff --git a/examples/sit.js b/examples/sit.js index 0f4b199855..072471aa30 100644 --- a/examples/sit.js +++ b/examples/sit.js @@ -89,7 +89,12 @@ var sittingDownAnimation = function(deltaTime) { var pos = { x: startPosition.x - 0.3 * factor, y: startPosition.y - 0.5 * factor, z: startPosition.z}; MyAvatar.position = pos; - } + } else { + Script.update.disconnect(sittingDownAnimation); + if (seat.model) { + MyAvatar.setModelReferential(seat.model.id); + } + } } var standingUpAnimation = function(deltaTime) { @@ -103,7 +108,10 @@ var standingUpAnimation = function(deltaTime) { var pos = { x: startPosition.x + 0.3 * (passedTime/animationLenght), y: startPosition.y + 0.5 * (passedTime/animationLenght), z: startPosition.z}; MyAvatar.position = pos; - } + } else { + Script.update.disconnect(standingUpAnimation); + + } } var goToSeatAnimation = function(deltaTime) { @@ -147,7 +155,8 @@ function standUp() { print("standUp sitting status: " + Settings.getValue(sittingSettingsHandle, false)); passedTime = 0.0; startPosition = MyAvatar.position; - try{ + MyAvatar.clearReferential(); + try{ Script.update.disconnect(sittingDownAnimation); } catch (e){} Script.update.connect(standingUpAnimation); @@ -197,8 +206,10 @@ Controller.mousePressEvent.connect(function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); if (clickedOverlay == sitDownButton) { + seat.model = null; sitDown(); } else if (clickedOverlay == standUpButton) { + seat.model = null; standUp(); } else { var pickRay = Camera.computePickRay(event.x, event.y); @@ -214,6 +225,7 @@ Controller.mousePressEvent.connect(function(event) { model.properties.sittingPoints[i].indicator.position, model.properties.sittingPoints[i].indicator.scale / 2)) { clickedOnSeat = true; + seat.model = model; seat.position = model.properties.sittingPoints[i].indicator.position; seat.rotation = model.properties.sittingPoints[i].indicator.orientation; } @@ -355,6 +367,7 @@ Script.update.connect(update); Controller.keyPressEvent.connect(keyPressEvent); Script.scriptEnding.connect(function() { + MyAvatar.clearReferential(); for (var i = 0; i < pose.length; i++){ MyAvatar.clearJointData(pose[i].joint); } diff --git a/examples/testXMLHttpRequest.js b/examples/testXMLHttpRequest.js index 421eb458e4..79d2842464 100644 --- a/examples/testXMLHttpRequest.js +++ b/examples/testXMLHttpRequest.js @@ -145,3 +145,98 @@ test("Test timeout", function() { this.assertEquals(0, req.status, "status should be `0`"); this.assertEquals(4, req.errorCode, "4 is the timeout error code for QNetworkReply::NetworkError"); }); + + +var localFile = Window.browse("Find defaultScripts.js file ...", "", "defaultScripts.js (defaultScripts.js)"); + +if (localFile !== null) { + + localFile = "file:///" + localFile; + + test("Test GET local file synchronously", function () { + var req = new XMLHttpRequest(); + + var statesVisited = [true, false, false, false, false] + req.onreadystatechange = function () { + statesVisited[req.readyState] = true; + }; + + req.open("GET", localFile, false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(200, req.status, "status should be `200`"); + this.assertEquals("OK", req.statusText, "statusText should be `OK`"); + this.assertEquals(0, req.errorCode); + this.assertNotEquals("", req.getAllResponseHeaders(), "headers should not be null"); + this.assertContains("High Fidelity", req.response.substring(0, 100), "expected text not found in response") + + for (var i = 0; i <= req.DONE; i++) { + this.assertEquals(true, statesVisited[i], i + " should be set"); + } + }); + + test("Test GET nonexistent local file", function () { + var nonexistentFile = localFile.replace(".js", "NoExist.js"); + + var req = new XMLHttpRequest(); + req.open("GET", nonexistentFile, false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(404, req.status, "status should be `404`"); + this.assertEquals("Not Found", req.statusText, "statusText should be `Not Found`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test GET local file already open", function () { + // Can't open file exclusively in order to test. + }); + + test("Test GET local file with data not implemented", function () { + var req = new XMLHttpRequest(); + req.open("GET", localFile, true); + req.send("data"); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test GET local file asynchronously not implemented", function () { + var req = new XMLHttpRequest(); + req.open("GET", localFile, true); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test POST local file not implemented", function () { + var req = new XMLHttpRequest(); + req.open("POST", localFile, false); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + + test("Test local file username and password not implemented", function () { + var req = new XMLHttpRequest(); + req.open("GET", localFile, false, "username", "password"); + req.send(); + + this.assertEquals(req.DONE, req.readyState, "readyState should be DONE"); + this.assertEquals(501, req.status, "status should be `501`"); + this.assertEquals("Not Implemented", req.statusText, "statusText should be `Not Implemented`"); + this.assertNotEquals(0, req.errorCode); + }); + +} else { + print("Local file operation not tested"); +} diff --git a/examples/toolBars.js b/examples/toolBars.js index 1a464b4e4f..ede3b80062 100644 --- a/examples/toolBars.js +++ b/examples/toolBars.js @@ -186,6 +186,14 @@ ToolBar = function(x, y, direction) { return this.tools.length; } + this.selectTool = function (tool, select) { + this.tools[tool].select(select); + } + + this.toolSelected = function (tool) { + return this.tools[tool].selected(); + } + this.cleanup = function() { for(var tool in this.tools) { this.tools[tool].cleanup(); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 18d5b9bd70..b8f25a3f2c 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -1,18 +1,13 @@ -set(ROOT_DIR ..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") - set(TARGET_NAME interface) project(${TARGET_NAME}) -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/modules/") - # set a default root dir for each of our optional externals if it was not passed -set(OPTIONAL_EXTERNALS "faceplus" "faceshift" "oculus" "priovr" "sixense" "visage" "leapmotion" "rtmidi" "qxmpp") +set(OPTIONAL_EXTERNALS "Faceplus" "Faceshift" "LibOVR" "PrioVR" "Sixense" "Visage" "LeapMotion" "RtMidi" "Qxmpp") foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) - string(TOUPPER ${EXTERNAL} UPPER_EXTERNAL) - if (NOT ${UPPER_EXTERNAL}_ROOT_DIR) - set(${UPPER_EXTERNAL}_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/${EXTERNAL}") + string(TOUPPER ${EXTERNAL} ${EXTERNAL}_UPPERCASE) + if (NOT ${${EXTERNAL}_UPPERCASE}_ROOT_DIR) + string(TOLOWER ${EXTERNAL} ${EXTERNAL}_LOWERCASE) + set(${${EXTERNAL}_UPPERCASE}_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/${${EXTERNAL}_LOWERCASE}") endif () endforeach() @@ -38,8 +33,7 @@ elseif (WIN32) endif () # set up the external glm library -include("${MACRO_DIR}/IncludeGLM.cmake") -include_glm(${TARGET_NAME} "${ROOT_DIR}") +include_glm() # create the InterfaceConfig.h file based on GL_HEADERS above configure_file(InterfaceConfig.h.in "${PROJECT_BINARY_DIR}/includes/InterfaceConfig.h") @@ -61,7 +55,7 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_H}) endif () -find_package(Qt5 COMPONENTS Core Gui Multimedia Network OpenGL Script Svg WebKit WebKitWidgets Xml UiTools) +find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Script Svg WebKitWidgets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -106,60 +100,36 @@ endif() # create the executable, make it a bundle on OS X add_executable(${TARGET_NAME} MACOSX_BUNDLE ${INTERFACE_SRCS} ${QM}) -# link in the hifi shared library -include(${MACRO_DIR}/LinkHifiLibrary.cmake) - # link required hifi libraries -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") -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}") +link_hifi_libraries(shared octree voxels fbx metavoxels networking particles models avatars audio animation script-engine) # find any optional and required libraries -find_package(Faceplus) -find_package(Faceshift) -find_package(LibOVR) -find_package(PrioVR) -find_package(SDL) -find_package(Sixense) -find_package(Visage) -find_package(LeapMotion) -find_package(ZLIB) -find_package(Qxmpp) -find_package(RtMidi) +find_package(ZLIB REQUIRED) find_package(OpenSSL REQUIRED) # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) - string(TOUPPER ${EXTERNAL} UPPER_EXTERNAL) - if (${UPPER_EXTERNAL} MATCHES "OCULUS") - # the oculus directory is named OCULUS and not LIBOVR so hack to fix that here - set(UPPER_EXTERNAL "LIBOVR") + if (${${EXTERNAL}_UPPERCASE}_REQUIRED) + find_package(${EXTERNAL} REQUIRED) + else () + find_package(${EXTERNAL}) endif () - if (${UPPER_EXTERNAL}_FOUND AND NOT DISABLE_${UPPER_EXTERNAL}) - add_definitions(-DHAVE_${UPPER_EXTERNAL}) + if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE}) + add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE}) # include the library directories (ignoring warnings) - include_directories(SYSTEM ${${UPPER_EXTERNAL}_INCLUDE_DIRS}) + include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) # perform the system include hack for OS X to ignore warnings if (APPLE) - foreach(EXTERNAL_INCLUDE_DIR ${${UPPER_EXTERNAL}_INCLUDE_DIRS}) + foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") endforeach() endif () - target_link_libraries(${TARGET_NAME} ${${UPPER_EXTERNAL}_LIBRARIES}) + target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES}) endif () endforeach() @@ -188,17 +158,11 @@ endif () # include headers for interface and InterfaceConfig. include_directories("${PROJECT_SOURCE_DIR}/src" "${PROJECT_BINARY_DIR}/includes") - -# include external library headers -# use system flag so warnings are supressed -include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") +include_directories("${OPENSSL_INCLUDE_DIR}") target_link_libraries( - ${TARGET_NAME} - "${ZLIB_LIBRARIES}" - ${OPENSSL_LIBRARIES} - Qt5::Core Qt5::Gui Qt5::Multimedia Qt5::Network Qt5::OpenGL - Qt5::Script Qt5::Svg Qt5::WebKit Qt5::WebKitWidgets Qt5::Xml Qt5::UiTools + ${TARGET_NAME} ${ZLIB_LIBRARIES} ${OPENSSL_LIBRARIES} + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets ) # assume we are using a Qt build without bearer management @@ -206,30 +170,12 @@ add_definitions(-DQT_NO_BEARERMANAGEMENT) if (APPLE) # link in required OS X frameworks and include the right GL headers - find_library(AppKit AppKit) find_library(CoreAudio CoreAudio) - find_library(CoreServices CoreServices) - find_library(Carbon Carbon) - find_library(Foundation Foundation) + find_library(CoreFoundation CoreFoundation) find_library(GLUT GLUT) find_library(OpenGL OpenGL) - find_library(IOKit IOKit) - find_library(QTKit QTKit) - find_library(QuartzCore QuartzCore) - target_link_libraries( - ${TARGET_NAME} - ${AppKit} - ${CoreAudio} - ${CoreServices} - ${Carbon} - ${Foundation} - ${GLUT} - ${OpenGL} - ${IOKit} - ${QTKit} - ${QuartzCore} - ) + target_link_libraries(${TARGET_NAME} ${CoreAudio} ${CoreFoundation} ${GLUT} ${OpenGL}) # install command for OS X bundle INSTALL(TARGETS ${TARGET_NAME} @@ -263,9 +209,9 @@ else (APPLE) # we're using static GLEW, so define GLEW_STATIC add_definitions(-DGLEW_STATIC) - # add a definition for ssize_t so that windows doesn't bail - add_definitions(-Dssize_t=long) - target_link_libraries(${TARGET_NAME} "${GLEW_LIBRARIES}" wsock32.lib opengl32.lib) endif() endif (APPLE) + +# link any dependencies bubbled up from our linked dependencies +link_shared_dependencies() diff --git a/interface/external/oculus/readme.txt b/interface/external/libovr/readme.txt similarity index 100% rename from interface/external/oculus/readme.txt rename to interface/external/libovr/readme.txt diff --git a/interface/interface_en.ts b/interface/interface_en.ts index 6c4426b2c6..b85628b104 100644 --- a/interface/interface_en.ts +++ b/interface/interface_en.ts @@ -245,28 +245,28 @@ QObject - - + + Import Voxels - + Loading ... - + Place voxels - + <b>Import</b> %1 as voxels - + Cancel diff --git a/interface/resources/shaders/metavoxel_heightfield.frag b/interface/resources/shaders/metavoxel_heightfield.frag new file mode 100644 index 0000000000..f99a0a6403 --- /dev/null +++ b/interface/resources/shaders/metavoxel_heightfield.frag @@ -0,0 +1,25 @@ +#version 120 + +// +// metavoxel_heightfield.frag +// fragment shader +// +// Created by Andrzej Kapolka on 7/28/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 +// + +// the diffuse texture +uniform sampler2D diffuseMap; + +// the interpolated normal +varying vec4 normal; + +void main(void) { + // compute the base color based on OpenGL lighting model + vec4 base = gl_Color * (gl_FrontLightModelProduct.sceneColor + gl_FrontLightProduct[0].ambient + + gl_FrontLightProduct[0].diffuse * max(0.0, dot(normalize(normal), gl_LightSource[0].position))); + gl_FragColor = base * texture2D(diffuseMap, gl_TexCoord[0].st); +} diff --git a/interface/resources/shaders/metavoxel_heightfield.vert b/interface/resources/shaders/metavoxel_heightfield.vert new file mode 100644 index 0000000000..70cf3f9419 --- /dev/null +++ b/interface/resources/shaders/metavoxel_heightfield.vert @@ -0,0 +1,51 @@ +#version 120 + +// +// metavoxel_heighfield.vert +// vertex shader +// +// Created by Andrzej Kapolka on 7/28/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 +// + +// the height texture +uniform sampler2D heightMap; + +// the distance between height points in texture space +uniform float heightScale; + +// the scale between height and color textures +uniform float colorScale; + +// the interpolated position +varying vec4 position; + +// the interpolated normal +varying vec4 normal; + +void main(void) { + // transform and store the normal for interpolation + vec2 heightCoord = gl_MultiTexCoord0.st; + float deltaX = texture2D(heightMap, heightCoord - vec2(heightScale, 0.0)).r - + texture2D(heightMap, heightCoord + vec2(heightScale, 0.0)).r; + float deltaZ = texture2D(heightMap, heightCoord - vec2(0.0, heightScale)).r - + texture2D(heightMap, heightCoord + vec2(0.0, heightScale)).r; + normal = normalize(gl_ModelViewMatrix * vec4(deltaX, heightScale, deltaZ, 0.0)); + + // add the height to the position + float height = texture2D(heightMap, heightCoord).r; + position = gl_ModelViewMatrix * (gl_Vertex + vec4(0.0, height, 0.0, 0.0)); + gl_Position = gl_ProjectionMatrix * position; + + // the zero height should be invisible + gl_FrontColor = vec4(1.0, 1.0, 1.0, step(height, 0.0)); + + // pass along the scaled/offset texture coordinates + gl_TexCoord[0] = (gl_MultiTexCoord0 - vec4(heightScale, heightScale, 0.0, 0.0)) * colorScale; + + // and the shadow texture coordinates + gl_TexCoord[1] = vec4(dot(gl_EyePlaneS[0], position), dot(gl_EyePlaneT[0], position), dot(gl_EyePlaneR[0], position), 1.0); +} diff --git a/interface/resources/shaders/metavoxel_heightfield_cascaded_shadow_map.frag b/interface/resources/shaders/metavoxel_heightfield_cascaded_shadow_map.frag new file mode 100644 index 0000000000..059a4e0296 --- /dev/null +++ b/interface/resources/shaders/metavoxel_heightfield_cascaded_shadow_map.frag @@ -0,0 +1,48 @@ +#version 120 + +// +// metavoxel_heightfield.frag +// fragment shader +// +// Created by Andrzej Kapolka on 7/28/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 +// + +// the diffuse texture +uniform sampler2D diffuseMap; + +// the shadow texture +uniform sampler2DShadow shadowMap; + +// the distances to the cascade sections +uniform vec3 shadowDistances; + +// the inverse of the size of the shadow map +const float shadowScale = 1.0 / 2048.0; + +// the interpolated position +varying vec4 position; + +// the interpolated normal +varying vec4 normal; + +void main(void) { + // compute the index of the cascade to use and the corresponding texture coordinates + int shadowIndex = int(dot(step(vec3(position.z), shadowDistances), vec3(1.0, 1.0, 1.0))); + vec3 shadowTexCoord = vec3(dot(gl_EyePlaneS[shadowIndex], position), dot(gl_EyePlaneT[shadowIndex], position), + dot(gl_EyePlaneR[shadowIndex], position)); + + // compute the base color based on OpenGL lighting model + float diffuse = dot(normalize(normal), gl_LightSource[0].position); + float facingLight = step(0.0, diffuse) * 0.25 * + (shadow2D(shadowMap, shadowTexCoord + vec3(-shadowScale, -shadowScale, 0.0)).r + + shadow2D(shadowMap, shadowTexCoord + vec3(-shadowScale, shadowScale, 0.0)).r + + shadow2D(shadowMap, shadowTexCoord + vec3(shadowScale, -shadowScale, 0.0)).r + + shadow2D(shadowMap, shadowTexCoord + vec3(shadowScale, shadowScale, 0.0)).r); + vec4 base = gl_Color * (gl_FrontLightModelProduct.sceneColor + gl_FrontLightProduct[0].ambient + + gl_FrontLightProduct[0].diffuse * (diffuse * facingLight)); + gl_FragColor = base * texture2D(diffuseMap, gl_TexCoord[0].st); +} diff --git a/interface/resources/shaders/metavoxel_heightfield_cursor.frag b/interface/resources/shaders/metavoxel_heightfield_cursor.frag new file mode 100644 index 0000000000..0bb5e21e7d --- /dev/null +++ b/interface/resources/shaders/metavoxel_heightfield_cursor.frag @@ -0,0 +1,32 @@ +#version 120 + +// +// metavoxel_heightfield_cursor.frag +// fragment shader +// +// Created by Andrzej Kapolka on 8/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 +// + +// the inner radius of the outline, squared +const float SQUARED_OUTLINE_INNER_RADIUS = 0.81; + +// the outer radius of the outline, squared +const float SQUARED_OUTLINE_OUTER_RADIUS = 1.0; + +// the inner radius of the inset, squared +const float SQUARED_INSET_INNER_RADIUS = 0.855625; + +// the outer radius of the inset, squared +const float SQUARED_INSET_OUTER_RADIUS = 0.950625; + +void main(void) { + // use the distance to compute the ring color, then multiply it by the varying color + float squaredDistance = dot(gl_TexCoord[0].st, gl_TexCoord[0].st); + float alpha = step(SQUARED_OUTLINE_INNER_RADIUS, squaredDistance) * step(squaredDistance, SQUARED_OUTLINE_OUTER_RADIUS); + float white = step(SQUARED_INSET_INNER_RADIUS, squaredDistance) * step(squaredDistance, SQUARED_INSET_OUTER_RADIUS); + gl_FragColor = gl_Color * vec4(white, white, white, alpha); +} diff --git a/interface/resources/shaders/metavoxel_heightfield_cursor.vert b/interface/resources/shaders/metavoxel_heightfield_cursor.vert new file mode 100644 index 0000000000..93ed36449e --- /dev/null +++ b/interface/resources/shaders/metavoxel_heightfield_cursor.vert @@ -0,0 +1,28 @@ +#version 120 + +// +// metavoxel_heighfield_cursor.vert +// vertex shader +// +// Created by Andrzej Kapolka on 8/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 +// + +// the height texture +uniform sampler2D heightMap; + +void main(void) { + // compute the view space coordinates + float height = texture2D(heightMap, gl_MultiTexCoord0.st).r; + vec4 viewPosition = gl_ModelViewMatrix * (gl_Vertex + vec4(0.0, height, 0.0, 0.0)); + gl_Position = gl_ProjectionMatrix * viewPosition; + + // generate the texture coordinates from the view position + gl_TexCoord[0] = vec4(dot(viewPosition, gl_EyePlaneS[4]), dot(viewPosition, gl_EyePlaneT[4]), 0.0, 1.0); + + // the zero height should be invisible + gl_FrontColor = vec4(1.0, 1.0, 1.0, 1.0 - step(height, 0.0)); +} diff --git a/interface/resources/shaders/metavoxel_heightfield_shadow_map.frag b/interface/resources/shaders/metavoxel_heightfield_shadow_map.frag new file mode 100644 index 0000000000..bf319462d3 --- /dev/null +++ b/interface/resources/shaders/metavoxel_heightfield_shadow_map.frag @@ -0,0 +1,37 @@ +#version 120 + +// +// metavoxel_heightfield.frag +// fragment shader +// +// Created by Andrzej Kapolka on 7/28/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 +// + +// the diffuse texture +uniform sampler2D diffuseMap; + +// the shadow texture +uniform sampler2DShadow shadowMap; + +// the inverse of the size of the shadow map +const float shadowScale = 1.0 / 2048.0; + +// the interpolated normal +varying vec4 normal; + +void main(void) { + // compute the base color based on OpenGL lighting model + float diffuse = dot(normalize(normal), gl_LightSource[0].position); + float facingLight = step(0.0, diffuse) * 0.25 * + (shadow2D(shadowMap, gl_TexCoord[1].stp + vec3(-shadowScale, -shadowScale, 0.0)).r + + shadow2D(shadowMap, gl_TexCoord[1].stp + vec3(-shadowScale, shadowScale, 0.0)).r + + shadow2D(shadowMap, gl_TexCoord[1].stp + vec3(shadowScale, -shadowScale, 0.0)).r + + shadow2D(shadowMap, gl_TexCoord[1].stp + vec3(shadowScale, shadowScale, 0.0)).r); + vec4 base = gl_Color * (gl_FrontLightModelProduct.sceneColor + gl_FrontLightProduct[0].ambient + + gl_FrontLightProduct[0].diffuse * (diffuse * facingLight)); + gl_FragColor = base * texture2D(diffuseMap, gl_TexCoord[0].st); +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f303cc1c5b..d3b1e81078 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -69,6 +69,7 @@ #include "InterfaceVersion.h" #include "Menu.h" #include "ModelUploader.h" +#include "PaymentManager.h" #include "Util.h" #include "devices/MIDIManager.h" #include "devices/OculusManager.h" @@ -136,9 +137,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _frameCount(0), _fps(60.0f), _justStarted(true), - _voxelImporter(NULL), + _voxelImportDialog(NULL), + _voxelImporter(), _importSucceded(false), _sharedVoxelSystem(TREE_SCALE, DEFAULT_MAX_VOXELS_PER_SYSTEM, &_clipboard), + _modelClipboardRenderer(), + _modelClipboard(), _wantToKillLocalVoxels(false), _viewFrustum(), _lastQueriedViewFrustum(), @@ -173,6 +177,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _lastNackTime(usecTimestampNow()), _lastSendDownstreamAudioStats(usecTimestampNow()) { + // read the ApplicationInfo.ini file for Name/Version/Domain information QSettings applicationInfo(Application::resourcesPath() + "info/ApplicationInfo.ini", QSettings::IniFormat); @@ -233,9 +238,19 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : connect(audioThread, SIGNAL(started()), &_audio, SLOT(start())); audioThread->start(); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); - connect(&nodeList->getDomainHandler(), SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); - connect(&nodeList->getDomainHandler(), SIGNAL(connectedToDomain(const QString&)), SLOT(connectedToDomain(const QString&))); + connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); + connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(connectedToDomain(const QString&))); + connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); + connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); + connect(&domainHandler, &DomainHandler::settingsReceived, this, &Application::domainSettingsReceived); + + // hookup VoxelEditSender to PaymentManager so we can pay for octree edits + const PaymentManager& paymentManager = PaymentManager::getInstance(); + connect(&_voxelEditSender, &VoxelEditPacketSender::octreePaymentRequired, + &paymentManager, &PaymentManager::sendSignedPayment); // update our location every 5 seconds in the data-server, assuming that we are authenticated with one const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * 1000; @@ -249,8 +264,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : connect(nodeList, SIGNAL(nodeKilled(SharedNodePointer)), SLOT(nodeKilled(SharedNodePointer))); connect(nodeList, SIGNAL(nodeAdded(SharedNodePointer)), &_voxels, SLOT(nodeAdded(SharedNodePointer))); connect(nodeList, SIGNAL(nodeKilled(SharedNodePointer)), &_voxels, SLOT(nodeKilled(SharedNodePointer))); - connect(nodeList, &NodeList::uuidChanged, this, &Application::updateWindowTitle); - connect(nodeList, SIGNAL(uuidChanged(const QUuid&)), _myAvatar, SLOT(setSessionUUID(const QUuid&))); + connect(nodeList, &NodeList::uuidChanged, _myAvatar, &MyAvatar::setSessionUUID); connect(nodeList, &NodeList::limitOfSilentDomainCheckInsReached, nodeList, &NodeList::reset); // connect to appropriate slots on AccountManager @@ -419,7 +433,7 @@ Application::~Application() { delete idleTimer; _sharedVoxelSystem.changeTree(new VoxelTree); - delete _voxelImporter; + delete _voxelImportDialog; // let the avatar mixer know we're out MyAvatar::sendKillAvatar(); @@ -454,8 +468,8 @@ void Application::saveSettings() { Menu::getInstance()->saveSettings(); _rearMirrorTools->saveSettings(_settings); - if (_voxelImporter) { - _voxelImporter->saveSettings(_settings); + if (_voxelImportDialog) { + _voxelImportDialog->saveSettings(_settings); } _settings->sync(); _numChangedSettings = 0; @@ -588,7 +602,7 @@ void Application::paintGL() { if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { _myCamera.setTightness(0.0f); // In first person, camera follows (untweaked) head exactly without delay - _myCamera.setTargetPosition(_myAvatar->getHead()->getFilteredEyePosition()); + _myCamera.setTargetPosition(_myAvatar->getHead()->getEyePosition()); _myCamera.setTargetRotation(_myAvatar->getHead()->getCameraOrientation()); } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { @@ -610,7 +624,7 @@ void Application::paintGL() { _myCamera.setTargetPosition(_myAvatar->getHead()->getEyePosition() + glm::vec3(0, _raiseMirror * _myAvatar->getScale(), 0)); } else { _myCamera.setTightness(0.0f); - glm::vec3 eyePosition = _myAvatar->getHead()->getFilteredEyePosition(); + glm::vec3 eyePosition = _myAvatar->getHead()->getEyePosition(); float headHeight = eyePosition.y - _myAvatar->getPosition().y; _myCamera.setDistance(MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); _myCamera.setTargetPosition(_myAvatar->getPosition() + glm::vec3(0, headHeight + (_raiseMirror * _myAvatar->getScale()), 0)); @@ -803,9 +817,8 @@ bool Application::event(QEvent* event) { QFileOpenEvent* fileEvent = static_cast(event); bool isHifiSchemeURL = !fileEvent->url().isEmpty() && fileEvent->url().toLocalFile().startsWith(CUSTOM_URL_SCHEME); if (isHifiSchemeURL) { - Menu::getInstance()->goTo(fileEvent->url().toString()); + Menu::getInstance()->goToURL(fileEvent->url().toLocalFile()); } - return false; } return QApplication::event(event); @@ -1038,9 +1051,22 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_R: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::FrustumRenderMode); + } else if (isMeta) { + if (_myAvatar->isRecording()) { + _myAvatar->stopRecording(); + } else { + _myAvatar->startRecording(); + _audio.setRecorder(_myAvatar->getRecorder()); + } + } else { + if (_myAvatar->isPlaying()) { + _myAvatar->stopPlaying(); + } else { + _myAvatar->startPlaying(); + _audio.setPlayer(_myAvatar->getPlayer()); + } } break; - break; case Qt::Key_Percent: Menu::getInstance()->triggerOption(MenuOption::Stats); break; @@ -1429,6 +1455,15 @@ void Application::setEnable3DTVMode(bool enable3DTVMode) { } void Application::setEnableVRMode(bool enableVRMode) { + if (enableVRMode) { + if (!OculusManager::isConnected()) { + // attempt to reconnect the Oculus manager - it's possible this was a workaround + // for the sixense crash + OculusManager::disconnect(); + OculusManager::connect(); + } + } + resizeGL(_glWidget->width(), _glWidget->height()); } @@ -1490,16 +1525,44 @@ glm::vec3 Application::getMouseVoxelWorldCoordinates(const VoxelDetail& mouseVox } FaceTracker* Application::getActiveFaceTracker() { - return _cara.isActive() ? static_cast(&_cara) : - (_faceshift.isActive() ? static_cast(&_faceshift) : - (_faceplus.isActive() ? static_cast(&_faceplus) : - (_visage.isActive() ? static_cast(&_visage) : NULL))); + return (_dde.isActive() ? static_cast(&_dde) : + (_cara.isActive() ? static_cast(&_cara) : + (_faceshift.isActive() ? static_cast(&_faceshift) : + (_faceplus.isActive() ? static_cast(&_faceplus) : + (_visage.isActive() ? static_cast(&_visage) : NULL))))); } struct SendVoxelsOperationArgs { const unsigned char* newBaseOctCode; }; +bool Application::exportModels(const QString& filename, float x, float y, float z, float scale) { + QVector models; + _models.getTree()->findModelsInCube(AACube(glm::vec3(x / (float)TREE_SCALE, y / (float)TREE_SCALE, z / (float)TREE_SCALE), scale / (float)TREE_SCALE), models); + if (models.size() > 0) { + glm::vec3 root(x, y, z); + ModelTree exportTree; + + for (int i = 0; i < models.size(); i++) { + ModelItemProperties properties; + ModelItemID id = models.at(i)->getModelItemID(); + id.isKnownID = false; + properties.copyFromNewModelItem(*models.at(i)); + properties.setPosition(properties.getPosition() - root); + exportTree.addModel(id, properties); + } + + exportTree.writeToSVOFile(filename.toLocal8Bit().constData()); + } else { + qDebug() << "No models were selected"; + return false; + } + + // restore the main window's active state + _window->activateWindow(); + return true; +} + bool Application::sendVoxelsOperation(OctreeElement* element, void* extraData) { VoxelTreeElement* voxel = (VoxelTreeElement*)element; SendVoxelsOperationArgs* args = (SendVoxelsOperationArgs*)extraData; @@ -1559,17 +1622,17 @@ void Application::exportVoxels(const VoxelDetail& sourceVoxel) { void Application::importVoxels() { _importSucceded = false; - if (!_voxelImporter) { - _voxelImporter = new VoxelImporter(_window); - _voxelImporter->loadSettings(_settings); + if (!_voxelImportDialog) { + _voxelImportDialog = new VoxelImportDialog(_window); + _voxelImportDialog->loadSettings(_settings); } - if (!_voxelImporter->exec()) { + if (!_voxelImportDialog->exec()) { qDebug() << "Import succeeded." << endl; _importSucceded = true; } else { qDebug() << "Import failed." << endl; - if (_sharedVoxelSystem.getTree() == _voxelImporter->getVoxelTree()) { + if (_sharedVoxelSystem.getTree() == _voxelImporter.getVoxelTree()) { _sharedVoxelSystem.killLocalVoxels(); _sharedVoxelSystem.changeTree(&_clipboard); } @@ -1581,6 +1644,19 @@ void Application::importVoxels() { emit importDone(); } +bool Application::importModels(const QString& filename) { + _modelClipboard.eraseAllOctreeElements(); + bool success = _modelClipboard.readFromSVOFile(filename.toLocal8Bit().constData()); + if (success) { + _modelClipboard.reaverageOctreeElements(); + } + return success; +} + +void Application::pasteModels(float x, float y, float z) { + _modelClipboard.sendModels(&_modelEditSender, x, y, z); +} + void Application::cutVoxels(const VoxelDetail& sourceVoxel) { copyVoxels(sourceVoxel); deleteVoxelAt(sourceVoxel); @@ -1671,7 +1747,7 @@ void Application::init() { // Cleanup of the original shared tree _sharedVoxelSystem.init(); - _voxelImporter = new VoxelImporter(_window); + _voxelImportDialog = new VoxelImportDialog(_window); _environment.init(); @@ -1744,6 +1820,10 @@ void Application::init() { _models.init(); _models.setViewFrustum(getViewFrustum()); + _modelClipboardRenderer.init(); + _modelClipboardRenderer.setViewFrustum(getViewFrustum()); + _modelClipboardRenderer.setTree(&_modelClipboard); + _metavoxels.init(); _particleCollisionSystem.init(&_particleEditSender, _particles.getTree(), _voxels.getTree(), &_audio, &_avatarManager); @@ -1881,13 +1961,21 @@ void Application::updateVisage() { _visage.update(); } +void Application::updateDDE() { + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateDDE()"); + + // Update Cara + _dde.update(); +} + void Application::updateCara() { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateCara()"); - + // Update Cara _cara.update(); - + // Copy angular velocity if measured by cara, to the head if (_cara.isActive()) { _myAvatar->getHead()->setAngularVelocity(_cara.getHeadAngularVelocity()); @@ -2074,12 +2162,6 @@ void Application::update(float deltaTime) { _prioVR.update(deltaTime); } - { - PerformanceTimer perfTimer("myAvatar"); - updateMyAvatarLookAtPosition(); - updateMyAvatar(deltaTime); // Sample hardware, update view frustum if needed, and send avatar data to mixer/nodes - } - // Dispatch input events _controllerScriptingInterface.updateInputControllers(); @@ -2111,6 +2193,12 @@ void Application::update(float deltaTime) { PerformanceTimer perfTimer("overlays"); _overlays.update(deltaTime); } + + { + PerformanceTimer perfTimer("myAvatar"); + updateMyAvatarLookAtPosition(); + updateMyAvatar(deltaTime); // Sample hardware, update view frustum if needed, and send avatar data to mixer/nodes + } { PerformanceTimer perfTimer("emitSimulating"); @@ -3209,6 +3297,7 @@ void Application::resetSensors() { _faceplus.reset(); _faceshift.reset(); _visage.reset(); + _dde.reset(); OculusManager::reset(); @@ -3255,9 +3344,10 @@ void Application::updateWindowTitle(){ QString buildVersion = " (build " + applicationVersion() + ")"; NodeList* nodeList = NodeList::getInstance(); + QString connectionStatus = nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED) "; QString username = AccountManager::getInstance().getAccountInfo().getUsername(); QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) - + nodeList->getDomainHandler().getHostname() + buildVersion; + + nodeList->getDomainHandler().getHostname() + connectionStatus + buildVersion; AccountManager& accountManager = AccountManager::getInstance(); if (accountManager.getAccountInfo().hasBalance()) { @@ -3669,6 +3759,8 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript connect(scriptEngine, SIGNAL(finished(const QString&)), this, SLOT(scriptFinished(const QString&))); + connect(scriptEngine, SIGNAL(loadScript(const QString&)), this, SLOT(loadScript(const QString&))); + scriptEngine->registerGlobalObject("Overlays", &_overlays); QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", WindowScriptingInterface::getInstance()); @@ -3795,6 +3887,38 @@ void Application::uploadAttachment() { uploadModel(ATTACHMENT_MODEL); } +void Application::domainSettingsReceived(const QJsonObject& domainSettingsObject) { + + // from the domain-handler, figure out the satoshi cost per voxel and per meter cubed + const QString VOXEL_SETTINGS_KEY = "voxels"; + const QString PER_VOXEL_COST_KEY = "per-voxel-credits"; + const QString PER_METER_CUBED_COST_KEY = "per-meter-cubed-credits"; + const QString VOXEL_WALLET_UUID = "voxel-wallet"; + + const QJsonObject& voxelObject = domainSettingsObject[VOXEL_SETTINGS_KEY].toObject(); + + qint64 satoshisPerVoxel = 0; + qint64 satoshisPerMeterCubed = 0; + QUuid voxelWalletUUID; + + if (!domainSettingsObject.isEmpty()) { + float perVoxelCredits = (float) voxelObject[PER_VOXEL_COST_KEY].toDouble(); + float perMeterCubedCredits = (float) voxelObject[PER_METER_CUBED_COST_KEY].toDouble(); + + satoshisPerVoxel = (qint64) floorf(perVoxelCredits * SATOSHIS_PER_CREDIT); + satoshisPerMeterCubed = (qint64) floorf(perMeterCubedCredits * SATOSHIS_PER_CREDIT); + + voxelWalletUUID = QUuid(voxelObject[VOXEL_WALLET_UUID].toString()); + } + + qDebug() << "Voxel costs are" << satoshisPerVoxel << "per voxel and" << satoshisPerMeterCubed << "per meter cubed"; + qDebug() << "Destination wallet UUID for voxel payments is" << voxelWalletUUID; + + _voxelEditSender.setSatoshisPerVoxel(satoshisPerVoxel); + _voxelEditSender.setSatoshisPerMeterCubed(satoshisPerMeterCubed); + _voxelEditSender.setDestinationWalletUUID(voxelWalletUUID); +} + QString Application::getPreviousScriptLocation() { QString suggestedName; if (_previousScriptLocation.isEmpty()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index a356b26725..bdbfaf3ac5 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -64,6 +64,7 @@ #include "devices/SixenseManager.h" #include "devices/Visage.h" #include "devices/CaraFaceTracker.h" +#include "devices/DdeFaceTracker.h" #include "models/ModelTreeRenderer.h" #include "particles/ParticleTreeRenderer.h" #include "renderer/AmbientOcclusionEffect.h" @@ -86,6 +87,7 @@ #include "ui/overlays/Overlays.h" #include "ui/ApplicationOverlay.h" #include "ui/RunningScriptsWidget.h" +#include "ui/VoxelImportDialog.h" #include "voxels/VoxelFade.h" #include "voxels/VoxelHideShowThread.h" #include "voxels/VoxelImporter.h" @@ -192,6 +194,7 @@ public: Camera* getCamera() { return &_myCamera; } ViewFrustum* getViewFrustum() { return &_viewFrustum; } ViewFrustum* getShadowViewFrustum() { return &_shadowViewFrustum; } + VoxelImporter* getVoxelImporter() { return &_voxelImporter; } VoxelSystem* getVoxels() { return &_voxels; } VoxelTree* getVoxelTree() { return _voxels.getTree(); } const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; } @@ -201,6 +204,8 @@ public: bool getImportSucceded() { return _importSucceded; } VoxelSystem* getSharedVoxelSystem() { return &_sharedVoxelSystem; } VoxelTree* getClipboard() { return &_clipboard; } + ModelTree* getModelClipboard() { return &_modelClipboard; } + ModelTreeRenderer* getModelClipboardRenderer() { return &_modelClipboardRenderer; } Environment* getEnvironment() { return &_environment; } bool isMousePressed() const { return _mousePressed; } bool isMouseHidden() const { return _mouseHidden; } @@ -212,6 +217,7 @@ public: Faceplus* getFaceplus() { return &_faceplus; } Faceshift* getFaceshift() { return &_faceshift; } Visage* getVisage() { return &_visage; } + DdeFaceTracker* getDDE() { return &_dde; } CaraFaceTracker* getCara() { return &_cara; } FaceTracker* getActiveFaceTracker(); SixenseManager* getSixenseManager() { return &_sixenseManager; } @@ -227,6 +233,7 @@ public: float getPacketsPerSecond() const { return _packetsPerSecond; } float getBytesPerSecond() const { return _bytesPerSecond; } const glm::vec3& getViewMatrixTranslation() const { return _viewMatrixTranslation; } + void setViewMatrixTranslation(const glm::vec3& translation) { _viewMatrixTranslation = translation; } /// if you need to access the application settings, use lockSettings()/unlockSettings() QSettings* lockSettings() { _settingsMutex.lock(); return _settings; } @@ -314,6 +321,10 @@ public slots: void nodeKilled(SharedNodePointer node); void packetSent(quint64 length); + void pasteModels(float x, float y, float z); + bool exportModels(const QString& filename, float x, float y, float z, float scale); + bool importModels(const QString& filename); + void importVoxels(); // doesn't include source voxel because it goes to clipboard void cutVoxels(const VoxelDetail& sourceVoxel); void copyVoxels(const VoxelDetail& sourceVoxel); @@ -342,6 +353,8 @@ public slots: void uploadAttachment(); void bumpSettings() { ++_numChangedSettings; } + + void domainSettingsReceived(const QJsonObject& domainSettingsObject); private slots: void timer(); @@ -384,6 +397,7 @@ private: void updateFaceplus(); void updateFaceshift(); void updateVisage(); + void updateDDE(); void updateCara(); void updateMyAvatarLookAtPosition(); void updateThreads(float deltaTime); @@ -451,7 +465,8 @@ private: VoxelSystem _voxels; VoxelTree _clipboard; // if I copy/paste - VoxelImporter* _voxelImporter; + VoxelImportDialog* _voxelImportDialog; + VoxelImporter _voxelImporter; bool _importSucceded; VoxelSystem _sharedVoxelSystem; ViewFrustum _sharedVoxelSystemViewFrustum; @@ -460,6 +475,8 @@ private: ParticleCollisionSystem _particleCollisionSystem; ModelTreeRenderer _models; + ModelTreeRenderer _modelClipboardRenderer; + ModelTree _modelClipboard; QByteArray _voxelsFilename; bool _wantToKillLocalVoxels; @@ -482,6 +499,7 @@ private: Faceshift _faceshift; Visage _visage; CaraFaceTracker _cara; + DdeFaceTracker _dde; SixenseManager _sixenseManager; PrioVR _prioVR; diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 4ed1f7aeb3..bb8b79ab59 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -49,10 +49,12 @@ static const float AUDIO_CALLBACK_MSECS = (float) NETWORK_BUFFER_LENGTH_SAMPLES_ static const int NUMBER_OF_NOISE_SAMPLE_FRAMES = 300; static const int FRAMES_AVAILABLE_STATS_WINDOW_SECONDS = 10; +static const int APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS = (int)(30.0f * 1000.0f / AUDIO_CALLBACK_MSECS); // Mute icon configration static const int MUTE_ICON_SIZE = 24; +static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 100; Audio::Audio(QObject* parent) : AbstractAudioInterface(parent), @@ -63,19 +65,14 @@ Audio::Audio(QObject* parent) : _audioOutput(NULL), _desiredOutputFormat(), _outputFormat(), - _outputDevice(NULL), + _outputFrameSize(0), _numOutputCallbackBytes(0), _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _proceduralAudioOutput(NULL), _proceduralOutputDevice(NULL), - - // NOTE: Be very careful making changes to the initializers of these ring buffers. There is a known problem with some - // Mac audio devices that slowly introduce additional delay in the audio device because they play out audio slightly - // slower than real time (or at least the desired sample rate). If you increase the size of the ring buffer, then it - // this delay will slowly add up and the longer someone runs, they more delayed their audio will be. _inputRingBuffer(0), - _receivedAudioStream(NETWORK_BUFFER_LENGTH_SAMPLES_STEREO, 100, true, 0, 0, true), + _receivedAudioStream(0, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, true, 0, 0, true), _isStereoInput(false), _averagedLatency(0.0), _lastInputLoudness(0), @@ -104,6 +101,7 @@ Audio::Audio(QObject* parent) : _scopeOutputOffset(0), _framesPerScope(DEFAULT_FRAMES_PER_SCOPE), _samplesPerScope(NETWORK_SAMPLES_PER_FRAME * _framesPerScope), + _peqEnabled(false), _scopeInput(0), _scopeOutputLeft(0), _scopeOutputRight(0), @@ -112,12 +110,17 @@ Audio::Audio(QObject* parent) : _outgoingAvatarAudioSequenceNumber(0), _audioInputMsecsReadStats(MSECS_PER_SECOND / (float)AUDIO_CALLBACK_MSECS * CALLBACK_ACCELERATOR_RATIO, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS), _inputRingBufferMsecsAvailableStats(1, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS), - _audioOutputMsecsUnplayedStats(1, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS) + _audioOutputMsecsUnplayedStats(1, FRAMES_AVAILABLE_STATS_WINDOW_SECONDS), + _lastSentAudioPacket(0), + _packetSentTimeGaps(1, APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS), + _audioOutputIODevice(*this) { // clear the array of locally injected samples memset(_localProceduralSamples, 0, NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL); // Create the noise sample array _noiseSampleFrames = new float[NUMBER_OF_NOISE_SAMPLE_FRAMES]; + + connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, this, &Audio::processReceivedAudioStreamSamples, Qt::DirectConnection); } void Audio::init(QGLWidget *parent) { @@ -128,8 +131,8 @@ void Audio::init(QGLWidget *parent) { void Audio::reset() { _receivedAudioStream.reset(); - resetStats(); + _peq.reset(); } void Audio::resetStats() { @@ -142,6 +145,7 @@ void Audio::resetStats() { _inputRingBufferMsecsAvailableStats.reset(); _audioOutputMsecsUnplayedStats.reset(); + _packetSentTimeGaps.reset(); } void Audio::audioMixerKilled() { @@ -308,7 +312,7 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, } } -void linearResampling(int16_t* sourceSamples, int16_t* destinationSamples, +void linearResampling(const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, unsigned int numDestinationSamples, const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) { if (sourceAudioFormat == destinationAudioFormat) { @@ -415,9 +419,15 @@ void Audio::start() { if (!outputFormatSupported) { qDebug() << "Unable to set up audio output because of a problem with output format."; } + + _peq.initialize( _inputFormat.sampleRate(), _audioInput->bufferSize() ); + } void Audio::stop() { + + _peq.finalize(); + // "switch" to invalid devices in order to shut down the state switchInputToAudioDevice(QAudioDeviceInfo()); switchOutputToAudioDevice(QAudioDeviceInfo()); @@ -459,7 +469,15 @@ void Audio::handleAudioInput() { int inputSamplesRequired = (int)((float)NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL * inputToNetworkInputRatio); QByteArray inputByteArray = _inputDevice->readAll(); - + + if (_peqEnabled && !_muted) { + // we wish to pre-filter our captured input, prior to loopback + + int16_t* ioBuffer = (int16_t*)inputByteArray.data(); + + _peq.render(ioBuffer, ioBuffer, inputByteArray.size() / sizeof(int16_t)); + } + if (Menu::getInstance()->isOptionChecked(MenuOption::EchoLocalAudio) && !_muted && _audioOutput) { // if this person wants local loopback add that to the locally injected audio @@ -658,6 +676,11 @@ void Audio::handleAudioInput() { NodeList* nodeList = NodeList::getInstance(); SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); + + if (_recorder && _recorder.data()->isRecording()) { + _recorder.data()->record(reinterpret_cast(networkAudioSamples), numNetworkBytes); + } + if (audioMixer && audioMixer->getActiveSocket()) { MyAvatar* interfaceAvatar = Application::getInstance()->getAvatar(); glm::vec3 headPosition = interfaceAvatar->getHead()->getPosition(); @@ -699,6 +722,17 @@ void Audio::handleAudioInput() { // memcpy our orientation memcpy(currentPacketPtr, &headOrientation, sizeof(headOrientation)); currentPacketPtr += sizeof(headOrientation); + + // first time this is 0 + if (_lastSentAudioPacket == 0) { + _lastSentAudioPacket = usecTimestampNow(); + } else { + quint64 now = usecTimestampNow(); + quint64 gap = now - _lastSentAudioPacket; + _packetSentTimeGaps.update(gap); + + _lastSentAudioPacket = now; + } nodeList->writeDatagram(audioDataPacket, numAudioBytes + leadingBytes, audioMixer); _outgoingAvatarAudioSequenceNumber++; @@ -708,21 +742,92 @@ void Audio::handleAudioInput() { } delete[] inputAudioSamples; } +} - if (_receivedAudioStream.getPacketReceived() > 0) { - pushAudioToOutput(); +void Audio::processReceivedAudioStreamSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer) { + + const int numNetworkOutputSamples = inputBuffer.size() / sizeof(int16_t); + const int numDeviceOutputSamples = numNetworkOutputSamples * (_outputFormat.sampleRate() * _outputFormat.channelCount()) + / (_desiredOutputFormat.sampleRate() * _desiredOutputFormat.channelCount()); + + + outputBuffer.resize(numDeviceOutputSamples * sizeof(int16_t)); + + const int16_t* receivedSamples; + if (_processSpatialAudio) { + unsigned int sampleTime = _spatialAudioStart; + QByteArray buffer = inputBuffer; + + // Accumulate direct transmission of audio from sender to receiver + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessingIncludeOriginal)) { + emit preProcessOriginalInboundAudio(sampleTime, buffer, _desiredOutputFormat); + addSpatialAudioToBuffer(sampleTime, buffer, numNetworkOutputSamples); + } + + // Send audio off for spatial processing + emit processInboundAudio(sampleTime, buffer, _desiredOutputFormat); + + // copy the samples we'll resample from the spatial audio ring buffer - this also + // pushes the read pointer of the spatial audio ring buffer forwards + _spatialAudioRingBuffer.readSamples(_outputProcessingBuffer, numNetworkOutputSamples); + + // Advance the start point for the next packet of audio to arrive + _spatialAudioStart += numNetworkOutputSamples / _desiredOutputFormat.channelCount(); + + receivedSamples = _outputProcessingBuffer; + } else { + // copy the samples we'll resample from the ring buffer - this also + // pushes the read pointer of the ring buffer forwards + //receivedAudioStreamPopOutput.readSamples(receivedSamples, numNetworkOutputSamples); + + receivedSamples = reinterpret_cast(inputBuffer.data()); + } + + // copy the packet from the RB to the output + linearResampling(receivedSamples, + (int16_t*)outputBuffer.data(), + numNetworkOutputSamples, + numDeviceOutputSamples, + _desiredOutputFormat, _outputFormat); + + + if (_scopeEnabled && !_scopeEnabledPause) { + unsigned int numAudioChannels = _desiredOutputFormat.channelCount(); + const int16_t* samples = receivedSamples; + for (int numSamples = numNetworkOutputSamples / numAudioChannels; numSamples > 0; numSamples -= NETWORK_SAMPLES_PER_FRAME) { + + unsigned int audioChannel = 0; + addBufferToScope( + _scopeOutputLeft, + _scopeOutputOffset, + samples, audioChannel, numAudioChannels); + + audioChannel = 1; + addBufferToScope( + _scopeOutputRight, + _scopeOutputOffset, + samples, audioChannel, numAudioChannels); + + _scopeOutputOffset += NETWORK_SAMPLES_PER_FRAME; + _scopeOutputOffset %= _samplesPerScope; + samples += NETWORK_SAMPLES_PER_FRAME * numAudioChannels; + } } } void Audio::addReceivedAudioToStream(const QByteArray& audioByteArray) { + if (_audioOutput) { // Audio output must exist and be correctly set up if we're going to process received audio - processReceivedAudio(audioByteArray); + _receivedAudioStream.parseData(audioByteArray); } Application::getInstance()->getBandwidthMeter()->inputStream(BandwidthMeter::AUDIO).updateValue(audioByteArray.size()); } + + + void Audio::parseAudioStreamStatsPacket(const QByteArray& packet) { int numBytesPacketHeader = numBytesForPacketHeader(packet); @@ -886,119 +991,6 @@ void Audio::toggleStereoInput() { } } -void Audio::processReceivedAudio(const QByteArray& audioByteArray) { - - // parse audio data - _receivedAudioStream.parseData(audioByteArray); - - - // This call has been moved to handleAudioInput. handleAudioInput is called at a much more regular interval - // than processReceivedAudio since handleAudioInput does not experience network-related jitter. - // This way, we reduce the jitter of the frames being pushed to the audio output, allowing us to use a reduced - // buffer size for it, which reduces latency. - - //pushAudioToOutput(); -} - -void Audio::pushAudioToOutput() { - - if (_audioOutput->bytesFree() == _audioOutput->bufferSize()) { - // the audio output has no samples to play. set the downstream audio to starved so that it - // refills to its desired size before pushing frames - _receivedAudioStream.setToStarved(); - } - - float networkOutputToOutputRatio = (_desiredOutputFormat.sampleRate() / (float)_outputFormat.sampleRate()) - * (_desiredOutputFormat.channelCount() / (float)_outputFormat.channelCount()); - - int numFramesToPush; - if (Menu::getInstance()->isOptionChecked(MenuOption::DisableQAudioOutputOverflowCheck)) { - numFramesToPush = _receivedAudioStream.getFramesAvailable(); - } else { - // make sure to push a whole number of frames to the audio output - int numFramesAudioOutputRoomFor = (int)(_audioOutput->bytesFree() / sizeof(int16_t) * networkOutputToOutputRatio) / _receivedAudioStream.getNumFrameSamples(); - numFramesToPush = std::min(_receivedAudioStream.getFramesAvailable(), numFramesAudioOutputRoomFor); - } - - // if there is data in the received stream and room in the audio output, decide what to do - - if (numFramesToPush > 0 && _receivedAudioStream.popFrames(numFramesToPush, false)) { - - int numNetworkOutputSamples = numFramesToPush * NETWORK_BUFFER_LENGTH_SAMPLES_STEREO; - int numDeviceOutputSamples = numNetworkOutputSamples / networkOutputToOutputRatio; - - QByteArray outputBuffer; - outputBuffer.resize(numDeviceOutputSamples * sizeof(int16_t)); - - AudioRingBuffer::ConstIterator receivedAudioStreamPopOutput = _receivedAudioStream.getLastPopOutput(); - - int16_t* receivedSamples = new int16_t[numNetworkOutputSamples]; - if (_processSpatialAudio) { - unsigned int sampleTime = _spatialAudioStart; - QByteArray buffer; - buffer.resize(numNetworkOutputSamples * sizeof(int16_t)); - - receivedAudioStreamPopOutput.readSamples((int16_t*)buffer.data(), numNetworkOutputSamples); - - // Accumulate direct transmission of audio from sender to receiver - if (Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessingIncludeOriginal)) { - emit preProcessOriginalInboundAudio(sampleTime, buffer, _desiredOutputFormat); - addSpatialAudioToBuffer(sampleTime, buffer, numNetworkOutputSamples); - } - - // Send audio off for spatial processing - emit processInboundAudio(sampleTime, buffer, _desiredOutputFormat); - - // copy the samples we'll resample from the spatial audio ring buffer - this also - // pushes the read pointer of the spatial audio ring buffer forwards - _spatialAudioRingBuffer.readSamples(receivedSamples, numNetworkOutputSamples); - - // Advance the start point for the next packet of audio to arrive - _spatialAudioStart += numNetworkOutputSamples / _desiredOutputFormat.channelCount(); - } else { - // copy the samples we'll resample from the ring buffer - this also - // pushes the read pointer of the ring buffer forwards - receivedAudioStreamPopOutput.readSamples(receivedSamples, numNetworkOutputSamples); - } - - // copy the packet from the RB to the output - linearResampling(receivedSamples, - (int16_t*)outputBuffer.data(), - numNetworkOutputSamples, - numDeviceOutputSamples, - _desiredOutputFormat, _outputFormat); - - if (_outputDevice) { - _outputDevice->write(outputBuffer); - } - - if (_scopeEnabled && !_scopeEnabledPause) { - unsigned int numAudioChannels = _desiredOutputFormat.channelCount(); - int16_t* samples = receivedSamples; - for (int numSamples = numNetworkOutputSamples / numAudioChannels; numSamples > 0; numSamples -= NETWORK_SAMPLES_PER_FRAME) { - - unsigned int audioChannel = 0; - addBufferToScope( - _scopeOutputLeft, - _scopeOutputOffset, - samples, audioChannel, numAudioChannels); - - audioChannel = 1; - addBufferToScope( - _scopeOutputRight, - _scopeOutputOffset, - samples, audioChannel, numAudioChannels); - - _scopeOutputOffset += NETWORK_SAMPLES_PER_FRAME; - _scopeOutputOffset %= _samplesPerScope; - samples += NETWORK_SAMPLES_PER_FRAME * numAudioChannels; - } - } - - delete[] receivedSamples; - } -} - void Audio::processProceduralAudio(int16_t* monoInput, int numSamples) { // zero out the locally injected audio in preparation for audio procedural sounds @@ -1200,6 +1192,31 @@ void Audio::renderToolBox(int x, int y, bool boxed) { glDisable(GL_TEXTURE_2D); } +void Audio::toggleAudioFilter() { + _peqEnabled = !_peqEnabled; +} + +void Audio::selectAudioFilterFlat() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterFlat)) { + _peq.loadProfile(0); + } +} +void Audio::selectAudioFilterTrebleCut() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterTrebleCut)) { + _peq.loadProfile(1); + } +} +void Audio::selectAudioFilterBassCut() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterBassCut)) { + _peq.loadProfile(2); + } +} +void Audio::selectAudioFilterSmiley() { + if (Menu::getInstance()->isOptionChecked(MenuOption::AudioFilterSmiley)) { + _peq.loadProfile(3); + } +} + void Audio::toggleScope() { _scopeEnabled = !_scopeEnabled; if (_scopeEnabled) { @@ -1307,10 +1324,10 @@ void Audio::renderStats(const float* color, int width, int height) { return; } - const int linesWhenCentered = _statsShowInjectedStreams ? 30 : 23; + const int linesWhenCentered = _statsShowInjectedStreams ? 34 : 27; const int CENTERED_BACKGROUND_HEIGHT = STATS_HEIGHT_PER_LINE * linesWhenCentered; - int lines = _statsShowInjectedStreams ? _audioMixerInjectedStreamAudioStatsMap.size() * 7 + 23 : 23; + int lines = _statsShowInjectedStreams ? _audioMixerInjectedStreamAudioStatsMap.size() * 7 + 27 : 27; int statsHeight = STATS_HEIGHT_PER_LINE * lines; @@ -1384,7 +1401,28 @@ void Audio::renderStats(const float* color, int width, int height) { verticalOffset += STATS_HEIGHT_PER_LINE; // blank line - char upstreamMicLabelString[] = "Upstream mic audio stats:"; + char clientUpstreamMicLabelString[] = "Upstream Mic Audio Packets Sent Gaps (by client):"; + verticalOffset += STATS_HEIGHT_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, clientUpstreamMicLabelString, color); + + char stringBuffer[512]; + sprintf(stringBuffer, " Inter-packet timegaps (overall) | min: %9s, max: %9s, avg: %9s", + formatUsecTime(_packetSentTimeGaps.getMin()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getMax()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getAverage()).toLatin1().data()); + verticalOffset += STATS_HEIGHT_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, stringBuffer, color); + + sprintf(stringBuffer, " Inter-packet timegaps (last 30s) | min: %9s, max: %9s, avg: %9s", + formatUsecTime(_packetSentTimeGaps.getWindowMin()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getWindowMax()).toLatin1().data(), + formatUsecTime(_packetSentTimeGaps.getWindowAverage()).toLatin1().data()); + verticalOffset += STATS_HEIGHT_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, stringBuffer, color); + + verticalOffset += STATS_HEIGHT_PER_LINE; // blank line + + char upstreamMicLabelString[] = "Upstream mic audio stats (received and reported by audio-mixer):"; verticalOffset += STATS_HEIGHT_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamMicLabelString, color); @@ -1424,9 +1462,9 @@ void Audio::renderAudioStreamStats(const AudioStreamStats& streamStats, int hori sprintf(stringBuffer, " Packet loss | overall: %5.2f%% (%d lost), last_30s: %5.2f%% (%d lost)", streamStats._packetStreamStats.getLostRate() * 100.0f, - streamStats._packetStreamStats._numLost, + streamStats._packetStreamStats._lost, streamStats._packetStreamWindowStats.getLostRate() * 100.0f, - streamStats._packetStreamWindowStats._numLost); + streamStats._packetStreamWindowStats._lost); verticalOffset += STATS_HEIGHT_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, stringBuffer, color); @@ -1478,11 +1516,11 @@ void Audio::renderScope(int width, int height) { if (!_scopeEnabled) return; - static const float backgroundColor[4] = { 0.2f, 0.2f, 0.2f, 0.6f }; + static const float backgroundColor[4] = { 0.4f, 0.4f, 0.4f, 0.6f }; static const float gridColor[4] = { 0.3f, 0.3f, 0.3f, 0.6f }; - static const float inputColor[4] = { 0.3f, .7f, 0.3f, 0.6f }; - static const float outputLeftColor[4] = { 0.7f, .3f, 0.3f, 0.6f }; - static const float outputRightColor[4] = { 0.3f, .3f, 0.7f, 0.6f }; + static const float inputColor[4] = { 0.3f, 1.0f, 0.3f, 1.0f }; + static const float outputLeftColor[4] = { 1.0f, 0.3f, 0.3f, 1.0f }; + static const float outputRightColor[4] = { 0.3f, 0.3f, 1.0f, 1.0f }; static const int gridRows = 2; int gridCols = _framesPerScope; @@ -1595,6 +1633,12 @@ void Audio::renderLineStrip(const float* color, int x, int y, int n, int offset, } +void Audio::outputFormatChanged() { + int outputFormatChannelCountTimesSampleRate = _outputFormat.channelCount() * _outputFormat.sampleRate(); + _outputFrameSize = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL * outputFormatChannelCountTimesSampleRate / _desiredOutputFormat.sampleRate(); + _receivedAudioStream.outputFormatChanged(outputFormatChannelCountTimesSampleRate); +} + bool Audio::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) { bool supportedFormat = false; @@ -1645,7 +1689,6 @@ bool Audio::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) // cleanup any previously initialized device if (_audioOutput) { _audioOutput->stop(); - _outputDevice = NULL; delete _audioOutput; _audioOutput = NULL; @@ -1667,13 +1710,17 @@ bool Audio::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) if (adjustedFormatForAudioDevice(outputDeviceInfo, _desiredOutputFormat, _outputFormat)) { qDebug() << "The format to be used for audio output is" << _outputFormat; - const int AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 10; + outputFormatChanged(); + + const int AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 3; // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - _audioOutput->setBufferSize(AUDIO_OUTPUT_BUFFER_SIZE_FRAMES * _outputFormat.bytesForDuration(BUFFER_SEND_INTERVAL_USECS)); - qDebug() << "Ring Buffer capacity in frames: " << AUDIO_OUTPUT_BUFFER_SIZE_FRAMES; - _outputDevice = _audioOutput->start(); + _audioOutput->setBufferSize(AUDIO_OUTPUT_BUFFER_SIZE_FRAMES * _outputFrameSize * sizeof(int16_t)); + qDebug() << "Ring Buffer capacity in frames: " << _audioOutput->bufferSize() / sizeof(int16_t) / (float)_outputFrameSize; + + _audioOutputIODevice.start(); + _audioOutput->start(&_audioOutputIODevice); // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); @@ -1743,3 +1790,21 @@ float Audio::getInputRingBufferMsecsAvailable() const { float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); return msecsInInputRingBuffer; } + +qint64 Audio::AudioOutputIODevice::readData(char * data, qint64 maxSize) { + MixedProcessedAudioStream& receivedAUdioStream = _parent._receivedAudioStream; + + int samplesRequested = maxSize / sizeof(int16_t); + int samplesPopped; + int bytesWritten; + if ((samplesPopped = receivedAUdioStream.popSamples(samplesRequested, false)) > 0) { + AudioRingBuffer::ConstIterator lastPopOutput = receivedAUdioStream.getLastPopOutput(); + lastPopOutput.readSamples((int16_t*)data, samplesPopped); + bytesWritten = samplesPopped * sizeof(int16_t); + } else { + memset(data, 0, maxSize); + bytesWritten = maxSize; + } + + return bytesWritten; +} diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 3006446db1..59a6d942a6 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -17,8 +17,10 @@ #include "InterfaceConfig.h" #include "AudioStreamStats.h" +#include "Recorder.h" #include "RingBufferHistory.h" #include "MovingMinMaxAvg.h" +#include "AudioFilter.h" #include #include @@ -33,7 +35,7 @@ #include #include -#include "MixedAudioStream.h" +#include "MixedProcessedAudioStream.h" static const int NUM_AUDIO_CHANNELS = 2; @@ -45,6 +47,20 @@ class QIODevice; class Audio : public AbstractAudioInterface { Q_OBJECT public: + + class AudioOutputIODevice : public QIODevice { + public: + AudioOutputIODevice(Audio& parent) : _parent(parent) {}; + + void start() { open(QIODevice::ReadOnly); } + void stop() { close(); } + qint64 readData(char * data, qint64 maxSize); + qint64 writeData(const char * data, qint64 maxSize) { return 0; } + private: + Audio& _parent; + }; + + // setup for audio I/O Audio(QObject* parent = 0); @@ -87,6 +103,9 @@ public: float getAudioOutputMsecsUnplayed() const; float getAudioOutputAverageMsecsUnplayed() const { return (float)_audioOutputMsecsUnplayedStats.getWindowAverage(); } + + void setRecorder(RecorderPointer recorder) { _recorder = recorder; } + void setPlayer(PlayerPointer player) { _player = player; } public slots: void start(); @@ -94,6 +113,7 @@ public slots: void addReceivedAudioToStream(const QByteArray& audioByteArray); void parseAudioStreamStatsPacket(const QByteArray& packet); void addSpatialAudioToBuffer(unsigned int sampleTime, const QByteArray& spatialAudio, unsigned int numSamples); + void processReceivedAudioStreamSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); void handleAudioInput(); void reset(); void resetStats(); @@ -110,7 +130,12 @@ public slots: void selectAudioScopeFiveFrames(); void selectAudioScopeTwentyFrames(); void selectAudioScopeFiftyFrames(); - + void toggleAudioFilter(); + void selectAudioFilterFlat(); + void selectAudioFilterTrebleCut(); + void selectAudioFilterBassCut(); + void selectAudioFilterSmiley(); + virtual void handleAudioByteArray(const QByteArray& audioByteArray); void sendDownstreamAudioStatsPacket(); @@ -133,7 +158,10 @@ signals: void preProcessOriginalInboundAudio(unsigned int sampleTime, QByteArray& samples, const QAudioFormat& format); void processInboundAudio(unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format); void processLocalAudio(unsigned int sampleTime, const QByteArray& samples, const QAudioFormat& format); - + +private: + void outputFormatChanged(); + private: QByteArray firstInputFrame; @@ -146,14 +174,15 @@ private: QAudioOutput* _audioOutput; QAudioFormat _desiredOutputFormat; QAudioFormat _outputFormat; - QIODevice* _outputDevice; + int _outputFrameSize; + int16_t _outputProcessingBuffer[NETWORK_BUFFER_LENGTH_SAMPLES_STEREO]; int _numOutputCallbackBytes; QAudioOutput* _loopbackAudioOutput; QIODevice* _loopbackOutputDevice; QAudioOutput* _proceduralAudioOutput; QIODevice* _proceduralOutputDevice; AudioRingBuffer _inputRingBuffer; - MixedAudioStream _receivedAudioStream; + MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; QString _inputAudioDeviceName; @@ -211,12 +240,6 @@ private: // Add sounds that we want the user to not hear themselves, by adding on top of mic input signal void addProceduralSounds(int16_t* monoInput, int numSamples); - - // Process received audio - void processReceivedAudio(const QByteArray& audioByteArray); - - // Pushes frames from the output ringbuffer to the audio output device - void pushAudioToOutput(); bool switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo); bool switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo); @@ -239,12 +262,12 @@ private: // Audio scope methods for rendering void renderBackground(const float* color, int x, int y, int width, int height); void renderGrid(const float* color, int x, int y, int width, int height, int rows, int cols); - void renderLineStrip(const float* color, int x, int y, int n, int offset, const QByteArray* byteArray); + void renderLineStrip(const float* color, int x, int y, int n, int offset, const QByteArray* byteArray); // audio stats methods for rendering void renderAudioStreamStats(const AudioStreamStats& streamStats, int horizontalOffset, int& verticalOffset, float scale, float rotation, int font, const float* color, bool isDownstreamStats = false); - + // Audio scope data static const unsigned int NETWORK_SAMPLES_PER_FRAME = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; static const unsigned int DEFAULT_FRAMES_PER_SCOPE = 5; @@ -257,6 +280,11 @@ private: int _scopeOutputOffset; int _framesPerScope; int _samplesPerScope; + + // Multi-band parametric EQ + bool _peqEnabled; + AudioFilterPEQ3 _peq; + QMutex _guard; QByteArray* _scopeInput; QByteArray* _scopeOutputLeft; @@ -279,6 +307,14 @@ private: MovingMinMaxAvg _inputRingBufferMsecsAvailableStats; MovingMinMaxAvg _audioOutputMsecsUnplayedStats; + + quint64 _lastSentAudioPacket; + MovingMinMaxAvg _packetSentTimeGaps; + + AudioOutputIODevice _audioOutputIODevice; + + WeakRecorderPointer _recorder; + WeakPlayerPointer _player; }; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index bb9ed9a566..b596ea2a13 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -495,6 +495,48 @@ Menu::Menu() : true, appInstance->getAudio(), SLOT(toggleAudioNoiseReduction())); + + addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioFilter, + 0, + false, + appInstance->getAudio(), + SLOT(toggleAudioFilter())); + + QMenu* audioFilterMenu = audioDebugMenu->addMenu("Audio Filter Options"); + addDisabledActionAndSeparator(audioFilterMenu, "Filter Response"); + { + QAction *flat = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterFlat, + 0, + true, + appInstance->getAudio(), + SLOT(selectAudioFilterFlat())); + + QAction *trebleCut = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterTrebleCut, + 0, + false, + appInstance->getAudio(), + SLOT(selectAudioFilterTrebleCut())); + + QAction *bassCut = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterBassCut, + 0, + false, + appInstance->getAudio(), + SLOT(selectAudioFilterBassCut())); + + QAction *smiley = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterSmiley, + 0, + false, + appInstance->getAudio(), + SLOT(selectAudioFilterSmiley())); + + + QActionGroup* audioFilterGroup = new QActionGroup(audioFilterMenu); + audioFilterGroup->addAction(flat); + audioFilterGroup->addAction(trebleCut); + audioFilterGroup->addAction(bassCut); + audioFilterGroup->addAction(smiley); + } + addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoServerAudio); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoLocalAudio); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::StereoAudio, 0, false, @@ -615,8 +657,6 @@ Menu::Menu() : appInstance->getAudio(), SLOT(toggleStatsShowInjectedStreams())); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::DisableQAudioOutputOverflowCheck, 0, false); - addActionToQMenuAndActionHash(developerMenu, MenuOption::PasteToVoxel, Qt::CTRL | Qt::SHIFT | Qt::Key_V, this, @@ -1129,8 +1169,8 @@ void Menu::goTo() { } bool Menu::goToURL(QString location) { - if (location.startsWith(CUSTOM_URL_SCHEME + "//")) { - QStringList urlParts = location.remove(0, CUSTOM_URL_SCHEME.length() + 2).split('/', QString::SkipEmptyParts); + if (location.startsWith(CUSTOM_URL_SCHEME + "/")) { + QStringList urlParts = location.remove(0, CUSTOM_URL_SCHEME.length()).split('/', QString::SkipEmptyParts); if (urlParts.count() > 1) { // if url has 2 or more parts, the first one is domain name diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 75371230ef..0f954175e4 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -322,6 +322,11 @@ namespace MenuOption { const QString Animations = "Animations..."; const QString Atmosphere = "Atmosphere"; const QString Attachments = "Attachments..."; + const QString AudioFilter = "Audio Filter Bank"; + const QString AudioFilterFlat = "Flat Response"; + const QString AudioFilterTrebleCut= "Treble Cut"; + const QString AudioFilterBassCut = "Bass Cut"; + const QString AudioFilterSmiley = "Smiley Curve"; const QString AudioNoiseReduction = "Audio Noise Reduction"; const QString AudioScope = "Audio Scope"; const QString AudioScopeFiftyFrames = "Fifty"; @@ -365,7 +370,6 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableAutoAdjustLOD = "Disable Automatically Adjusting LOD"; const QString DisableNackPackets = "Disable NACK Packets"; - const QString DisableQAudioOutputOverflowCheck = "Disable Audio Output Device Overflow Check"; const QString DisplayFrustum = "Display Frustum"; const QString DisplayHands = "Display Hands"; const QString DisplayHandTargets = "Display Hand Targets"; diff --git a/interface/src/MetavoxelSystem.cpp b/interface/src/MetavoxelSystem.cpp index 00d69c8c25..433c8af23e 100644 --- a/interface/src/MetavoxelSystem.cpp +++ b/interface/src/MetavoxelSystem.cpp @@ -25,7 +25,7 @@ #include "MetavoxelSystem.h" #include "renderer/Model.h" -REGISTER_META_OBJECT(PointMetavoxelRendererImplementation) +REGISTER_META_OBJECT(DefaultMetavoxelRendererImplementation) REGISTER_META_OBJECT(SphereRenderer) REGISTER_META_OBJECT(StaticModelRenderer) @@ -33,8 +33,13 @@ static int bufferPointVectorMetaTypeId = qRegisterMetaType(); void MetavoxelSystem::init() { MetavoxelClientManager::init(); - PointMetavoxelRendererImplementation::init(); - _pointBufferAttribute = AttributeRegistry::getInstance()->registerAttribute(new PointBufferAttribute()); + DefaultMetavoxelRendererImplementation::init(); + _pointBufferAttribute = AttributeRegistry::getInstance()->registerAttribute(new BufferDataAttribute("pointBuffer")); + _heightfieldBufferAttribute = AttributeRegistry::getInstance()->registerAttribute( + new BufferDataAttribute("heightfieldBuffer")); + + _heightfieldBufferAttribute->setLODThresholdMultiplier( + AttributeRegistry::getInstance()->getHeightfieldAttribute()->getLODThresholdMultiplier()); } MetavoxelLOD MetavoxelSystem::getLOD() { @@ -42,28 +47,31 @@ MetavoxelLOD MetavoxelSystem::getLOD() { return _lod; } -class SpannerSimulateVisitor : public SpannerVisitor { +class SimulateVisitor : public MetavoxelVisitor { public: - SpannerSimulateVisitor(float deltaTime); + SimulateVisitor(float deltaTime, const MetavoxelLOD& lod); - virtual bool visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize); + virtual int visit(MetavoxelInfo& info); private: float _deltaTime; }; -SpannerSimulateVisitor::SpannerSimulateVisitor(float deltaTime) : - SpannerVisitor(QVector() << AttributeRegistry::getInstance()->getSpannersAttribute(), - QVector(), QVector(), QVector(), - Application::getInstance()->getMetavoxels()->getLOD()), +SimulateVisitor::SimulateVisitor(float deltaTime, const MetavoxelLOD& lod) : + MetavoxelVisitor(QVector() << AttributeRegistry::getInstance()->getRendererAttribute(), + QVector(), lod), _deltaTime(deltaTime) { } -bool SpannerSimulateVisitor::visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize) { - spanner->getRenderer()->simulate(_deltaTime); - return true; +int SimulateVisitor::visit(MetavoxelInfo& info) { + if (!info.isLeaf) { + return DEFAULT_ORDER; + } + static_cast(info.inputValues.at(0).getInlineValue< + SharedObjectPointer>().data())->getImplementation()->simulate(*_data, _deltaTime, info, _lod); + return STOP_RECURSION; } void MetavoxelSystem::simulate(float deltaTime) { @@ -76,28 +84,8 @@ void MetavoxelSystem::simulate(float deltaTime) { BASE_LOD_THRESHOLD * Menu::getInstance()->getAvatarLODDistanceMultiplier()); } - SpannerSimulateVisitor spannerSimulateVisitor(deltaTime); - guide(spannerSimulateVisitor); -} - -class SpannerRenderVisitor : public SpannerVisitor { -public: - - SpannerRenderVisitor(); - - virtual bool visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize); -}; - -SpannerRenderVisitor::SpannerRenderVisitor() : - SpannerVisitor(QVector() << AttributeRegistry::getInstance()->getSpannersAttribute(), - QVector(), QVector(), QVector(), - Application::getInstance()->getMetavoxels()->getLOD(), - encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())) { -} - -bool SpannerRenderVisitor::visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize) { - spanner->getRenderer()->render(1.0f, SpannerRenderer::DEFAULT_MODE, clipMinimum, clipSize); - return true; + SimulateVisitor simulateVisitor(deltaTime, getLOD()); + guideToAugmented(simulateVisitor); } class RenderVisitor : public MetavoxelVisitor { @@ -123,11 +111,347 @@ int RenderVisitor::visit(MetavoxelInfo& info) { } void MetavoxelSystem::render() { + // update the frustum + ViewFrustum* viewFrustum = Application::getInstance()->getViewFrustum(); + _frustum.set(viewFrustum->getFarTopLeft(), viewFrustum->getFarTopRight(), viewFrustum->getFarBottomLeft(), + viewFrustum->getFarBottomRight(), viewFrustum->getNearTopLeft(), viewFrustum->getNearTopRight(), + viewFrustum->getNearBottomLeft(), viewFrustum->getNearBottomRight()); + RenderVisitor renderVisitor(getLOD()); guideToAugmented(renderVisitor); +} + +class RayHeightfieldIntersectionVisitor : public RayIntersectionVisitor { +public: - SpannerRenderVisitor spannerRenderVisitor; - guide(spannerRenderVisitor); + float intersectionDistance; + + RayHeightfieldIntersectionVisitor(const glm::vec3& origin, const glm::vec3& direction, const MetavoxelLOD& lod); + + virtual int visit(MetavoxelInfo& info, float distance); +}; + +RayHeightfieldIntersectionVisitor::RayHeightfieldIntersectionVisitor(const glm::vec3& origin, + const glm::vec3& direction, const MetavoxelLOD& lod) : + RayIntersectionVisitor(origin, direction, QVector() << + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute(), QVector(), lod), + intersectionDistance(FLT_MAX) { +} + +static const float EIGHT_BIT_MAXIMUM_RECIPROCAL = 1.0f / 255.0f; + +int RayHeightfieldIntersectionVisitor::visit(MetavoxelInfo& info, float distance) { + if (!info.isLeaf) { + return _order; + } + const HeightfieldBuffer* buffer = static_cast( + info.inputValues.at(0).getInlineValue().data()); + if (!buffer) { + return STOP_RECURSION; + } + const QByteArray& contents = buffer->getHeight(); + const uchar* src = (const uchar*)contents.constData(); + int size = glm::sqrt((float)contents.size()); + int unextendedSize = size - HeightfieldBuffer::HEIGHT_EXTENSION; + int highest = HeightfieldBuffer::HEIGHT_BORDER + unextendedSize; + float heightScale = unextendedSize * EIGHT_BIT_MAXIMUM_RECIPROCAL; + + // find the initial location in heightfield coordinates + glm::vec3 entry = (_origin + distance * _direction - info.minimum) * (float)unextendedSize / info.size; + entry.x += HeightfieldBuffer::HEIGHT_BORDER; + entry.z += HeightfieldBuffer::HEIGHT_BORDER; + glm::vec3 floors = glm::floor(entry); + glm::vec3 ceils = glm::ceil(entry); + if (floors.x == ceils.x) { + if (_direction.x > 0.0f) { + ceils.x += 1.0f; + } else { + floors.x -= 1.0f; + } + } + if (floors.z == ceils.z) { + if (_direction.z > 0.0f) { + ceils.z += 1.0f; + } else { + floors.z -= 1.0f; + } + } + + bool withinBounds = true; + float accumulatedDistance = 0.0f; + while (withinBounds) { + // find the heights at the corners of the current cell + int floorX = qMin(qMax((int)floors.x, HeightfieldBuffer::HEIGHT_BORDER), highest); + int floorZ = qMin(qMax((int)floors.z, HeightfieldBuffer::HEIGHT_BORDER), highest); + int ceilX = qMin(qMax((int)ceils.x, HeightfieldBuffer::HEIGHT_BORDER), highest); + int ceilZ = qMin(qMax((int)ceils.z, HeightfieldBuffer::HEIGHT_BORDER), highest); + float upperLeft = src[floorZ * size + floorX] * heightScale; + float upperRight = src[floorZ * size + ceilX] * heightScale; + float lowerLeft = src[ceilZ * size + floorX] * heightScale; + float lowerRight = src[ceilZ * size + ceilX] * heightScale; + + // find the distance to the next x coordinate + float xDistance = FLT_MAX; + if (_direction.x > 0.0f) { + xDistance = (ceils.x - entry.x) / _direction.x; + } else if (_direction.x < 0.0f) { + xDistance = (floors.x - entry.x) / _direction.x; + } + + // and the distance to the next z coordinate + float zDistance = FLT_MAX; + if (_direction.z > 0.0f) { + zDistance = (ceils.z - entry.z) / _direction.z; + } else if (_direction.z < 0.0f) { + zDistance = (floors.z - entry.z) / _direction.z; + } + + // the exit distance is the lower of those two + float exitDistance = qMin(xDistance, zDistance); + glm::vec3 exit, nextFloors = floors, nextCeils = ceils; + if (exitDistance == FLT_MAX) { + if (_direction.y > 0.0f) { + return SHORT_CIRCUIT; // line points upwards; no collisions possible + } + withinBounds = false; // line points downwards; check this cell only + + } else { + // find the exit point and the next cell, and determine whether it's still within the bounds + exit = entry + exitDistance * _direction; + withinBounds = (exit.y >= HeightfieldBuffer::HEIGHT_BORDER && exit.y <= highest); + if (exitDistance == xDistance) { + if (_direction.x > 0.0f) { + nextFloors.x += 1.0f; + withinBounds &= (nextCeils.x += 1.0f) <= highest; + } else { + withinBounds &= (nextFloors.x -= 1.0f) >= HeightfieldBuffer::HEIGHT_BORDER; + nextCeils.x -= 1.0f; + } + } + if (exitDistance == zDistance) { + if (_direction.z > 0.0f) { + nextFloors.z += 1.0f; + withinBounds &= (nextCeils.z += 1.0f) <= highest; + } else { + withinBounds &= (nextFloors.z -= 1.0f) >= HeightfieldBuffer::HEIGHT_BORDER; + nextCeils.z -= 1.0f; + } + } + // check the vertical range of the ray against the ranges of the cell heights + if (qMin(entry.y, exit.y) > qMax(qMax(upperLeft, upperRight), qMax(lowerLeft, lowerRight)) || + qMax(entry.y, exit.y) < qMin(qMin(upperLeft, upperRight), qMin(lowerLeft, lowerRight))) { + entry = exit; + floors = nextFloors; + ceils = nextCeils; + accumulatedDistance += exitDistance; + continue; + } + } + // having passed the bounds check, we must check against the planes + glm::vec3 relativeEntry = entry - glm::vec3(floors.x, upperLeft, floors.z); + + // first check the triangle including the Z+ segment + glm::vec3 lowerNormal(lowerLeft - lowerRight, 1.0f, upperLeft - lowerLeft); + float lowerProduct = glm::dot(lowerNormal, _direction); + if (lowerProduct < 0.0f) { + float planeDistance = -glm::dot(lowerNormal, relativeEntry) / lowerProduct; + glm::vec3 intersection = relativeEntry + planeDistance * _direction; + if (intersection.x >= 0.0f && intersection.x <= 1.0f && intersection.z >= 0.0f && intersection.z <= 1.0f && + intersection.z >= intersection.x) { + intersectionDistance = qMin(intersectionDistance, distance + + (accumulatedDistance + planeDistance) * (info.size / unextendedSize)); + return SHORT_CIRCUIT; + } + } + + // then the one with the X+ segment + glm::vec3 upperNormal(upperLeft - upperRight, 1.0f, upperRight - lowerRight); + float upperProduct = glm::dot(upperNormal, _direction); + if (upperProduct < 0.0f) { + float planeDistance = -glm::dot(upperNormal, relativeEntry) / upperProduct; + glm::vec3 intersection = relativeEntry + planeDistance * _direction; + if (intersection.x >= 0.0f && intersection.x <= 1.0f && intersection.z >= 0.0f && intersection.z <= 1.0f && + intersection.x >= intersection.z) { + intersectionDistance = qMin(intersectionDistance, distance + + (accumulatedDistance + planeDistance) * (info.size / unextendedSize)); + return SHORT_CIRCUIT; + } + } + + // no joy; continue on our way + entry = exit; + floors = nextFloors; + ceils = nextCeils; + accumulatedDistance += exitDistance; + } + + return STOP_RECURSION; +} + +bool MetavoxelSystem::findFirstRayHeightfieldIntersection(const glm::vec3& origin, + const glm::vec3& direction, float& distance) { + RayHeightfieldIntersectionVisitor visitor(origin, direction, getLOD()); + guideToAugmented(visitor); + if (visitor.intersectionDistance == FLT_MAX) { + return false; + } + distance = visitor.intersectionDistance; + return true; +} + +class HeightfieldHeightVisitor : public MetavoxelVisitor { +public: + + float height; + + HeightfieldHeightVisitor(const MetavoxelLOD& lod, const glm::vec3& location); + + virtual int visit(MetavoxelInfo& info); + +private: + + glm::vec3 _location; +}; + +HeightfieldHeightVisitor::HeightfieldHeightVisitor(const MetavoxelLOD& lod, const glm::vec3& location) : + MetavoxelVisitor(QVector() << + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute(), QVector(), lod), + height(-FLT_MAX), + _location(location) { +} + +static const int REVERSE_ORDER = MetavoxelVisitor::encodeOrder(7, 6, 5, 4, 3, 2, 1, 0); + +int HeightfieldHeightVisitor::visit(MetavoxelInfo& info) { + glm::vec3 relative = _location - info.minimum; + if (relative.x < 0.0f || relative.z < 0.0f || relative.x > info.size || relative.z > info.size || + height >= info.minimum.y + info.size) { + return STOP_RECURSION; + } + if (!info.isLeaf) { + return REVERSE_ORDER; + } + const HeightfieldBuffer* buffer = static_cast( + info.inputValues.at(0).getInlineValue().data()); + if (!buffer) { + return STOP_RECURSION; + } + const QByteArray& contents = buffer->getHeight(); + const uchar* src = (const uchar*)contents.constData(); + int size = glm::sqrt((float)contents.size()); + int unextendedSize = size - HeightfieldBuffer::HEIGHT_EXTENSION; + int highest = HeightfieldBuffer::HEIGHT_BORDER + unextendedSize; + relative *= unextendedSize / info.size; + relative.x += HeightfieldBuffer::HEIGHT_BORDER; + relative.z += HeightfieldBuffer::HEIGHT_BORDER; + + // find the bounds of the cell containing the point and the shared vertex heights + glm::vec3 floors = glm::floor(relative); + glm::vec3 ceils = glm::ceil(relative); + glm::vec3 fracts = glm::fract(relative); + int floorX = qMin(qMax((int)floors.x, HeightfieldBuffer::HEIGHT_BORDER), highest); + int floorZ = qMin(qMax((int)floors.z, HeightfieldBuffer::HEIGHT_BORDER), highest); + int ceilX = qMin(qMax((int)ceils.x, HeightfieldBuffer::HEIGHT_BORDER), highest); + int ceilZ = qMin(qMax((int)ceils.z, HeightfieldBuffer::HEIGHT_BORDER), highest); + float upperLeft = src[floorZ * size + floorX]; + float lowerRight = src[ceilZ * size + ceilX]; + float interpolatedHeight = glm::mix(upperLeft, lowerRight, fracts.z); + + // the final vertex (and thus which triangle we check) depends on which half we're on + if (fracts.x >= fracts.z) { + float upperRight = src[floorZ * size + ceilX]; + interpolatedHeight = glm::mix(interpolatedHeight, glm::mix(upperRight, lowerRight, fracts.z), + (fracts.x - fracts.z) / (1.0f - fracts.z)); + + } else { + float lowerLeft = src[ceilZ * size + floorX]; + interpolatedHeight = glm::mix(glm::mix(upperLeft, lowerLeft, fracts.z), interpolatedHeight, fracts.x / fracts.z); + } + if (interpolatedHeight == 0.0f) { + return STOP_RECURSION; // ignore zero values + } + + // convert the interpolated height into world space + height = qMax(height, info.minimum.y + interpolatedHeight * info.size * EIGHT_BIT_MAXIMUM_RECIPROCAL); + return SHORT_CIRCUIT; +} + +float MetavoxelSystem::getHeightfieldHeight(const glm::vec3& location) { + HeightfieldHeightVisitor visitor(getLOD(), location); + guideToAugmented(visitor); + return visitor.height; +} + +class HeightfieldCursorRenderVisitor : public MetavoxelVisitor { +public: + + HeightfieldCursorRenderVisitor(const Box& bounds); + + virtual int visit(MetavoxelInfo& info); + +private: + + Box _bounds; +}; + +HeightfieldCursorRenderVisitor::HeightfieldCursorRenderVisitor(const Box& bounds) : + MetavoxelVisitor(QVector() << + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute()), + _bounds(bounds) { +} + +int HeightfieldCursorRenderVisitor::visit(MetavoxelInfo& info) { + if (!info.getBounds().intersects(_bounds)) { + return STOP_RECURSION; + } + if (!info.isLeaf) { + return DEFAULT_ORDER; + } + BufferDataPointer buffer = info.inputValues.at(0).getInlineValue(); + if (buffer) { + buffer->render(true); + } + return STOP_RECURSION; +} + +void MetavoxelSystem::renderHeightfieldCursor(const glm::vec3& position, float radius) { + glDepthFunc(GL_LEQUAL); + glEnable(GL_CULL_FACE); + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(-1.0f, -1.0f); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + + DefaultMetavoxelRendererImplementation::getHeightfieldCursorProgram().bind(); + + glActiveTexture(GL_TEXTURE4); + float scale = 1.0f / radius; + glm::vec4 sCoefficients(scale, 0.0f, 0.0f, -scale * position.x); + glm::vec4 tCoefficients(0.0f, 0.0f, scale, -scale * position.z); + glTexGenfv(GL_S, GL_EYE_PLANE, (const GLfloat*)&sCoefficients); + glTexGenfv(GL_T, GL_EYE_PLANE, (const GLfloat*)&tCoefficients); + glActiveTexture(GL_TEXTURE0); + + glm::vec3 extents(radius, radius, radius); + HeightfieldCursorRenderVisitor visitor(Box(position - extents, position + extents)); + guideToAugmented(visitor); + + DefaultMetavoxelRendererImplementation::getHeightfieldCursorProgram().release(); + + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); + + glDisable(GL_POLYGON_OFFSET_FILL); + glDisable(GL_CULL_FACE); + glDepthFunc(GL_LESS); +} + +void MetavoxelSystem::deleteTextures(int heightID, int colorID) { + glDeleteTextures(1, (GLuint*)&heightID); + glDeleteTextures(1, (GLuint*)&colorID); } MetavoxelClient* MetavoxelSystem::createClient(const SharedNodePointer& node) { @@ -239,11 +563,14 @@ void MetavoxelSystemClient::sendDatagram(const QByteArray& data) { Application::getInstance()->getBandwidthMeter()->outputStream(BandwidthMeter::METAVOXELS).updateValue(data.size()); } +BufferData::~BufferData() { +} + PointBuffer::PointBuffer(const BufferPointVector& points) : _points(points) { } -void PointBuffer::render() { +void PointBuffer::render(bool cursor) { // initialize buffer, etc. on first render if (!_buffer.isCreated()) { _buffer.setUsagePattern(QOpenGLBuffer::StaticDraw); @@ -269,34 +596,321 @@ void PointBuffer::render() { _buffer.release(); } -PointBufferAttribute::PointBufferAttribute() : - InlineAttribute("pointBuffer") { +const int HeightfieldBuffer::HEIGHT_BORDER = 1; +const int HeightfieldBuffer::SHARED_EDGE = 1; +const int HeightfieldBuffer::HEIGHT_EXTENSION = 2 * HeightfieldBuffer::HEIGHT_BORDER + HeightfieldBuffer::SHARED_EDGE; + +HeightfieldBuffer::HeightfieldBuffer(const glm::vec3& translation, float scale, + const QByteArray& height, const QByteArray& color) : + _translation(translation), + _scale(scale), + _heightBounds(translation, translation + glm::vec3(scale, scale, scale)), + _colorBounds(_heightBounds), + _height(height), + _color(color), + _heightTextureID(0), + _colorTextureID(0), + _heightSize(glm::sqrt(height.size())), + _heightIncrement(scale / (_heightSize - HEIGHT_EXTENSION)), + _colorSize(glm::sqrt(color.size() / HeightfieldData::COLOR_BYTES)), + _colorIncrement(scale / (_colorSize - SHARED_EDGE)) { + + _heightBounds.minimum.x -= _heightIncrement * HEIGHT_BORDER; + _heightBounds.minimum.z -= _heightIncrement * HEIGHT_BORDER; + _heightBounds.maximum.x += _heightIncrement * (SHARED_EDGE + HEIGHT_BORDER); + _heightBounds.maximum.z += _heightIncrement * (SHARED_EDGE + HEIGHT_BORDER); + + _colorBounds.maximum.x += _colorIncrement * SHARED_EDGE; + _colorBounds.maximum.z += _colorIncrement * SHARED_EDGE; } -bool PointBufferAttribute::merge(void*& parent, void* children[], bool postRead) const { - PointBufferPointer firstChild = decodeInline(children[0]); - for (int i = 1; i < MERGE_COUNT; i++) { - if (firstChild != decodeInline(children[i])) { - *(PointBufferPointer*)&parent = _defaultValue; +HeightfieldBuffer::~HeightfieldBuffer() { + // the textures have to be deleted on the main thread (for its opengl context) + if (QThread::currentThread() != Application::getInstance()->thread()) { + QMetaObject::invokeMethod(Application::getInstance()->getMetavoxels(), "deleteTextures", + Q_ARG(int, _heightTextureID), Q_ARG(int, _colorTextureID)); + } else { + glDeleteTextures(1, &_heightTextureID); + glDeleteTextures(1, &_colorTextureID); + } +} + +QByteArray HeightfieldBuffer::getUnextendedHeight() const { + int srcSize = glm::sqrt(_height.size()); + int destSize = srcSize - 3; + QByteArray unextended(destSize * destSize, 0); + const char* src = _height.constData() + srcSize + 1; + char* dest = unextended.data(); + for (int z = 0; z < destSize; z++, src += srcSize, dest += destSize) { + memcpy(dest, src, destSize); + } + return unextended; +} + +QByteArray HeightfieldBuffer::getUnextendedColor() const { + int srcSize = glm::sqrt(_color.size() / HeightfieldData::COLOR_BYTES); + int destSize = srcSize - 1; + QByteArray unextended(destSize * destSize * HeightfieldData::COLOR_BYTES, 0); + const char* src = _color.constData(); + int srcStride = srcSize * HeightfieldData::COLOR_BYTES; + char* dest = unextended.data(); + int destStride = destSize * HeightfieldData::COLOR_BYTES; + for (int z = 0; z < destSize; z++, src += srcStride, dest += destStride) { + memcpy(dest, src, destStride); + } + return unextended; +} + +class HeightfieldPoint { +public: + glm::vec2 textureCoord; + glm::vec3 vertex; +}; + +void HeightfieldBuffer::render(bool cursor) { + // initialize textures, etc. on first render + if (_heightTextureID == 0) { + glGenTextures(1, &_heightTextureID); + glBindTexture(GL_TEXTURE_2D, _heightTextureID); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, _heightSize, _heightSize, 0, + GL_LUMINANCE, GL_UNSIGNED_BYTE, _height.constData()); + + glGenTextures(1, &_colorTextureID); + glBindTexture(GL_TEXTURE_2D, _colorTextureID); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + if (_color.isEmpty()) { + const quint8 WHITE_COLOR[] = { 255, 255, 255 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, WHITE_COLOR); + + } else { + int colorSize = glm::sqrt(_color.size() / HeightfieldData::COLOR_BYTES); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, colorSize, colorSize, 0, GL_RGB, GL_UNSIGNED_BYTE, _color.constData()); + } + } + // create the buffer objects lazily + int innerSize = _heightSize - 2 * HeightfieldBuffer::HEIGHT_BORDER; + int vertexCount = _heightSize * _heightSize; + int rows = _heightSize - 1; + int indexCount = rows * rows * 3 * 2; + BufferPair& bufferPair = _bufferPairs[_heightSize]; + if (!bufferPair.first.isCreated()) { + QVector vertices(vertexCount); + HeightfieldPoint* point = vertices.data(); + + float vertexStep = 1.0f / (innerSize - 1); + float z = -vertexStep; + float textureStep = 1.0f / _heightSize; + float t = textureStep / 2.0f; + for (int i = 0; i < _heightSize; i++, z += vertexStep, t += textureStep) { + float x = -vertexStep; + float s = textureStep / 2.0f; + const float SKIRT_LENGTH = 0.25f; + float baseY = (i == 0 || i == _heightSize - 1) ? -SKIRT_LENGTH : 0.0f; + for (int j = 0; j < _heightSize; j++, point++, x += vertexStep, s += textureStep) { + point->vertex = glm::vec3(x, (j == 0 || j == _heightSize - 1) ? -SKIRT_LENGTH : baseY, z); + point->textureCoord = glm::vec2(s, t); + } + } + + bufferPair.first.setUsagePattern(QOpenGLBuffer::StaticDraw); + bufferPair.first.create(); + bufferPair.first.bind(); + bufferPair.first.allocate(vertices.constData(), vertexCount * sizeof(HeightfieldPoint)); + + QVector indices(indexCount); + int* index = indices.data(); + for (int i = 0; i < rows; i++) { + int lineIndex = i * _heightSize; + int nextLineIndex = (i + 1) * _heightSize; + for (int j = 0; j < rows; j++) { + *index++ = lineIndex + j; + *index++ = nextLineIndex + j; + *index++ = nextLineIndex + j + 1; + + *index++ = nextLineIndex + j + 1; + *index++ = lineIndex + j + 1; + *index++ = lineIndex + j; + } + } + + bufferPair.second = QOpenGLBuffer(QOpenGLBuffer::IndexBuffer); + bufferPair.second.create(); + bufferPair.second.bind(); + bufferPair.second.allocate(indices.constData(), indexCount * sizeof(int)); + + } else { + bufferPair.first.bind(); + bufferPair.second.bind(); + } + + HeightfieldPoint* point = 0; + glVertexPointer(3, GL_FLOAT, sizeof(HeightfieldPoint), &point->vertex); + glTexCoordPointer(2, GL_FLOAT, sizeof(HeightfieldPoint), &point->textureCoord); + + glPushMatrix(); + glTranslatef(_translation.x, _translation.y, _translation.z); + glScalef(_scale, _scale, _scale); + + glBindTexture(GL_TEXTURE_2D, _heightTextureID); + + if (!cursor) { + int heightScaleLocation = DefaultMetavoxelRendererImplementation::getHeightScaleLocation(); + int colorScaleLocation = DefaultMetavoxelRendererImplementation::getColorScaleLocation(); + ProgramObject* program = &DefaultMetavoxelRendererImplementation::getHeightfieldProgram(); + if (Menu::getInstance()->isOptionChecked(MenuOption::SimpleShadows)) { + heightScaleLocation = DefaultMetavoxelRendererImplementation::getShadowMapHeightScaleLocation(); + colorScaleLocation = DefaultMetavoxelRendererImplementation::getShadowMapColorScaleLocation(); + program = &DefaultMetavoxelRendererImplementation::getShadowMapHeightfieldProgram(); + + } else if (Menu::getInstance()->isOptionChecked(MenuOption::CascadedShadows)) { + heightScaleLocation = DefaultMetavoxelRendererImplementation::getCascadedShadowMapHeightScaleLocation(); + colorScaleLocation = DefaultMetavoxelRendererImplementation::getCascadedShadowMapColorScaleLocation(); + program = &DefaultMetavoxelRendererImplementation::getCascadedShadowMapHeightfieldProgram(); + } + program->setUniformValue(heightScaleLocation, 1.0f / _heightSize); + program->setUniformValue(colorScaleLocation, (float)_heightSize / innerSize); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, _colorTextureID); + } + + glDrawRangeElements(GL_TRIANGLES, 0, vertexCount - 1, indexCount, GL_UNSIGNED_INT, 0); + + if (!cursor) { + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + } + + glBindTexture(GL_TEXTURE_2D, 0); + + glPopMatrix(); + + bufferPair.first.release(); + bufferPair.second.release(); +} + +QHash HeightfieldBuffer::_bufferPairs; + +void HeightfieldPreview::render(const glm::vec3& translation, float scale) const { + glDisable(GL_BLEND); + glEnable(GL_CULL_FACE); + glEnable(GL_ALPHA_TEST); + glAlphaFunc(GL_EQUAL, 0.0f); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + + DefaultMetavoxelRendererImplementation::getHeightfieldProgram().bind(); + + glPushMatrix(); + glTranslatef(translation.x, translation.y, translation.z); + glScalef(scale, scale, scale); + + foreach (const BufferDataPointer& buffer, _buffers) { + buffer->render(); + } + + glPopMatrix(); + + DefaultMetavoxelRendererImplementation::getHeightfieldProgram().release(); + + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); + + glDisable(GL_ALPHA_TEST); + glDisable(GL_CULL_FACE); + glEnable(GL_BLEND); +} + +BufferDataAttribute::BufferDataAttribute(const QString& name) : + InlineAttribute(name) { +} + +bool BufferDataAttribute::merge(void*& parent, void* children[], bool postRead) const { + *(BufferDataPointer*)&parent = _defaultValue; + for (int i = 0; i < MERGE_COUNT; i++) { + if (decodeInline(children[i])) { return false; } } - *(PointBufferPointer*)&parent = firstChild; return true; } -void PointMetavoxelRendererImplementation::init() { - if (!_program.isLinked()) { - _program.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + "shaders/metavoxel_point.vert"); - _program.link(); +AttributeValue BufferDataAttribute::inherit(const AttributeValue& parentValue) const { + return AttributeValue(parentValue.getAttribute()); +} + +void DefaultMetavoxelRendererImplementation::init() { + if (!_pointProgram.isLinked()) { + _pointProgram.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + "shaders/metavoxel_point.vert"); + _pointProgram.link(); - _program.bind(); - _pointScaleLocation = _program.uniformLocation("pointScale"); - _program.release(); + _pointProgram.bind(); + _pointScaleLocation = _pointProgram.uniformLocation("pointScale"); + _pointProgram.release(); + + _heightfieldProgram.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + + "shaders/metavoxel_heightfield.vert"); + _heightfieldProgram.addShaderFromSourceFile(QGLShader::Fragment, Application::resourcesPath() + + "shaders/metavoxel_heightfield.frag"); + _heightfieldProgram.link(); + + _heightfieldProgram.bind(); + _heightfieldProgram.setUniformValue("heightMap", 0); + _heightfieldProgram.setUniformValue("diffuseMap", 1); + _heightScaleLocation = _heightfieldProgram.uniformLocation("heightScale"); + _colorScaleLocation = _heightfieldProgram.uniformLocation("colorScale"); + _heightfieldProgram.release(); + + _shadowMapHeightfieldProgram.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + + "shaders/metavoxel_heightfield.vert"); + _shadowMapHeightfieldProgram.addShaderFromSourceFile(QGLShader::Fragment, Application::resourcesPath() + + "shaders/metavoxel_heightfield_shadow_map.frag"); + _shadowMapHeightfieldProgram.link(); + + _shadowMapHeightfieldProgram.bind(); + _shadowMapHeightfieldProgram.setUniformValue("heightMap", 0); + _shadowMapHeightfieldProgram.setUniformValue("diffuseMap", 1); + _shadowMapHeightfieldProgram.setUniformValue("shadowMap", 2); + _shadowMapHeightScaleLocation = _shadowMapHeightfieldProgram.uniformLocation("heightScale"); + _shadowMapColorScaleLocation = _shadowMapHeightfieldProgram.uniformLocation("colorScale"); + _shadowMapHeightfieldProgram.release(); + + _cascadedShadowMapHeightfieldProgram.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + + "shaders/metavoxel_heightfield.vert"); + _cascadedShadowMapHeightfieldProgram.addShaderFromSourceFile(QGLShader::Fragment, Application::resourcesPath() + + "shaders/metavoxel_heightfield_cascaded_shadow_map.frag"); + _cascadedShadowMapHeightfieldProgram.link(); + + _cascadedShadowMapHeightfieldProgram.bind(); + _cascadedShadowMapHeightfieldProgram.setUniformValue("heightMap", 0); + _cascadedShadowMapHeightfieldProgram.setUniformValue("diffuseMap", 1); + _cascadedShadowMapHeightfieldProgram.setUniformValue("shadowMap", 2); + _cascadedShadowMapHeightScaleLocation = _cascadedShadowMapHeightfieldProgram.uniformLocation("heightScale"); + _cascadedShadowMapColorScaleLocation = _cascadedShadowMapHeightfieldProgram.uniformLocation("colorScale"); + _shadowDistancesLocation = _cascadedShadowMapHeightfieldProgram.uniformLocation("shadowDistances"); + _cascadedShadowMapHeightfieldProgram.release(); + + _heightfieldCursorProgram.addShaderFromSourceFile(QGLShader::Vertex, Application::resourcesPath() + + "shaders/metavoxel_heightfield_cursor.vert"); + _heightfieldCursorProgram.addShaderFromSourceFile(QGLShader::Fragment, Application::resourcesPath() + + "shaders/metavoxel_heightfield_cursor.frag"); + _heightfieldCursorProgram.link(); + + _heightfieldCursorProgram.bind(); + _heightfieldCursorProgram.setUniformValue("heightMap", 0); + _heightfieldCursorProgram.release(); } } -PointMetavoxelRendererImplementation::PointMetavoxelRendererImplementation() { +DefaultMetavoxelRendererImplementation::DefaultMetavoxelRendererImplementation() { } class PointAugmentVisitor : public MetavoxelVisitor { @@ -341,11 +955,14 @@ int PointAugmentVisitor::visit(MetavoxelInfo& info) { { quint8(qRed(normal)), quint8(qGreen(normal)), quint8(qBlue(normal)) } }; _points.append(point); } - if (info.size >= _pointLeafSize) { - BufferPointVector swapPoints; - _points.swap(swapPoints); - info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(PointBufferPointer( - new PointBuffer(swapPoints)))); + if (info.size >= _pointLeafSize) { + PointBuffer* buffer = NULL; + if (!_points.isEmpty()) { + BufferPointVector swapPoints; + _points.swap(swapPoints); + buffer = new PointBuffer(swapPoints); + } + info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(BufferDataPointer(buffer))); } return STOP_RECURSION; } @@ -354,14 +971,292 @@ bool PointAugmentVisitor::postVisit(MetavoxelInfo& info) { if (info.size != _pointLeafSize) { return false; } - BufferPointVector swapPoints; - _points.swap(swapPoints); - info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(PointBufferPointer( - new PointBuffer(swapPoints)))); + PointBuffer* buffer = NULL; + if (!_points.isEmpty()) { + BufferPointVector swapPoints; + _points.swap(swapPoints); + buffer = new PointBuffer(swapPoints); + } + info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(BufferDataPointer(buffer))); return true; } -void PointMetavoxelRendererImplementation::augment(MetavoxelData& data, const MetavoxelData& previous, +class HeightfieldFetchVisitor : public MetavoxelVisitor { +public: + + HeightfieldFetchVisitor(const MetavoxelLOD& lod, const QVector& intersections); + + void init(HeightfieldBuffer* buffer) { _buffer = buffer; } + + virtual int visit(MetavoxelInfo& info); + +private: + + const QVector& _intersections; + HeightfieldBuffer* _buffer; +}; + +HeightfieldFetchVisitor::HeightfieldFetchVisitor(const MetavoxelLOD& lod, const QVector& intersections) : + MetavoxelVisitor(QVector() << AttributeRegistry::getInstance()->getHeightfieldAttribute() << + AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), QVector(), lod), + _intersections(intersections) { +} + +int HeightfieldFetchVisitor::visit(MetavoxelInfo& info) { + Box bounds = info.getBounds(); + const Box& heightBounds = _buffer->getHeightBounds(); + if (!bounds.intersects(heightBounds)) { + return STOP_RECURSION; + } + if (!info.isLeaf && info.size > _buffer->getScale()) { + return DEFAULT_ORDER; + } + HeightfieldDataPointer height = info.inputValues.at(0).getInlineValue(); + if (!height) { + return STOP_RECURSION; + } + foreach (const Box& intersection, _intersections) { + Box overlap = intersection.getIntersection(bounds); + if (overlap.isEmpty()) { + continue; + } + float heightIncrement = _buffer->getHeightIncrement(); + int destX = (overlap.minimum.x - heightBounds.minimum.x) / heightIncrement; + int destY = (overlap.minimum.z - heightBounds.minimum.z) / heightIncrement; + int destWidth = glm::ceil((overlap.maximum.x - overlap.minimum.x) / heightIncrement); + int destHeight = glm::ceil((overlap.maximum.z - overlap.minimum.z) / heightIncrement); + int heightSize = _buffer->getHeightSize(); + char* dest = _buffer->getHeight().data() + destY * heightSize + destX; + + const QByteArray& srcHeight = height->getContents(); + int srcSize = glm::sqrt(srcHeight.size()); + float srcIncrement = info.size / srcSize; + + if (info.size == _buffer->getScale() && srcSize == (heightSize - HeightfieldBuffer::HEIGHT_EXTENSION)) { + // easy case: same resolution + int srcX = (overlap.minimum.x - info.minimum.x) / srcIncrement; + int srcY = (overlap.minimum.z - info.minimum.z) / srcIncrement; + + const char* src = srcHeight.constData() + srcY * srcSize + srcX; + for (int y = 0; y < destHeight; y++, src += srcSize, dest += heightSize) { + memcpy(dest, src, destWidth); + } + } else { + // more difficult: different resolutions + float srcX = (overlap.minimum.x - info.minimum.x) / srcIncrement; + float srcY = (overlap.minimum.z - info.minimum.z) / srcIncrement; + float srcAdvance = heightIncrement / srcIncrement; + int shift = 0; + float size = _buffer->getScale(); + while (size < info.size) { + shift++; + size *= 2.0f; + } + const int EIGHT_BIT_MAXIMUM = 255; + int subtract = (_buffer->getTranslation().y - info.minimum.y) * EIGHT_BIT_MAXIMUM / _buffer->getScale(); + for (int y = 0; y < destHeight; y++, dest += heightSize, srcY += srcAdvance) { + const uchar* src = (const uchar*)srcHeight.constData() + (int)srcY * srcSize; + float lineSrcX = srcX; + for (char* lineDest = dest, *end = dest + destWidth; lineDest != end; lineDest++, lineSrcX += srcAdvance) { + *lineDest = qMin(qMax(0, (src[(int)lineSrcX] << shift) - subtract), EIGHT_BIT_MAXIMUM); + } + } + } + + int colorSize = _buffer->getColorSize(); + if (colorSize == 0) { + return STOP_RECURSION; + } + HeightfieldDataPointer color = info.inputValues.at(1).getInlineValue(); + if (!color) { + return STOP_RECURSION; + } + const Box& colorBounds = _buffer->getColorBounds(); + overlap = colorBounds.getIntersection(overlap); + float colorIncrement = _buffer->getColorIncrement(); + destX = (overlap.minimum.x - colorBounds.minimum.x) / colorIncrement; + destY = (overlap.minimum.z - colorBounds.minimum.z) / colorIncrement; + destWidth = glm::ceil((overlap.maximum.x - overlap.minimum.x) / colorIncrement); + destHeight = glm::ceil((overlap.maximum.z - overlap.minimum.z) / colorIncrement); + dest = _buffer->getColor().data() + (destY * colorSize + destX) * HeightfieldData::COLOR_BYTES; + int destStride = colorSize * HeightfieldData::COLOR_BYTES; + int destBytes = destWidth * HeightfieldData::COLOR_BYTES; + + const QByteArray& srcColor = color->getContents(); + srcSize = glm::sqrt(srcColor.size() / HeightfieldData::COLOR_BYTES); + int srcStride = srcSize * HeightfieldData::COLOR_BYTES; + srcIncrement = info.size / srcSize; + + if (srcIncrement == colorIncrement) { + // easy case: same resolution + int srcX = (overlap.minimum.x - info.minimum.x) / srcIncrement; + int srcY = (overlap.minimum.z - info.minimum.z) / srcIncrement; + + const char* src = srcColor.constData() + (srcY * srcSize + srcX) * HeightfieldData::COLOR_BYTES; + for (int y = 0; y < destHeight; y++, src += srcStride, dest += destStride) { + memcpy(dest, src, destBytes); + } + } else { + // more difficult: different resolutions + float srcX = (overlap.minimum.x - info.minimum.x) / srcIncrement; + float srcY = (overlap.minimum.z - info.minimum.z) / srcIncrement; + float srcAdvance = colorIncrement / srcIncrement; + for (int y = 0; y < destHeight; y++, dest += destStride, srcY += srcAdvance) { + const char* src = srcColor.constData() + (int)srcY * srcStride; + float lineSrcX = srcX; + for (char* lineDest = dest, *end = dest + destBytes; lineDest != end; lineDest += HeightfieldData::COLOR_BYTES, + lineSrcX += srcAdvance) { + const char* lineSrc = src + (int)lineSrcX * HeightfieldData::COLOR_BYTES; + lineDest[0] = lineSrc[0]; + lineDest[1] = lineSrc[1]; + lineDest[2] = lineSrc[2]; + } + } + } + } + return STOP_RECURSION; +} + +class HeightfieldRegionVisitor : public MetavoxelVisitor { +public: + + QVector regions; + Box regionBounds; + + HeightfieldRegionVisitor(const MetavoxelLOD& lod); + + virtual int visit(MetavoxelInfo& info); + +private: + + void addRegion(const Box& unextended, const Box& extended); + + QVector _intersections; + HeightfieldFetchVisitor _fetchVisitor; +}; + +HeightfieldRegionVisitor::HeightfieldRegionVisitor(const MetavoxelLOD& lod) : + MetavoxelVisitor(QVector() << AttributeRegistry::getInstance()->getHeightfieldAttribute() << + AttributeRegistry::getInstance()->getHeightfieldColorAttribute() << + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute(), QVector() << + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute(), lod), + regionBounds(glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX), glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX)), + _fetchVisitor(lod, _intersections) { +} + +int HeightfieldRegionVisitor::visit(MetavoxelInfo& info) { + if (!info.isLeaf) { + return DEFAULT_ORDER; + } + HeightfieldBuffer* buffer = NULL; + HeightfieldDataPointer height = info.inputValues.at(0).getInlineValue(); + if (height) { + const QByteArray& heightContents = height->getContents(); + int size = glm::sqrt(heightContents.size()); + int extendedSize = size + HeightfieldBuffer::HEIGHT_EXTENSION; + int heightContentsSize = extendedSize * extendedSize; + + HeightfieldDataPointer color = info.inputValues.at(1).getInlineValue(); + int colorContentsSize = 0; + if (color) { + const QByteArray& colorContents = color->getContents(); + int colorSize = glm::sqrt(colorContents.size() / HeightfieldData::COLOR_BYTES); + int extendedColorSize = colorSize + HeightfieldBuffer::SHARED_EDGE; + colorContentsSize = extendedColorSize * extendedColorSize * HeightfieldData::COLOR_BYTES; + } + + const HeightfieldBuffer* existingBuffer = static_cast( + info.inputValues.at(2).getInlineValue().data()); + Box bounds = info.getBounds(); + if (existingBuffer && existingBuffer->getHeight().size() == heightContentsSize && + existingBuffer->getColor().size() == colorContentsSize) { + // we already have a buffer of the correct resolution + addRegion(bounds, existingBuffer->getHeightBounds()); + return STOP_RECURSION; + } + // we must create a new buffer and update its borders + buffer = new HeightfieldBuffer(info.minimum, info.size, QByteArray(heightContentsSize, 0), + QByteArray(colorContentsSize, 0)); + const Box& heightBounds = buffer->getHeightBounds(); + addRegion(bounds, heightBounds); + + _intersections.clear(); + _intersections.append(Box(heightBounds.minimum, + glm::vec3(bounds.maximum.x, heightBounds.maximum.y, bounds.minimum.z))); + _intersections.append(Box(glm::vec3(bounds.maximum.x, heightBounds.minimum.y, heightBounds.minimum.z), + glm::vec3(heightBounds.maximum.x, heightBounds.maximum.y, bounds.maximum.z))); + _intersections.append(Box(glm::vec3(bounds.minimum.x, heightBounds.minimum.y, bounds.maximum.z), + heightBounds.maximum)); + _intersections.append(Box(glm::vec3(heightBounds.minimum.x, heightBounds.minimum.y, bounds.minimum.z), + glm::vec3(bounds.minimum.x, heightBounds.maximum.y, heightBounds.maximum.z))); + + _fetchVisitor.init(buffer); + _data->guide(_fetchVisitor); + } + info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(BufferDataPointer(buffer))); + return STOP_RECURSION; +} + +void HeightfieldRegionVisitor::addRegion(const Box& unextended, const Box& extended) { + regions.append(unextended); + regionBounds.add(extended); +} + +class HeightfieldUpdateVisitor : public MetavoxelVisitor { +public: + + HeightfieldUpdateVisitor(const MetavoxelLOD& lod, const QVector& regions, const Box& regionBounds); + + virtual int visit(MetavoxelInfo& info); + +private: + + const QVector& _regions; + const Box& _regionBounds; + QVector _intersections; + HeightfieldFetchVisitor _fetchVisitor; +}; + +HeightfieldUpdateVisitor::HeightfieldUpdateVisitor(const MetavoxelLOD& lod, const QVector& regions, + const Box& regionBounds) : + MetavoxelVisitor(QVector() << + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute(), QVector() << + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute(), lod), + _regions(regions), + _regionBounds(regionBounds), + _fetchVisitor(lod, _intersections) { +} + +int HeightfieldUpdateVisitor::visit(MetavoxelInfo& info) { + if (!info.getBounds().intersects(_regionBounds)) { + return STOP_RECURSION; + } + if (!info.isLeaf) { + return DEFAULT_ORDER; + } + const HeightfieldBuffer* buffer = static_cast( + info.inputValues.at(0).getInlineValue().data()); + if (!buffer) { + return STOP_RECURSION; + } + _intersections.clear(); + foreach (const Box& region, _regions) { + if (region.intersects(buffer->getHeightBounds())) { + _intersections.append(region.getIntersection(buffer->getHeightBounds())); + } + } + if (_intersections.isEmpty()) { + return STOP_RECURSION; + } + HeightfieldBuffer* newBuffer = new HeightfieldBuffer(info.minimum, info.size, + buffer->getHeight(), buffer->getColor()); + _fetchVisitor.init(newBuffer); + _data->guide(_fetchVisitor); + info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(BufferDataPointer(newBuffer))); + return STOP_RECURSION; +} + +void DefaultMetavoxelRendererImplementation::augment(MetavoxelData& data, const MetavoxelData& previous, MetavoxelInfo& info, const MetavoxelLOD& lod) { // copy the previous buffers MetavoxelData expandedPrevious = previous; @@ -374,38 +1269,133 @@ void PointMetavoxelRendererImplementation::augment(MetavoxelData& data, const Me data.setRoot(pointBufferAttribute, root); root->incrementReferenceCount(); } + const AttributePointer& heightfieldBufferAttribute = + Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute(); + root = expandedPrevious.getRoot(heightfieldBufferAttribute); + if (root) { + data.setRoot(heightfieldBufferAttribute, root); + root->incrementReferenceCount(); + } - PointAugmentVisitor visitor(lod); - data.guideToDifferent(expandedPrevious, visitor); + PointAugmentVisitor pointAugmentVisitor(lod); + data.guideToDifferent(expandedPrevious, pointAugmentVisitor); + + HeightfieldRegionVisitor heightfieldRegionVisitor(lod); + data.guideToDifferent(expandedPrevious, heightfieldRegionVisitor); + + HeightfieldUpdateVisitor heightfieldUpdateVisitor(lod, heightfieldRegionVisitor.regions, + heightfieldRegionVisitor.regionBounds); + data.guide(heightfieldUpdateVisitor); } -class PointRenderVisitor : public MetavoxelVisitor { +class SpannerSimulateVisitor : public SpannerVisitor { public: - PointRenderVisitor(const MetavoxelLOD& lod); + SpannerSimulateVisitor(float deltaTime, const MetavoxelLOD& lod); + + virtual bool visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize); + +private: + + float _deltaTime; +}; + +SpannerSimulateVisitor::SpannerSimulateVisitor(float deltaTime, const MetavoxelLOD& lod) : + SpannerVisitor(QVector() << AttributeRegistry::getInstance()->getSpannersAttribute(), + QVector(), QVector(), QVector(), lod), + _deltaTime(deltaTime) { +} + +bool SpannerSimulateVisitor::visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize) { + spanner->getRenderer()->simulate(_deltaTime); + return true; +} + +void DefaultMetavoxelRendererImplementation::simulate(MetavoxelData& data, float deltaTime, + MetavoxelInfo& info, const MetavoxelLOD& lod) { + SpannerSimulateVisitor spannerSimulateVisitor(deltaTime, lod); + data.guide(spannerSimulateVisitor); +} + +class SpannerRenderVisitor : public SpannerVisitor { +public: + + SpannerRenderVisitor(const MetavoxelLOD& lod); + + virtual int visit(MetavoxelInfo& info); + virtual bool visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize); + +private: + + int _containmentDepth; +}; + +SpannerRenderVisitor::SpannerRenderVisitor(const MetavoxelLOD& lod) : + SpannerVisitor(QVector() << AttributeRegistry::getInstance()->getSpannersAttribute(), + QVector(), QVector(), QVector(), + lod, encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())), + _containmentDepth(INT_MAX) { +} + +int SpannerRenderVisitor::visit(MetavoxelInfo& info) { + if (_containmentDepth >= _depth) { + Frustum::IntersectionType intersection = Application::getInstance()->getMetavoxels()->getFrustum().getIntersectionType( + info.getBounds()); + if (intersection == Frustum::NO_INTERSECTION) { + return STOP_RECURSION; + } + _containmentDepth = (intersection == Frustum::CONTAINS_INTERSECTION) ? _depth : INT_MAX; + } + return SpannerVisitor::visit(info); +} + +bool SpannerRenderVisitor::visit(Spanner* spanner, const glm::vec3& clipMinimum, float clipSize) { + spanner->getRenderer()->render(1.0f, SpannerRenderer::DEFAULT_MODE, clipMinimum, clipSize); + return true; +} + +class BufferRenderVisitor : public MetavoxelVisitor { +public: + + BufferRenderVisitor(const AttributePointer& attribute); virtual int visit(MetavoxelInfo& info); private: int _order; + int _containmentDepth; }; -PointRenderVisitor::PointRenderVisitor(const MetavoxelLOD& lod) : - MetavoxelVisitor(QVector() << Application::getInstance()->getMetavoxels()->getPointBufferAttribute(), - QVector(), lod), - _order(encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())) { +BufferRenderVisitor::BufferRenderVisitor(const AttributePointer& attribute) : + MetavoxelVisitor(QVector() << attribute), + _order(encodeOrder(Application::getInstance()->getViewFrustum()->getDirection())), + _containmentDepth(INT_MAX) { } -int PointRenderVisitor::visit(MetavoxelInfo& info) { - PointBufferPointer buffer = info.inputValues.at(0).getInlineValue(); +int BufferRenderVisitor::visit(MetavoxelInfo& info) { + if (_containmentDepth >= _depth) { + Frustum::IntersectionType intersection = Application::getInstance()->getMetavoxels()->getFrustum().getIntersectionType( + info.getBounds()); + if (intersection == Frustum::NO_INTERSECTION) { + return STOP_RECURSION; + } + _containmentDepth = (intersection == Frustum::CONTAINS_INTERSECTION) ? _depth : INT_MAX; + } + if (!info.isLeaf) { + return _order; + } + BufferDataPointer buffer = info.inputValues.at(0).getInlineValue(); if (buffer) { buffer->render(); } - return info.isLeaf ? STOP_RECURSION : _order; + return STOP_RECURSION; } -void PointMetavoxelRendererImplementation::render(MetavoxelData& data, MetavoxelInfo& info, const MetavoxelLOD& lod) { +void DefaultMetavoxelRendererImplementation::render(MetavoxelData& data, MetavoxelInfo& info, const MetavoxelLOD& lod) { + SpannerRenderVisitor spannerRenderVisitor(lod); + data.guide(spannerRenderVisitor); + int viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); const int VIEWPORT_WIDTH_INDEX = 2; @@ -416,8 +1406,8 @@ void PointMetavoxelRendererImplementation::render(MetavoxelData& data, Metavoxel float worldDiagonal = glm::distance(Application::getInstance()->getViewFrustum()->getNearBottomLeft(), Application::getInstance()->getViewFrustum()->getNearTopRight()); - _program.bind(); - _program.setUniformValue(_pointScaleLocation, viewportDiagonal * + _pointProgram.bind(); + _pointProgram.setUniformValue(_pointScaleLocation, viewportDiagonal * Application::getInstance()->getViewFrustum()->getNearClip() / worldDiagonal); glEnableClientState(GL_VERTEX_ARRAY); @@ -428,22 +1418,71 @@ void PointMetavoxelRendererImplementation::render(MetavoxelData& data, Metavoxel glDisable(GL_BLEND); - PointRenderVisitor visitor(lod); - data.guide(visitor); - - glEnable(GL_BLEND); + BufferRenderVisitor pointRenderVisitor(Application::getInstance()->getMetavoxels()->getPointBufferAttribute()); + data.guide(pointRenderVisitor); glDisable(GL_VERTEX_PROGRAM_POINT_SIZE_ARB); - glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); glDisableClientState(GL_NORMAL_ARRAY); - _program.release(); + _pointProgram.release(); + + glEnable(GL_CULL_FACE); + glEnable(GL_ALPHA_TEST); + glAlphaFunc(GL_EQUAL, 0.0f); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + + ProgramObject* program = &_heightfieldProgram; + if (Menu::getInstance()->getShadowsEnabled()) { + if (Menu::getInstance()->isOptionChecked(MenuOption::CascadedShadows)) { + program = &_cascadedShadowMapHeightfieldProgram; + program->bind(); + program->setUniform(_shadowDistancesLocation, Application::getInstance()->getShadowDistances()); + + } else { + program = &_shadowMapHeightfieldProgram; + } + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, Application::getInstance()->getTextureCache()->getShadowDepthTextureID()); + glActiveTexture(GL_TEXTURE0); + } + + program->bind(); + + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + + BufferRenderVisitor heightfieldRenderVisitor(Application::getInstance()->getMetavoxels()->getHeightfieldBufferAttribute()); + data.guide(heightfieldRenderVisitor); + + program->release(); + + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); + + glDisable(GL_ALPHA_TEST); + glDisable(GL_CULL_FACE); + glEnable(GL_BLEND); } - -ProgramObject PointMetavoxelRendererImplementation::_program; -int PointMetavoxelRendererImplementation::_pointScaleLocation; + +ProgramObject DefaultMetavoxelRendererImplementation::_pointProgram; +int DefaultMetavoxelRendererImplementation::_pointScaleLocation; +ProgramObject DefaultMetavoxelRendererImplementation::_heightfieldProgram; +int DefaultMetavoxelRendererImplementation::_heightScaleLocation; +int DefaultMetavoxelRendererImplementation::_colorScaleLocation; +ProgramObject DefaultMetavoxelRendererImplementation::_shadowMapHeightfieldProgram; +int DefaultMetavoxelRendererImplementation::_shadowMapHeightScaleLocation; +int DefaultMetavoxelRendererImplementation::_shadowMapColorScaleLocation; +ProgramObject DefaultMetavoxelRendererImplementation::_cascadedShadowMapHeightfieldProgram; +int DefaultMetavoxelRendererImplementation::_cascadedShadowMapHeightScaleLocation; +int DefaultMetavoxelRendererImplementation::_cascadedShadowMapColorScaleLocation; +int DefaultMetavoxelRendererImplementation::_shadowDistancesLocation; +ProgramObject DefaultMetavoxelRendererImplementation::_heightfieldCursorProgram; static void enableClipPlane(GLenum plane, float x, float y, float z, float w) { GLdouble coefficients[] = { x, y, z, w }; diff --git a/interface/src/MetavoxelSystem.h b/interface/src/MetavoxelSystem.h index 4a5b99aa47..38d67bcaed 100644 --- a/interface/src/MetavoxelSystem.h +++ b/interface/src/MetavoxelSystem.h @@ -34,12 +34,23 @@ public: virtual void init(); virtual MetavoxelLOD getLOD(); + + const Frustum& getFrustum() const { return _frustum; } const AttributePointer& getPointBufferAttribute() { return _pointBufferAttribute; } + const AttributePointer& getHeightfieldBufferAttribute() { return _heightfieldBufferAttribute; } void simulate(float deltaTime); void render(); + void renderHeightfieldCursor(const glm::vec3& position, float radius); + + bool findFirstRayHeightfieldIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance); + + Q_INVOKABLE float getHeightfieldHeight(const glm::vec3& location); + + Q_INVOKABLE void deleteTextures(int heightID, int colorID); + protected: virtual MetavoxelClient* createClient(const SharedNodePointer& node); @@ -49,9 +60,11 @@ private: void guideToAugmented(MetavoxelVisitor& visitor); AttributePointer _pointBufferAttribute; + AttributePointer _heightfieldBufferAttribute; MetavoxelLOD _lod; QReadWriteLock _lodLock; + Frustum _frustum; }; /// Describes contents of a point in a point buffer. @@ -92,13 +105,24 @@ private: QReadWriteLock _augmentedDataLock; }; +/// Base class for cached static buffers. +class BufferData : public QSharedData { +public: + + virtual ~BufferData(); + + virtual void render(bool cursor = false) = 0; +}; + +typedef QExplicitlySharedDataPointer BufferDataPointer; + /// Contains the information necessary to render a group of points. -class PointBuffer : public QSharedData { +class PointBuffer : public BufferData { public: PointBuffer(const BufferPointVector& points); - void render(); + virtual void render(bool cursor = false); private: @@ -107,36 +131,133 @@ private: int _pointCount; }; -typedef QExplicitlySharedDataPointer PointBufferPointer; +/// Contains the information necessary to render a heightfield block. +class HeightfieldBuffer : public BufferData { +public: + + static const int HEIGHT_BORDER; + static const int SHARED_EDGE; + static const int HEIGHT_EXTENSION; + + HeightfieldBuffer(const glm::vec3& translation, float scale, const QByteArray& height, const QByteArray& color); + ~HeightfieldBuffer(); + + const glm::vec3& getTranslation() const { return _translation; } + float getScale() const { return _scale; } + + const Box& getHeightBounds() const { return _heightBounds; } + const Box& getColorBounds() const { return _colorBounds; } + + QByteArray& getHeight() { return _height; } + const QByteArray& getHeight() const { return _height; } + + QByteArray& getColor() { return _color; } + const QByteArray& getColor() const { return _color; } + + QByteArray getUnextendedHeight() const; + QByteArray getUnextendedColor() const; + + int getHeightSize() const { return _heightSize; } + float getHeightIncrement() const { return _heightIncrement; } + + int getColorSize() const { return _colorSize; } + float getColorIncrement() const { return _colorIncrement; } + + virtual void render(bool cursor = false); -/// A client-side attribute that stores point buffers. -class PointBufferAttribute : public InlineAttribute { +private: + + glm::vec3 _translation; + float _scale; + Box _heightBounds; + Box _colorBounds; + QByteArray _height; + QByteArray _color; + GLuint _heightTextureID; + GLuint _colorTextureID; + int _heightSize; + float _heightIncrement; + int _colorSize; + float _colorIncrement; + + typedef QPair BufferPair; + static QHash _bufferPairs; +}; + +/// Convenience class for rendering a preview of a heightfield. +class HeightfieldPreview { +public: + + void setBuffers(const QVector& buffers) { _buffers = buffers; } + const QVector& getBuffers() const { return _buffers; } + + void render(const glm::vec3& translation, float scale) const; + +private: + + QVector _buffers; +}; + +/// A client-side attribute that stores renderable buffers. +class BufferDataAttribute : public InlineAttribute { Q_OBJECT public: - Q_INVOKABLE PointBufferAttribute(); + Q_INVOKABLE BufferDataAttribute(const QString& name = QString()); virtual bool merge(void*& parent, void* children[], bool postRead = false) const; + + virtual AttributeValue inherit(const AttributeValue& parentValue) const; }; /// Renders metavoxels as points. -class PointMetavoxelRendererImplementation : public MetavoxelRendererImplementation { +class DefaultMetavoxelRendererImplementation : public MetavoxelRendererImplementation { Q_OBJECT public: static void init(); + + static ProgramObject& getHeightfieldProgram() { return _heightfieldProgram; } + static int getHeightScaleLocation() { return _heightScaleLocation; } + static int getColorScaleLocation() { return _colorScaleLocation; } - Q_INVOKABLE PointMetavoxelRendererImplementation(); + static ProgramObject& getShadowMapHeightfieldProgram() { return _shadowMapHeightfieldProgram; } + static int getShadowMapHeightScaleLocation() { return _shadowMapHeightScaleLocation; } + static int getShadowMapColorScaleLocation() { return _shadowMapColorScaleLocation; } + + static ProgramObject& getCascadedShadowMapHeightfieldProgram() { return _cascadedShadowMapHeightfieldProgram; } + static int getCascadedShadowMapHeightScaleLocation() { return _cascadedShadowMapHeightScaleLocation; } + static int getCascadedShadowMapColorScaleLocation() { return _cascadedShadowMapColorScaleLocation; } + + static ProgramObject& getHeightfieldCursorProgram() { return _heightfieldCursorProgram; } + + Q_INVOKABLE DefaultMetavoxelRendererImplementation(); virtual void augment(MetavoxelData& data, const MetavoxelData& previous, MetavoxelInfo& info, const MetavoxelLOD& lod); + virtual void simulate(MetavoxelData& data, float deltaTime, MetavoxelInfo& info, const MetavoxelLOD& lod); virtual void render(MetavoxelData& data, MetavoxelInfo& info, const MetavoxelLOD& lod); private: - static ProgramObject _program; - static int _pointScaleLocation; + static ProgramObject _pointProgram; + static int _pointScaleLocation; + + static ProgramObject _heightfieldProgram; + static int _heightScaleLocation; + static int _colorScaleLocation; + + static ProgramObject _shadowMapHeightfieldProgram; + static int _shadowMapHeightScaleLocation; + static int _shadowMapColorScaleLocation; + + static ProgramObject _cascadedShadowMapHeightfieldProgram; + static int _cascadedShadowMapHeightScaleLocation; + static int _cascadedShadowMapColorScaleLocation; + static int _shadowDistancesLocation; + + static ProgramObject _heightfieldCursorProgram; }; /// Base class for spanner renderers; provides clipping. diff --git a/interface/src/PaymentManager.cpp b/interface/src/PaymentManager.cpp new file mode 100644 index 0000000000..a0c05f34b3 --- /dev/null +++ b/interface/src/PaymentManager.cpp @@ -0,0 +1,64 @@ +// +// PaymentManager.cpp +// interface/src +// +// Created by Stephen Birarda on 2014-07-30. +// 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 + +#include +#include +#include + +#include "SignedWalletTransaction.h" + +#include "PaymentManager.h" + +PaymentManager& PaymentManager::getInstance() { + static PaymentManager sharedInstance; + return sharedInstance; +} + +void PaymentManager::sendSignedPayment(qint64 satoshiAmount, const QUuid& nodeUUID, const QUuid& destinationWalletUUID) { + + QByteArray walletPrivateKeyByteArray = Menu::getInstance()->getWalletPrivateKey(); + + if (!walletPrivateKeyByteArray.isEmpty()) { + // setup a signed wallet transaction + const qint64 DEFAULT_TRANSACTION_EXPIRY_SECONDS = 60; + qint64 currentTimestamp = QDateTime::currentDateTimeUtc().toTime_t(); + SignedWalletTransaction newTransaction(destinationWalletUUID, satoshiAmount, + currentTimestamp, DEFAULT_TRANSACTION_EXPIRY_SECONDS); + + // send the signed transaction to the redeeming node + QByteArray transactionByteArray = byteArrayWithPopulatedHeader(PacketTypeSignedTransactionPayment); + + // append the binary message and the signed message digest + transactionByteArray.append(newTransaction.binaryMessage()); + QByteArray signedMessageDigest = newTransaction.signedMessageDigest(); + + if (!signedMessageDigest.isEmpty()) { + transactionByteArray.append(signedMessageDigest); + + qDebug() << "Paying" << satoshiAmount << "satoshis to" << destinationWalletUUID << "via" << nodeUUID; + + // use the NodeList to send that to the right node + NodeList* nodeList = NodeList::getInstance(); + nodeList->writeDatagram(transactionByteArray, nodeList->nodeWithUUID(nodeUUID)); + } else { + qDebug() << "Payment of" << satoshiAmount << "satoshis to" << destinationWalletUUID << "via" << nodeUUID << + "cannot be sent because there was an error signing the transaction."; + } + + } else { + qDebug() << "Payment of" << satoshiAmount << "satoshis to" << destinationWalletUUID << "via" << nodeUUID << + "cannot be sent because there is no stored wallet private key."; + } +} \ No newline at end of file diff --git a/interface/src/PaymentManager.h b/interface/src/PaymentManager.h new file mode 100644 index 0000000000..67419a39a4 --- /dev/null +++ b/interface/src/PaymentManager.h @@ -0,0 +1,25 @@ +// +// PaymentManager.h +// interface/src +// +// Created by Stephen Birarda on 2014-07-30. +// 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_PaymentManager_h +#define hifi_PaymentManager_h + +#include + +class PaymentManager : public QObject { + Q_OBJECT +public: + static PaymentManager& getInstance(); +public slots: + void sendSignedPayment(qint64 satoshiAmount, const QUuid& nodeUUID, const QUuid& destinationWalletUUID); +}; + +#endif // hifi_PaymentManager_h \ No newline at end of file diff --git a/interface/src/Recorder.cpp b/interface/src/Recorder.cpp new file mode 100644 index 0000000000..30b14ac589 --- /dev/null +++ b/interface/src/Recorder.cpp @@ -0,0 +1,321 @@ +// +// Recorder.cpp +// +// +// Created by Clement on 8/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 +// + +#include + +#include + +#include "Recorder.h" + +void RecordingFrame::setBlendshapeCoefficients(QVector blendshapeCoefficients) { + _blendshapeCoefficients = blendshapeCoefficients; +} + +void RecordingFrame::setJointRotations(QVector jointRotations) { + _jointRotations = jointRotations; +} + +void RecordingFrame::setTranslation(glm::vec3 translation) { + _translation = translation; +} + +void RecordingFrame::setRotation(glm::quat rotation) { + _rotation = rotation; +} + +void RecordingFrame::setScale(float scale) { + _scale = scale; +} + +void RecordingFrame::setHeadRotation(glm::quat headRotation) { + _headRotation = headRotation; +} + +void RecordingFrame::setLeanSideways(float leanSideways) { + _leanSideways = leanSideways; +} + +void RecordingFrame::setLeanForward(float leanForward) { + _leanForward = leanForward; +} + +Recording::Recording() : _audio(NULL) { +} + +Recording::~Recording() { + delete _audio; +} + +void Recording::addFrame(int timestamp, RecordingFrame &frame) { + _timestamps << timestamp; + _frames << frame; +} + +void Recording::addAudioPacket(QByteArray byteArray) { + if (!_audio) { + _audio = new Sound(byteArray); + } + _audio->append(byteArray); +} + +void Recording::clear() { + _timestamps.clear(); + _frames.clear(); + delete _audio; + _audio = NULL; +} + +Recorder::Recorder(AvatarData* avatar) : + _recording(new Recording()), + _avatar(avatar) +{ + _timer.invalidate(); +} + +bool Recorder::isRecording() const { + return _timer.isValid(); +} + +qint64 Recorder::elapsed() const { + if (isRecording()) { + return _timer.elapsed(); + } else { + return 0; + } +} + +void Recorder::startRecording() { + qDebug() << "Recorder::startRecording()"; + _recording->clear(); + _timer.start(); + + RecordingFrame frame; + frame.setBlendshapeCoefficients(_avatar->getHeadData()->getBlendshapeCoefficients()); + frame.setJointRotations(_avatar->getJointRotations()); + frame.setTranslation(_avatar->getPosition()); + frame.setRotation(_avatar->getOrientation()); + frame.setScale(_avatar->getTargetScale()); + + const HeadData* head = _avatar->getHeadData(); + glm::quat rotation = glm::quat(glm::radians(glm::vec3(head->getFinalPitch(), + head->getFinalYaw(), + head->getFinalRoll()))); + frame.setHeadRotation(rotation); + frame.setLeanForward(_avatar->getHeadData()->getLeanForward()); + frame.setLeanSideways(_avatar->getHeadData()->getLeanSideways()); + + _recording->addFrame(0, frame); +} + +void Recorder::stopRecording() { + qDebug() << "Recorder::stopRecording()"; + _timer.invalidate(); + + qDebug().nospace() << "Recorded " << _recording->getFrameNumber() << " during " << _recording->getLength() << " msec (" << _recording->getFrameNumber() / (_recording->getLength() / 1000.0f) << " fps)"; +} + +void Recorder::saveToFile(QString file) { + if (_recording->isEmpty()) { + qDebug() << "Cannot save recording to file, recording is empty."; + } + + writeRecordingToFile(_recording, file); +} + +void Recorder::record() { + if (isRecording()) { + const RecordingFrame& referenceFrame = _recording->getFrame(0); + RecordingFrame frame; + frame.setBlendshapeCoefficients(_avatar->getHeadData()->getBlendshapeCoefficients()); + frame.setJointRotations(_avatar->getJointRotations()); + frame.setTranslation(_avatar->getPosition() - referenceFrame.getTranslation()); + frame.setRotation(glm::inverse(referenceFrame.getRotation()) * _avatar->getOrientation()); + frame.setScale(_avatar->getTargetScale() / referenceFrame.getScale()); + + + const HeadData* head = _avatar->getHeadData(); + glm::quat rotation = glm::quat(glm::radians(glm::vec3(head->getFinalPitch(), + head->getFinalYaw(), + head->getFinalRoll()))); + frame.setHeadRotation(rotation); + frame.setLeanForward(_avatar->getHeadData()->getLeanForward()); + frame.setLeanSideways(_avatar->getHeadData()->getLeanSideways()); + + _recording->addFrame(_timer.elapsed(), frame); + } +} + +void Recorder::record(char* samples, int size) { + QByteArray byteArray(samples, size); + _recording->addAudioPacket(byteArray); +} + + +Player::Player(AvatarData* avatar) : + _recording(new Recording()), + _avatar(avatar), + _audioThread(NULL) +{ + _timer.invalidate(); + _options.setLoop(false); + _options.setVolume(1.0f); +} + +bool Player::isPlaying() const { + return _timer.isValid(); +} + +qint64 Player::elapsed() const { + if (isPlaying()) { + return _timer.elapsed(); + } else { + return 0; + } +} + +glm::quat Player::getHeadRotation() { + if (!computeCurrentFrame()) { + qWarning() << "Incorrect use of Player::getHeadRotation()"; + return glm::quat(); + } + + if (_currentFrame == 0) { + return _recording->getFrame(_currentFrame).getHeadRotation(); + } + return _recording->getFrame(0).getHeadRotation() * + _recording->getFrame(_currentFrame).getHeadRotation(); +} + +float Player::getLeanSideways() { + if (!computeCurrentFrame()) { + qWarning() << "Incorrect use of Player::getLeanSideways()"; + return 0.0f; + } + + return _recording->getFrame(_currentFrame).getLeanSideways(); +} + +float Player::getLeanForward() { + if (!computeCurrentFrame()) { + qWarning() << "Incorrect use of Player::getLeanForward()"; + return 0.0f; + } + + return _recording->getFrame(_currentFrame).getLeanForward(); +} + +void Player::startPlaying() { + if (_recording && _recording->getFrameNumber() > 0) { + qDebug() << "Recorder::startPlaying()"; + _currentFrame = 0; + + // Setup audio thread + _audioThread = new QThread(); + _options.setPosition(_avatar->getPosition()); + _options.setOrientation(_avatar->getOrientation()); + _injector.reset(new AudioInjector(_recording->getAudio(), _options), &QObject::deleteLater); + _injector->moveToThread(_audioThread); + _audioThread->start(); + QMetaObject::invokeMethod(_injector.data(), "injectAudio", Qt::QueuedConnection); + + _timer.start(); + } +} + +void Player::stopPlaying() { + if (!isPlaying()) { + return; + } + + _timer.invalidate(); + + _avatar->clearJointsData(); + + // Cleanup audio thread + _injector->stop(); + _injector.clear(); + _audioThread->exit(); + _audioThread->deleteLater(); + qDebug() << "Recorder::stopPlaying()"; +} + +void Player::loadFromFile(QString file) { + if (_recording) { + _recording->clear(); + } else { + _recording = RecordingPointer(new Recording()); + } + readRecordingFromFile(_recording, file); +} + +void Player::loadRecording(RecordingPointer recording) { + _recording = recording; +} + +void Player::play() { + computeCurrentFrame(); + if (_currentFrame < 0 || _currentFrame >= _recording->getFrameNumber() - 1) { + // If it's the end of the recording, stop playing + stopPlaying(); + return; + } + + if (_currentFrame == 0) { + _avatar->setPosition(_recording->getFrame(_currentFrame).getTranslation()); + _avatar->setOrientation(_recording->getFrame(_currentFrame).getRotation()); + _avatar->setTargetScale(_recording->getFrame(_currentFrame).getScale()); + _avatar->setJointRotations(_recording->getFrame(_currentFrame).getJointRotations()); + HeadData* head = const_cast(_avatar->getHeadData()); + head->setBlendshapeCoefficients(_recording->getFrame(_currentFrame).getBlendshapeCoefficients()); + } else { + _avatar->setPosition(_recording->getFrame(0).getTranslation() + + _recording->getFrame(_currentFrame).getTranslation()); + _avatar->setOrientation(_recording->getFrame(0).getRotation() * + _recording->getFrame(_currentFrame).getRotation()); + _avatar->setTargetScale(_recording->getFrame(0).getScale() * + _recording->getFrame(_currentFrame).getScale()); + _avatar->setJointRotations(_recording->getFrame(_currentFrame).getJointRotations()); + HeadData* head = const_cast(_avatar->getHeadData()); + head->setBlendshapeCoefficients(_recording->getFrame(_currentFrame).getBlendshapeCoefficients()); + } + + _options.setPosition(_avatar->getPosition()); + _options.setOrientation(_avatar->getOrientation()); + _injector->setOptions(_options); +} + +bool Player::computeCurrentFrame() { + if (!isPlaying()) { + _currentFrame = -1; + return false; + } + if (_currentFrame < 0) { + _currentFrame = 0; + } + + while (_currentFrame < _recording->getFrameNumber() - 1 && + _recording->getFrameTimestamp(_currentFrame) < _timer.elapsed()) { + ++_currentFrame; + } + + return true; +} + +void writeRecordingToFile(RecordingPointer recording, QString file) { + // TODO + qDebug() << "Writing recording to " << file; +} + +RecordingPointer readRecordingFromFile(RecordingPointer recording, QString file) { + // TODO + qDebug() << "Reading recording from " << file; + return recording; +} \ No newline at end of file diff --git a/interface/src/Recorder.h b/interface/src/Recorder.h new file mode 100644 index 0000000000..9f7eb66ec6 --- /dev/null +++ b/interface/src/Recorder.h @@ -0,0 +1,170 @@ +// +// Recorder.h +// +// +// Created by Clement on 8/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 +// + +#ifndef hifi_Recorder_h +#define hifi_Recorder_h + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +class Recorder; +class Recording; +class Player; + +typedef QSharedPointer RecordingPointer; +typedef QSharedPointer RecorderPointer; +typedef QWeakPointer WeakRecorderPointer; +typedef QSharedPointer PlayerPointer; +typedef QWeakPointer WeakPlayerPointer; + +/// Stores the different values associated to one recording frame +class RecordingFrame { +public: + QVector getBlendshapeCoefficients() const { return _blendshapeCoefficients; } + QVector getJointRotations() const { return _jointRotations; } + glm::vec3 getTranslation() const { return _translation; } + glm::quat getRotation() const { return _rotation; } + float getScale() const { return _scale; } + glm::quat getHeadRotation() const { return _headRotation; } + float getLeanSideways() const { return _leanSideways; } + float getLeanForward() const { return _leanForward; } + +protected: + void setBlendshapeCoefficients(QVector blendshapeCoefficients); + void setJointRotations(QVector jointRotations); + void setTranslation(glm::vec3 translation); + void setRotation(glm::quat rotation); + void setScale(float scale); + void setHeadRotation(glm::quat headRotation); + void setLeanSideways(float leanSideways); + void setLeanForward(float leanForward); + +private: + QVector _blendshapeCoefficients; + QVector _jointRotations; + glm::vec3 _translation; + glm::quat _rotation; + float _scale; + glm::quat _headRotation; + float _leanSideways; + float _leanForward; + + friend class Recorder; + friend void writeRecordingToFile(RecordingPointer recording, QString file); + friend RecordingPointer readRecordingFromFile(RecordingPointer recording, QString file); +}; + +/// Stores a recording +class Recording { +public: + Recording(); + ~Recording(); + + bool isEmpty() const { return _timestamps.isEmpty(); } + int getLength() const { return _timestamps.last(); } // in ms + + int getFrameNumber() const { return _frames.size(); } + qint32 getFrameTimestamp(int i) const { return _timestamps[i]; } + const RecordingFrame& getFrame(int i) const { return _frames[i]; } + Sound* getAudio() const { return _audio; } + +protected: + void addFrame(int timestamp, RecordingFrame& frame); + void addAudioPacket(QByteArray byteArray); + void clear(); + +private: + QVector _timestamps; + QVector _frames; + + Sound* _audio; + + friend class Recorder; + friend class Player; + friend void writeRecordingToFile(RecordingPointer recording, QString file); + friend RecordingPointer readRecordingFromFile(RecordingPointer recording, QString file); +}; + +/// Records a recording +class Recorder { +public: + Recorder(AvatarData* avatar); + + bool isRecording() const; + qint64 elapsed() const; + + RecordingPointer getRecording() const { return _recording; } + +public slots: + void startRecording(); + void stopRecording(); + void saveToFile(QString file); + void record(); + void record(char* samples, int size); + +private: + QElapsedTimer _timer; + RecordingPointer _recording; + + AvatarData* _avatar; +}; + +/// Plays back a recording +class Player { +public: + Player(AvatarData* avatar); + + bool isPlaying() const; + qint64 elapsed() const; + + // Those should only be called if isPlaying() returns true + glm::quat getHeadRotation(); + float getLeanSideways(); + float getLeanForward(); + + +public slots: + void startPlaying(); + void stopPlaying(); + void loadFromFile(QString file); + void loadRecording(RecordingPointer recording); + void play(); + +private: + bool computeCurrentFrame(); + + QElapsedTimer _timer; + RecordingPointer _recording; + int _currentFrame; + + QSharedPointer _injector; + AudioInjectorOptions _options; + + AvatarData* _avatar; + QThread* _audioThread; +}; + +void writeRecordingToFile(RecordingPointer recording, QString file); +RecordingPointer readRecordingFromFile(RecordingPointer recording, QString file); + +#endif // hifi_Recorder_h \ No newline at end of file diff --git a/interface/src/SignedWalletTransaction.cpp b/interface/src/SignedWalletTransaction.cpp index d29207e4f5..8a0d4e6fdb 100644 --- a/interface/src/SignedWalletTransaction.cpp +++ b/interface/src/SignedWalletTransaction.cpp @@ -32,7 +32,7 @@ SignedWalletTransaction::SignedWalletTransaction(const QUuid& destinationUUID, q } -QByteArray SignedWalletTransaction::hexMessage() { +QByteArray SignedWalletTransaction::binaryMessage() { // build the message using the components of this transaction // UUID, source UUID, destination UUID, message timestamp, expiry delta, amount @@ -49,7 +49,11 @@ QByteArray SignedWalletTransaction::hexMessage() { messageBinary.append(reinterpret_cast(&_amount), sizeof(_amount)); - return messageBinary.toHex(); + return messageBinary; +} + +QByteArray SignedWalletTransaction::hexMessage() { + return binaryMessage().toHex(); } QByteArray SignedWalletTransaction::messageDigest() { diff --git a/interface/src/SignedWalletTransaction.h b/interface/src/SignedWalletTransaction.h index 3b13f73335..6bc66a535e 100644 --- a/interface/src/SignedWalletTransaction.h +++ b/interface/src/SignedWalletTransaction.h @@ -19,6 +19,7 @@ class SignedWalletTransaction : public WalletTransaction { public: SignedWalletTransaction(const QUuid& destinationUUID, qint64 amount, qint64 messageTimestamp, qint64 expiryDelta); + QByteArray binaryMessage(); QByteArray hexMessage(); QByteArray messageDigest(); QByteArray signedMessageDigest(); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 27c4e39062..41912afd09 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -27,7 +27,9 @@ #include "Hand.h" #include "Head.h" #include "Menu.h" +#include "ModelReferential.h" #include "Physics.h" +#include "Recorder.h" #include "world.h" #include "devices/OculusManager.h" #include "renderer/TextureCache.h" @@ -102,6 +104,31 @@ float Avatar::getLODDistance() const { void Avatar::simulate(float deltaTime) { PerformanceTimer perfTimer("simulate"); + + // update the avatar's position according to its referential + if (_referential) { + if (_referential->hasExtraData()) { + ModelTree* tree = Application::getInstance()->getModels()->getTree(); + switch (_referential->type()) { + case Referential::MODEL: + _referential = new ModelReferential(_referential, + tree, + this); + break; + case Referential::JOINT: + _referential = new JointReferential(_referential, + tree, + this); + break; + default: + qDebug() << "[WARNING] Avatar::simulate(): Unknown referential type."; + break; + } + } + + _referential->update(); + } + if (_scale != _targetScale) { setScale(_targetScale); } @@ -218,6 +245,9 @@ static TextRenderer* textRenderer(TextRendererType type) { } void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { + if (_referential) { + _referential->update(); + } if (glm::distance(Application::getInstance()->getAvatar()->getPosition(), _position) < 10.0f) { @@ -268,7 +298,7 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { float boundingRadius = getBillboardSize(); ViewFrustum* frustum = (renderMode == Avatar::SHADOW_RENDER_MODE) ? Application::getInstance()->getShadowViewFrustum() : Application::getInstance()->getViewFrustum(); - if (frustum->sphereInFrustum(_position, boundingRadius) == ViewFrustum::OUTSIDE) { + if (frustum->sphereInFrustum(getPosition(), boundingRadius) == ViewFrustum::OUTSIDE) { return; } @@ -696,6 +726,17 @@ bool Avatar::findCollisions(const QVector& shapes, CollisionList& return collided; } +QVector Avatar::getJointRotations() const { + if (QThread::currentThread() != thread()) { + return AvatarData::getJointRotations(); + } + QVector jointRotations(_skeletonModel.getJointStateCount()); + for (int i = 0; i < _skeletonModel.getJointStateCount(); ++i) { + _skeletonModel.getJointState(i, jointRotations[i]); + } + return jointRotations; +} + glm::quat Avatar::getJointRotation(int index) const { if (QThread::currentThread() != thread()) { return AvatarData::getJointRotation(index); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 6b7408309d..c8ecb23913 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -23,6 +23,7 @@ #include "Hand.h" #include "Head.h" #include "InterfaceConfig.h" +#include "Recorder.h" #include "SkeletonModel.h" #include "world.h" @@ -121,6 +122,7 @@ public: virtual bool isMyAvatar() { return false; } + virtual QVector getJointRotations() const; virtual glm::quat getJointRotation(int index) const; virtual int getJointIndex(const QString& name) const; virtual QStringList getJointNames() const; @@ -209,7 +211,7 @@ protected: virtual void renderAttachments(RenderMode renderMode); virtual void updateJointMappings(); - + private: bool _initialized; @@ -220,8 +222,6 @@ private: void renderBillboard(); float getBillboardSize() const; - - }; #endif // hifi_Avatar_h diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 0db12276ad..5cc8812b40 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -41,7 +41,7 @@ void AvatarManager::init() { } void AvatarManager::updateOtherAvatars(float deltaTime) { - if (_avatarHash.size() < 2) { + if (_avatarHash.size() < 2 && _avatarFades.isEmpty()) { return; } bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index d3d1e74fc8..9bf8158ba7 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -64,10 +64,18 @@ void Head::reset() { void Head::simulate(float deltaTime, bool isMine, bool billboard) { // Update audio trailing average for rendering facial animations if (isMine) { - FaceTracker* faceTracker = Application::getInstance()->getActiveFaceTracker(); - if ((_isFaceshiftConnected = faceTracker)) { - _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); - _isFaceshiftConnected = true; + MyAvatar* myAvatar = static_cast(_owningAvatar); + + // Only use face trackers when not playing back a recording. + if (!myAvatar->isPlaying()) { + FaceTracker* faceTracker = Application::getInstance()->getActiveFaceTracker(); + if ((_isFaceshiftConnected = faceTracker)) { + _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); + _isFaceshiftConnected = true; + } else if (Application::getInstance()->getDDE()->isActive()) { + faceTracker = Application::getInstance()->getDDE(); + _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); + } } } @@ -160,9 +168,6 @@ void Head::simulate(float deltaTime, bool isMine, bool billboard) { } _eyePosition = calculateAverageEyePosition(); - float velocityFilter = glm::clamp(1.0f - glm::length(_filteredEyePosition - _eyePosition), 0.0f, 1.0f); - _filteredEyePosition = velocityFilter * _filteredEyePosition + (1.0f - velocityFilter) * _eyePosition; - } void Head::relaxLean(float deltaTime) { diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index 6d1e82b97f..0409ee3295 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -48,8 +48,6 @@ public: void setAverageLoudness(float averageLoudness) { _averageLoudness = averageLoudness; } void setReturnToCenter (bool returnHeadToCenter) { _returnHeadToCenter = returnHeadToCenter; } void setRenderLookatVectors(bool onOff) { _renderLookatVectors = onOff; } - void setLeanSideways(float leanSideways) { _leanSideways = leanSideways; } - void setLeanForward(float leanForward) { _leanForward = leanForward; } /// \return orientationBase+Delta glm::quat getFinalOrientationInLocalFrame() const; @@ -57,7 +55,6 @@ public: /// \return orientationBody * (orientationBase+Delta) glm::quat getFinalOrientationInWorldFrame() const; - /// \return orientationBody * orientationBasePitch glm::quat getCameraOrientation () const; @@ -71,8 +68,6 @@ public: glm::vec3 getRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getUpDirection() const { return getOrientation() * IDENTITY_UP; } glm::vec3 getFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } - float getLeanSideways() const { return _leanSideways; } - float getLeanForward() const { return _leanForward; } float getFinalLeanSideways() const { return _leanSideways + _deltaLeanSideways; } float getFinalLeanForward() const { return _leanForward + _deltaLeanForward; } @@ -88,7 +83,6 @@ public: const bool getReturnToCenter() const { return _returnHeadToCenter; } // Do you want head to try to return to center (depends on interface detected) float getAverageLoudness() const { return _averageLoudness; } - glm::vec3 getFilteredEyePosition() const { return _filteredEyePosition; } /// \return the point about which scaling occurs. glm::vec3 getScalePivot() const; @@ -121,7 +115,6 @@ private: glm::vec3 _leftEyePosition; glm::vec3 _rightEyePosition; glm::vec3 _eyePosition; - glm::vec3 _filteredEyePosition; // velocity filtered world space eye position float _scale; float _lastLoudness; diff --git a/interface/src/avatar/ModelReferential.cpp b/interface/src/avatar/ModelReferential.cpp new file mode 100644 index 0000000000..d38abae70a --- /dev/null +++ b/interface/src/avatar/ModelReferential.cpp @@ -0,0 +1,189 @@ +// +// ModelReferential.cpp +// +// +// Created by Clement on 7/30/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 "ModelTree.h" +#include "../renderer/Model.h" + +#include "ModelReferential.h" + +ModelReferential::ModelReferential(Referential* referential, ModelTree* tree, AvatarData* avatar) : + Referential(MODEL, avatar), + _tree(tree) { + _translation = referential->getTranslation(); + _rotation = referential->getRotation(); + _scale = referential->getScale(); + unpackExtraData(reinterpret_cast(referential->getExtraData().data()), + referential->getExtraData().size()); + + if (!isValid()) { + qDebug() << "ModelReferential::copyConstructor(): Not Valid"; + return; + } + + const ModelItem* item = _tree->findModelByID(_modelID); + if (item != NULL) { + _refScale = item->getRadius(); + _refRotation = item->getModelRotation(); + _refPosition = item->getPosition() * (float)TREE_SCALE; + update(); + } +} + +ModelReferential::ModelReferential(uint32_t modelID, ModelTree* tree, AvatarData* avatar) : + Referential(MODEL, avatar), + _modelID(modelID), + _tree(tree) +{ + const ModelItem* item = _tree->findModelByID(_modelID); + if (!isValid() || item == NULL) { + qDebug() << "ModelReferential::constructor(): Not Valid"; + _isValid = false; + return; + } + + _refScale = item->getRadius(); + _refRotation = item->getModelRotation(); + _refPosition = item->getPosition() * (float)TREE_SCALE; + + glm::quat refInvRot = glm::inverse(_refRotation); + _scale = _avatar->getTargetScale() / _refScale; + _rotation = refInvRot * _avatar->getOrientation(); + _translation = refInvRot * (avatar->getPosition() - _refPosition) / _refScale; +} + +void ModelReferential::update() { + const ModelItem* item = _tree->findModelByID(_modelID); + if (!isValid() || item == NULL || _avatar == NULL) { + return; + } + + bool somethingChanged = false; + if (item->getRadius() != _refScale) { + _refScale = item->getRadius(); + _avatar->setTargetScale(_refScale * _scale, true); + somethingChanged = true; + } + if (item->getModelRotation() != _refRotation) { + _refRotation = item->getModelRotation(); + _avatar->setOrientation(_refRotation * _rotation, true); + somethingChanged = true; + } + if (item->getPosition() != _refPosition || somethingChanged) { + _refPosition = item->getPosition(); + _avatar->setPosition(_refPosition * (float)TREE_SCALE + _refRotation * (_translation * _refScale), true); + } +} + +int ModelReferential::packExtraData(unsigned char* destinationBuffer) const { + memcpy(destinationBuffer, &_modelID, sizeof(_modelID)); + return sizeof(_modelID); +} + +int ModelReferential::unpackExtraData(const unsigned char *sourceBuffer, int size) { + memcpy(&_modelID, sourceBuffer, sizeof(_modelID)); + return sizeof(_modelID); +} + +JointReferential::JointReferential(Referential* referential, ModelTree* tree, AvatarData* avatar) : + ModelReferential(referential, tree, avatar) +{ + _type = JOINT; + if (!isValid()) { + qDebug() << "JointReferential::copyConstructor(): Not Valid"; + return; + } + + const ModelItem* item = _tree->findModelByID(_modelID); + const Model* model = getModel(item); + if (!isValid() || model == NULL || _jointIndex >= model->getJointStateCount()) { + _refScale = item->getRadius(); + model->getJointRotationInWorldFrame(_jointIndex, _refRotation); + model->getJointPositionInWorldFrame(_jointIndex, _refPosition); + } + update(); +} + +JointReferential::JointReferential(uint32_t jointIndex, uint32_t modelID, ModelTree* tree, AvatarData* avatar) : + ModelReferential(modelID, tree, avatar), + _jointIndex(jointIndex) +{ + _type = JOINT; + const ModelItem* item = _tree->findModelByID(_modelID); + const Model* model = getModel(item); + if (!isValid() || model == NULL || _jointIndex >= model->getJointStateCount()) { + qDebug() << "JointReferential::constructor(): Not Valid"; + _isValid = false; + return; + } + + _refScale = item->getRadius(); + model->getJointRotationInWorldFrame(_jointIndex, _refRotation); + model->getJointPositionInWorldFrame(_jointIndex, _refPosition); + + glm::quat refInvRot = glm::inverse(_refRotation); + _scale = _avatar->getTargetScale() / _refScale; + _rotation = refInvRot * _avatar->getOrientation(); + _translation = refInvRot * (avatar->getPosition() - _refPosition) / _refScale; +} + +void JointReferential::update() { + const ModelItem* item = _tree->findModelByID(_modelID); + const Model* model = getModel(item); + if (!isValid() || model == NULL || _jointIndex >= model->getJointStateCount()) { + return; + } + + bool somethingChanged = false; + if (item->getRadius() != _refScale) { + _refScale = item->getRadius(); + _avatar->setTargetScale(_refScale * _scale, true); + somethingChanged = true; + } + if (item->getModelRotation() != _refRotation) { + model->getJointRotationInWorldFrame(_jointIndex, _refRotation); + _avatar->setOrientation(_refRotation * _rotation, true); + somethingChanged = true; + } + if (item->getPosition() != _refPosition || somethingChanged) { + model->getJointPositionInWorldFrame(_jointIndex, _refPosition); + _avatar->setPosition(_refPosition + _refRotation * (_translation * _refScale), true); + } +} + +const Model* JointReferential::getModel(const ModelItem* item) { + ModelItemFBXService* fbxService = _tree->getFBXService(); + if (item != NULL && fbxService != NULL) { + return fbxService->getModelForModelItem(*item); + } + return NULL; +} + +int JointReferential::packExtraData(unsigned char* destinationBuffer) const { + unsigned char* startPosition = destinationBuffer; + destinationBuffer += ModelReferential::packExtraData(destinationBuffer); + + memcpy(destinationBuffer, &_jointIndex, sizeof(_jointIndex)); + destinationBuffer += sizeof(_jointIndex); + + return destinationBuffer - startPosition; +} + +int JointReferential::unpackExtraData(const unsigned char *sourceBuffer, int size) { + const unsigned char* startPosition = sourceBuffer; + sourceBuffer += ModelReferential::unpackExtraData(sourceBuffer, size); + + memcpy(&_jointIndex, sourceBuffer, sizeof(_jointIndex)); + sourceBuffer += sizeof(_jointIndex); + + return sourceBuffer - startPosition; +} \ No newline at end of file diff --git a/interface/src/avatar/ModelReferential.h b/interface/src/avatar/ModelReferential.h new file mode 100644 index 0000000000..b3a718d728 --- /dev/null +++ b/interface/src/avatar/ModelReferential.h @@ -0,0 +1,48 @@ +// +// ModelReferential.h +// +// +// Created by Clement on 7/30/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_ModelReferential_h +#define hifi_ModelReferential_h + +#include + +class ModelTree; +class Model; + +class ModelReferential : public Referential { +public: + ModelReferential(Referential* ref, ModelTree* tree, AvatarData* avatar); + ModelReferential(uint32_t modelID, ModelTree* tree, AvatarData* avatar); + virtual void update(); + +protected: + virtual int packExtraData(unsigned char* destinationBuffer) const; + virtual int unpackExtraData(const unsigned char* sourceBuffer, int size); + + uint32_t _modelID; + ModelTree* _tree; +}; + +class JointReferential : public ModelReferential { +public: + JointReferential(Referential* ref, ModelTree* tree, AvatarData* avatar); + JointReferential(uint32_t jointIndex, uint32_t modelID, ModelTree* tree, AvatarData* avatar); + virtual void update(); + +protected: + const Model* getModel(const ModelItem* item); + virtual int packExtraData(unsigned char* destinationBuffer) const; + virtual int unpackExtraData(const unsigned char* sourceBuffer, int size); + + uint32_t _jointIndex; +}; + +#endif // hifi_ModelReferential_h \ No newline at end of file diff --git a/interface/src/avatar/MuscleConstraint.cpp b/interface/src/avatar/MuscleConstraint.cpp index f4ba7975d0..76f30fdbc4 100644 --- a/interface/src/avatar/MuscleConstraint.cpp +++ b/interface/src/avatar/MuscleConstraint.cpp @@ -16,14 +16,13 @@ const float DEFAULT_MUSCLE_STRENGTH = 0.5f * MAX_MUSCLE_STRENGTH; -MuscleConstraint::MuscleConstraint(VerletPoint* parent, VerletPoint* child) - : _rootPoint(parent), _childPoint(child), +MuscleConstraint::MuscleConstraint(VerletPoint* parent, VerletPoint* child) : _rootPoint(parent), _childPoint(child), _parentIndex(-1), _childndex(-1), _strength(DEFAULT_MUSCLE_STRENGTH) { _childOffset = child->_position - parent->_position; } float MuscleConstraint::enforce() { - _childPoint->_position = (1.0f - _strength) * _childPoint->_position + _strength * (_rootPoint->_position + _childOffset); + _childPoint->_position += _strength * (_rootPoint->_position + _childOffset - _childPoint->_position); return 0.0f; } diff --git a/interface/src/avatar/MuscleConstraint.h b/interface/src/avatar/MuscleConstraint.h index 882b351b80..b2387a33f0 100644 --- a/interface/src/avatar/MuscleConstraint.h +++ b/interface/src/avatar/MuscleConstraint.h @@ -17,7 +17,7 @@ // MuscleConstraint is a simple constraint that pushes the child toward an offset relative to the parent. // It does NOT push the parent. -const float MAX_MUSCLE_STRENGTH = 0.5f; +const float MAX_MUSCLE_STRENGTH = 0.75f; class MuscleConstraint : public Constraint { public: diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4d2d679956..e1d8274993 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -32,8 +32,10 @@ #include "Audio.h" #include "Environment.h" #include "Menu.h" +#include "ModelReferential.h" #include "MyAvatar.h" #include "Physics.h" +#include "Recorder.h" #include "devices/Faceshift.h" #include "devices/OculusManager.h" #include "ui/TextRenderer.h" @@ -82,15 +84,16 @@ MyAvatar::MyAvatar() : for (int i = 0; i < MAX_DRIVE_KEYS; i++) { _driveKeys[i] = 0.0f; } + _physicsSimulation.setEntity(&_skeletonModel); + _skeletonModel.setEnableShapes(true); - // The skeleton is both a PhysicsEntity and Ragdoll, so we add it to the simulation once for each type. - _physicsSimulation.addEntity(&_skeletonModel); - _physicsSimulation.addRagdoll(&_skeletonModel); + Ragdoll* ragdoll = _skeletonModel.buildRagdoll(); + _physicsSimulation.setRagdoll(ragdoll); } MyAvatar::~MyAvatar() { - _physicsSimulation.removeEntity(&_skeletonModel); - _physicsSimulation.removeRagdoll(&_skeletonModel); + _physicsSimulation.setRagdoll(NULL); + _physicsSimulation.setEntity(NULL); _lookAtTargetAvatar.clear(); } @@ -108,6 +111,10 @@ void MyAvatar::reset() { } void MyAvatar::update(float deltaTime) { + if (_referential) { + _referential->update(); + } + Head* head = getHead(); head->relaxLean(deltaTime); updateFromTrackers(deltaTime); @@ -130,6 +137,12 @@ void MyAvatar::update(float deltaTime) { void MyAvatar::simulate(float deltaTime) { PerformanceTimer perfTimer("simulate"); + + // Play back recording + if (_player && _player->isPlaying()) { + _player->play(); + } + if (_scale != _targetScale) { float scale = (1.0f - SMOOTHING_RATIO) * _scale + SMOOTHING_RATIO * _targetScale; setScale(scale); @@ -142,7 +155,7 @@ void MyAvatar::simulate(float deltaTime) { updateOrientation(deltaTime); updatePosition(deltaTime); } - + { PerformanceTimer perfTimer("hand"); // update avatar skeleton and simulate hand and head @@ -200,11 +213,21 @@ void MyAvatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("ragdoll"); - if (Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { - const int minError = 0.01f; - const float maxIterations = 10; - const quint64 maxUsec = 2000; + Ragdoll* ragdoll = _skeletonModel.getRagdoll(); + if (ragdoll && Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { + const float minError = 0.00001f; + const float maxIterations = 3; + const quint64 maxUsec = 4000; + _physicsSimulation.setTranslation(_position); _physicsSimulation.stepForward(deltaTime, minError, maxIterations, maxUsec); + + // harvest any displacement of the Ragdoll that is a result of collisions + glm::vec3 ragdollDisplacement = ragdoll->getAndClearAccumulatedMovement(); + const float MAX_RAGDOLL_DISPLACEMENT_2 = 1.0f; + float length2 = glm::length2(ragdollDisplacement); + if (length2 > EPSILON && length2 < MAX_RAGDOLL_DISPLACEMENT_2) { + setPosition(getPosition() + ragdollDisplacement); + } } else { _skeletonModel.moveShapesTowardJoints(1.0f); } @@ -230,14 +253,17 @@ void MyAvatar::simulate(float deltaTime) { } else { _trapDuration = 0.0f; } - /* TODO: Andrew to make this work if (_collisionGroups & COLLISION_GROUP_AVATARS) { PerformanceTimer perfTimer("avatars"); updateCollisionWithAvatars(deltaTime); } - */ } + // Record avatars movements. + if (_recorder && _recorder->isRecording()) { + _recorder->record(); + } + // consider updating our billboard maybeUpdateBillboard(); } @@ -246,11 +272,19 @@ void MyAvatar::simulate(float deltaTime) { void MyAvatar::updateFromTrackers(float deltaTime) { glm::vec3 estimatedPosition, estimatedRotation; - if (Application::getInstance()->getPrioVR()->hasHeadRotation()) { + if (isPlaying()) { + estimatedRotation = glm::degrees(safeEulerAngles(_player->getHeadRotation())); + } else if (Application::getInstance()->getPrioVR()->hasHeadRotation()) { estimatedRotation = glm::degrees(safeEulerAngles(Application::getInstance()->getPrioVR()->getHeadRotation())); estimatedRotation.x *= -1.0f; estimatedRotation.z *= -1.0f; + } else if (OculusManager::isConnected()) { + estimatedPosition = OculusManager::getRelativePosition(); + estimatedPosition.x *= -1.0f; + + const float OCULUS_LEAN_SCALE = 0.05f; + estimatedPosition /= OCULUS_LEAN_SCALE; } else { FaceTracker* tracker = Application::getInstance()->getActiveFaceTracker(); if (tracker) { @@ -282,7 +316,7 @@ void MyAvatar::updateFromTrackers(float deltaTime) { Head* head = getHead(); - if (OculusManager::isConnected()) { + if (OculusManager::isConnected() || isPlaying()) { head->setDeltaPitch(estimatedRotation.x); head->setDeltaYaw(estimatedRotation.y); } else { @@ -292,6 +326,11 @@ void MyAvatar::updateFromTrackers(float deltaTime) { } head->setDeltaRoll(estimatedRotation.z); + if (isPlaying()) { + head->setLeanSideways(_player->getLeanSideways()); + head->setLeanForward(_player->getLeanForward()); + return; + } // the priovr can give us exact lean if (Application::getInstance()->getPrioVR()->isActive()) { glm::vec3 eulers = glm::degrees(safeEulerAngles(Application::getInstance()->getPrioVR()->getTorsoRotation())); @@ -299,7 +338,6 @@ void MyAvatar::updateFromTrackers(float deltaTime) { 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); @@ -444,6 +482,71 @@ glm::vec3 MyAvatar::getRightPalmPosition() { return rightHandPosition; } +void MyAvatar::clearReferential() { + changeReferential(NULL); +} + +bool MyAvatar::setModelReferential(int id) { + ModelTree* tree = Application::getInstance()->getModels()->getTree(); + changeReferential(new ModelReferential(id, tree, this)); + if (_referential->isValid()) { + return true; + } else { + changeReferential(NULL); + return false; + } +} + +bool MyAvatar::setJointReferential(int id, int jointIndex) { + ModelTree* tree = Application::getInstance()->getModels()->getTree(); + changeReferential(new JointReferential(jointIndex, id, tree, this)); + if (!_referential->isValid()) { + return true; + } else { + changeReferential(NULL); + return false; + } +} + +bool MyAvatar::isRecording() const { + return _recorder && _recorder->isRecording(); +} + +RecorderPointer MyAvatar::startRecording() { + if (!_recorder) { + _recorder = RecorderPointer(new Recorder(this)); + } + _recorder->startRecording(); + return _recorder; +} + +void MyAvatar::stopRecording() { + if (_recorder) { + _recorder->stopRecording(); + } +} + +bool MyAvatar::isPlaying() const { + return _player && _player->isPlaying(); +} + +PlayerPointer MyAvatar::startPlaying() { + if (!_player) { + _player = PlayerPointer(new Player(this)); + } + if (_recorder) { + _player->loadRecording(_recorder->getRecording()); + _player->startPlaying(); + } + return _player; +} + +void MyAvatar::stopPlaying() { + if (_player) { + _player->stopPlaying(); + } +} + void MyAvatar::setLocalGravity(glm::vec3 gravity) { _motionBehaviors |= AVATAR_MOTION_OBEY_LOCAL_GRAVITY; // Environmental and Local gravities are incompatible. Since Local is being set here @@ -826,6 +929,14 @@ glm::vec3 MyAvatar::getUprightHeadPosition() const { const float JOINT_PRIORITY = 2.0f; +void MyAvatar::setJointRotations(QVector jointRotations) { + for (int i = 0; i < jointRotations.size(); ++i) { + if (i < _jointData.size()) { + _skeletonModel.setJointState(i, true, jointRotations[i], JOINT_PRIORITY + 1.0f); + } + } +} + void MyAvatar::setJointData(int index, const glm::quat& rotation) { Avatar::setJointData(index, rotation); if (QThread::currentThread() == thread()) { @@ -840,6 +951,15 @@ void MyAvatar::clearJointData(int index) { } } +void MyAvatar::clearJointsData() { + for (int i = 0; i < _jointData.size(); ++i) { + Avatar::clearJointData(i); + if (QThread::currentThread() == thread()) { + _skeletonModel.clearJointState(i); + } + } +} + void MyAvatar::setFaceModelURL(const QUrl& faceModelURL) { Avatar::setFaceModelURL(faceModelURL); _billboardValid = false; @@ -1520,8 +1640,6 @@ bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float return false; } -const float BODY_COLLISION_RESOLUTION_TIMESCALE = 0.5f; // seconds - void MyAvatar::updateCollisionWithAvatars(float deltaTime) { // Reset detector for nearest avatar _distanceToNearestAvatar = std::numeric_limits::max(); @@ -1532,41 +1650,51 @@ void MyAvatar::updateCollisionWithAvatars(float deltaTime) { } float myBoundingRadius = getBoundingRadius(); - const float BODY_COLLISION_RESOLUTION_FACTOR = glm::max(1.0f, deltaTime / BODY_COLLISION_RESOLUTION_TIMESCALE); - + // find nearest avatar + float nearestDistance2 = std::numeric_limits::max(); + Avatar* nearestAvatar = NULL; foreach (const AvatarSharedPointer& avatarPointer, avatars) { Avatar* avatar = static_cast(avatarPointer.data()); if (static_cast(this) == avatar) { // don't collide with ourselves continue; } - float distance = glm::length(_position - avatar->getPosition()); - if (_distanceToNearestAvatar > distance) { - _distanceToNearestAvatar = distance; + float distance2 = glm::distance2(_position, avatar->getPosition()); + if (nearestDistance2 > distance2) { + nearestDistance2 = distance2; + nearestAvatar = avatar; } - float theirBoundingRadius = avatar->getBoundingRadius(); - if (distance < myBoundingRadius + theirBoundingRadius) { - // collide our body against theirs - QVector myShapes; - _skeletonModel.getBodyShapes(myShapes); - QVector theirShapes; - avatar->getSkeletonModel().getBodyShapes(theirShapes); + } + _distanceToNearestAvatar = glm::sqrt(nearestDistance2); - CollisionInfo collision; - if (ShapeCollider::collideShapesCoarse(myShapes, theirShapes, collision)) { - float penetrationDepth = glm::length(collision._penetration); - if (penetrationDepth > myBoundingRadius) { - qDebug() << "WARNING: ignoring avatar-avatar penetration depth " << penetrationDepth; - } - else if (penetrationDepth > EPSILON) { - setPosition(getPosition() - BODY_COLLISION_RESOLUTION_FACTOR * collision._penetration); - _lastBodyPenetration += collision._penetration; - emit collisionWithAvatar(getSessionUUID(), avatar->getSessionUUID(), collision); - } + if (Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { + if (nearestAvatar != NULL) { + if (_distanceToNearestAvatar > myBoundingRadius + nearestAvatar->getBoundingRadius()) { + // they aren't close enough to put into the _physicsSimulation + // so we clear the pointer + nearestAvatar = NULL; + } + } + + foreach (const AvatarSharedPointer& avatarPointer, avatars) { + Avatar* avatar = static_cast(avatarPointer.data()); + if (static_cast(this) == avatar) { + // don't collide with ourselves + continue; + } + SkeletonModel* skeleton = &(avatar->getSkeletonModel()); + PhysicsSimulation* simulation = skeleton->getSimulation(); + if (avatar == nearestAvatar) { + if (simulation != &(_physicsSimulation)) { + skeleton->setEnableShapes(true); + _physicsSimulation.addEntity(skeleton); + _physicsSimulation.addRagdoll(skeleton->getRagdoll()); + } + } else if (simulation == &(_physicsSimulation)) { + _physicsSimulation.removeRagdoll(skeleton->getRagdoll()); + _physicsSimulation.removeEntity(skeleton); + skeleton->setEnableShapes(false); } - - // collide their hands against us - avatar->getHand()->collideAgainstAvatar(this, false); } } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 581044c522..2c1695a499 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -18,6 +18,8 @@ #include "Avatar.h" +class ModelItemID; + enum AvatarHandState { HAND_STATE_NULL = 0, @@ -110,8 +112,10 @@ public: void updateLookAtTargetAvatar(); void clearLookAtTargetAvatar(); + virtual void setJointRotations(QVector jointRotations); virtual void setJointData(int index, const glm::quat& rotation); virtual void clearJointData(int index); + virtual void clearJointsData(); virtual void setFaceModelURL(const QUrl& faceModelURL); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setAttachmentData(const QVector& attachmentData); @@ -149,6 +153,21 @@ public slots: glm::vec3 getLeftPalmPosition(); glm::vec3 getRightPalmPosition(); + void clearReferential(); + bool setModelReferential(int id); + bool setJointReferential(int id, int jointIndex); + + const RecorderPointer getRecorder() const { return _recorder; } + bool isRecording() const; + RecorderPointer startRecording(); + void stopRecording(); + + const PlayerPointer getPlayer() const { return _player; } + bool isPlaying() const; + PlayerPointer startPlaying(); + void stopPlaying(); + + signals: void transformChanged(); @@ -186,6 +205,9 @@ private: QList _animationHandles; PhysicsSimulation _physicsSimulation; + RecorderPointer _recorder; + PlayerPointer _player; + // private methods float computeDistanceToFloor(const glm::vec3& startPoint); void updateOrientation(float deltaTime); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index dc6a309e70..f9a73f2431 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -14,22 +14,25 @@ #include #include -#include -#include #include "Application.h" #include "Avatar.h" #include "Hand.h" #include "Menu.h" -#include "MuscleConstraint.h" #include "SkeletonModel.h" +#include "SkeletonRagdoll.h" SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : Model(parent), - Ragdoll(), _owningAvatar(owningAvatar), _boundingShape(), - _boundingShapeLocalOffset(0.0f) { + _boundingShapeLocalOffset(0.0f), + _ragdoll(NULL) { +} + +SkeletonModel::~SkeletonModel() { + delete _ragdoll; + _ragdoll = NULL; } void SkeletonModel::setJointStates(QVector states) { @@ -59,9 +62,15 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { Model::simulate(deltaTime, fullUpdate); - if (!(isActive() && _owningAvatar->isMyAvatar())) { + if (!isActive() || !_owningAvatar->isMyAvatar()) { return; // only simulate for own avatar } + + MyAvatar* myAvatar = static_cast(_owningAvatar); + if (myAvatar->isPlaying()) { + // Don't take inputs if playing back a recording. + return; + } const FBXGeometry& geometry = _geometry->getFBXGeometry(); PrioVR* prioVR = Application::getInstance()->getPrioVR(); @@ -155,9 +164,6 @@ void SkeletonModel::getBodyShapes(QVector& shapes) const { void SkeletonModel::renderIKConstraints() { renderJointConstraints(getRightHandJointIndex()); renderJointConstraints(getLeftHandJointIndex()); - //if (isActive() && _owningAvatar->isMyAvatar()) { - // renderRagdoll(); - //} } class IndexValue { @@ -432,6 +438,10 @@ bool SkeletonModel::getHeadPosition(glm::vec3& headPosition) const { } bool SkeletonModel::getNeckPosition(glm::vec3& neckPosition) const { + if (_owningAvatar->isMyAvatar() && + Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { + return isActive() && getVisibleJointPositionInWorldFrame(_geometry->getFBXGeometry().neckJointIndex, neckPosition); + } return isActive() && getJointPositionInWorldFrame(_geometry->getFBXGeometry().neckJointIndex, neckPosition); } @@ -445,11 +455,16 @@ bool SkeletonModel::getNeckParentRotationFromDefaultOrientation(glm::quat& neckP } int parentIndex = geometry.joints.at(geometry.neckJointIndex).parentIndex; glm::quat worldFrameRotation; - if (getJointRotationInWorldFrame(parentIndex, worldFrameRotation)) { - neckParentRotation = worldFrameRotation * _jointStates[parentIndex].getFBXJoint().inverseDefaultRotation; - return true; + bool success = false; + if (Menu::getInstance()->isOptionChecked(MenuOption::CollideAsRagdoll)) { + success = getVisibleJointRotationInWorldFrame(parentIndex, worldFrameRotation); + } else { + success = getJointRotationInWorldFrame(parentIndex, worldFrameRotation); } - return false; + if (success) { + neckParentRotation = worldFrameRotation * _jointStates[parentIndex].getFBXJoint().inverseDefaultRotation; + } + return success; } bool SkeletonModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { @@ -480,21 +495,27 @@ bool SkeletonModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& seco } void SkeletonModel::renderRagdoll() { + if (!_ragdoll) { + return; + } + const QVector& points = _ragdoll->getPoints(); const int BALL_SUBDIVISIONS = 6; glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); glPushMatrix(); Application::getInstance()->loadTranslatedViewMatrix(_translation); - int numPoints = _ragdollPoints.size(); + int numPoints = points.size(); float alpha = 0.3f; float radius1 = 0.008f; float radius2 = 0.01f; + glm::vec3 simulationTranslation = _ragdoll->getTranslationInSimulationFrame(); for (int i = 0; i < numPoints; ++i) { glPushMatrix(); - // draw each point as a yellow hexagon with black border - glm::vec3 position = _rotation * _ragdollPoints[i]._position; + // NOTE: ragdollPoints are in simulation-frame but we want them to be model-relative + glm::vec3 position = points[i]._position - simulationTranslation; glTranslatef(position.x, position.y, position.z); + // draw each point as a yellow hexagon with black border glColor4f(0.0f, 0.0f, 0.0f, alpha); glutSolidSphere(radius2, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); glColor4f(1.0f, 1.0f, 0.0f, alpha); @@ -506,108 +527,18 @@ void SkeletonModel::renderRagdoll() { glEnable(GL_LIGHTING); } -// virtual -void SkeletonModel::initRagdollPoints() { - clearRagdollConstraintsAndPoints(); - _muscleConstraints.clear(); - - // one point for each joint - int numJoints = _jointStates.size(); - _ragdollPoints.fill(VerletPoint(), numJoints); - for (int i = 0; i < numJoints; ++i) { - const JointState& state = _jointStates.at(i); - glm::vec3 position = state.getPosition(); - _ragdollPoints[i].initPosition(position); - } -} - -void SkeletonModel::buildRagdollConstraints() { - // NOTE: the length of DistanceConstraints is computed and locked in at this time - // so make sure the ragdoll positions are in a normal configuration before here. - const int numPoints = _ragdollPoints.size(); - assert(numPoints == _jointStates.size()); - - float minBone = FLT_MAX; - float maxBone = -FLT_MAX; - QMultiMap families; - for (int i = 0; i < numPoints; ++i) { - const JointState& state = _jointStates.at(i); - int parentIndex = state.getParentIndex(); - if (parentIndex == -1) { - FixedConstraint* anchor = new FixedConstraint(&(_ragdollPoints[i]), glm::vec3(0.0f)); - _ragdollConstraints.push_back(anchor); - } else { - DistanceConstraint* bone = new DistanceConstraint(&(_ragdollPoints[i]), &(_ragdollPoints[parentIndex])); - bone->setDistance(state.getDistanceToParent()); - _ragdollConstraints.push_back(bone); - families.insert(parentIndex, i); - } - float boneLength = glm::length(state.getPositionInParentFrame()); - if (boneLength > maxBone) { - maxBone = boneLength; - } else if (boneLength < minBone) { - minBone = boneLength; - } - } - // Joints that have multiple children effectively have rigid constraints between the children - // in the parent frame, so we add DistanceConstraints between children in the same family. - QMultiMap::iterator itr = families.begin(); - while (itr != families.end()) { - QList children = families.values(itr.key()); - int numChildren = children.size(); - if (numChildren > 1) { - for (int i = 1; i < numChildren; ++i) { - DistanceConstraint* bone = new DistanceConstraint(&(_ragdollPoints[children[i-1]]), &(_ragdollPoints[children[i]])); - _ragdollConstraints.push_back(bone); - } - if (numChildren > 2) { - DistanceConstraint* bone = new DistanceConstraint(&(_ragdollPoints[children[numChildren-1]]), &(_ragdollPoints[children[0]])); - _ragdollConstraints.push_back(bone); - } - } - ++itr; - } - - float MAX_STRENGTH = 0.3f; - float MIN_STRENGTH = 0.005f; - // each joint gets a MuscleConstraint to its parent - for (int i = 1; i < numPoints; ++i) { - const JointState& state = _jointStates.at(i); - int p = state.getParentIndex(); - if (p == -1) { - continue; - } - MuscleConstraint* constraint = new MuscleConstraint(&(_ragdollPoints[p]), &(_ragdollPoints[i])); - _ragdollConstraints.push_back(constraint); - _muscleConstraints.push_back(constraint); - - // Short joints are more susceptible to wiggle so we modulate the strength based on the joint's length: - // long = weak and short = strong. - constraint->setIndices(p, i); - float boneLength = glm::length(state.getPositionInParentFrame()); - - float strength = MIN_STRENGTH + (MAX_STRENGTH - MIN_STRENGTH) * (maxBone - boneLength) / (maxBone - minBone); - if (!families.contains(i)) { - // Although muscles only pull on the children not parents, nevertheless those joints that have - // parents AND children are more stable than joints at the end such as fingers. For such joints we - // bestow maximum strength which helps reduce wiggle. - strength = MAX_MUSCLE_STRENGTH; - } - constraint->setStrength(strength); - } - -} - void SkeletonModel::updateVisibleJointStates() { - if (_showTrueJointTransforms) { + if (_showTrueJointTransforms || !_ragdoll) { // no need to update visible transforms return; } + const QVector& ragdollPoints = _ragdoll->getPoints(); QVector points; points.reserve(_jointStates.size()); + glm::quat invRotation = glm::inverse(_rotation); for (int i = 0; i < _jointStates.size(); i++) { JointState& state = _jointStates[i]; - points.push_back(_ragdollPoints[i]._position); + points.push_back(ragdollPoints[i]._position); // get the parent state (this is the state that we want to rotate) int parentIndex = state.getParentIndex(); @@ -629,8 +560,9 @@ void SkeletonModel::updateVisibleJointStates() { // we're looking for the rotation that moves visible bone parallel to ragdoll bone // rotationBetween(jointTip - jointPivot, shapeTip - shapePivot) + // NOTE: points are in simulation-frame so rotate line segment into model-frame glm::quat delta = rotationBetween(state.getVisiblePosition() - extractTranslation(parentTransform), - points[i] - points[parentIndex]); + invRotation * (points[i] - points[parentIndex])); // apply parentState.mixVisibleRotationDelta(delta, 0.01f); @@ -640,10 +572,14 @@ void SkeletonModel::updateVisibleJointStates() { } } -// virtual -void SkeletonModel::stepRagdollForward(float deltaTime) { - Ragdoll::stepRagdollForward(deltaTime); - updateMuscles(); +SkeletonRagdoll* SkeletonModel::buildRagdoll() { + if (!_ragdoll) { + _ragdoll = new SkeletonRagdoll(this); + if (_enableShapes) { + buildShapes(); + } + } + return _ragdoll; } float DENSITY_OF_WATER = 1000.0f; // kg/m^3 @@ -661,10 +597,17 @@ void SkeletonModel::buildShapes() { return; } - initRagdollPoints(); + if (!_ragdoll) { + _ragdoll = new SkeletonRagdoll(this); + } + _ragdoll->initPoints(); + QVector& points = _ragdoll->getPoints(); + + float massScale = _ragdoll->getMassScale(); float uniformScale = extractUniformScale(_scale); const int numStates = _jointStates.size(); + float totalMass = 0.0f; for (int i = 0; i < numStates; i++) { JointState& state = _jointStates[i]; const FBXJoint& joint = state.getFBXJoint(); @@ -675,29 +618,39 @@ void SkeletonModel::buildShapes() { // this shape is forced to be a sphere type = Shape::SPHERE_SHAPE; } + if (radius < EPSILON) { + type = Shape::UNKNOWN_SHAPE; + } Shape* shape = NULL; int parentIndex = joint.parentIndex; if (type == Shape::SPHERE_SHAPE) { - shape = new VerletSphereShape(radius, &(_ragdollPoints[i])); + shape = new VerletSphereShape(radius, &(points[i])); shape->setEntity(this); - _ragdollPoints[i]._mass = glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume()); + float mass = massScale * glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume()); + points[i].setMass(mass); + totalMass += mass; } else if (type == Shape::CAPSULE_SHAPE) { assert(parentIndex != -1); - shape = new VerletCapsuleShape(radius, &(_ragdollPoints[parentIndex]), &(_ragdollPoints[i])); + shape = new VerletCapsuleShape(radius, &(points[parentIndex]), &(points[i])); shape->setEntity(this); - _ragdollPoints[i]._mass = glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume()); + float mass = massScale * glm::max(MIN_JOINT_MASS, DENSITY_OF_WATER * shape->getVolume()); + points[i].setMass(mass); + totalMass += mass; } if (parentIndex != -1) { // always disable collisions between joint and its parent - disableCollisions(i, parentIndex); - } else { - // give the base joint a very large mass since it doesn't actually move - // in the local-frame simulation (it defines the origin) - _ragdollPoints[i]._mass = VERY_BIG_MASS; - } + if (shape) { + disableCollisions(i, parentIndex); + } + } _shapes.push_back(shape); } + // set the mass of the root + if (numStates > 0) { + points[0].setMass(totalMass); + } + // This method moves the shapes to their default positions in Model frame. computeBoundingShape(geometry); @@ -705,16 +658,11 @@ void SkeletonModel::buildShapes() { // joints that are currently colliding. disableCurrentSelfCollisions(); - buildRagdollConstraints(); + _ragdoll->buildConstraints(); // ... then move shapes back to current joint positions - if (_ragdollPoints.size() == numStates) { - int numJoints = _jointStates.size(); - for (int i = 0; i < numJoints; ++i) { - _ragdollPoints[i].initPosition(_jointStates.at(i).getPosition()); - } - } - enforceRagdollConstraints(); + _ragdoll->slamPointPositions(); + _ragdoll->enforceConstraints(); } void SkeletonModel::moveShapesTowardJoints(float deltaTime) { @@ -722,8 +670,9 @@ void SkeletonModel::moveShapesTowardJoints(float deltaTime) { // unravel a skelton that has become tangled in its constraints. So let's keep this // around for a while just in case. const int numStates = _jointStates.size(); - assert(_jointStates.size() == _ragdollPoints.size()); - if (_ragdollPoints.size() != numStates) { + QVector& ragdollPoints = _ragdoll->getPoints(); + assert(_jointStates.size() == ragdollPoints.size()); + if (ragdollPoints.size() != numStates) { return; } @@ -732,45 +681,31 @@ void SkeletonModel::moveShapesTowardJoints(float deltaTime) { float fraction = glm::clamp(deltaTime / RAGDOLL_FOLLOWS_JOINTS_TIMESCALE, 0.0f, 1.0f); float oneMinusFraction = 1.0f - fraction; + glm::vec3 simulationTranslation = _ragdoll->getTranslationInSimulationFrame(); for (int i = 0; i < numStates; ++i) { - _ragdollPoints[i].initPosition(oneMinusFraction * _ragdollPoints[i]._position + - fraction * _jointStates.at(i).getPosition()); - } -} - -void SkeletonModel::updateMuscles() { - int numConstraints = _muscleConstraints.size(); - for (int i = 0; i < numConstraints; ++i) { - MuscleConstraint* constraint = _muscleConstraints[i]; - int j = constraint->getParentIndex(); - if (j == -1) { - continue; - } - int k = constraint->getChildIndex(); - if (k == -1) { - continue; - } - constraint->setChildOffset(_jointStates.at(k).getPosition() - _jointStates.at(j).getPosition()); + // ragdollPoints are in simulation-frame but jointStates are in model-frame + ragdollPoints[i].initPosition(oneMinusFraction * ragdollPoints[i]._position + + fraction * (simulationTranslation + _rotation * (_jointStates.at(i).getPosition()))); } } void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { // compute default joint transforms - int numJoints = geometry.joints.size(); - if (numJoints != _ragdollPoints.size()) { - return; - } + int numStates = _jointStates.size(); QVector transforms; - transforms.fill(glm::mat4(), numJoints); + transforms.fill(glm::mat4(), numStates); + + QVector& ragdollPoints = _ragdoll->getPoints(); // compute the default transforms and slam the ragdoll positions accordingly // (which puts the shapes where we want them) - for (int i = 0; i < numJoints; i++) { - const FBXJoint& joint = geometry.joints.at(i); + for (int i = 0; i < numStates; i++) { + JointState& state = _jointStates[i]; + const FBXJoint& joint = state.getFBXJoint(); int parentIndex = joint.parentIndex; if (parentIndex == -1) { transforms[i] = _jointStates[i].getTransform(); - _ragdollPoints[i].initPosition(extractTranslation(transforms[i])); + ragdollPoints[i].initPosition(extractTranslation(transforms[i])); continue; } @@ -778,7 +713,7 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { transforms[i] = transforms[parentIndex] * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(modifiedRotation) * joint.postTransform; // setting the ragdollPoints here slams the VerletShapes into their default positions - _ragdollPoints[i].initPosition(extractTranslation(transforms[i])); + ragdollPoints[i].initPosition(extractTranslation(transforms[i])); } // compute bounding box that encloses all shapes @@ -838,7 +773,7 @@ void SkeletonModel::resetShapePositionsToDefaultPose() { } const FBXGeometry& geometry = _geometry->getFBXGeometry(); - if (geometry.joints.isEmpty() || _shapes.size() != geometry.joints.size()) { + if (geometry.joints.isEmpty()) { return; } @@ -893,3 +828,57 @@ void SkeletonModel::renderBoundingCollisionShapes(float alpha) { glPopMatrix(); } +const int BALL_SUBDIVISIONS = 10; + +// virtual +void SkeletonModel::renderJointCollisionShapes(float alpha) { + if (!_ragdoll) { + return; + } + glPushMatrix(); + Application::getInstance()->loadTranslatedViewMatrix(_translation); + glm::vec3 simulationTranslation = _ragdoll->getTranslationInSimulationFrame(); + for (int i = 0; i < _shapes.size(); i++) { + Shape* shape = _shapes[i]; + if (!shape) { + continue; + } + + glPushMatrix(); + // shapes are stored in simulation-frame but we want position to be model-relative + if (shape->getType() == Shape::SPHERE_SHAPE) { + glm::vec3 position = shape->getTranslation() - simulationTranslation; + glTranslatef(position.x, position.y, position.z); + // draw a grey sphere at shape position + glColor4f(0.75f, 0.75f, 0.75f, alpha); + glutSolidSphere(shape->getBoundingRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); + } else if (shape->getType() == Shape::CAPSULE_SHAPE) { + CapsuleShape* capsule = static_cast(shape); + + // draw a blue sphere at the capsule endpoint + glm::vec3 endPoint; + capsule->getEndPoint(endPoint); + endPoint = endPoint - simulationTranslation; + glTranslatef(endPoint.x, endPoint.y, endPoint.z); + glColor4f(0.6f, 0.6f, 0.8f, alpha); + glutSolidSphere(capsule->getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); + + // draw a yellow sphere at the capsule startpoint + glm::vec3 startPoint; + capsule->getStartPoint(startPoint); + startPoint = startPoint - simulationTranslation; + glm::vec3 axis = endPoint - startPoint; + glTranslatef(-axis.x, -axis.y, -axis.z); + glColor4f(0.8f, 0.8f, 0.6f, alpha); + glutSolidSphere(capsule->getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); + + // draw a green cylinder between the two points + glm::vec3 origin(0.0f); + glColor4f(0.6f, 0.8f, 0.6f, alpha); + Avatar::renderJointConnectingCone( origin, axis, capsule->getRadius(), capsule->getRadius()); + } + glPopMatrix(); + } + glPopMatrix(); +} + diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 5cb89f5e44..b0d6ed7325 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -15,18 +15,20 @@ #include "renderer/Model.h" #include -#include +#include "SkeletonRagdoll.h" class Avatar; class MuscleConstraint; +class SkeletonRagdoll; /// A skeleton loaded from a model. -class SkeletonModel : public Model, public Ragdoll { +class SkeletonModel : public Model { Q_OBJECT public: SkeletonModel(Avatar* owningAvatar, QObject* parent = NULL); + ~SkeletonModel(); void setJointStates(QVector states); @@ -96,15 +98,15 @@ public: bool getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; virtual void updateVisibleJointStates(); - - // virtual overrride from Ragdoll - virtual void stepRagdollForward(float deltaTime); + SkeletonRagdoll* buildRagdoll(); + SkeletonRagdoll* getRagdoll() { return _ragdoll; } + void moveShapesTowardJoints(float fraction); - void updateMuscles(); void computeBoundingShape(const FBXGeometry& geometry); void renderBoundingCollisionShapes(float alpha); + void renderJointCollisionShapes(float alpha); float getBoundingShapeRadius() const { return _boundingShape.getRadius(); } const CapsuleShape& getBoundingShape() const { return _boundingShape; } @@ -114,10 +116,6 @@ public: protected: - // virtual overrrides from Ragdoll - void initRagdollPoints(); - void buildRagdollConstraints(); - void buildShapes(); /// \param jointIndex index of joint in model @@ -146,7 +144,7 @@ private: CapsuleShape _boundingShape; glm::vec3 _boundingShapeLocalOffset; - QVector _muscleConstraints; + SkeletonRagdoll* _ragdoll; }; #endif // hifi_SkeletonModel_h diff --git a/interface/src/avatar/SkeletonRagdoll.cpp b/interface/src/avatar/SkeletonRagdoll.cpp new file mode 100644 index 0000000000..6318323990 --- /dev/null +++ b/interface/src/avatar/SkeletonRagdoll.cpp @@ -0,0 +1,145 @@ +// +// SkeletonRagdoll.cpp +// interface/src/avatar +// +// Created by Andrew Meadows 2014.08.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 "SkeletonRagdoll.h" +#include "MuscleConstraint.h" +#include "../renderer/Model.h" + +SkeletonRagdoll::SkeletonRagdoll(Model* model) : Ragdoll(), _model(model) { + assert(_model); +} + +SkeletonRagdoll::~SkeletonRagdoll() { +} + +// virtual +void SkeletonRagdoll::stepForward(float deltaTime) { + setTransform(_model->getTranslation(), _model->getRotation()); + Ragdoll::stepForward(deltaTime); + updateMuscles(); + int numConstraints = _muscleConstraints.size(); + for (int i = 0; i < numConstraints; ++i) { + _muscleConstraints[i]->enforce(); + } +} + +void SkeletonRagdoll::slamPointPositions() { + QVector& jointStates = _model->getJointStates(); + int numStates = jointStates.size(); + for (int i = 0; i < numStates; ++i) { + _points[i].initPosition(jointStates.at(i).getPosition()); + } +} + +// virtual +void SkeletonRagdoll::initPoints() { + clearConstraintsAndPoints(); + _muscleConstraints.clear(); + + initTransform(); + // one point for each joint + QVector& jointStates = _model->getJointStates(); + int numStates = jointStates.size(); + _points.fill(VerletPoint(), numStates); + slamPointPositions(); +} + +// virtual +void SkeletonRagdoll::buildConstraints() { + QVector& jointStates = _model->getJointStates(); + + // NOTE: the length of DistanceConstraints is computed and locked in at this time + // so make sure the ragdoll positions are in a normal configuration before here. + const int numPoints = _points.size(); + assert(numPoints == jointStates.size()); + + float minBone = FLT_MAX; + float maxBone = -FLT_MAX; + QMultiMap families; + for (int i = 0; i < numPoints; ++i) { + const JointState& state = jointStates.at(i); + int parentIndex = state.getParentIndex(); + if (parentIndex != -1) { + DistanceConstraint* bone = new DistanceConstraint(&(_points[i]), &(_points[parentIndex])); + bone->setDistance(state.getDistanceToParent()); + _boneConstraints.push_back(bone); + families.insert(parentIndex, i); + } + float boneLength = glm::length(state.getPositionInParentFrame()); + if (boneLength > maxBone) { + maxBone = boneLength; + } else if (boneLength < minBone) { + minBone = boneLength; + } + } + // Joints that have multiple children effectively have rigid constraints between the children + // in the parent frame, so we add DistanceConstraints between children in the same family. + QMultiMap::iterator itr = families.begin(); + while (itr != families.end()) { + QList children = families.values(itr.key()); + int numChildren = children.size(); + if (numChildren > 1) { + for (int i = 1; i < numChildren; ++i) { + DistanceConstraint* bone = new DistanceConstraint(&(_points[children[i-1]]), &(_points[children[i]])); + _boneConstraints.push_back(bone); + } + if (numChildren > 2) { + DistanceConstraint* bone = new DistanceConstraint(&(_points[children[numChildren-1]]), &(_points[children[0]])); + _boneConstraints.push_back(bone); + } + } + ++itr; + } + + float MAX_STRENGTH = 0.6f; + float MIN_STRENGTH = 0.05f; + // each joint gets a MuscleConstraint to its parent + for (int i = 1; i < numPoints; ++i) { + const JointState& state = jointStates.at(i); + int p = state.getParentIndex(); + if (p == -1) { + continue; + } + MuscleConstraint* constraint = new MuscleConstraint(&(_points[p]), &(_points[i])); + _muscleConstraints.push_back(constraint); + + // Short joints are more susceptible to wiggle so we modulate the strength based on the joint's length: + // long = weak and short = strong. + constraint->setIndices(p, i); + float boneLength = glm::length(state.getPositionInParentFrame()); + + float strength = MIN_STRENGTH + (MAX_STRENGTH - MIN_STRENGTH) * (maxBone - boneLength) / (maxBone - minBone); + if (!families.contains(i)) { + // Although muscles only pull on the children not parents, nevertheless those joints that have + // parents AND children are more stable than joints at the end such as fingers. For such joints we + // bestow maximum strength which helps reduce wiggle. + strength = MAX_MUSCLE_STRENGTH; + } + constraint->setStrength(strength); + } +} + +void SkeletonRagdoll::updateMuscles() { + QVector& jointStates = _model->getJointStates(); + int numConstraints = _muscleConstraints.size(); + glm::quat rotation = _model->getRotation(); + for (int i = 0; i < numConstraints; ++i) { + MuscleConstraint* constraint = _muscleConstraints[i]; + int j = constraint->getParentIndex(); + int k = constraint->getChildIndex(); + assert(j != -1 && k != -1); + // ragdollPoints are in simulation-frame but jointStates are in model-frame + constraint->setChildOffset(rotation * (jointStates.at(k).getPosition() - jointStates.at(j).getPosition())); + } +} diff --git a/interface/src/avatar/SkeletonRagdoll.h b/interface/src/avatar/SkeletonRagdoll.h new file mode 100644 index 0000000000..ae9bec9116 --- /dev/null +++ b/interface/src/avatar/SkeletonRagdoll.h @@ -0,0 +1,44 @@ +// +// SkeletonkRagdoll.h +// interface/src/avatar +// +// Created by Andrew Meadows 2014.08.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_SkeletonRagdoll_h +#define hifi_SkeletonRagdoll_h + +#include + +#include + +#include "../renderer/JointState.h" + +class MuscleConstraint; +class Model; + +class SkeletonRagdoll : public Ragdoll { +public: + + SkeletonRagdoll(Model* model); + virtual ~SkeletonRagdoll(); + + void slamPointPositions(); + virtual void stepForward(float deltaTime); + + virtual void initPoints(); + virtual void buildConstraints(); + +protected: + void updateMuscles(); + +private: + Model* _model; + QVector _muscleConstraints; +}; + +#endif // hifi_SkeletonRagdoll_h diff --git a/interface/src/devices/CaraFaceTracker.cpp b/interface/src/devices/CaraFaceTracker.cpp index 9c67163dca..27cf3b175b 100644 --- a/interface/src/devices/CaraFaceTracker.cpp +++ b/interface/src/devices/CaraFaceTracker.cpp @@ -10,7 +10,7 @@ // #include "CaraFaceTracker.h" -#include +#include //qt #include @@ -37,6 +37,7 @@ static QString sampleJson = "[{\"id\":1, \ static const glm::vec3 DEFAULT_HEAD_ORIGIN(0.0f, 0.0f, 0.0f); static const float TRANSLATION_SCALE = 1.0f; static const int NUM_BLENDSHAPE_COEFF = 30; +static const int NUM_SMOOTHING_SAMPLES = 3; struct CaraPerson { struct CaraPose { @@ -217,9 +218,9 @@ private: CaraFaceTracker::CaraFaceTracker() : _lastReceiveTimestamp(0), - _previousPitch(0.0f), - _previousYaw(0.0f), - _previousRoll(0.0f), + _pitchAverage(NUM_SMOOTHING_SAMPLES), + _yawAverage(NUM_SMOOTHING_SAMPLES), + _rollAverage(NUM_SMOOTHING_SAMPLES), _eyeGazeLeftPitch(0.0f), _eyeGazeLeftYaw(0.0f), _eyeGazeRightPitch(0.0f), @@ -252,9 +253,9 @@ CaraFaceTracker::CaraFaceTracker() : CaraFaceTracker::CaraFaceTracker(const QHostAddress& host, quint16 port) : _lastReceiveTimestamp(0), - _previousPitch(0.0f), - _previousYaw(0.0f), - _previousRoll(0.0f), + _pitchAverage(NUM_SMOOTHING_SAMPLES), + _yawAverage(NUM_SMOOTHING_SAMPLES), + _rollAverage(NUM_SMOOTHING_SAMPLES), _eyeGazeLeftPitch(0.0f), _eyeGazeLeftYaw(0.0f), _eyeGazeRightPitch(0.0f), @@ -371,6 +372,7 @@ void CaraFaceTracker::decodePacket(const QByteArray& buffer) { CaraPerson person = CaraPacketDecoder::extractOne(buffer, &jsonError); if(jsonError.error == QJsonParseError::NoError) { + //do some noise filtering to the head poses //reduce the noise first by truncating to 1 dp person.pose.roll = glm::floor(person.pose.roll * 10) / 10; @@ -386,43 +388,39 @@ void CaraFaceTracker::decodePacket(const QByteArray& buffer) { float theta = 2 * acos(r.w); if (theta > EPSILON) { float rMag = glm::length(glm::vec3(r.x, r.y, r.z)); - const float AVERAGE_CARA_FRAME_TIME = 0.033f; + const float AVERAGE_CARA_FRAME_TIME = 0.04f; const float ANGULAR_VELOCITY_MIN = 1.2f; const float YAW_STANDARD_DEV_DEG = 2.5f; _headAngularVelocity = theta / AVERAGE_CARA_FRAME_TIME * glm::vec3(r.x, r.y, r.z) / rMag; + _pitchAverage.updateAverage(person.pose.pitch); + _rollAverage.updateAverage(person.pose.roll); - //use the angular velocity for roll and pitch, if it's below the threshold don't move - if(glm::abs(_headAngularVelocity.x) < ANGULAR_VELOCITY_MIN) { - person.pose.pitch = _previousPitch; - } + //could use the angular velocity to detemine whether to update pitch and roll to further remove the noise. + //use the angular velocity for roll and pitch, update if > THRESHOLD + //if(glm::abs(_headAngularVelocity.x) > ANGULAR_VELOCITY_MIN) { + // _pitchAverage.updateAverage(person.pose.pitch); + //} - if(glm::abs(_headAngularVelocity.z) < ANGULAR_VELOCITY_MIN) { - person.pose.roll = _previousRoll; - } + //if(glm::abs(_headAngularVelocity.z) > ANGULAR_VELOCITY_MIN) { + // _rollAverage.updateAverage(person.pose.roll);; + //} //for yaw, the jitter is great, you can't use angular velocity because it swings too much //use the previous and current yaw, calculate the //abs difference and move it the difference is above the standard deviation which is around 2.5 - // (this will introduce some jerks but will not encounter lag) - - // < the standard deviation 2.5 deg, no move - if(glm::abs(person.pose.yaw - _previousYaw) < YAW_STANDARD_DEV_DEG) { + // > the standard deviation 2.5 deg, update the yaw smoothing average + if(glm::abs(person.pose.yaw - _yawAverage.getAverage()) > YAW_STANDARD_DEV_DEG) { //qDebug() << "Yaw Diff: " << glm::abs(person.pose.yaw - _previousYaw); - person.pose.yaw = _previousYaw; + _yawAverage.updateAverage(person.pose.yaw); } - //update the previous angles - _previousPitch = person.pose.pitch; - _previousYaw = person.pose.yaw; - _previousRoll = person.pose.roll; - //set the new rotation - newRotation = glm::quat(glm::vec3(DEGTORAD(person.pose.pitch), DEGTORAD(person.pose.yaw), DEGTORAD(-person.pose.roll))); + newRotation = glm::quat(glm::vec3(DEGTORAD(_pitchAverage.getAverage()), DEGTORAD(_yawAverage.getAverage()), DEGTORAD(-_rollAverage.getAverage()))); } else { - //no change in position - newRotation = glm::quat(glm::vec3(DEGTORAD(_previousPitch), DEGTORAD(_previousYaw), DEGTORAD(-_previousRoll))); + //no change in position, use previous averages + newRotation = glm::quat(glm::vec3(DEGTORAD(_pitchAverage.getAverage()), DEGTORAD(_yawAverage.getAverage()), DEGTORAD(-_rollAverage.getAverage()))); _headAngularVelocity = glm::vec3(0,0,0); } @@ -456,4 +454,3 @@ void CaraFaceTracker::decodePacket(const QByteArray& buffer) { float CaraFaceTracker::getBlendshapeCoefficient(int index) const { return (index >= 0 && index < (int)_blendshapeCoefficients.size()) ? _blendshapeCoefficients[index] : 0.0f; } - diff --git a/interface/src/devices/CaraFaceTracker.h b/interface/src/devices/CaraFaceTracker.h index f51fed0f1b..0c715afda9 100644 --- a/interface/src/devices/CaraFaceTracker.h +++ b/interface/src/devices/CaraFaceTracker.h @@ -9,11 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hi_fi_CaraFaceTracker_h -#define hi_fi_CaraFaceTracker_h +#ifndef hifi_CaraFaceTracker_h +#define hifi_CaraFaceTracker_h #include +#include #include "FaceTracker.h" /*! @@ -90,10 +91,10 @@ private: //head tracking glm::vec3 _headAngularVelocity; - //pose history - float _previousPitch; - float _previousYaw; - float _previousRoll; + //pose average + SimpleMovingAverage _pitchAverage; + SimpleMovingAverage _yawAverage; + SimpleMovingAverage _rollAverage; // eye gaze degrees float _eyeGazeLeftPitch; @@ -120,4 +121,4 @@ private: int _jawOpenIndex; }; -#endif //endif hi_fi_CaraFaceTracker_h \ No newline at end of file +#endif //endif hifi_CaraFaceTracker_h diff --git a/interface/src/devices/DdeFaceTracker.cpp b/interface/src/devices/DdeFaceTracker.cpp new file mode 100644 index 0000000000..ae5beb8c85 --- /dev/null +++ b/interface/src/devices/DdeFaceTracker.cpp @@ -0,0 +1,282 @@ +// +// DdeFaceTracker.cpp +// +// +// Created by Clement on 8/2/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 +#include +#include + +#include "DdeFaceTracker.h" + +static const QHostAddress DDE_FEATURE_POINT_SERVER_ADDR("127.0.0.1"); +static const quint16 DDE_FEATURE_POINT_SERVER_PORT = 5555; + +static const int NUM_EXPRESSION = 46; +static const int MIN_PACKET_SIZE = (8 + NUM_EXPRESSION) * sizeof(float) + sizeof(int); +static const int MAX_NAME_SIZE = 31; + +struct Packet{ + //roughly in mm + float focal_length[1]; + float translation[3]; + + //quaternion + float rotation[4]; + + //blendshape coefficients ranging between -0.2 and 1.5 + float expressions[NUM_EXPRESSION]; + + //avatar id selected on the UI + int avatar_id; + + //client name, arbitrary length + char name[MAX_NAME_SIZE + 1]; +}; + +DdeFaceTracker::DdeFaceTracker() : +_lastReceiveTimestamp(0), +_reset(false), +_leftBlinkIndex(0), // see http://support.faceshift.com/support/articles/35129-export-of-blendshapes +_rightBlinkIndex(1), +_leftEyeOpenIndex(8), +_rightEyeOpenIndex(9), +_browDownLeftIndex(14), +_browDownRightIndex(15), +_browUpCenterIndex(16), +_browUpLeftIndex(17), +_browUpRightIndex(18), +_mouthSmileLeftIndex(28), +_mouthSmileRightIndex(29), +_jawOpenIndex(21) +{ + _blendshapeCoefficients.resize(NUM_EXPRESSION); + + connect(&_udpSocket, SIGNAL(readyRead()), SLOT(readPendingDatagrams())); + connect(&_udpSocket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(socketErrorOccurred(QAbstractSocket::SocketError))); + connect(&_udpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), SLOT(socketStateChanged(QAbstractSocket::SocketState))); + + bindTo(DDE_FEATURE_POINT_SERVER_PORT); +} + +DdeFaceTracker::DdeFaceTracker(const QHostAddress& host, quint16 port) : +_lastReceiveTimestamp(0), +_reset(false), +_leftBlinkIndex(0), // see http://support.faceshift.com/support/articles/35129-export-of-blendshapes +_rightBlinkIndex(1), +_leftEyeOpenIndex(8), +_rightEyeOpenIndex(9), +_browDownLeftIndex(14), +_browDownRightIndex(15), +_browUpCenterIndex(16), +_browUpLeftIndex(17), +_browUpRightIndex(18), +_mouthSmileLeftIndex(28), +_mouthSmileRightIndex(29), +_jawOpenIndex(21) +{ + _blendshapeCoefficients.resize(NUM_EXPRESSION); + + connect(&_udpSocket, SIGNAL(readyRead()), SLOT(readPendingDatagrams())); + connect(&_udpSocket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(socketErrorOccurred(QAbstractSocket::SocketError))); + connect(&_udpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), SIGNAL(socketStateChanged(QAbstractSocket::SocketState))); + + bindTo(host, port); +} + +DdeFaceTracker::~DdeFaceTracker() { + if(_udpSocket.isOpen()) + _udpSocket.close(); +} + +void DdeFaceTracker::init() { + +} + +void DdeFaceTracker::reset() { + _reset = true; +} + +void DdeFaceTracker::update() { + +} + +void DdeFaceTracker::bindTo(quint16 port) { + bindTo(QHostAddress::Any, port); +} + +void DdeFaceTracker::bindTo(const QHostAddress& host, quint16 port) { + if(_udpSocket.isOpen()) { + _udpSocket.close(); + } + _udpSocket.bind(host, port); +} + +bool DdeFaceTracker::isActive() const { + static const int ACTIVE_TIMEOUT_USECS = 3000000; //3 secs + return (usecTimestampNow() - _lastReceiveTimestamp < ACTIVE_TIMEOUT_USECS); +} + +//private slots and methods +void DdeFaceTracker::socketErrorOccurred(QAbstractSocket::SocketError socketError) { + qDebug() << "[Error] DDE Face Tracker Socket Error: " << _udpSocket.errorString(); +} + +void DdeFaceTracker::socketStateChanged(QAbstractSocket::SocketState socketState) { + QString state; + switch(socketState) { + case QAbstractSocket::BoundState: + state = "Bounded"; + break; + case QAbstractSocket::ClosingState: + state = "Closing"; + break; + case QAbstractSocket::ConnectedState: + state = "Connected"; + break; + case QAbstractSocket::ConnectingState: + state = "Connecting"; + break; + case QAbstractSocket::HostLookupState: + state = "Host Lookup"; + break; + case QAbstractSocket::ListeningState: + state = "Listening"; + break; + case QAbstractSocket::UnconnectedState: + state = "Unconnected"; + break; + } + qDebug() << "[Info] DDE Face Tracker Socket: " << socketState; +} + +void DdeFaceTracker::readPendingDatagrams() { + QByteArray buffer; + while (_udpSocket.hasPendingDatagrams()) { + buffer.resize(_udpSocket.pendingDatagramSize()); + _udpSocket.readDatagram(buffer.data(), buffer.size()); + } + decodePacket(buffer); +} + +float DdeFaceTracker::getBlendshapeCoefficient(int index) const { + return (index >= 0 && index < (int)_blendshapeCoefficients.size()) ? _blendshapeCoefficients[index] : 0.0f; +} + +static const float DDE_MIN_RANGE = -0.2; +static const float DDE_MAX_RANGE = 1.5; +float rescaleCoef(float ddeCoef) { + return (ddeCoef - DDE_MIN_RANGE) / (DDE_MAX_RANGE - DDE_MIN_RANGE); +} + +const int MIN = 0; +const int AVG = 1; +const int MAX = 2; +const float LONG_TERM_AVERAGE = 0.999f; + + +void resetCoefficient(float * coefficient, float currentValue) { + coefficient[MIN] = coefficient[MAX] = coefficient[AVG] = currentValue; +} + +float updateAndGetCoefficient(float * coefficient, float currentValue, bool scaleToRange = false) { + coefficient[MIN] = (currentValue < coefficient[MIN]) ? currentValue : coefficient[MIN]; + coefficient[MAX] = (currentValue > coefficient[MAX]) ? currentValue : coefficient[MAX]; + coefficient[AVG] = LONG_TERM_AVERAGE * coefficient[AVG] + (1.f - LONG_TERM_AVERAGE) * currentValue; + if (coefficient[MAX] > coefficient[MIN]) { + if (scaleToRange) { + return glm::clamp((currentValue - coefficient[AVG]) / (coefficient[MAX] - coefficient[MIN]), 0.f, 1.f); + } else { + return glm::clamp(currentValue - coefficient[AVG], 0.f, 1.f); + } + } else { + return 0.f; + } +} + +void DdeFaceTracker::decodePacket(const QByteArray& buffer) { + if(buffer.size() > MIN_PACKET_SIZE) { + Packet packet; + int bytesToCopy = glm::min((int)sizeof(packet), buffer.size()); + memset(&packet.name, '\n', MAX_NAME_SIZE + 1); + memcpy(&packet, buffer.data(), bytesToCopy); + + glm::vec3 translation; + memcpy(&translation, packet.translation, sizeof(packet.translation)); + glm::quat rotation; + memcpy(&rotation, &packet.rotation, sizeof(packet.rotation)); + if (_reset || (_lastReceiveTimestamp == 0)) { + memcpy(&_referenceTranslation, &translation, sizeof(glm::vec3)); + memcpy(&_referenceRotation, &rotation, sizeof(glm::quat)); + + resetCoefficient(_rightEye, packet.expressions[0]); + resetCoefficient(_leftEye, packet.expressions[1]); + _reset = false; + } + + // Compute relative translation + float LEAN_DAMPING_FACTOR = 200.0f; + translation -= _referenceTranslation; + translation /= LEAN_DAMPING_FACTOR; + translation.x *= -1; + + // Compute relative rotation + rotation = glm::inverse(_referenceRotation) * rotation; + + // copy values + _headTranslation = translation; + _headRotation = rotation; + + if (_lastReceiveTimestamp == 0) { + // On first packet, reset coefficients + } + + // Set blendshapes + float EYE_MAGNIFIER = 4.0f; + + float rightEye = (updateAndGetCoefficient(_rightEye, packet.expressions[0])) * EYE_MAGNIFIER; + _blendshapeCoefficients[_rightBlinkIndex] = rightEye; + float leftEye = (updateAndGetCoefficient(_leftEye, packet.expressions[1])) * EYE_MAGNIFIER; + _blendshapeCoefficients[_leftBlinkIndex] = leftEye; + + // Right eye = packet.expressions[0]; + + float leftBrow = 1.0f - rescaleCoef(packet.expressions[14]); + if (leftBrow < 0.5f) { + _blendshapeCoefficients[_browDownLeftIndex] = 1.0f - 2.0f * leftBrow; + _blendshapeCoefficients[_browUpLeftIndex] = 0.0f; + } else { + _blendshapeCoefficients[_browDownLeftIndex] = 0.0f; + _blendshapeCoefficients[_browUpLeftIndex] = 2.0f * (leftBrow - 0.5f); + } + float rightBrow = 1.0f - rescaleCoef(packet.expressions[15]); + if (rightBrow < 0.5f) { + _blendshapeCoefficients[_browDownRightIndex] = 1.0f - 2.0f * rightBrow; + _blendshapeCoefficients[_browUpRightIndex] = 0.0f; + } else { + _blendshapeCoefficients[_browDownRightIndex] = 0.0f; + _blendshapeCoefficients[_browUpRightIndex] = 2.0f * (rightBrow - 0.5f); + } + + float JAW_OPEN_MAGNIFIER = 1.4f; + _blendshapeCoefficients[_jawOpenIndex] = rescaleCoef(packet.expressions[21]) * JAW_OPEN_MAGNIFIER; + + + _blendshapeCoefficients[_mouthSmileLeftIndex] = rescaleCoef(packet.expressions[24]); + _blendshapeCoefficients[_mouthSmileRightIndex] = rescaleCoef(packet.expressions[23]); + + + } else { + qDebug() << "[Error] DDE Face Tracker Decode Error"; + } + _lastReceiveTimestamp = usecTimestampNow(); +} diff --git a/interface/src/devices/DdeFaceTracker.h b/interface/src/devices/DdeFaceTracker.h new file mode 100644 index 0000000000..68d9e7d487 --- /dev/null +++ b/interface/src/devices/DdeFaceTracker.h @@ -0,0 +1,93 @@ +// +// DdeFaceTracker.h +// +// +// Created by Clement on 8/2/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_DdeFaceTracker_h +#define hifi_DdeFaceTracker_h + +#include + +#include "FaceTracker.h" + +class DdeFaceTracker : public FaceTracker { + Q_OBJECT + +public: + DdeFaceTracker(); + DdeFaceTracker(const QHostAddress& host, quint16 port); + ~DdeFaceTracker(); + + //initialization + void init(); + void reset(); + void update(); + + //sockets + void bindTo(quint16 port); + void bindTo(const QHostAddress& host, quint16 port); + bool isActive() const; + + float getLeftBlink() const { return getBlendshapeCoefficient(_leftBlinkIndex); } + float getRightBlink() const { return getBlendshapeCoefficient(_rightBlinkIndex); } + float getLeftEyeOpen() const { return getBlendshapeCoefficient(_leftEyeOpenIndex); } + float getRightEyeOpen() const { return getBlendshapeCoefficient(_rightEyeOpenIndex); } + + float getBrowDownLeft() const { return getBlendshapeCoefficient(_browDownLeftIndex); } + float getBrowDownRight() const { return getBlendshapeCoefficient(_browDownRightIndex); } + float getBrowUpCenter() const { return getBlendshapeCoefficient(_browUpCenterIndex); } + float getBrowUpLeft() const { return getBlendshapeCoefficient(_browUpLeftIndex); } + float getBrowUpRight() const { return getBlendshapeCoefficient(_browUpRightIndex); } + + float getMouthSize() const { return getBlendshapeCoefficient(_jawOpenIndex); } + float getMouthSmileLeft() const { return getBlendshapeCoefficient(_mouthSmileLeftIndex); } + float getMouthSmileRight() const { return getBlendshapeCoefficient(_mouthSmileRightIndex); } + +private slots: + + //sockets + void socketErrorOccurred(QAbstractSocket::SocketError socketError); + void readPendingDatagrams(); + void socketStateChanged(QAbstractSocket::SocketState socketState); + +private: + float getBlendshapeCoefficient(int index) const; + void decodePacket(const QByteArray& buffer); + + // sockets + QUdpSocket _udpSocket; + quint64 _lastReceiveTimestamp; + + bool _reset; + glm::vec3 _referenceTranslation; + glm::quat _referenceRotation; + + int _leftBlinkIndex; + int _rightBlinkIndex; + int _leftEyeOpenIndex; + int _rightEyeOpenIndex; + + // Brows + int _browDownLeftIndex; + int _browDownRightIndex; + int _browUpCenterIndex; + int _browUpLeftIndex; + int _browUpRightIndex; + + int _mouthSmileLeftIndex; + int _mouthSmileRightIndex; + + int _jawOpenIndex; + + float _leftEye[3]; + float _rightEye[3]; + +}; + +#endif // hifi_DdeFaceTracker_h \ No newline at end of file diff --git a/interface/src/devices/OculusManager.cpp b/interface/src/devices/OculusManager.cpp index 49164cc8dd..3582f82820 100644 --- a/interface/src/devices/OculusManager.cpp +++ b/interface/src/devices/OculusManager.cpp @@ -44,7 +44,7 @@ ovrFovPort OculusManager::_eyeFov[ovrEye_Count]; ovrEyeRenderDesc OculusManager::_eyeRenderDesc[ovrEye_Count]; ovrSizei OculusManager::_renderTargetSize; ovrVector2f OculusManager::_UVScaleOffset[ovrEye_Count][2]; -GLuint OculusManager::_vertices[ovrEye_Count] = { 0, 0 }; +GLuint OculusManager::_vertices[ovrEye_Count] = { 0, 0 }; GLuint OculusManager::_indices[ovrEye_Count] = { 0, 0 }; GLsizei OculusManager::_meshSize[ovrEye_Count] = { 0, 0 }; ovrFrameTiming OculusManager::_hmdFrameTiming; @@ -66,12 +66,14 @@ void OculusManager::connect() { UserActivityLogger::getInstance().connectedDevice("hmd", "oculus"); } _isConnected = true; - +#if defined(__APPLE__) || defined(_WIN32) + _eyeFov[0] = _ovrHmd->DefaultEyeFov[0]; + _eyeFov[1] = _ovrHmd->DefaultEyeFov[1]; +#else ovrHmd_GetDesc(_ovrHmd, &_ovrHmdDesc); - _eyeFov[0] = _ovrHmdDesc.DefaultEyeFov[0]; _eyeFov[1] = _ovrHmdDesc.DefaultEyeFov[1]; - +#endif //Get texture size ovrSizei recommendedTex0Size = ovrHmd_GetFovTextureSize(_ovrHmd, ovrEye_Left, _eyeFov[0], 1.0f); @@ -86,11 +88,21 @@ void OculusManager::connect() { _eyeRenderDesc[0] = ovrHmd_GetRenderDesc(_ovrHmd, ovrEye_Left, _eyeFov[0]); _eyeRenderDesc[1] = ovrHmd_GetRenderDesc(_ovrHmd, ovrEye_Right, _eyeFov[1]); +#if defined(__APPLE__) || defined(_WIN32) + ovrHmd_SetEnabledCaps(_ovrHmd, ovrHmdCap_LowPersistence); +#else ovrHmd_SetEnabledCaps(_ovrHmd, ovrHmdCap_LowPersistence | ovrHmdCap_LatencyTest); +#endif +#if defined(__APPLE__) || defined(_WIN32) + ovrHmd_ConfigureTracking(_ovrHmd, ovrTrackingCap_Orientation | ovrTrackingCap_Position | + ovrTrackingCap_MagYawCorrection, + ovrTrackingCap_Orientation); +#else ovrHmd_StartSensor(_ovrHmd, ovrSensorCap_Orientation | ovrSensorCap_YawCorrection | ovrSensorCap_Position, ovrSensorCap_Orientation); +#endif if (!_camera) { _camera = new Camera; @@ -123,6 +135,10 @@ void OculusManager::connect() { } else { _isConnected = false; + + // we're definitely not in "VR mode" so tell the menu that + Menu::getInstance()->getActionForOption(MenuOption::EnableVRMode)->setChecked(false); + ovrHmd_Destroy(_ovrHmd); ovr_Shutdown(); } @@ -183,6 +199,16 @@ void OculusManager::generateDistortionMesh() { DistortionVertex* v = pVBVerts; ovrDistortionVertex* ov = meshData.pVertexData; for (unsigned int vertNum = 0; vertNum < meshData.VertexCount; vertNum++) { +#if defined(__APPLE__) || defined(_WIN32) + v->pos.x = ov->ScreenPosNDC.x; + v->pos.y = ov->ScreenPosNDC.y; + v->texR.x = ov->TanEyeAnglesR.x; + v->texR.y = ov->TanEyeAnglesR.y; + v->texG.x = ov->TanEyeAnglesG.x; + v->texG.y = ov->TanEyeAnglesG.y; + v->texB.x = ov->TanEyeAnglesB.x; + v->texB.y = ov->TanEyeAnglesB.y; +#else v->pos.x = ov->Pos.x; v->pos.y = ov->Pos.y; v->texR.x = ov->TexR.x; @@ -191,6 +217,7 @@ void OculusManager::generateDistortionMesh() { v->texG.y = ov->TexG.y; v->texB.x = ov->TexB.x; v->texB.y = ov->TexB.y; +#endif v->color.r = v->color.g = v->color.b = (GLubyte)(ov->VignetteFactor * 255.99f); v->color.a = (GLubyte)(ov->TimeWarpFactor * 255.99f); v++; @@ -291,12 +318,23 @@ void OculusManager::display(const glm::quat &bodyOrientation, const glm::vec3 &p glPushMatrix(); glm::quat orientation; - + glm::vec3 trackerPosition; + +#if defined(__APPLE__) || defined(_WIN32) + ovrTrackingState ts = ovrHmd_GetTrackingState(_ovrHmd, ovr_GetTimeInSeconds()); + ovrVector3f ovrHeadPosition = ts.HeadPose.ThePose.Position; + + trackerPosition = glm::vec3(ovrHeadPosition.x, ovrHeadPosition.y, ovrHeadPosition.z); + trackerPosition = bodyOrientation * trackerPosition; +#endif + //Render each eye into an fbo for (int eyeIndex = 0; eyeIndex < ovrEye_Count; eyeIndex++) { - +#if defined(__APPLE__) || defined(_WIN32) + ovrEyeType eye = _ovrHmd->EyeRenderOrder[eyeIndex]; +#else ovrEyeType eye = _ovrHmdDesc.EyeRenderOrder[eyeIndex]; - +#endif //Set the camera rotation for this eye eyeRenderPose[eye] = ovrHmd_GetEyePose(_ovrHmd, eye); orientation.x = eyeRenderPose[eye].Orientation.x; @@ -305,7 +343,8 @@ void OculusManager::display(const glm::quat &bodyOrientation, const glm::vec3 &p orientation.w = eyeRenderPose[eye].Orientation.w; _camera->setTargetRotation(bodyOrientation * orientation); - _camera->setTargetPosition(position); + _camera->setTargetPosition(position + trackerPosition); + _camera->update(1.0f / Application::getInstance()->getFps()); Matrix4f proj = ovrMatrix4f_Projection(_eyeRenderDesc[eye].Fov, whichCamera.getNearClip(), whichCamera.getFarClip(), true); @@ -425,19 +464,32 @@ void OculusManager::renderDistortionMesh(ovrPosef eyeRenderPose[ovrEye_Count]) { //Tries to reconnect to the sensors void OculusManager::reset() { #ifdef HAVE_LIBOVR - disconnect(); - connect(); + if (_isConnected) { + ovrHmd_RecenterPose(_ovrHmd); + } #endif } //Gets the current predicted angles from the oculus sensors void OculusManager::getEulerAngles(float& yaw, float& pitch, float& roll) { #ifdef HAVE_LIBOVR +#if defined(__APPLE__) || defined(_WIN32) + ovrTrackingState ts = ovrHmd_GetTrackingState(_ovrHmd, ovr_GetTimeInSeconds()); +#else ovrSensorState ss = ovrHmd_GetSensorState(_ovrHmd, _hmdFrameTiming.ScanoutMidpointSeconds); - +#endif +#if defined(__APPLE__) || defined(_WIN32) + if (ts.StatusFlags & (ovrStatus_OrientationTracked | ovrStatus_PositionTracked)) { +#else if (ss.StatusFlags & (ovrStatus_OrientationTracked | ovrStatus_PositionTracked)) { - ovrPosef pose = ss.Predicted.Pose; - Quatf orientation = Quatf(pose.Orientation); +#endif + +#if defined(__APPLE__) || defined(_WIN32) + ovrPosef headPose = ts.HeadPose.ThePose; +#else + ovrPosef headPose = ss.Predicted.Pose; +#endif + Quatf orientation = Quatf(headPose.Orientation); orientation.GetEulerAngles(&yaw, &pitch, &roll); } else { yaw = 0.0f; @@ -450,6 +502,18 @@ void OculusManager::getEulerAngles(float& yaw, float& pitch, float& roll) { roll = 0.0f; #endif } + +glm::vec3 OculusManager::getRelativePosition() { +#if (defined(__APPLE__) || defined(_WIN32)) && HAVE_LIBOVR + ovrTrackingState trackingState = ovrHmd_GetTrackingState(_ovrHmd, ovr_GetTimeInSeconds()); + ovrVector3f headPosition = trackingState.HeadPose.ThePose.Position; + + return glm::vec3(headPosition.x, headPosition.y, headPosition.z); +#else + // no positional tracking in Linux yet + return glm::vec3(0.0f, 0.0f, 0.0f); +#endif +} //Used to set the size of the glow framebuffers QSize OculusManager::getRenderTargetSize() { diff --git a/interface/src/devices/OculusManager.h b/interface/src/devices/OculusManager.h index 8c929bb50a..3959ea1ab7 100644 --- a/interface/src/devices/OculusManager.h +++ b/interface/src/devices/OculusManager.h @@ -40,6 +40,7 @@ public: /// param \pitch[out] pitch in radians /// param \roll[out] roll in radians static void getEulerAngles(float& yaw, float& pitch, float& roll); + static glm::vec3 getRelativePosition(); static QSize getRenderTargetSize(); private: diff --git a/interface/src/devices/SixenseManager.cpp b/interface/src/devices/SixenseManager.cpp index 089d478198..803060e5d3 100644 --- a/interface/src/devices/SixenseManager.cpp +++ b/interface/src/devices/SixenseManager.cpp @@ -19,6 +19,7 @@ #include "UserActivityLogger.h" #ifdef HAVE_SIXENSE + const int CALIBRATION_STATE_IDLE = 0; const int CALIBRATION_STATE_X = 1; const int CALIBRATION_STATE_Y = 2; @@ -41,8 +42,9 @@ SixenseManager::SixenseManager() { // By default we assume the _neckBase (in orb frame) is as high above the orb // as the "torso" is below it. _neckBase = glm::vec3(NECK_X, -NECK_Y, NECK_Z); - + sixenseInit(); + #endif _hydrasConnected = false; _triggerPressed[0] = false; diff --git a/interface/src/devices/SixenseManager.h b/interface/src/devices/SixenseManager.h index 91f9e9884f..664c102f76 100644 --- a/interface/src/devices/SixenseManager.h +++ b/interface/src/devices/SixenseManager.h @@ -71,7 +71,7 @@ private: glm::vec3 _reachUp; glm::vec3 _reachForward; float _lastDistance; - + #endif bool _hydrasConnected; quint64 _lastMovement; diff --git a/interface/src/models/ModelTreeRenderer.cpp b/interface/src/models/ModelTreeRenderer.cpp index 78107db699..acbb373ffc 100644 --- a/interface/src/models/ModelTreeRenderer.cpp +++ b/interface/src/models/ModelTreeRenderer.cpp @@ -76,6 +76,10 @@ const FBXGeometry* ModelTreeRenderer::getGeometryForModel(const ModelItem& model return result; } +const Model* ModelTreeRenderer::getModelForModelItem(const ModelItem& modelItem) { + return getModel(modelItem); +} + Model* ModelTreeRenderer::getModel(const ModelItem& modelItem) { Model* model = NULL; diff --git a/interface/src/models/ModelTreeRenderer.h b/interface/src/models/ModelTreeRenderer.h index d69b85efe9..63363e4ce2 100644 --- a/interface/src/models/ModelTreeRenderer.h +++ b/interface/src/models/ModelTreeRenderer.h @@ -51,7 +51,7 @@ public: virtual void render(RenderMode renderMode = DEFAULT_RENDER_MODE); virtual const FBXGeometry* getGeometryForModel(const ModelItem& modelItem); - + virtual const Model* getModelForModelItem(const ModelItem& modelItem); /// clears the tree virtual void clear(); diff --git a/interface/src/renderer/JointState.h b/interface/src/renderer/JointState.h index 81591e816b..21961ba48c 100644 --- a/interface/src/renderer/JointState.h +++ b/interface/src/renderer/JointState.h @@ -16,6 +16,7 @@ #include #include +#include #include class AngularConstraint; diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 63a94772a7..290f9b5c6f 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -188,10 +188,12 @@ void Model::initSkinProgram(ProgramObject& program, Model::SkinLocations& locati QVector Model::createJointStates(const FBXGeometry& geometry) { QVector jointStates; - foreach (const FBXJoint& joint, geometry.joints) { - // NOTE: the state keeps a pointer to an FBXJoint + for (int i = 0; i < geometry.joints.size(); ++i) { + const FBXJoint& joint = geometry.joints[i]; + // store a pointer to the FBXJoint in the JointState JointState state; state.setFBXJoint(&joint); + jointStates.append(state); } return jointStates; @@ -199,8 +201,8 @@ QVector Model::createJointStates(const FBXGeometry& geometry) { void Model::initJointTransforms() { // compute model transforms - int numJoints = _jointStates.size(); - for (int i = 0; i < numJoints; ++i) { + int numStates = _jointStates.size(); + for (int i = 0; i < numStates; ++i) { JointState& state = _jointStates[i]; const FBXJoint& joint = state.getFBXJoint(); int parentIndex = joint.parentIndex; @@ -538,9 +540,9 @@ void Model::setJointStates(QVector states) { _jointStates = states; initJointTransforms(); - int numJoints = _jointStates.size(); + int numStates = _jointStates.size(); float radius = 0.0f; - for (int i = 0; i < numJoints; ++i) { + for (int i = 0; i < numStates; ++i) { float distance = glm::length(_jointStates[i].getPosition()); if (distance > radius) { radius = distance; @@ -690,6 +692,14 @@ bool Model::getVisibleJointState(int index, glm::quat& rotation) const { return !state.rotationIsDefault(rotation); } +void Model::clearJointState(int index) { + if (index != -1 && index < _jointStates.size()) { + JointState& state = _jointStates[index]; + state.setRotationInConstrainedFrame(glm::quat()); + state._animationPriority = 0.0f; + } +} + void Model::setJointState(int index, bool valid, const glm::quat& rotation, float priority) { if (index != -1 && index < _jointStates.size()) { JointState& state = _jointStates[index]; @@ -1243,55 +1253,7 @@ float Model::getLimbLength(int jointIndex) const { const int BALL_SUBDIVISIONS = 10; void Model::renderJointCollisionShapes(float alpha) { - glPushMatrix(); - Application::getInstance()->loadTranslatedViewMatrix(_translation); - for (int i = 0; i < _shapes.size(); i++) { - Shape* shape = _shapes[i]; - if (!shape) { - continue; - } - - glPushMatrix(); - // NOTE: the shapes are in the avatar local-frame - if (shape->getType() == Shape::SPHERE_SHAPE) { - // shapes are stored in world-frame, so we have to transform into model frame - glm::vec3 position = _rotation * shape->getTranslation(); - glTranslatef(position.x, position.y, position.z); - const glm::quat& rotation = shape->getRotation(); - glm::vec3 axis = glm::axis(rotation); - glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); - - // draw a grey sphere at shape position - glColor4f(0.75f, 0.75f, 0.75f, alpha); - glutSolidSphere(shape->getBoundingRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); - } else if (shape->getType() == Shape::CAPSULE_SHAPE) { - CapsuleShape* capsule = static_cast(shape); - - // draw a blue sphere at the capsule endpoint - glm::vec3 endPoint; - capsule->getEndPoint(endPoint); - endPoint = _rotation * endPoint; - glTranslatef(endPoint.x, endPoint.y, endPoint.z); - glColor4f(0.6f, 0.6f, 0.8f, alpha); - glutSolidSphere(capsule->getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); - - // draw a yellow sphere at the capsule startpoint - glm::vec3 startPoint; - capsule->getStartPoint(startPoint); - startPoint = _rotation * startPoint; - glm::vec3 axis = endPoint - startPoint; - glTranslatef(-axis.x, -axis.y, -axis.z); - glColor4f(0.8f, 0.8f, 0.6f, alpha); - glutSolidSphere(capsule->getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); - - // draw a green cylinder between the two points - glm::vec3 origin(0.0f); - glColor4f(0.6f, 0.8f, 0.6f, alpha); - Avatar::renderJointConnectingCone( origin, axis, capsule->getRadius(), capsule->getRadius()); - } - glPopMatrix(); - } - glPopMatrix(); + // implement this when we have shapes for regular models } void Model::setBlendedVertices(const QVector& vertices, const QVector& normals) { @@ -1495,9 +1457,11 @@ void Model::renderMeshes(float alpha, RenderMode mode, bool translucent, bool re if (cascadedShadows) { activeProgram->setUniform(activeLocations->shadowDistances, Application::getInstance()->getShadowDistances()); } - activeProgram->setUniformValueArray(activeLocations->localLightDirections, - (const GLfloat*)_localLightDirections, MAX_LOCAL_LIGHTS, 4); - + if (mode != SHADOW_RENDER_MODE) { + activeProgram->setUniformValueArray(activeLocations->localLightDirections, + (const GLfloat*)_localLightDirections, MAX_LOCAL_LIGHTS, 4); + } + if (mesh.blendshapes.isEmpty()) { if (!(mesh.tangents.isEmpty() || mode == SHADOW_RENDER_MODE)) { activeProgram->setAttributeBuffer(activeLocations->tangent, GL_FLOAT, vertexCount * 2 * sizeof(glm::vec3), 3); diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index cbed941791..431d17bf92 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -118,6 +118,9 @@ public: /// \return whether or not the joint state is "valid" (that is, non-default) bool getVisibleJointState(int index, glm::quat& rotation) const; + /// Clear the joint states + void clearJointState(int index); + /// Sets the joint state at the specified index. void setJointState(int index, bool valid, const glm::quat& rotation = glm::quat(), float priority = 1.0f); @@ -149,7 +152,7 @@ public: virtual void buildShapes(); virtual void updateShapePositions(); - void renderJointCollisionShapes(float alpha); + virtual void renderJointCollisionShapes(float alpha); /// Sets blended vertices computed in a separate thread. void setBlendedVertices(const QVector& vertices, const QVector& normals); @@ -164,6 +167,9 @@ public: const QVector& getLocalLights() const { return _localLights; } void setShowTrueJointTransforms(bool show) { _showTrueJointTransforms = show; } + + QVector& getJointStates() { return _jointStates; } + const QVector& getJointStates() const { return _jointStates; } protected: QSharedPointer _geometry; diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index e8fb545343..084c7c7787 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -87,6 +87,37 @@ bool ClipboardScriptingInterface::importVoxels() { return Application::getInstance()->getImportSucceded(); } +bool ClipboardScriptingInterface::importVoxels(const QString& filename) { + qDebug() << "Importing ... "; + + VoxelImporter* importer = Application::getInstance()->getVoxelImporter(); + + if (!importer->validImportFile(filename)) { + return false; + } + + QEventLoop loop; + connect(importer, SIGNAL(importDone()), &loop, SLOT(quit())); + importer->import(filename); + loop.exec(); + + return true; +} + +bool ClipboardScriptingInterface::importVoxels(const QString& filename, float x, float y, float z, float s) { + bool success = importVoxels(filename); + + if (success) { + pasteVoxel(x, y, z, s); + } + + return success; +} + +bool ClipboardScriptingInterface::importVoxels(const QString& filename, const VoxelDetail& destinationVoxel) { + return importVoxels(filename, destinationVoxel.x, destinationVoxel.y, destinationVoxel.z, destinationVoxel.s); +} + void ClipboardScriptingInterface::nudgeVoxel(const VoxelDetail& sourceVoxel, const glm::vec3& nudgeVec) { nudgeVoxel(sourceVoxel.x, sourceVoxel.y, sourceVoxel.z, sourceVoxel.s, nudgeVec); } @@ -100,3 +131,16 @@ void ClipboardScriptingInterface::nudgeVoxel(float x, float y, float z, float s, Application::getInstance()->nudgeVoxelsByVector(sourceVoxel, nudgeVecInTreeSpace); } + + +bool ClipboardScriptingInterface::exportModels(const QString& filename, float x, float y, float z, float s) { + return Application::getInstance()->exportModels(filename, x, y, z, s); +} + +bool ClipboardScriptingInterface::importModels(const QString& filename) { + return Application::getInstance()->importModels(filename); +} + +void ClipboardScriptingInterface::pasteModels(float x, float y, float z, float s) { + Application::getInstance()->pasteModels(x, y, z); +} diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index f0258b0cc7..d322fea1f7 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -39,9 +39,16 @@ public slots: void exportVoxel(float x, float y, float z, float s); bool importVoxels(); + bool importVoxels(const QString& filename); + bool importVoxels(const QString& filename, float x, float y, float z, float s); + bool importVoxels(const QString& filename, const VoxelDetail& destinationVoxel); void nudgeVoxel(const VoxelDetail& sourceVoxel, const glm::vec3& nudgeVec); void nudgeVoxel(float x, float y, float z, float s, const glm::vec3& nudgeVec); + + bool importModels(const QString& filename); + bool exportModels(const QString& filename, float x, float y, float z, float s); + void pasteModels(float x, float y, float z, float s); }; #endif // hifi_ClipboardScriptingInterface_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 827f66c8d5..7cf37b4424 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -67,6 +67,15 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin return retVal; } +QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { + QScriptValue retVal; + QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QScriptValue, retVal), + Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter), + Q_ARG(QFileDialog::AcceptMode, QFileDialog::AcceptSave)); + return retVal; +} + QScriptValue WindowScriptingInterface::s3Browse(const QString& nameFilter) { QScriptValue retVal; QMetaObject::invokeMethod(this, "showS3Browse", Qt::BlockingQueuedConnection, @@ -91,14 +100,52 @@ QScriptValue WindowScriptingInterface::showConfirm(const QString& message) { return QScriptValue(response == QMessageBox::Yes); } +void WindowScriptingInterface::chooseDirectory() { + QPushButton* button = reinterpret_cast(sender()); + + QString title = button->property("title").toString(); + QString path = button->property("path").toString(); + QRegExp displayAs = button->property("displayAs").toRegExp(); + QRegExp validateAs = button->property("validateAs").toRegExp(); + QString errorMessage = button->property("errorMessage").toString(); + + QString directory = QFileDialog::getExistingDirectory(button, title, path); + if (directory.isEmpty()) { + return; + } + + if (!validateAs.exactMatch(directory)) { + QMessageBox::warning(NULL, "Invalid Directory", errorMessage); + return; + } + + button->setProperty("path", directory); + + displayAs.indexIn(directory); + QString buttonText = displayAs.cap(1) != "" ? displayAs.cap(1) : "."; + button->setText(buttonText); +} + +QString WindowScriptingInterface::jsRegExp2QtRegExp(QString string) { + // Converts string representation of RegExp from JavaScript format to Qt format. + return string.mid(1, string.length() - 2) // No enclosing slashes. + .replace("\\/", "/"); // No escaping of forward slash. +} + /// Display a form layout with an edit box /// \param const QString& title title to display -/// \param const QScriptValue form to display (array containing labels and values) -/// \return QScriptValue result form (unchanged is dialog canceled) +/// \param const QScriptValue form to display as an array of objects: +/// - label, value +/// - label, directory, title, display regexp, validate regexp, error message +/// - button ("Cancel") +/// \return QScriptValue `true` if 'OK' was clicked, `false` otherwise QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptValue form) { + if (form.isArray() && form.property("length").toInt32() > 0) { QDialog* editDialog = new QDialog(Application::getInstance()->getWindow()); editDialog->setWindowTitle(title); + + bool cancelButton = false; QVBoxLayout* layout = new QVBoxLayout(); editDialog->setLayout(layout); @@ -118,44 +165,104 @@ QScriptValue WindowScriptingInterface::showForm(const QString& title, QScriptVal area->setWidget(container); QVector edits; + QVector directories; for (int i = 0; i < form.property("length").toInt32(); ++i) { QScriptValue item = form.property(i); - edits.push_back(new QLineEdit(item.property("value").toString())); - formLayout->addRow(item.property("label").toString(), edits.back()); + + if (item.property("button").toString() != "") { + cancelButton = cancelButton || item.property("button").toString().toLower() == "cancel"; + + } else if (item.property("directory").toString() != "") { + QString path = item.property("directory").toString(); + QString title = item.property("title").toString(); + if (title == "") { + title = "Choose Directory"; + } + QString displayAsString = item.property("displayAs").toString(); + QRegExp displayAs = QRegExp(displayAsString != "" ? jsRegExp2QtRegExp(displayAsString) : "^(.*)$"); + QString validateAsString = item.property("validateAs").toString(); + QRegExp validateAs = QRegExp(validateAsString != "" ? jsRegExp2QtRegExp(validateAsString) : ".*"); + QString errorMessage = item.property("errorMessage").toString(); + if (errorMessage == "") { + errorMessage = "Invalid directory"; + } + + QPushButton* directory = new QPushButton(displayAs.cap(1)); + directory->setProperty("title", title); + directory->setProperty("path", path); + directory->setProperty("displayAs", displayAs); + directory->setProperty("validateAs", validateAs); + directory->setProperty("errorMessage", errorMessage); + displayAs.indexIn(path); + directory->setText(displayAs.cap(1) != "" ? displayAs.cap(1) : "."); + + directory->setMinimumWidth(200); + directories.push_back(directory); + + formLayout->addRow(new QLabel(item.property("label").toString()), directory); + connect(directory, SIGNAL(clicked(bool)), SLOT(chooseDirectory())); + + } else { + QLineEdit* edit = new QLineEdit(item.property("value").toString()); + edit->setMinimumWidth(200); + edits.push_back(edit); + formLayout->addRow(new QLabel(item.property("label").toString()), edit); + } } - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok); + + QDialogButtonBox* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok + | (cancelButton ? QDialogButtonBox::Cancel : QDialogButtonBox::NoButton) + ); connect(buttons, SIGNAL(accepted()), editDialog, SLOT(accept())); + connect(buttons, SIGNAL(rejected()), editDialog, SLOT(reject())); layout->addWidget(buttons); - if (editDialog->exec() == QDialog::Accepted) { + int result = editDialog->exec(); + if (result == QDialog::Accepted) { + int e = -1; + int d = -1; for (int i = 0; i < form.property("length").toInt32(); ++i) { QScriptValue item = form.property(i); QScriptValue value = item.property("value"); - bool ok = true; - if (value.isNumber()) { - value = edits.at(i)->text().toDouble(&ok); - } else if (value.isString()) { - value = edits.at(i)->text(); - } else if (value.isBool()) { - if (edits.at(i)->text() == "true") { - value = true; - } else if (edits.at(i)->text() == "false") { - value = false; - } else { - ok = false; - } - } - if (ok) { - item.setProperty("value", value); + + if (item.property("button").toString() != "") { + // Nothing to do + } else if (item.property("directory").toString() != "") { + d += 1; + value = directories.at(d)->property("path").toString(); + item.setProperty("directory", value); form.setProperty(i, item); + } else { + e += 1; + bool ok = true; + if (value.isNumber()) { + value = edits.at(e)->text().toDouble(&ok); + } else if (value.isString()) { + value = edits.at(e)->text(); + } else if (value.isBool()) { + if (edits.at(e)->text() == "true") { + value = true; + } else if (edits.at(e)->text() == "false") { + value = false; + } else { + ok = false; + } + } + if (ok) { + item.setProperty("value", value); + form.setProperty(i, item); + } } } } delete editDialog; + + return (result == QDialog::Accepted); } - return form; + return false; } /// Display a prompt with a text box @@ -182,18 +289,26 @@ QScriptValue WindowScriptingInterface::showPrompt(const QString& message, const /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` -QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter) { +QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter, + QFileDialog::AcceptMode acceptMode) { // On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus // filename if the directory is valid. QString path = ""; QFileInfo fileInfo = QFileInfo(directory); + qDebug() << "File: " << directory << fileInfo.isFile(); if (fileInfo.isDir()) { fileInfo.setFile(directory, "__HIFI_INVALID_FILE__"); path = fileInfo.filePath(); } QFileDialog fileDialog(Application::getInstance()->getWindow(), title, path, nameFilter); - fileDialog.setFileMode(QFileDialog::ExistingFile); + fileDialog.setAcceptMode(acceptMode); + qDebug() << "Opening!"; + QUrl fileUrl(directory); + if (acceptMode == QFileDialog::AcceptSave) { + fileDialog.setFileMode(QFileDialog::Directory); + fileDialog.selectFile(fileUrl.fileName()); + } if (fileDialog.exec()) { return QScriptValue(fileDialog.selectedFiles().first()); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 654b048b24..ec7e1b224e 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -31,6 +31,7 @@ public slots: QScriptValue form(const QString& title, QScriptValue array); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue s3Browse(const QString& nameFilter = ""); private slots: @@ -38,11 +39,15 @@ private slots: QScriptValue showConfirm(const QString& message); QScriptValue showForm(const QString& title, QScriptValue form); QScriptValue showPrompt(const QString& message, const QString& defaultText); - QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter); + QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter, + QFileDialog::AcceptMode acceptMode = QFileDialog::AcceptOpen); QScriptValue showS3Browse(const QString& nameFilter); + void chooseDirectory(); private: WindowScriptingInterface(); + + QString jsRegExp2QtRegExp(QString string); }; #endif // hifi_WindowScriptingInterface_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index c49873b5f5..7dcb50c314 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -57,6 +57,9 @@ ApplicationOverlay::~ApplicationOverlay() { const float WHITE_TEXT[] = { 0.93f, 0.93f, 0.93f }; const float RETICLE_COLOR[] = { 0.0f, 198.0f / 255.0f, 244.0f / 255.0f }; +const float CONNECTION_STATUS_BORDER_COLOR[] = { 1.0f, 0.0f, 0.0f }; +const float CONNECTION_STATUS_BORDER_LINE_WIDTH = 4.0f; + // Renders the overlays either to a texture or to the screen void ApplicationOverlay::renderOverlay(bool renderToTexture) { @@ -115,6 +118,8 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { renderPointers(); + renderDomainConnectionStatusBorder(); + glPopMatrix(); @@ -1234,6 +1239,30 @@ void ApplicationOverlay::renderTexturedHemisphere() { } +void ApplicationOverlay::renderDomainConnectionStatusBorder() { + NodeList* nodeList = NodeList::getInstance(); + + if (nodeList && !nodeList->getDomainHandler().isConnected()) { + QGLWidget* glWidget = Application::getInstance()->getGLWidget(); + int right = glWidget->width(); + int bottom = glWidget->height(); + + glColor3f(CONNECTION_STATUS_BORDER_COLOR[0], + CONNECTION_STATUS_BORDER_COLOR[1], + CONNECTION_STATUS_BORDER_COLOR[2]); + glLineWidth(CONNECTION_STATUS_BORDER_LINE_WIDTH); + + glBegin(GL_LINE_LOOP); + + glVertex2i(0, 0); + glVertex2i(0, bottom); + glVertex2i(right, bottom); + glVertex2i(right, 0); + + glEnd(); + } +} + QOpenGLFramebufferObject* ApplicationOverlay::getFramebufferObject() { QSize size = Application::getInstance()->getGLWidget()->size(); if (!_framebufferObject || _framebufferObject->size() != size) { diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index 0c2ccc7b21..a493f6cd1b 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -56,6 +56,7 @@ private: void renderAudioMeter(); void renderStatsAndLogs(); void renderTexturedHemisphere(); + void renderDomainConnectionStatusBorder(); QOpenGLFramebufferObject* _framebufferObject; float _trailingAudioLoudness; @@ -76,4 +77,4 @@ private: GLuint _crosshairTexture; }; -#endif // hifi_ApplicationOverlay_h \ No newline at end of file +#endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/MetavoxelEditor.cpp b/interface/src/ui/MetavoxelEditor.cpp index 1f0c2498c5..b7057532fb 100644 --- a/interface/src/ui/MetavoxelEditor.cpp +++ b/interface/src/ui/MetavoxelEditor.cpp @@ -15,21 +15,24 @@ #include #include #include +#include #include #include #include #include -#include +#include #include #include #include #include #include +#include #include #include #include #include +#include #include "Application.h" #include "MetavoxelEditor.h" @@ -113,6 +116,10 @@ MetavoxelEditor::MetavoxelEditor() : addTool(new RemoveSpannerTool(this)); addTool(new ClearSpannersTool(this)); addTool(new SetSpannerTool(this)); + addTool(new ImportHeightfieldTool(this)); + addTool(new EraseHeightfieldTool(this)); + addTool(new HeightfieldHeightBrushTool(this)); + addTool(new HeightfieldColorBrushTool(this)); updateAttributes(); @@ -891,3 +898,260 @@ void SetSpannerTool::applyEdit(const AttributePointer& attribute, const SharedOb QThreadPool::globalInstance()->start(new Voxelizer(size, cellBounds, spannerData->getVoxelizationGranularity(), directionImages)); } + +HeightfieldTool::HeightfieldTool(MetavoxelEditor* editor, const QString& name) : + MetavoxelTool(editor, name, false) { + + QWidget* widget = new QWidget(); + widget->setLayout(_form = new QFormLayout()); + layout()->addWidget(widget); + + _form->addRow("Translation:", _translation = new Vec3Editor(widget)); + _form->addRow("Scale:", _scale = new QDoubleSpinBox()); + _scale->setMinimum(-FLT_MAX); + _scale->setMaximum(FLT_MAX); + _scale->setPrefix("2^"); + _scale->setValue(3.0); + + QPushButton* applyButton = new QPushButton("Apply"); + layout()->addWidget(applyButton); + connect(applyButton, &QAbstractButton::clicked, this, &HeightfieldTool::apply); +} + +bool HeightfieldTool::appliesTo(const AttributePointer& attribute) const { + return attribute->inherits("HeightfieldAttribute"); +} + +void HeightfieldTool::render() { + float scale = pow(2.0, _scale->value()); + _translation->setSingleStep(scale); + glm::vec3 quantizedTranslation = scale * glm::floor(_translation->getValue() / scale); + _translation->setValue(quantizedTranslation); +} + +ImportHeightfieldTool::ImportHeightfieldTool(MetavoxelEditor* editor) : + HeightfieldTool(editor, "Import Heightfield") { + + _form->addRow("Block Size:", _blockSize = new QSpinBox()); + _blockSize->setPrefix("2^"); + _blockSize->setMinimum(1); + _blockSize->setValue(5); + + connect(_blockSize, static_cast(&QSpinBox::valueChanged), this, + &ImportHeightfieldTool::updatePreview); + _form->addRow("Height:", _height = new QPushButton()); + connect(_height, &QAbstractButton::clicked, this, &ImportHeightfieldTool::selectHeightFile); + _form->addRow("Color:", _color = new QPushButton()); + connect(_color, &QAbstractButton::clicked, this, &ImportHeightfieldTool::selectColorFile); +} + +void ImportHeightfieldTool::render() { + HeightfieldTool::render(); + _preview.render(_translation->getValue(), _translation->getSingleStep()); +} + +void ImportHeightfieldTool::apply() { + float scale = _translation->getSingleStep(); + foreach (const BufferDataPointer& bufferData, _preview.getBuffers()) { + HeightfieldBuffer* buffer = static_cast(bufferData.data()); + MetavoxelData data; + data.setSize(scale); + HeightfieldDataPointer heightPointer(new HeightfieldData(buffer->getUnextendedHeight())); + data.setRoot(AttributeRegistry::getInstance()->getHeightfieldAttribute(), new MetavoxelNode(AttributeValue( + AttributeRegistry::getInstance()->getHeightfieldAttribute(), encodeInline(heightPointer)))); + if (!buffer->getColor().isEmpty()) { + HeightfieldDataPointer colorPointer(new HeightfieldData(buffer->getUnextendedColor())); + data.setRoot(AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), new MetavoxelNode(AttributeValue( + AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), encodeInline(colorPointer)))); + } + MetavoxelEditMessage message = { QVariant::fromValue(SetDataEdit( + _translation->getValue() + buffer->getTranslation() * scale, data)) }; + Application::getInstance()->getMetavoxels()->applyEdit(message, true); + } +} + +void ImportHeightfieldTool::selectHeightFile() { + QString filename = QFileDialog::getOpenFileName(this, "Select Height Image", QString(), "Images (*.png *.jpg)"); + if (filename.isNull()) { + return; + } + if (!_heightImage.load(filename)) { + QMessageBox::warning(this, "Invalid Image", "The selected image could not be read."); + return; + } + _heightImage = _heightImage.convertToFormat(QImage::Format_RGB888); + _height->setText(filename); + updatePreview(); +} + +void ImportHeightfieldTool::selectColorFile() { + QString filename = QFileDialog::getOpenFileName(this, "Select Color Image", QString(), "Images (*.png *.jpg)"); + if (filename.isNull()) { + return; + } + if (!_colorImage.load(filename)) { + QMessageBox::warning(this, "Invalid Image", "The selected image could not be read."); + return; + } + _colorImage = _colorImage.convertToFormat(QImage::Format_RGB888); + _color->setText(filename); + updatePreview(); +} + +void ImportHeightfieldTool::updatePreview() { + QVector buffers; + if (_heightImage.width() > 0 && _heightImage.height() > 0) { + float z = 0.0f; + int blockSize = pow(2.0, _blockSize->value()); + int heightSize = blockSize + HeightfieldBuffer::HEIGHT_EXTENSION; + int colorSize = blockSize + HeightfieldBuffer::SHARED_EDGE; + for (int i = 0; i < _heightImage.height(); i += blockSize, z++) { + float x = 0.0f; + for (int j = 0; j < _heightImage.width(); j += blockSize, x++) { + QByteArray height(heightSize * heightSize, 0); + int extendedI = qMax(i - HeightfieldBuffer::HEIGHT_BORDER, 0); + int extendedJ = qMax(j - HeightfieldBuffer::HEIGHT_BORDER, 0); + int offsetY = extendedI - i + HeightfieldBuffer::HEIGHT_BORDER; + int offsetX = extendedJ - j + HeightfieldBuffer::HEIGHT_BORDER; + int rows = qMin(heightSize - offsetY, _heightImage.height() - extendedI); + int columns = qMin(heightSize - offsetX, _heightImage.width() - extendedJ); + for (int y = 0; y < rows; y++) { + uchar* src = _heightImage.scanLine(extendedI + y) + extendedJ * HeightfieldData::COLOR_BYTES; + char* dest = height.data() + (y + offsetY) * heightSize + offsetX; + for (int x = 0; x < columns; x++) { + *dest++ = *src; + src += HeightfieldData::COLOR_BYTES; + } + } + QByteArray color; + if (!_colorImage.isNull()) { + color = QByteArray(colorSize * colorSize * HeightfieldData::COLOR_BYTES, 0); + rows = qMax(0, qMin(colorSize, _colorImage.height() - i)); + columns = qMax(0, qMin(colorSize, _colorImage.width() - j)); + for (int y = 0; y < rows; y++) { + memcpy(color.data() + y * colorSize * HeightfieldData::COLOR_BYTES, + _colorImage.scanLine(i + y) + j * HeightfieldData::COLOR_BYTES, + columns * HeightfieldData::COLOR_BYTES); + } + } + buffers.append(BufferDataPointer(new HeightfieldBuffer(glm::vec3(x, 0.0f, z), 1.0f, height, color))); + } + } + } + _preview.setBuffers(buffers); +} + +EraseHeightfieldTool::EraseHeightfieldTool(MetavoxelEditor* editor) : + HeightfieldTool(editor, "Erase Heightfield") { + + _form->addRow("Width:", _width = new QSpinBox()); + _width->setMinimum(1); + _width->setMaximum(INT_MAX); + _form->addRow("Length:", _length = new QSpinBox()); + _length->setMinimum(1); + _length->setMaximum(INT_MAX); +} + +void EraseHeightfieldTool::render() { + HeightfieldTool::render(); + + glColor3f(1.0f, 0.0f, 0.0f); + glLineWidth(4.0f); + + glPushMatrix(); + glm::vec3 translation = _translation->getValue(); + glTranslatef(translation.x, translation.y, translation.z); + float scale = _translation->getSingleStep(); + glScalef(scale * _width->value(), scale, scale * _length->value()); + glTranslatef(0.5f, 0.5f, 0.5f); + + glutWireCube(1.0); + + glPopMatrix(); + + glLineWidth(1.0f); +} + +void EraseHeightfieldTool::apply() { + // clear the heightfield + float scale = _translation->getSingleStep(); + BoxSetEdit edit(Box(_translation->getValue(), _translation->getValue() + + glm::vec3(_width->value() * scale, scale, _length->value() * scale)), scale, + OwnedAttributeValue(AttributeRegistry::getInstance()->getHeightfieldAttribute())); + MetavoxelEditMessage message = { QVariant::fromValue(edit) }; + Application::getInstance()->getMetavoxels()->applyEdit(message, true); + + // and the color + edit.value = OwnedAttributeValue(AttributeRegistry::getInstance()->getHeightfieldColorAttribute()); + message.edit = QVariant::fromValue(edit); + Application::getInstance()->getMetavoxels()->applyEdit(message, true); +} + +HeightfieldBrushTool::HeightfieldBrushTool(MetavoxelEditor* editor, const QString& name) : + MetavoxelTool(editor, name, false) { + + QWidget* widget = new QWidget(); + widget->setLayout(_form = new QFormLayout()); + layout()->addWidget(widget); + + _form->addRow("Radius:", _radius = new QDoubleSpinBox()); + _radius->setSingleStep(0.01); + _radius->setMaximum(FLT_MAX); + _radius->setValue(1.0); +} + +void HeightfieldBrushTool::render() { + if (Application::getInstance()->isMouseHidden()) { + return; + } + + // find the intersection with the heightfield + glm::vec3 origin = Application::getInstance()->getMouseRayOrigin(); + glm::vec3 direction = Application::getInstance()->getMouseRayDirection(); + + float distance; + if (!Application::getInstance()->getMetavoxels()->findFirstRayHeightfieldIntersection(origin, direction, distance)) { + return; + } + Application::getInstance()->getMetavoxels()->renderHeightfieldCursor( + _position = origin + distance * direction, _radius->value()); +} + +bool HeightfieldBrushTool::eventFilter(QObject* watched, QEvent* event) { + if (event->type() == QEvent::Wheel) { + float angle = static_cast(event)->angleDelta().y(); + const float ANGLE_SCALE = 1.0f / 1000.0f; + _radius->setValue(_radius->value() * glm::pow(2.0f, angle * ANGLE_SCALE)); + return true; + + } else if (event->type() == QEvent::MouseButtonPress) { + MetavoxelEditMessage message = { createEdit(static_cast(event)->button() == Qt::RightButton) }; + Application::getInstance()->getMetavoxels()->applyEdit(message, true); + return true; + } + return false; +} + +HeightfieldHeightBrushTool::HeightfieldHeightBrushTool(MetavoxelEditor* editor) : + HeightfieldBrushTool(editor, "Height Brush") { + + _form->addRow("Height:", _height = new QDoubleSpinBox()); + _height->setMinimum(-FLT_MAX); + _height->setMaximum(FLT_MAX); + _height->setValue(1.0); +} + +QVariant HeightfieldHeightBrushTool::createEdit(bool alternate) { + return QVariant::fromValue(PaintHeightfieldHeightEdit(_position, _radius->value(), + alternate ? -_height->value() : _height->value())); +} + +HeightfieldColorBrushTool::HeightfieldColorBrushTool(MetavoxelEditor* editor) : + HeightfieldBrushTool(editor, "Color Brush") { + + _form->addRow("Color:", _color = new QColorEditor(this)); +} + +QVariant HeightfieldColorBrushTool::createEdit(bool alternate) { + return QVariant::fromValue(PaintHeightfieldColorEdit(_position, _radius->value(), _color->getColor())); +} diff --git a/interface/src/ui/MetavoxelEditor.h b/interface/src/ui/MetavoxelEditor.h index 79ffd1e64c..87d95a6927 100644 --- a/interface/src/ui/MetavoxelEditor.h +++ b/interface/src/ui/MetavoxelEditor.h @@ -15,16 +15,20 @@ #include #include +#include "MetavoxelSystem.h" #include "renderer/ProgramObject.h" +class QColorEditor; class QComboBox; class QDoubleSpinBox; class QGroupBox; class QListWidget; class QPushButton; class QScrollArea; +class QSpinBox; class MetavoxelTool; +class Vec3Editor; /// Allows editing metavoxels. class MetavoxelEditor : public QWidget { @@ -223,4 +227,136 @@ protected: virtual void applyEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner); }; +/// Base class for heightfield tools. +class HeightfieldTool : public MetavoxelTool { + Q_OBJECT + +public: + + HeightfieldTool(MetavoxelEditor* editor, const QString& name); + + virtual bool appliesTo(const AttributePointer& attribute) const; + + virtual void render(); + +protected slots: + + virtual void apply() = 0; + +protected: + + QFormLayout* _form; + Vec3Editor* _translation; + QDoubleSpinBox* _scale; +}; + +/// Allows importing a heightfield. +class ImportHeightfieldTool : public HeightfieldTool { + Q_OBJECT + +public: + + ImportHeightfieldTool(MetavoxelEditor* editor); + + virtual void render(); + +protected: + + virtual void apply(); + +private slots: + + void selectHeightFile(); + void selectColorFile(); + void updatePreview(); + +private: + + QSpinBox* _blockSize; + + QPushButton* _height; + QPushButton* _color; + + QImage _heightImage; + QImage _colorImage; + + HeightfieldPreview _preview; +}; + +/// Allows clearing heighfield blocks. +class EraseHeightfieldTool : public HeightfieldTool { + Q_OBJECT + +public: + + EraseHeightfieldTool(MetavoxelEditor* editor); + + virtual void render(); + +protected: + + virtual void apply(); + +private: + + QSpinBox* _width; + QSpinBox* _length; +}; + +/// Base class for tools that allow painting on heightfields. +class HeightfieldBrushTool : public MetavoxelTool { + Q_OBJECT + +public: + + HeightfieldBrushTool(MetavoxelEditor* editor, const QString& name); + + virtual void render(); + + virtual bool eventFilter(QObject* watched, QEvent* event); + +protected: + + virtual QVariant createEdit(bool alternate) = 0; + + QFormLayout* _form; + QDoubleSpinBox* _radius; + + glm::vec3 _position; +}; + +/// Allows raising or lowering parts of the heightfield. +class HeightfieldHeightBrushTool : public HeightfieldBrushTool { + Q_OBJECT + +public: + + HeightfieldHeightBrushTool(MetavoxelEditor* editor); + +protected: + + virtual QVariant createEdit(bool alternate); + +private: + + QDoubleSpinBox* _height; +}; + +/// Allows coloring parts of the heightfield. +class HeightfieldColorBrushTool : public HeightfieldBrushTool { + Q_OBJECT + +public: + + HeightfieldColorBrushTool(MetavoxelEditor* editor); + +protected: + + virtual QVariant createEdit(bool alternate); + +private: + + QColorEditor* _color; +}; + #endif // hifi_MetavoxelEditor_h diff --git a/interface/src/ui/OctreeStatsDialog.cpp b/interface/src/ui/OctreeStatsDialog.cpp index afa799815f..e548d5c276 100644 --- a/interface/src/ui/OctreeStatsDialog.cpp +++ b/interface/src/ui/OctreeStatsDialog.cpp @@ -366,13 +366,12 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser QString incomingBytesString = locale.toString((uint)stats.getIncomingBytes()); QString incomingWastedBytesString = locale.toString((uint)stats.getIncomingWastedBytes()); const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); - QString incomingOutOfOrderString = locale.toString((uint)seqStats.getNumOutOfOrder()); - QString incomingLateString = locale.toString((uint)seqStats.getNumLate()); - QString incomingUnreasonableString = locale.toString((uint)seqStats.getNumUnreasonable()); - QString incomingEarlyString = locale.toString((uint)seqStats.getNumEarly()); - QString incomingLikelyLostString = locale.toString((uint)seqStats.getNumLost()); - QString incomingRecovered = locale.toString((uint)seqStats.getNumRecovered()); - QString incomingDuplicateString = locale.toString((uint)seqStats.getNumDuplicate()); + QString incomingOutOfOrderString = locale.toString((uint)seqStats.getOutOfOrder()); + QString incomingLateString = locale.toString((uint)seqStats.getLate()); + QString incomingUnreasonableString = locale.toString((uint)seqStats.getUnreasonable()); + QString incomingEarlyString = locale.toString((uint)seqStats.getEarly()); + QString incomingLikelyLostString = locale.toString((uint)seqStats.getLost()); + QString incomingRecovered = locale.toString((uint)seqStats.getRecovered()); int clockSkewInMS = node->getClockSkewUsec() / (int)USECS_PER_MSEC; QString incomingFlightTimeString = locale.toString((int)stats.getIncomingFlightTimeAverage()); @@ -386,8 +385,7 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser serverDetails << "
" << " Out of Order: " << qPrintable(incomingOutOfOrderString) << "/ Early: " << qPrintable(incomingEarlyString) << "/ Late: " << qPrintable(incomingLateString) << - "/ Unreasonable: " << qPrintable(incomingUnreasonableString) << - "/ Duplicate: " << qPrintable(incomingDuplicateString); + "/ Unreasonable: " << qPrintable(incomingUnreasonableString); serverDetails << "
" << " Average Flight Time: " << qPrintable(incomingFlightTimeString) << " msecs"; diff --git a/interface/src/ui/ImportDialog.cpp b/interface/src/ui/VoxelImportDialog.cpp similarity index 77% rename from interface/src/ui/ImportDialog.cpp rename to interface/src/ui/VoxelImportDialog.cpp index 67b89773fb..2d1b71ba7f 100644 --- a/interface/src/ui/ImportDialog.cpp +++ b/interface/src/ui/VoxelImportDialog.cpp @@ -1,5 +1,5 @@ // -// ImportDialog.cpp +// VoxelImportDialog.cpp // interface/src/ui // // Created by Clement Brisset on 8/12/13. @@ -20,7 +20,11 @@ #include "Application.h" -#include "ImportDialog.h" +#include "VoxelImportDialog.h" +#include "voxels/VoxelImporter.h" + +const QString SETTINGS_GROUP_NAME = "VoxelImport"; +const QString IMPORT_DIALOG_SETTINGS_KEY = "VoxelImportDialogSettings"; const QString WINDOW_NAME = QObject::tr("Import Voxels"); const QString IMPORT_BUTTON_NAME = QObject::tr("Import Voxels"); @@ -97,12 +101,14 @@ QString HiFiIconProvider::type(const QFileInfo &info) const { return QFileIconProvider::type(info); } -ImportDialog::ImportDialog(QWidget* parent) : +VoxelImportDialog::VoxelImportDialog(QWidget* parent) : QFileDialog(parent, WINDOW_NAME, DOWNLOAD_LOCATION, NULL), - _progressBar(this), - _importButton(IMPORT_BUTTON_NAME, this), _cancelButton(CANCEL_BUTTON_NAME, this), - _mode(importMode) { + _importButton(IMPORT_BUTTON_NAME, this), + _importer(Application::getInstance()->getVoxelImporter()), + _mode(importMode), + _progressBar(this), + _didImport(false) { setOption(QFileDialog::DontUseNativeDialog, true); setFileMode(QFileDialog::ExistingFile); @@ -113,41 +119,54 @@ ImportDialog::ImportDialog(QWidget* parent) : _progressBar.setRange(0, 100); - connect(&_importButton, SIGNAL(pressed()), SLOT(accept())); - connect(&_cancelButton, SIGNAL(pressed()), SIGNAL(canceled())); + connect(&_importButton, SIGNAL(pressed()), this, SLOT(accept())); + connect(&_cancelButton, SIGNAL(pressed()), this, SLOT(cancel())); connect(this, SIGNAL(currentChanged(QString)), SLOT(saveCurrentFile(QString))); } -void ImportDialog::reset() { - setMode(importMode); - _progressBar.setValue(0); +void VoxelImportDialog::cancel() { + switch (getMode()) { + case importMode: + _importer->cancel(); + close(); + break; + default: + _importer->reset(); + setMode(importMode); + break; + } + emit canceled(); } -void ImportDialog::setMode(dialogMode mode) { +void VoxelImportDialog::saveSettings(QSettings* settings) { + settings->beginGroup(SETTINGS_GROUP_NAME); + settings->setValue(IMPORT_DIALOG_SETTINGS_KEY, saveState()); + settings->endGroup(); +} + +void VoxelImportDialog::loadSettings(QSettings* settings) { + settings->beginGroup(SETTINGS_GROUP_NAME); + restoreState(settings->value(IMPORT_DIALOG_SETTINGS_KEY).toByteArray()); + settings->endGroup(); +} + +bool VoxelImportDialog::prompt() { + reset(); + exec(); + return _didImport; +} + +void VoxelImportDialog::reset() { + setMode(importMode); + _didImport = false; +} + +void VoxelImportDialog::setMode(dialogMode mode) { + dialogMode previousMode = _mode; _mode = mode; switch (_mode) { - case loadingMode: - _importButton.setEnabled(false); - _importButton.setText(LOADING_BUTTON_NAME); - findChild("sidebar")->setEnabled(false); - findChild("treeView")->setEnabled(false); - findChild("backButton")->setEnabled(false); - findChild("forwardButton")->setEnabled(false); - findChild("toParentButton")->setEnabled(false); - break; - case placeMode: - _progressBar.setValue(100); - _importButton.setEnabled(true); - _importButton.setText(PLACE_BUTTON_NAME); - findChild("sidebar")->setEnabled(false); - findChild("treeView")->setEnabled(false); - findChild("backButton")->setEnabled(false); - findChild("forwardButton")->setEnabled(false); - findChild("toParentButton")->setEnabled(false); - break; case importMode: - default: _progressBar.setValue(0); _importButton.setEnabled(true); _importButton.setText(IMPORT_BUTTON_NAME); @@ -157,22 +176,60 @@ void ImportDialog::setMode(dialogMode mode) { findChild("forwardButton")->setEnabled(true); findChild("toParentButton")->setEnabled(true); break; + case loadingMode: + // Connect to VoxelImporter signals + connect(_importer, SIGNAL(importProgress(int)), this, SLOT(updateProgressBar(int))); + connect(_importer, SIGNAL(importDone()), this, SLOT(afterImport())); + + _importButton.setEnabled(false); + _importButton.setText(LOADING_BUTTON_NAME); + findChild("sidebar")->setEnabled(false); + findChild("treeView")->setEnabled(false); + findChild("backButton")->setEnabled(false); + findChild("forwardButton")->setEnabled(false); + findChild("toParentButton")->setEnabled(false); + break; + case finishedMode: + if (previousMode == loadingMode) { + // Disconnect from VoxelImporter signals + disconnect(_importer, SIGNAL(importProgress(int)), this, SLOT(setProgressBarValue(int))); + disconnect(_importer, SIGNAL(importDone()), this, SLOT(afterImport())); + } + setMode(importMode); + break; } } -void ImportDialog::setProgressBarValue(int value) { +void VoxelImportDialog::setProgressBarValue(int value) { _progressBar.setValue(value); } -void ImportDialog::accept() { - emit accepted(); +void VoxelImportDialog::accept() { + if (getMode() == importMode) { + QString filename = getCurrentFile(); + + // If file is invalid we ignore the call + if (!_importer->validImportFile(filename)) { + return; + } + // Let's prepare the dialog window for import + setMode(loadingMode); + + _importer->import(filename); + } } -void ImportDialog::saveCurrentFile(QString filename) { +void VoxelImportDialog::afterImport() { + setMode(finishedMode); + _didImport = true; + close(); +} + +void VoxelImportDialog::saveCurrentFile(QString filename) { _currentFile = QFileInfo(filename).isFile() ? filename : ""; } -void ImportDialog::setLayout() { +void VoxelImportDialog::setLayout() { QGridLayout* gridLayout = (QGridLayout*) layout(); gridLayout->addWidget(&_progressBar, 2, 0, 2, 1); gridLayout->addWidget(&_cancelButton, 2, 1, 2, 1); @@ -258,7 +315,7 @@ void ImportDialog::setLayout() { } -void ImportDialog::setImportTypes() { +void VoxelImportDialog::setImportTypes() { QFile config(Application::resourcesPath() + "config/config.json"); config.open(QFile::ReadOnly | QFile::Text); QJsonDocument document = QJsonDocument::fromJson(config.readAll()); diff --git a/interface/src/ui/ImportDialog.h b/interface/src/ui/VoxelImportDialog.h similarity index 74% rename from interface/src/ui/ImportDialog.h rename to interface/src/ui/VoxelImportDialog.h index 88cfda7a7c..54faf7449a 100644 --- a/interface/src/ui/ImportDialog.h +++ b/interface/src/ui/VoxelImportDialog.h @@ -1,5 +1,5 @@ // -// ImportDialog.h +// VoxelImportDialog.h // interface/src/ui // // Created by Clement Brisset on 8/12/13. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_ImportDialog_h -#define hifi_ImportDialog_h +#ifndef hifi_VoxelImportDialog_h +#define hifi_VoxelImportDialog_h #include "InterfaceConfig.h" @@ -23,6 +23,8 @@ #include +#include "voxels/VoxelImporter.h" + class HiFiIconProvider : public QFileIconProvider { public: HiFiIconProvider(const QHash map) { iconsMap = map; }; @@ -35,39 +37,45 @@ public: enum dialogMode { importMode, loadingMode, - placeMode + finishedMode }; -class ImportDialog : public QFileDialog { +class VoxelImportDialog : public QFileDialog { Q_OBJECT public: - ImportDialog(QWidget* parent = NULL); - void reset(); + VoxelImportDialog(QWidget* parent = NULL); QString getCurrentFile() const { return _currentFile; } dialogMode getMode() const { return _mode; } + void setMode(dialogMode mode); + void reset(); + bool prompt(); + void loadSettings(QSettings* settings); + void saveSettings(QSettings* settings); signals: void canceled(); - -public slots: - void setProgressBarValue(int value); private slots: + void setProgressBarValue(int value); void accept(); + void cancel(); void saveCurrentFile(QString filename); + void afterImport(); private: - QString _currentFile; - QProgressBar _progressBar; - QPushButton _importButton; QPushButton _cancelButton; + QString _currentFile; + QPushButton _importButton; + VoxelImporter* _importer; dialogMode _mode; + QProgressBar _progressBar; + bool _didImport; void setLayout(); void setImportTypes(); }; -#endif // hifi_ImportDialog_h +#endif // hifi_VoxelImportDialog_h diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index e2dcb82454..ffe73f0023 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -11,6 +11,8 @@ #ifndef hifi_Base3DOverlay_h #define hifi_Base3DOverlay_h +#include + #include "Overlay.h" class Base3DOverlay : public Overlay { diff --git a/interface/src/ui/overlays/LocalModelsOverlay.cpp b/interface/src/ui/overlays/LocalModelsOverlay.cpp new file mode 100644 index 0000000000..6bb1d9ce88 --- /dev/null +++ b/interface/src/ui/overlays/LocalModelsOverlay.cpp @@ -0,0 +1,40 @@ +// +// LocalModelsOverlay.cpp +// interface/src/ui/overlays +// +// Created by Ryan Huffman on 07/08/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 "Application.h" + +#include "LocalModelsOverlay.h" + +LocalModelsOverlay::LocalModelsOverlay(ModelTreeRenderer* modelTreeRenderer) : + Volume3DOverlay(), + _modelTreeRenderer(modelTreeRenderer) { +} + +LocalModelsOverlay::~LocalModelsOverlay() { +} + +void LocalModelsOverlay::update(float deltatime) { + _modelTreeRenderer->update(); +} + +void LocalModelsOverlay::render() { + if (_visible) { + glPushMatrix(); { + Application* app = Application::getInstance(); + glm::vec3 oldTranslation = app->getViewMatrixTranslation(); + app->setViewMatrixTranslation(oldTranslation + _position); + + _modelTreeRenderer->render(); + + Application::getInstance()->setViewMatrixTranslation(oldTranslation); + } glPopMatrix(); + } +} diff --git a/interface/src/ui/overlays/LocalModelsOverlay.h b/interface/src/ui/overlays/LocalModelsOverlay.h new file mode 100644 index 0000000000..7c4bffa342 --- /dev/null +++ b/interface/src/ui/overlays/LocalModelsOverlay.h @@ -0,0 +1,32 @@ +// +// LocalModelsOverlay.h +// interface/src/ui/overlays +// +// Created by Ryan Huffman on 07/08/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_LocalModelsOverlay_h +#define hifi_LocalModelsOverlay_h + +#include "models/ModelTreeRenderer.h" + +#include "Volume3DOverlay.h" + +class LocalModelsOverlay : public Volume3DOverlay { + Q_OBJECT +public: + LocalModelsOverlay(ModelTreeRenderer* modelTreeRenderer); + ~LocalModelsOverlay(); + + virtual void update(float deltatime); + virtual void render(); + +private: + ModelTreeRenderer *_modelTreeRenderer; +}; + +#endif // hifi_LocalModelsOverlay_h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 5d16bd78e5..91856d0722 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -14,6 +14,7 @@ #include "Cube3DOverlay.h" #include "ImageOverlay.h" #include "Line3DOverlay.h" +#include "LocalModelsOverlay.h" #include "LocalVoxelsOverlay.h" #include "ModelOverlay.h" #include "Overlays.h" @@ -158,6 +159,12 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope thisOverlay->setProperties(properties); created = true; is3D = true; + } else if (type == "localmodels") { + thisOverlay = new LocalModelsOverlay(Application::getInstance()->getModelClipboardRenderer()); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; } else if (type == "model") { thisOverlay = new ModelOverlay(); thisOverlay->init(_parent); diff --git a/interface/src/voxels/OctreePacketProcessor.cpp b/interface/src/voxels/OctreePacketProcessor.cpp index 66190a5689..ee139ccf1d 100644 --- a/interface/src/voxels/OctreePacketProcessor.cpp +++ b/interface/src/voxels/OctreePacketProcessor.cpp @@ -26,7 +26,7 @@ void OctreePacketProcessor::processPacket(const SharedNodePointer& sendingNode, if (packetsToProcessCount() > WAY_BEHIND && Application::getInstance()->getLogger()->extraDebugging()) { qDebug("OctreePacketProcessor::processPacket() packets to process=%d", packetsToProcessCount()); } - ssize_t messageLength = mutablePacket.size(); + int messageLength = mutablePacket.size(); Application* app = Application::getInstance(); bool wasStatsPacket = false; diff --git a/interface/src/voxels/VoxelImporter.cpp b/interface/src/voxels/VoxelImporter.cpp index f7d5562c06..b713813fb0 100644 --- a/interface/src/voxels/VoxelImporter.cpp +++ b/interface/src/voxels/VoxelImporter.cpp @@ -20,8 +20,7 @@ #include "voxels/VoxelImporter.h" -const QString SETTINGS_GROUP_NAME = "VoxelImport"; -const QString IMPORT_DIALOG_SETTINGS_KEY = "ImportDialogSettings"; +const QStringList SUPPORTED_EXTENSIONS = QStringList() << "png" << "svo" << "schematic"; class ImportTask : public QObject, public QRunnable { public: @@ -32,104 +31,46 @@ private: QString _filename; }; -VoxelImporter::VoxelImporter(QWidget* parent) : - QObject(parent), +VoxelImporter::VoxelImporter() : _voxelTree(true), - _importDialog(parent), - _task(NULL), - _didImport(false) + _task(NULL) { LocalVoxelsList::getInstance()->addPersistantTree(IMPORT_TREE_NAME, &_voxelTree); - connect(&_voxelTree, SIGNAL(importProgress(int)), &_importDialog, SLOT(setProgressBarValue(int))); - connect(&_importDialog, SIGNAL(canceled()), this, SLOT(cancel())); - connect(&_importDialog, SIGNAL(accepted()), this, SLOT(import())); -} - -void VoxelImporter::saveSettings(QSettings* settings) { - settings->beginGroup(SETTINGS_GROUP_NAME); - settings->setValue(IMPORT_DIALOG_SETTINGS_KEY, _importDialog.saveState()); - settings->endGroup(); -} - -void VoxelImporter::loadSettings(QSettings* settings) { - settings->beginGroup(SETTINGS_GROUP_NAME); - _importDialog.restoreState(settings->value(IMPORT_DIALOG_SETTINGS_KEY).toByteArray()); - settings->endGroup(); + connect(&_voxelTree, SIGNAL(importProgress(int)), this, SIGNAL(importProgress(int))); } VoxelImporter::~VoxelImporter() { cleanupTask(); } +void VoxelImporter::cancel() { + if (_task) { + disconnect(_task, 0, 0, 0); + } + reset(); +} + void VoxelImporter::reset() { _voxelTree.eraseAllOctreeElements(); - _importDialog.reset(); - cleanupTask(); } -int VoxelImporter::exec() { - reset(); - _importDialog.exec(); - - if (!_didImport) { - // if the import is rejected, we make sure to cleanup before leaving +void VoxelImporter::import(const QString& filename) { + // If present, abort existing import + if (_task) { cleanupTask(); - return 1; - } else { - _didImport = false; - return 0; } -} -void VoxelImporter::import() { - switch (_importDialog.getMode()) { - case loadingMode: - _importDialog.setMode(placeMode); - return; - case placeMode: - // Means the user chose to import - _didImport = true; - _importDialog.close(); - return; - case importMode: - default: - QString filename = _importDialog.getCurrentFile(); - // if it's not a file, we ignore the call - if (!QFileInfo(filename).isFile()) { - return; - } - - // Let's prepare the dialog window for import - _importDialog.setMode(loadingMode); - - // If not already done, we switch to the local tree - if (Application::getInstance()->getSharedVoxelSystem()->getTree() != &_voxelTree) { - Application::getInstance()->getSharedVoxelSystem()->changeTree(&_voxelTree); - } - - // Creation and launch of the import task on the thread pool - _task = new ImportTask(filename); - connect(_task, SIGNAL(destroyed()), SLOT(import())); - QThreadPool::globalInstance()->start(_task); - break; + // If not already done, we switch to the local tree + if (Application::getInstance()->getSharedVoxelSystem()->getTree() != &_voxelTree) { + Application::getInstance()->getSharedVoxelSystem()->changeTree(&_voxelTree); } -} -void VoxelImporter::cancel() { - switch (_importDialog.getMode()) { - case loadingMode: - disconnect(_task, 0, 0, 0); - cleanupTask(); - case placeMode: - _importDialog.setMode(importMode); - break; - case importMode: - default: - _importDialog.close(); - break; - } + // Creation and launch of the import task on the thread pool + _task = new ImportTask(filename); + connect(_task, SIGNAL(destroyed()), SLOT(finishImport())); + QThreadPool::globalInstance()->start(_task); } void VoxelImporter::cleanupTask() { @@ -140,6 +81,16 @@ void VoxelImporter::cleanupTask() { } } +void VoxelImporter::finishImport() { + cleanupTask(); + emit importDone(); +} + +bool VoxelImporter::validImportFile(const QString& filename) { + QFileInfo fileInfo = QFileInfo(filename); + return fileInfo.isFile() && SUPPORTED_EXTENSIONS.indexOf(fileInfo.suffix().toLower()) != -1; +} + ImportTask::ImportTask(const QString &filename) : _filename(filename) { @@ -151,7 +102,7 @@ void ImportTask::run() { // We start by cleaning up the shared voxel system just in case voxelSystem->killLocalVoxels(); - // Then we call the righ method for the job + // Then we call the right method for the job if (_filename.endsWith(".png", Qt::CaseInsensitive)) { voxelSystem->getTree()->readFromSquareARGB32Pixels(_filename.toLocal8Bit().data()); } else if (_filename.endsWith(".svo", Qt::CaseInsensitive)) { @@ -163,6 +114,6 @@ void ImportTask::run() { qDebug() << "[ERROR] Invalid file extension." << endl; } - // Here we reaverage the tree so that he is ready for preview + // Here we reaverage the tree so that it is ready for preview voxelSystem->getTree()->reaverageOctreeElements(); } diff --git a/interface/src/voxels/VoxelImporter.h b/interface/src/voxels/VoxelImporter.h index 7da89c5a11..21ebbeea2e 100644 --- a/interface/src/voxels/VoxelImporter.h +++ b/interface/src/voxels/VoxelImporter.h @@ -14,8 +14,8 @@ #include #include +#include -#include "ui/ImportDialog.h" #include "voxels/VoxelSystem.h" class ImportTask; @@ -23,28 +23,29 @@ class ImportTask; class VoxelImporter : public QObject { Q_OBJECT public: - VoxelImporter(QWidget* parent = NULL); + VoxelImporter(); ~VoxelImporter(); void reset(); - void loadSettings(QSettings* settings); - void saveSettings(QSettings* settings); - + void cancel(); VoxelTree* getVoxelTree() { return &_voxelTree; } + bool validImportFile(const QString& filename); public slots: - int exec(); - void import(); - void cancel(); + void import(const QString& filename); + +signals: + void importDone(); + void importProgress(int); private: VoxelTree _voxelTree; - ImportDialog _importDialog; - ImportTask* _task; - bool _didImport; void cleanupTask(); + +private slots: + void finishImport(); }; #endif // hifi_VoxelImporter_h diff --git a/libraries/animation/CMakeLists.txt b/libraries/animation/CMakeLists.txt index b1dc59fff8..3f65c1ab6c 100644 --- a/libraries/animation/CMakeLists.txt +++ b/libraries/animation/CMakeLists.txt @@ -1,29 +1,9 @@ -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) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network Script) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +link_hifi_libraries(shared fbx) -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) - -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/audio/CMakeLists.txt b/libraries/audio/CMakeLists.txt index 2ad8a4b0bc..6ade1fc423 100644 --- a/libraries/audio/CMakeLists.txt +++ b/libraries/audio/CMakeLists.txt @@ -1,26 +1,11 @@ -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 audio) -find_package(Qt5 COMPONENTS Script) -include_directories(SYSTEM "${Qt5Script_INCLUDE_DIRS}") +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network) -# set up the external glm library -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +include_glm() -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +link_hifi_libraries(networking shared) -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/audio/src/AudioFilter.cpp b/libraries/audio/src/AudioFilter.cpp new file mode 100644 index 0000000000..28e7716578 --- /dev/null +++ b/libraries/audio/src/AudioFilter.cpp @@ -0,0 +1,26 @@ +// +// AudioFilter.cpp +// hifi +// +// Created by Craig Hansen-Sturm on 8/10/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 +#include "AudioRingBuffer.h" +#include "AudioFilter.h" + +template<> +AudioFilterPEQ3::FilterParameter AudioFilterPEQ3::_profiles[ AudioFilterPEQ3::_profileCount ][ AudioFilterPEQ3::_filterCount ] = { + + // Freq Gain Q Freq Gain Q Freq Gain Q + { { 300.0f, 1.0f, 1.0f }, { 1000.0f, 1.0f, 1.0f }, { 4000.0f, 1.0f, 1.0f } }, // flat response (default) + { { 300.0f, 1.0f, 1.0f }, { 1000.0f, 1.0f, 1.0f }, { 4000.0f, 0.1f, 1.0f } }, // treble cut + { { 300.0f, 0.1f, 1.0f }, { 1000.0f, 1.0f, 1.0f }, { 4000.0f, 1.0f, 1.0f } }, // bass cut + { { 300.0f, 1.5f, 0.71f }, { 1000.0f, 0.5f, 1.0f }, { 4000.0f, 1.50f, 0.71f } } // smiley curve +}; diff --git a/libraries/audio/src/AudioFilter.h b/libraries/audio/src/AudioFilter.h new file mode 100644 index 0000000000..0f3ec06f64 --- /dev/null +++ b/libraries/audio/src/AudioFilter.h @@ -0,0 +1,298 @@ +// +// AudioFilter.h +// hifi +// +// Created by Craig Hansen-Sturm on 8/9/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_AudioFilter_h +#define hifi_AudioFilter_h + +//////////////////////////////////////////////////////////////////////////////////////////// +// Implements a standard biquad filter in "Direct Form 1" +// Reference http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt +// +class AudioBiquad { + + // + // private data + // + float _a0; // gain + float _a1; // feedforward 1 + float _a2; // feedforward 2 + float _b1; // feedback 1 + float _b2; // feedback 2 + + float _xm1; + float _xm2; + float _ym1; + float _ym2; + +public: + + // + // ctor/dtor + // + AudioBiquad() + : _xm1(0.) + , _xm2(0.) + , _ym1(0.) + , _ym2(0.) { + setParameters(0.,0.,0.,0.,0.); + } + + ~AudioBiquad() { + } + + // + // public interface + // + void setParameters( const float a0, const float a1, const float a2, const float b1, const float b2 ) { + _a0 = a0; _a1 = a1; _a2 = a2; _b1 = b1; _b2 = b2; + } + + void getParameters( float& a0, float& a1, float& a2, float& b1, float& b2 ) { + a0 = _a0; a1 = _a1; a2 = _a2; b1 = _b1; b2 = _b2; + } + + void render( const float* in, float* out, const int frames) { + + float x; + float y; + + for (int i = 0; i < frames; ++i) { + + x = *in++; + + // biquad + y = (_a0 * x) + + (_a1 * _xm1) + + (_a2 * _xm2) + - (_b1 * _ym1) + - (_b2 * _ym2); + + // update delay line + _xm2 = _xm1; + _xm1 = x; + _ym2 = _ym1; + _ym1 = y; + + *out++ = y; + } + } + + void reset() { + _xm1 = _xm2 = _ym1 = _ym2 = 0.; + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////// +// Implements a single-band parametric EQ using a biquad "peaking EQ" configuration +// +// gain > 1.0 boosts the center frequency +// gain < 1.0 cuts the center frequency +// +class AudioParametricEQ { + + // + // private data + // + AudioBiquad _kernel; + float _sampleRate; + float _frequency; + float _gain; + float _slope; + + // helpers + void updateKernel() { + + /* + a0 = 1 + alpha*A + a1 = -2*cos(w0) + a2 = 1 - alpha*A + b1 = -2*cos(w0) + b2 = 1 - alpha/A + */ + + const float a = _gain; + const float omega = TWO_PI * _frequency / _sampleRate; + const float alpha = 0.5f * sinf(omega) / _slope; + const float gamma = 1.0f / ( 1.0f + (alpha/a) ); + + const float a0 = 1.0f + (alpha*a); + const float a1 = -2.0f * cosf(omega); + const float a2 = 1.0f - (alpha*a); + const float b1 = a1; + const float b2 = 1.0f - (alpha/a); + + _kernel.setParameters( a0*gamma,a1*gamma,a2*gamma,b1*gamma,b2*gamma ); + } + +public: + // + // ctor/dtor + // + AudioParametricEQ() { + + setParameters(0.,0.,0.,0.); + updateKernel(); + } + + ~AudioParametricEQ() { + } + + // + // public interface + // + void setParameters( const float sampleRate, const float frequency, const float gain, const float slope ) { + + _sampleRate = std::max(sampleRate,1.0f); + _frequency = std::max(frequency,2.0f); + _gain = std::max(gain,0.0f); + _slope = std::max(slope,0.00001f); + + updateKernel(); + } + + void getParameters( float& sampleRate, float& frequency, float& gain, float& slope ) { + sampleRate = _sampleRate; frequency = _frequency; gain = _gain; slope = _slope; + } + + void render(const float* in, float* out, const int frames ) { + _kernel.render(in,out,frames); + } + + void reset() { + _kernel.reset(); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////// +// Helper/convenience class that implements a bank of EQ objects +// +template< typename T, const int N> +class AudioFilterBank { + + // + // types + // + struct FilterParameter { + float _p1; + float _p2; + float _p3; + }; + + // + // private static data + // + static const int _filterCount = N; + static const int _profileCount = 4; + + static FilterParameter _profiles[_profileCount][_filterCount]; + + // + // private data + // + T _filters[ _filterCount ]; + float* _buffer; + float _sampleRate; + uint16_t _frameCount; + +public: + + // + // ctor/dtor + // + AudioFilterBank() + : _buffer(NULL) + , _sampleRate(0.) + , _frameCount(0) { + } + + ~AudioFilterBank() { + finalize(); + } + + // + // public interface + // + void initialize( const float sampleRate, const int frameCount ) { + finalize(); + + _buffer = (float*)malloc( frameCount * sizeof(float) ); + if(!_buffer) { + return; + } + + _sampleRate = sampleRate; + _frameCount = frameCount; + + reset(); + loadProfile(0); // load default profile "flat response" into the bank (see AudioFilter.cpp) + } + + void finalize() { + if (_buffer ) { + free (_buffer); + _buffer = NULL; + } + } + + void loadProfile( int profileIndex ) { + if (profileIndex >= 0 && profileIndex < _profileCount) { + + for (int i = 0; i < _filterCount; ++i) { + FilterParameter p = _profiles[profileIndex][i]; + + _filters[i].setParameters(_sampleRate,p._p1,p._p2,p._p3); + } + } + } + + void render( const float* in, float* out, const int frameCount ) { + for (int i = 0; i < _filterCount; ++i) { + _filters[i].render( in, out, frameCount ); + } + } + + void render( const int16_t* in, int16_t* out, const int frameCount ) { + if (!_buffer || ( frameCount > _frameCount )) + return; + + const int scale = (2 << ((8*sizeof(int16_t))-1)); + + // convert int16_t to float32 (normalized to -1. ... 1.) + for (int i = 0; i < frameCount; ++i) { + _buffer[i] = ((float)(*in++)) / scale; + } + // for this filter, we share input/output buffers at each stage, but our design does not mandate this + render( _buffer, _buffer, frameCount ); + + // convert float32 to int16_t + for (int i = 0; i < frameCount; ++i) { + *out++ = (int16_t)(_buffer[i] * scale); + } + } + + void reset() { + for (int i = 0; i < _filterCount; ++i ) { + _filters[i].reset(); + } + } + +}; + +//////////////////////////////////////////////////////////////////////////////////////////// +// Specializations of AudioFilterBank +// +typedef AudioFilterBank< AudioParametricEQ, 1> AudioFilterPEQ1; // bank with one band of PEQ +typedef AudioFilterBank< AudioParametricEQ, 2> AudioFilterPEQ2; // bank with two bands of PEQ +typedef AudioFilterBank< AudioParametricEQ, 3> AudioFilterPEQ3; // bank with three bands of PEQ +// etc.... + + +#endif // hifi_AudioFilter_h diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index e5c1230832..114ab1c95c 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -27,7 +27,6 @@ AudioInjector::AudioInjector(QObject* parent) : _options(), _shouldStop(false) { - } AudioInjector::AudioInjector(Sound* sound, const AudioInjectorOptions& injectorOptions) : @@ -35,7 +34,10 @@ AudioInjector::AudioInjector(Sound* sound, const AudioInjectorOptions& injectorO _options(injectorOptions), _shouldStop(false) { - +} + +void AudioInjector::setOptions(AudioInjectorOptions& options) { + _options = options; } const uchar MAX_INJECTOR_VOLUME = 0xFF; @@ -73,9 +75,11 @@ void AudioInjector::injectAudio() { packetStream << loopbackFlag; // pack the position for injected audio + int positionOptionOffset = injectAudioPacket.size(); packetStream.writeRawData(reinterpret_cast(&_options.getPosition()), sizeof(_options.getPosition())); // pack our orientation for injected audio + int orientationOptionOffset = injectAudioPacket.size(); packetStream.writeRawData(reinterpret_cast(&_options.getOrientation()), sizeof(_options.getOrientation())); // pack zero for radius @@ -101,6 +105,12 @@ void AudioInjector::injectAudio() { int bytesToCopy = std::min(NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL, soundByteArray.size() - currentSendPosition); + memcpy(injectAudioPacket.data() + positionOptionOffset, + &_options.getPosition(), + sizeof(_options.getPosition())); + memcpy(injectAudioPacket.data() + orientationOptionOffset, + &_options.getOrientation(), + sizeof(_options.getOrientation())); // resize the QByteArray to the right size injectAudioPacket.resize(numPreAudioDataBytes + bytesToCopy); diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 08fe544255..966a4dd1cf 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -29,6 +29,7 @@ public: public slots: void injectAudio(); void stop() { _shouldStop = true; } + void setOptions(AudioInjectorOptions& options); signals: void finished(); private: diff --git a/libraries/audio/src/AudioInjectorOptions.cpp b/libraries/audio/src/AudioInjectorOptions.cpp index 49f1571c98..01aa43a0cd 100644 --- a/libraries/audio/src/AudioInjectorOptions.cpp +++ b/libraries/audio/src/AudioInjectorOptions.cpp @@ -19,7 +19,6 @@ AudioInjectorOptions::AudioInjectorOptions(QObject* parent) : _orientation(glm::vec3(0.0f, 0.0f, 0.0f)), _loopbackAudioInterface(NULL) { - } AudioInjectorOptions::AudioInjectorOptions(const AudioInjectorOptions& other) { @@ -29,3 +28,11 @@ AudioInjectorOptions::AudioInjectorOptions(const AudioInjectorOptions& other) { _orientation = other._orientation; _loopbackAudioInterface = other._loopbackAudioInterface; } + +void AudioInjectorOptions::operator=(const AudioInjectorOptions& other) { + _position = other._position; + _volume = other._volume; + _loop = other._loop; + _orientation = other._orientation; + _loopbackAudioInterface = other._loopbackAudioInterface; +} \ No newline at end of file diff --git a/libraries/audio/src/AudioInjectorOptions.h b/libraries/audio/src/AudioInjectorOptions.h index b90deb93f1..64936e4bc9 100644 --- a/libraries/audio/src/AudioInjectorOptions.h +++ b/libraries/audio/src/AudioInjectorOptions.h @@ -30,6 +30,7 @@ class AudioInjectorOptions : public QObject { public: AudioInjectorOptions(QObject* parent = 0); AudioInjectorOptions(const AudioInjectorOptions& other); + void operator=(const AudioInjectorOptions& other); const glm::vec3& getPosition() const { return _position; } void setPosition(const glm::vec3& position) { _position = position; } @@ -37,8 +38,8 @@ public: float getVolume() const { return _volume; } void setVolume(float volume) { _volume = volume; } - float getLoop() const { return _loop; } - void setLoop(float loop) { _loop = loop; } + bool getLoop() const { return _loop; } + void setLoop(bool loop) { _loop = loop; } const glm::quat& getOrientation() const { return _orientation; } void setOrientation(const glm::quat& orientation) { _orientation = orientation; } diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index c687ab8648..cae663758d 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -15,9 +15,9 @@ #include -#include "PacketHeaders.h" -#include "AudioRingBuffer.h" +#include +#include "AudioRingBuffer.h" AudioRingBuffer::AudioRingBuffer(int numFrameSamples, bool randomAccessMode, int numFramesCapacity) : _frameCapacity(numFramesCapacity), diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index ed680b18b1..b4b30b1f56 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -15,12 +15,10 @@ #include #include -#include - #include -#include "NodeData.h" -#include "SharedUtil.h" +#include +#include const int SAMPLE_RATE = 24000; diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 6ade4b17e9..ba8d9481b5 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "InboundAudioStream.h" #include "PacketHeaders.h" @@ -70,7 +72,12 @@ void InboundAudioStream::clearBuffer() { _currentJitterBufferFrames = 0; } +int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) { + return _ringBuffer.writeData(packetAfterStreamProperties.data(), numAudioSamples * sizeof(int16_t)); +} + int InboundAudioStream::parseData(const QByteArray& packet) { + PacketType packetType = packetTypeForPacket(packet); QUuid senderUUID = uuidFromPacketHeader(packet); @@ -82,7 +89,9 @@ int InboundAudioStream::parseData(const QByteArray& packet) { // parse sequence number and track it quint16 sequence = *(reinterpret_cast(sequenceAt)); readBytes += sizeof(quint16); - SequenceNumberStats::ArrivalInfo arrivalInfo = frameReceivedUpdateNetworkStats(sequence, senderUUID); + SequenceNumberStats::ArrivalInfo arrivalInfo = _incomingSequenceNumberStats.sequenceNumberReceived(sequence, senderUUID); + + frameReceivedUpdateTimingStats(); // TODO: handle generalized silent packet here????? @@ -130,32 +139,71 @@ int InboundAudioStream::parseData(const QByteArray& packet) { return readBytes; } -bool InboundAudioStream::popFrames(int numFrames, bool starveOnFail) { - int numSamplesRequested = numFrames * _ringBuffer.getNumFrameSamples(); +int InboundAudioStream::popSamples(int maxSamples, bool allOrNothing, bool starveIfNoSamplesPopped) { + int samplesPopped = 0; + int samplesAvailable = _ringBuffer.samplesAvailable(); if (_isStarved) { // we're still refilling; don't pop _consecutiveNotMixedCount++; _lastPopSucceeded = false; } else { - if (_ringBuffer.samplesAvailable() >= numSamplesRequested) { - // we have enough samples to pop, so we're good to mix - _lastPopOutput = _ringBuffer.nextOutput(); - _ringBuffer.shiftReadPosition(numSamplesRequested); - framesAvailableChanged(); - - _hasStarted = true; - _lastPopSucceeded = true; + if (samplesAvailable >= maxSamples) { + // we have enough samples to pop, so we're good to pop + popSamplesNoCheck(maxSamples); + samplesPopped = maxSamples; + } else if (!allOrNothing && samplesAvailable > 0) { + // we don't have the requested number of samples, but we do have some + // samples available, so pop all those (except in all-or-nothing mode) + popSamplesNoCheck(samplesAvailable); + samplesPopped = samplesAvailable; } else { - // we don't have enough samples, so set this stream to starve - // if starveOnFail is true - if (starveOnFail) { - starved(); + // we can't pop any samples. set this stream to starved if needed + if (starveIfNoSamplesPopped) { + setToStarved(); _consecutiveNotMixedCount++; } _lastPopSucceeded = false; } } - return _lastPopSucceeded; + return samplesPopped; +} + +int InboundAudioStream::popFrames(int maxFrames, bool allOrNothing, bool starveIfNoFramesPopped) { + int framesPopped = 0; + int framesAvailable = _ringBuffer.framesAvailable(); + if (_isStarved) { + // we're still refilling; don't pop + _consecutiveNotMixedCount++; + _lastPopSucceeded = false; + } else { + if (framesAvailable >= maxFrames) { + // we have enough frames to pop, so we're good to pop + popSamplesNoCheck(maxFrames * _ringBuffer.getNumFrameSamples()); + framesPopped = maxFrames; + } else if (!allOrNothing && framesAvailable > 0) { + // we don't have the requested number of frames, but we do have some + // frames available, so pop all those (except in all-or-nothing mode) + popSamplesNoCheck(framesAvailable * _ringBuffer.getNumFrameSamples()); + framesPopped = framesAvailable; + } else { + // we can't pop any frames. set this stream to starved if needed + if (starveIfNoFramesPopped) { + setToStarved(); + _consecutiveNotMixedCount = 1; + } + _lastPopSucceeded = false; + } + } + return framesPopped; +} + +void InboundAudioStream::popSamplesNoCheck(int samples) { + _lastPopOutput = _ringBuffer.nextOutput(); + _ringBuffer.shiftReadPosition(samples); + framesAvailableChanged(); + + _hasStarted = true; + _lastPopSucceeded = true; } void InboundAudioStream::framesAvailableChanged() { @@ -168,16 +216,12 @@ void InboundAudioStream::framesAvailableChanged() { } void InboundAudioStream::setToStarved() { - starved(); - if (_ringBuffer.framesAvailable() >= _desiredJitterBufferFrames) { - _isStarved = false; - } -} - -void InboundAudioStream::starved() { _isStarved = true; _consecutiveNotMixedCount = 0; _starveCount++; + // if we have more than the desired frames when setToStarved() is called, then we'll immediately + // be considered refilled. in that case, there's no need to set _isStarved to true. + _isStarved = (_ringBuffer.framesAvailable() < _desiredJitterBufferFrames); } void InboundAudioStream::setDynamicJitterBuffers(bool dynamicJitterBuffers) { @@ -204,15 +248,13 @@ int InboundAudioStream::clampDesiredJitterBufferFramesValue(int desired) const { return glm::clamp(desired, MIN_FRAMES_DESIRED, MAX_FRAMES_DESIRED); } -SequenceNumberStats::ArrivalInfo InboundAudioStream::frameReceivedUpdateNetworkStats(quint16 sequenceNumber, const QUuid& senderUUID) { - // track the sequence number we received - SequenceNumberStats::ArrivalInfo arrivalInfo = _incomingSequenceNumberStats.sequenceNumberReceived(sequenceNumber, senderUUID); +void InboundAudioStream::frameReceivedUpdateTimingStats() { // update our timegap stats and desired jitter buffer frames if necessary // discard the first few packets we receive since they usually have gaps that aren't represensative of normal jitter const int NUM_INITIAL_PACKETS_DISCARD = 3; quint64 now = usecTimestampNow(); - if (_incomingSequenceNumberStats.getNumReceived() > NUM_INITIAL_PACKETS_DISCARD) { + if (_incomingSequenceNumberStats.getReceived() > NUM_INITIAL_PACKETS_DISCARD) { quint64 gap = now - _lastFrameReceivedTime; _interframeTimeGapStatsForStatsPacket.update(gap); @@ -243,8 +285,6 @@ SequenceNumberStats::ArrivalInfo InboundAudioStream::frameReceivedUpdateNetworkS } } _lastFrameReceivedTime = now; - - return arrivalInfo; } int InboundAudioStream::writeDroppableSilentSamples(int numSilentSamples) { diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index 06bd329fee..b65d5c5de0 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -63,8 +63,8 @@ public: virtual int parseData(const QByteArray& packet); - - bool popFrames(int numFrames, bool starveOnFail = true); + int popFrames(int maxFrames, bool allOrNothing, bool starveIfNoFramesPopped = true); + int popSamples(int maxSamples, bool allOrNothing, bool starveIfNoSamplesPopped = true); bool lastPopSucceeded() const { return _lastPopSucceeded; }; const AudioRingBuffer::ConstIterator& getLastPopOutput() const { return _lastPopOutput; } @@ -108,16 +108,15 @@ public: int getSilentFramesDropped() const { return _silentFramesDropped; } int getOverflowCount() const { return _ringBuffer.getOverflowCount(); } - int getPacketReceived() const { return _incomingSequenceNumberStats.getNumReceived(); } + int getPacketsReceived() const { return _incomingSequenceNumberStats.getReceived(); } private: - void starved(); - - SequenceNumberStats::ArrivalInfo frameReceivedUpdateNetworkStats(quint16 sequenceNumber, const QUuid& senderUUID); + void frameReceivedUpdateTimingStats(); int clampDesiredJitterBufferFramesValue(int desired) const; int writeSamplesForDroppedPackets(int numSamples); + void popSamplesNoCheck(int samples); void framesAvailableChanged(); protected: @@ -126,11 +125,12 @@ protected: InboundAudioStream& operator= (const InboundAudioStream&); /// parses the info between the seq num and the audio data in the network packet and calculates - /// how many audio samples this packet contains + /// how many audio samples this packet contains (used when filling in samples for dropped packets). virtual int parseStreamProperties(PacketType type, const QByteArray& packetAfterSeqNum, int& numAudioSamples) = 0; - /// parses the audio data in the network packet - virtual int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) = 0; + /// parses the audio data in the network packet. + /// default implementation assumes packet contains raw audio samples after stream properties + virtual int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples); int writeDroppableSilentSamples(int numSilentSamples); diff --git a/libraries/audio/src/InjectedAudioStream.cpp b/libraries/audio/src/InjectedAudioStream.cpp index 37190abc73..5c1c2ed269 100644 --- a/libraries/audio/src/InjectedAudioStream.cpp +++ b/libraries/audio/src/InjectedAudioStream.cpp @@ -58,10 +58,6 @@ int InjectedAudioStream::parseStreamProperties(PacketType type, const QByteArray return packetStream.device()->pos(); } -int InjectedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) { - return _ringBuffer.writeData(packetAfterStreamProperties.data(), numAudioSamples * sizeof(int16_t)); -} - AudioStreamStats InjectedAudioStream::getAudioStreamStats() const { AudioStreamStats streamStats = PositionalAudioStream::getAudioStreamStats(); streamStats._streamIdentifier = _streamIdentifier; diff --git a/libraries/audio/src/InjectedAudioStream.h b/libraries/audio/src/InjectedAudioStream.h index 3cbfad9276..d8d9a54c6e 100644 --- a/libraries/audio/src/InjectedAudioStream.h +++ b/libraries/audio/src/InjectedAudioStream.h @@ -32,7 +32,6 @@ private: AudioStreamStats getAudioStreamStats() const; int parseStreamProperties(PacketType type, const QByteArray& packetAfterSeqNum, int& numAudioSamples); - int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples); const QUuid _streamIdentifier; float _radius; diff --git a/libraries/audio/src/MixedAudioStream.cpp b/libraries/audio/src/MixedAudioStream.cpp index b9e2abfe0b..38c4ae641d 100644 --- a/libraries/audio/src/MixedAudioStream.cpp +++ b/libraries/audio/src/MixedAudioStream.cpp @@ -1,3 +1,13 @@ +// +// MixedAudioStream.cpp +// libraries/audio/src +// +// Created by Yixin Wang on 8/4/14. +// Copyright 2013 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 "MixedAudioStream.h" @@ -11,7 +21,3 @@ int MixedAudioStream::parseStreamProperties(PacketType type, const QByteArray& p numAudioSamples = packetAfterSeqNum.size() / sizeof(int16_t); return 0; } - -int MixedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) { - return _ringBuffer.writeData(packetAfterStreamProperties.data(), numAudioSamples * sizeof(int16_t)); -} diff --git a/libraries/audio/src/MixedAudioStream.h b/libraries/audio/src/MixedAudioStream.h index 5b79519ac5..d19f19af07 100644 --- a/libraries/audio/src/MixedAudioStream.h +++ b/libraries/audio/src/MixedAudioStream.h @@ -2,7 +2,7 @@ // MixedAudioStream.h // libraries/audio/src // -// Created by Stephen Birarda on 6/5/13. +// Created by Yixin Wang on 8/4/14. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -23,7 +23,6 @@ public: protected: int parseStreamProperties(PacketType type, const QByteArray& packetAfterSeqNum, int& numAudioSamples); - int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples); }; #endif // hifi_MixedAudioStream_h diff --git a/libraries/audio/src/MixedProcessedAudioStream.cpp b/libraries/audio/src/MixedProcessedAudioStream.cpp new file mode 100644 index 0000000000..49990dcd22 --- /dev/null +++ b/libraries/audio/src/MixedProcessedAudioStream.cpp @@ -0,0 +1,45 @@ +// +// MixedProcessedAudioStream.cpp +// libraries/audio/src +// +// Created by Yixin Wang on 8/4/14. +// Copyright 2013 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 "MixedProcessedAudioStream.h" + +MixedProcessedAudioStream ::MixedProcessedAudioStream (int numFrameSamples, int numFramesCapacity, bool dynamicJitterBuffers, int staticDesiredJitterBufferFrames, int maxFramesOverDesired, bool useStDevForJitterCalc) + : InboundAudioStream(numFrameSamples, numFramesCapacity, dynamicJitterBuffers, staticDesiredJitterBufferFrames, maxFramesOverDesired, useStDevForJitterCalc) +{ +} + +void MixedProcessedAudioStream::outputFormatChanged(int outputFormatChannelCountTimesSampleRate) { + _outputFormatChannelsTimesSampleRate = outputFormatChannelCountTimesSampleRate; + int deviceOutputFrameSize = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL * _outputFormatChannelsTimesSampleRate / SAMPLE_RATE; + _ringBuffer.resizeForFrameSize(deviceOutputFrameSize); +} + +int MixedProcessedAudioStream::parseStreamProperties(PacketType type, const QByteArray& packetAfterSeqNum, int& numAudioSamples) { + // mixed audio packets do not have any info between the seq num and the audio data. + int numNetworkSamples = packetAfterSeqNum.size() / sizeof(int16_t); + + // since numAudioSamples is used to know how many samples to add for each dropped packet before this one, + // we want to set it to the number of device audio samples since this stream contains device audio samples, not network samples. + const int STEREO_DIVIDER = 2; + numAudioSamples = numNetworkSamples * _outputFormatChannelsTimesSampleRate / (STEREO_DIVIDER * SAMPLE_RATE); + + return 0; +} + +int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples) { + + QByteArray outputBuffer; + emit processSamples(packetAfterStreamProperties, outputBuffer); + + _ringBuffer.writeData(outputBuffer.data(), outputBuffer.size()); + + return packetAfterStreamProperties.size(); +} diff --git a/libraries/audio/src/MixedProcessedAudioStream.h b/libraries/audio/src/MixedProcessedAudioStream.h new file mode 100644 index 0000000000..5a5b73115d --- /dev/null +++ b/libraries/audio/src/MixedProcessedAudioStream.h @@ -0,0 +1,37 @@ +// +// MixedProcessedAudioStream.h +// libraries/audio/src +// +// Created by Yixin Wang on 8/4/14. +// Copyright 2013 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_MixedProcessedAudioStream_h +#define hifi_MixedProcessedAudioStream_h + +#include "InboundAudioStream.h" + +class MixedProcessedAudioStream : public InboundAudioStream { + Q_OBJECT +public: + MixedProcessedAudioStream (int numFrameSamples, int numFramesCapacity, bool dynamicJitterBuffers, int staticDesiredJitterBufferFrames, int maxFramesOverDesired, bool useStDevForJitterCalc); + +signals: + + void processSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); + +public: + void outputFormatChanged(int outputFormatChannelCountTimesSampleRate); + +protected: + int parseStreamProperties(PacketType type, const QByteArray& packetAfterSeqNum, int& numAudioSamples); + int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties, int numAudioSamples); + +private: + int _outputFormatChannelsTimesSampleRate; +}; + +#endif // hifi_MixedProcessedAudioStream_h diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 03c9f6b8ee..f52f5c04dd 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -82,6 +82,17 @@ Sound::Sound(const QUrl& sampleURL, QObject* parent) : connect(soundDownload, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(replyError(QNetworkReply::NetworkError))); } +Sound::Sound(const QByteArray byteArray, QObject* parent) : + QObject(parent), + _byteArray(byteArray), + _hasDownloaded(true) +{ +} + +void Sound::append(const QByteArray byteArray) { + _byteArray.append(byteArray); +} + void Sound::replyFinished() { QNetworkReply* reply = reinterpret_cast(sender()); diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index c473cdff83..7dae3679f1 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -22,6 +22,8 @@ class Sound : public QObject { public: Sound(const QUrl& sampleURL, QObject* parent = NULL); Sound(float volume, float frequency, float duration, float decay, QObject* parent = NULL); + Sound(const QByteArray byteArray, QObject* parent = NULL); + void append(const QByteArray byteArray); bool hasDownloaded() const { return _hasDownloaded; } diff --git a/libraries/avatars/CMakeLists.txt b/libraries/avatars/CMakeLists.txt index ca4a2630b4..1d287ee7a2 100644 --- a/libraries/avatars/CMakeLists.txt +++ b/libraries/avatars/CMakeLists.txt @@ -1,30 +1,12 @@ -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 avatars) -find_package(Qt5Script REQUIRED) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network Script) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +include_glm() -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +link_hifi_libraries(shared octree voxels networking) +include_hifi_library_headers(fbx) -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") - -# link in the hifi voxels library -link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") - -target_link_libraries(${TARGET_NAME} Qt5::Script) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c3ea2f8b50..9653999555 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include #include @@ -36,6 +36,7 @@ using namespace std; AvatarData::AvatarData() : _sessionUUID(), _handPosition(0,0,0), + _referential(NULL), _bodyYaw(-90.f), _bodyPitch(0.0f), _bodyRoll(0.0f), @@ -62,6 +63,59 @@ AvatarData::AvatarData() : AvatarData::~AvatarData() { delete _headData; delete _handData; + delete _referential; +} + +const glm::vec3& AvatarData::getPosition() { + if (_referential) { + _referential->update(); + } + return _position; +} + +void AvatarData::setPosition(const glm::vec3 position, bool overideReferential) { + if (!_referential || overideReferential) { + _position = position; + } +} + +glm::quat AvatarData::getOrientation() const { + if (_referential) { + _referential->update(); + } + + return glm::quat(glm::radians(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll))); +} + +void AvatarData::setOrientation(const glm::quat& orientation, bool overideReferential) { + if (!_referential || overideReferential) { + glm::vec3 eulerAngles = glm::degrees(safeEulerAngles(orientation)); + _bodyPitch = eulerAngles.x; + _bodyYaw = eulerAngles.y; + _bodyRoll = eulerAngles.z; + } +} + +float AvatarData::getTargetScale() const { + if (_referential) { + _referential->update(); + } + + return _targetScale; +} + +void AvatarData::setTargetScale(float targetScale, bool overideReferential) { + if (!_referential || overideReferential) { + _targetScale = targetScale; + } +} + +void AvatarData::setClampedTargetScale(float targetScale, bool overideReferential) { + + targetScale = glm::clamp(targetScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); + + setTargetScale(targetScale, overideReferential); + qDebug() << "Changed scale to " << _targetScale; } glm::vec3 AvatarData::getHandPosition() const { @@ -135,11 +189,21 @@ QByteArray AvatarData::toByteArray() { // hand state setSemiNibbleAt(bitItems,HAND_STATE_START_BIT,_handState); // faceshift state - if (_headData->_isFaceshiftConnected) { setAtBit(bitItems, IS_FACESHIFT_CONNECTED); } + if (_headData->_isFaceshiftConnected) { + setAtBit(bitItems, IS_FACESHIFT_CONNECTED); + } if (_isChatCirclingEnabled) { setAtBit(bitItems, IS_CHAT_CIRCLING_ENABLED); } + if (_referential != NULL && _referential->isValid()) { + setAtBit(bitItems, HAS_REFERENTIAL); + } *destinationBuffer++ = bitItems; + + // Add referential + if (_referential != NULL && _referential->isValid()) { + destinationBuffer += _referential->packReferential(destinationBuffer); + } // If it is connected, pack up the data if (_headData->_isFaceshiftConnected) { @@ -370,18 +434,32 @@ int AvatarData::parseDataAtOffset(const QByteArray& packet, int offset) { } // 1 + chatMessageSize bytes { // bitFlags and face data - unsigned char bitItems = 0; - bitItems = (unsigned char)*sourceBuffer++; - + unsigned char bitItems = *sourceBuffer++; + // key state, stored as a semi-nibble in the bitItems _keyState = (KeyState)getSemiNibbleAt(bitItems,KEY_STATE_START_BIT); - // hand state, stored as a semi-nibble in the bitItems _handState = getSemiNibbleAt(bitItems,HAND_STATE_START_BIT); _headData->_isFaceshiftConnected = oneAtBit(bitItems, IS_FACESHIFT_CONNECTED); _isChatCirclingEnabled = oneAtBit(bitItems, IS_CHAT_CIRCLING_ENABLED); + bool hasReferential = oneAtBit(bitItems, HAS_REFERENTIAL); + // Referential + if (hasReferential) { + Referential* ref = new Referential(sourceBuffer, this); + if (_referential == NULL || + ref->version() != _referential->version()) { + changeReferential(ref); + } else { + delete ref; + } + _referential->update(); + } else if (_referential != NULL) { + changeReferential(NULL); + } + + if (_headData->_isFaceshiftConnected) { float leftEyeBlink, rightEyeBlink, averageLoudness, browAudioLift; minPossibleSize += sizeof(leftEyeBlink) + sizeof(rightEyeBlink) + sizeof(averageLoudness) + sizeof(browAudioLift); @@ -503,6 +581,15 @@ int AvatarData::parseDataAtOffset(const QByteArray& packet, int offset) { return sourceBuffer - startPosition; } +bool AvatarData::hasReferential() { + return _referential != NULL; +} + +void AvatarData::changeReferential(Referential *ref) { + delete _referential; + _referential = ref; +} + void AvatarData::setJointData(int index, const glm::quat& rotation) { if (index == -1) { return; @@ -596,6 +683,41 @@ glm::quat AvatarData::getJointRotation(const QString& name) const { return getJointRotation(getJointIndex(name)); } +QVector AvatarData::getJointRotations() const { + if (QThread::currentThread() != thread()) { + QVector result; + QMetaObject::invokeMethod(const_cast(this), + "getJointRotation", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVector, result)); + return result; + } + QVector jointRotations(_jointData.size()); + for (int i = 0; i < _jointData.size(); ++i) { + jointRotations[i] = _jointData[i].rotation; + } + return jointRotations; +} + +void AvatarData::setJointRotations(QVector jointRotations) { + if (QThread::currentThread() != thread()) { + QVector result; + QMetaObject::invokeMethod(const_cast(this), + "setJointRotation", Qt::BlockingQueuedConnection, + Q_ARG(QVector, jointRotations)); + } + for (int i = 0; i < jointRotations.size(); ++i) { + if (i < _jointData.size()) { + setJointData(i, jointRotations[i]); + } + } +} + +void AvatarData::clearJointsData() { + for (int i = 0; i < _jointData.size(); ++i) { + clearJointData(i); + } +} + bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray &packet) { QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); @@ -803,21 +925,6 @@ void AvatarData::setJointMappingsFromNetworkReply() { networkReply->deleteLater(); } -void AvatarData::setClampedTargetScale(float targetScale) { - - targetScale = glm::clamp(targetScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); - - _targetScale = targetScale; - qDebug() << "Changed scale to " << _targetScale; -} - -void AvatarData::setOrientation(const glm::quat& orientation) { - glm::vec3 eulerAngles = glm::degrees(safeEulerAngles(orientation)); - _bodyPitch = eulerAngles.x; - _bodyYaw = eulerAngles.y; - _bodyRoll = eulerAngles.z; -} - void AvatarData::sendIdentityPacket() { QByteArray identityPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarIdentity); identityPacket.append(identityByteArray()); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 8533b8b0e8..fa884c0229 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -49,6 +49,7 @@ typedef unsigned long long quint64; #include +#include "Referential.h" #include "HeadData.h" #include "HandData.h" @@ -80,7 +81,8 @@ const quint32 AVATAR_MOTION_SCRIPTABLE_BITS = const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits const int IS_FACESHIFT_CONNECTED = 4; // 5th bit -const int IS_CHAT_CIRCLING_ENABLED = 5; +const int IS_CHAT_CIRCLING_ENABLED = 5; // 6th bit +const int HAS_REFERENTIAL = 6; // 7th bit static const float MAX_AVATAR_SCALE = 1000.f; static const float MIN_AVATAR_SCALE = .005f; @@ -141,8 +143,8 @@ public: const QUuid& getSessionUUID() { return _sessionUUID; } - const glm::vec3& getPosition() const { return _position; } - void setPosition(const glm::vec3 position) { _position = position; } + const glm::vec3& getPosition(); + void setPosition(const glm::vec3 position, bool overideReferential = false); glm::vec3 getHandPosition() const; void setHandPosition(const glm::vec3& handPosition); @@ -165,8 +167,8 @@ public: float getBodyRoll() const { return _bodyRoll; } void setBodyRoll(float bodyRoll) { _bodyRoll = bodyRoll; } - glm::quat getOrientation() const { return glm::quat(glm::radians(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll))); } - void setOrientation(const glm::quat& orientation); + glm::quat getOrientation() const; + void setOrientation(const glm::quat& orientation, bool overideReferential = false); glm::quat getHeadOrientation() const { return _headData->getOrientation(); } void setHeadOrientation(const glm::quat& orientation) { _headData->setOrientation(orientation); } @@ -188,9 +190,9 @@ public: void setAudioAverageLoudness(float value) { _headData->setAudioAverageLoudness(value); } // Scale - float getTargetScale() const { return _targetScale; } - void setTargetScale(float targetScale) { _targetScale = targetScale; } - void setClampedTargetScale(float targetScale); + float getTargetScale() const; + void setTargetScale(float targetScale, bool overideReferential = false); + void setClampedTargetScale(float targetScale, bool overideReferential = false); // Hand State Q_INVOKABLE void setHandState(char s) { _handState = s; } @@ -208,7 +210,12 @@ public: Q_INVOKABLE void clearJointData(const QString& name); Q_INVOKABLE bool isJointDataValid(const QString& name) const; Q_INVOKABLE glm::quat getJointRotation(const QString& name) const; - + + Q_INVOKABLE virtual QVector getJointRotations() const; + Q_INVOKABLE virtual void setJointRotations(QVector jointRotations); + + Q_INVOKABLE virtual void clearJointsData(); + /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const { return _jointIndices.value(name) - 1; } @@ -280,17 +287,23 @@ public: QElapsedTimer& getLastUpdateTimer() { return _lastUpdateTimer; } virtual float getBoundingRadius() const { return 1.f; } + + const Referential* getReferential() const { return _referential; } public slots: void sendIdentityPacket(); void sendBillboardPacket(); void setBillboardFromNetworkReply(); void setJointMappingsFromNetworkReply(); - void setSessionUUID(const QUuid& id) { _sessionUUID = id; } + void setSessionUUID(const QUuid& sessionUUID) { _sessionUUID = sessionUUID; } + bool hasReferential(); + protected: QUuid _sessionUUID; glm::vec3 _position; glm::vec3 _handPosition; + + Referential* _referential; // Body rotation float _bodyYaw; // degrees @@ -340,6 +353,7 @@ protected: /// Loads the joint indices, names from the FST file (if any) virtual void updateJointMappings(); + void changeReferential(Referential* ref); private: // privatize the copy constructor and assignment operator so they cannot be called diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 8e3797cbc0..202121bad3 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -9,19 +9,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include "AvatarHashMap.h" AvatarHashMap::AvatarHashMap() : - _avatarHash() + _avatarHash(), + _lastOwnerSessionUUID() { - + connect(NodeList::getInstance(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged); } -void AvatarHashMap::insert(const QUuid& id, AvatarSharedPointer avatar) { - _avatarHash.insert(id, avatar); - avatar->setSessionUUID(id); +void AvatarHashMap::insert(const QUuid& sessionUUID, AvatarSharedPointer avatar) { + _avatarHash.insert(sessionUUID, avatar); + avatar->setSessionUUID(sessionUUID); } AvatarHash::iterator AvatarHashMap::erase(const AvatarHash::iterator& iterator) { @@ -110,10 +112,16 @@ void AvatarHashMap::processAvatarDataPacket(const QByteArray &datagram, const QW QUuid sessionUUID = QUuid::fromRfc4122(datagram.mid(bytesRead, NUM_BYTES_RFC4122_UUID)); bytesRead += NUM_BYTES_RFC4122_UUID; - AvatarSharedPointer matchingAvatarData = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); - - // have the matching (or new) avatar parse the data from the packet - bytesRead += matchingAvatarData->parseDataAtOffset(datagram, bytesRead); + if (sessionUUID != _lastOwnerSessionUUID) { + AvatarSharedPointer matchingAvatarData = matchingOrNewAvatar(sessionUUID, mixerWeakPointer); + + // have the matching (or new) avatar parse the data from the packet + bytesRead += matchingAvatarData->parseDataAtOffset(datagram, bytesRead); + } else { + // create a dummy AvatarData class to throw this data on the ground + AvatarData dummyData; + bytesRead += dummyData.parseDataAtOffset(datagram, bytesRead); + } } } @@ -177,3 +185,7 @@ void AvatarHashMap::processKillAvatar(const QByteArray& datagram) { erase(matchedAvatar); } } + +void AvatarHashMap::sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID) { + _lastOwnerSessionUUID = oldUUID; +} \ No newline at end of file diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 542a2d62ab..fe9ab3d634 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -31,12 +31,15 @@ public: const AvatarHash& getAvatarHash() { return _avatarHash; } int size() const { return _avatarHash.size(); } - virtual void insert(const QUuid& id, AvatarSharedPointer avatar); + virtual void insert(const QUuid& sessionUUID, AvatarSharedPointer avatar); public slots: void processAvatarMixerDatagram(const QByteArray& datagram, const QWeakPointer& mixerWeakPointer); bool containsAvatarWithDisplayName(const QString& displayName); - + +private slots: + void sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID); + protected: virtual AvatarHash::iterator erase(const AvatarHash::iterator& iterator); @@ -51,6 +54,7 @@ protected: void processKillAvatar(const QByteArray& datagram); AvatarHash _avatarHash; + QUuid _lastOwnerSessionUUID; }; #endif // hifi_AvatarHashMap_h diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index b29277ddeb..2bdb203034 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -11,13 +11,13 @@ #include +#include +#include #include #include "AvatarData.h" #include "HeadData.h" -#include "../fbx/src/FBXReader.h" - HeadData::HeadData(AvatarData* owningAvatar) : _baseYaw(0.0f), _basePitch(0.0f), diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 782386c649..310437689c 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -56,6 +56,7 @@ public: void setBlendshape(QString name, float val); const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } + void setBlendshapeCoefficients(const QVector& blendshapeCoefficients) { _blendshapeCoefficients = blendshapeCoefficients; } float getPupilDilation() const { return _pupilDilation; } void setPupilDilation(float pupilDilation) { _pupilDilation = pupilDilation; } @@ -68,6 +69,15 @@ public: const glm::vec3& getLookAtPosition() const { return _lookAtPosition; } void setLookAtPosition(const glm::vec3& lookAtPosition) { _lookAtPosition = lookAtPosition; } + + float getLeanSideways() const { return _leanSideways; } + float getLeanForward() const { return _leanForward; } + virtual float getFinalLeanSideways() const { return _leanSideways; } + virtual float getFinalLeanForward() const { return _leanForward; } + + void setLeanSideways(float leanSideways) { _leanSideways = leanSideways; } + void setLeanForward(float leanForward) { _leanForward = leanForward; } + friend class AvatarData; protected: diff --git a/libraries/avatars/src/Referential.cpp b/libraries/avatars/src/Referential.cpp new file mode 100644 index 0000000000..784ad3963c --- /dev/null +++ b/libraries/avatars/src/Referential.cpp @@ -0,0 +1,113 @@ +// +// Referential.cpp +// +// +// Created by Clement on 7/30/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 "AvatarData.h" +#include "Referential.h" + +Referential::Referential(Type type, AvatarData* avatar) : + _type(type), + _version(0), + _isValid(true), + _avatar(avatar) +{ + if (_avatar == NULL) { + _isValid = false; + return; + } + if (_avatar->hasReferential()) { + _version = _avatar->getReferential()->version() + 1; + } +} + +Referential::Referential(const unsigned char*& sourceBuffer, AvatarData* avatar) : + _isValid(false), + _avatar(avatar) +{ + // Since we can't return how many byte have been read + // We take a reference to the pointer as argument and increment the pointer ouself. + sourceBuffer += unpackReferential(sourceBuffer); + // The actual unpacking to the right referential type happens in Avatar::simulate() + // If subclassed, make sure to add a case there to unpack the new referential type correctly +} + +Referential::~Referential() { +} + +int Referential::packReferential(unsigned char* destinationBuffer) const { + const unsigned char* startPosition = destinationBuffer; + destinationBuffer += pack(destinationBuffer); + + unsigned char* sizePosition = destinationBuffer++; // Save a spot for the extra data size + char size = packExtraData(destinationBuffer); + *sizePosition = size; // write extra data size in saved spot here + destinationBuffer += size; + + return destinationBuffer - startPosition; +} + +int Referential::unpackReferential(const unsigned char* sourceBuffer) { + const unsigned char* startPosition = sourceBuffer; + sourceBuffer += unpack(sourceBuffer); + + char expectedSize = *sourceBuffer++; + char bytesRead = unpackExtraData(sourceBuffer, expectedSize); + _isValid = (bytesRead == expectedSize); + if (!_isValid) { + // Will occur if the new instance unpacking is of the wrong type + qDebug() << "[ERROR] Referential extra data overflow"; + } + sourceBuffer += expectedSize; + + return sourceBuffer - startPosition; +} + +int Referential::pack(unsigned char* destinationBuffer) const { + unsigned char* startPosition = destinationBuffer; + *destinationBuffer++ = (unsigned char)_type; + memcpy(destinationBuffer, &_version, sizeof(_version)); + destinationBuffer += sizeof(_version); + + destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, _translation, 0); + destinationBuffer += packOrientationQuatToBytes(destinationBuffer, _rotation); + destinationBuffer += packFloatScalarToSignedTwoByteFixed(destinationBuffer, _scale, 0); + return destinationBuffer - startPosition; +} + +int Referential::unpack(const unsigned char* sourceBuffer) { + const unsigned char* startPosition = sourceBuffer; + _type = (Type)*sourceBuffer++; + if (_type < 0 || _type >= NUM_TYPE) { + _type = UNKNOWN; + } + memcpy(&_version, sourceBuffer, sizeof(_version)); + sourceBuffer += sizeof(_version); + + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, _translation, 0); + sourceBuffer += unpackOrientationQuatFromBytes(sourceBuffer, _rotation); + sourceBuffer += unpackFloatScalarFromSignedTwoByteFixed((const int16_t*) sourceBuffer, &_scale, 0); + return sourceBuffer - startPosition; +} + +int Referential::packExtraData(unsigned char *destinationBuffer) const { + // Since we can't interpret that data, just store it in a buffer for later use. + memcpy(destinationBuffer, _extraDataBuffer.data(), _extraDataBuffer.size()); + return _extraDataBuffer.size(); +} + + +int Referential::unpackExtraData(const unsigned char* sourceBuffer, int size) { + _extraDataBuffer.clear(); + _extraDataBuffer.setRawData(reinterpret_cast(sourceBuffer), size); + return size; +} + diff --git a/libraries/avatars/src/Referential.h b/libraries/avatars/src/Referential.h new file mode 100644 index 0000000000..f24d66c160 --- /dev/null +++ b/libraries/avatars/src/Referential.h @@ -0,0 +1,75 @@ +// +// Referential.h +// +// +// Created by Clement on 7/30/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_Referential_h +#define hifi_Referential_h + +#include +#include + +class AvatarData; + +/// Stores and enforce the relative position of an avatar to a given referential (ie. model, joint, ...) +class Referential { +public: + enum Type { + UNKNOWN, + MODEL, + JOINT, + AVATAR, + + NUM_TYPE + }; + + Referential(const unsigned char*& sourceBuffer, AvatarData* avatar); + virtual ~Referential(); + + Type type() const { return _type; } + quint8 version() const { return _version; } + bool isValid() const { return _isValid; } + bool hasExtraData() const { return !_extraDataBuffer.isEmpty(); } + + glm::vec3 getTranslation() const { return _translation; } + glm::quat getRotation() const { return _rotation; } + float getScale() const {return _scale; } + QByteArray getExtraData() const { return _extraDataBuffer; } + + virtual void update() {} + int packReferential(unsigned char* destinationBuffer) const; + int unpackReferential(const unsigned char* sourceBuffer); + +protected: + Referential(Type type, AvatarData* avatar); + + // packs the base class data + int pack(unsigned char* destinationBuffer) const; + int unpack(const unsigned char* sourceBuffer); + // virtual functions that pack fthe extra data of subclasses (needs to be reimplemented in subclass) + virtual int packExtraData(unsigned char* destinationBuffer) const; + virtual int unpackExtraData(const unsigned char* sourceBuffer, int size); + + Type _type; + quint8 _version; + bool _isValid; + AvatarData* _avatar; + QByteArray _extraDataBuffer; + + glm::vec3 _refPosition; + glm::quat _refRotation; + float _refScale; + + glm::vec3 _translation; + glm::quat _rotation; + float _scale; +}; + + +#endif // hifi_Referential_h \ No newline at end of file diff --git a/libraries/embedded-webserver/CMakeLists.txt b/libraries/embedded-webserver/CMakeLists.txt index 1386bcc392..6e4b92e217 100644 --- a/libraries/embedded-webserver/CMakeLists.txt +++ b/libraries/embedded-webserver/CMakeLists.txt @@ -1,14 +1,7 @@ -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 embedded-webserver) -find_package(Qt5Network REQUIRED) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) - -target_link_libraries(${TARGET_NAME} Qt5::Network) \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index beb107c4cf..82d3d7eba6 100755 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -21,6 +21,7 @@ const char* HTTPConnection::StatusCode200 = "200 OK"; const char* HTTPConnection::StatusCode301 = "301 Moved Permanently"; const char* HTTPConnection::StatusCode302 = "302 Found"; const char* HTTPConnection::StatusCode400 = "400 Bad Request"; +const char* HTTPConnection::StatusCode401 = "401 Unauthorized"; const char* HTTPConnection::StatusCode404 = "404 Not Found"; const char* HTTPConnection::DefaultContentType = "text/plain; charset=ISO-8859-1"; @@ -42,7 +43,8 @@ HTTPConnection::HTTPConnection (QTcpSocket* socket, HTTPManager* parentManager) HTTPConnection::~HTTPConnection() { // log the destruction - if (_socket->error() != QAbstractSocket::UnknownSocketError) { + if (_socket->error() != QAbstractSocket::UnknownSocketError + && _socket->error() != QAbstractSocket::RemoteHostClosedError) { qDebug() << _socket->errorString() << "-" << _socket->error(); } } diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index e2352ed250..c981537c15 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -46,6 +46,7 @@ public: static const char* StatusCode301; static const char* StatusCode302; static const char* StatusCode400; + static const char* StatusCode401; static const char* StatusCode404; static const char* DefaultContentType; diff --git a/libraries/fbx/CMakeLists.txt b/libraries/fbx/CMakeLists.txt index 45d1051dc6..894fa14c33 100644 --- a/libraries/fbx/CMakeLists.txt +++ b/libraries/fbx/CMakeLists.txt @@ -1,30 +1,15 @@ -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 fbx) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library() -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +include_glm() -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") - -# link ZLIB -find_package(ZLIB) +link_hifi_libraries(shared networking octree voxels) +find_package(ZLIB REQUIRED) include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets) +list(APPEND ${TARGET_NAME}_LIBRARIES_TO_LINK "${ZLIB_LIBRARIES}") -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index cf726800a1..1a152dc217 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -23,9 +23,9 @@ #include #include +#include #include #include -#include #include diff --git a/libraries/metavoxels/CMakeLists.txt b/libraries/metavoxels/CMakeLists.txt index c79631ce06..aab8d2184d 100644 --- a/libraries/metavoxels/CMakeLists.txt +++ b/libraries/metavoxels/CMakeLists.txt @@ -1,29 +1,14 @@ -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 metavoxels) -find_package(Qt5 COMPONENTS Network Script Widgets) +auto_mtc() -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} "${ROOT_DIR}") - -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME} "${AUTOMTC_SRC}") +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network Script Widgets) # link in the networking library -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") +link_hifi_libraries(shared networking) -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +include_glm() -target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/metavoxels/src/AttributeRegistry.cpp b/libraries/metavoxels/src/AttributeRegistry.cpp index 7c7ded0fb7..1e30aee576 100644 --- a/libraries/metavoxels/src/AttributeRegistry.cpp +++ b/libraries/metavoxels/src/AttributeRegistry.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include +#include #include #include #include @@ -21,6 +23,8 @@ REGISTER_META_OBJECT(QRgbAttribute) REGISTER_META_OBJECT(PackedNormalAttribute) REGISTER_META_OBJECT(SpannerQRgbAttribute) REGISTER_META_OBJECT(SpannerPackedNormalAttribute) +REGISTER_META_OBJECT(HeightfieldAttribute) +REGISTER_META_OBJECT(HeightfieldColorAttribute) REGISTER_META_OBJECT(SharedObjectAttribute) REGISTER_META_OBJECT(SharedObjectSetAttribute) REGISTER_META_OBJECT(SpannerSetAttribute) @@ -37,17 +41,23 @@ AttributeRegistry::AttributeRegistry() : _guideAttribute(registerAttribute(new SharedObjectAttribute("guide", &MetavoxelGuide::staticMetaObject, new DefaultMetavoxelGuide()))), _rendererAttribute(registerAttribute(new SharedObjectAttribute("renderer", &MetavoxelRenderer::staticMetaObject, - new PointMetavoxelRenderer()))), + new DefaultMetavoxelRenderer()))), _spannersAttribute(registerAttribute(new SpannerSetAttribute("spanners", &Spanner::staticMetaObject))), _colorAttribute(registerAttribute(new QRgbAttribute("color"))), _normalAttribute(registerAttribute(new PackedNormalAttribute("normal"))), _spannerColorAttribute(registerAttribute(new SpannerQRgbAttribute("spannerColor"))), _spannerNormalAttribute(registerAttribute(new SpannerPackedNormalAttribute("spannerNormal"))), - _spannerMaskAttribute(registerAttribute(new FloatAttribute("spannerMask"))) { + _spannerMaskAttribute(registerAttribute(new FloatAttribute("spannerMask"))), + _heightfieldAttribute(registerAttribute(new HeightfieldAttribute("heightfield"))), + _heightfieldColorAttribute(registerAttribute(new HeightfieldColorAttribute("heightfieldColor"))) { - // our baseline LOD threshold is for voxels; spanners are a different story + // our baseline LOD threshold is for voxels; spanners and heightfields are a different story const float SPANNER_LOD_THRESHOLD_MULTIPLIER = 8.0f; _spannersAttribute->setLODThresholdMultiplier(SPANNER_LOD_THRESHOLD_MULTIPLIER); + + const float HEIGHTFIELD_LOD_THRESHOLD_MULTIPLIER = 32.0f; + _heightfieldAttribute->setLODThresholdMultiplier(HEIGHTFIELD_LOD_THRESHOLD_MULTIPLIER); + _heightfieldColorAttribute->setLODThresholdMultiplier(HEIGHTFIELD_LOD_THRESHOLD_MULTIPLIER); } static QScriptValue qDebugFunction(QScriptContext* context, QScriptEngine* engine) { @@ -232,6 +242,30 @@ bool Attribute::metavoxelRootsEqual(const MetavoxelNode& firstRoot, const Metavo return firstRoot.deepEquals(this, secondRoot, minimum, size, lod); } +MetavoxelNode* Attribute::expandMetavoxelRoot(const MetavoxelNode& root) { + AttributePointer attribute(this); + MetavoxelNode* newParent = new MetavoxelNode(attribute); + for (int i = 0; i < MetavoxelNode::CHILD_COUNT; i++) { + MetavoxelNode* newChild = new MetavoxelNode(attribute); + newParent->setChild(i, newChild); + int index = MetavoxelNode::getOppositeChildIndex(i); + if (root.isLeaf()) { + newChild->setChild(index, new MetavoxelNode(root.getAttributeValue(attribute))); + } else { + MetavoxelNode* grandchild = root.getChild(i); + grandchild->incrementReferenceCount(); + newChild->setChild(index, grandchild); + } + for (int j = 1; j < MetavoxelNode::CHILD_COUNT; j++) { + MetavoxelNode* newGrandchild = new MetavoxelNode(attribute); + newChild->setChild((index + j) % MetavoxelNode::CHILD_COUNT, newGrandchild); + } + newChild->mergeChildren(attribute); + } + newParent->mergeChildren(attribute); + return newParent; +} + FloatAttribute::FloatAttribute(const QString& name, float defaultValue) : SimpleInlineAttribute(name, defaultValue) { } @@ -451,6 +485,492 @@ AttributeValue SpannerPackedNormalAttribute::inherit(const AttributeValue& paren return AttributeValue(parentValue.getAttribute()); } +HeightfieldData::HeightfieldData(const QByteArray& contents) : + _contents(contents) { +} + +HeightfieldData::HeightfieldData(Bitstream& in, int bytes, bool color) { + read(in, bytes, color); +} + +enum HeightfieldImage { NULL_HEIGHTFIELD_IMAGE, NORMAL_HEIGHTFIELD_IMAGE, DEFLATED_HEIGHTFIELD_IMAGE }; + +static QByteArray encodeHeightfieldImage(const QImage& image) { + if (image.isNull()) { + return QByteArray(1, NULL_HEIGHTFIELD_IMAGE); + } + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + const int JPEG_ENCODE_THRESHOLD = 16; + if (image.width() >= JPEG_ENCODE_THRESHOLD && image.height() >= JPEG_ENCODE_THRESHOLD) { + qint32 offsetX = image.offset().x(), offsetY = image.offset().y(); + buffer.write((char*)&offsetX, sizeof(qint32)); + buffer.write((char*)&offsetY, sizeof(qint32)); + image.save(&buffer, "JPG"); + return QByteArray(1, DEFLATED_HEIGHTFIELD_IMAGE) + qCompress(buffer.data()); + + } else { + buffer.putChar(NORMAL_HEIGHTFIELD_IMAGE); + image.save(&buffer, "PNG"); + return buffer.data(); + } +} + +const QImage decodeHeightfieldImage(const QByteArray& data) { + switch (data.at(0)) { + case NULL_HEIGHTFIELD_IMAGE: + default: + return QImage(); + + case NORMAL_HEIGHTFIELD_IMAGE: + return QImage::fromData(QByteArray::fromRawData(data.constData() + 1, data.size() - 1)); + + case DEFLATED_HEIGHTFIELD_IMAGE: { + QByteArray inflated = qUncompress((const uchar*)data.constData() + 1, data.size() - 1); + const int OFFSET_SIZE = sizeof(qint32) * 2; + QImage image = QImage::fromData((const uchar*)inflated.constData() + OFFSET_SIZE, inflated.size() - OFFSET_SIZE); + const qint32* offsets = (const qint32*)inflated.constData(); + image.setOffset(QPoint(offsets[0], offsets[1])); + return image; + } + } +} + +HeightfieldData::HeightfieldData(Bitstream& in, int bytes, const HeightfieldDataPointer& reference, bool color) { + if (!reference) { + read(in, bytes, color); + return; + } + QMutexLocker locker(&reference->_encodedDeltaMutex); + reference->_encodedDelta = in.readAligned(bytes); + reference->_deltaData = this; + _contents = reference->_contents; + QImage image = decodeHeightfieldImage(reference->_encodedDelta); + if (image.isNull()) { + return; + } + QPoint offset = image.offset(); + image = image.convertToFormat(QImage::Format_RGB888); + if (offset.x() == 0) { + set(image, color); + return; + } + int minX = offset.x() - 1; + int minY = offset.y() - 1; + if (color) { + int size = glm::sqrt(_contents.size() / (float)COLOR_BYTES); + char* dest = _contents.data() + (minY * size + minX) * COLOR_BYTES; + int destStride = size * COLOR_BYTES; + int srcStride = image.width() * COLOR_BYTES; + for (int y = 0; y < image.height(); y++) { + memcpy(dest, image.constScanLine(y), srcStride); + dest += destStride; + } + } else { + int size = glm::sqrt((float)_contents.size()); + char* lineDest = _contents.data() + minY * size + minX; + for (int y = 0; y < image.height(); y++) { + const uchar* src = image.constScanLine(y); + for (char* dest = lineDest, *end = dest + image.width(); dest != end; dest++, src += COLOR_BYTES) { + *dest = *src; + } + lineDest += size; + } + } +} + +void HeightfieldData::write(Bitstream& out, bool color) { + QMutexLocker locker(&_encodedMutex); + if (_encoded.isEmpty()) { + QImage image; + if (color) { + int size = glm::sqrt(_contents.size() / (float)COLOR_BYTES); + image = QImage((uchar*)_contents.data(), size, size, QImage::Format_RGB888); + } else { + int size = glm::sqrt((float)_contents.size()); + image = QImage(size, size, QImage::Format_RGB888); + uchar* dest = image.bits(); + for (const char* src = _contents.constData(), *end = src + _contents.size(); src != end; src++) { + *dest++ = *src; + *dest++ = *src; + *dest++ = *src; + } + } + _encoded = encodeHeightfieldImage(image); + } + out << _encoded.size(); + out.writeAligned(_encoded); +} + +void HeightfieldData::writeDelta(Bitstream& out, const HeightfieldDataPointer& reference, bool color) { + if (!reference || reference->getContents().size() != _contents.size()) { + write(out, color); + return; + } + QMutexLocker locker(&reference->_encodedDeltaMutex); + if (reference->_encodedDelta.isEmpty() || reference->_deltaData != this) { + QImage image; + int minX, minY; + if (color) { + int size = glm::sqrt(_contents.size() / (float)COLOR_BYTES); + minX = size; + minY = size; + int maxX = -1, maxY = -1; + const char* src = _contents.constData(); + const char* ref = reference->_contents.constData(); + for (int y = 0; y < size; y++) { + bool difference = false; + for (int x = 0; x < size; x++, src += COLOR_BYTES, ref += COLOR_BYTES) { + if (src[0] != ref[0] || src[1] != ref[1] || src[2] != ref[2]) { + minX = qMin(minX, x); + maxX = qMax(maxX, x); + difference = true; + } + } + if (difference) { + minY = qMin(minY, y); + maxY = qMax(maxY, y); + } + } + if (maxX >= minX) { + int width = maxX - minX + 1; + int height = maxY - minY + 1; + image = QImage(width, height, QImage::Format_RGB888); + src = _contents.constData() + (minY * size + minX) * COLOR_BYTES; + int srcStride = size * COLOR_BYTES; + int destStride = width * COLOR_BYTES; + for (int y = 0; y < height; y++) { + memcpy(image.scanLine(y), src, destStride); + src += srcStride; + } + } + } else { + int size = glm::sqrt((float)_contents.size()); + minX = size; + minY = size; + int maxX = -1, maxY = -1; + const char* src = _contents.constData(); + const char* ref = reference->_contents.constData(); + for (int y = 0; y < size; y++) { + bool difference = false; + for (int x = 0; x < size; x++) { + if (*src++ != *ref++) { + minX = qMin(minX, x); + maxX = qMax(maxX, x); + difference = true; + } + } + if (difference) { + minY = qMin(minY, y); + maxY = qMax(maxY, y); + } + } + if (maxX >= minX) { + int width = qMax(maxX - minX + 1, 0); + int height = qMax(maxY - minY + 1, 0); + image = QImage(width, height, QImage::Format_RGB888); + const uchar* lineSrc = (const uchar*)_contents.constData() + minY * size + minX; + for (int y = 0; y < height; y++) { + uchar* dest = image.scanLine(y); + for (const uchar* src = lineSrc, *end = src + width; src != end; src++) { + *dest++ = *src; + *dest++ = *src; + *dest++ = *src; + } + lineSrc += size; + } + } + } + image.setOffset(QPoint(minX + 1, minY + 1)); + reference->_encodedDelta = encodeHeightfieldImage(image); + reference->_deltaData = this; + } + out << reference->_encodedDelta.size(); + out.writeAligned(reference->_encodedDelta); +} + +void HeightfieldData::read(Bitstream& in, int bytes, bool color) { + set(decodeHeightfieldImage(_encoded = in.readAligned(bytes)).convertToFormat(QImage::Format_RGB888), color); +} + +void HeightfieldData::set(const QImage& image, bool color) { + if (color) { + _contents.resize(image.width() * image.height() * COLOR_BYTES); + memcpy(_contents.data(), image.constBits(), _contents.size()); + + } else { + _contents.resize(image.width() * image.height()); + char* dest = _contents.data(); + for (const uchar* src = image.constBits(), *end = src + _contents.size() * COLOR_BYTES; + src != end; src += COLOR_BYTES) { + *dest++ = *src; + } + } +} + +HeightfieldAttribute::HeightfieldAttribute(const QString& name) : + InlineAttribute(name) { +} + +void HeightfieldAttribute::read(Bitstream& in, void*& value, bool isLeaf) const { + if (!isLeaf) { + return; + } + int size; + in >> size; + if (size == 0) { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(); + } else { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(new HeightfieldData(in, size, false)); + } +} + +void HeightfieldAttribute::write(Bitstream& out, void* value, bool isLeaf) const { + if (!isLeaf) { + return; + } + HeightfieldDataPointer data = decodeInline(value); + if (data) { + data->write(out, false); + } else { + out << 0; + } +} + +void HeightfieldAttribute::readDelta(Bitstream& in, void*& value, void* reference, bool isLeaf) const { + if (!isLeaf) { + return; + } + int size; + in >> size; + if (size == 0) { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(); + } else { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(new HeightfieldData( + in, size, decodeInline(reference), false)); + } +} + +void HeightfieldAttribute::writeDelta(Bitstream& out, void* value, void* reference, bool isLeaf) const { + if (!isLeaf) { + return; + } + HeightfieldDataPointer data = decodeInline(value); + if (data) { + data->writeDelta(out, decodeInline(reference), false); + } else { + out << 0; + } +} + +bool HeightfieldAttribute::merge(void*& parent, void* children[], bool postRead) const { + int maxSize = 0; + for (int i = 0; i < MERGE_COUNT; i++) { + HeightfieldDataPointer pointer = decodeInline(children[i]); + if (pointer) { + maxSize = qMax(maxSize, pointer->getContents().size()); + } + } + if (maxSize == 0) { + *(HeightfieldDataPointer*)&parent = HeightfieldDataPointer(); + return true; + } + int size = glm::sqrt((float)maxSize); + QByteArray contents(size * size, 0); + int halfSize = size / 2; + for (int i = 0; i < MERGE_COUNT; i++) { + HeightfieldDataPointer child = decodeInline(children[i]); + if (!child) { + continue; + } + const QByteArray& childContents = child->getContents(); + int childSize = glm::sqrt((float)childContents.size()); + const int INDEX_MASK = 1; + int xIndex = i & INDEX_MASK; + const int Y_SHIFT = 1; + int yIndex = (i >> Y_SHIFT) & INDEX_MASK; + if (yIndex == 0 && decodeInline(children[i | (1 << Y_SHIFT)])) { + continue; // bottom is overriden by top + } + const int HALF_RANGE = 128; + int yOffset = yIndex * HALF_RANGE; + int Z_SHIFT = 2; + int zIndex = (i >> Z_SHIFT) & INDEX_MASK; + char* dest = contents.data() + (zIndex * halfSize * size) + (xIndex * halfSize); + uchar* src = (uchar*)childContents.data(); + int childSizePlusOne = childSize + 1; + if (childSize == size) { + // simple case: one destination value for four child values + for (int z = 0; z < halfSize; z++) { + for (char* end = dest + halfSize; dest != end; src += 2) { + int max = qMax(qMax(src[0], src[1]), qMax(src[childSize], src[childSizePlusOne])); + *dest++ = (max == 0) ? 0 : (yOffset + (max >> 1)); + } + dest += halfSize; + src += childSize; + } + } else { + // more complex: N destination values for four child values + int halfChildSize = childSize / 2; + int destPerSrc = size / childSize; + for (int z = 0; z < halfChildSize; z++) { + for (uchar* end = src + childSize; src != end; src += 2) { + int max = qMax(qMax(src[0], src[1]), qMax(src[childSize], src[childSizePlusOne])); + memset(dest, (max == 0) ? 0 : (yOffset + (max >> 1)), destPerSrc); + dest += destPerSrc; + } + dest += halfSize; + for (int j = 1; j < destPerSrc; j++) { + memcpy(dest, dest - size, halfSize); + dest += size; + } + src += childSize; + } + } + } + *(HeightfieldDataPointer*)&parent = HeightfieldDataPointer(new HeightfieldData(contents)); + return false; +} + +HeightfieldColorAttribute::HeightfieldColorAttribute(const QString& name) : + InlineAttribute(name) { +} + +void HeightfieldColorAttribute::read(Bitstream& in, void*& value, bool isLeaf) const { + if (!isLeaf) { + return; + } + int size; + in >> size; + if (size == 0) { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(); + } else { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(new HeightfieldData(in, size, true)); + } +} + +void HeightfieldColorAttribute::write(Bitstream& out, void* value, bool isLeaf) const { + if (!isLeaf) { + return; + } + HeightfieldDataPointer data = decodeInline(value); + if (data) { + data->write(out, true); + } else { + out << 0; + } +} + +void HeightfieldColorAttribute::readDelta(Bitstream& in, void*& value, void* reference, bool isLeaf) const { + if (!isLeaf) { + return; + } + int size; + in >> size; + if (size == 0) { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(); + } else { + *(HeightfieldDataPointer*)&value = HeightfieldDataPointer(new HeightfieldData( + in, size, decodeInline(reference), true)); + } +} + +void HeightfieldColorAttribute::writeDelta(Bitstream& out, void* value, void* reference, bool isLeaf) const { + if (!isLeaf) { + return; + } + HeightfieldDataPointer data = decodeInline(value); + if (data) { + data->writeDelta(out, decodeInline(reference), true); + } else { + out << 0; + } +} + +bool HeightfieldColorAttribute::merge(void*& parent, void* children[], bool postRead) const { + int maxSize = 0; + for (int i = 0; i < MERGE_COUNT; i++) { + HeightfieldDataPointer pointer = decodeInline(children[i]); + if (pointer) { + maxSize = qMax(maxSize, pointer->getContents().size()); + } + } + if (maxSize == 0) { + *(HeightfieldDataPointer*)&parent = HeightfieldDataPointer(); + return true; + } + int size = glm::sqrt(maxSize / (float)HeightfieldData::COLOR_BYTES); + QByteArray contents(size * size * HeightfieldData::COLOR_BYTES, 0); + int halfSize = size / 2; + for (int i = 0; i < MERGE_COUNT; i++) { + HeightfieldDataPointer child = decodeInline(children[i]); + if (!child) { + continue; + } + const QByteArray& childContents = child->getContents(); + int childSize = glm::sqrt(childContents.size() / (float)HeightfieldData::COLOR_BYTES); + const int INDEX_MASK = 1; + int xIndex = i & INDEX_MASK; + const int Y_SHIFT = 1; + int yIndex = (i >> Y_SHIFT) & INDEX_MASK; + if (yIndex == 0 && decodeInline(children[i | (1 << Y_SHIFT)])) { + continue; // bottom is overriden by top + } + int Z_SHIFT = 2; + int zIndex = (i >> Z_SHIFT) & INDEX_MASK; + char* dest = contents.data() + ((zIndex * halfSize * size) + (xIndex * halfSize)) * HeightfieldData::COLOR_BYTES; + uchar* src = (uchar*)childContents.data(); + int childStride = childSize * HeightfieldData::COLOR_BYTES; + int stride = size * HeightfieldData::COLOR_BYTES; + int halfStride = stride / 2; + int childStep = 2 * HeightfieldData::COLOR_BYTES; + int redOffset3 = childStride + HeightfieldData::COLOR_BYTES; + int greenOffset1 = HeightfieldData::COLOR_BYTES + 1; + int greenOffset2 = childStride + 1; + int greenOffset3 = childStride + HeightfieldData::COLOR_BYTES + 1; + int blueOffset1 = HeightfieldData::COLOR_BYTES + 2; + int blueOffset2 = childStride + 2; + int blueOffset3 = childStride + HeightfieldData::COLOR_BYTES + 2; + if (childSize == size) { + // simple case: one destination value for four child values + for (int z = 0; z < halfSize; z++) { + for (char* end = dest + halfSize * HeightfieldData::COLOR_BYTES; dest != end; src += childStep) { + *dest++ = ((int)src[0] + (int)src[HeightfieldData::COLOR_BYTES] + + (int)src[childStride] + (int)src[redOffset3]) >> 2; + *dest++ = ((int)src[1] + (int)src[greenOffset1] + (int)src[greenOffset2] + (int)src[greenOffset3]) >> 2; + *dest++ = ((int)src[2] + (int)src[blueOffset1] + (int)src[blueOffset2] + (int)src[blueOffset3]) >> 2; + } + dest += halfStride; + src += childStride; + } + } else { + // more complex: N destination values for four child values + int halfChildSize = childSize / 2; + int destPerSrc = size / childSize; + for (int z = 0; z < halfChildSize; z++) { + for (uchar* end = src + childSize * HeightfieldData::COLOR_BYTES; src != end; src += childStep) { + *dest++ = ((int)src[0] + (int)src[HeightfieldData::COLOR_BYTES] + + (int)src[childStride] + (int)src[redOffset3]) >> 2; + *dest++ = ((int)src[1] + (int)src[greenOffset1] + (int)src[greenOffset2] + (int)src[greenOffset3]) >> 2; + *dest++ = ((int)src[2] + (int)src[blueOffset1] + (int)src[blueOffset2] + (int)src[blueOffset3]) >> 2; + for (int j = 1; j < destPerSrc; j++) { + memcpy(dest, dest - HeightfieldData::COLOR_BYTES, HeightfieldData::COLOR_BYTES); + dest += HeightfieldData::COLOR_BYTES; + } + } + dest += halfStride; + for (int j = 1; j < destPerSrc; j++) { + memcpy(dest, dest - stride, halfStride); + dest += stride; + } + src += childStride; + } + } + } + *(HeightfieldDataPointer*)&parent = HeightfieldDataPointer(new HeightfieldData(contents)); + return false; +} + SharedObjectAttribute::SharedObjectAttribute(const QString& name, const QMetaObject* metaObject, const SharedObjectPointer& defaultValue) : InlineAttribute(name, defaultValue), @@ -545,6 +1065,29 @@ bool SharedObjectSetAttribute::deepEqual(void* first, void* second) const { return setsEqual(decodeInline(first), decodeInline(second)); } +MetavoxelNode* SharedObjectSetAttribute::expandMetavoxelRoot(const MetavoxelNode& root) { + AttributePointer attribute(this); + MetavoxelNode* newParent = new MetavoxelNode(attribute); + for (int i = 0; i < MetavoxelNode::CHILD_COUNT; i++) { + MetavoxelNode* newChild = new MetavoxelNode(root.getAttributeValue(attribute)); + newParent->setChild(i, newChild); + if (root.isLeaf()) { + continue; + } + MetavoxelNode* grandchild = root.getChild(i); + grandchild->incrementReferenceCount(); + int index = MetavoxelNode::getOppositeChildIndex(i); + newChild->setChild(index, grandchild); + for (int j = 1; j < MetavoxelNode::CHILD_COUNT; j++) { + MetavoxelNode* newGrandchild = new MetavoxelNode(attribute); + newChild->setChild((index + j) % MetavoxelNode::CHILD_COUNT, newGrandchild); + } + newChild->mergeChildren(attribute); + } + newParent->mergeChildren(attribute); + return newParent; +} + bool SharedObjectSetAttribute::merge(void*& parent, void* children[], bool postRead) const { for (int i = 0; i < MERGE_COUNT; i++) { if (!decodeInline(children[i]).isEmpty()) { diff --git a/libraries/metavoxels/src/AttributeRegistry.h b/libraries/metavoxels/src/AttributeRegistry.h index d07503335f..ddf6105662 100644 --- a/libraries/metavoxels/src/AttributeRegistry.h +++ b/libraries/metavoxels/src/AttributeRegistry.h @@ -13,6 +13,7 @@ #define hifi_AttributeRegistry_h #include +#include #include #include #include @@ -27,6 +28,7 @@ class QScriptEngine; class QScriptValue; class Attribute; +class HeightfieldData; class MetavoxelData; class MetavoxelLOD; class MetavoxelNode; @@ -94,6 +96,12 @@ public: /// Returns a reference to the standard "spannerMask" attribute. const AttributePointer& getSpannerMaskAttribute() const { return _spannerMaskAttribute; } + /// Returns a reference to the standard HeightfieldPointer "heightfield" attribute. + const AttributePointer& getHeightfieldAttribute() const { return _heightfieldAttribute; } + + /// Returns a reference to the standard HeightfieldColorPointer "heightfieldColor" attribute. + const AttributePointer& getHeightfieldColorAttribute() const { return _heightfieldColorAttribute; } + private: static QScriptValue getAttribute(QScriptContext* context, QScriptEngine* engine); @@ -109,6 +117,8 @@ private: AttributePointer _spannerColorAttribute; AttributePointer _spannerNormalAttribute; AttributePointer _spannerMaskAttribute; + AttributePointer _heightfieldAttribute; + AttributePointer _heightfieldColorAttribute; }; /// Converts a value to a void pointer. @@ -229,6 +239,10 @@ public: virtual bool metavoxelRootsEqual(const MetavoxelNode& firstRoot, const MetavoxelNode& secondRoot, const glm::vec3& minimum, float size, const MetavoxelLOD& lod); + /// Expands the specified root, doubling its size in each dimension. + /// \return a new node representing the result + virtual MetavoxelNode* expandMetavoxelRoot(const MetavoxelNode& root); + /// Merges the value of a parent and its children. /// \param postRead whether or not the merge is happening after a read /// \return whether or not the children and parent values are all equal @@ -408,6 +422,71 @@ public: virtual AttributeValue inherit(const AttributeValue& parentValue) const; }; +typedef QExplicitlySharedDataPointer HeightfieldDataPointer; + +/// Contains a block of heightfield data. +class HeightfieldData : public QSharedData { +public: + + static const int COLOR_BYTES = 3; + + HeightfieldData(const QByteArray& contents); + HeightfieldData(Bitstream& in, int bytes, bool color); + HeightfieldData(Bitstream& in, int bytes, const HeightfieldDataPointer& reference, bool color); + + const QByteArray& getContents() const { return _contents; } + + void write(Bitstream& out, bool color); + void writeDelta(Bitstream& out, const HeightfieldDataPointer& reference, bool color); + +private: + + void read(Bitstream& in, int bytes, bool color); + void set(const QImage& image, bool color); + + QByteArray _contents; + QByteArray _encoded; + QMutex _encodedMutex; + + HeightfieldDataPointer _deltaData; + QByteArray _encodedDelta; + QMutex _encodedDeltaMutex; +}; + +/// An attribute that stores heightfield data. +class HeightfieldAttribute : public InlineAttribute { + Q_OBJECT + +public: + + Q_INVOKABLE HeightfieldAttribute(const QString& name = QString()); + + virtual void read(Bitstream& in, void*& value, bool isLeaf) const; + virtual void write(Bitstream& out, void* value, bool isLeaf) const; + + virtual void readDelta(Bitstream& in, void*& value, void* reference, bool isLeaf) const; + virtual void writeDelta(Bitstream& out, void* value, void* reference, bool isLeaf) const; + + virtual bool merge(void*& parent, void* children[], bool postRead = false) const; +}; + +/// An attribute that stores heightfield colors. +class HeightfieldColorAttribute : public InlineAttribute { + Q_OBJECT + +public: + + Q_INVOKABLE HeightfieldColorAttribute(const QString& name = QString()); + + virtual void read(Bitstream& in, void*& value, bool isLeaf) const; + virtual void write(Bitstream& out, void* value, bool isLeaf) const; + + virtual void readDelta(Bitstream& in, void*& value, void* reference, bool isLeaf) const; + virtual void writeDelta(Bitstream& out, void* value, void* reference, bool isLeaf) const; + + virtual bool merge(void*& parent, void* children[], bool postRead = false) const; +}; + /// An attribute that takes the form of QObjects of a given meta-type (a subclass of SharedObject). class SharedObjectAttribute : public InlineAttribute { Q_OBJECT @@ -454,6 +533,8 @@ public: virtual bool deepEqual(void* first, void* second) const; + virtual MetavoxelNode* expandMetavoxelRoot(const MetavoxelNode& root); + virtual bool merge(void*& parent, void* children[], bool postRead = false) const; virtual AttributeValue inherit(const AttributeValue& parentValue) const; diff --git a/libraries/metavoxels/src/Bitstream.cpp b/libraries/metavoxels/src/Bitstream.cpp index f49ae1c04f..e9ac3d6319 100644 --- a/libraries/metavoxels/src/Bitstream.cpp +++ b/libraries/metavoxels/src/Bitstream.cpp @@ -639,6 +639,16 @@ void Bitstream::readRawDelta(QScriptValue& value, const QScriptValue& reference) } } +void Bitstream::writeAligned(const QByteArray& data) { + flush(); + _underlying.device()->write(data); +} + +QByteArray Bitstream::readAligned(int bytes) { + reset(); + return _underlying.device()->read(bytes); +} + Bitstream& Bitstream::operator<<(bool value) { if (value) { _byte |= (1 << _position); diff --git a/libraries/metavoxels/src/Bitstream.h b/libraries/metavoxels/src/Bitstream.h index 7602424ded..1fd9205387 100644 --- a/libraries/metavoxels/src/Bitstream.h +++ b/libraries/metavoxels/src/Bitstream.h @@ -430,6 +430,11 @@ public: template void writeRawDelta(const QHash& value, const QHash& reference); template void readRawDelta(QHash& value, const QHash& reference); + /// Writes the specified array aligned on byte boundaries to avoid the inefficiency + /// of bit-twiddling (at the cost of up to seven bits of wasted space). + void writeAligned(const QByteArray& data); + QByteArray readAligned(int bytes); + Bitstream& operator<<(bool value); Bitstream& operator>>(bool& value); diff --git a/libraries/metavoxels/src/MetavoxelData.cpp b/libraries/metavoxels/src/MetavoxelData.cpp index 3bf43ef8d4..3607441461 100644 --- a/libraries/metavoxels/src/MetavoxelData.cpp +++ b/libraries/metavoxels/src/MetavoxelData.cpp @@ -26,7 +26,7 @@ REGISTER_META_OBJECT(DefaultMetavoxelGuide) REGISTER_META_OBJECT(ScriptedMetavoxelGuide) REGISTER_META_OBJECT(ThrobbingMetavoxelGuide) REGISTER_META_OBJECT(MetavoxelRenderer) -REGISTER_META_OBJECT(PointMetavoxelRenderer) +REGISTER_META_OBJECT(DefaultMetavoxelRenderer) REGISTER_META_OBJECT(Spanner) REGISTER_META_OBJECT(Sphere) REGISTER_META_OBJECT(StaticModel) @@ -54,6 +54,18 @@ bool MetavoxelLOD::becameSubdivided(const glm::vec3& minimum, float size, return true; } +bool MetavoxelLOD::becameSubdividedOrCollapsed(const glm::vec3& minimum, float size, + const MetavoxelLOD& reference, float multiplier) const { + if (position == reference.position && threshold == reference.threshold) { + return false; // first off, nothing becomes subdivided or collapsed if it doesn't change + } + if (!(shouldSubdivide(minimum, size, multiplier) || reference.shouldSubdivide(minimum, size, multiplier))) { + return false; // this one or the reference must be subdivided + } + // TODO: find some way of culling subtrees that can't possibly contain subdivided or collapsed nodes + return true; +} + MetavoxelData::MetavoxelData() : _size(1.0f) { } @@ -500,33 +512,11 @@ void MetavoxelData::set(const glm::vec3& minimum, const MetavoxelData& data, boo } } -static int getOppositeIndex(int index) { - return index ^ MAXIMUM_FLAG_MASK; -} - void MetavoxelData::expand() { for (QHash::iterator it = _roots.begin(); it != _roots.end(); it++) { - MetavoxelNode* newParent = new MetavoxelNode(it.key()); - for (int i = 0; i < MetavoxelNode::CHILD_COUNT; i++) { - MetavoxelNode* newChild = new MetavoxelNode(it.key()); - newParent->setChild(i, newChild); - int index = getOppositeIndex(i); - if (it.value()->isLeaf()) { - newChild->setChild(index, new MetavoxelNode(it.value()->getAttributeValue(it.key()))); - } else { - MetavoxelNode* grandchild = it.value()->getChild(i); - grandchild->incrementReferenceCount(); - newChild->setChild(index, grandchild); - } - for (int j = 1; j < MetavoxelNode::CHILD_COUNT; j++) { - MetavoxelNode* newGrandchild = new MetavoxelNode(it.key()); - newChild->setChild((index + j) % MetavoxelNode::CHILD_COUNT, newGrandchild); - } - newChild->mergeChildren(it.key()); - } - newParent->mergeChildren(it.key()); + MetavoxelNode* newNode = it.key()->expandMetavoxelRoot(*it.value()); it.value()->decrementReferenceCount(it.key()); - it.value() = newParent; + it.value() = newNode; } _size *= 2.0f; } @@ -567,53 +557,68 @@ void MetavoxelData::readDelta(const MetavoxelData& reference, const MetavoxelLOD // shallow copy the reference *this = reference; + QHash remainingRoots = _roots; + bool changed; in >> changed; - if (!changed) { - return; - } - - bool sizeChanged; - in >> sizeChanged; - if (sizeChanged) { - float size; - in >> size; - while (_size < size) { - expand(); - } - } - - forever { - AttributePointer attribute; - in >> attribute; - if (!attribute) { - break; - } - MetavoxelStreamBase base = { attribute, in, lod, referenceLOD }; - MetavoxelStreamState state = { base, getMinimum(), _size }; - MetavoxelNode* oldRoot = _roots.value(attribute); - if (oldRoot) { - bool changed; - in >> changed; - if (changed) { - oldRoot->incrementReferenceCount(); - attribute->readMetavoxelDelta(*this, *oldRoot, state); - oldRoot->decrementReferenceCount(attribute); - } else { - attribute->readMetavoxelSubdivision(*this, state); + if (changed) { + bool sizeChanged; + in >> sizeChanged; + if (sizeChanged) { + float size; + in >> size; + while (_size < size) { + expand(); } - } else { - attribute->readMetavoxelRoot(*this, state); - } + } + + glm::vec3 minimum = getMinimum(); + forever { + AttributePointer attribute; + in >> attribute; + if (!attribute) { + break; + } + MetavoxelStreamBase base = { attribute, in, lod, referenceLOD }; + MetavoxelStreamState state = { base, minimum, _size }; + MetavoxelNode* oldRoot = _roots.value(attribute); + if (oldRoot) { + bool changed; + in >> changed; + if (changed) { + oldRoot->incrementReferenceCount(); + attribute->readMetavoxelDelta(*this, *oldRoot, state); + oldRoot->decrementReferenceCount(attribute); + } else { + attribute->readMetavoxelSubdivision(*this, state); + } + remainingRoots.remove(attribute); + + } else { + attribute->readMetavoxelRoot(*this, state); + } + } + + forever { + AttributePointer attribute; + in >> attribute; + if (!attribute) { + break; + } + _roots.take(attribute)->decrementReferenceCount(attribute); + remainingRoots.remove(attribute); + } } - forever { - AttributePointer attribute; - in >> attribute; - if (!attribute) { - break; + // read subdivisions for the remaining roots if there's any chance of a collapse + if (!(lod.position == referenceLOD.position && lod.threshold <= referenceLOD.threshold)) { + glm::vec3 minimum = getMinimum(); + for (QHash::const_iterator it = remainingRoots.constBegin(); + it != remainingRoots.constEnd(); it++) { + MetavoxelStreamBase base = { it.key(), in, lod, referenceLOD }; + MetavoxelStreamState state = { base, minimum, _size }; + it.key()->readMetavoxelSubdivision(*this, state); } - _roots.take(attribute)->decrementReferenceCount(attribute); } } @@ -788,10 +793,18 @@ bool MetavoxelStreamState::becameSubdivided() const { return base.lod.becameSubdivided(minimum, size, base.referenceLOD, base.attribute->getLODThresholdMultiplier()); } +bool MetavoxelStreamState::becameSubdividedOrCollapsed() const { + return base.lod.becameSubdividedOrCollapsed(minimum, size, base.referenceLOD, base.attribute->getLODThresholdMultiplier()); +} + void MetavoxelStreamState::setMinimum(const glm::vec3& lastMinimum, int index) { minimum = getNextMinimum(lastMinimum, size, index); } +int MetavoxelNode::getOppositeChildIndex(int index) { + return index ^ MAXIMUM_FLAG_MASK; +} + MetavoxelNode::MetavoxelNode(const AttributeValue& attributeValue, const MetavoxelNode* copyChildren) : _referenceCount(1) { @@ -923,7 +936,7 @@ void MetavoxelNode::readDelta(const MetavoxelNode& reference, MetavoxelStreamSta _children[i] = new MetavoxelNode(state.base.attribute); _children[i]->readDelta(*reference._children[i], nextState); } else { - if (nextState.becameSubdivided()) { + if (nextState.becameSubdividedOrCollapsed()) { _children[i] = reference._children[i]->readSubdivision(nextState); if (_children[i] == reference._children[i]) { _children[i]->incrementReferenceCount(); @@ -972,42 +985,46 @@ void MetavoxelNode::writeDelta(const MetavoxelNode& reference, MetavoxelStreamSt } MetavoxelNode* MetavoxelNode::readSubdivision(MetavoxelStreamState& state) { - if (!state.shouldSubdivideReference()) { - bool leaf; - state.base.stream >> leaf; - if (leaf) { - return isLeaf() ? this : new MetavoxelNode(getAttributeValue(state.base.attribute)); - - } else { - MetavoxelNode* newNode = new MetavoxelNode(getAttributeValue(state.base.attribute)); + if (state.shouldSubdivide()) { + if (!state.shouldSubdivideReference()) { + bool leaf; + state.base.stream >> leaf; + if (leaf) { + return isLeaf() ? this : new MetavoxelNode(getAttributeValue(state.base.attribute)); + + } else { + MetavoxelNode* newNode = new MetavoxelNode(getAttributeValue(state.base.attribute)); + MetavoxelStreamState nextState = { state.base, glm::vec3(), state.size * 0.5f }; + for (int i = 0; i < CHILD_COUNT; i++) { + nextState.setMinimum(state.minimum, i); + newNode->_children[i] = new MetavoxelNode(state.base.attribute); + newNode->_children[i]->read(nextState); + } + return newNode; + } + } else if (!isLeaf()) { + MetavoxelNode* node = this; MetavoxelStreamState nextState = { state.base, glm::vec3(), state.size * 0.5f }; for (int i = 0; i < CHILD_COUNT; i++) { nextState.setMinimum(state.minimum, i); - newNode->_children[i] = new MetavoxelNode(state.base.attribute); - newNode->_children[i]->read(nextState); - } - return newNode; - } - } else if (!isLeaf()) { - MetavoxelNode* node = this; - MetavoxelStreamState nextState = { state.base, glm::vec3(), state.size * 0.5f }; - for (int i = 0; i < CHILD_COUNT; i++) { - nextState.setMinimum(state.minimum, i); - if (nextState.becameSubdivided()) { - MetavoxelNode* child = _children[i]->readSubdivision(nextState); - if (child != _children[i]) { - if (node == this) { - node = new MetavoxelNode(state.base.attribute, this); + if (nextState.becameSubdividedOrCollapsed()) { + MetavoxelNode* child = _children[i]->readSubdivision(nextState); + if (child != _children[i]) { + if (node == this) { + node = new MetavoxelNode(state.base.attribute, this); + } + node->_children[i] = child; + _children[i]->decrementReferenceCount(state.base.attribute); } - node->_children[i] = child; - _children[i]->decrementReferenceCount(state.base.attribute); } } + if (node != this) { + node->mergeChildren(state.base.attribute, true); + } + return node; } - if (node != this) { - node->mergeChildren(state.base.attribute, true); - } - return node; + } else if (!isLeaf()) { + return new MetavoxelNode(getAttributeValue(state.base.attribute)); } return this; } @@ -1879,20 +1896,27 @@ void MetavoxelRendererImplementation::init(MetavoxelRenderer* renderer) { void MetavoxelRendererImplementation::augment(MetavoxelData& data, const MetavoxelData& previous, MetavoxelInfo& info, const MetavoxelLOD& lod) { + // nothing by default +} + +void MetavoxelRendererImplementation::simulate(MetavoxelData& data, float deltaTime, + MetavoxelInfo& info, const MetavoxelLOD& lod) { + // nothing by default } void MetavoxelRendererImplementation::render(MetavoxelData& data, MetavoxelInfo& info, const MetavoxelLOD& lod) { + // nothing by default } QByteArray MetavoxelRenderer::getImplementationClassName() const { return "MetavoxelRendererImplementation"; } -PointMetavoxelRenderer::PointMetavoxelRenderer() { +DefaultMetavoxelRenderer::DefaultMetavoxelRenderer() { } -QByteArray PointMetavoxelRenderer::getImplementationClassName() const { - return "PointMetavoxelRendererImplementation"; +QByteArray DefaultMetavoxelRenderer::getImplementationClassName() const { + return "DefaultMetavoxelRendererImplementation"; } const float DEFAULT_PLACEMENT_GRANULARITY = 0.01f; diff --git a/libraries/metavoxels/src/MetavoxelData.h b/libraries/metavoxels/src/MetavoxelData.h index 2dc778cf71..9e5b2f04d1 100644 --- a/libraries/metavoxels/src/MetavoxelData.h +++ b/libraries/metavoxels/src/MetavoxelData.h @@ -55,6 +55,11 @@ public: /// Checks whether the node or any of the nodes underneath it have had subdivision enabled as compared to the reference. bool becameSubdivided(const glm::vec3& minimum, float size, const MetavoxelLOD& reference, float multiplier = 1.0f) const; + + /// Checks whether the node or any of the nodes underneath it have had subdivision + /// enabled or disabled as compared to the reference. + bool becameSubdividedOrCollapsed(const glm::vec3& minimum, float size, + const MetavoxelLOD& reference, float multiplier = 1.0f) const; }; DECLARE_STREAMABLE_METATYPE(MetavoxelLOD) @@ -181,6 +186,7 @@ public: bool shouldSubdivide() const; bool shouldSubdivideReference() const; bool becameSubdivided() const; + bool becameSubdividedOrCollapsed() const; void setMinimum(const glm::vec3& lastMinimum, int index); }; @@ -191,6 +197,8 @@ public: static const int CHILD_COUNT = 8; + static int getOppositeChildIndex(int index); + MetavoxelNode(const AttributeValue& attributeValue, const MetavoxelNode* copyChildren = NULL); MetavoxelNode(const AttributePointer& attribute, const MetavoxelNode* copy); @@ -555,6 +563,7 @@ public: virtual void init(MetavoxelRenderer* renderer); virtual void augment(MetavoxelData& data, const MetavoxelData& previous, MetavoxelInfo& info, const MetavoxelLOD& lod); + virtual void simulate(MetavoxelData& data, float deltaTime, MetavoxelInfo& info, const MetavoxelLOD& lod); virtual void render(MetavoxelData& data, MetavoxelInfo& info, const MetavoxelLOD& lod); protected: @@ -562,13 +571,13 @@ protected: MetavoxelRenderer* _renderer; }; -/// Renders metavoxels as points. -class PointMetavoxelRenderer : public MetavoxelRenderer { +/// The standard, usual renderer. +class DefaultMetavoxelRenderer : public MetavoxelRenderer { Q_OBJECT public: - Q_INVOKABLE PointMetavoxelRenderer(); + Q_INVOKABLE DefaultMetavoxelRenderer(); virtual QByteArray getImplementationClassName() const; }; diff --git a/libraries/metavoxels/src/MetavoxelMessages.cpp b/libraries/metavoxels/src/MetavoxelMessages.cpp index dba9bc9c5c..df6e8172e4 100644 --- a/libraries/metavoxels/src/MetavoxelMessages.cpp +++ b/libraries/metavoxels/src/MetavoxelMessages.cpp @@ -318,3 +318,177 @@ SetDataEdit::SetDataEdit(const glm::vec3& minimum, const MetavoxelData& data, bo void SetDataEdit::apply(MetavoxelData& data, const WeakSharedObjectHash& objects) const { data.set(minimum, this->data, blend); } + +PaintHeightfieldHeightEdit::PaintHeightfieldHeightEdit(const glm::vec3& position, float radius, float height) : + position(position), + radius(radius), + height(height) { +} + +class PaintHeightfieldHeightEditVisitor : public MetavoxelVisitor { +public: + + PaintHeightfieldHeightEditVisitor(const PaintHeightfieldHeightEdit& edit); + + virtual int visit(MetavoxelInfo& info); + +private: + + PaintHeightfieldHeightEdit _edit; + Box _bounds; +}; + +PaintHeightfieldHeightEditVisitor::PaintHeightfieldHeightEditVisitor(const PaintHeightfieldHeightEdit& edit) : + MetavoxelVisitor(QVector() << AttributeRegistry::getInstance()->getHeightfieldAttribute(), + QVector() << AttributeRegistry::getInstance()->getHeightfieldAttribute()), + _edit(edit) { + + glm::vec3 extents(_edit.radius, _edit.radius, _edit.radius); + _bounds = Box(_edit.position - extents, _edit.position + extents); +} + +int PaintHeightfieldHeightEditVisitor::visit(MetavoxelInfo& info) { + if (!info.getBounds().intersects(_bounds)) { + return STOP_RECURSION; + } + if (!info.isLeaf) { + return DEFAULT_ORDER; + } + HeightfieldDataPointer pointer = info.inputValues.at(0).getInlineValue(); + if (!pointer) { + return STOP_RECURSION; + } + QByteArray contents(pointer->getContents()); + int size = glm::sqrt((float)contents.size()); + int highest = size - 1; + float heightScale = size / info.size; + + glm::vec3 center = (_edit.position - info.minimum) * heightScale; + float scaledRadius = _edit.radius * heightScale; + glm::vec3 extents(scaledRadius, scaledRadius, scaledRadius); + + glm::vec3 start = glm::floor(center - extents); + glm::vec3 end = glm::ceil(center + extents); + + // raise/lower all points within the radius + float z = qMax(start.z, 0.0f); + float startX = qMax(start.x, 0.0f), endX = qMin(end.x, (float)highest); + uchar* lineDest = (uchar*)contents.data() + (int)z * size + (int)startX; + float squaredRadius = scaledRadius * scaledRadius; + float squaredRadiusReciprocal = 1.0f / squaredRadius; + const int EIGHT_BIT_MAXIMUM = 255; + float scaledHeight = _edit.height * EIGHT_BIT_MAXIMUM / info.size; + bool changed = false; + for (float endZ = qMin(end.z, (float)highest); z <= endZ; z += 1.0f) { + uchar* dest = lineDest; + for (float x = startX; x <= endX; x += 1.0f, dest++) { + float dx = x - center.x, dz = z - center.z; + float distanceSquared = dx * dx + dz * dz; + if (distanceSquared <= squaredRadius) { + // height falls off towards edges + int value = *dest + scaledHeight * (squaredRadius - distanceSquared) * squaredRadiusReciprocal; + if (value != *dest) { + *dest = qMin(qMax(value, 0), EIGHT_BIT_MAXIMUM); + changed = true; + } + } + } + lineDest += size; + } + if (changed) { + HeightfieldDataPointer newPointer(new HeightfieldData(contents)); + info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(newPointer)); + } + return STOP_RECURSION; +} + +void PaintHeightfieldHeightEdit::apply(MetavoxelData& data, const WeakSharedObjectHash& objects) const { + PaintHeightfieldHeightEditVisitor visitor(*this); + data.guide(visitor); +} + +PaintHeightfieldColorEdit::PaintHeightfieldColorEdit(const glm::vec3& position, float radius, const QColor& color) : + position(position), + radius(radius), + color(color) { +} + +class PaintHeightfieldColorEditVisitor : public MetavoxelVisitor { +public: + + PaintHeightfieldColorEditVisitor(const PaintHeightfieldColorEdit& edit); + + virtual int visit(MetavoxelInfo& info); + +private: + + PaintHeightfieldColorEdit _edit; + Box _bounds; +}; + +PaintHeightfieldColorEditVisitor::PaintHeightfieldColorEditVisitor(const PaintHeightfieldColorEdit& edit) : + MetavoxelVisitor(QVector() << AttributeRegistry::getInstance()->getHeightfieldColorAttribute(), + QVector() << AttributeRegistry::getInstance()->getHeightfieldColorAttribute()), + _edit(edit) { + + glm::vec3 extents(_edit.radius, _edit.radius, _edit.radius); + _bounds = Box(_edit.position - extents, _edit.position + extents); +} + +int PaintHeightfieldColorEditVisitor::visit(MetavoxelInfo& info) { + if (!info.getBounds().intersects(_bounds)) { + return STOP_RECURSION; + } + if (!info.isLeaf) { + return DEFAULT_ORDER; + } + HeightfieldDataPointer pointer = info.inputValues.at(0).getInlineValue(); + if (!pointer) { + return STOP_RECURSION; + } + QByteArray contents(pointer->getContents()); + const int BYTES_PER_PIXEL = 3; + int size = glm::sqrt((float)contents.size() / BYTES_PER_PIXEL); + int highest = size - 1; + float heightScale = size / info.size; + + glm::vec3 center = (_edit.position - info.minimum) * heightScale; + float scaledRadius = _edit.radius * heightScale; + glm::vec3 extents(scaledRadius, scaledRadius, scaledRadius); + + glm::vec3 start = glm::floor(center - extents); + glm::vec3 end = glm::ceil(center + extents); + + // paint all points within the radius + float z = qMax(start.z, 0.0f); + float startX = qMax(start.x, 0.0f), endX = qMin(end.x, (float)highest); + int stride = size * BYTES_PER_PIXEL; + char* lineDest = contents.data() + (int)z * stride + (int)startX * BYTES_PER_PIXEL; + float squaredRadius = scaledRadius * scaledRadius; + char red = _edit.color.red(), green = _edit.color.green(), blue = _edit.color.blue(); + bool changed = false; + for (float endZ = qMin(end.z, (float)highest); z <= endZ; z += 1.0f) { + char* dest = lineDest; + for (float x = startX; x <= endX; x += 1.0f, dest += BYTES_PER_PIXEL) { + float dx = x - center.x, dz = z - center.z; + if (dx * dx + dz * dz <= squaredRadius) { + dest[0] = red; + dest[1] = green; + dest[2] = blue; + changed = true; + } + } + lineDest += stride; + } + if (changed) { + HeightfieldDataPointer newPointer(new HeightfieldData(contents)); + info.outputValues[0] = AttributeValue(_outputs.at(0), encodeInline(newPointer)); + } + return STOP_RECURSION; +} + +void PaintHeightfieldColorEdit::apply(MetavoxelData& data, const WeakSharedObjectHash& objects) const { + PaintHeightfieldColorEditVisitor visitor(*this); + data.guide(visitor); +} + diff --git a/libraries/metavoxels/src/MetavoxelMessages.h b/libraries/metavoxels/src/MetavoxelMessages.h index 91d73c08a9..2fc8cbf030 100644 --- a/libraries/metavoxels/src/MetavoxelMessages.h +++ b/libraries/metavoxels/src/MetavoxelMessages.h @@ -207,4 +207,38 @@ public: DECLARE_STREAMABLE_METATYPE(SetDataEdit) +/// An edit that sets a region of a heightfield height. +class PaintHeightfieldHeightEdit : public MetavoxelEdit { + STREAMABLE + +public: + + STREAM glm::vec3 position; + STREAM float radius; + STREAM float height; + + PaintHeightfieldHeightEdit(const glm::vec3& position = glm::vec3(), float radius = 0.0f, float height = 0.0f); + + virtual void apply(MetavoxelData& data, const WeakSharedObjectHash& objects) const; +}; + +DECLARE_STREAMABLE_METATYPE(PaintHeightfieldHeightEdit) + +/// An edit that sets a region of a heightfield color. +class PaintHeightfieldColorEdit : public MetavoxelEdit { + STREAMABLE + +public: + + STREAM glm::vec3 position; + STREAM float radius; + STREAM QColor color; + + PaintHeightfieldColorEdit(const glm::vec3& position = glm::vec3(), float radius = 0.0f, const QColor& color = QColor()); + + virtual void apply(MetavoxelData& data, const WeakSharedObjectHash& objects) const; +}; + +DECLARE_STREAMABLE_METATYPE(PaintHeightfieldColorEdit) + #endif // hifi_MetavoxelMessages_h diff --git a/libraries/metavoxels/src/MetavoxelUtil.cpp b/libraries/metavoxels/src/MetavoxelUtil.cpp index d35b9a698a..e6b96e97b0 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.cpp +++ b/libraries/metavoxels/src/MetavoxelUtil.cpp @@ -24,6 +24,8 @@ #include #include +#include + #include "MetavoxelUtil.h" #include "ScriptCache.h" #include "StreamUtils.h" @@ -162,6 +164,11 @@ Box::Box(const glm::vec3& minimum, const glm::vec3& maximum) : minimum(minimum), maximum(maximum) { } +void Box::add(const Box& other) { + minimum = glm::min(minimum, other.minimum); + maximum = glm::max(maximum, other.maximum); +} + bool Box::contains(const glm::vec3& point) const { return point.x >= minimum.x && point.x <= maximum.x && point.y >= minimum.y && point.y <= maximum.y && @@ -180,6 +187,14 @@ bool Box::intersects(const Box& other) const { other.maximum.z >= minimum.z && other.minimum.z <= maximum.z; } +Box Box::getIntersection(const Box& other) const { + return Box(glm::max(minimum, other.minimum), glm::min(maximum, other.maximum)); +} + +bool Box::isEmpty() const { + return minimum.x >= maximum.x || minimum.y >= maximum.y || minimum.z >= maximum.z; +} + const int X_MAXIMUM_FLAG = 1; const int Y_MAXIMUM_FLAG = 2; const int Z_MAXIMUM_FLAG = 4; @@ -315,6 +330,136 @@ QDebug& operator<<(QDebug& dbg, const Box& box) { return dbg.nospace() << "{type='Box', minimum=" << box.minimum << ", maximum=" << box.maximum << "}"; } +AxisExtents::AxisExtents(const glm::vec3& first0, const glm::vec3& first1, const glm::vec3& first2, const glm::vec3& second) : + axis(glm::cross(first2 - first1, first0 - first1)), + minimum(glm::dot(first1, axis)), + maximum(glm::dot(second, axis)) { +} + +AxisExtents::AxisExtents(const glm::vec3& axis, float minimum, float maximum) : + axis(axis), + minimum(minimum), + maximum(maximum) { +} + +void Frustum::set(const glm::vec3& farTopLeft, const glm::vec3& farTopRight, const glm::vec3& farBottomLeft, + const glm::vec3& farBottomRight, const glm::vec3& nearTopLeft, const glm::vec3& nearTopRight, + const glm::vec3& nearBottomLeft, const glm::vec3& nearBottomRight) { + + _vertices[0] = farBottomLeft; + _vertices[1] = farBottomRight; + _vertices[2] = farTopLeft; + _vertices[3] = farTopRight; + _vertices[4] = nearBottomLeft; + _vertices[5] = nearBottomRight; + _vertices[6] = nearTopLeft; + _vertices[7] = nearTopRight; + + // compute the bounds + _bounds.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX); + _bounds.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); + for (int i = 0; i < VERTEX_COUNT; i++) { + _bounds.minimum = glm::min(_bounds.minimum, _vertices[i]); + _bounds.maximum = glm::max(_bounds.maximum, _vertices[i]); + } + + // compute the extents for each side + _sideExtents[0] = AxisExtents(nearBottomLeft, nearTopLeft, nearTopRight, farBottomLeft); + _sideExtents[1] = AxisExtents(nearBottomLeft, farBottomLeft, farTopLeft, farBottomRight); + _sideExtents[2] = AxisExtents(nearBottomRight, nearTopRight, farTopRight, farBottomLeft); + _sideExtents[3] = AxisExtents(nearBottomLeft, nearBottomRight, farBottomRight, farTopLeft); + _sideExtents[4] = AxisExtents(nearTopLeft, farTopLeft, farTopRight, farBottomRight); + + // the other set of extents are derived from the cross products of the frustum and box edges + glm::vec3 edges[] = { nearBottomRight - nearBottomLeft, nearTopLeft - nearBottomLeft, farBottomLeft - nearBottomLeft, + farBottomRight - nearBottomRight, farTopLeft - nearTopLeft, farTopRight - nearTopRight }; + const int AXIS_COUNT = 3; + for (uint i = 0, extentIndex = 0; i < sizeof(edges) / sizeof(edges[0]); i++) { + for (int j = 0; j < AXIS_COUNT; j++) { + glm::vec3 axis; + axis[j] = 1.0f; + glm::vec3 crossProduct = glm::cross(edges[i], axis); + float minimum = FLT_MAX, maximum = -FLT_MAX; + for (int k = 0; k < VERTEX_COUNT; k++) { + float projection = glm::dot(crossProduct, _vertices[k]); + minimum = glm::min(minimum, projection); + maximum = glm::max(maximum, projection); + } + _crossProductExtents[extentIndex++] = AxisExtents(crossProduct, minimum, maximum); + } + } +} + +Frustum::IntersectionType Frustum::getIntersectionType(const Box& box) const { + // first check the bounds (equivalent to checking frustum vertices against box extents) + if (!_bounds.intersects(box)) { + return NO_INTERSECTION; + } + + // check box vertices against side extents + bool allInside = true; + for (int i = 0; i < SIDE_EXTENT_COUNT; i++) { + const AxisExtents& extents = _sideExtents[i]; + float firstProjection = glm::dot(box.getVertex(0), extents.axis); + if (firstProjection < extents.minimum) { + allInside = false; + for (int j = 1; j < Box::VERTEX_COUNT; j++) { + if (glm::dot(box.getVertex(j), extents.axis) >= extents.minimum) { + goto sideContinue; + } + } + return NO_INTERSECTION; + + } else if (firstProjection > extents.maximum) { + allInside = false; + for (int j = 1; j < Box::VERTEX_COUNT; j++) { + if (glm::dot(box.getVertex(j), extents.axis) <= extents.maximum) { + goto sideContinue; + } + } + return NO_INTERSECTION; + + } else if (allInside) { + for (int j = 1; j < Box::VERTEX_COUNT; j++) { + float projection = glm::dot(box.getVertex(j), extents.axis); + if (projection < extents.minimum || projection > extents.maximum) { + allInside = false; + goto sideContinue; + } + } + } + sideContinue: ; + } + if (allInside) { + return CONTAINS_INTERSECTION; + } + + // check box vertices against cross product extents + for (int i = 0; i < CROSS_PRODUCT_EXTENT_COUNT; i++) { + const AxisExtents& extents = _crossProductExtents[i]; + float firstProjection = glm::dot(box.getVertex(0), extents.axis); + if (firstProjection < extents.minimum) { + for (int j = 1; j < Box::VERTEX_COUNT; j++) { + if (glm::dot(box.getVertex(j), extents.axis) >= extents.minimum) { + goto crossProductContinue; + } + } + return NO_INTERSECTION; + + } else if (firstProjection > extents.maximum) { + for (int j = 1; j < Box::VERTEX_COUNT; j++) { + if (glm::dot(box.getVertex(j), extents.axis) <= extents.maximum) { + goto crossProductContinue; + } + } + return NO_INTERSECTION; + } + crossProductContinue: ; + } + + return PARTIAL_INTERSECTION; +} + QMetaObjectEditor::QMetaObjectEditor(QWidget* parent) : QWidget(parent) { QVBoxLayout* layout = new QVBoxLayout(); layout->setContentsMargins(QMargins()); @@ -401,6 +546,16 @@ BaseVec3Editor::BaseVec3Editor(QWidget* parent) : QWidget(parent) { layout->addWidget(_z = createComponentBox()); } +void BaseVec3Editor::setSingleStep(double singleStep) { + _x->setSingleStep(singleStep); + _y->setSingleStep(singleStep); + _z->setSingleStep(singleStep); +} + +double BaseVec3Editor::getSingleStep() const { + return _x->singleStep(); +} + QDoubleSpinBox* BaseVec3Editor::createComponentBox() { QDoubleSpinBox* box = new QDoubleSpinBox(); box->setMinimum(-FLT_MAX); @@ -411,9 +566,7 @@ QDoubleSpinBox* BaseVec3Editor::createComponentBox() { } Vec3Editor::Vec3Editor(QWidget* parent) : BaseVec3Editor(parent) { - _x->setSingleStep(0.01); - _y->setSingleStep(0.01); - _z->setSingleStep(0.01); + setSingleStep(0.01); } static void setComponentValue(QDoubleSpinBox* box, double value) { diff --git a/libraries/metavoxels/src/MetavoxelUtil.h b/libraries/metavoxels/src/MetavoxelUtil.h index 83aac1318e..4228af059f 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.h +++ b/libraries/metavoxels/src/MetavoxelUtil.h @@ -44,12 +44,18 @@ public: explicit Box(const glm::vec3& minimum = glm::vec3(), const glm::vec3& maximum = glm::vec3()); + void add(const Box& other); + bool contains(const glm::vec3& point) const; bool contains(const Box& other) const; bool intersects(const Box& other) const; + Box getIntersection(const Box& other) const; + + bool isEmpty() const; + float getLongestSide() const { return qMax(qMax(maximum.x - minimum.x, maximum.y - minimum.y), maximum.z - minimum.z); } glm::vec3 getVertex(int index) const; @@ -65,6 +71,43 @@ Box operator*(const glm::mat4& matrix, const Box& box); QDebug& operator<<(QDebug& out, const Box& box); +/// Represents the extents along an axis. +class AxisExtents { +public: + glm::vec3 axis; + float minimum; + float maximum; + + /// Creates a set of extents given three points on the first plane and one on the second. + AxisExtents(const glm::vec3& first0, const glm::vec3& first1, const glm::vec3& first2, const glm::vec3& second); + + AxisExtents(const glm::vec3& axis = glm::vec3(), float minimum = 0.0f, float maximum = 0.0f); +}; + +/// A simple pyramidal frustum for intersection testing. +class Frustum { +public: + + void set(const glm::vec3& farTopLeft, const glm::vec3& farTopRight, const glm::vec3& farBottomLeft, + const glm::vec3& farBottomRight, const glm::vec3& nearTopLeft, const glm::vec3& nearTopRight, + const glm::vec3& nearBottomLeft, const glm::vec3& nearBottomRight); + + enum IntersectionType { NO_INTERSECTION, PARTIAL_INTERSECTION, CONTAINS_INTERSECTION }; + + IntersectionType getIntersectionType(const Box& box) const; + +private: + + static const int VERTEX_COUNT = 8; + static const int SIDE_EXTENT_COUNT = 5; + static const int CROSS_PRODUCT_EXTENT_COUNT = 18; + + glm::vec3 _vertices[VERTEX_COUNT]; + Box _bounds; + AxisExtents _sideExtents[SIDE_EXTENT_COUNT]; + AxisExtents _crossProductExtents[CROSS_PRODUCT_EXTENT_COUNT]; +}; + /// Editor for meta-object values. class QMetaObjectEditor : public QWidget { Q_OBJECT @@ -101,6 +144,8 @@ public: QColorEditor(QWidget* parent); + const QColor& getColor() const { return _color; } + signals: void colorChanged(const QColor& color); @@ -153,6 +198,9 @@ public: BaseVec3Editor(QWidget* parent); + void setSingleStep(double singleStep); + double getSingleStep() const; + protected slots: virtual void updateValue() = 0; @@ -175,6 +223,8 @@ public: Vec3Editor(QWidget* parent); + const glm::vec3& getValue() const { return _value; } + signals: void valueChanged(const glm::vec3& vector); diff --git a/libraries/models/CMakeLists.txt b/libraries/models/CMakeLists.txt index 8056d215da..858d01efa1 100644 --- a/libraries/models/CMakeLists.txt +++ b/libraries/models/CMakeLists.txt @@ -1,39 +1,11 @@ -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 models) -find_package(Qt5Widgets REQUIRED) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network Script) -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} "${ROOT_DIR}") +include_glm() -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME} "${AUTOMTC_SRC}") +link_hifi_libraries(shared octree fbx networking animation) -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(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}") - -# link ZLIB -find_package(ZLIB) - -include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/models/src/ModelEditPacketSender.cpp b/libraries/models/src/ModelEditPacketSender.cpp index 5059b8891b..c21a4a236a 100644 --- a/libraries/models/src/ModelEditPacketSender.cpp +++ b/libraries/models/src/ModelEditPacketSender.cpp @@ -38,7 +38,7 @@ void ModelEditPacketSender::sendEditModelMessage(PacketType type, ModelItemID mo } } -void ModelEditPacketSender::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew) { +void ModelEditPacketSender::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew) { ModelItem::adjustEditPacketForClockSkew(codeColorBuffer, length, clockSkew); } diff --git a/libraries/models/src/ModelEditPacketSender.h b/libraries/models/src/ModelEditPacketSender.h index 198c9a3be2..332f7f729d 100644 --- a/libraries/models/src/ModelEditPacketSender.h +++ b/libraries/models/src/ModelEditPacketSender.h @@ -32,6 +32,6 @@ public: // My server type is the model server virtual char getMyNodeType() const { return NodeType::ModelServer; } - virtual void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew); + virtual void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew); }; #endif // hifi_ModelEditPacketSender_h diff --git a/libraries/models/src/ModelItem.cpp b/libraries/models/src/ModelItem.cpp index 750af8f1b6..e555aff7ed 100644 --- a/libraries/models/src/ModelItem.cpp +++ b/libraries/models/src/ModelItem.cpp @@ -11,19 +11,12 @@ #include +#include #include #include -#include // usecTimestampNow() #include #include - -// This is not ideal, but adding script-engine as a linked library, will cause a circular reference -// I'm open to other potential solutions. Could we change cmake to allow libraries to reference each others -// headers, but not link to each other, this is essentially what this construct is doing, but would be -// better to add includes to the include path, but not link -#include "../../script-engine/src/ScriptEngine.h" - #include "ModelsScriptingInterface.h" #include "ModelItem.h" #include "ModelTree.h" @@ -641,7 +634,7 @@ bool ModelItem::encodeModelEditMessageDetails(PacketType command, ModelItemID id } // adjust any internal timestamps to fix clock skew for this server -void ModelItem::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew) { +void ModelItem::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew) { unsigned char* dataAt = codeColorBuffer; int octets = numberOfThreeBitSectionsInCode(dataAt); int lengthOfOctcode = bytesRequiredForCodeLength(octets); @@ -1157,6 +1150,38 @@ void ModelItemProperties::copyFromModelItem(const ModelItem& modelItem) { _defaultSettings = false; } +void ModelItemProperties::copyFromNewModelItem(const ModelItem& modelItem) { + _position = modelItem.getPosition() * (float) TREE_SCALE; + _color = modelItem.getXColor(); + _radius = modelItem.getRadius() * (float) TREE_SCALE; + _shouldDie = modelItem.getShouldDie(); + _modelURL = modelItem.getModelURL(); + _modelRotation = modelItem.getModelRotation(); + _animationURL = modelItem.getAnimationURL(); + _animationIsPlaying = modelItem.getAnimationIsPlaying(); + _animationFrameIndex = modelItem.getAnimationFrameIndex(); + _animationFPS = modelItem.getAnimationFPS(); + _glowLevel = modelItem.getGlowLevel(); + _sittingPoints = modelItem.getSittingPoints(); + + _id = modelItem.getID(); + _idSet = true; + + _positionChanged = true; + _colorChanged = true; + _radiusChanged = true; + + _shouldDieChanged = true; + _modelURLChanged = true; + _modelRotationChanged = true; + _animationURLChanged = true; + _animationIsPlayingChanged = true; + _animationFrameIndexChanged = true; + _animationFPSChanged = true; + _glowLevelChanged = true; + _defaultSettings = true; +} + QScriptValue ModelItemPropertiesToScriptValue(QScriptEngine* engine, const ModelItemProperties& properties) { return properties.copyToScriptValue(engine); } diff --git a/libraries/models/src/ModelItem.h b/libraries/models/src/ModelItem.h index 43aaca48a0..be52b1e235 100644 --- a/libraries/models/src/ModelItem.h +++ b/libraries/models/src/ModelItem.h @@ -74,6 +74,7 @@ public: void copyToModelItem(ModelItem& modelItem) const; void copyFromModelItem(const ModelItem& modelItem); + void copyFromNewModelItem(const ModelItem& modelItem); const glm::vec3& getPosition() const { return _position; } xColor getColor() const { return _color; } @@ -178,7 +179,6 @@ void ModelItemIDfromScriptValue(const QScriptValue &object, ModelItemID& propert /// ModelItem class - this is the actual model item class. class ModelItem { - public: ModelItem(); @@ -270,7 +270,7 @@ public: static bool encodeModelEditMessageDetails(PacketType command, ModelItemID id, const ModelItemProperties& details, unsigned char* bufferOut, int sizeIn, int& sizeOut); - static void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew); + static void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew); void update(const quint64& now); diff --git a/libraries/models/src/ModelTree.cpp b/libraries/models/src/ModelTree.cpp index 763f0a969e..206e67078c 100644 --- a/libraries/models/src/ModelTree.cpp +++ b/libraries/models/src/ModelTree.cpp @@ -9,6 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "ModelEditPacketSender.h" +#include "ModelItem.h" + #include "ModelTree.h" ModelTree::ModelTree(bool shouldReaverage) : Octree(shouldReaverage) { @@ -108,6 +111,7 @@ bool FindAndUpdateModelOperator::PostRecursion(OctreeElement* element) { return !_found; // if we haven't yet found it, keep looking } + // TODO: improve this to not use multiple recursions void ModelTree::storeModel(const ModelItem& model, const SharedNodePointer& senderNode) { // First, look for the existing model in the tree.. @@ -199,6 +203,32 @@ void ModelTree::deleteModel(const ModelItemID& modelID) { } } +void ModelTree::sendModels(ModelEditPacketSender* packetSender, float x, float y, float z) { + SendModelsOperationArgs args; + args.packetSender = packetSender; + args.root = glm::vec3(x, y, z); + recurseTreeWithOperation(sendModelsOperation, &args); + packetSender->releaseQueuedMessages(); +} + +bool ModelTree::sendModelsOperation(OctreeElement* element, void* extraData) { + SendModelsOperationArgs* args = static_cast(extraData); + ModelTreeElement* modelTreeElement = static_cast(element); + + const QList& modelList = modelTreeElement->getModels(); + + for (int i = 0; i < modelList.size(); i++) { + uint32_t creatorTokenID = ModelItem::getNextCreatorTokenID(); + ModelItemID id(NEW_MODEL, creatorTokenID, false); + ModelItemProperties properties; + properties.copyFromNewModelItem(modelList.at(i)); + properties.setPosition(properties.getPosition() + args->root); + args->packetSender->queueModelEditMessage(PacketTypeModelAddOrEdit, id, properties); + } + + return true; +} + // scans the tree and handles mapping locally created models to know IDs. // in the event that this tree is also viewing the scene, then we need to also // search the tree to make sure we don't have a duplicate model from the viewing @@ -353,8 +383,28 @@ public: QVector _foundModels; }; +void ModelTree::findModelsInCube(const AACube& cube, QVector& foundModels) { + FindModelsInCubeArgs args(cube); + lockForRead(); + recurseTreeWithOperation(findInCubeOperation, &args); + unlock(); + // swap the two lists of model pointers instead of copy + foundModels.swap(args._foundModels); +} + +bool ModelTree::findInCubeOperation(OctreeElement* element, void* extraData) { + FindModelsInCubeArgs* args = static_cast(extraData); + const AACube& elementCube = element->getAACube(); + if (elementCube.touches(args->_cube)) { + ModelTreeElement* modelTreeElement = static_cast(element); + modelTreeElement->getModelsInside(args->_cube, args->_foundModels); + return true; + } + return false; +} + bool ModelTree::findInCubeForUpdateOperation(OctreeElement* element, void* extraData) { - FindModelsInCubeArgs* args = static_cast< FindModelsInCubeArgs*>(extraData); + FindModelsInCubeArgs* args = static_cast(extraData); const AACube& elementCube = element->getAACube(); if (elementCube.touches(args->_cube)) { ModelTreeElement* modelTreeElement = static_cast(element); @@ -364,7 +414,7 @@ bool ModelTree::findInCubeForUpdateOperation(OctreeElement* element, void* extra return false; } -void ModelTree::findModelsForUpdate(const AACube& cube, QVector foundModels) { +void ModelTree::findModelsForUpdate(const AACube& cube, QVector& foundModels) { FindModelsInCubeArgs args(cube); lockForRead(); recurseTreeWithOperation(findInCubeForUpdateOperation, &args); diff --git a/libraries/models/src/ModelTree.h b/libraries/models/src/ModelTree.h index a2a3c9cd28..9872e332ba 100644 --- a/libraries/models/src/ModelTree.h +++ b/libraries/models/src/ModelTree.h @@ -15,6 +15,8 @@ #include #include "ModelTreeElement.h" +class Model; + class NewlyCreatedModelHook { public: virtual void modelCreated(const ModelItem& newModel, const SharedNodePointer& senderNode) = 0; @@ -23,6 +25,7 @@ public: class ModelItemFBXService { public: virtual const FBXGeometry* getGeometryForModel(const ModelItem& modelItem) = 0; + virtual const Model* getModelForModelItem(const ModelItem& modelItem) = 0; }; class ModelTree : public Octree { @@ -36,7 +39,6 @@ public: /// Type safe version of getRoot() ModelTreeElement* getRoot() { return static_cast(_rootElement); } - // These methods will allow the OctreeServer to send your tree inbound edit packets of your // own definition. Implement these to allow your octree based server to support editing virtual bool getWantSVOfileVersions() const { return true; } @@ -62,12 +64,13 @@ public: /// \param foundModels[out] vector of const ModelItem* /// \remark Side effect: any initial contents in foundModels will be lost void findModels(const glm::vec3& center, float radius, QVector& foundModels); + void findModelsInCube(const AACube& cube, QVector& foundModels); /// finds all models that touch a cube /// \param cube the query cube /// \param foundModels[out] vector of non-const ModelItem* /// \remark Side effect: any initial contents in models will be lost - void findModelsForUpdate(const AACube& cube, QVector foundModels); + void findModelsForUpdate(const AACube& cube, QVector& foundModels); void addNewlyCreatedHook(NewlyCreatedModelHook* hook); void removeNewlyCreatedHook(NewlyCreatedModelHook* hook); @@ -80,14 +83,19 @@ public: void processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode); void handleAddModelResponse(const QByteArray& packet); + ModelItemFBXService* getFBXService() const { return _fbxService; } void setFBXService(ModelItemFBXService* service) { _fbxService = service; } const FBXGeometry* getGeometryForModel(const ModelItem& modelItem) { return _fbxService ? _fbxService->getGeometryForModel(modelItem) : NULL; + } + void sendModels(ModelEditPacketSender* packetSender, float x, float y, float z); private: + static bool sendModelsOperation(OctreeElement* element, void* extraData); static bool updateOperation(OctreeElement* element, void* extraData); + static bool findInCubeOperation(OctreeElement* element, void* extraData); static bool findAndUpdateOperation(OctreeElement* element, void* extraData); static bool findAndUpdateWithIDandPropertiesOperation(OctreeElement* element, void* extraData); static bool findNearPointOperation(OctreeElement* element, void* extraData); diff --git a/libraries/models/src/ModelTreeElement.cpp b/libraries/models/src/ModelTreeElement.cpp index 960d1dd4cb..47ea8babac 100644 --- a/libraries/models/src/ModelTreeElement.cpp +++ b/libraries/models/src/ModelTreeElement.cpp @@ -399,6 +399,19 @@ void ModelTreeElement::getModels(const glm::vec3& searchPosition, float searchRa } } +void ModelTreeElement::getModelsInside(const AACube& box, QVector& foundModels) { + QList::iterator modelItr = _modelItems->begin(); + QList::iterator modelEnd = _modelItems->end(); + AACube modelCube; + while(modelItr != modelEnd) { + ModelItem* model = &(*modelItr); + if (box.contains(model->getPosition())) { + foundModels.push_back(model); + } + ++modelItr; + } +} + void ModelTreeElement::getModelsForUpdate(const AACube& box, QVector& foundModels) { QList::iterator modelItr = _modelItems->begin(); QList::iterator modelEnd = _modelItems->end(); diff --git a/libraries/models/src/ModelTreeElement.h b/libraries/models/src/ModelTreeElement.h index 8d2f5064bd..c0e2e36095 100644 --- a/libraries/models/src/ModelTreeElement.h +++ b/libraries/models/src/ModelTreeElement.h @@ -44,6 +44,12 @@ public: bool isViewing; }; +class SendModelsOperationArgs { +public: + glm::vec3 root; + ModelEditPacketSender* packetSender; +}; + class ModelTreeElement : public OctreeElement { @@ -132,6 +138,8 @@ public: /// \param models[out] vector of non-const ModelItem* void getModelsForUpdate(const AACube& box, QVector& foundModels); + void getModelsInside(const AACube& box, QVector& foundModels); + const ModelItem* getModelWithID(uint32_t id) const; bool removeModelWithID(uint32_t id); diff --git a/libraries/networking/CMakeLists.txt b/libraries/networking/CMakeLists.txt index 9a13374a4f..501437fab2 100644 --- a/libraries/networking/CMakeLists.txt +++ b/libraries/networking/CMakeLists.txt @@ -1,21 +1,14 @@ -set(ROOT_DIR ../..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") - set(TARGET_NAME networking) -project(${TARGET_NAME}) -find_package(Qt5 COMPONENTS Network) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +link_hifi_libraries(shared) -# include GLM -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") - -target_link_libraries(${TARGET_NAME} Qt5::Network) - -# add a definition for ssize_t so that windows doesn't bail if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file + # we need ws2_32.lib on windows, but it's static so we don't bubble it up + target_link_libraries(${TARGET_NAME} ws2_32.lib) +endif () + +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 563d735790..7fda9d74c9 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -15,8 +15,8 @@ #include #include #include +#include #include -#include #include "NodeList.h" #include "PacketHeaders.h" @@ -62,6 +62,8 @@ AccountManager::AccountManager() : qRegisterMetaType("QNetworkAccessManager::Operation"); qRegisterMetaType("JSONCallbackParameters"); + + qRegisterMetaType("QHttpMultiPart*"); connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged); } @@ -142,90 +144,113 @@ void AccountManager::authenticatedRequest(const QString& path, QNetworkAccessMan const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart) { + QMetaObject::invokeMethod(this, "invokedRequest", Q_ARG(const QString&, path), + Q_ARG(bool, true), Q_ARG(QNetworkAccessManager::Operation, operation), Q_ARG(const JSONCallbackParameters&, callbackParams), Q_ARG(const QByteArray&, dataByteArray), Q_ARG(QHttpMultiPart*, dataMultiPart)); } -void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::Operation operation, +void AccountManager::unauthenticatedRequest(const QString& path, QNetworkAccessManager::Operation operation, + const JSONCallbackParameters& callbackParams, + const QByteArray& dataByteArray, + QHttpMultiPart* dataMultiPart) { + + QMetaObject::invokeMethod(this, "invokedRequest", + Q_ARG(const QString&, path), + Q_ARG(bool, false), + Q_ARG(QNetworkAccessManager::Operation, operation), + Q_ARG(const JSONCallbackParameters&, callbackParams), + Q_ARG(const QByteArray&, dataByteArray), + Q_ARG(QHttpMultiPart*, dataMultiPart)); +} + +void AccountManager::invokedRequest(const QString& path, + bool requiresAuthentication, + QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart) { NetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - if (hasValidAccessToken()) { - QNetworkRequest authenticatedRequest; - - QUrl requestURL = _authURL; - - if (path.startsWith("/")) { - requestURL.setPath(path); + QNetworkRequest networkRequest; + + QUrl requestURL = _authURL; + + if (path.startsWith("/")) { + requestURL.setPath(path); + } else { + requestURL.setPath("/" + path); + } + + if (requiresAuthentication) { + if (hasValidAccessToken()) { + requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token); } else { - requestURL.setPath("/" + path); + qDebug() << "No valid access token present. Bailing on authenticated invoked request."; + return; } - - requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token); - - authenticatedRequest.setUrl(requestURL); - - if (VERBOSE_HTTP_REQUEST_DEBUGGING) { - qDebug() << "Making an authenticated request to" << qPrintable(requestURL.toString()); - - if (!dataByteArray.isEmpty()) { - qDebug() << "The POST/PUT body -" << QString(dataByteArray); - } + } + + networkRequest.setUrl(requestURL); + + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { + qDebug() << "Making a request to" << qPrintable(requestURL.toString()); + + if (!dataByteArray.isEmpty()) { + qDebug() << "The POST/PUT body -" << QString(dataByteArray); } - - QNetworkReply* networkReply = NULL; - - switch (operation) { - case QNetworkAccessManager::GetOperation: - networkReply = networkAccessManager.get(authenticatedRequest); - break; - case QNetworkAccessManager::PostOperation: - case QNetworkAccessManager::PutOperation: - if (dataMultiPart) { - if (operation == QNetworkAccessManager::PostOperation) { - networkReply = networkAccessManager.post(authenticatedRequest, dataMultiPart); - } else { - networkReply = networkAccessManager.put(authenticatedRequest, dataMultiPart); - } - dataMultiPart->setParent(networkReply); + } + + QNetworkReply* networkReply = NULL; + + switch (operation) { + case QNetworkAccessManager::GetOperation: + networkReply = networkAccessManager.get(networkRequest); + break; + case QNetworkAccessManager::PostOperation: + case QNetworkAccessManager::PutOperation: + if (dataMultiPart) { + if (operation == QNetworkAccessManager::PostOperation) { + networkReply = networkAccessManager.post(networkRequest, dataMultiPart); } else { - authenticatedRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - if (operation == QNetworkAccessManager::PostOperation) { - networkReply = networkAccessManager.post(authenticatedRequest, dataByteArray); - } else { - networkReply = networkAccessManager.put(authenticatedRequest, dataByteArray); - } + networkReply = networkAccessManager.put(networkRequest, dataMultiPart); } - - break; - case QNetworkAccessManager::DeleteOperation: - networkReply = networkAccessManager.sendCustomRequest(authenticatedRequest, "DELETE"); - break; - default: - // other methods not yet handled - break; - } - - if (networkReply) { - if (!callbackParams.isEmpty()) { - // if we have information for a callback, insert the callbackParams into our local map - _pendingCallbackMap.insert(networkReply, callbackParams); - - if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) { - callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)), - callbackParams.updateSlot.toStdString().c_str()); + dataMultiPart->setParent(networkReply); + } else { + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (operation == QNetworkAccessManager::PostOperation) { + networkReply = networkAccessManager.post(networkRequest, dataByteArray); + } else { + networkReply = networkAccessManager.put(networkRequest, dataByteArray); } } - - // if we ended up firing of a request, hook up to it now - connect(networkReply, SIGNAL(finished()), SLOT(processReply())); + + break; + case QNetworkAccessManager::DeleteOperation: + networkReply = networkAccessManager.sendCustomRequest(networkRequest, "DELETE"); + break; + default: + // other methods not yet handled + break; + } + + if (networkReply) { + if (!callbackParams.isEmpty()) { + // if we have information for a callback, insert the callbackParams into our local map + _pendingCallbackMap.insert(networkReply, callbackParams); + + if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) { + callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)), + callbackParams.updateSlot.toStdString().c_str()); + } } + + // if we ended up firing of a request, hook up to it now + connect(networkReply, SIGNAL(finished()), SLOT(processReply())); } } @@ -276,6 +301,7 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Received error response from data-server that has no matching callback."; qDebug() << "Error" << requestReply->error() << "-" << requestReply->errorString(); + qDebug() << requestReply->readAll(); } } } diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 49a39c1a22..64d62cd1c2 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -47,6 +47,12 @@ public: const JSONCallbackParameters& callbackParams = JSONCallbackParameters(), const QByteArray& dataByteArray = QByteArray(), QHttpMultiPart* dataMultiPart = NULL); + + void unauthenticatedRequest(const QString& path, + QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, + const JSONCallbackParameters& callbackParams = JSONCallbackParameters(), + const QByteArray& dataByteArray = QByteArray(), + QHttpMultiPart* dataMultiPart = NULL); const QUrl& getAuthURL() const { return _authURL; } void setAuthURL(const QUrl& authURL); @@ -88,7 +94,9 @@ private: void passSuccessToCallback(QNetworkReply* reply); void passErrorToCallback(QNetworkReply* reply); - Q_INVOKABLE void invokedRequest(const QString& path, QNetworkAccessManager::Operation operation, + Q_INVOKABLE void invokedRequest(const QString& path, + bool requiresAuthentication, + QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index f603d21240..91166129ad 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -9,6 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + +#include + +#include "Assignment.h" #include "NodeList.h" #include "PacketHeaders.h" #include "UserActivityLogger.h" @@ -21,7 +26,9 @@ DomainHandler::DomainHandler(QObject* parent) : _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), _assignmentUUID(), _isConnected(false), - _handshakeTimer(NULL) + _handshakeTimer(NULL), + _settingsObject(), + _failedSettingsRequests(0) { } @@ -29,6 +36,7 @@ DomainHandler::DomainHandler(QObject* parent) : void DomainHandler::clearConnectionInfo() { _uuid = QUuid(); _isConnected = false; + emit disconnectedFromDomain(); if (_handshakeTimer) { _handshakeTimer->stop(); @@ -37,8 +45,18 @@ void DomainHandler::clearConnectionInfo() { } } -void DomainHandler::reset() { +void DomainHandler::clearSettings() { + _settingsObject = QJsonObject(); + _failedSettingsRequests = 0; +} + +void DomainHandler::softReset() { clearConnectionInfo(); + clearSettings(); +} + +void DomainHandler::hardReset() { + softReset(); _hostname = QString(); _sockAddr.setAddress(QHostAddress::Null); } @@ -46,7 +64,7 @@ void DomainHandler::reset() { void DomainHandler::setSockAddr(const HifiSockAddr& sockAddr, const QString& hostname) { if (_sockAddr != sockAddr) { // we should reset on a sockAddr change - reset(); + hardReset(); // change the sockAddr _sockAddr = sockAddr; } @@ -59,7 +77,7 @@ void DomainHandler::setHostname(const QString& hostname) { if (hostname != _hostname) { // re-set the domain info so that auth information is reloaded - reset(); + hardReset(); int colonIndex = hostname.indexOf(':'); @@ -109,10 +127,66 @@ void DomainHandler::setIsConnected(bool isConnected) { if (_isConnected) { emit connectedToDomain(_hostname); + + // we've connected to new domain - time to ask it for global settings + requestDomainSettings(); + } else { + emit disconnectedFromDomain(); } } } +void DomainHandler::requestDomainSettings() const { + if (_settingsObject.isEmpty()) { + // setup the URL required to grab settings JSON + QUrl settingsJSONURL; + settingsJSONURL.setScheme("http"); + settingsJSONURL.setHost(_hostname); + settingsJSONURL.setPort(DOMAIN_SERVER_HTTP_PORT); + settingsJSONURL.setPath("/settings.json"); + Assignment::Type assignmentType = Assignment::typeForNodeType(NodeList::getInstance()->getOwnerType()); + settingsJSONURL.setQuery(QString("type=%1").arg(assignmentType)); + + qDebug() << "Requesting domain-server settings at" << settingsJSONURL.toString(); + + QNetworkReply* reply = NetworkAccessManager::getInstance().get(QNetworkRequest(settingsJSONURL)); + connect(reply, &QNetworkReply::finished, this, &DomainHandler::settingsRequestFinished); + } +} + +const int MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS = 5; + +void DomainHandler::settingsRequestFinished() { + QNetworkReply* settingsReply = reinterpret_cast(sender()); + + int replyCode = settingsReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (settingsReply->error() == QNetworkReply::NoError && replyCode != 301 && replyCode != 302) { + // parse the JSON to a QJsonObject and save it + _settingsObject = QJsonDocument::fromJson(settingsReply->readAll()).object(); + + qDebug() << "Received domain settings."; + emit settingsReceived(_settingsObject); + + // reset failed settings requests to 0, we got them + _failedSettingsRequests = 0; + } else { + // error grabbing the settings - in some cases this means we are stuck + // so we should retry until we get it + qDebug() << "Error getting domain settings -" << settingsReply->errorString() << "- retrying"; + + if (++_failedSettingsRequests >= MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS) { + qDebug() << "Failed to retreive domain-server settings" << MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS + << "times. Re-setting connection to domain."; + clearSettings(); + clearConnectionInfo(); + emit settingsReceiveFail(); + } else { + requestDomainSettings(); + } + } +} + void DomainHandler::parseDTLSRequirementPacket(const QByteArray& dtlsRequirementPacket) { // figure out the port that the DS wants us to use for us to talk to them with DTLS int numBytesPacketHeader = numBytesForPacketHeader(dtlsRequirementPacket); @@ -125,4 +199,4 @@ void DomainHandler::parseDTLSRequirementPacket(const QByteArray& dtlsRequirement _sockAddr.setPort(dtlsPort); // initializeDTLSSession(); -} \ No newline at end of file +} diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 782332f06a..91caddca22 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -12,6 +12,7 @@ #ifndef hifi_DomainHandler_h #define hifi_DomainHandler_h +#include #include #include #include @@ -33,6 +34,7 @@ public: DomainHandler(QObject* parent = 0); void clearConnectionInfo(); + void clearSettings(); const QUuid& getUUID() const { return _uuid; } void setUUID(const QUuid& uuid) { _uuid = uuid; } @@ -54,16 +56,27 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); + bool hasSettings() const { return !_settingsObject.isEmpty(); } + void requestDomainSettings() const; + const QJsonObject& getSettingsObject() const { return _settingsObject; } + void parseDTLSRequirementPacket(const QByteArray& dtlsRequirementPacket); + void softReset(); + private slots: void completedHostnameLookup(const QHostInfo& hostInfo); + void settingsRequestFinished(); signals: void hostnameChanged(const QString& hostname); void connectedToDomain(const QString& hostname); + void disconnectedFromDomain(); + + void settingsReceived(const QJsonObject& domainSettingsObject); + void settingsReceiveFail(); private: - void reset(); + void hardReset(); QUuid _uuid; QString _hostname; @@ -71,6 +84,8 @@ private: QUuid _assignmentUUID; bool _isConnected; QTimer* _handshakeTimer; + QJsonObject _settingsObject; + int _failedSettingsRequests; }; #endif // hifi_DomainHandler_h diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index c0d7941edf..f50f7493fb 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -82,8 +82,8 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short qDebug() << "NodeList DTLS socket is listening on" << _dtlsSocket->localPort(); } - const int LARGER_SNDBUF_SIZE = 1048576; - changeSendSocketBufferSize(LARGER_SNDBUF_SIZE); + const int LARGER_BUFFER_SIZE = 1048576; + changeSocketBufferSizes(LARGER_BUFFER_SIZE); _packetStatTimer.start(); } @@ -95,7 +95,7 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) { if (sessionUUID != oldUUID) { qDebug() << "NodeList UUID changed from" << uuidStringWithoutCurlyBraces(oldUUID) << "to" << uuidStringWithoutCurlyBraces(_sessionUUID); - emit uuidChanged(sessionUUID); + emit uuidChanged(sessionUUID, oldUUID); } } @@ -129,7 +129,7 @@ QUdpSocket& LimitedNodeList::getDTLSSocket() { return *_dtlsSocket; } -void LimitedNodeList::changeSendSocketBufferSize(int numSendBytes) { +void LimitedNodeList::changeSocketBufferSizes(int numBytes) { // change the socket send buffer size to be 1MB int oldBufferSize = 0; @@ -139,15 +139,28 @@ void LimitedNodeList::changeSendSocketBufferSize(int numSendBytes) { unsigned int sizeOfInt = sizeof(oldBufferSize); #endif - getsockopt(_nodeSocket.socketDescriptor(), SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&oldBufferSize), &sizeOfInt); - - setsockopt(_nodeSocket.socketDescriptor(), SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&numSendBytes), - sizeof(numSendBytes)); - - int newBufferSize = 0; - getsockopt(_nodeSocket.socketDescriptor(), SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&newBufferSize), &sizeOfInt); - - qDebug() << "Changed socket send buffer size from" << oldBufferSize << "to" << newBufferSize << "bytes"; + for (int i = 0; i < 2; i++) { + int bufferOpt = (i == 0) ? SO_SNDBUF : SO_RCVBUF; + + getsockopt(_nodeSocket.socketDescriptor(), SOL_SOCKET, bufferOpt, reinterpret_cast(&oldBufferSize), &sizeOfInt); + + setsockopt(_nodeSocket.socketDescriptor(), SOL_SOCKET, bufferOpt, reinterpret_cast(&numBytes), + sizeof(numBytes)); + + QString bufferTypeString = (i == 0) ? "send" : "receive"; + + if (oldBufferSize < numBytes) { + int newBufferSize = 0; + getsockopt(_nodeSocket.socketDescriptor(), SOL_SOCKET, bufferOpt, reinterpret_cast(&newBufferSize), &sizeOfInt); + + qDebug() << "Changed socket" << bufferTypeString << "buffer size from" << oldBufferSize << "to" + << newBufferSize << "bytes"; + } else { + // don't make the buffer smaller + qDebug() << "Did not change socket" << bufferTypeString << "buffer size from" << oldBufferSize + << "since it is larger than desired size of" << numBytes; + } + } } bool LimitedNodeList::packetVersionAndHashMatch(const QByteArray& packet) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index a4bc8022bf..114d3de910 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -107,7 +107,7 @@ public slots: void killNodeWithUUID(const QUuid& nodeUUID); signals: - void uuidChanged(const QUuid& ownerUUID); + void uuidChanged(const QUuid& ownerUUID, const QUuid& oldUUID); void nodeAdded(SharedNodePointer); void nodeKilled(SharedNodePointer); protected: @@ -123,7 +123,7 @@ protected: NodeHash::iterator killNodeAtHashIterator(NodeHash::iterator& nodeItemToKill); - void changeSendSocketBufferSize(int numSendBytes); + void changeSocketBufferSizes(int numBytes); QUuid _sessionUUID; NodeHash _nodeHash; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index b872ec12cf..992a168244 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -178,7 +178,7 @@ void NodeList::reset() { setSessionUUID(QUuid()); // clear the domain connection information - _domainHandler.clearConnectionInfo(); + _domainHandler.softReset(); // if we setup the DTLS socket, also disconnect from the DTLS socket readyRead() so it can handle handshaking if (_dtlsSocket) { diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp index f17715ddfe..1b48a2e333 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -80,6 +80,8 @@ PacketVersion versionForPacketType(PacketType type) { return 1; case PacketTypeAudioStreamStats: return 1; + case PacketTypeMetavoxelData: + return 1; default: return 0; } diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h index 83350a32d1..436c34e6eb 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -70,6 +70,7 @@ enum PacketType { PacketTypeVoxelEditNack, PacketTypeParticleEditNack, PacketTypeModelEditNack, + PacketTypeSignedTransactionPayment }; typedef char PacketVersion; diff --git a/libraries/networking/src/PacketSender.h b/libraries/networking/src/PacketSender.h index 7d2c0dc8aa..29d9287127 100644 --- a/libraries/networking/src/PacketSender.h +++ b/libraries/networking/src/PacketSender.h @@ -39,10 +39,6 @@ public: ~PacketSender(); /// Add packet to outbound queue. - /// \param HifiSockAddr& address the destination address - /// \param packetData pointer to data - /// \param ssize_t packetLength size of data - /// \thread any thread, typically the application thread void queuePacketForSending(const SharedNodePointer& destinationNode, const QByteArray& packet); void setPacketsPerSecond(int packetsPerSecond); diff --git a/libraries/networking/src/ReceivedPacketProcessor.h b/libraries/networking/src/ReceivedPacketProcessor.h index 607f9e54c2..d5fc006882 100644 --- a/libraries/networking/src/ReceivedPacketProcessor.h +++ b/libraries/networking/src/ReceivedPacketProcessor.h @@ -24,10 +24,6 @@ public: ReceivedPacketProcessor() { } /// Add packet from network receive thread to the processing queue. - /// \param sockaddr& senderAddress the address of the sender - /// \param packetData pointer to received data - /// \param ssize_t packetLength size of received data - /// \thread network receive thread void queueReceivedPacket(const SharedNodePointer& sendingNode, const QByteArray& packet); /// Are there received packets waiting to be processed diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index a183e2f9a1..73c01ef582 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -40,6 +40,15 @@ void ResourceCache::refresh(const QUrl& url) { } QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& fallback, bool delayLoad, void* extra) { + + if (QThread::currentThread() != thread()) { + QSharedPointer result; + QMetaObject::invokeMethod(this, "getResource", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QSharedPointer, result), Q_ARG(const QUrl&, url), Q_ARG(const QUrl&, fallback), + Q_ARG(bool, delayLoad), Q_ARG(void*, extra)); + return result; + } + if (!url.isValid() && !url.isEmpty() && fallback.isValid()) { return getResource(fallback, QUrl(), delayLoad); } diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 1593ad45fc..c3a5974da7 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -52,7 +52,7 @@ protected: /// \param fallback a fallback URL to load if the desired one is unavailable /// \param delayLoad if true, don't load the resource immediately; wait until load is first requested /// \param extra extra data to pass to the creator, if appropriate - QSharedPointer getResource(const QUrl& url, const QUrl& fallback = QUrl(), + Q_INVOKABLE QSharedPointer getResource(const QUrl& url, const QUrl& fallback = QUrl(), bool delayLoad = false, void* extra = NULL); /// Creates a new resource. diff --git a/libraries/networking/src/SequenceNumberStats.cpp b/libraries/networking/src/SequenceNumberStats.cpp index 66d57500a5..f472159164 100644 --- a/libraries/networking/src/SequenceNumberStats.cpp +++ b/libraries/networking/src/SequenceNumberStats.cpp @@ -13,19 +13,24 @@ #include -SequenceNumberStats::SequenceNumberStats(int statsHistoryLength) - : _lastReceived(std::numeric_limits::max()), +SequenceNumberStats::SequenceNumberStats(int statsHistoryLength, bool canDetectOutOfSync) + : _lastReceivedSequence(0), _missingSet(), _stats(), _lastSenderUUID(), - _statsHistory(statsHistoryLength) + _statsHistory(statsHistoryLength), + _lastUnreasonableSequence(0), + _consecutiveUnreasonableOnTime(0) { } void SequenceNumberStats::reset() { _missingSet.clear(); _stats = PacketStreamStats(); + _lastSenderUUID = QUuid(); _statsHistory.clear(); + _lastUnreasonableSequence = 0; + _consecutiveUnreasonableOnTime = 0; } static const int UINT16_RANGE = std::numeric_limits::max() + 1; @@ -36,7 +41,7 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui // if the sender node has changed, reset all stats if (senderUUID != _lastSenderUUID) { - if (_stats._numReceived > 0) { + if (_stats._received > 0) { qDebug() << "sequence number stats was reset due to new sender node"; qDebug() << "previous:" << _lastSenderUUID << "current:" << senderUUID; reset(); @@ -45,13 +50,14 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui } // determine our expected sequence number... handle rollover appropriately - quint16 expected = _stats._numReceived > 0 ? _lastReceived + (quint16)1 : incoming; + quint16 expected = _stats._received > 0 ? _lastReceivedSequence + (quint16)1 : incoming; - _stats._numReceived++; + _stats._received++; if (incoming == expected) { // on time arrivalInfo._status = OnTime; - _lastReceived = incoming; + _lastReceivedSequence = incoming; + _stats._expectedReceived++; } else { // out of order if (wantExtraDebugging) { @@ -74,14 +80,15 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui } else if (absGap > MAX_REASONABLE_SEQUENCE_GAP) { arrivalInfo._status = Unreasonable; - // ignore packet if gap is unreasonable - qDebug() << "ignoring unreasonable sequence number:" << incoming - << "previous:" << _lastReceived; - _stats._numUnreasonable++; + qDebug() << "unreasonable sequence number:" << incoming << "previous:" << _lastReceivedSequence; + _stats._unreasonable++; + + receivedUnreasonable(incoming); return arrivalInfo; } + // now that rollover has been corrected for (if it occurred), incomingInt and expectedInt can be // compared to each other directly, though one of them might be negative @@ -94,16 +101,17 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui qDebug() << "this packet is earlier than expected..."; qDebug() << ">>>>>>>> missing gap=" << (incomingInt - expectedInt); } - - _stats._numEarly++; - _stats._numLost += (incomingInt - expectedInt); - _lastReceived = incoming; + int skipped = incomingInt - expectedInt; + _stats._early++; + _stats._lost += skipped; + _stats._expectedReceived += (skipped + 1); + _lastReceivedSequence = incoming; // add all sequence numbers that were skipped to the missing sequence numbers list for (int missingInt = expectedInt; missingInt < incomingInt; missingInt++) { _missingSet.insert((quint16)(missingInt < 0 ? missingInt + UINT16_RANGE : missingInt)); } - + // prune missing sequence list if it gets too big; sequence numbers that are older than MAX_REASONABLE_SEQUENCE_GAP // will be removed. if (_missingSet.size() > MAX_REASONABLE_SEQUENCE_GAP) { @@ -114,32 +122,76 @@ SequenceNumberStats::ArrivalInfo SequenceNumberStats::sequenceNumberReceived(qui qDebug() << "this packet is later than expected..."; } - _stats._numLate++; + _stats._late++; // do not update _lastReceived; it shouldn't become smaller // remove this from missing sequence number if it's in there if (_missingSet.remove(incoming)) { - arrivalInfo._status = Late; + arrivalInfo._status = Recovered; if (wantExtraDebugging) { qDebug() << "found it in _missingSet"; } - _stats._numLost--; - _stats._numRecovered++; + _stats._lost--; + _stats._recovered++; } else { - arrivalInfo._status = Duplicate; + // this late seq num is not in our missing set. it is possibly a duplicate, or possibly a late + // packet that should have arrived before our first received packet. we'll count these + // as unreasonable. - if (wantExtraDebugging) { - qDebug() << "sequence:" << incoming << "was NOT found in _missingSet and is probably a duplicate"; - } - _stats._numDuplicate++; + arrivalInfo._status = Unreasonable; + + qDebug() << "unreasonable sequence number:" << incoming << "(possible duplicate)"; + + _stats._unreasonable++; + + receivedUnreasonable(incoming); + return arrivalInfo; } } } + + // if we've made it here, we received a reasonable seq number. + _consecutiveUnreasonableOnTime = 0; + return arrivalInfo; } +void SequenceNumberStats::receivedUnreasonable(quint16 incoming) { + + const int CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD = 8; + + quint16 expected = _consecutiveUnreasonableOnTime > 0 ? _lastUnreasonableSequence + (quint16)1 : incoming; + if (incoming == expected) { + _consecutiveUnreasonableOnTime++; + _lastUnreasonableSequence = incoming; + + if (_consecutiveUnreasonableOnTime >= CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD) { + // we've received many unreasonable seq numbers in a row, all in order. we're probably out of sync with + // the seq num sender. update our state to get back in sync with the sender. + + _lastReceivedSequence = incoming; + _missingSet.clear(); + + _stats._received = CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD; + _stats._unreasonable = 0; + _stats._early = 0; + _stats._late = 0; + _stats._lost = 0; + _stats._recovered = 0; + _stats._expectedReceived = CONSECUTIVE_UNREASONABLE_ON_TIME_THRESHOLD; + + _statsHistory.clear(); + _consecutiveUnreasonableOnTime = 0; + + qDebug() << "re-synced with sequence number sender"; + } + } else { + _consecutiveUnreasonableOnTime = 0; + } +} + void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) { if (wantExtraDebugging) { qDebug() << "pruning _missingSet! size:" << _missingSet.size(); @@ -148,7 +200,7 @@ void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) { // some older sequence numbers may be from before a rollover point; this must be handled. // some sequence numbers in this list may be larger than _incomingLastSequence, indicating that they were received // before the most recent rollover. - int cutoff = (int)_lastReceived - MAX_REASONABLE_SEQUENCE_GAP; + int cutoff = (int)_lastReceivedSequence - MAX_REASONABLE_SEQUENCE_GAP; if (cutoff >= 0) { quint16 nonRolloverCutoff = (quint16)cutoff; QSet::iterator i = _missingSet.begin(); @@ -159,7 +211,7 @@ void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) { qDebug() << "old age cutoff:" << nonRolloverCutoff; } - if (missing > _lastReceived || missing < nonRolloverCutoff) { + if (missing > _lastReceivedSequence || missing < nonRolloverCutoff) { i = _missingSet.erase(i); if (wantExtraDebugging) { qDebug() << "pruning really old missing sequence:" << missing; @@ -178,7 +230,7 @@ void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) { qDebug() << "old age cutoff:" << rolloverCutoff; } - if (missing > _lastReceived && missing < rolloverCutoff) { + if (missing > _lastReceivedSequence && missing < rolloverCutoff) { i = _missingSet.erase(i); if (wantExtraDebugging) { qDebug() << "pruning really old missing sequence:" << missing; @@ -194,21 +246,26 @@ PacketStreamStats SequenceNumberStats::getStatsForHistoryWindow() const { const PacketStreamStats* newestStats = _statsHistory.getNewestEntry(); const PacketStreamStats* oldestStats = _statsHistory.get(_statsHistory.getNumEntries() - 1); - + // this catches cases where history is length 1 or 0 (both are NULL in case of 0) if (newestStats == oldestStats) { return PacketStreamStats(); } // calculate difference between newest stats and oldest stats to get window stats - PacketStreamStats windowStats; - windowStats._numReceived = newestStats->_numReceived - oldestStats->_numReceived; - windowStats._numUnreasonable = newestStats->_numUnreasonable - oldestStats->_numUnreasonable; - windowStats._numEarly = newestStats->_numEarly - oldestStats->_numEarly; - windowStats._numLate = newestStats->_numLate - oldestStats->_numLate; - windowStats._numLost = newestStats->_numLost - oldestStats->_numLost; - windowStats._numRecovered = newestStats->_numRecovered - oldestStats->_numRecovered; - windowStats._numDuplicate = newestStats->_numDuplicate - oldestStats->_numDuplicate; - - return windowStats; + return *newestStats - *oldestStats; } + +PacketStreamStats SequenceNumberStats::getStatsForLastHistoryInterval() const { + + const PacketStreamStats* newestStats = _statsHistory.getNewestEntry(); + const PacketStreamStats* secondNewestStats = _statsHistory.get(1); + + // this catches cases where history is length 1 or 0 (both are NULL in case of 0) + if (newestStats == NULL || secondNewestStats == NULL) { + return PacketStreamStats(); + } + + return *newestStats - *secondNewestStats; +} + diff --git a/libraries/networking/src/SequenceNumberStats.h b/libraries/networking/src/SequenceNumberStats.h index f4e85b6fb3..434d5a18db 100644 --- a/libraries/networking/src/SequenceNumberStats.h +++ b/libraries/networking/src/SequenceNumberStats.h @@ -18,32 +18,40 @@ const int MAX_REASONABLE_SEQUENCE_GAP = 1000; + class PacketStreamStats { public: PacketStreamStats() - : _numReceived(0), - _numUnreasonable(0), - _numEarly(0), - _numLate(0), - _numLost(0), - _numRecovered(0), - _numDuplicate(0) + : _received(0), + _unreasonable(0), + _early(0), + _late(0), + _lost(0), + _recovered(0), + _expectedReceived(0) {} - float getUnreasonableRate() const { return (float)_numUnreasonable / _numReceived; } - float getNumEaryRate() const { return (float)_numEarly / _numReceived; } - float getLateRate() const { return (float)_numLate / _numReceived; } - float getLostRate() const { return (float)_numLost / _numReceived; } - float getRecoveredRate() const { return (float)_numRecovered / _numReceived; } - float getDuplicateRate() const { return (float)_numDuplicate / _numReceived; } + PacketStreamStats operator-(const PacketStreamStats& rhs) const { + PacketStreamStats diff; + diff._received = _received - rhs._received; + diff._unreasonable = _unreasonable - rhs._unreasonable; + diff._early = _early - rhs._early; + diff._late = _late - rhs._late; + diff._lost = _lost - rhs._lost; + diff._recovered = _recovered - rhs._recovered; + diff._expectedReceived = _expectedReceived - rhs._expectedReceived; + return diff; + } - quint32 _numReceived; - quint32 _numUnreasonable; - quint32 _numEarly; - quint32 _numLate; - quint32 _numLost; - quint32 _numRecovered; - quint32 _numDuplicate; + float getLostRate() const { return (float)_lost / _expectedReceived; } + + quint32 _received; + quint32 _unreasonable; + quint32 _early; + quint32 _late; + quint32 _lost; + quint32 _recovered; + quint32 _expectedReceived; }; class SequenceNumberStats { @@ -52,8 +60,7 @@ public: OnTime, Unreasonable, Early, - Late, // recovered - Duplicate + Recovered, }; class ArrivalInfo { @@ -63,27 +70,32 @@ public: }; - SequenceNumberStats(int statsHistoryLength = 0); + SequenceNumberStats(int statsHistoryLength = 0, bool canDetectOutOfSync = true); void reset(); ArrivalInfo sequenceNumberReceived(quint16 incoming, QUuid senderUUID = QUuid(), const bool wantExtraDebugging = false); void pruneMissingSet(const bool wantExtraDebugging = false); void pushStatsToHistory() { _statsHistory.insert(_stats); } - quint32 getNumReceived() const { return _stats._numReceived; } - quint32 getNumUnreasonable() const { return _stats._numUnreasonable; } - quint32 getNumOutOfOrder() const { return _stats._numEarly + _stats._numLate; } - quint32 getNumEarly() const { return _stats._numEarly; } - quint32 getNumLate() const { return _stats._numLate; } - quint32 getNumLost() const { return _stats._numLost; } - quint32 getNumRecovered() const { return _stats._numRecovered; } - quint32 getNumDuplicate() const { return _stats._numDuplicate; } + quint32 getReceived() const { return _stats._received; } + quint32 getExpectedReceived() const { return _stats._expectedReceived; } + quint32 getUnreasonable() const { return _stats._unreasonable; } + quint32 getOutOfOrder() const { return _stats._early + _stats._late; } + quint32 getEarly() const { return _stats._early; } + quint32 getLate() const { return _stats._late; } + quint32 getLost() const { return _stats._lost; } + quint32 getRecovered() const { return _stats._recovered; } + const PacketStreamStats& getStats() const { return _stats; } PacketStreamStats getStatsForHistoryWindow() const; + PacketStreamStats getStatsForLastHistoryInterval() const; const QSet& getMissingSet() const { return _missingSet; } private: - quint16 _lastReceived; + void receivedUnreasonable(quint16 incoming); + +private: + quint16 _lastReceivedSequence; QSet _missingSet; PacketStreamStats _stats; @@ -91,6 +103,9 @@ private: QUuid _lastSenderUUID; RingBufferHistory _statsHistory; + + quint16 _lastUnreasonableSequence; + int _consecutiveUnreasonableOnTime; }; #endif // hifi_SequenceNumberStats_h diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index 4b92f8ba38..cd80c441c1 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include "Logging.h" @@ -18,7 +19,8 @@ ThreadedAssignment::ThreadedAssignment(const QByteArray& packet) : Assignment(packet), - _isFinished(false) + _isFinished(false), + _datagramProcessingThread(NULL) { } @@ -28,10 +30,25 @@ void ThreadedAssignment::setFinished(bool isFinished) { if (_isFinished) { aboutToFinish(); - emit finished(); + + NodeList* nodeList = NodeList::getInstance(); + + // if we have a datagram processing thread, quit it and wait on it to make sure that + // the node socket is back on the same thread as the NodeList + + if (_datagramProcessingThread) { + // tell the datagram processing thread to quit and wait until it is done, then return the node socket to the NodeList + _datagramProcessingThread->quit(); + _datagramProcessingThread->wait(); + + // set node socket parent back to NodeList + nodeList->getNodeSocket().setParent(nodeList); + } // move the NodeList back to the QCoreApplication instance's thread - NodeList::getInstance()->moveToThread(QCoreApplication::instance()->thread()); + nodeList->moveToThread(QCoreApplication::instance()->thread()); + + emit finished(); } } diff --git a/libraries/networking/src/ThreadedAssignment.h b/libraries/networking/src/ThreadedAssignment.h index e9241d0272..454baa85f2 100644 --- a/libraries/networking/src/ThreadedAssignment.h +++ b/libraries/networking/src/ThreadedAssignment.h @@ -20,6 +20,7 @@ class ThreadedAssignment : public Assignment { Q_OBJECT public: ThreadedAssignment(const QByteArray& packet); + void setFinished(bool isFinished); virtual void aboutToFinish() { }; void addPacketStatsAndSendStatsPacket(QJsonObject& statsObject); @@ -29,15 +30,18 @@ public slots: virtual void run() = 0; virtual void readPendingDatagrams() = 0; virtual void sendStatsPacket(); - +signals: + void finished(); + protected: bool readAvailableDatagram(QByteArray& destinationByteArray, HifiSockAddr& senderSockAddr); void commonInit(const QString& targetName, NodeType_t nodeType, bool shouldSendStats = true); bool _isFinished; + QThread* _datagramProcessingThread; + private slots: void checkInWithDomainServerOrExit(); -signals: - void finished(); + }; typedef QSharedPointer SharedAssignmentPointer; diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 90b9da07dc..e2d3434867 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -70,7 +70,7 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall } void UserActivityLogger::requestFinished(const QJsonObject& object) { - qDebug() << object; + // qDebug() << object; } void UserActivityLogger::requestError(QNetworkReply::NetworkError error,const QString& string) { diff --git a/libraries/octree/CMakeLists.txt b/libraries/octree/CMakeLists.txt index 031f7ef69a..c302a082be 100644 --- a/libraries/octree/CMakeLists.txt +++ b/libraries/octree/CMakeLists.txt @@ -1,30 +1,19 @@ -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 octree) -find_package(Qt5Widgets REQUIRED) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library() -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +include_glm() -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +link_hifi_libraries(shared networking) -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") - -# link ZLIB -find_package(ZLIB) +# find ZLIB +find_package(ZLIB REQUIRED) include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets) -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# append ZLIB and OpenSSL to our list of libraries to link +list(APPEND ${TARGET_NAME}_LIBRARIES_TO_LINK "${ZLIB_LIBRARIES}") + +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/octree/src/EditPacketBuffer.cpp b/libraries/octree/src/EditPacketBuffer.cpp new file mode 100644 index 0000000000..6d2d8cc085 --- /dev/null +++ b/libraries/octree/src/EditPacketBuffer.cpp @@ -0,0 +1,30 @@ +// +// EditPacketBuffer.cpp +// libraries/octree/src +// +// Created by Stephen Birarda on 2014-07-30. +// 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 "EditPacketBuffer.h" + +EditPacketBuffer::EditPacketBuffer() : + _nodeUUID(), + _currentType(PacketTypeUnknown), + _currentSize(0), + _satoshiCost(0) +{ + +} + +EditPacketBuffer::EditPacketBuffer(PacketType type, unsigned char* buffer, size_t length, qint64 satoshiCost, QUuid nodeUUID) : + _nodeUUID(nodeUUID), + _currentType(type), + _currentSize(length), + _satoshiCost(satoshiCost) +{ + memcpy(_currentBuffer, buffer, length); +}; \ No newline at end of file diff --git a/libraries/octree/src/EditPacketBuffer.h b/libraries/octree/src/EditPacketBuffer.h new file mode 100644 index 0000000000..e816bf6558 --- /dev/null +++ b/libraries/octree/src/EditPacketBuffer.h @@ -0,0 +1,34 @@ +// +// EditPacketBuffer.h +// libraries/octree/src +// +// Created by Stephen Birarda on 2014-07-30. +// 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_EditPacketBuffer_h +#define hifi_EditPacketBuffer_h + +#include + +#include +#include + +/// Used for construction of edit packets +class EditPacketBuffer { +public: + EditPacketBuffer(); + EditPacketBuffer(PacketType type, unsigned char* codeColorBuffer, size_t length, + qint64 satoshiCost = 0, const QUuid nodeUUID = QUuid()); + + QUuid _nodeUUID; + PacketType _currentType; + unsigned char _currentBuffer[MAX_PACKET_SIZE]; + size_t _currentSize; + qint64 _satoshiCost; +}; + +#endif // hifi_EditPacketBuffer_h \ No newline at end of file diff --git a/libraries/octree/src/JurisdictionListener.cpp b/libraries/octree/src/JurisdictionListener.cpp index 453ff10a42..f3d9e31acc 100644 --- a/libraries/octree/src/JurisdictionListener.cpp +++ b/libraries/octree/src/JurisdictionListener.cpp @@ -35,7 +35,7 @@ void JurisdictionListener::nodeKilled(SharedNodePointer node) { bool JurisdictionListener::queueJurisdictionRequest() { static unsigned char buffer[MAX_PACKET_SIZE]; unsigned char* bufferOut = &buffer[0]; - ssize_t sizeOut = populatePacketHeader(reinterpret_cast(bufferOut), PacketTypeJurisdictionRequest); + int sizeOut = populatePacketHeader(reinterpret_cast(bufferOut), PacketTypeJurisdictionRequest); int nodeCount = 0; NodeList* nodeList = NodeList::getInstance(); diff --git a/libraries/octree/src/JurisdictionListener.h b/libraries/octree/src/JurisdictionListener.h index 01f0392796..eb1c27f2ff 100644 --- a/libraries/octree/src/JurisdictionListener.h +++ b/libraries/octree/src/JurisdictionListener.h @@ -47,10 +47,6 @@ public slots: protected: /// Callback for processing of received packets. Will process any queued PacketType_JURISDICTION and update the /// jurisdiction map member variable - /// \param sockaddr& senderAddress the address of the sender - /// \param packetData pointer to received data - /// \param ssize_t packetLength size of received data - /// \thread "this" individual processing thread virtual void processPacket(const SharedNodePointer& sendingNode, const QByteArray& packet); private: diff --git a/libraries/octree/src/JurisdictionSender.cpp b/libraries/octree/src/JurisdictionSender.cpp index c151999305..d78d883204 100644 --- a/libraries/octree/src/JurisdictionSender.cpp +++ b/libraries/octree/src/JurisdictionSender.cpp @@ -47,7 +47,7 @@ bool JurisdictionSender::process() { // add our packet to our own queue, then let the PacketSender class do the rest of the work. static unsigned char buffer[MAX_PACKET_SIZE]; unsigned char* bufferOut = &buffer[0]; - ssize_t sizeOut = 0; + int sizeOut = 0; if (_jurisdictionMap) { sizeOut = _jurisdictionMap->packIntoMessage(bufferOut, MAX_PACKET_SIZE); diff --git a/libraries/octree/src/OctreeEditPacketSender.cpp b/libraries/octree/src/OctreeEditPacketSender.cpp index 2ed8f6c2a0..d4bdcaa59e 100644 --- a/libraries/octree/src/OctreeEditPacketSender.cpp +++ b/libraries/octree/src/OctreeEditPacketSender.cpp @@ -17,14 +17,6 @@ #include #include "OctreeEditPacketSender.h" -EditPacketBuffer::EditPacketBuffer(PacketType type, unsigned char* buffer, ssize_t length, QUuid nodeUUID) : - _nodeUUID(nodeUUID), - _currentType(type), - _currentSize(length) -{ - memcpy(_currentBuffer, buffer, length); -}; - const int OctreeEditPacketSender::DEFAULT_MAX_PENDING_MESSAGES = PacketSender::DEFAULT_PACKETS_PER_SECOND; @@ -34,7 +26,10 @@ OctreeEditPacketSender::OctreeEditPacketSender() : _maxPendingMessages(DEFAULT_MAX_PENDING_MESSAGES), _releaseQueuedMessagesPending(false), _serverJurisdictions(NULL), - _maxPacketSize(MAX_PACKET_SIZE) { + _maxPacketSize(MAX_PACKET_SIZE), + _destinationWalletUUID() +{ + } OctreeEditPacketSender::~OctreeEditPacketSender() { @@ -87,7 +82,8 @@ bool OctreeEditPacketSender::serversExist() const { // This method is called when the edit packet layer has determined that it has a fully formed packet destined for // a known nodeID. -void OctreeEditPacketSender::queuePacketToNode(const QUuid& nodeUUID, unsigned char* buffer, ssize_t length) { +void OctreeEditPacketSender::queuePacketToNode(const QUuid& nodeUUID, unsigned char* buffer, + size_t length, qint64 satoshiCost) { NodeList* nodeList = NodeList::getInstance(); foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { @@ -101,10 +97,16 @@ void OctreeEditPacketSender::queuePacketToNode(const QUuid& nodeUUID, unsigned c unsigned char* sequenceAt = buffer + numBytesPacketHeader; quint16 sequence = _outgoingSequenceNumbers[nodeUUID]++; memcpy(sequenceAt, &sequence, sizeof(quint16)); - + // send packet QByteArray packet(reinterpret_cast(buffer), length); queuePacketForSending(node, packet); + + if (hasDestinationWalletUUID() && satoshiCost > 0) { + // if we have a destination wallet UUID and a cost associated with this packet, signal that it + // needs to be sent + emit octreePaymentRequired(satoshiCost, nodeUUID, _destinationWalletUUID); + } // add packet to history _sentPacketHistories[nodeUUID].packetSent(sequence, packet); @@ -120,6 +122,7 @@ void OctreeEditPacketSender::queuePacketToNode(const QUuid& nodeUUID, unsigned c qDebug() << "OctreeEditPacketSender::queuePacketToNode() queued " << buffer[0] << " - command to node bytes=" << length << + " satoshiCost=" << satoshiCost << " sequence=" << sequence << " transitTimeSoFar=" << transitTime << " usecs"; } @@ -135,7 +138,7 @@ void OctreeEditPacketSender::processPreServerExistsPackets() { _pendingPacketsLock.lock(); while (!_preServerSingleMessagePackets.empty()) { EditPacketBuffer* packet = _preServerSingleMessagePackets.front(); - queuePacketToNodes(&packet->_currentBuffer[0], packet->_currentSize); + queuePacketToNodes(&packet->_currentBuffer[0], packet->_currentSize, packet->_satoshiCost); delete packet; _preServerSingleMessagePackets.erase(_preServerSingleMessagePackets.begin()); } @@ -157,11 +160,12 @@ void OctreeEditPacketSender::processPreServerExistsPackets() { } } -void OctreeEditPacketSender::queuePendingPacketToNodes(PacketType type, unsigned char* buffer, ssize_t length) { +void OctreeEditPacketSender::queuePendingPacketToNodes(PacketType type, unsigned char* buffer, + size_t length, qint64 satoshiCost) { // If we're asked to save messages while waiting for voxel servers to arrive, then do so... if (_maxPendingMessages > 0) { - EditPacketBuffer* packet = new EditPacketBuffer(type, buffer, length); + EditPacketBuffer* packet = new EditPacketBuffer(type, buffer, length, satoshiCost); _pendingPacketsLock.lock(); _preServerSingleMessagePackets.push_back(packet); // if we've saved MORE than our max, then clear out the oldest packet... @@ -175,7 +179,7 @@ void OctreeEditPacketSender::queuePendingPacketToNodes(PacketType type, unsigned } } -void OctreeEditPacketSender::queuePacketToNodes(unsigned char* buffer, ssize_t length) { +void OctreeEditPacketSender::queuePacketToNodes(unsigned char* buffer, size_t length, qint64 satoshiCost) { if (!_shouldSend) { return; // bail early } @@ -202,7 +206,7 @@ void OctreeEditPacketSender::queuePacketToNodes(unsigned char* buffer, ssize_t l isMyJurisdiction = (map.isMyJurisdiction(octCode, CHECK_NODE_ONLY) == JurisdictionMap::WITHIN); _serverJurisdictions->unlock(); if (isMyJurisdiction) { - queuePacketToNode(nodeUUID, buffer, length); + queuePacketToNode(nodeUUID, buffer, length, satoshiCost); } } } @@ -210,7 +214,8 @@ void OctreeEditPacketSender::queuePacketToNodes(unsigned char* buffer, ssize_t l // NOTE: codeColorBuffer - is JUST the octcode/color and does not contain the packet header! -void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, unsigned char* codeColorBuffer, ssize_t length) { +void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, unsigned char* codeColorBuffer, + size_t length, qint64 satoshiCost) { if (!_shouldSend) { return; // bail early @@ -285,6 +290,7 @@ void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, unsigned ch memcpy(&packetBuffer._currentBuffer[packetBuffer._currentSize], codeColorBuffer, length); packetBuffer._currentSize += length; + packetBuffer._satoshiCost += satoshiCost; } } } @@ -306,7 +312,8 @@ void OctreeEditPacketSender::releaseQueuedMessages() { void OctreeEditPacketSender::releaseQueuedPacket(EditPacketBuffer& packetBuffer) { _releaseQueuedPacketMutex.lock(); if (packetBuffer._currentSize > 0 && packetBuffer._currentType != PacketTypeUnknown) { - queuePacketToNode(packetBuffer._nodeUUID, &packetBuffer._currentBuffer[0], packetBuffer._currentSize); + queuePacketToNode(packetBuffer._nodeUUID, &packetBuffer._currentBuffer[0], + packetBuffer._currentSize, packetBuffer._satoshiCost); packetBuffer._currentSize = 0; packetBuffer._currentType = PacketTypeUnknown; } @@ -326,6 +333,9 @@ void OctreeEditPacketSender::initializePacket(EditPacketBuffer& packetBuffer, Pa packetBuffer._currentSize += sizeof(quint64); // nudge past timestamp packetBuffer._currentType = type; + + // reset cost for packet to 0 + packetBuffer._satoshiCost = 0; } bool OctreeEditPacketSender::process() { diff --git a/libraries/octree/src/OctreeEditPacketSender.h b/libraries/octree/src/OctreeEditPacketSender.h index cdcfc21d4a..cefa5cefb0 100644 --- a/libraries/octree/src/OctreeEditPacketSender.h +++ b/libraries/octree/src/OctreeEditPacketSender.h @@ -15,20 +15,11 @@ #include #include #include + +#include "EditPacketBuffer.h" #include "JurisdictionMap.h" #include "SentPacketHistory.h" -/// Used for construction of edit packets -class EditPacketBuffer { -public: - EditPacketBuffer() : _nodeUUID(), _currentType(PacketTypeUnknown), _currentSize(0) { } - EditPacketBuffer(PacketType type, unsigned char* codeColorBuffer, ssize_t length, const QUuid nodeUUID = QUuid()); - QUuid _nodeUUID; - PacketType _currentType; - unsigned char _currentBuffer[MAX_PACKET_SIZE]; - ssize_t _currentSize; -}; - /// Utility for processing, packing, queueing and sending of outbound edit messages. class OctreeEditPacketSender : public PacketSender { Q_OBJECT @@ -39,7 +30,7 @@ public: /// Queues a single edit message. Will potentially send a pending multi-command packet. Determines which server /// node or nodes the packet should be sent to. Can be called even before servers are known, in which case up to /// MaxPendingMessages will be buffered and processed when servers are known. - void queueOctreeEditMessage(PacketType type, unsigned char* buffer, ssize_t length); + void queueOctreeEditMessage(PacketType type, unsigned char* buffer, size_t length, qint64 satoshiCost = 0); /// Releases all queued messages even if those messages haven't filled an MTU packet. This will move the packed message /// packets onto the send queue. If running in threaded mode, the caller does not need to do any further processing to @@ -90,19 +81,25 @@ public: // you must override these... virtual char getMyNodeType() const = 0; - virtual void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew) { }; + virtual void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew) { }; + + bool hasDestinationWalletUUID() const { return !_destinationWalletUUID.isNull(); } + void setDestinationWalletUUID(const QUuid& destinationWalletUUID) { _destinationWalletUUID = destinationWalletUUID; } + const QUuid& getDestinationWalletUUID() { return _destinationWalletUUID; } + + void processNackPacket(const QByteArray& packet); public slots: void nodeKilled(SharedNodePointer node); -public: - void processNackPacket(const QByteArray& packet); - +signals: + void octreePaymentRequired(qint64 satoshiAmount, const QUuid& nodeUUID, const QUuid& destinationWalletUUID); + protected: bool _shouldSend; - void queuePacketToNode(const QUuid& nodeID, unsigned char* buffer, ssize_t length); - void queuePendingPacketToNodes(PacketType type, unsigned char* buffer, ssize_t length); - void queuePacketToNodes(unsigned char* buffer, ssize_t length); + void queuePacketToNode(const QUuid& nodeID, unsigned char* buffer, size_t length, qint64 satoshiCost = 0); + void queuePendingPacketToNodes(PacketType type, unsigned char* buffer, size_t length, qint64 satoshiCost = 0); + void queuePacketToNodes(unsigned char* buffer, size_t length, qint64 satoshiCost = 0); void initializePacket(EditPacketBuffer& packetBuffer, PacketType type); void releaseQueuedPacket(EditPacketBuffer& packetBuffer); // releases specific queued packet @@ -127,5 +124,7 @@ protected: // TODO: add locks for this and _pendingEditPackets QHash _sentPacketHistories; QHash _outgoingSequenceNumbers; + + QUuid _destinationWalletUUID; }; #endif // hifi_OctreeEditPacketSender_h diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index b54a87bef8..718f2534f8 100644 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -9,7 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include + #include "OctreePacketData.h" bool OctreePacketData::_debug = false; diff --git a/libraries/octree/src/OctreePacketData.h b/libraries/octree/src/OctreePacketData.h index d704923a11..9a6ebfa9ff 100644 --- a/libraries/octree/src/OctreePacketData.h +++ b/libraries/octree/src/OctreePacketData.h @@ -22,7 +22,6 @@ #ifndef hifi_OctreePacketData_h #define hifi_OctreePacketData_h -#include #include "OctreeConstants.h" #include "OctreeElement.h" diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index 687dd18037..94d9bf458e 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include #include "OctreeConstants.h" #include "OctreeQuery.h" diff --git a/libraries/particles/CMakeLists.txt b/libraries/particles/CMakeLists.txt index 76b3373466..27fc816530 100644 --- a/libraries/particles/CMakeLists.txt +++ b/libraries/particles/CMakeLists.txt @@ -1,33 +1,12 @@ -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 particles) -find_package(Qt5Widgets REQUIRED) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Gui Network Script) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +include_glm() -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +link_hifi_libraries(shared octree fbx networking animation) +include_hifi_library_headers(script-engine) -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -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 -find_package(ZLIB) - -include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/particles/src/Particle.cpp b/libraries/particles/src/Particle.cpp index 5fffefd8b1..e3b568365b 100644 --- a/libraries/particles/src/Particle.cpp +++ b/libraries/particles/src/Particle.cpp @@ -13,7 +13,7 @@ #include #include -#include // usecTimestampNow() +#include #include #include @@ -22,7 +22,7 @@ // I'm open to other potential solutions. Could we change cmake to allow libraries to reference each others // headers, but not link to each other, this is essentially what this construct is doing, but would be // better to add includes to the include path, but not link -#include "../../script-engine/src/ScriptEngine.h" +#include #include "ParticlesScriptingInterface.h" #include "Particle.h" @@ -856,7 +856,7 @@ bool Particle::encodeParticleEditMessageDetails(PacketType command, ParticleID i } // adjust any internal timestamps to fix clock skew for this server -void Particle::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew) { +void Particle::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew) { unsigned char* dataAt = codeColorBuffer; int octets = numberOfThreeBitSectionsInCode(dataAt); int lengthOfOctcode = bytesRequiredForCodeLength(octets); diff --git a/libraries/particles/src/Particle.h b/libraries/particles/src/Particle.h index c243363241..1ee0f6e79b 100644 --- a/libraries/particles/src/Particle.h +++ b/libraries/particles/src/Particle.h @@ -292,7 +292,7 @@ public: static bool encodeParticleEditMessageDetails(PacketType command, ParticleID id, const ParticleProperties& details, unsigned char* bufferOut, int sizeIn, int& sizeOut); - static void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew); + static void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew); void applyHardCollision(const CollisionInfo& collisionInfo); diff --git a/libraries/particles/src/ParticleEditPacketSender.cpp b/libraries/particles/src/ParticleEditPacketSender.cpp index 21a910ff16..689b734521 100644 --- a/libraries/particles/src/ParticleEditPacketSender.cpp +++ b/libraries/particles/src/ParticleEditPacketSender.cpp @@ -38,7 +38,7 @@ void ParticleEditPacketSender::sendEditParticleMessage(PacketType type, Particle } } -void ParticleEditPacketSender::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew) { +void ParticleEditPacketSender::adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew) { Particle::adjustEditPacketForClockSkew(codeColorBuffer, length, clockSkew); } diff --git a/libraries/particles/src/ParticleEditPacketSender.h b/libraries/particles/src/ParticleEditPacketSender.h index 5a367347ea..2ee6d84eb8 100644 --- a/libraries/particles/src/ParticleEditPacketSender.h +++ b/libraries/particles/src/ParticleEditPacketSender.h @@ -31,6 +31,6 @@ public: // My server type is the particle server virtual char getMyNodeType() const { return NodeType::ParticleServer; } - virtual void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, ssize_t length, int clockSkew); + virtual void adjustEditPacketForClockSkew(unsigned char* codeColorBuffer, size_t length, int clockSkew); }; #endif // hifi_ParticleEditPacketSender_h diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 2dd40c7ece..dd28e77b7e 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,35 +1,11 @@ -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 script-engine) -find_package(Qt5Widgets REQUIRED) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Gui Network Script Widgets) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +include_glm() -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +link_hifi_libraries(shared octree voxels fbx particles models animation) -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") -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) - -include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/script-engine/src/ArrayBufferPrototype.cpp b/libraries/script-engine/src/ArrayBufferPrototype.cpp index 53ebebc740..9739f67381 100644 --- a/libraries/script-engine/src/ArrayBufferPrototype.cpp +++ b/libraries/script-engine/src/ArrayBufferPrototype.cpp @@ -11,9 +11,15 @@ #include +#include +#include + #include "ArrayBufferClass.h" #include "ArrayBufferPrototype.h" +static const int QCOMPRESS_HEADER_POSITION = 0; +static const int QCOMPRESS_HEADER_SIZE = 4; + Q_DECLARE_METATYPE(QByteArray*) ArrayBufferPrototype::ArrayBufferPrototype(QObject* parent) : QObject(parent) { @@ -43,6 +49,41 @@ QByteArray ArrayBufferPrototype::slice(qint32 begin) const { return ba->mid(begin, -1); } +QByteArray ArrayBufferPrototype::compress() const { + // Compresses the ArrayBuffer data in Zlib format. + QByteArray* ba = thisArrayBuffer(); + + QByteArray buffer = qCompress(*ba); + buffer.remove(QCOMPRESS_HEADER_POSITION, QCOMPRESS_HEADER_SIZE); // Remove Qt's custom header to make it proper Zlib. + + return buffer; +} + +QByteArray ArrayBufferPrototype::recodeImage(const QString& sourceFormat, const QString& targetFormat, qint32 maxSize) const { + // Recodes image data if sourceFormat and targetFormat are different. + // Rescales image data if either dimension is greater than the specified maximum. + QByteArray* ba = thisArrayBuffer(); + + bool mustRecode = sourceFormat.toLower() != targetFormat.toLower(); + + QImage image = QImage::fromData(*ba); + if (image.width() > maxSize || image.height() > maxSize) { + image = image.scaled(maxSize, maxSize, Qt::KeepAspectRatio); + mustRecode = true; + } + + if (mustRecode) { + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + std::string str = targetFormat.toUpper().toStdString(); + const char* format = str.c_str(); + image.save(&buffer, format); + return buffer.data(); + } + + return *ba; +} + QByteArray* ArrayBufferPrototype::thisArrayBuffer() const { return qscriptvalue_cast(thisObject().data()); } diff --git a/libraries/script-engine/src/ArrayBufferPrototype.h b/libraries/script-engine/src/ArrayBufferPrototype.h index 09d4596f28..f9dd667dc4 100644 --- a/libraries/script-engine/src/ArrayBufferPrototype.h +++ b/libraries/script-engine/src/ArrayBufferPrototype.h @@ -23,6 +23,8 @@ public: public slots: QByteArray slice(qint32 begin, qint32 end) const; QByteArray slice(qint32 begin) const; + QByteArray compress() const; + QByteArray recodeImage(const QString& sourceFormat, const QString& targetFormat, qint32 maxSize) const; private: QByteArray* thisArrayBuffer() const; diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 66281883f0..5985858026 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include "Quat.h" diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index df66fa44d5..49cab1a1fb 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -156,7 +156,7 @@ ScriptEngine::ScriptEngine(const QUrl& scriptURL, } else { NetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkReply* reply = networkAccessManager.get(QNetworkRequest(url)); - qDebug() << "Downloading included script at" << url; + qDebug() << "Downloading script at" << url; QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); @@ -681,12 +681,12 @@ void ScriptEngine::include(const QString& includeFile) { #endif QFile scriptFile(fileName); if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { - qDebug() << "Loading file:" << fileName; + qDebug() << "Including file:" << fileName; QTextStream in(&scriptFile); includeContents = in.readAll(); } else { - qDebug() << "ERROR Loading file:" << fileName; - emit errorMessage("ERROR Loading file:" + fileName); + qDebug() << "ERROR Including file:" << fileName; + emit errorMessage("ERROR Including file:" + fileName); } } @@ -699,6 +699,11 @@ void ScriptEngine::include(const QString& includeFile) { } } +void ScriptEngine::load(const QString& loadFile) { + QUrl url = resolveInclude(loadFile); + emit loadScript(url.toString()); +} + void ScriptEngine::nodeKilled(SharedNodePointer node) { _outgoingScriptAudioSequenceNumbers.remove(node->getUUID()); } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index fe39f286be..17cda5e183 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -102,6 +102,7 @@ public slots: void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void include(const QString& includeFile); + void load(const QString& loadfile); void print(const QString& message); void nodeKilled(SharedNodePointer node); @@ -115,6 +116,7 @@ signals: void errorMessage(const QString& message); void runningStateChanged(); void evaluationFinished(QScriptValue result, bool isException); + void loadScript(const QString& scriptName); protected: QString _scriptContents; diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index d9b7312bf4..cb891c2ab1 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -13,10 +13,15 @@ // #include +#include #include +#include #include "XMLHttpRequestClass.h" +#include "ScriptEngine.h" + +Q_DECLARE_METATYPE(QByteArray*) XMLHttpRequestClass::XMLHttpRequestClass(QScriptEngine* engine) : _engine(engine), @@ -33,6 +38,7 @@ XMLHttpRequestClass::XMLHttpRequestClass(QScriptEngine* engine) : _onReadyStateChange(QScriptValue::NullValue), _readyState(XMLHttpRequestClass::UNSENT), _errorCode(QNetworkReply::NoError), + _file(NULL), _timeout(0), _timer(this), _numRedirects(0) { @@ -52,6 +58,20 @@ QScriptValue XMLHttpRequestClass::constructor(QScriptContext* context, QScriptEn QScriptValue XMLHttpRequestClass::getStatus() const { if (_reply) { return QScriptValue(_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } + if(_url.isLocalFile()) { + switch (_errorCode) { + case QNetworkReply::NoError: + return QScriptValue(200); + case QNetworkReply::ContentNotFoundError: + return QScriptValue(404); + case QNetworkReply::ContentAccessDenied: + return QScriptValue(409); + case QNetworkReply::TimeoutError: + return QScriptValue(408); + case QNetworkReply::ContentOperationNotPermittedError: + return QScriptValue(501); + } } return QScriptValue(0); } @@ -60,6 +80,20 @@ QString XMLHttpRequestClass::getStatusText() const { if (_reply) { return _reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); } + if (_url.isLocalFile()) { + switch (_errorCode) { + case QNetworkReply::NoError: + return "OK"; + case QNetworkReply::ContentNotFoundError: + return "Not Found"; + case QNetworkReply::ContentAccessDenied: + return "Conflict"; + case QNetworkReply::TimeoutError: + return "Timeout"; + case QNetworkReply::ContentOperationNotPermittedError: + return "Not Implemented"; + } + } return ""; } @@ -104,6 +138,13 @@ QScriptValue XMLHttpRequestClass::getAllResponseHeaders() const { } return QString(headers.data()); } + if (_url.isLocalFile()) { + QString headers = QString("Content-Type: application/octet-stream\n"); + headers.append("Content-Length: "); + headers.append(QString("%1").arg(_rawResponseData.length())); + headers.append("\n"); + return headers; + } return QScriptValue(""); } @@ -111,6 +152,14 @@ QScriptValue XMLHttpRequestClass::getResponseHeader(const QString& name) const { if (_reply && _reply->hasRawHeader(name.toLatin1())) { return QScriptValue(QString(_reply->rawHeader(name.toLatin1()))); } + if (_url.isLocalFile()) { + if (name.toLower() == "content-type") { + return QString("application/octet-stream"); + } + if (name.toLower() == "content-length") { + return QString("%1").arg(_rawResponseData.length()); + } + } return QScriptValue::NullValue; } @@ -126,34 +175,72 @@ void XMLHttpRequestClass::setReadyState(ReadyState readyState) { 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); + _url.setUrl(url); + _async = async; + + if (_url.isLocalFile()) { + if (_method.toUpper() == "GET" && !_async && username.isEmpty() && password.isEmpty()) { + _file = new QFile(_url.toLocalFile()); + if (!_file->exists()) { + qDebug() << "Can't find file " << _url.fileName(); + abortRequest(); + _errorCode = QNetworkReply::ContentNotFoundError; + setReadyState(DONE); + emit requestComplete(); + } else if (!_file->open(QIODevice::ReadOnly)) { + qDebug() << "Can't open file " << _url.fileName(); + abortRequest(); + //_errorCode = QNetworkReply::ContentConflictError; // TODO: Use this status when update to Qt 5.3 + _errorCode = QNetworkReply::ContentAccessDenied; + setReadyState(DONE); + emit requestComplete(); + } else { + setReadyState(OPENED); + } + } else { + notImplemented(); + } + } else { + if (url.toLower().left(33) == "https://data.highfidelity.io/api/") { + _url.setQuery("access_token=" + AccountManager::getInstance().getAccountInfo().getAccessToken().token); + } + if (!username.isEmpty()) { + _url.setUserName(username); + } + if (!password.isEmpty()) { + _url.setPassword(password); + } + _request.setUrl(_url); + setReadyState(OPENED); + } } } void XMLHttpRequestClass::send() { - send(QString::Null()); + send(QScriptValue::NullValue); } -void XMLHttpRequestClass::send(const QString& data) { +void XMLHttpRequestClass::send(const QScriptValue& data) { if (_readyState == OPENED && !_reply) { if (!data.isNull()) { - _sendData = new QBuffer(this); - _sendData->setData(data.toUtf8()); + if (_url.isLocalFile()) { + notImplemented(); + return; + } else { + _sendData = new QBuffer(this); + if (data.isObject()) { + QByteArray ba = qscriptvalue_cast(data); + _sendData->setData(ba); + } else { + _sendData->setData(data.toString().toUtf8()); + } + } } doSend(); - if (!_async) { + if (!_async && !_url.isLocalFile()) { QEventLoop loop; connect(this, SIGNAL(requestComplete()), &loop, SLOT(quit())); loop.exec(); @@ -162,14 +249,24 @@ void XMLHttpRequestClass::send(const QString& data) { } void XMLHttpRequestClass::doSend() { - _reply = NetworkAccessManager::getInstance().sendCustomRequest(_request, _method.toLatin1(), _sendData); - - connectToReply(_reply); + + if (!_url.isLocalFile()) { + _reply = NetworkAccessManager::getInstance().sendCustomRequest(_request, _method.toLatin1(), _sendData); + connectToReply(_reply); + } if (_timeout > 0) { _timer.start(_timeout); connect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout())); } + + if (_url.isLocalFile()) { + setReadyState(HEADERS_RECEIVED); + setReadyState(LOADING); + _rawResponseData = _file->readAll(); + _file->close(); + requestFinished(); + } } void XMLHttpRequestClass::requestTimeout() { @@ -188,9 +285,16 @@ void XMLHttpRequestClass::requestError(QNetworkReply::NetworkError code) { void XMLHttpRequestClass::requestFinished() { disconnect(&_timer, SIGNAL(timeout()), this, SLOT(requestTimeout())); - _errorCode = _reply->error(); + if (!_url.isLocalFile()) { + _errorCode = _reply->error(); + } else { + _errorCode = QNetworkReply::NoError; + } + if (_errorCode == QNetworkReply::NoError) { - _rawResponseData.append(_reply->readAll()); + if (!_url.isLocalFile()) { + _rawResponseData.append(_reply->readAll()); + } if (_responseType == "json") { _responseData = _engine->evaluate("(" + QString(_rawResponseData.data()) + ")"); @@ -199,11 +303,13 @@ void XMLHttpRequestClass::requestFinished() { _responseData = QScriptValue::NullValue; } } else if (_responseType == "arraybuffer") { - _responseData = QScriptValue(_rawResponseData.data()); + QScriptValue data = _engine->newVariant(QVariant::fromValue(_rawResponseData)); + _responseData = _engine->newObject(reinterpret_cast(_engine)->getArrayBufferClass(), data); } else { _responseData = QScriptValue(QString(_rawResponseData.data())); } } + setReadyState(DONE); emit requestComplete(); } @@ -217,6 +323,19 @@ void XMLHttpRequestClass::abortRequest() { delete _reply; _reply = NULL; } + + if (_file != NULL) { + _file->close(); + _file = NULL; + } +} + +void XMLHttpRequestClass::notImplemented() { + abortRequest(); + //_errorCode = QNetworkReply::OperationNotImplementedError; TODO: Use this status code when update to Qt 5.3 + _errorCode = QNetworkReply::ContentOperationNotPermittedError; + setReadyState(DONE); + emit requestComplete(); } void XMLHttpRequestClass::connectToReply(QNetworkReply* reply) { diff --git a/libraries/script-engine/src/XMLHttpRequestClass.h b/libraries/script-engine/src/XMLHttpRequestClass.h index 48f1a596e1..55bf646476 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.h +++ b/libraries/script-engine/src/XMLHttpRequestClass.h @@ -84,7 +84,7 @@ public slots: void open(const QString& method, const QString& url, bool async = true, const QString& username = "", const QString& password = ""); void send(); - void send(const QString& data); + void send(const QScriptValue& data); QScriptValue getAllResponseHeaders() const; QScriptValue getResponseHeader(const QString& name) const; @@ -97,6 +97,7 @@ private: void connectToReply(QNetworkReply* reply); void disconnectFromReply(QNetworkReply* reply); void abortRequest(); + void notImplemented(); QScriptEngine* _engine; bool _async; @@ -112,6 +113,7 @@ private: QScriptValue _onReadyStateChange; ReadyState _readyState; QNetworkReply::NetworkError _errorCode; + QFile* _file; int _timeout; QTimer _timer; int _numRedirects; diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index f099f424e9..17ccbdc6ce 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -1,33 +1,7 @@ -set(ROOT_DIR ../..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") - set(TARGET_NAME shared) -project(${TARGET_NAME}) -find_package(Qt5 COMPONENTS Network Widgets Xml Script) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Network Widgets) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) - -# include GLM -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") - -set(EXTERNAL_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external") - -if (WIN32) - # include headers for external libraries and InterfaceConfig. - include_directories("${EXTERNAL_ROOT_DIR}") -endif (WIN32) - -# link required libraries on UNIX -if (UNIX AND NOT APPLE) - find_package(Threads REQUIRED) - target_link_libraries(${TARGET_NAME} "${CMAKE_THREAD_LIBS_INIT}") -endif (UNIX AND NOT APPLE) - -# There is something special (bug) about Qt5Scripts, that we have to explicitly add its include -# directory when Qt5 (5.2.1) is compiled from source and is not in a standard place. -include_directories(SYSTEM "${Qt5Script_INCLUDE_DIRS}") - -target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets) +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/shared/external/pthread.h b/libraries/shared/external/pthread.h deleted file mode 100644 index f910eb4b0e..0000000000 --- a/libraries/shared/external/pthread.h +++ /dev/null @@ -1,1403 +0,0 @@ -/* This is an implementation of the threads API of POSIX 1003.1-2001. - * - * -------------------------------------------------------------------------- - * - * Pthreads-win32 - POSIX Threads Library for Win32 - * Copyright(C) 1998 John E. Bossom - * Copyright(C) 1999,2005 Pthreads-win32 contributors - * - * Contact Email: rpj@callisto.canberra.edu.au - * - * The current list of contributors is contained - * in the file CONTRIBUTORS included with the source - * code distribution. The list can also be seen at the - * following World Wide Web location: - * http://sources.redhat.com/pthreads-win32/contributors.html - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library in the file COPYING.LIB; - * if not, write to the Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA - */ - -#if !defined( PTHREAD_H ) -#define PTHREAD_H - -#define PTW32_STATIC_LIB - -/* - * See the README file for an explanation of the pthreads-win32 version - * numbering scheme and how the DLL is named etc. - */ -#define PTW32_VERSION 2,10,0,0 -#define PTW32_VERSION_STRING "2, 10, 0, 0\0" - -/* There are three implementations of cancel cleanup. - * Note that pthread.h is included in both application - * compilation units and also internally for the library. - * The code here and within the library aims to work - * for all reasonable combinations of environments. - * - * The three implementations are: - * - * WIN32 SEH - * C - * C++ - * - * Please note that exiting a push/pop block via - * "return", "exit", "break", or "continue" will - * lead to different behaviour amongst applications - * depending upon whether the library was built - * using SEH, C++, or C. For example, a library built - * with SEH will call the cleanup routine, while both - * C++ and C built versions will not. - */ - -/* - * Define defaults for cleanup code. - * Note: Unless the build explicitly defines one of the following, then - * we default to standard C style cleanup. This style uses setjmp/longjmp - * in the cancellation and thread exit implementations and therefore won't - * do stack unwinding if linked to applications that have it (e.g. - * C++ apps). This is currently consistent with most/all commercial Unix - * POSIX threads implementations. - */ -#if !defined( __CLEANUP_SEH ) && !defined( __CLEANUP_CXX ) && !defined( __CLEANUP_C ) -/* - [i_a] fix for apps using pthreads-Win32: when they do not define __CLEANUP_SEH themselves, - they're screwed as they'll receive the '__CLEANUP_C' macros which do NOT work when - the pthreads library code itself has actually been build with __CLEANUP_SEH, - which is the case when building this stuff in MSVC. - - Hence this section is made to 'sensibly autodetect' the cleanup mode, when it hasn't - been hardwired in the makefiles / project files. - - After all, who expects he MUST define one of these __CLEANUP_XXX defines in his own - code when using pthreads-Win32, for whatever reason. - */ -#if (defined(_MSC_VER) || defined(PTW32_RC_MSC)) -#define __CLEANUP_SEH -#elif defined(__cplusplus) -#define __CLEANUP_CXX -#else -#define __CLEANUP_C -#endif -#endif - -#if defined( __CLEANUP_SEH ) && ( !defined( _MSC_VER ) && !defined(PTW32_RC_MSC)) -#error ERROR [__FILE__, line __LINE__]: SEH is not supported for this compiler. -#endif - -/* - * Stop here if we are being included by the resource compiler. - */ -#if !defined(RC_INVOKED) - -#undef PTW32_LEVEL - -#if defined(_POSIX_SOURCE) -#define PTW32_LEVEL 0 -/* Early POSIX */ -#endif - -#if defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 199309 -#undef PTW32_LEVEL -#define PTW32_LEVEL 1 -/* Include 1b, 1c and 1d */ -#endif - -#if defined(INCLUDE_NP) -#undef PTW32_LEVEL -#define PTW32_LEVEL 2 -/* Include Non-Portable extensions */ -#endif - -#define PTW32_LEVEL_MAX 3 - -#if ( defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200112 ) || !defined(PTW32_LEVEL) -#define PTW32_LEVEL PTW32_LEVEL_MAX -/* Include everything */ -#endif - -#if defined(_UWIN) -# define HAVE_STRUCT_TIMESPEC 1 -# define HAVE_SIGNAL_H 1 -# undef HAVE_PTW32_CONFIG_H -# pragma comment(lib, "pthread") -#endif - -#if defined(__MINGW32__) || defined(__MINGW64__) -# define PTW32_CONFIG_MINGW -#endif -#if defined(_MSC_VER) -# if _MSC_VER < 1300 -# define PTW32_CONFIG_MSVC6 -# endif -# if _MSC_VER < 1400 -# define PTW32_CONFIG_MSVC7 -# endif -#endif - -/* - * ------------------------------------------------------------- - * - * - * Module: pthread.h - * - * Purpose: - * Provides an implementation of PThreads based upon the - * standard: - * - * POSIX 1003.1-2001 - * and - * The Single Unix Specification version 3 - * - * (these two are equivalent) - * - * in order to enhance code portability between Windows, - * various commercial Unix implementations, and Linux. - * - * See the ANNOUNCE file for a full list of conforming - * routines and defined constants, and a list of missing - * routines and constants not defined in this implementation. - * - * Authors: - * There have been many contributors to this library. - * The initial implementation was contributed by - * John Bossom, and several others have provided major - * sections or revisions of parts of the implementation. - * Often significant effort has been contributed to - * find and fix important bugs and other problems to - * improve the reliability of the library, which sometimes - * is not reflected in the amount of code which changed as - * result. - * As much as possible, the contributors are acknowledged - * in the ChangeLog file in the source code distribution - * where their changes are noted in detail. - * - * Contributors are listed in the CONTRIBUTORS file. - * - * As usual, all bouquets go to the contributors, and all - * brickbats go to the project maintainer. - * - * Maintainer: - * The code base for this project is coordinated and - * eventually pre-tested, packaged, and made available by - * - * Ross Johnson - * - * QA Testers: - * Ultimately, the library is tested in the real world by - * a host of competent and demanding scientists and - * engineers who report bugs and/or provide solutions - * which are then fixed or incorporated into subsequent - * versions of the library. Each time a bug is fixed, a - * test case is written to prove the fix and ensure - * that later changes to the code don't reintroduce the - * same error. The number of test cases is slowly growing - * and therefore so is the code reliability. - * - * Compliance: - * See the file ANNOUNCE for the list of implemented - * and not-implemented routines and defined options. - * Of course, these are all defined is this file as well. - * - * Web site: - * The source code and other information about this library - * are available from - * - * http://sources.redhat.com/pthreads-win32/ - * - * ------------------------------------------------------------- - */ - -/* Try to avoid including windows.h */ -#if defined(PTW32_CONFIG_MINGW) && defined(__cplusplus) -#define PTW32_INCLUDE_WINDOWS_H -#endif - -#if defined(PTW32_INCLUDE_WINDOWS_H) -#include -#endif - -#if defined(PTW32_CONFIG_MSVC6) || defined(__DMC__) -/* - * VC++6.0 or early compiler's header has no DWORD_PTR type. - */ -typedef unsigned long DWORD_PTR; -typedef unsigned long ULONG_PTR; -#endif -/* - * ----------------- - * autoconf switches - * ----------------- - */ - -#if defined(HAVE_PTW32_CONFIG_H) -#include "config.h" -#endif /* HAVE_PTW32_CONFIG_H */ - -#if !defined(NEED_FTIME) -#include -#else /* NEED_FTIME */ -/* use native WIN32 time API */ -#endif /* NEED_FTIME */ - -#if defined(HAVE_SIGNAL_H) -#include -#endif /* HAVE_SIGNAL_H */ - -#include - -/* - * Boolean values to make us independent of system includes. - */ -enum { - PTW32_FALSE = 0, - PTW32_TRUE = (! PTW32_FALSE) -}; - -/* - * This is a duplicate of what is in the autoconf config.h, - * which is only used when building the pthread-win32 libraries. - */ - -#if !defined(PTW32_CONFIG_H) -# if defined(WINCE) -# define NEED_ERRNO -# define NEED_SEM -# endif -# if defined(__MINGW64__) -# define HAVE_STRUCT_TIMESPEC -# define HAVE_MODE_T -# elif defined(_UWIN) || defined(__MINGW32__) -# define HAVE_MODE_T -# endif -#endif - -/* - * - */ - -#if PTW32_LEVEL >= PTW32_LEVEL_MAX -#if defined(NEED_ERRNO) -#include "need_errno.h" -#else -#include -#endif -#endif /* PTW32_LEVEL >= PTW32_LEVEL_MAX */ - -/* - * Several systems don't define some error numbers. - */ -#if !defined(ENOTSUP) -# define ENOTSUP 48 /* This is the value in Solaris. */ -#endif - -#if !defined(ETIMEDOUT) -# define ETIMEDOUT 10060 /* Same as WSAETIMEDOUT */ -#endif - -#if !defined(ENOSYS) -# define ENOSYS 140 /* Semi-arbitrary value */ -#endif - -#if !defined(EDEADLK) -# if defined(EDEADLOCK) -# define EDEADLK EDEADLOCK -# else -# define EDEADLK 36 /* This is the value in MSVC. */ -# endif -#endif - -/* POSIX 2008 - related to robust mutexes */ -#if !defined(EOWNERDEAD) -# define EOWNERDEAD 43 -#endif -#if !defined(ENOTRECOVERABLE) -# define ENOTRECOVERABLE 44 -#endif - -#include - -/* - * To avoid including windows.h we define only those things that we - * actually need from it. - */ -#if !defined(PTW32_INCLUDE_WINDOWS_H) -#if !defined(HANDLE) -# define PTW32__HANDLE_DEF -# define HANDLE void * -#endif -#if !defined(DWORD) -# define PTW32__DWORD_DEF -# define DWORD unsigned long -#endif -#endif - -#if !defined(HAVE_STRUCT_TIMESPEC) -#define HAVE_STRUCT_TIMESPEC -#if !defined(_TIMESPEC_DEFINED) -#define _TIMESPEC_DEFINED -struct timespec { - time_t tv_sec; - long tv_nsec; -}; -#endif /* _TIMESPEC_DEFINED */ -#endif /* HAVE_STRUCT_TIMESPEC */ - -#if !defined(SIG_BLOCK) -#define SIG_BLOCK 0 -#endif /* SIG_BLOCK */ - -#if !defined(SIG_UNBLOCK) -#define SIG_UNBLOCK 1 -#endif /* SIG_UNBLOCK */ - -#if !defined(SIG_SETMASK) -#define SIG_SETMASK 2 -#endif /* SIG_SETMASK */ - -#if defined(__cplusplus) -extern "C" -{ -#endif /* __cplusplus */ - -/* - * ------------------------------------------------------------- - * - * POSIX 1003.1-2001 Options - * ========================= - * - * Options are normally set in , which is not provided - * with pthreads-win32. - * - * For conformance with the Single Unix Specification (version 3), all of the - * options below are defined, and have a value of either -1 (not supported) - * or 200112L (supported). - * - * These options can neither be left undefined nor have a value of 0, because - * either indicates that sysconf(), which is not implemented, may be used at - * runtime to check the status of the option. - * - * _POSIX_THREADS (== 200112L) - * If == 200112L, you can use threads - * - * _POSIX_THREAD_ATTR_STACKSIZE (== 200112L) - * If == 200112L, you can control the size of a thread's - * stack - * pthread_attr_getstacksize - * pthread_attr_setstacksize - * - * _POSIX_THREAD_ATTR_STACKADDR (== -1) - * If == 200112L, you can allocate and control a thread's - * stack. If not supported, the following functions - * will return ENOSYS, indicating they are not - * supported: - * pthread_attr_getstackaddr - * pthread_attr_setstackaddr - * - * _POSIX_THREAD_PRIORITY_SCHEDULING (== -1) - * If == 200112L, you can use realtime scheduling. - * This option indicates that the behaviour of some - * implemented functions conforms to the additional TPS - * requirements in the standard. E.g. rwlocks favour - * writers over readers when threads have equal priority. - * - * _POSIX_THREAD_PRIO_INHERIT (== -1) - * If == 200112L, you can create priority inheritance - * mutexes. - * pthread_mutexattr_getprotocol + - * pthread_mutexattr_setprotocol + - * - * _POSIX_THREAD_PRIO_PROTECT (== -1) - * If == 200112L, you can create priority ceiling mutexes - * Indicates the availability of: - * pthread_mutex_getprioceiling - * pthread_mutex_setprioceiling - * pthread_mutexattr_getprioceiling - * pthread_mutexattr_getprotocol + - * pthread_mutexattr_setprioceiling - * pthread_mutexattr_setprotocol + - * - * _POSIX_THREAD_PROCESS_SHARED (== -1) - * If set, you can create mutexes and condition - * variables that can be shared with another - * process.If set, indicates the availability - * of: - * pthread_mutexattr_getpshared - * pthread_mutexattr_setpshared - * pthread_condattr_getpshared - * pthread_condattr_setpshared - * - * _POSIX_THREAD_SAFE_FUNCTIONS (== 200112L) - * If == 200112L you can use the special *_r library - * functions that provide thread-safe behaviour - * - * _POSIX_READER_WRITER_LOCKS (== 200112L) - * If == 200112L, you can use read/write locks - * - * _POSIX_SPIN_LOCKS (== 200112L) - * If == 200112L, you can use spin locks - * - * _POSIX_BARRIERS (== 200112L) - * If == 200112L, you can use barriers - * - * + These functions provide both 'inherit' and/or - * 'protect' protocol, based upon these macro - * settings. - * - * ------------------------------------------------------------- - */ - -/* - * POSIX Options - */ -#undef _POSIX_THREADS -#define _POSIX_THREADS 200809L - -#undef _POSIX_READER_WRITER_LOCKS -#define _POSIX_READER_WRITER_LOCKS 200809L - -#undef _POSIX_SPIN_LOCKS -#define _POSIX_SPIN_LOCKS 200809L - -#undef _POSIX_BARRIERS -#define _POSIX_BARRIERS 200809L - -#undef _POSIX_THREAD_SAFE_FUNCTIONS -#define _POSIX_THREAD_SAFE_FUNCTIONS 200809L - -#undef _POSIX_THREAD_ATTR_STACKSIZE -#define _POSIX_THREAD_ATTR_STACKSIZE 200809L - -/* - * The following options are not supported - */ -#undef _POSIX_THREAD_ATTR_STACKADDR -#define _POSIX_THREAD_ATTR_STACKADDR -1 - -#undef _POSIX_THREAD_PRIO_INHERIT -#define _POSIX_THREAD_PRIO_INHERIT -1 - -#undef _POSIX_THREAD_PRIO_PROTECT -#define _POSIX_THREAD_PRIO_PROTECT -1 - -/* TPS is not fully supported. */ -#undef _POSIX_THREAD_PRIORITY_SCHEDULING -#define _POSIX_THREAD_PRIORITY_SCHEDULING -1 - -#undef _POSIX_THREAD_PROCESS_SHARED -#define _POSIX_THREAD_PROCESS_SHARED -1 - - -/* - * POSIX 1003.1-2001 Limits - * =========================== - * - * These limits are normally set in , which is not provided with - * pthreads-win32. - * - * PTHREAD_DESTRUCTOR_ITERATIONS - * Maximum number of attempts to destroy - * a thread's thread-specific data on - * termination (must be at least 4) - * - * PTHREAD_KEYS_MAX - * Maximum number of thread-specific data keys - * available per process (must be at least 128) - * - * PTHREAD_STACK_MIN - * Minimum supported stack size for a thread - * - * PTHREAD_THREADS_MAX - * Maximum number of threads supported per - * process (must be at least 64). - * - * SEM_NSEMS_MAX - * The maximum number of semaphores a process can have. - * (must be at least 256) - * - * SEM_VALUE_MAX - * The maximum value a semaphore can have. - * (must be at least 32767) - * - */ -#undef _POSIX_THREAD_DESTRUCTOR_ITERATIONS -#define _POSIX_THREAD_DESTRUCTOR_ITERATIONS 4 - -#undef PTHREAD_DESTRUCTOR_ITERATIONS -#define PTHREAD_DESTRUCTOR_ITERATIONS _POSIX_THREAD_DESTRUCTOR_ITERATIONS - -#undef _POSIX_THREAD_KEYS_MAX -#define _POSIX_THREAD_KEYS_MAX 128 - -#undef PTHREAD_KEYS_MAX -#define PTHREAD_KEYS_MAX _POSIX_THREAD_KEYS_MAX - -#undef PTHREAD_STACK_MIN -#define PTHREAD_STACK_MIN 0 - -#undef _POSIX_THREAD_THREADS_MAX -#define _POSIX_THREAD_THREADS_MAX 64 - - /* Arbitrary value */ -#undef PTHREAD_THREADS_MAX -#define PTHREAD_THREADS_MAX 2019 - -#undef _POSIX_SEM_NSEMS_MAX -#define _POSIX_SEM_NSEMS_MAX 256 - - /* Arbitrary value */ -#undef SEM_NSEMS_MAX -#define SEM_NSEMS_MAX 1024 - -#undef _POSIX_SEM_VALUE_MAX -#define _POSIX_SEM_VALUE_MAX 32767 - -#undef SEM_VALUE_MAX -#define SEM_VALUE_MAX INT_MAX - - -#if defined(__GNUC__) && !defined(__declspec) -# error Please upgrade your GNU compiler to one that supports __declspec. -#endif - -/* - * When building the library, you should define PTW32_BUILD so that - * the variables/functions are exported correctly. When using the library, - * do NOT define PTW32_BUILD, and then the variables/functions will - * be imported correctly. - */ -#if !defined(PTW32_STATIC_LIB) -# if defined(PTW32_BUILD) -# define PTW32_DLLPORT __declspec (dllexport) -# else -# define PTW32_DLLPORT __declspec (dllimport) -# endif -#else -# define PTW32_DLLPORT -#endif - -/* - * The Open Watcom C/C++ compiler uses a non-standard calling convention - * that passes function args in registers unless __cdecl is explicitly specified - * in exposed function prototypes. - * - * We force all calls to cdecl even though this could slow Watcom code down - * slightly. If you know that the Watcom compiler will be used to build both - * the DLL and application, then you can probably define this as a null string. - * Remember that pthread.h (this file) is used for both the DLL and application builds. - */ -#define PTW32_CDECL __cdecl - -#if defined(_UWIN) && PTW32_LEVEL >= PTW32_LEVEL_MAX -# include -#else -/* - * Generic handle type - intended to extend uniqueness beyond - * that available with a simple pointer. It should scale for either - * IA-32 or IA-64. - */ -typedef struct { - void * p; /* Pointer to actual object */ - unsigned int x; /* Extra information - reuse count etc */ -} ptw32_handle_t; - -typedef ptw32_handle_t pthread_t; -typedef struct pthread_attr_t_ * pthread_attr_t; -typedef struct pthread_once_t_ pthread_once_t; -typedef struct pthread_key_t_ * pthread_key_t; -typedef struct pthread_mutex_t_ * pthread_mutex_t; -typedef struct pthread_mutexattr_t_ * pthread_mutexattr_t; -typedef struct pthread_cond_t_ * pthread_cond_t; -typedef struct pthread_condattr_t_ * pthread_condattr_t; -#endif -typedef struct pthread_rwlock_t_ * pthread_rwlock_t; -typedef struct pthread_rwlockattr_t_ * pthread_rwlockattr_t; -typedef struct pthread_spinlock_t_ * pthread_spinlock_t; -typedef struct pthread_barrier_t_ * pthread_barrier_t; -typedef struct pthread_barrierattr_t_ * pthread_barrierattr_t; - -/* - * ==================== - * ==================== - * POSIX Threads - * ==================== - * ==================== - */ - -enum { -/* - * pthread_attr_{get,set}detachstate - */ - PTHREAD_CREATE_JOINABLE = 0, /* Default */ - PTHREAD_CREATE_DETACHED = 1, - -/* - * pthread_attr_{get,set}inheritsched - */ - PTHREAD_INHERIT_SCHED = 0, - PTHREAD_EXPLICIT_SCHED = 1, /* Default */ - -/* - * pthread_{get,set}scope - */ - PTHREAD_SCOPE_PROCESS = 0, - PTHREAD_SCOPE_SYSTEM = 1, /* Default */ - -/* - * pthread_setcancelstate paramters - */ - PTHREAD_CANCEL_ENABLE = 0, /* Default */ - PTHREAD_CANCEL_DISABLE = 1, - -/* - * pthread_setcanceltype parameters - */ - PTHREAD_CANCEL_ASYNCHRONOUS = 0, - PTHREAD_CANCEL_DEFERRED = 1, /* Default */ - -/* - * pthread_mutexattr_{get,set}pshared - * pthread_condattr_{get,set}pshared - */ - PTHREAD_PROCESS_PRIVATE = 0, - PTHREAD_PROCESS_SHARED = 1, - -/* - * pthread_mutexattr_{get,set}robust - */ - PTHREAD_MUTEX_STALLED = 0, /* Default */ - PTHREAD_MUTEX_ROBUST = 1, - -/* - * pthread_barrier_wait - */ - PTHREAD_BARRIER_SERIAL_THREAD = -1 -}; - -/* - * ==================== - * ==================== - * cancellation - * ==================== - * ==================== - */ -#define PTHREAD_CANCELED ((void *)(size_t) -1) - - -/* - * ==================== - * ==================== - * Once Key - * ==================== - * ==================== - */ -#define PTHREAD_ONCE_INIT { PTW32_FALSE, 0, 0, 0} - -struct pthread_once_t_ -{ - int done; /* indicates if user function has been executed */ - void * lock; - int reserved1; - int reserved2; -}; - - -/* - * ==================== - * ==================== - * Object initialisers - * ==================== - * ==================== - */ -#define PTHREAD_MUTEX_INITIALIZER ((pthread_mutex_t)(size_t) -1) -#define PTHREAD_RECURSIVE_MUTEX_INITIALIZER ((pthread_mutex_t)(size_t) -2) -#define PTHREAD_ERRORCHECK_MUTEX_INITIALIZER ((pthread_mutex_t)(size_t) -3) - -/* - * Compatibility with LinuxThreads - */ -#define PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP PTHREAD_RECURSIVE_MUTEX_INITIALIZER -#define PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP PTHREAD_ERRORCHECK_MUTEX_INITIALIZER - -#define PTHREAD_COND_INITIALIZER ((pthread_cond_t)(size_t) -1) - -#define PTHREAD_RWLOCK_INITIALIZER ((pthread_rwlock_t)(size_t) -1) - -#define PTHREAD_SPINLOCK_INITIALIZER ((pthread_spinlock_t)(size_t) -1) - - -/* - * Mutex types. - */ -enum -{ - /* Compatibility with LinuxThreads */ - PTHREAD_MUTEX_FAST_NP, - PTHREAD_MUTEX_RECURSIVE_NP, - PTHREAD_MUTEX_ERRORCHECK_NP, - PTHREAD_MUTEX_TIMED_NP = PTHREAD_MUTEX_FAST_NP, - PTHREAD_MUTEX_ADAPTIVE_NP = PTHREAD_MUTEX_FAST_NP, - /* For compatibility with POSIX */ - PTHREAD_MUTEX_NORMAL = PTHREAD_MUTEX_FAST_NP, - PTHREAD_MUTEX_RECURSIVE = PTHREAD_MUTEX_RECURSIVE_NP, - PTHREAD_MUTEX_ERRORCHECK = PTHREAD_MUTEX_ERRORCHECK_NP, - PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL -}; - - -typedef struct ptw32_cleanup_t ptw32_cleanup_t; - -#if defined(_MSC_VER) -/* Disable MSVC 'anachronism used' warning */ -#pragma warning( disable : 4229 ) -#endif - -typedef void (* PTW32_CDECL ptw32_cleanup_callback_t)(void *); - -#if defined(_MSC_VER) -#pragma warning( default : 4229 ) -#endif - -struct ptw32_cleanup_t -{ - ptw32_cleanup_callback_t routine; - void *arg; - struct ptw32_cleanup_t *prev; -}; - -#if defined(__CLEANUP_SEH) - /* - * WIN32 SEH version of cancel cleanup. - */ - -#define pthread_cleanup_push( _rout, _arg ) \ - { \ - ptw32_cleanup_t _cleanup; \ - \ - _cleanup.routine = (ptw32_cleanup_callback_t)(_rout); \ - _cleanup.arg = (_arg); \ - __try \ - { \ - -#define pthread_cleanup_pop( _execute ) \ - } \ - __finally \ - { \ - if( _execute || AbnormalTermination()) \ - { \ - (*(_cleanup.routine))( _cleanup.arg ); \ - } \ - } \ - } - -#else /* __CLEANUP_SEH */ - -#if defined(__CLEANUP_C) - - /* - * C implementation of PThreads cancel cleanup - */ - -#define pthread_cleanup_push( _rout, _arg ) \ - { \ - ptw32_cleanup_t _cleanup; \ - \ - ptw32_push_cleanup( &_cleanup, (ptw32_cleanup_callback_t) (_rout), (_arg) ); \ - -#define pthread_cleanup_pop( _execute ) \ - (void) ptw32_pop_cleanup( _execute ); \ - } - -#else /* __CLEANUP_C */ - -#if defined(__CLEANUP_CXX) - - /* - * C++ version of cancel cleanup. - * - John E. Bossom. - */ - - class PThreadCleanup { - /* - * PThreadCleanup - * - * Purpose - * This class is a C++ helper class that is - * used to implement pthread_cleanup_push/ - * pthread_cleanup_pop. - * The destructor of this class automatically - * pops the pushed cleanup routine regardless - * of how the code exits the scope - * (i.e. such as by an exception) - */ - ptw32_cleanup_callback_t cleanUpRout; - void * obj; - int executeIt; - - public: - PThreadCleanup() : - cleanUpRout( 0 ), - obj( 0 ), - executeIt( 0 ) - /* - * No cleanup performed - */ - { - } - - PThreadCleanup( - ptw32_cleanup_callback_t routine, - void * arg ) : - cleanUpRout( routine ), - obj( arg ), - executeIt( 1 ) - /* - * Registers a cleanup routine for 'arg' - */ - { - } - - ~PThreadCleanup() - { - if ( executeIt && ((void *) cleanUpRout != (void *) 0) ) - { - (void) (*cleanUpRout)( obj ); - } - } - - void execute( int exec ) - { - executeIt = exec; - } - }; - - /* - * C++ implementation of PThreads cancel cleanup; - * This implementation takes advantage of a helper - * class who's destructor automatically calls the - * cleanup routine if we exit our scope weirdly - */ -#define pthread_cleanup_push( _rout, _arg ) \ - { \ - PThreadCleanup cleanup((ptw32_cleanup_callback_t)(_rout), \ - (void *) (_arg) ); - -#define pthread_cleanup_pop( _execute ) \ - cleanup.execute( _execute ); \ - } - -#else - -#error ERROR [__FILE__, line __LINE__]: Cleanup type undefined. - -#endif /* __CLEANUP_CXX */ - -#endif /* __CLEANUP_C */ - -#endif /* __CLEANUP_SEH */ - -/* - * =============== - * =============== - * Methods - * =============== - * =============== - */ - -/* - * PThread Attribute Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_attr_init (pthread_attr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_destroy (pthread_attr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_getdetachstate (const pthread_attr_t * attr, - int *detachstate); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_getstackaddr (const pthread_attr_t * attr, - void **stackaddr); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_getstacksize (const pthread_attr_t * attr, - size_t * stacksize); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_setdetachstate (pthread_attr_t * attr, - int detachstate); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_setstackaddr (pthread_attr_t * attr, - void *stackaddr); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_setstacksize (pthread_attr_t * attr, - size_t stacksize); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_getschedparam (const pthread_attr_t *attr, - struct sched_param *param); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_setschedparam (pthread_attr_t *attr, - const struct sched_param *param); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_setschedpolicy (pthread_attr_t *, - int); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_getschedpolicy (const pthread_attr_t *, - int *); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_setinheritsched(pthread_attr_t * attr, - int inheritsched); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_getinheritsched(const pthread_attr_t * attr, - int * inheritsched); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_setscope (pthread_attr_t *, - int); - -PTW32_DLLPORT int PTW32_CDECL pthread_attr_getscope (const pthread_attr_t *, - int *); - -/* - * PThread Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_create (pthread_t * tid, - const pthread_attr_t * attr, - void *(PTW32_CDECL *start) (void *), - void *arg); - -PTW32_DLLPORT int PTW32_CDECL pthread_detach (pthread_t tid); - -PTW32_DLLPORT int PTW32_CDECL pthread_equal (pthread_t t1, - pthread_t t2); - -PTW32_DLLPORT void PTW32_CDECL pthread_exit (void *value_ptr); - -PTW32_DLLPORT int PTW32_CDECL pthread_join (pthread_t thread, - void **value_ptr); - -PTW32_DLLPORT pthread_t PTW32_CDECL pthread_self (void); - -PTW32_DLLPORT int PTW32_CDECL pthread_cancel (pthread_t thread); - -PTW32_DLLPORT int PTW32_CDECL pthread_setcancelstate (int state, - int *oldstate); - -PTW32_DLLPORT int PTW32_CDECL pthread_setcanceltype (int type, - int *oldtype); - -PTW32_DLLPORT void PTW32_CDECL pthread_testcancel (void); - -PTW32_DLLPORT int PTW32_CDECL pthread_once (pthread_once_t * once_control, - void (PTW32_CDECL *init_routine) (void)); - -#if PTW32_LEVEL >= PTW32_LEVEL_MAX -PTW32_DLLPORT ptw32_cleanup_t * PTW32_CDECL ptw32_pop_cleanup (int execute); - -PTW32_DLLPORT void PTW32_CDECL ptw32_push_cleanup (ptw32_cleanup_t * cleanup, - ptw32_cleanup_callback_t routine, - void *arg); -#endif /* PTW32_LEVEL >= PTW32_LEVEL_MAX */ - -/* - * Thread Specific Data Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_key_create (pthread_key_t * key, - void (PTW32_CDECL *destructor) (void *)); - -PTW32_DLLPORT int PTW32_CDECL pthread_key_delete (pthread_key_t key); - -PTW32_DLLPORT int PTW32_CDECL pthread_setspecific (pthread_key_t key, - const void *value); - -PTW32_DLLPORT void * PTW32_CDECL pthread_getspecific (pthread_key_t key); - - -/* - * Mutex Attribute Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_init (pthread_mutexattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_destroy (pthread_mutexattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_getpshared (const pthread_mutexattr_t - * attr, - int *pshared); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_setpshared (pthread_mutexattr_t * attr, - int pshared); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_settype (pthread_mutexattr_t * attr, int kind); -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_gettype (const pthread_mutexattr_t * attr, int *kind); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_setrobust( - pthread_mutexattr_t *attr, - int robust); -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_getrobust( - const pthread_mutexattr_t * attr, - int * robust); - -/* - * Barrier Attribute Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_barrierattr_init (pthread_barrierattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_barrierattr_destroy (pthread_barrierattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_barrierattr_getpshared (const pthread_barrierattr_t - * attr, - int *pshared); - -PTW32_DLLPORT int PTW32_CDECL pthread_barrierattr_setpshared (pthread_barrierattr_t * attr, - int pshared); - -/* - * Mutex Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_mutex_init (pthread_mutex_t * mutex, - const pthread_mutexattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutex_destroy (pthread_mutex_t * mutex); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutex_lock (pthread_mutex_t * mutex); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutex_timedlock(pthread_mutex_t * mutex, - const struct timespec *abstime); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutex_trylock (pthread_mutex_t * mutex); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutex_unlock (pthread_mutex_t * mutex); - -PTW32_DLLPORT int PTW32_CDECL pthread_mutex_consistent (pthread_mutex_t * mutex); - -/* - * Spinlock Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_spin_init (pthread_spinlock_t * lock, int pshared); - -PTW32_DLLPORT int PTW32_CDECL pthread_spin_destroy (pthread_spinlock_t * lock); - -PTW32_DLLPORT int PTW32_CDECL pthread_spin_lock (pthread_spinlock_t * lock); - -PTW32_DLLPORT int PTW32_CDECL pthread_spin_trylock (pthread_spinlock_t * lock); - -PTW32_DLLPORT int PTW32_CDECL pthread_spin_unlock (pthread_spinlock_t * lock); - -/* - * Barrier Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_barrier_init (pthread_barrier_t * barrier, - const pthread_barrierattr_t * attr, - unsigned int count); - -PTW32_DLLPORT int PTW32_CDECL pthread_barrier_destroy (pthread_barrier_t * barrier); - -PTW32_DLLPORT int PTW32_CDECL pthread_barrier_wait (pthread_barrier_t * barrier); - -/* - * Condition Variable Attribute Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_condattr_init (pthread_condattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_condattr_destroy (pthread_condattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_condattr_getpshared (const pthread_condattr_t * attr, - int *pshared); - -PTW32_DLLPORT int PTW32_CDECL pthread_condattr_setpshared (pthread_condattr_t * attr, - int pshared); - -/* - * Condition Variable Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_cond_init (pthread_cond_t * cond, - const pthread_condattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_cond_destroy (pthread_cond_t * cond); - -PTW32_DLLPORT int PTW32_CDECL pthread_cond_wait (pthread_cond_t * cond, - pthread_mutex_t * mutex); - -PTW32_DLLPORT int PTW32_CDECL pthread_cond_timedwait (pthread_cond_t * cond, - pthread_mutex_t * mutex, - const struct timespec *abstime); - -PTW32_DLLPORT int PTW32_CDECL pthread_cond_signal (pthread_cond_t * cond); - -PTW32_DLLPORT int PTW32_CDECL pthread_cond_broadcast (pthread_cond_t * cond); - -/* - * Scheduling - */ -PTW32_DLLPORT int PTW32_CDECL pthread_setschedparam (pthread_t thread, - int policy, - const struct sched_param *param); - -PTW32_DLLPORT int PTW32_CDECL pthread_getschedparam (pthread_t thread, - int *policy, - struct sched_param *param); - -PTW32_DLLPORT int PTW32_CDECL pthread_setconcurrency (int); - -PTW32_DLLPORT int PTW32_CDECL pthread_getconcurrency (void); - -/* - * Read-Write Lock Functions - */ -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_init(pthread_rwlock_t *lock, - const pthread_rwlockattr_t *attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_destroy(pthread_rwlock_t *lock); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_tryrdlock(pthread_rwlock_t *); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_trywrlock(pthread_rwlock_t *); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_rdlock(pthread_rwlock_t *lock); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_timedrdlock(pthread_rwlock_t *lock, - const struct timespec *abstime); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_wrlock(pthread_rwlock_t *lock); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_timedwrlock(pthread_rwlock_t *lock, - const struct timespec *abstime); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlock_unlock(pthread_rwlock_t *lock); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlockattr_init (pthread_rwlockattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlockattr_destroy (pthread_rwlockattr_t * attr); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlockattr_getpshared (const pthread_rwlockattr_t * attr, - int *pshared); - -PTW32_DLLPORT int PTW32_CDECL pthread_rwlockattr_setpshared (pthread_rwlockattr_t * attr, - int pshared); - -#if PTW32_LEVEL >= PTW32_LEVEL_MAX - 1 - -/* - * Signal Functions. Should be defined in but MSVC and MinGW32 - * already have signal.h that don't define these. - */ -PTW32_DLLPORT int PTW32_CDECL pthread_kill(pthread_t thread, int sig); - -/* - * Non-portable functions - */ - -/* - * Compatibility with Linux. - */ -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_setkind_np(pthread_mutexattr_t * attr, - int kind); -PTW32_DLLPORT int PTW32_CDECL pthread_mutexattr_getkind_np(pthread_mutexattr_t * attr, - int *kind); -PTW32_DLLPORT int PTW32_CDECL pthread_timedjoin_np(pthread_t thread, - void **value_ptr, - const struct timespec *abstime); - -/* - * Possibly supported by other POSIX threads implementations - */ -PTW32_DLLPORT int PTW32_CDECL pthread_delay_np (struct timespec * interval); -PTW32_DLLPORT int PTW32_CDECL pthread_num_processors_np(void); -PTW32_DLLPORT unsigned __int64 PTW32_CDECL pthread_getunique_np(pthread_t thread); - -/* - * Useful if an application wants to statically link - * the lib rather than load the DLL at run-time. - */ -PTW32_DLLPORT int PTW32_CDECL pthread_win32_process_attach_np(void); -PTW32_DLLPORT int PTW32_CDECL pthread_win32_process_detach_np(void); -PTW32_DLLPORT int PTW32_CDECL pthread_win32_thread_attach_np(void); -PTW32_DLLPORT int PTW32_CDECL pthread_win32_thread_detach_np(void); - -/* - * Features that are auto-detected at load/run time. - */ -PTW32_DLLPORT int PTW32_CDECL pthread_win32_test_features_np(int); -enum ptw32_features { - PTW32_SYSTEM_INTERLOCKED_COMPARE_EXCHANGE = 0x0001, /* System provides it. */ - PTW32_ALERTABLE_ASYNC_CANCEL = 0x0002 /* Can cancel blocked threads. */ -}; - -/* - * Register a system time change with the library. - * Causes the library to perform various functions - * in response to the change. Should be called whenever - * the application's top level window receives a - * WM_TIMECHANGE message. It can be passed directly to - * pthread_create() as a new thread if desired. - */ -PTW32_DLLPORT void * PTW32_CDECL pthread_timechange_handler_np(void *); - -#endif /*PTW32_LEVEL >= PTW32_LEVEL_MAX - 1 */ - -#if PTW32_LEVEL >= PTW32_LEVEL_MAX - -/* - * Returns the Win32 HANDLE for the POSIX thread. - */ -PTW32_DLLPORT HANDLE PTW32_CDECL pthread_getw32threadhandle_np(pthread_t thread); -/* - * Returns the win32 thread ID for POSIX thread. - */ -PTW32_DLLPORT DWORD PTW32_CDECL pthread_getw32threadid_np (pthread_t thread); - - -/* - * Protected Methods - * - * This function blocks until the given WIN32 handle - * is signaled or pthread_cancel had been called. - * This function allows the caller to hook into the - * PThreads cancel mechanism. It is implemented using - * - * WaitForMultipleObjects - * - * on 'waitHandle' and a manually reset WIN32 Event - * used to implement pthread_cancel. The 'timeout' - * argument to TimedWait is simply passed to - * WaitForMultipleObjects. - */ -PTW32_DLLPORT int PTW32_CDECL pthreadCancelableWait (HANDLE waitHandle); -PTW32_DLLPORT int PTW32_CDECL pthreadCancelableTimedWait (HANDLE waitHandle, - DWORD timeout); - -#endif /* PTW32_LEVEL >= PTW32_LEVEL_MAX */ - -/* - * Thread-Safe C Runtime Library Mappings. - */ -#if !defined(_UWIN) -# if defined(NEED_ERRNO) - PTW32_DLLPORT int * PTW32_CDECL _errno( void ); -# else -# if !defined(errno) -# if (defined(_MT) || defined(_DLL)) - __declspec(dllimport) extern int * __cdecl _errno(void); -# define errno (*_errno()) -# endif -# endif -# endif -#endif - -/* - * Some compiler environments don't define some things. - */ -#if defined(__BORLANDC__) -# define _ftime ftime -# define _timeb timeb -#endif - -#if defined(__cplusplus) - -/* - * Internal exceptions - */ -class ptw32_exception {}; -class ptw32_exception_cancel : public ptw32_exception {}; -class ptw32_exception_exit : public ptw32_exception {}; - -#endif - -#if PTW32_LEVEL >= PTW32_LEVEL_MAX - -/* FIXME: This is only required if the library was built using SEH */ -/* - * Get internal SEH tag - */ -PTW32_DLLPORT DWORD PTW32_CDECL ptw32_get_exception_services_code(void); - -#endif /* PTW32_LEVEL >= PTW32_LEVEL_MAX */ - -#if !defined(PTW32_BUILD) - -#if defined(__CLEANUP_SEH) - -/* - * Redefine the SEH __except keyword to ensure that applications - * propagate our internal exceptions up to the library's internal handlers. - */ -#define __except( E ) \ - __except( ( GetExceptionCode() == ptw32_get_exception_services_code() ) \ - ? EXCEPTION_CONTINUE_SEARCH : ( E ) ) - -#endif /* __CLEANUP_SEH */ - -#if defined(__CLEANUP_CXX) - -/* - * Redefine the C++ catch keyword to ensure that applications - * propagate our internal exceptions up to the library's internal handlers. - */ -#if defined(_MSC_VER) - /* - * WARNING: Replace any 'catch( ... )' with 'PtW32CatchAll' - * if you want Pthread-Win32 cancellation and pthread_exit to work. - */ - -#if !defined(PtW32NoCatchWarn) - -#pragma message("Specify \"/DPtW32NoCatchWarn\" compiler flag to skip this message.") -#pragma message("------------------------------------------------------------------") -#pragma message("When compiling applications with MSVC++ and C++ exception handling:") -#pragma message(" Replace any 'catch( ... )' in routines called from POSIX threads") -#pragma message(" with 'PtW32CatchAll' or 'CATCHALL' if you want POSIX thread") -#pragma message(" cancellation and pthread_exit to work. For example:") -#pragma message("") -#pragma message(" #if defined(PtW32CatchAll)") -#pragma message(" PtW32CatchAll") -#pragma message(" #else") -#pragma message(" catch(...)") -#pragma message(" #endif") -#pragma message(" {") -#pragma message(" /* Catchall block processing */") -#pragma message(" }") -#pragma message("------------------------------------------------------------------") - -#endif - -#define PtW32CatchAll \ - catch( ptw32_exception & ) { throw; } \ - catch( ... ) - -#else /* _MSC_VER */ - -#define catch( E ) \ - catch( ptw32_exception & ) { throw; } \ - catch( E ) - -#endif /* _MSC_VER */ - -#endif /* __CLEANUP_CXX */ - -#endif /* ! PTW32_BUILD */ - -#if defined(__cplusplus) -} /* End of extern "C" */ -#endif /* __cplusplus */ - -#if defined(PTW32__HANDLE_DEF) -# undef HANDLE -#endif -#if defined(PTW32__DWORD_DEF) -# undef DWORD -#endif - -#undef PTW32_LEVEL -#undef PTW32_LEVEL_MAX - -#endif /* ! RC_INVOKED */ - -#endif /* PTHREAD_H */ diff --git a/libraries/shared/external/sched.h b/libraries/shared/external/sched.h deleted file mode 100644 index d43ff8dcb2..0000000000 --- a/libraries/shared/external/sched.h +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Module: sched.h - * - * Purpose: - * Provides an implementation of POSIX realtime extensions - * as defined in - * - * POSIX 1003.1b-1993 (POSIX.1b) - * - * -------------------------------------------------------------------------- - * - * Pthreads-win32 - POSIX Threads Library for Win32 - * Copyright(C) 1998 John E. Bossom - * Copyright(C) 1999,2005 Pthreads-win32 contributors - * - * Contact Email: rpj@callisto.canberra.edu.au - * - * The current list of contributors is contained - * in the file CONTRIBUTORS included with the source - * code distribution. The list can also be seen at the - * following World Wide Web location: - * http://sources.redhat.com/pthreads-win32/contributors.html - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library in the file COPYING.LIB; - * if not, write to the Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA - */ -#if !defined(_SCHED_H) -#define _SCHED_H - -#undef PTW32_SCHED_LEVEL - -#if defined(_POSIX_SOURCE) -#define PTW32_SCHED_LEVEL 0 -/* Early POSIX */ -#endif - -#if defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 199309 -#undef PTW32_SCHED_LEVEL -#define PTW32_SCHED_LEVEL 1 -/* Include 1b, 1c and 1d */ -#endif - -#if defined(INCLUDE_NP) -#undef PTW32_SCHED_LEVEL -#define PTW32_SCHED_LEVEL 2 -/* Include Non-Portable extensions */ -#endif - -#define PTW32_SCHED_LEVEL_MAX 3 - -#if ( defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200112 ) || !defined(PTW32_SCHED_LEVEL) -#define PTW32_SCHED_LEVEL PTW32_SCHED_LEVEL_MAX -/* Include everything */ -#endif - - -#if defined(__GNUC__) && !defined(__declspec) -# error Please upgrade your GNU compiler to one that supports __declspec. -#endif - -/* - * When building the library, you should define PTW32_BUILD so that - * the variables/functions are exported correctly. When using the library, - * do NOT define PTW32_BUILD, and then the variables/functions will - * be imported correctly. - */ -#if !defined(PTW32_STATIC_LIB) -# if defined(PTW32_BUILD) -# define PTW32_DLLPORT __declspec (dllexport) -# else -# define PTW32_DLLPORT __declspec (dllimport) -# endif -#else -# define PTW32_DLLPORT -#endif - -/* - * This is a duplicate of what is in the autoconf config.h, - * which is only used when building the pthread-win32 libraries. - */ - -#if !defined(PTW32_CONFIG_H) -# if defined(WINCE) -# define NEED_ERRNO -# define NEED_SEM -# endif -# if defined(__MINGW64__) -# define HAVE_STRUCT_TIMESPEC -# define HAVE_MODE_T -# elif defined(_UWIN) || defined(__MINGW32__) -# define HAVE_MODE_T -# endif -#endif - -/* - * - */ - -#if PTW32_SCHED_LEVEL >= PTW32_SCHED_LEVEL_MAX -#if defined(NEED_ERRNO) -#include "need_errno.h" -#else -#include -#endif -#endif /* PTW32_SCHED_LEVEL >= PTW32_SCHED_LEVEL_MAX */ - -#if (defined(__MINGW64__) || defined(__MINGW32__)) || defined(_UWIN) -# if PTW32_SCHED_LEVEL >= PTW32_SCHED_LEVEL_MAX -/* For pid_t */ -# include -/* Required by Unix 98 */ -# include -# else - typedef int pid_t; -# endif -#else - /* [i_a] fix for using pthread_win32 with mongoose code, which #define's its own pid_t akin to typedef HANDLE pid_t; */ - #undef pid_t -# if defined(_MSC_VER) - typedef void *pid_t; -# else - typedef int pid_t; -# endif -#endif - -/* Thread scheduling policies */ - -enum { - SCHED_OTHER = 0, - SCHED_FIFO, - SCHED_RR, - SCHED_MIN = SCHED_OTHER, - SCHED_MAX = SCHED_RR -}; - -struct sched_param { - int sched_priority; -}; - -#if defined(__cplusplus) -extern "C" -{ -#endif /* __cplusplus */ - -PTW32_DLLPORT int __cdecl sched_yield (void); - -PTW32_DLLPORT int __cdecl sched_get_priority_min (int policy); - -PTW32_DLLPORT int __cdecl sched_get_priority_max (int policy); - -PTW32_DLLPORT int __cdecl sched_setscheduler (pid_t pid, int policy); - -PTW32_DLLPORT int __cdecl sched_getscheduler (pid_t pid); - -/* - * Note that this macro returns ENOTSUP rather than - * ENOSYS as might be expected. However, returning ENOSYS - * should mean that sched_get_priority_{min,max} are - * not implemented as well as sched_rr_get_interval. - * This is not the case, since we just don't support - * round-robin scheduling. Therefore I have chosen to - * return the same value as sched_setscheduler when - * SCHED_RR is passed to it. - */ -#define sched_rr_get_interval(_pid, _interval) \ - ( errno = ENOTSUP, (int) -1 ) - - -#if defined(__cplusplus) -} /* End of extern "C" */ -#endif /* __cplusplus */ - -#undef PTW32_SCHED_LEVEL -#undef PTW32_SCHED_LEVEL_MAX - -#endif /* !_SCHED_H */ - diff --git a/libraries/shared/src/AngularConstraint.cpp b/libraries/shared/src/AngularConstraint.cpp index 4689568ac8..b39823ee3b 100644 --- a/libraries/shared/src/AngularConstraint.cpp +++ b/libraries/shared/src/AngularConstraint.cpp @@ -11,8 +11,9 @@ #include +#include "GLMHelpers.h" + #include "AngularConstraint.h" -#include "SharedUtil.h" // helper function /// \param angle radian angle to be clamped within angleMin and angleMax diff --git a/libraries/shared/src/AngularConstraint.h b/libraries/shared/src/AngularConstraint.h index 929a58959b..74d3fdb82b 100644 --- a/libraries/shared/src/AngularConstraint.h +++ b/libraries/shared/src/AngularConstraint.h @@ -14,7 +14,6 @@ #include - class AngularConstraint { public: /// \param minAngles minumum euler angles for the constraint diff --git a/libraries/shared/src/CapsuleShape.cpp b/libraries/shared/src/CapsuleShape.cpp index 12ab6ba479..03bc48bd94 100644 --- a/libraries/shared/src/CapsuleShape.cpp +++ b/libraries/shared/src/CapsuleShape.cpp @@ -73,7 +73,7 @@ void CapsuleShape::setEndPoints(const glm::vec3& startPoint, const glm::vec3& en if (height > EPSILON) { _halfHeight = 0.5f * height; axis /= height; - computeNewRotation(axis); + _rotation = computeNewRotation(axis); } updateBoundingRadius(); } diff --git a/libraries/shared/src/CollisionInfo.cpp b/libraries/shared/src/CollisionInfo.cpp index e862a22f4a..9dc321fa44 100644 --- a/libraries/shared/src/CollisionInfo.cpp +++ b/libraries/shared/src/CollisionInfo.cpp @@ -26,6 +26,16 @@ CollisionInfo::CollisionInfo() : _addedVelocity(0.f) { } +quint64 CollisionInfo::getShapePairKey() const { + if (_shapeB == NULL || _shapeA == NULL) { + // zero is an invalid key + return 0; + } + quint32 idA = _shapeA->getID(); + quint32 idB = _shapeB->getID(); + return idA < idB ? ((quint64)idA << 32) + (quint64)idB : ((quint64)idB << 32) + (quint64)idA; +} + CollisionList::CollisionList(int maxSize) : _maxSize(maxSize), _size(0) { diff --git a/libraries/shared/src/CollisionInfo.h b/libraries/shared/src/CollisionInfo.h index 1ab06e2ef5..6e70654d15 100644 --- a/libraries/shared/src/CollisionInfo.h +++ b/libraries/shared/src/CollisionInfo.h @@ -15,6 +15,7 @@ #include #include +#include #include class Shape; @@ -47,6 +48,9 @@ public: Shape* getShapeA() const { return const_cast(_shapeA); } Shape* getShapeB() const { return const_cast(_shapeB); } + /// \return unique key for shape pair + quint64 getShapePairKey() const; + const Shape* _shapeA; // pointer to shapeA in this collision const Shape* _shapeB; // pointer to shapeB in this collision diff --git a/libraries/shared/src/Constraint.h b/libraries/shared/src/Constraint.h index 422675b85d..9bbdc185e1 100644 --- a/libraries/shared/src/Constraint.h +++ b/libraries/shared/src/Constraint.h @@ -20,9 +20,6 @@ public: /// Enforce contraint by moving relevant points. /// \return max distance of point movement virtual float enforce() = 0; - -protected: - int _type; }; #endif // hifi_Constraint_h diff --git a/libraries/shared/src/ContactPoint.cpp b/libraries/shared/src/ContactPoint.cpp new file mode 100644 index 0000000000..02cf896594 --- /dev/null +++ b/libraries/shared/src/ContactPoint.cpp @@ -0,0 +1,215 @@ +// +// ContactPoint.cpp +// libraries/shared/src +// +// Created by Andrew Meadows 2014.07.30 +// 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 "ContactPoint.h" +#include "Shape.h" +#include "SharedUtil.h" + +// This parameter helps keep the actual point of contact slightly inside each shape +// which allows the collisions to happen almost every frame for more frequent updates. +const float CONTACT_PENETRATION_ALLOWANCE = 0.005f; + +ContactPoint::ContactPoint() : + _lastFrame(0), _shapeA(NULL), _shapeB(NULL), + _offsetA(0.0f), _offsetB(0.0f), + _relativeMassA(0.5f), _relativeMassB(0.5f), + _numPointsA(0), _numPoints(0), _normal(0.0f) { +} + +ContactPoint::ContactPoint(const CollisionInfo& collision, quint32 frame) : + _lastFrame(frame), _shapeA(collision.getShapeA()), _shapeB(collision.getShapeB()), + _offsetA(0.0f), _offsetB(0.0f), + _relativeMassA(0.5f), _relativeMassB(0.5f), + _numPointsA(0), _numPoints(0), _normal(0.0f) { + + glm::vec3 pointA = collision._contactPoint; + glm::vec3 pointB = collision._contactPoint - collision._penetration; + + float pLength = glm::length(collision._penetration); + if (pLength > EPSILON) { + _normal = collision._penetration / pLength; + } + if (_shapeA->getID() > _shapeB->getID()) { + // swap so that _shapeA always has lower ID + _shapeA = collision.getShapeB(); + _shapeB = collision.getShapeA(); + _normal = - _normal; + pointA = pointB; + pointB = collision._contactPoint; + } + + // bring the contact points inside the shapes to help maintain collision updates + pointA -= CONTACT_PENETRATION_ALLOWANCE * _normal; + pointB += CONTACT_PENETRATION_ALLOWANCE * _normal; + + _offsetA = pointA - _shapeA->getTranslation(); + _offsetB = pointB - _shapeB->getTranslation(); + + _shapeA->getVerletPoints(_points); + _numPointsA = _points.size(); + _shapeB->getVerletPoints(_points); + _numPoints = _points.size(); + + // compute and cache relative masses + float massA = EPSILON; + for (int i = 0; i < _numPointsA; ++i) { + massA += _points[i]->getMass(); + } + float massB = EPSILON; + for (int i = _numPointsA; i < _numPoints; ++i) { + massB += _points[i]->getMass(); + } + float invTotalMass = 1.0f / (massA + massB); + _relativeMassA = massA * invTotalMass; + _relativeMassB = massB * invTotalMass; + + // _contactPoint will be the weighted average of the two + _contactPoint = _relativeMassA * pointA + _relativeMassB * pointB; + + // compute offsets for shapeA + for (int i = 0; i < _numPointsA; ++i) { + glm::vec3 offset = _points[i]->_position - pointA; + _offsets.push_back(offset); + _distances.push_back(glm::length(offset)); + } + // compute offsets for shapeB + for (int i = _numPointsA; i < _numPoints; ++i) { + glm::vec3 offset = _points[i]->_position - pointB; + _offsets.push_back(offset); + _distances.push_back(glm::length(offset)); + } +} + +float ContactPoint::enforce() { + glm::vec3 pointA = _shapeA->getTranslation() + _offsetA; + glm::vec3 pointB = _shapeB->getTranslation() + _offsetB; + glm::vec3 penetration = pointA - pointB; + float pDotN = glm::dot(penetration, _normal); + bool constraintViolation = (pDotN > CONTACT_PENETRATION_ALLOWANCE); + + // the contact point will be the average of the two points on the shapes + _contactPoint = _relativeMassA * pointA + _relativeMassB * pointB; + + if (constraintViolation) { + for (int i = 0; i < _numPointsA; ++i) { + VerletPoint* point = _points[i]; + glm::vec3 offset = _offsets[i]; + + // split delta into parallel and perpendicular components + glm::vec3 delta = _contactPoint + offset - point->_position; + glm::vec3 paraDelta = glm::dot(delta, _normal) * _normal; + glm::vec3 perpDelta = delta - paraDelta; + + // use the relative sizes of the components to decide how much perpenducular delta to use + // perpendicular < parallel ==> static friction ==> perpFactor = 1.0 + // perpendicular > parallel ==> dynamic friction ==> cap to length of paraDelta ==> perpFactor < 1.0 + float paraLength = _relativeMassB * glm::length(paraDelta); + float perpLength = _relativeMassA * glm::length(perpDelta); + float perpFactor = (perpLength > paraLength && perpLength > EPSILON) ? (paraLength / perpLength) : 1.0f; + + // recombine the two components to get the final delta + delta = paraDelta + perpFactor * perpDelta; + + glm::vec3 targetPosition = point->_position + delta; + _distances[i] = glm::distance(_contactPoint, targetPosition); + point->_position += delta; + } + for (int i = _numPointsA; i < _numPoints; ++i) { + VerletPoint* point = _points[i]; + glm::vec3 offset = _offsets[i]; + + // split delta into parallel and perpendicular components + glm::vec3 delta = _contactPoint + offset - point->_position; + glm::vec3 paraDelta = glm::dot(delta, _normal) * _normal; + glm::vec3 perpDelta = delta - paraDelta; + + // use the relative sizes of the components to decide how much perpenducular delta to use + // perpendicular < parallel ==> static friction ==> perpFactor = 1.0 + // perpendicular > parallel ==> dynamic friction ==> cap to length of paraDelta ==> perpFactor < 1.0 + float paraLength = _relativeMassA * glm::length(paraDelta); + float perpLength = _relativeMassB * glm::length(perpDelta); + float perpFactor = (perpLength > paraLength && perpLength > EPSILON) ? (paraLength / perpLength) : 1.0f; + + // recombine the two components to get the final delta + delta = paraDelta + perpFactor * perpDelta; + + glm::vec3 targetPosition = point->_position + delta; + _distances[i] = glm::distance(_contactPoint, targetPosition); + point->_position += delta; + } + } else { + for (int i = 0; i < _numPoints; ++i) { + _distances[i] = glm::length(glm::length(_offsets[i])); + } + } + return 0.0f; +} + +// virtual +void ContactPoint::applyFriction() { + // TODO: Andrew to re-implement this in a different way + /* + for (int i = 0; i < _numPoints; ++i) { + glm::vec3& position = _points[i]->_position; + // TODO: use a fast distance approximation + float newDistance = glm::distance(_contactPoint, position); + float constrainedDistance = _distances[i]; + // NOTE: these "distance" constraints only push OUT, don't pull IN. + if (newDistance > EPSILON && newDistance < constrainedDistance) { + glm::vec3 direction = (_contactPoint - position) / newDistance; + glm::vec3 center = 0.5f * (_contactPoint + position); + _contactPoint = center + (0.5f * constrainedDistance) * direction; + position = center - (0.5f * constrainedDistance) * direction; + } + } + */ +} + +void ContactPoint::updateContact(const CollisionInfo& collision, quint32 frame) { + _lastFrame = frame; + + // compute contact points on surface of each shape + glm::vec3 pointA = collision._contactPoint; + glm::vec3 pointB = pointA - collision._penetration; + + // compute the normal (which points from A into B) + float pLength = glm::length(collision._penetration); + if (pLength > EPSILON) { + _normal = collision._penetration / pLength; + } else { + _normal = glm::vec3(0.0f); + } + + if (collision._shapeA->getID() > collision._shapeB->getID()) { + // our _shapeA always has lower ID + _normal = - _normal; + pointA = pointB; + pointB = collision._contactPoint; + } + + // bring the contact points inside the shapes to help maintain collision updates + pointA -= CONTACT_PENETRATION_ALLOWANCE * _normal; + pointB += CONTACT_PENETRATION_ALLOWANCE * _normal; + + // compute relative offsets to per-shape contact points + _offsetA = pointA - collision._shapeA->getTranslation(); + _offsetB = pointB - collision._shapeB->getTranslation(); + + // compute offsets for shapeA + assert(_offsets.size() == _numPoints); + for (int i = 0; i < _numPointsA; ++i) { + _offsets[i] = _points[i]->_position - pointA; + } + // compute offsets for shapeB + for (int i = _numPointsA; i < _numPoints; ++i) { + _offsets[i] = _points[i]->_position - pointB; + } +} diff --git a/libraries/shared/src/ContactPoint.h b/libraries/shared/src/ContactPoint.h new file mode 100644 index 0000000000..d584945970 --- /dev/null +++ b/libraries/shared/src/ContactPoint.h @@ -0,0 +1,54 @@ +// +// ContactPoint.h +// libraries/shared/src +// +// Created by Andrew Meadows 2014.07.30 +// 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_ContactPoint_h +#define hifi_ContactPoint_h + +#include +#include + +#include "CollisionInfo.h" +#include "VerletPoint.h" + +class Shape; + +class ContactPoint { +public: + ContactPoint(); + ContactPoint(const CollisionInfo& collision, quint32 frame); + + virtual float enforce(); + + void applyFriction(); + void updateContact(const CollisionInfo& collision, quint32 frame); + quint32 getLastFrame() const { return _lastFrame; } + + Shape* getShapeA() const { return _shapeA; } + Shape* getShapeB() const { return _shapeB; } + +protected: + quint32 _lastFrame; // frame count of last update + Shape* _shapeA; + Shape* _shapeB; + glm::vec3 _offsetA; // contact point relative to A's center + glm::vec3 _offsetB; // contact point relative to B's center + glm::vec3 _contactPoint; // a "virtual" point that is added to the simulation + float _relativeMassA; // massA / totalMass + float _relativeMassB; // massB / totalMass + int _numPointsA; // number of VerletPoints that belong to _shapeA + int _numPoints; // total number of VerletPoints + QVector _points; // points that belong to colliding shapes + QVector _offsets; // offsets to _points from contactPoint + QVector _distances; // distances to _points from contactPoint (during enforcement stage) + glm::vec3 _normal; // (points from A toward B) +}; + +#endif // hifi_ContactPoint_h diff --git a/libraries/shared/src/FixedConstraint.cpp b/libraries/shared/src/FixedConstraint.cpp index 099c6d7bac..8f1edc1fb5 100644 --- a/libraries/shared/src/FixedConstraint.cpp +++ b/libraries/shared/src/FixedConstraint.cpp @@ -10,26 +10,26 @@ // #include "FixedConstraint.h" -#include "Shape.h" // for MAX_SHAPE_MASS #include "VerletPoint.h" -FixedConstraint::FixedConstraint(VerletPoint* point, const glm::vec3& anchor) : _point(point), _anchor(anchor) { +FixedConstraint::FixedConstraint(glm::vec3* anchor, VerletPoint* point) : _anchor(anchor), _point(point) { + assert(anchor); + assert(point); } float FixedConstraint::enforce() { assert(_point != NULL); - // TODO: use fast approximate sqrt here - float distance = glm::distance(_anchor, _point->_position); - _point->_position = _anchor; - return distance; + _point->_position = *_anchor; + _point->_lastPosition = *_anchor; + return 0.0f; +} + +void FixedConstraint::setAnchor(glm::vec3* anchor) { + assert(anchor); + _anchor = anchor; } void FixedConstraint::setPoint(VerletPoint* point) { assert(point); _point = point; - _point->_mass = MAX_SHAPE_MASS; -} - -void FixedConstraint::setAnchor(const glm::vec3& anchor) { - _anchor = anchor; } diff --git a/libraries/shared/src/FixedConstraint.h b/libraries/shared/src/FixedConstraint.h index 050232a027..66a27369ce 100644 --- a/libraries/shared/src/FixedConstraint.h +++ b/libraries/shared/src/FixedConstraint.h @@ -18,15 +18,19 @@ class VerletPoint; +// FixedConstraint takes pointers to a glm::vec3 and a VerletPoint. +// The enforcement will copy the value of the vec3 into the VerletPoint. + class FixedConstraint : public Constraint { public: - FixedConstraint(VerletPoint* point, const glm::vec3& anchor); + FixedConstraint(glm::vec3* anchor, VerletPoint* point); + ~FixedConstraint() {} float enforce(); + void setAnchor(glm::vec3* anchor); void setPoint(VerletPoint* point); - void setAnchor(const glm::vec3& anchor); private: + glm::vec3* _anchor; VerletPoint* _point; - glm::vec3 _anchor; }; #endif // hifi_FixedConstraint_h diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp new file mode 100644 index 0000000000..566983679b --- /dev/null +++ b/libraries/shared/src/GLMHelpers.cpp @@ -0,0 +1,299 @@ +// +// GLMHelpers.cpp +// libraries/shared/src +// +// Created by Stephen Birarda on 2014-08-07. +// 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 "GLMHelpers.h" + +// Safe version of glm::mix; based on the code in Nick Bobick's article, +// http://www.gamasutra.com/features/19980703/quaternions_01.htm (via Clyde, +// https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Quaternion.java) +glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float proportion) { + float cosa = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w; + float ox = q2.x, oy = q2.y, oz = q2.z, ow = q2.w, s0, s1; + + // adjust signs if necessary + if (cosa < 0.0f) { + cosa = -cosa; + ox = -ox; + oy = -oy; + oz = -oz; + ow = -ow; + } + + // calculate coefficients; if the angle is too close to zero, we must fall back + // to linear interpolation + if ((1.0f - cosa) > EPSILON) { + float angle = acosf(cosa), sina = sinf(angle); + s0 = sinf((1.0f - proportion) * angle) / sina; + s1 = sinf(proportion * angle) / sina; + + } else { + s0 = 1.0f - proportion; + s1 = proportion; + } + + return glm::normalize(glm::quat(s0 * q1.w + s1 * ow, s0 * q1.x + s1 * ox, s0 * q1.y + s1 * oy, s0 * q1.z + s1 * oz)); +} + +// Allows sending of fixed-point numbers: radix 1 makes 15.1 number, radix 8 makes 8.8 number, etc +int packFloatScalarToSignedTwoByteFixed(unsigned char* buffer, float scalar, int radix) { + int16_t outVal = (int16_t)(scalar * (float)(1 << radix)); + memcpy(buffer, &outVal, sizeof(uint16_t)); + return sizeof(uint16_t); +} + +int unpackFloatScalarFromSignedTwoByteFixed(const int16_t* byteFixedPointer, float* destinationPointer, int radix) { + *destinationPointer = *byteFixedPointer / (float)(1 << radix); + return sizeof(int16_t); +} + +int packFloatVec3ToSignedTwoByteFixed(unsigned char* destBuffer, const glm::vec3& srcVector, int radix) { + const unsigned char* startPosition = destBuffer; + destBuffer += packFloatScalarToSignedTwoByteFixed(destBuffer, srcVector.x, radix); + destBuffer += packFloatScalarToSignedTwoByteFixed(destBuffer, srcVector.y, radix); + destBuffer += packFloatScalarToSignedTwoByteFixed(destBuffer, srcVector.z, radix); + return destBuffer - startPosition; +} + +int unpackFloatVec3FromSignedTwoByteFixed(const unsigned char* sourceBuffer, glm::vec3& destination, int radix) { + const unsigned char* startPosition = sourceBuffer; + sourceBuffer += unpackFloatScalarFromSignedTwoByteFixed((int16_t*) sourceBuffer, &(destination.x), radix); + sourceBuffer += unpackFloatScalarFromSignedTwoByteFixed((int16_t*) sourceBuffer, &(destination.y), radix); + sourceBuffer += unpackFloatScalarFromSignedTwoByteFixed((int16_t*) sourceBuffer, &(destination.z), radix); + return sourceBuffer - startPosition; +} + + +int packFloatAngleToTwoByte(unsigned char* buffer, float degrees) { + const float ANGLE_CONVERSION_RATIO = (std::numeric_limits::max() / 360.f); + + uint16_t angleHolder = floorf((degrees + 180.f) * ANGLE_CONVERSION_RATIO); + memcpy(buffer, &angleHolder, sizeof(uint16_t)); + + return sizeof(uint16_t); +} + +int unpackFloatAngleFromTwoByte(const uint16_t* byteAnglePointer, float* destinationPointer) { + *destinationPointer = (*byteAnglePointer / (float) std::numeric_limits::max()) * 360.f - 180.f; + return sizeof(uint16_t); +} + +int packOrientationQuatToBytes(unsigned char* buffer, const glm::quat& quatInput) { + const float QUAT_PART_CONVERSION_RATIO = (std::numeric_limits::max() / 2.f); + uint16_t quatParts[4]; + quatParts[0] = floorf((quatInput.x + 1.f) * QUAT_PART_CONVERSION_RATIO); + quatParts[1] = floorf((quatInput.y + 1.f) * QUAT_PART_CONVERSION_RATIO); + quatParts[2] = floorf((quatInput.z + 1.f) * QUAT_PART_CONVERSION_RATIO); + quatParts[3] = floorf((quatInput.w + 1.f) * QUAT_PART_CONVERSION_RATIO); + + memcpy(buffer, &quatParts, sizeof(quatParts)); + return sizeof(quatParts); +} + +int unpackOrientationQuatFromBytes(const unsigned char* buffer, glm::quat& quatOutput) { + uint16_t quatParts[4]; + memcpy(&quatParts, buffer, sizeof(quatParts)); + + quatOutput.x = ((quatParts[0] / (float) std::numeric_limits::max()) * 2.f) - 1.f; + quatOutput.y = ((quatParts[1] / (float) std::numeric_limits::max()) * 2.f) - 1.f; + quatOutput.z = ((quatParts[2] / (float) std::numeric_limits::max()) * 2.f) - 1.f; + quatOutput.w = ((quatParts[3] / (float) std::numeric_limits::max()) * 2.f) - 1.f; + + return sizeof(quatParts); +} + +// Safe version of glm::eulerAngles; uses the factorization method described in David Eberly's +// http://www.geometrictools.com/Documentation/EulerAngles.pdf (via Clyde, +// https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Quaternion.java) +glm::vec3 safeEulerAngles(const glm::quat& q) { + float sy = 2.0f * (q.y * q.w - q.x * q.z); + glm::vec3 eulers; + if (sy < 1.0f - EPSILON) { + if (sy > -1.0f + EPSILON) { + eulers = glm::vec3( + atan2f(q.y * q.z + q.x * q.w, 0.5f - (q.x * q.x + q.y * q.y)), + asinf(sy), + atan2f(q.x * q.y + q.z * q.w, 0.5f - (q.y * q.y + q.z * q.z))); + + } else { + // not a unique solution; x + z = atan2(-m21, m11) + eulers = glm::vec3( + 0.0f, + - PI_OVER_TWO, + atan2f(q.x * q.w - q.y * q.z, 0.5f - (q.x * q.x + q.z * q.z))); + } + } else { + // not a unique solution; x - z = atan2(-m21, m11) + eulers = glm::vec3( + 0.0f, + PI_OVER_TWO, + -atan2f(q.x * q.w - q.y * q.z, 0.5f - (q.x * q.x + q.z * q.z))); + } + + // adjust so that z, rather than y, is in [-pi/2, pi/2] + if (eulers.z < -PI_OVER_TWO) { + if (eulers.x < 0.0f) { + eulers.x += PI; + } else { + eulers.x -= PI; + } + eulers.y = -eulers.y; + if (eulers.y < 0.0f) { + eulers.y += PI; + } else { + eulers.y -= PI; + } + eulers.z += PI; + + } else if (eulers.z > PI_OVER_TWO) { + if (eulers.x < 0.0f) { + eulers.x += PI; + } else { + eulers.x -= PI; + } + eulers.y = -eulers.y; + if (eulers.y < 0.0f) { + eulers.y += PI; + } else { + eulers.y -= PI; + } + eulers.z -= PI; + } + return eulers; +} + +// Helper function returns the positive angle (in radians) between two 3D vectors +float angleBetween(const glm::vec3& v1, const glm::vec3& v2) { + return acosf((glm::dot(v1, v2)) / (glm::length(v1) * glm::length(v2))); +} + +// Helper function return the rotation from the first vector onto the second +glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2) { + float angle = angleBetween(v1, v2); + if (glm::isnan(angle) || angle < EPSILON) { + return glm::quat(); + } + glm::vec3 axis; + if (angle > 179.99f * RADIANS_PER_DEGREE) { // 180 degree rotation; must use another axis + axis = glm::cross(v1, glm::vec3(1.0f, 0.0f, 0.0f)); + float axisLength = glm::length(axis); + if (axisLength < EPSILON) { // parallel to x; y will work + axis = glm::normalize(glm::cross(v1, glm::vec3(0.0f, 1.0f, 0.0f))); + } else { + axis /= axisLength; + } + } else { + axis = glm::normalize(glm::cross(v1, v2)); + // It is possible for axis to be nan even when angle is not less than EPSILON. + // For example when angle is small but not tiny but v1 and v2 and have very short lengths. + if (glm::isnan(glm::dot(axis, axis))) { + // set angle and axis to values that will generate an identity rotation + angle = 0.0f; + axis = glm::vec3(1.0f, 0.0f, 0.0f); + } + } + return glm::angleAxis(angle, axis); +} + +glm::vec3 extractTranslation(const glm::mat4& matrix) { + return glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]); +} + +void setTranslation(glm::mat4& matrix, const glm::vec3& translation) { + matrix[3][0] = translation.x; + matrix[3][1] = translation.y; + matrix[3][2] = translation.z; +} + +glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal) { + // uses the iterative polar decomposition algorithm described by Ken Shoemake at + // http://www.cs.wisc.edu/graphics/Courses/838-s2002/Papers/polar-decomp.pdf + // code adapted from Clyde, https://github.com/threerings/clyde/blob/master/core/src/main/java/com/threerings/math/Matrix4f.java + // start with the contents of the upper 3x3 portion of the matrix + glm::mat3 upper = glm::mat3(matrix); + if (!assumeOrthogonal) { + for (int i = 0; i < 10; i++) { + // store the results of the previous iteration + glm::mat3 previous = upper; + + // compute average of the matrix with its inverse transpose + float sd00 = previous[1][1] * previous[2][2] - previous[2][1] * previous[1][2]; + float sd10 = previous[0][1] * previous[2][2] - previous[2][1] * previous[0][2]; + float sd20 = previous[0][1] * previous[1][2] - previous[1][1] * previous[0][2]; + float det = previous[0][0] * sd00 + previous[2][0] * sd20 - previous[1][0] * sd10; + if (fabs(det) == 0.0f) { + // determinant is zero; matrix is not invertible + break; + } + float hrdet = 0.5f / det; + upper[0][0] = +sd00 * hrdet + previous[0][0] * 0.5f; + upper[1][0] = -sd10 * hrdet + previous[1][0] * 0.5f; + upper[2][0] = +sd20 * hrdet + previous[2][0] * 0.5f; + + upper[0][1] = -(previous[1][0] * previous[2][2] - previous[2][0] * previous[1][2]) * hrdet + previous[0][1] * 0.5f; + upper[1][1] = +(previous[0][0] * previous[2][2] - previous[2][0] * previous[0][2]) * hrdet + previous[1][1] * 0.5f; + upper[2][1] = -(previous[0][0] * previous[1][2] - previous[1][0] * previous[0][2]) * hrdet + previous[2][1] * 0.5f; + + upper[0][2] = +(previous[1][0] * previous[2][1] - previous[2][0] * previous[1][1]) * hrdet + previous[0][2] * 0.5f; + upper[1][2] = -(previous[0][0] * previous[2][1] - previous[2][0] * previous[0][1]) * hrdet + previous[1][2] * 0.5f; + upper[2][2] = +(previous[0][0] * previous[1][1] - previous[1][0] * previous[0][1]) * hrdet + previous[2][2] * 0.5f; + + // compute the difference; if it's small enough, we're done + glm::mat3 diff = upper - previous; + if (diff[0][0] * diff[0][0] + diff[1][0] * diff[1][0] + diff[2][0] * diff[2][0] + diff[0][1] * diff[0][1] + + diff[1][1] * diff[1][1] + diff[2][1] * diff[2][1] + diff[0][2] * diff[0][2] + diff[1][2] * diff[1][2] + + diff[2][2] * diff[2][2] < EPSILON) { + break; + } + } + } + + // now that we have a nice orthogonal matrix, we can extract the rotation quaternion + // using the method described in http://en.wikipedia.org/wiki/Rotation_matrix#Conversions + float x2 = fabs(1.0f + upper[0][0] - upper[1][1] - upper[2][2]); + float y2 = fabs(1.0f - upper[0][0] + upper[1][1] - upper[2][2]); + float z2 = fabs(1.0f - upper[0][0] - upper[1][1] + upper[2][2]); + float w2 = fabs(1.0f + upper[0][0] + upper[1][1] + upper[2][2]); + return glm::normalize(glm::quat(0.5f * sqrtf(w2), + 0.5f * sqrtf(x2) * (upper[1][2] >= upper[2][1] ? 1.0f : -1.0f), + 0.5f * sqrtf(y2) * (upper[2][0] >= upper[0][2] ? 1.0f : -1.0f), + 0.5f * sqrtf(z2) * (upper[0][1] >= upper[1][0] ? 1.0f : -1.0f))); +} + +glm::vec3 extractScale(const glm::mat4& matrix) { + return glm::vec3(glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2])); +} + +float extractUniformScale(const glm::mat4& matrix) { + return extractUniformScale(extractScale(matrix)); +} + +float extractUniformScale(const glm::vec3& scale) { + return (scale.x + scale.y + scale.z) / 3.0f; +} + +QByteArray createByteArray(const glm::vec3& vector) { + return QByteArray::number(vector.x) + ',' + QByteArray::number(vector.y) + ',' + QByteArray::number(vector.z); +} + +bool isSimilarOrientation(const glm::quat& orientionA, const glm::quat& orientionB, float similarEnough) { + // Compute the angular distance between the two orientations + float angleOrientation = orientionA == orientionB ? 0.0f : glm::degrees(glm::angle(orientionA * glm::inverse(orientionB))); + if (isNaN(angleOrientation)) { + angleOrientation = 0.0f; + } + return (angleOrientation <= similarEnough); +} + +bool isSimilarPosition(const glm::vec3& positionA, const glm::vec3& positionB, float similarEnough) { + // Compute the distance between the two points + float positionDistance = glm::distance(positionA, positionB); + return (positionDistance <= similarEnough); +} \ No newline at end of file diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h new file mode 100644 index 0000000000..43a1d09722 --- /dev/null +++ b/libraries/shared/src/GLMHelpers.h @@ -0,0 +1,89 @@ +// +// GLMHelpers.h +// libraries/shared/src +// +// Created by Stephen Birarda on 2014-08-07. +// 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_GLMHelpers_h +#define hifi_GLMHelpers_h + +#include + +#include +#include + +#include + +#include "SharedUtil.h" + +glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); + +// These pack/unpack functions are designed to start specific known types in as efficient a manner +// as possible. Taking advantage of the known characteristics of the semantic types. + +// Angles are known to be between 0 and 360 degrees, this allows us to encode in 16bits with great accuracy +int packFloatAngleToTwoByte(unsigned char* buffer, float degrees); +int unpackFloatAngleFromTwoByte(const uint16_t* byteAnglePointer, float* destinationPointer); + +// Orientation Quats are known to have 4 normalized components be between -1.0 and 1.0 +// this allows us to encode each component in 16bits with great accuracy +int packOrientationQuatToBytes(unsigned char* buffer, const glm::quat& quatInput); +int unpackOrientationQuatFromBytes(const unsigned char* buffer, glm::quat& quatOutput); + +// Ratios need the be highly accurate when less than 10, but not very accurate above 10, and they +// are never greater than 1000 to 1, this allows us to encode each component in 16bits +int packFloatRatioToTwoByte(unsigned char* buffer, float ratio); +int unpackFloatRatioFromTwoByte(const unsigned char* buffer, float& ratio); + +// Near/Far Clip values need the be highly accurate when less than 10, but only integer accuracy above 10 and +// they are never greater than 16,000, this allows us to encode each component in 16bits +int packClipValueToTwoByte(unsigned char* buffer, float clipValue); +int unpackClipValueFromTwoByte(const unsigned char* buffer, float& clipValue); + +// Positive floats that don't need to be very precise +int packFloatToByte(unsigned char* buffer, float value, float scaleBy); +int unpackFloatFromByte(const unsigned char* buffer, float& value, float scaleBy); + +// Allows sending of fixed-point numbers: radix 1 makes 15.1 number, radix 8 makes 8.8 number, etc +int packFloatScalarToSignedTwoByteFixed(unsigned char* buffer, float scalar, int radix); +int unpackFloatScalarFromSignedTwoByteFixed(const int16_t* byteFixedPointer, float* destinationPointer, int radix); + +// A convenience for sending vec3's as fixed-point floats +int packFloatVec3ToSignedTwoByteFixed(unsigned char* destBuffer, const glm::vec3& srcVector, int radix); +int unpackFloatVec3FromSignedTwoByteFixed(const unsigned char* sourceBuffer, glm::vec3& destination, int radix); + +/// \return vec3 with euler angles in radians +glm::vec3 safeEulerAngles(const glm::quat& q); + +float angleBetween(const glm::vec3& v1, const glm::vec3& v2); + +glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); + +glm::vec3 extractTranslation(const glm::mat4& matrix); + +void setTranslation(glm::mat4& matrix, const glm::vec3& translation); + +glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal = false); + +glm::vec3 extractScale(const glm::mat4& matrix); + +float extractUniformScale(const glm::mat4& matrix); + +float extractUniformScale(const glm::vec3& scale); + +QByteArray createByteArray(const glm::vec3& vector); + +/// \return bool are two orientations similar to each other +const float ORIENTATION_SIMILAR_ENOUGH = 5.0f; // 10 degrees in any direction +bool isSimilarOrientation(const glm::quat& orientionA, const glm::quat& orientionB, + float similarEnough = ORIENTATION_SIMILAR_ENOUGH); +const float POSITION_SIMILAR_ENOUGH = 0.1f; // 0.1 meter +bool isSimilarPosition(const glm::vec3& positionA, const glm::vec3& positionB, float similarEnough = POSITION_SIMILAR_ENOUGH); + + +#endif // hifi_GLMHelpers_h \ No newline at end of file diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 02cce80104..6fe39a8ccf 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include #include @@ -68,29 +69,35 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL int configIndex = argumentList.indexOf(CONFIG_FILE_OPTION); + QString configFilePath; + if (configIndex != -1) { // we have a config file - try and read it - QString configFilePath = argumentList[configIndex + 1]; - QFile configFile(configFilePath); - - if (configFile.exists()) { - qDebug() << "Reading JSON config file at" << configFilePath; - configFile.open(QIODevice::ReadOnly); - - QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); - QJsonObject rootObject = configDocument.object(); - - // enumerate the keys of the configDocument object - foreach(const QString& key, rootObject.keys()) { - - if (!mergedMap.contains(key)) { - // no match in existing list, add it - mergedMap.insert(key, QVariant(rootObject[key])); - } + configFilePath = argumentList[configIndex + 1]; + } else { + // no config file - try to read a file at resources/config.json + configFilePath = QCoreApplication::applicationDirPath() + "/resources/config.json"; + } + + QFile configFile(configFilePath); + + if (configFile.exists()) { + qDebug() << "Reading JSON config file at" << configFilePath; + configFile.open(QIODevice::ReadOnly); + + QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); + QJsonObject rootObject = configDocument.object(); + + // enumerate the keys of the configDocument object + foreach(const QString& key, rootObject.keys()) { + + if (!mergedMap.contains(key)) { + // no match in existing list, add it + mergedMap.insert(key, QVariant(rootObject[key])); } - } else { - qDebug() << "Could not find JSON config file at" << configFilePath; } + } else { + qDebug() << "Could not find JSON config file at" << configFilePath; } return mergedMap; diff --git a/libraries/shared/src/MovingMinMaxAvg.h b/libraries/shared/src/MovingMinMaxAvg.h index 734018b469..7d4b3df124 100644 --- a/libraries/shared/src/MovingMinMaxAvg.h +++ b/libraries/shared/src/MovingMinMaxAvg.h @@ -12,6 +12,7 @@ #ifndef hifi_MovingMinMaxAvg_h #define hifi_MovingMinMaxAvg_h +#include #include #include "RingBufferHistory.h" diff --git a/libraries/shared/src/PhysicsEntity.cpp b/libraries/shared/src/PhysicsEntity.cpp index 37d1a88d67..09b00c201c 100644 --- a/libraries/shared/src/PhysicsEntity.cpp +++ b/libraries/shared/src/PhysicsEntity.cpp @@ -65,6 +65,9 @@ void PhysicsEntity::setEnableShapes(bool enable) { } void PhysicsEntity::clearShapes() { + if (_simulation) { + _simulation->removeShapes(this); + } for (int i = 0; i < _shapes.size(); ++i) { delete _shapes[i]; } diff --git a/libraries/shared/src/PhysicsSimulation.cpp b/libraries/shared/src/PhysicsSimulation.cpp index ca9d3d303a..6c4901bcd5 100644 --- a/libraries/shared/src/PhysicsSimulation.cpp +++ b/libraries/shared/src/PhysicsSimulation.cpp @@ -10,10 +10,10 @@ // #include -#include #include "PhysicsSimulation.h" +#include "PerfStat.h" #include "PhysicsEntity.h" #include "Ragdoll.h" #include "SharedUtil.h" @@ -24,20 +24,51 @@ int MAX_ENTITIES_PER_SIMULATION = 64; int MAX_COLLISIONS_PER_SIMULATION = 256; -PhysicsSimulation::PhysicsSimulation() : _collisionList(MAX_COLLISIONS_PER_SIMULATION), - _numIterations(0), _numCollisions(0), _constraintError(0.0f), _stepTime(0) { +PhysicsSimulation::PhysicsSimulation() : _translation(0.0f), _frameCount(0), _entity(NULL), _ragdoll(NULL), + _collisions(MAX_COLLISIONS_PER_SIMULATION) { } PhysicsSimulation::~PhysicsSimulation() { // entities have a backpointer to this simulator that must be cleaned up - int numEntities = _entities.size(); + int numEntities = _otherEntities.size(); for (int i = 0; i < numEntities; ++i) { - _entities[i]->_simulation = NULL; + _otherEntities[i]->_simulation = NULL; + } + _otherEntities.clear(); + if (_entity) { + _entity->_simulation = NULL; } - _entities.clear(); // but Ragdolls do not - _dolls.clear(); + _ragdoll = NULL; + _otherRagdolls.clear(); +} + +void PhysicsSimulation::setRagdoll(Ragdoll* ragdoll) { + if (_ragdoll != ragdoll) { + if (_ragdoll) { + _ragdoll->_simulation = NULL; + } + _ragdoll = ragdoll; + if (_ragdoll) { + assert(!(_ragdoll->_simulation)); + _ragdoll->_simulation = this; + } + } +} + +void PhysicsSimulation::setEntity(PhysicsEntity* entity) { + if (_entity != entity) { + if (_entity) { + assert(_entity->_simulation == this); + _entity->_simulation = NULL; + } + _entity = entity; + if (_entity) { + assert(!(_entity->_simulation)); + _entity->_simulation = this; + } + } } bool PhysicsSimulation::addEntity(PhysicsEntity* entity) { @@ -45,25 +76,25 @@ bool PhysicsSimulation::addEntity(PhysicsEntity* entity) { return false; } if (entity->_simulation == this) { - int numEntities = _entities.size(); + int numEntities = _otherEntities.size(); for (int i = 0; i < numEntities; ++i) { - if (entity == _entities.at(i)) { + if (entity == _otherEntities.at(i)) { // already in list - assert(entity->_simulation == this); return true; } } // belongs to some other simulation return false; } - int numEntities = _entities.size(); + int numEntities = _otherEntities.size(); if (numEntities > MAX_ENTITIES_PER_SIMULATION) { // list is full return false; } // add to list + assert(!(entity->_simulation)); entity->_simulation = this; - _entities.push_back(entity); + _otherEntities.push_back(entity); return true; } @@ -71,17 +102,18 @@ void PhysicsSimulation::removeEntity(PhysicsEntity* entity) { if (!entity || !entity->_simulation || !(entity->_simulation == this)) { return; } - int numEntities = _entities.size(); + removeShapes(entity); + int numEntities = _otherEntities.size(); for (int i = 0; i < numEntities; ++i) { - if (entity == _entities.at(i)) { + if (entity == _otherEntities.at(i)) { if (i == numEntities - 1) { // remove it - _entities.pop_back(); + _otherEntities.pop_back(); } else { // swap the last for this one - PhysicsEntity* lastEntity = _entities[numEntities - 1]; - _entities.pop_back(); - _entities[i] = lastEntity; + PhysicsEntity* lastEntity = _otherEntities[numEntities - 1]; + _otherEntities.pop_back(); + _otherEntities[i] = lastEntity; } entity->_simulation = NULL; break; @@ -89,126 +121,168 @@ void PhysicsSimulation::removeEntity(PhysicsEntity* entity) { } } +void PhysicsSimulation::removeShapes(const PhysicsEntity* entity) { + // remove data structures with pointers to entity's shapes + QMap::iterator itr = _contacts.begin(); + while (itr != _contacts.end()) { + if (entity == itr.value().getShapeA()->getEntity() || entity == itr.value().getShapeB()->getEntity()) { + itr = _contacts.erase(itr); + } else { + ++itr; + } + } +} + +const float OTHER_RAGDOLL_MASS_SCALE = 10.0f; + bool PhysicsSimulation::addRagdoll(Ragdoll* doll) { if (!doll) { return false; } - int numDolls = _dolls.size(); + int numDolls = _otherRagdolls.size(); if (numDolls > MAX_DOLLS_PER_SIMULATION) { // list is full return false; } - for (int i = 0; i < numDolls; ++i) { - if (doll == _dolls[i]) { - // already in list - return true; + if (doll->_simulation == this) { + for (int i = 0; i < numDolls; ++i) { + if (doll == _otherRagdolls[i]) { + // already in list + return true; + } } } // add to list - _dolls.push_back(doll); + assert(!(doll->_simulation)); + doll->_simulation = this; + _otherRagdolls.push_back(doll); + + // set the massScale of otherRagdolls artificially high + doll->setMassScale(OTHER_RAGDOLL_MASS_SCALE); return true; } void PhysicsSimulation::removeRagdoll(Ragdoll* doll) { - int numDolls = _dolls.size(); + if (!doll || doll->_simulation != this) { + return; + } + int numDolls = _otherRagdolls.size(); for (int i = 0; i < numDolls; ++i) { - if (doll == _dolls[i]) { + if (doll == _otherRagdolls[i]) { if (i == numDolls - 1) { // remove it - _dolls.pop_back(); + _otherRagdolls.pop_back(); } else { // swap the last for this one - Ragdoll* lastDoll = _dolls[numDolls - 1]; - _dolls.pop_back(); - _dolls[i] = lastDoll; + Ragdoll* lastDoll = _otherRagdolls[numDolls - 1]; + _otherRagdolls.pop_back(); + _otherRagdolls[i] = lastDoll; } + doll->_simulation = NULL; + doll->setMassScale(1.0f); break; } } } void PhysicsSimulation::stepForward(float deltaTime, float minError, int maxIterations, quint64 maxUsec) { + ++_frameCount; + if (!_ragdoll) { + return; + } quint64 now = usecTimestampNow(); quint64 startTime = now; quint64 expiry = startTime + maxUsec; moveRagdolls(deltaTime); + enforceContacts(); + int numDolls = _otherRagdolls.size(); + { + PerformanceTimer perfTimer("enforce"); + _ragdoll->enforceConstraints(); + for (int i = 0; i < numDolls; ++i) { + _otherRagdolls[i]->enforceConstraints(); + } + } - int numDolls = _dolls.size(); - _numCollisions = 0; + bool collidedWithOtherRagdoll = false; int iterations = 0; float error = 0.0f; do { - computeCollisions(); - processCollisions(); + collidedWithOtherRagdoll = computeCollisions() || collidedWithOtherRagdoll; + updateContacts(); + resolveCollisions(); - // enforce constraints - error = 0.0f; - for (int i = 0; i < numDolls; ++i) { - error = glm::max(error, _dolls[i]->enforceRagdollConstraints()); + { // enforce constraints + PerformanceTimer perfTimer("enforce"); + error = _ragdoll->enforceConstraints(); + for (int i = 0; i < numDolls; ++i) { + error = glm::max(error, _otherRagdolls[i]->enforceConstraints()); + } } + applyContactFriction(); ++iterations; now = usecTimestampNow(); - } while (_numCollisions != 0 && (iterations < maxIterations) && (error > minError) && (now < expiry)); + } while (_collisions.size() != 0 && (iterations < maxIterations) && (error > minError) && (now < expiry)); - _numIterations = iterations; - _constraintError = error; - _stepTime = usecTimestampNow()- startTime; + // the collisions may have moved the main ragdoll from the simulation center + // so we remove this offset (potentially storing it as movement of the Ragdoll owner) + _ragdoll->removeRootOffset(collidedWithOtherRagdoll); -#ifdef ANDREW_DEBUG - // temporary debug info for watching simulation performance - static int adebug = 0; ++adebug; - if (0 == (adebug % 100)) { - std::cout << "adebug Ni = " << _numIterations << " E = " << error << " t = " << _stepTime << std::endl; // adebug + // also remove any offsets from the other ragdolls + for (int i = 0; i < numDolls; ++i) { + _otherRagdolls[i]->removeRootOffset(false); } -#endif // ANDREW_DEBUG + pruneContacts(); } void PhysicsSimulation::moveRagdolls(float deltaTime) { - int numDolls = _dolls.size(); + PerformanceTimer perfTimer("integrate"); + _ragdoll->stepForward(deltaTime); + int numDolls = _otherRagdolls.size(); for (int i = 0; i < numDolls; ++i) { - _dolls.at(i)->stepRagdollForward(deltaTime); + _otherRagdolls[i]->stepForward(deltaTime); } } -void PhysicsSimulation::computeCollisions() { - _collisionList.clear(); - // TODO: keep track of QSet collidedEntities; - int numEntities = _entities.size(); +bool PhysicsSimulation::computeCollisions() { + PerformanceTimer perfTimer("collide"); + _collisions.clear(); + + const QVector shapes = _entity->getShapes(); + int numShapes = shapes.size(); + // collide main ragdoll with self + for (int i = 0; i < numShapes; ++i) { + const Shape* shape = shapes.at(i); + if (!shape) { + continue; + } + for (int j = i+1; j < numShapes; ++j) { + const Shape* otherShape = shapes.at(j); + if (otherShape && _entity->collisionsAreEnabled(i, j)) { + ShapeCollider::collideShapes(shape, otherShape, _collisions); + } + } + } + + // collide main ragdoll with others + bool otherCollisions = false; + int numEntities = _otherEntities.size(); for (int i = 0; i < numEntities; ++i) { - PhysicsEntity* entity = _entities.at(i); - const QVector shapes = entity->getShapes(); - int numShapes = shapes.size(); - // collide with self - for (int j = 0; j < numShapes; ++j) { - const Shape* shape = shapes.at(j); - if (!shape) { - continue; - } - for (int k = j+1; k < numShapes; ++k) { - const Shape* otherShape = shapes.at(k); - if (otherShape && entity->collisionsAreEnabled(j, k)) { - ShapeCollider::collideShapes(shape, otherShape, _collisionList); - } - } - } - - // collide with others - for (int j = i+1; j < numEntities; ++j) { - const QVector otherShapes = _entities.at(j)->getShapes(); - ShapeCollider::collideShapesWithShapes(shapes, otherShapes, _collisionList); - } + const QVector otherShapes = _otherEntities.at(i)->getShapes(); + otherCollisions = ShapeCollider::collideShapesWithShapes(shapes, otherShapes, _collisions) || otherCollisions; } - _numCollisions = _collisionList.size(); + return otherCollisions; } -void PhysicsSimulation::processCollisions() { +void PhysicsSimulation::resolveCollisions() { + PerformanceTimer perfTimer("resolve"); // walk all collisions, accumulate movement on shapes, and build a list of affected shapes QSet shapes; - int numCollisions = _collisionList.size(); + int numCollisions = _collisions.size(); for (int i = 0; i < numCollisions; ++i) { - CollisionInfo* collision = _collisionList.getCollision(i); + CollisionInfo* collision = _collisions.getCollision(i); collision->apply(); // there is always a shapeA shapes.insert(collision->getShapeA()); @@ -224,3 +298,52 @@ void PhysicsSimulation::processCollisions() { ++shapeItr; } } + +void PhysicsSimulation::enforceContacts() { + PerformanceTimer perfTimer("contacts"); + QMap::iterator itr = _contacts.begin(); + while (itr != _contacts.end()) { + itr.value().enforce(); + ++itr; + } +} + +void PhysicsSimulation::applyContactFriction() { + PerformanceTimer perfTimer("contacts"); + QMap::iterator itr = _contacts.begin(); + while (itr != _contacts.end()) { + itr.value().applyFriction(); + ++itr; + } +} + +void PhysicsSimulation::updateContacts() { + PerformanceTimer perfTimer("contacts"); + int numCollisions = _collisions.size(); + for (int i = 0; i < numCollisions; ++i) { + CollisionInfo* collision = _collisions.getCollision(i); + quint64 key = collision->getShapePairKey(); + if (key == 0) { + continue; + } + QMap::iterator itr = _contacts.find(key); + if (itr == _contacts.end()) { + _contacts.insert(key, ContactPoint(*collision, _frameCount)); + } else { + itr.value().updateContact(*collision, _frameCount); + } + } +} + +const quint32 MAX_CONTACT_FRAME_LIFETIME = 2; + +void PhysicsSimulation::pruneContacts() { + QMap::iterator itr = _contacts.begin(); + while (itr != _contacts.end()) { + if (_frameCount - itr.value().getLastFrame() > MAX_CONTACT_FRAME_LIFETIME) { + itr = _contacts.erase(itr); + } else { + ++itr; + } + } +} diff --git a/libraries/shared/src/PhysicsSimulation.h b/libraries/shared/src/PhysicsSimulation.h index c611e06870..1db56a46e2 100644 --- a/libraries/shared/src/PhysicsSimulation.h +++ b/libraries/shared/src/PhysicsSimulation.h @@ -12,9 +12,12 @@ #ifndef hifi_PhysicsSimulation #define hifi_PhysicsSimulation +#include +#include #include #include "CollisionInfo.h" +#include "ContactPoint.h" class PhysicsEntity; class Ragdoll; @@ -25,10 +28,17 @@ public: PhysicsSimulation(); ~PhysicsSimulation(); + void setTranslation(const glm::vec3& translation) { _translation = translation; } + const glm::vec3& getTranslation() const { return _translation; } + + void setRagdoll(Ragdoll* ragdoll); + void setEntity(PhysicsEntity* entity); + /// \return true if entity was added to or is already in the list bool addEntity(PhysicsEntity* entity); void removeEntity(PhysicsEntity* entity); + void removeShapes(const PhysicsEntity* entity); /// \return true if doll was added to or is already in the list bool addRagdoll(Ragdoll* doll); @@ -41,20 +51,30 @@ public: /// \return distance of largest movement void stepForward(float deltaTime, float minError, int maxIterations, quint64 maxUsec); +protected: void moveRagdolls(float deltaTime); - void computeCollisions(); - void processCollisions(); + + /// \return true if main ragdoll collides with other avatar + bool computeCollisions(); + + void resolveCollisions(); + void enforceContacts(); + void applyContactFriction(); + void updateContacts(); + void pruneContacts(); private: - CollisionList _collisionList; - QVector _entities; - QVector _dolls; + glm::vec3 _translation; // origin of simulation in world-frame - // some stats - int _numIterations; - int _numCollisions; - float _constraintError; - quint64 _stepTime; + quint32 _frameCount; + + PhysicsEntity* _entity; + Ragdoll* _ragdoll; + + QVector _otherRagdolls; + QVector _otherEntities; + CollisionList _collisions; + QMap _contacts; }; #endif // hifi_PhysicsSimulation diff --git a/libraries/shared/src/Ragdoll.cpp b/libraries/shared/src/Ragdoll.cpp index 6282db4dfb..70ea63930b 100644 --- a/libraries/shared/src/Ragdoll.cpp +++ b/libraries/shared/src/Ragdoll.cpp @@ -9,42 +9,135 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "Ragdoll.h" #include "Constraint.h" #include "DistanceConstraint.h" #include "FixedConstraint.h" +#include "PhysicsSimulation.h" +#include "SharedUtil.h" // for EPSILON -Ragdoll::Ragdoll() { +Ragdoll::Ragdoll() : _massScale(1.0f), _translation(0.0f), _translationInSimulationFrame(0.0f), + _accumulatedMovement(0.0f), _simulation(NULL) { } Ragdoll::~Ragdoll() { - clearRagdollConstraintsAndPoints(); + clearConstraintsAndPoints(); + if (_simulation) { + _simulation->removeRagdoll(this); + } } -void Ragdoll::stepRagdollForward(float deltaTime) { - int numPoints = _ragdollPoints.size(); +void Ragdoll::stepForward(float deltaTime) { + if (_simulation) { + updateSimulationTransforms(_translation - _simulation->getTranslation(), _rotation); + } + int numPoints = _points.size(); for (int i = 0; i < numPoints; ++i) { - _ragdollPoints[i].integrateForward(); + _points[i].integrateForward(); } } -void Ragdoll::clearRagdollConstraintsAndPoints() { - int numConstraints = _ragdollConstraints.size(); +void Ragdoll::clearConstraintsAndPoints() { + int numConstraints = _boneConstraints.size(); for (int i = 0; i < numConstraints; ++i) { - delete _ragdollConstraints[i]; + delete _boneConstraints[i]; } - _ragdollConstraints.clear(); - _ragdollPoints.clear(); + _boneConstraints.clear(); + numConstraints = _fixedConstraints.size(); + for (int i = 0; i < numConstraints; ++i) { + delete _fixedConstraints[i]; + } + _fixedConstraints.clear(); + _points.clear(); } -float Ragdoll::enforceRagdollConstraints() { +float Ragdoll::enforceConstraints() { float maxDistance = 0.0f; - const int numConstraints = _ragdollConstraints.size(); + // enforce the bone constraints first + int numConstraints = _boneConstraints.size(); for (int i = 0; i < numConstraints; ++i) { - DistanceConstraint* c = static_cast(_ragdollConstraints[i]); - //maxDistance = glm::max(maxDistance, _ragdollConstraints[i]->enforce()); - maxDistance = glm::max(maxDistance, c->enforce()); + maxDistance = glm::max(maxDistance, _boneConstraints[i]->enforce()); + } + // enforce FixedConstraints second + numConstraints = _fixedConstraints.size(); + for (int i = 0; i < _fixedConstraints.size(); ++i) { + maxDistance = glm::max(maxDistance, _fixedConstraints[i]->enforce()); } return maxDistance; } + +void Ragdoll::initTransform() { + _translation = glm::vec3(0.0f); + _rotation = glm::quat(); + _translationInSimulationFrame = glm::vec3(0.0f); + _rotationInSimulationFrame = glm::quat(); +} + +void Ragdoll::setTransform(const glm::vec3& translation, const glm::quat& rotation) { + _translation = translation; + _rotation = rotation; +} + +void Ragdoll::updateSimulationTransforms(const glm::vec3& translation, const glm::quat& rotation) { + const float EPSILON2 = EPSILON * EPSILON; + if (glm::distance2(translation, _translationInSimulationFrame) < EPSILON2 && + glm::abs(1.0f - glm::abs(glm::dot(rotation, _rotationInSimulationFrame))) < EPSILON2) { + // nothing to do + return; + } + + // compute linear and angular deltas + glm::vec3 deltaPosition = translation - _translationInSimulationFrame; + glm::quat deltaRotation = rotation * glm::inverse(_rotationInSimulationFrame); + + // apply the deltas to all ragdollPoints + int numPoints = _points.size(); + for (int i = 0; i < numPoints; ++i) { + _points[i].move(deltaPosition, deltaRotation, _translationInSimulationFrame); + } + + // remember the current transform + _translationInSimulationFrame = translation; + _rotationInSimulationFrame = rotation; +} + +void Ragdoll::setMassScale(float scale) { + const float MIN_SCALE = 1.0e-2f; + const float MAX_SCALE = 1.0e6f; + scale = glm::clamp(glm::abs(scale), MIN_SCALE, MAX_SCALE); + if (scale != _massScale) { + float rescale = scale / _massScale; + int numPoints = _points.size(); + for (int i = 0; i < numPoints; ++i) { + _points[i].setMass(rescale * _points[i].getMass()); + } + _massScale = scale; + } +} + +void Ragdoll::removeRootOffset(bool accumulateMovement) { + const int numPoints = _points.size(); + if (numPoints > 0) { + // shift all points so that the root aligns with the the ragdoll's position in the simulation + glm::vec3 offset = _translationInSimulationFrame - _points[0]._position; + float offsetLength = glm::length(offset); + if (offsetLength > EPSILON) { + for (int i = 0; i < numPoints; ++i) { + _points[i].shift(offset); + } + const float MIN_ROOT_OFFSET = 0.02f; + if (accumulateMovement && offsetLength > MIN_ROOT_OFFSET) { + _accumulatedMovement -= (1.0f - MIN_ROOT_OFFSET / offsetLength) * offset; + } + } + } +} + +glm::vec3 Ragdoll::getAndClearAccumulatedMovement() { + glm::vec3 movement = _accumulatedMovement; + _accumulatedMovement = glm::vec3(0.0f); + return movement; +} diff --git a/libraries/shared/src/Ragdoll.h b/libraries/shared/src/Ragdoll.h index 91a7e7330e..1ffbdb29ab 100644 --- a/libraries/shared/src/Ragdoll.h +++ b/libraries/shared/src/Ragdoll.h @@ -18,7 +18,14 @@ #include -class Constraint; +//#include "PhysicsSimulation.h" + +class DistanceConstraint; +class FixedConstraint; +class PhysicsSimulation; + +// TODO: don't derive SkeletonModel from Ragdoll so we can clean up the Ragdoll API +// (==> won't need to worry about namespace conflicts between Entity and Ragdoll). class Ragdoll { public: @@ -26,22 +33,54 @@ public: Ragdoll(); virtual ~Ragdoll(); - virtual void stepRagdollForward(float deltaTime); + virtual void stepForward(float deltaTime); /// \return max distance of point movement - float enforceRagdollConstraints(); + float enforceConstraints(); // both const and non-const getPoints() - const QVector& getRagdollPoints() const { return _ragdollPoints; } - QVector& getRagdollPoints() { return _ragdollPoints; } + const QVector& getPoints() const { return _points; } + QVector& getPoints() { return _points; } + + void initTransform(); + + /// set the translation and rotation of the Ragdoll and adjust all VerletPoints. + void setTransform(const glm::vec3& translation, const glm::quat& rotation); + + const glm::vec3& getTranslationInSimulationFrame() const { return _translationInSimulationFrame; } + + void setMassScale(float scale); + float getMassScale() const { return _massScale; } + + void clearConstraintsAndPoints(); + virtual void initPoints() = 0; + virtual void buildConstraints() = 0; + + void removeRootOffset(bool accumulateMovement); + + glm::vec3 getAndClearAccumulatedMovement(); protected: - void clearRagdollConstraintsAndPoints(); - virtual void initRagdollPoints() = 0; - virtual void buildRagdollConstraints() = 0; + float _massScale; + glm::vec3 _translation; // world-frame + glm::quat _rotation; // world-frame + glm::vec3 _translationInSimulationFrame; + glm::quat _rotationInSimulationFrame; - QVector _ragdollPoints; - QVector _ragdollConstraints; + QVector _points; + QVector _boneConstraints; + QVector _fixedConstraints; + + // The collisions are typically done in a simulation frame that is slaved to the center of one of the Ragdolls. + // To allow the Ragdoll to provide feedback of its own displacement we store it in _accumulatedMovement. + // The owner of the Ragdoll can harvest this displacement to update the rest of the object positions in the simulation. + glm::vec3 _accumulatedMovement; + +private: + void updateSimulationTransforms(const glm::vec3& translation, const glm::quat& rotation); + + friend class PhysicsSimulation; + PhysicsSimulation* _simulation; }; #endif // hifi_Ragdoll_h diff --git a/libraries/shared/src/Shape.h b/libraries/shared/src/Shape.h index 09ed30a116..2efa5b824f 100644 --- a/libraries/shared/src/Shape.h +++ b/libraries/shared/src/Shape.h @@ -14,13 +14,17 @@ #include #include +#include +#include class PhysicsEntity; +class VerletPoint; const float MAX_SHAPE_MASS = 1.0e18f; // something less than sqrt(FLT_MAX) class Shape { public: + static quint32 getNextID() { static quint32 nextID = 0; return ++nextID; } enum Type{ UNKNOWN_SHAPE = 0, @@ -30,10 +34,14 @@ public: LIST_SHAPE }; - Shape() : _type(UNKNOWN_SHAPE), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation(), _mass(MAX_SHAPE_MASS) { } - virtual ~Shape() {} + Shape() : _type(UNKNOWN_SHAPE), _owningEntity(NULL), _boundingRadius(0.f), + _translation(0.f), _rotation(), _mass(MAX_SHAPE_MASS) { + _id = getNextID(); + } + virtual ~Shape() { } int getType() const { return _type; } + quint32 getID() const { return _id; } void setEntity(PhysicsEntity* entity) { _owningEntity = entity; } PhysicsEntity* getEntity() const { return _owningEntity; } @@ -67,19 +75,28 @@ public: /// \return volume of shape in cubic meters virtual float getVolume() const { return 1.0; } + virtual void getVerletPoints(QVector& points) {} + protected: // these ctors are protected (used by derived classes only) - Shape(Type type) : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation() {} + Shape(Type type) : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(0.f), _rotation() { + _id = getNextID(); + } Shape(Type type, const glm::vec3& position) - : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation() {} + : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation() { + _id = getNextID(); + } Shape(Type type, const glm::vec3& position, const glm::quat& rotation) - : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation(rotation) {} + : _type(type), _owningEntity(NULL), _boundingRadius(0.f), _translation(position), _rotation(rotation) { + _id = getNextID(); + } void setBoundingRadius(float radius) { _boundingRadius = radius; } int _type; + unsigned int _id; PhysicsEntity* _owningEntity; float _boundingRadius; glm::vec3 _translation; diff --git a/libraries/shared/src/ShapeCollider.cpp b/libraries/shared/src/ShapeCollider.cpp index 5e4eff67ec..805e7f30f6 100644 --- a/libraries/shared/src/ShapeCollider.cpp +++ b/libraries/shared/src/ShapeCollider.cpp @@ -189,7 +189,7 @@ bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, Col glm::vec3 capsuleAxis; capsuleB->computeNormalizedAxis(capsuleAxis); float axialDistance = - glm::dot(BA, capsuleAxis); - float absAxialDistance = fabs(axialDistance); + float absAxialDistance = fabsf(axialDistance); float totalRadius = sphereA->getRadius() + capsuleB->getRadius(); if (absAxialDistance < totalRadius + capsuleB->getHalfHeight()) { glm::vec3 radialAxis = BA + axialDistance * capsuleAxis; // points from A to axis of B @@ -274,7 +274,7 @@ bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, Col glm::vec3 capsuleAxis; capsuleA->computeNormalizedAxis(capsuleAxis); float axialDistance = - glm::dot(AB, capsuleAxis); - float absAxialDistance = fabs(axialDistance); + float absAxialDistance = fabsf(axialDistance); float totalRadius = sphereB->getRadius() + capsuleA->getRadius(); if (absAxialDistance < totalRadius + capsuleA->getHalfHeight()) { glm::vec3 radialAxis = AB + axialDistance * capsuleAxis; // from sphereB to axis of capsuleA @@ -501,7 +501,7 @@ bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, // capsules are approximiately parallel but might still collide glm::vec3 BA = centerB - centerA; float axialDistance = glm::dot(BA, axisB); - if (axialDistance > totalRadius + capsuleA->getHalfHeight() + capsuleB->getHalfHeight()) { + if (fabsf(axialDistance) > totalRadius + capsuleA->getHalfHeight() + capsuleB->getHalfHeight()) { return false; } BA = BA - axialDistance * axisB; // BA now points from centerA to axisB (perp to axis) @@ -847,7 +847,7 @@ bool sphereAACube_StarkAngles(const glm::vec3& sphereCenter, float sphereRadius, // compute the nearest point on sphere glm::vec3 surfaceA = sphereCenter + sphereRadius * BA; // compute the nearest point on cube - float maxBA = glm::max(glm::max(fabs(BA.x), fabs(BA.y)), fabs(BA.z)); + float maxBA = glm::max(glm::max(fabsf(BA.x), fabsf(BA.y)), fabsf(BA.z)); glm::vec3 surfaceB = cubeCenter - (0.5f * cubeSide / maxBA) * BA; // collision happens when "vector to surfaceA from surfaceB" dots with BA to produce a positive value glm::vec3 surfaceAB = surfaceA - surfaceB; diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 57d1d7faac..ecf96e0190 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include #include @@ -79,40 +80,6 @@ bool shouldDo(float desiredInterval, float deltaTime) { return randFloat() < deltaTime / desiredInterval; } -// Safe version of glm::mix; based on the code in Nick Bobick's article, -// http://www.gamasutra.com/features/19980703/quaternions_01.htm (via Clyde, -// https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Quaternion.java) -glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float proportion) { - float cosa = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w; - float ox = q2.x, oy = q2.y, oz = q2.z, ow = q2.w, s0, s1; - - // adjust signs if necessary - if (cosa < 0.0f) { - cosa = -cosa; - ox = -ox; - oy = -oy; - oz = -oz; - ow = -ow; - } - - // calculate coefficients; if the angle is too close to zero, we must fall back - // to linear interpolation - if ((1.0f - cosa) > EPSILON) { - float angle = acosf(cosa), sina = sinf(angle); - s0 = sinf((1.0f - proportion) * angle) / sina; - s1 = sinf(proportion * angle) / sina; - - } else { - s0 = 1.0f - proportion; - s1 = proportion; - } - - return glm::normalize(glm::quat(s0 * q1.w + s1 * ow, s0 * q1.x + s1 * ox, s0 * q1.y + s1 * oy, s0 * q1.z + s1 * oz)); -} - - - - void outputBufferBits(const unsigned char* buffer, int length, QDebug* continuedDebug) { for (int i = 0; i < length; i++) { outputBits(buffer[i], continuedDebug); @@ -167,7 +134,7 @@ bool oneAtBit(unsigned char byte, int bitIndex) { } void setAtBit(unsigned char& byte, int bitIndex) { - byte += (1 << (7 - bitIndex)); + byte |= (1 << (7 - bitIndex)); } void clearAtBit(unsigned char& byte, int bitIndex) { @@ -176,7 +143,7 @@ void clearAtBit(unsigned char& byte, int bitIndex) { } } -int getSemiNibbleAt(unsigned char& byte, int bitIndex) { +int getSemiNibbleAt(unsigned char byte, int bitIndex) { return (byte >> (6 - bitIndex) & 3); // semi-nibbles store 00, 01, 10, or 11 } @@ -207,7 +174,7 @@ bool isBetween(int64_t value, int64_t max, int64_t min) { void setSemiNibbleAt(unsigned char& byte, int bitIndex, int value) { //assert(value <= 3 && value >= 0); - byte += ((value & 3) << (6 - bitIndex)); // semi-nibbles store 00, 01, 10, or 11 + byte |= ((value & 3) << (6 - bitIndex)); // semi-nibbles store 00, 01, 10, or 11 } bool isInEnvironment(const char* environment) { @@ -489,73 +456,6 @@ int removeFromSortedArrays(void* value, void** valueArray, float* keyArray, int* return -1; // error case } -// Allows sending of fixed-point numbers: radix 1 makes 15.1 number, radix 8 makes 8.8 number, etc -int packFloatScalarToSignedTwoByteFixed(unsigned char* buffer, float scalar, int radix) { - int16_t outVal = (int16_t)(scalar * (float)(1 << radix)); - memcpy(buffer, &outVal, sizeof(uint16_t)); - return sizeof(uint16_t); -} - -int unpackFloatScalarFromSignedTwoByteFixed(int16_t* byteFixedPointer, float* destinationPointer, int radix) { - *destinationPointer = *byteFixedPointer / (float)(1 << radix); - return sizeof(int16_t); -} - -int packFloatVec3ToSignedTwoByteFixed(unsigned char* destBuffer, const glm::vec3& srcVector, int radix) { - const unsigned char* startPosition = destBuffer; - destBuffer += packFloatScalarToSignedTwoByteFixed(destBuffer, srcVector.x, radix); - destBuffer += packFloatScalarToSignedTwoByteFixed(destBuffer, srcVector.y, radix); - destBuffer += packFloatScalarToSignedTwoByteFixed(destBuffer, srcVector.z, radix); - return destBuffer - startPosition; -} - -int unpackFloatVec3FromSignedTwoByteFixed(const unsigned char* sourceBuffer, glm::vec3& destination, int radix) { - const unsigned char* startPosition = sourceBuffer; - sourceBuffer += unpackFloatScalarFromSignedTwoByteFixed((int16_t*) sourceBuffer, &(destination.x), radix); - sourceBuffer += unpackFloatScalarFromSignedTwoByteFixed((int16_t*) sourceBuffer, &(destination.y), radix); - sourceBuffer += unpackFloatScalarFromSignedTwoByteFixed((int16_t*) sourceBuffer, &(destination.z), radix); - return sourceBuffer - startPosition; -} - - -int packFloatAngleToTwoByte(unsigned char* buffer, float degrees) { - const float ANGLE_CONVERSION_RATIO = (std::numeric_limits::max() / 360.f); - - uint16_t angleHolder = floorf((degrees + 180.f) * ANGLE_CONVERSION_RATIO); - memcpy(buffer, &angleHolder, sizeof(uint16_t)); - - return sizeof(uint16_t); -} - -int unpackFloatAngleFromTwoByte(const uint16_t* byteAnglePointer, float* destinationPointer) { - *destinationPointer = (*byteAnglePointer / (float) std::numeric_limits::max()) * 360.f - 180.f; - return sizeof(uint16_t); -} - -int packOrientationQuatToBytes(unsigned char* buffer, const glm::quat& quatInput) { - const float QUAT_PART_CONVERSION_RATIO = (std::numeric_limits::max() / 2.f); - uint16_t quatParts[4]; - quatParts[0] = floorf((quatInput.x + 1.f) * QUAT_PART_CONVERSION_RATIO); - quatParts[1] = floorf((quatInput.y + 1.f) * QUAT_PART_CONVERSION_RATIO); - quatParts[2] = floorf((quatInput.z + 1.f) * QUAT_PART_CONVERSION_RATIO); - quatParts[3] = floorf((quatInput.w + 1.f) * QUAT_PART_CONVERSION_RATIO); - - memcpy(buffer, &quatParts, sizeof(quatParts)); - return sizeof(quatParts); -} - -int unpackOrientationQuatFromBytes(const unsigned char* buffer, glm::quat& quatOutput) { - uint16_t quatParts[4]; - memcpy(&quatParts, buffer, sizeof(quatParts)); - - quatOutput.x = ((quatParts[0] / (float) std::numeric_limits::max()) * 2.f) - 1.f; - quatOutput.y = ((quatParts[1] / (float) std::numeric_limits::max()) * 2.f) - 1.f; - quatOutput.z = ((quatParts[2] / (float) std::numeric_limits::max()) * 2.f) - 1.f; - quatOutput.w = ((quatParts[3] / (float) std::numeric_limits::max()) * 2.f) - 1.f; - - return sizeof(quatParts); -} - float SMALL_LIMIT = 10.f; float LARGE_LIMIT = 1000.f; @@ -651,199 +551,10 @@ void debug::checkDeadBeef(void* memoryVoid, int size) { assert(memcmp((unsigned char*)memoryVoid, DEADBEEF, std::min(size, DEADBEEF_SIZE)) != 0); } -// Safe version of glm::eulerAngles; uses the factorization method described in David Eberly's -// http://www.geometrictools.com/Documentation/EulerAngles.pdf (via Clyde, -// https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Quaternion.java) -glm::vec3 safeEulerAngles(const glm::quat& q) { - float sy = 2.0f * (q.y * q.w - q.x * q.z); - glm::vec3 eulers; - if (sy < 1.0f - EPSILON) { - if (sy > -1.0f + EPSILON) { - eulers = glm::vec3( - atan2f(q.y * q.z + q.x * q.w, 0.5f - (q.x * q.x + q.y * q.y)), - asinf(sy), - atan2f(q.x * q.y + q.z * q.w, 0.5f - (q.y * q.y + q.z * q.z))); - - } else { - // not a unique solution; x + z = atan2(-m21, m11) - eulers = glm::vec3( - 0.0f, - - PI_OVER_TWO, - atan2f(q.x * q.w - q.y * q.z, 0.5f - (q.x * q.x + q.z * q.z))); - } - } else { - // not a unique solution; x - z = atan2(-m21, m11) - eulers = glm::vec3( - 0.0f, - PI_OVER_TWO, - -atan2f(q.x * q.w - q.y * q.z, 0.5f - (q.x * q.x + q.z * q.z))); - } - - // adjust so that z, rather than y, is in [-pi/2, pi/2] - if (eulers.z < -PI_OVER_TWO) { - if (eulers.x < 0.0f) { - eulers.x += PI; - } else { - eulers.x -= PI; - } - eulers.y = -eulers.y; - if (eulers.y < 0.0f) { - eulers.y += PI; - } else { - eulers.y -= PI; - } - eulers.z += PI; - - } else if (eulers.z > PI_OVER_TWO) { - if (eulers.x < 0.0f) { - eulers.x += PI; - } else { - eulers.x -= PI; - } - eulers.y = -eulers.y; - if (eulers.y < 0.0f) { - eulers.y += PI; - } else { - eulers.y -= PI; - } - eulers.z -= PI; - } - return eulers; -} - -// Helper function returns the positive angle (in radians) between two 3D vectors -float angleBetween(const glm::vec3& v1, const glm::vec3& v2) { - return acosf((glm::dot(v1, v2)) / (glm::length(v1) * glm::length(v2))); -} - -// Helper function return the rotation from the first vector onto the second -glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2) { - float angle = angleBetween(v1, v2); - if (glm::isnan(angle) || angle < EPSILON) { - return glm::quat(); - } - glm::vec3 axis; - if (angle > 179.99f * RADIANS_PER_DEGREE) { // 180 degree rotation; must use another axis - axis = glm::cross(v1, glm::vec3(1.0f, 0.0f, 0.0f)); - float axisLength = glm::length(axis); - if (axisLength < EPSILON) { // parallel to x; y will work - axis = glm::normalize(glm::cross(v1, glm::vec3(0.0f, 1.0f, 0.0f))); - } else { - axis /= axisLength; - } - } else { - axis = glm::normalize(glm::cross(v1, v2)); - // It is possible for axis to be nan even when angle is not less than EPSILON. - // For example when angle is small but not tiny but v1 and v2 and have very short lengths. - if (glm::isnan(glm::dot(axis, axis))) { - // set angle and axis to values that will generate an identity rotation - angle = 0.0f; - axis = glm::vec3(1.0f, 0.0f, 0.0f); - } - } - return glm::angleAxis(angle, axis); -} - -glm::vec3 extractTranslation(const glm::mat4& matrix) { - return glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]); -} - -void setTranslation(glm::mat4& matrix, const glm::vec3& translation) { - matrix[3][0] = translation.x; - matrix[3][1] = translation.y; - matrix[3][2] = translation.z; -} - -glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal) { - // uses the iterative polar decomposition algorithm described by Ken Shoemake at - // http://www.cs.wisc.edu/graphics/Courses/838-s2002/Papers/polar-decomp.pdf - // code adapted from Clyde, https://github.com/threerings/clyde/blob/master/core/src/main/java/com/threerings/math/Matrix4f.java - // start with the contents of the upper 3x3 portion of the matrix - glm::mat3 upper = glm::mat3(matrix); - if (!assumeOrthogonal) { - for (int i = 0; i < 10; i++) { - // store the results of the previous iteration - glm::mat3 previous = upper; - - // compute average of the matrix with its inverse transpose - float sd00 = previous[1][1] * previous[2][2] - previous[2][1] * previous[1][2]; - float sd10 = previous[0][1] * previous[2][2] - previous[2][1] * previous[0][2]; - float sd20 = previous[0][1] * previous[1][2] - previous[1][1] * previous[0][2]; - float det = previous[0][0] * sd00 + previous[2][0] * sd20 - previous[1][0] * sd10; - if (fabs(det) == 0.0f) { - // determinant is zero; matrix is not invertible - break; - } - float hrdet = 0.5f / det; - upper[0][0] = +sd00 * hrdet + previous[0][0] * 0.5f; - upper[1][0] = -sd10 * hrdet + previous[1][0] * 0.5f; - upper[2][0] = +sd20 * hrdet + previous[2][0] * 0.5f; - - upper[0][1] = -(previous[1][0] * previous[2][2] - previous[2][0] * previous[1][2]) * hrdet + previous[0][1] * 0.5f; - upper[1][1] = +(previous[0][0] * previous[2][2] - previous[2][0] * previous[0][2]) * hrdet + previous[1][1] * 0.5f; - upper[2][1] = -(previous[0][0] * previous[1][2] - previous[1][0] * previous[0][2]) * hrdet + previous[2][1] * 0.5f; - - upper[0][2] = +(previous[1][0] * previous[2][1] - previous[2][0] * previous[1][1]) * hrdet + previous[0][2] * 0.5f; - upper[1][2] = -(previous[0][0] * previous[2][1] - previous[2][0] * previous[0][1]) * hrdet + previous[1][2] * 0.5f; - upper[2][2] = +(previous[0][0] * previous[1][1] - previous[1][0] * previous[0][1]) * hrdet + previous[2][2] * 0.5f; - - // compute the difference; if it's small enough, we're done - glm::mat3 diff = upper - previous; - if (diff[0][0] * diff[0][0] + diff[1][0] * diff[1][0] + diff[2][0] * diff[2][0] + diff[0][1] * diff[0][1] + - diff[1][1] * diff[1][1] + diff[2][1] * diff[2][1] + diff[0][2] * diff[0][2] + diff[1][2] * diff[1][2] + - diff[2][2] * diff[2][2] < EPSILON) { - break; - } - } - } - - // now that we have a nice orthogonal matrix, we can extract the rotation quaternion - // using the method described in http://en.wikipedia.org/wiki/Rotation_matrix#Conversions - float x2 = fabs(1.0f + upper[0][0] - upper[1][1] - upper[2][2]); - float y2 = fabs(1.0f - upper[0][0] + upper[1][1] - upper[2][2]); - float z2 = fabs(1.0f - upper[0][0] - upper[1][1] + upper[2][2]); - float w2 = fabs(1.0f + upper[0][0] + upper[1][1] + upper[2][2]); - return glm::normalize(glm::quat(0.5f * sqrtf(w2), - 0.5f * sqrtf(x2) * (upper[1][2] >= upper[2][1] ? 1.0f : -1.0f), - 0.5f * sqrtf(y2) * (upper[2][0] >= upper[0][2] ? 1.0f : -1.0f), - 0.5f * sqrtf(z2) * (upper[0][1] >= upper[1][0] ? 1.0f : -1.0f))); -} - -glm::vec3 extractScale(const glm::mat4& matrix) { - return glm::vec3(glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2])); -} - -float extractUniformScale(const glm::mat4& matrix) { - return extractUniformScale(extractScale(matrix)); -} - -float extractUniformScale(const glm::vec3& scale) { - return (scale.x + scale.y + scale.z) / 3.0f; -} - bool isNaN(float value) { return value != value; } -bool isSimilarOrientation(const glm::quat& orientionA, const glm::quat& orientionB, float similarEnough) { - // Compute the angular distance between the two orientations - float angleOrientation = orientionA == orientionB ? 0.0f : glm::degrees(glm::angle(orientionA * glm::inverse(orientionB))); - if (isNaN(angleOrientation)) { - angleOrientation = 0.0f; - } - return (angleOrientation <= similarEnough); -} - -bool isSimilarPosition(const glm::vec3& positionA, const glm::vec3& positionB, float similarEnough) { - // Compute the distance between the two points - float positionDistance = glm::distance(positionA, positionB); - return (positionDistance <= similarEnough); -} - -QByteArray createByteArray(const glm::vec3& vector) { - return QByteArray::number(vector.x) + ',' + QByteArray::number(vector.y) + ',' + QByteArray::number(vector.z); -} - QString formatUsecTime(float usecs, int prec) { static const quint64 SECONDS_PER_MINUTE = 60; static const quint64 USECS_PER_MINUTE = USECS_PER_SECOND * SECONDS_PER_MINUTE; diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 6bb39f7e12..2a4d4143dc 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -19,9 +19,6 @@ #include // not on windows, not needed for mac or windows #endif -#include -#include - #include const int BYTES_PER_COLOR = 3; @@ -71,8 +68,6 @@ float randomSign(); /// \return -1.0 or 1.0 unsigned char randomColorValue(int minimum = 0); bool randomBoolean(); -glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); - bool shouldDo(float desiredInterval, float deltaTime); void outputBufferBits(const unsigned char* buffer, int length, QDebug* continuedDebug = NULL); @@ -82,7 +77,7 @@ int numberOfOnes(unsigned char byte); bool oneAtBit(unsigned char byte, int bitIndex); void setAtBit(unsigned char& byte, int bitIndex); void clearAtBit(unsigned char& byte, int bitIndex); -int getSemiNibbleAt(unsigned char& byte, int bitIndex); +int getSemiNibbleAt(unsigned char byte, int bitIndex); void setSemiNibbleAt(unsigned char& byte, int bitIndex, int value); int getNthBit(unsigned char byte, int ordinal); /// determines the bit placement 0-7 of the ordinal set bit @@ -108,8 +103,6 @@ int insertIntoSortedArrays(void* value, float key, int originalIndex, int removeFromSortedArrays(void* value, void** valueArray, float* keyArray, int* originalIndexArray, int currentCount, int maxCount); - - // Helper Class for debugging class debug { public: @@ -124,71 +117,10 @@ private: bool isBetween(int64_t value, int64_t max, int64_t min); -// These pack/unpack functions are designed to start specific known types in as efficient a manner -// as possible. Taking advantage of the known characteristics of the semantic types. - -// Angles are known to be between 0 and 360 degrees, this allows us to encode in 16bits with great accuracy -int packFloatAngleToTwoByte(unsigned char* buffer, float degrees); -int unpackFloatAngleFromTwoByte(const uint16_t* byteAnglePointer, float* destinationPointer); - -// Orientation Quats are known to have 4 normalized components be between -1.0 and 1.0 -// this allows us to encode each component in 16bits with great accuracy -int packOrientationQuatToBytes(unsigned char* buffer, const glm::quat& quatInput); -int unpackOrientationQuatFromBytes(const unsigned char* buffer, glm::quat& quatOutput); - -// Ratios need the be highly accurate when less than 10, but not very accurate above 10, and they -// are never greater than 1000 to 1, this allows us to encode each component in 16bits -int packFloatRatioToTwoByte(unsigned char* buffer, float ratio); -int unpackFloatRatioFromTwoByte(const unsigned char* buffer, float& ratio); - -// Near/Far Clip values need the be highly accurate when less than 10, but only integer accuracy above 10 and -// they are never greater than 16,000, this allows us to encode each component in 16bits -int packClipValueToTwoByte(unsigned char* buffer, float clipValue); -int unpackClipValueFromTwoByte(const unsigned char* buffer, float& clipValue); - -// Positive floats that don't need to be very precise -int packFloatToByte(unsigned char* buffer, float value, float scaleBy); -int unpackFloatFromByte(const unsigned char* buffer, float& value, float scaleBy); - -// Allows sending of fixed-point numbers: radix 1 makes 15.1 number, radix 8 makes 8.8 number, etc -int packFloatScalarToSignedTwoByteFixed(unsigned char* buffer, float scalar, int radix); -int unpackFloatScalarFromSignedTwoByteFixed(const int16_t* byteFixedPointer, float* destinationPointer, int radix); - -// A convenience for sending vec3's as fixed-point floats -int packFloatVec3ToSignedTwoByteFixed(unsigned char* destBuffer, const glm::vec3& srcVector, int radix); -int unpackFloatVec3FromSignedTwoByteFixed(const unsigned char* sourceBuffer, glm::vec3& destination, int radix); - -/// \return vec3 with euler angles in radians -glm::vec3 safeEulerAngles(const glm::quat& q); - -float angleBetween(const glm::vec3& v1, const glm::vec3& v2); - -glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); - -glm::vec3 extractTranslation(const glm::mat4& matrix); - -void setTranslation(glm::mat4& matrix, const glm::vec3& translation); - -glm::quat extractRotation(const glm::mat4& matrix, bool assumeOrthogonal = false); - -glm::vec3 extractScale(const glm::mat4& matrix); - -float extractUniformScale(const glm::mat4& matrix); - -float extractUniformScale(const glm::vec3& scale); - -/// \return bool are two orientations similar to each other -const float ORIENTATION_SIMILAR_ENOUGH = 5.0f; // 10 degrees in any direction -bool isSimilarOrientation(const glm::quat& orientionA, const glm::quat& orientionB, - float similarEnough = ORIENTATION_SIMILAR_ENOUGH); -const float POSITION_SIMILAR_ENOUGH = 0.1f; // 0.1 meter -bool isSimilarPosition(const glm::vec3& positionA, const glm::vec3& positionB, float similarEnough = POSITION_SIMILAR_ENOUGH); - /// \return bool is the float NaN bool isNaN(float value); -QByteArray createByteArray(const glm::vec3& vector); - QString formatUsecTime(float usecs, int prec = 3); + #endif // hifi_SharedUtil_h diff --git a/libraries/shared/src/VerletCapsuleShape.cpp b/libraries/shared/src/VerletCapsuleShape.cpp index 3ac4899682..ce324a781a 100644 --- a/libraries/shared/src/VerletCapsuleShape.cpp +++ b/libraries/shared/src/VerletCapsuleShape.cpp @@ -90,22 +90,20 @@ float VerletCapsuleShape::computeEffectiveMass(const glm::vec3& penetration, con // one endpoint will move the full amount while the other will move less. _startLagrangeCoef = startCoef / maxCoef; _endLagrangeCoef = endCoef / maxCoef; - assert(!glm::isnan(_startLagrangeCoef)); - assert(!glm::isnan(_startLagrangeCoef)); } else { // The coefficients are the same --> the collision will move both equally - // as if the object were solid. + // as if the contact were at the center of mass. _startLagrangeCoef = 1.0f; _endLagrangeCoef = 1.0f; } // the effective mass is the weighted sum of the two endpoints - return _startLagrangeCoef * _startPoint->_mass + _endLagrangeCoef * _endPoint->_mass; + return _startLagrangeCoef * _startPoint->getMass() + _endLagrangeCoef * _endPoint->getMass(); } void VerletCapsuleShape::accumulateDelta(float relativeMassFactor, const glm::vec3& penetration) { assert(!glm::isnan(relativeMassFactor)); - _startPoint->accumulateDelta(relativeMassFactor * _startLagrangeCoef * penetration); - _endPoint->accumulateDelta(relativeMassFactor * _endLagrangeCoef * penetration); + _startPoint->accumulateDelta((relativeMassFactor * _startLagrangeCoef) * penetration); + _endPoint->accumulateDelta((relativeMassFactor * _endLagrangeCoef) * penetration); } void VerletCapsuleShape::applyAccumulatedDelta() { @@ -113,6 +111,11 @@ void VerletCapsuleShape::applyAccumulatedDelta() { _endPoint->applyAccumulatedDelta(); } +void VerletCapsuleShape::getVerletPoints(QVector& points) { + points.push_back(_startPoint); + points.push_back(_endPoint); +} + // virtual float VerletCapsuleShape::getHalfHeight() const { return 0.5f * glm::distance(_startPoint->_position, _endPoint->_position); diff --git a/libraries/shared/src/VerletCapsuleShape.h b/libraries/shared/src/VerletCapsuleShape.h index 1fd84f5b1e..828e5def6c 100644 --- a/libraries/shared/src/VerletCapsuleShape.h +++ b/libraries/shared/src/VerletCapsuleShape.h @@ -47,6 +47,7 @@ public: float computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint); void accumulateDelta(float relativeMassFactor, const glm::vec3& penetration); void applyAccumulatedDelta(); + virtual void getVerletPoints(QVector& points); //float getRadius() const { return _radius; } virtual float getHalfHeight() const; diff --git a/libraries/shared/src/VerletPoint.cpp b/libraries/shared/src/VerletPoint.cpp index 641ac39341..cf9aeca149 100644 --- a/libraries/shared/src/VerletPoint.cpp +++ b/libraries/shared/src/VerletPoint.cpp @@ -11,9 +11,11 @@ #include "VerletPoint.h" +const float INTEGRATION_FRICTION_FACTOR = 0.6f; + void VerletPoint::integrateForward() { glm::vec3 oldPosition = _position; - _position += 0.6f * (_position - _lastPosition); + _position += INTEGRATION_FRICTION_FACTOR * (_position - _lastPosition); _lastPosition = oldPosition; } @@ -29,3 +31,24 @@ void VerletPoint::applyAccumulatedDelta() { _numDeltas = 0; } } + +void VerletPoint::move(const glm::vec3& deltaPosition, const glm::quat& deltaRotation, const glm::vec3& oldPivot) { + glm::vec3 arm = _position - oldPivot; + _position += deltaPosition + (deltaRotation * arm - arm); + arm = _lastPosition - oldPivot; + _lastPosition += deltaPosition + (deltaRotation * arm - arm); +} + +void VerletPoint::shift(const glm::vec3& deltaPosition) { + _position += deltaPosition; + _lastPosition += deltaPosition; +} + +void VerletPoint::setMass(float mass) { + const float MIN_MASS = 1.0e-6f; + const float MAX_MASS = 1.0e18f; + if (glm::isnan(mass)) { + mass = MIN_MASS; + } + _mass = glm::clamp(glm::abs(mass), MIN_MASS, MAX_MASS); +} diff --git a/libraries/shared/src/VerletPoint.h b/libraries/shared/src/VerletPoint.h index 076a624776..3c73e5eb01 100644 --- a/libraries/shared/src/VerletPoint.h +++ b/libraries/shared/src/VerletPoint.h @@ -13,6 +13,8 @@ #define hifi_VerletPoint_h #include +#include + class VerletPoint { public: @@ -22,16 +24,17 @@ public: void integrateForward(); void accumulateDelta(const glm::vec3& delta); void applyAccumulatedDelta(); + void move(const glm::vec3& deltaPosition, const glm::quat& deltaRotation, const glm::vec3& oldPivot); + void shift(const glm::vec3& deltaPosition); - glm::vec3 getAccumulatedDelta() const { - return (_numDeltas > 0) ? _accumulatedDelta / (float)_numDeltas : glm::vec3(0.0f); - } + void setMass(float mass); + float getMass() const { return _mass; } glm::vec3 _position; glm::vec3 _lastPosition; - float _mass; private: + float _mass; glm::vec3 _accumulatedDelta; int _numDeltas; }; diff --git a/libraries/shared/src/VerletSphereShape.cpp b/libraries/shared/src/VerletSphereShape.cpp index 10c40c6611..da8242f26a 100644 --- a/libraries/shared/src/VerletSphereShape.cpp +++ b/libraries/shared/src/VerletSphereShape.cpp @@ -36,7 +36,7 @@ const glm::vec3& VerletSphereShape::getTranslation() const { // virtual float VerletSphereShape::computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint) { - return _point->_mass; + return _point->getMass(); } // virtual @@ -48,3 +48,7 @@ void VerletSphereShape::accumulateDelta(float relativeMassFactor, const glm::vec void VerletSphereShape::applyAccumulatedDelta() { _point->applyAccumulatedDelta(); } + +void VerletSphereShape::getVerletPoints(QVector& points) { + points.push_back(_point); +} diff --git a/libraries/shared/src/VerletSphereShape.h b/libraries/shared/src/VerletSphereShape.h index 65da3b2597..c9a23faef2 100644 --- a/libraries/shared/src/VerletSphereShape.h +++ b/libraries/shared/src/VerletSphereShape.h @@ -38,6 +38,8 @@ public: float computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint); void accumulateDelta(float relativeMassFactor, const glm::vec3& penetration); void applyAccumulatedDelta(); + void getVerletPoints(QVector& points); + protected: // NOTE: VerletSphereShape does NOT own its _point diff --git a/libraries/voxels/CMakeLists.txt b/libraries/voxels/CMakeLists.txt index 2ae35da2c0..3214978a2a 100644 --- a/libraries/voxels/CMakeLists.txt +++ b/libraries/voxels/CMakeLists.txt @@ -1,31 +1,18 @@ -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 voxels) -find_package(Qt5 COMPONENTS Widgets Script) +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(Widgets Script) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) +include_glm() -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(octree ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") - -# link ZLIB -find_package(ZLIB) +link_hifi_libraries(shared octree networking) +# find ZLIB +find_package(ZLIB REQUIRED) include_directories(SYSTEM "${ZLIB_INCLUDE_DIRS}") -target_link_libraries(${TARGET_NAME} "${ZLIB_LIBRARIES}" Qt5::Widgets Qt5::Script) -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +# add it to our list of libraries to link +list(APPEND ${TARGET}_LIBRARIES_TO_LINK "${ZLIB_LIBRARIES}") + +# call macro to link our dependencies and bubble them up via a property on our target +link_shared_dependencies() \ No newline at end of file diff --git a/libraries/voxels/src/VoxelEditPacketSender.cpp b/libraries/voxels/src/VoxelEditPacketSender.cpp index 1832d5436e..50d6617278 100644 --- a/libraries/voxels/src/VoxelEditPacketSender.cpp +++ b/libraries/voxels/src/VoxelEditPacketSender.cpp @@ -117,9 +117,9 @@ void VoxelEditPacketSender::sendVoxelEditMessage(PacketType type, const VoxelDet // If we don't have voxel jurisdictions, then we will simply queue up these packets and wait till we have // jurisdictions for processing if (!voxelServersExist()) { - queuePendingPacketToNodes(type, bufferOut, sizeOut); + queuePendingPacketToNodes(type, bufferOut, sizeOut, satoshiCostForMessage(detail)); } else { - queuePacketToNodes(bufferOut, sizeOut); + queuePacketToNodes(bufferOut, sizeOut, satoshiCostForMessage(detail)); } // either way, clean up the created buffer @@ -138,7 +138,18 @@ void VoxelEditPacketSender::queueVoxelEditMessages(PacketType type, int numberOf int sizeOut = 0; if (encodeVoxelEditMessageDetails(type, 1, &details[i], &bufferOut[0], _maxPacketSize, sizeOut)) { - queueOctreeEditMessage(type, bufferOut, sizeOut); + queueOctreeEditMessage(type, bufferOut, sizeOut, satoshiCostForMessage(details[i])); } } } + +qint64 VoxelEditPacketSender::satoshiCostForMessage(const VoxelDetail& details) { + if (_satoshisPerVoxel == 0 && _satoshisPerMeterCubed == 0) { + return 0; + } else { + float meterScale = details.s * TREE_SCALE; + float totalVolume = meterScale * meterScale * meterScale; + + return _satoshisPerVoxel + (qint64) floorf(totalVolume * _satoshisPerMeterCubed); + } +} diff --git a/libraries/voxels/src/VoxelEditPacketSender.h b/libraries/voxels/src/VoxelEditPacketSender.h index 3ef257de6c..3b85155036 100644 --- a/libraries/voxels/src/VoxelEditPacketSender.h +++ b/libraries/voxels/src/VoxelEditPacketSender.h @@ -15,6 +15,7 @@ #define hifi_VoxelEditPacketSender_h #include + #include "VoxelDetail.h" /// Utility for processing, packing, queueing and sending of outbound edit voxel messages. @@ -27,7 +28,7 @@ public: /// Queues a single voxel edit message. Will potentially send a pending multi-command packet. Determines which voxel-server /// node or nodes the packet should be sent to. Can be called even before voxel servers are known, in which case up to /// MaxPendingMessages will be buffered and processed when voxel servers are known. - void queueVoxelEditMessage(PacketType type, unsigned char* codeColorBuffer, ssize_t length) { + void queueVoxelEditMessage(PacketType type, unsigned char* codeColorBuffer, size_t length) { queueOctreeEditMessage(type, codeColorBuffer, length); } @@ -49,5 +50,14 @@ public: // My server type is the voxel server virtual char getMyNodeType() const { return NodeType::VoxelServer; } + + void setSatoshisPerVoxel(qint64 satoshisPerVoxel) { _satoshisPerVoxel = satoshisPerVoxel; } + void setSatoshisPerMeterCubed(qint64 satoshisPerMeterCubed) { _satoshisPerMeterCubed = satoshisPerMeterCubed; } + + qint64 satoshiCostForMessage(const VoxelDetail& details); + +private: + qint64 _satoshisPerVoxel; + qint64 _satoshisPerMeterCubed; }; #endif // hifi_VoxelEditPacketSender_h diff --git a/libraries/voxels/src/VoxelsScriptingInterface.cpp b/libraries/voxels/src/VoxelsScriptingInterface.cpp index e877f99760..c11aca2872 100644 --- a/libraries/voxels/src/VoxelsScriptingInterface.cpp +++ b/libraries/voxels/src/VoxelsScriptingInterface.cpp @@ -71,7 +71,6 @@ void VoxelsScriptingInterface::setVoxel(float x, float y, float z, float scale, VoxelDetail addVoxelDetail = {x / (float)TREE_SCALE, y / (float)TREE_SCALE, z / (float)TREE_SCALE, scale / (float)TREE_SCALE, red, green, blue}; - // handle the local tree also... if (_tree) { if (_undoStack) { @@ -81,11 +80,16 @@ void VoxelsScriptingInterface::setVoxel(float x, float y, float z, float scale, DeleteVoxelCommand* deleteCommand = new DeleteVoxelCommand(_tree, addVoxelDetail, getVoxelPacketSender()); + _undoStackMutex.lock(); + _undoStack->beginMacro(addCommand->text()); // As QUndoStack automatically executes redo() on push, we don't need to execute the command ourselves. _undoStack->push(deleteCommand); _undoStack->push(addCommand); _undoStack->endMacro(); + + //Unlock the mutex + _undoStackMutex.unlock(); } else { // queue the destructive add queueVoxelAdd(PacketTypeVoxelSetDestructive, addVoxelDetail); @@ -110,11 +114,15 @@ void VoxelsScriptingInterface::eraseVoxel(float x, float y, float z, float scale } if (_undoStack) { + DeleteVoxelCommand* command = new DeleteVoxelCommand(_tree, deleteVoxelDetail, getVoxelPacketSender()); + + _undoStackMutex.lock(); // As QUndoStack automatically executes redo() on push, we don't need to execute the command ourselves. _undoStack->push(command); + _undoStackMutex.unlock(); } else { getVoxelPacketSender()->queueVoxelEditMessages(PacketTypeVoxelErase, 1, &deleteVoxelDetail); _tree->deleteVoxelAt(deleteVoxelDetail.x, deleteVoxelDetail.y, deleteVoxelDetail.z, deleteVoxelDetail.s); diff --git a/libraries/voxels/src/VoxelsScriptingInterface.h b/libraries/voxels/src/VoxelsScriptingInterface.h index 787c37fb20..2e1fc2a8d5 100644 --- a/libraries/voxels/src/VoxelsScriptingInterface.h +++ b/libraries/voxels/src/VoxelsScriptingInterface.h @@ -102,6 +102,7 @@ private: void queueVoxelAdd(PacketType addPacketType, VoxelDetail& addVoxelDetails); VoxelTree* _tree; QUndoStack* _undoStack; + QMutex _undoStackMutex; }; #endif // hifi_VoxelsScriptingInterface_h diff --git a/tests/audio/CMakeLists.txt b/tests/audio/CMakeLists.txt index b7375a0086..974b4dcd09 100644 --- a/tests/audio/CMakeLists.txt +++ b/tests/audio/CMakeLists.txt @@ -1,34 +1,10 @@ set(TARGET_NAME audio-tests) -set(ROOT_DIR ../..) -set(MACRO_DIR ${ROOT_DIR}/cmake/macros) +setup_hifi_project() -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") - -#find_package(Qt5Network REQUIRED) -#find_package(Qt5Script REQUIRED) -#find_package(Qt5Widgets REQUIRED) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} ${ROOT_DIR}) - -#qt5_use_modules(${TARGET_NAME} Network Script Widgets) - -#include glm -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} ${ROOT_DIR}) +include_glm() # link in the shared libraries -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) - -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) +link_hifi_libraries(shared audio networking) +link_shared_dependencies() \ No newline at end of file diff --git a/tests/jitter/CMakeLists.txt b/tests/jitter/CMakeLists.txt new file mode 100644 index 0000000000..d0b366e7ef --- /dev/null +++ b/tests/jitter/CMakeLists.txt @@ -0,0 +1,8 @@ +set(TARGET_NAME jitter-tests) + +setup_hifi_project() + +# link in the shared libraries +link_hifi_libraries(shared networking) + +link_shared_dependencies() \ No newline at end of file diff --git a/tests/jitter/src/main.cpp b/tests/jitter/src/main.cpp new file mode 100644 index 0000000000..8c93b7dbec --- /dev/null +++ b/tests/jitter/src/main.cpp @@ -0,0 +1,375 @@ +// +// main.cpp +// JitterTester +// +// Created by Philip on 8/1/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include +#ifdef _WINDOWS +#include +#else +#include +#include +#endif +#include +#include + +#include +#include +#include // for usecTimestampNow +#include +#include + +const quint64 MSEC_TO_USEC = 1000; +const quint64 LARGE_STATS_TIME = 500; // we don't expect stats calculation to take more than this many usecs + +void runSend(const char* addressOption, int port, int gap, int size, int report); +void runReceive(const char* addressOption, int port, int gap, int size, int report); + +int main(int argc, const char * argv[]) { + if (argc != 7) { + printf("usage: jitter-tests <--send|--receive>
\n"); + exit(1); + } + const char* typeOption = argv[1]; + const char* addressOption = argv[2]; + const char* portOption = argv[3]; + const char* gapOption = argv[4]; + const char* sizeOption = argv[5]; + const char* reportOption = argv[6]; + int port = atoi(portOption); + int gap = atoi(gapOption); + int size = atoi(sizeOption); + int report = atoi(reportOption); + + std::cout << "type:" << typeOption << "\n"; + std::cout << "address:" << addressOption << "\n"; + std::cout << "port:" << port << "\n"; + std::cout << "gap:" << gap << "\n"; + std::cout << "size:" << size << "\n"; + + if (strcmp(typeOption, "--send") == 0) { + runSend(addressOption, port, gap, size, report); + } else if (strcmp(typeOption, "--receive") == 0) { + runReceive(addressOption, port, gap, size, report); + } + exit(1); +} + +void runSend(const char* addressOption, int port, int gap, int size, int report) { + std::cout << "runSend...\n"; + +#ifdef _WIN32 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { + printf("WSAStartup failed with error %d\n", WSAGetLastError()); + return; + } +#endif + + int sockfd; + struct sockaddr_in servaddr; + + sockfd = socket(AF_INET, SOCK_DGRAM, 0); + + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = inet_addr(addressOption); + servaddr.sin_port = htons(port); + + const int SAMPLES_FOR_SECOND = 1000000 / gap; + std::cout << "SAMPLES_FOR_SECOND:" << SAMPLES_FOR_SECOND << "\n"; + const int INTERVALS_PER_30_SECONDS = 30; + std::cout << "INTERVALS_PER_30_SECONDS:" << INTERVALS_PER_30_SECONDS << "\n"; + const int SAMPLES_FOR_30_SECONDS = 30 * SAMPLES_FOR_SECOND; + std::cout << "SAMPLES_FOR_30_SECONDS:" << SAMPLES_FOR_30_SECONDS << "\n"; + const int REPORTS_FOR_30_SECONDS = 30 * MSECS_PER_SECOND / report; + std::cout << "REPORTS_FOR_30_SECONDS:" << REPORTS_FOR_30_SECONDS << "\n"; + + int intervalsPerReport = report / MSEC_TO_USEC; + if (intervalsPerReport < 1) { + intervalsPerReport = 1; + } + std::cout << "intervalsPerReport:" << intervalsPerReport << "\n"; + MovingMinMaxAvg timeGaps(SAMPLES_FOR_SECOND, INTERVALS_PER_30_SECONDS); + MovingMinMaxAvg timeGapsPerReport(SAMPLES_FOR_SECOND, intervalsPerReport); + + char* outputBuffer = new char[size]; + memset(outputBuffer, 0, size); + + quint16 outgoingSequenceNumber = 0; + + + StDev stDevReportInterval; + StDev stDev30s; + StDev stDev; + + SimpleMovingAverage averageNetworkTime(SAMPLES_FOR_30_SECONDS); + SimpleMovingAverage averageStatsCalcultionTime(SAMPLES_FOR_30_SECONDS); + float lastStatsCalculationTime = 0.0f; // we add out stats calculation time in the next calculation window + bool hasStatsCalculationTime = false; + + quint64 last = usecTimestampNow(); + quint64 lastReport = 0; + + while (true) { + + quint64 now = usecTimestampNow(); + int actualGap = now - last; + + + if (actualGap >= gap) { + + // pack seq num + memcpy(outputBuffer, &outgoingSequenceNumber, sizeof(quint16)); + + quint64 networkStart = usecTimestampNow(); + int n = sendto(sockfd, outputBuffer, size, 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); + quint64 networkEnd = usecTimestampNow(); + float networkElapsed = (float)(networkEnd - networkStart); + + if (n < 0) { + std::cout << "Send error: " << strerror(errno) << "\n"; + } + outgoingSequenceNumber++; + + quint64 statsCalcultionStart = usecTimestampNow(); + + int gapDifferece = actualGap - gap; + + timeGaps.update(gapDifferece); + timeGapsPerReport.update(gapDifferece); + stDev.addValue(gapDifferece); + stDev30s.addValue(gapDifferece); + stDevReportInterval.addValue(gapDifferece); + last = now; + + // track out network time and stats calculation times + averageNetworkTime.updateAverage(networkElapsed); + + // for our stats calculation time, we actually delay the updating by one sample. + // we do this so that the calculation of the average timing for the stats calculation + // happen inside of the calculation processing. This ensures that tracking stats on + // stats calculation doesn't side effect the remaining running time. + if (hasStatsCalculationTime) { + averageStatsCalcultionTime.updateAverage(lastStatsCalculationTime); + } + + if (now - lastReport >= (report * MSEC_TO_USEC)) { + + std::cout << "\n" + << "SEND gap Difference From Expected\n" + << "Overall:\n" + << "min: " << timeGaps.getMin() << " usecs, " + << "max: " << timeGaps.getMax() << " usecs, " + << "avg: " << timeGaps.getAverage() << " usecs, " + << "stdev: " << stDev.getStDev() << " usecs\n" + << "Last 30s:\n" + << "min: " << timeGaps.getWindowMin() << " usecs, " + << "max: " << timeGaps.getWindowMax() << " usecs, " + << "avg: " << timeGaps.getWindowAverage() << " usecs, " + << "stdev: " << stDev30s.getStDev() << " usecs\n" + << "Last report interval:\n" + << "min: " << timeGapsPerReport.getWindowMin() << " usecs, " + << "max: " << timeGapsPerReport.getWindowMax() << " usecs, " + << "avg: " << timeGapsPerReport.getWindowAverage() << " usecs, " + << "stdev: " << stDevReportInterval.getStDev() << " usecs\n" + << "Average Execution Times Last 30s:\n" + << " network: " << averageNetworkTime.getAverage() << " usecs average\n" + << " stats: " << averageStatsCalcultionTime.getAverage() << " usecs average" + << "\n"; + + stDevReportInterval.reset(); + if (stDev30s.getSamples() > SAMPLES_FOR_30_SECONDS) { + stDev30s.reset(); + } + + lastReport = now; + } + + quint64 statsCalcultionEnd = usecTimestampNow(); + lastStatsCalculationTime = (float)(statsCalcultionEnd - statsCalcultionStart); + if (lastStatsCalculationTime > LARGE_STATS_TIME) { + qDebug() << "WARNING -- unexpectedly large lastStatsCalculationTime=" << lastStatsCalculationTime; + } + hasStatsCalculationTime = true; + + } + } + delete[] outputBuffer; + +#ifdef _WIN32 + WSACleanup(); +#endif +} + +void runReceive(const char* addressOption, int port, int gap, int size, int report) { + std::cout << "runReceive...\n"; + +#ifdef _WIN32 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { + printf("WSAStartup failed with error %d\n", WSAGetLastError()); + return; + } +#endif + + int sockfd, n; + struct sockaddr_in myaddr; + + sockfd = socket(AF_INET, SOCK_DGRAM, 0); + + memset(&myaddr, 0, sizeof(myaddr)); + myaddr.sin_family = AF_INET; + myaddr.sin_addr.s_addr = htonl(INADDR_ANY); + myaddr.sin_port = htons(port); + + + const int SAMPLES_FOR_SECOND = 1000000 / gap; + std::cout << "SAMPLES_FOR_SECOND:" << SAMPLES_FOR_SECOND << "\n"; + const int INTERVALS_PER_30_SECONDS = 30; + std::cout << "INTERVALS_PER_30_SECONDS:" << INTERVALS_PER_30_SECONDS << "\n"; + const int SAMPLES_FOR_30_SECONDS = 30 * SAMPLES_FOR_SECOND; + std::cout << "SAMPLES_FOR_30_SECONDS:" << SAMPLES_FOR_30_SECONDS << "\n"; + const int REPORTS_FOR_30_SECONDS = 30 * MSECS_PER_SECOND / report; + std::cout << "REPORTS_FOR_30_SECONDS:" << REPORTS_FOR_30_SECONDS << "\n"; + + int intervalsPerReport = report / MSEC_TO_USEC; + if (intervalsPerReport < 1) { + intervalsPerReport = 1; + } + std::cout << "intervalsPerReport:" << intervalsPerReport << "\n"; + MovingMinMaxAvg timeGaps(SAMPLES_FOR_SECOND, INTERVALS_PER_30_SECONDS); + MovingMinMaxAvg timeGapsPerReport(SAMPLES_FOR_SECOND, intervalsPerReport); + + char* inputBuffer = new char[size]; + memset(inputBuffer, 0, size); + + + SequenceNumberStats seqStats(REPORTS_FOR_30_SECONDS); + + StDev stDevReportInterval; + StDev stDev30s; + StDev stDev; + + SimpleMovingAverage averageNetworkTime(SAMPLES_FOR_30_SECONDS); + SimpleMovingAverage averageStatsCalcultionTime(SAMPLES_FOR_30_SECONDS); + float lastStatsCalculationTime = 0.0f; // we add out stats calculation time in the next calculation window + bool hasStatsCalculationTime = false; + + if (bind(sockfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) { + std::cout << "bind failed\n"; + return; + } + + quint64 last = 0; // first case + quint64 lastReport = 0; + + while (true) { + + quint64 networkStart = usecTimestampNow(); + n = recvfrom(sockfd, inputBuffer, size, 0, NULL, NULL); // we don't care about where it came from + quint64 networkEnd = usecTimestampNow(); + float networkElapsed = (float)(networkEnd - networkStart); + + if (n < 0) { + std::cout << "Receive error: " << strerror(errno) << "\n"; + } + + // parse seq num + quint16 incomingSequenceNumber = *(reinterpret_cast(inputBuffer)); + seqStats.sequenceNumberReceived(incomingSequenceNumber); + + if (last == 0) { + last = usecTimestampNow(); + std::cout << "first packet received\n"; + } else { + + quint64 statsCalcultionStart = usecTimestampNow(); + quint64 now = usecTimestampNow(); + int actualGap = now - last; + + int gapDifferece = actualGap - gap; + timeGaps.update(gapDifferece); + timeGapsPerReport.update(gapDifferece); + stDev.addValue(gapDifferece); + stDev30s.addValue(gapDifferece); + stDevReportInterval.addValue(gapDifferece); + last = now; + + // track out network time and stats calculation times + averageNetworkTime.updateAverage(networkElapsed); + + // for our stats calculation time, we actually delay the updating by one sample. + // we do this so that the calculation of the average timing for the stats calculation + // happen inside of the calculation processing. This ensures that tracking stats on + // stats calculation doesn't side effect the remaining running time. + if (hasStatsCalculationTime) { + averageStatsCalcultionTime.updateAverage(lastStatsCalculationTime); + } + + if (now - lastReport >= (report * MSEC_TO_USEC)) { + + seqStats.pushStatsToHistory(); + + std::cout << "RECEIVE gap Difference From Expected\n" + << "Overall:\n" + << "min: " << timeGaps.getMin() << " usecs, " + << "max: " << timeGaps.getMax() << " usecs, " + << "avg: " << timeGaps.getAverage() << " usecs, " + << "stdev: " << stDev.getStDev() << " usecs\n" + << "Last 30s:\n" + << "min: " << timeGaps.getWindowMin() << " usecs, " + << "max: " << timeGaps.getWindowMax() << " usecs, " + << "avg: " << timeGaps.getWindowAverage() << " usecs, " + << "stdev: " << stDev30s.getStDev() << " usecs\n" + << "Last report interval:\n" + << "min: " << timeGapsPerReport.getWindowMin() << " usecs, " + << "max: " << timeGapsPerReport.getWindowMax() << " usecs, " + << "avg: " << timeGapsPerReport.getWindowAverage() << " usecs, " + << "stdev: " << stDevReportInterval.getStDev() << " usecs\n" + << "Average Execution Times Last 30s:\n" + << " network: " << averageNetworkTime.getAverage() << " usecs average\n" + << " stats: " << averageStatsCalcultionTime.getAverage() << " usecs average" + << "\n"; + stDevReportInterval.reset(); + + if (stDev30s.getSamples() > SAMPLES_FOR_30_SECONDS) { + stDev30s.reset(); + } + + PacketStreamStats packetStatsLast30s = seqStats.getStatsForHistoryWindow(); + PacketStreamStats packetStatsLastReportInterval = seqStats.getStatsForLastHistoryInterval(); + + std::cout << "RECEIVE Packet Stats\n" + << "Overall:\n" + << "lost: " << seqStats.getLost() << ", " + << "lost %: " << seqStats.getStats().getLostRate() * 100.0f << "%\n" + << "Last 30s:\n" + << "lost: " << packetStatsLast30s._lost << ", " + << "lost %: " << packetStatsLast30s.getLostRate() * 100.0f << "%\n" + << "Last report interval:\n" + << "lost: " << packetStatsLastReportInterval._lost << ", " + << "lost %: " << packetStatsLastReportInterval.getLostRate() * 100.0f << "%\n" + << "\n\n"; + + lastReport = now; + } + + quint64 statsCalcultionEnd = usecTimestampNow(); + + lastStatsCalculationTime = (float)(statsCalcultionEnd - statsCalcultionStart); + if (lastStatsCalculationTime > LARGE_STATS_TIME) { + qDebug() << "WARNING -- unexpectedly large lastStatsCalculationTime=" << lastStatsCalculationTime; + } + hasStatsCalculationTime = true; + } + } + delete[] inputBuffer; + +#ifdef _WIN32 + WSACleanup(); +#endif +} diff --git a/tests/metavoxels/CMakeLists.txt b/tests/metavoxels/CMakeLists.txt index f4c0695362..732c974f95 100644 --- a/tests/metavoxels/CMakeLists.txt +++ b/tests/metavoxels/CMakeLists.txt @@ -1,32 +1,12 @@ set(TARGET_NAME metavoxel-tests) -set(ROOT_DIR ../..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") +auto_mtc() -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") +include_glm() -find_package(Qt5 COMPONENTS Network Script Widgets) - -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} "${ROOT_DIR}") - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE "${AUTOMTC_SRC}") - -#include glm -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +setup_hifi_project(Network Script Widgets) # link in the shared libraries -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") - -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) - -target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script) +link_hifi_libraries(metavoxels networking shared) +link_shared_dependencies() \ No newline at end of file diff --git a/tests/networking/CMakeLists.txt b/tests/networking/CMakeLists.txt index 64b5f273d1..a7293226b3 100644 --- a/tests/networking/CMakeLists.txt +++ b/tests/networking/CMakeLists.txt @@ -1,33 +1,8 @@ set(TARGET_NAME networking-tests) -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/") - -#find_package(Qt5Network REQUIRED) -#find_package(Qt5Script REQUIRED) -#find_package(Qt5Widgets REQUIRED) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} ${ROOT_DIR}) - -#qt5_use_modules(${TARGET_NAME} Network Script Widgets) - -#include glm -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} ${ROOT_DIR}) +setup_hifi_project() # link in the shared libraries -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) - -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) +link_hifi_libraries(shared networking) +link_shared_dependencies() \ No newline at end of file diff --git a/tests/networking/src/SequenceNumberStatsTests.cpp b/tests/networking/src/SequenceNumberStatsTests.cpp index de487267e0..ded67b1ab6 100644 --- a/tests/networking/src/SequenceNumberStatsTests.cpp +++ b/tests/networking/src/SequenceNumberStatsTests.cpp @@ -9,18 +9,19 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "SequenceNumberStatsTests.h" - -#include "SharedUtil.h" +#include #include +#include + +#include "SequenceNumberStatsTests.h" void SequenceNumberStatsTests::runAllTests() { - rolloverTest(); earlyLateTest(); duplicateTest(); pruneTest(); + resyncTest(); } const quint32 UINT16_RANGE = std::numeric_limits::max() + 1; @@ -38,12 +39,11 @@ void SequenceNumberStatsTests::rolloverTest() { stats.sequenceNumberReceived(seq); seq = seq + (quint16)1; - assert(stats.getNumDuplicate() == 0); - assert(stats.getNumEarly() == 0); - assert(stats.getNumLate() == 0); - assert(stats.getNumLost() == 0); - assert(stats.getNumReceived() == i + 1); - assert(stats.getNumRecovered() == 0); + assert(stats.getEarly() == 0); + assert(stats.getLate() == 0); + assert(stats.getLost() == 0); + assert(stats.getReceived() == i + 1); + assert(stats.getRecovered() == 0); } stats.reset(); } @@ -69,12 +69,11 @@ void SequenceNumberStatsTests::earlyLateTest() { seq = seq + (quint16)1; numSent++; - assert(stats.getNumDuplicate() == 0); - assert(stats.getNumEarly() == numEarly); - assert(stats.getNumLate() == numLate); - assert(stats.getNumLost() == numLost); - assert(stats.getNumReceived() == numSent); - assert(stats.getNumRecovered() == numRecovered); + assert(stats.getEarly() == numEarly); + assert(stats.getLate() == numLate); + assert(stats.getLost() == numLost); + assert(stats.getReceived() == numSent); + assert(stats.getRecovered() == numRecovered); } // skip 10 @@ -89,12 +88,11 @@ void SequenceNumberStatsTests::earlyLateTest() { seq = seq + (quint16)1; numSent++; - assert(stats.getNumDuplicate() == 0); - assert(stats.getNumEarly() == numEarly); - assert(stats.getNumLate() == numLate); - assert(stats.getNumLost() == numLost); - assert(stats.getNumReceived() == numSent); - assert(stats.getNumRecovered() == numRecovered); + assert(stats.getEarly() == numEarly); + assert(stats.getLate() == numLate); + assert(stats.getLost() == numLost); + assert(stats.getReceived() == numSent); + assert(stats.getRecovered() == numRecovered); } // send ones we skipped @@ -106,12 +104,11 @@ void SequenceNumberStatsTests::earlyLateTest() { numLost--; numRecovered++; - assert(stats.getNumDuplicate() == 0); - assert(stats.getNumEarly() == numEarly); - assert(stats.getNumLate() == numLate); - assert(stats.getNumLost() == numLost); - assert(stats.getNumReceived() == numSent); - assert(stats.getNumRecovered() == numRecovered); + assert(stats.getEarly() == numEarly); + assert(stats.getLate() == numLate); + assert(stats.getLost() == numLost); + assert(stats.getReceived() == numSent); + assert(stats.getRecovered() == numRecovered); } } stats.reset(); @@ -135,7 +132,7 @@ void SequenceNumberStatsTests::duplicateTest() { quint32 numLost = 0; for (int R = 0; R < 2; R++) { - for (int T = 0; T < 10000; T++) { + for (int T = 0; T < 5; T++) { quint16 duplicate = seq; @@ -145,12 +142,12 @@ void SequenceNumberStatsTests::duplicateTest() { seq = seq + (quint16)1; numSent++; - assert(stats.getNumDuplicate() == numDuplicate); - assert(stats.getNumEarly() == numEarly); - assert(stats.getNumLate() == numLate); - assert(stats.getNumLost() == numLost); - assert(stats.getNumReceived() == numSent); - assert(stats.getNumRecovered() == 0); + assert(stats.getUnreasonable() == numDuplicate); + assert(stats.getEarly() == numEarly); + assert(stats.getLate() == numLate); + assert(stats.getLost() == numLost); + assert(stats.getReceived() == numSent); + assert(stats.getRecovered() == 0); } // skip 10 @@ -167,12 +164,12 @@ void SequenceNumberStatsTests::duplicateTest() { seq = seq + (quint16)1; numSent++; - assert(stats.getNumDuplicate() == numDuplicate); - assert(stats.getNumEarly() == numEarly); - assert(stats.getNumLate() == numLate); - assert(stats.getNumLost() == numLost); - assert(stats.getNumReceived() == numSent); - assert(stats.getNumRecovered() == 0); + assert(stats.getUnreasonable() == numDuplicate); + assert(stats.getEarly() == numEarly); + assert(stats.getLate() == numLate); + assert(stats.getLost() == numLost); + assert(stats.getReceived() == numSent); + assert(stats.getRecovered() == 0); } // send 5 duplicates from before skip @@ -183,12 +180,12 @@ void SequenceNumberStatsTests::duplicateTest() { numDuplicate++; numLate++; - assert(stats.getNumDuplicate() == numDuplicate); - assert(stats.getNumEarly() == numEarly); - assert(stats.getNumLate() == numLate); - assert(stats.getNumLost() == numLost); - assert(stats.getNumReceived() == numSent); - assert(stats.getNumRecovered() == 0); + assert(stats.getUnreasonable() == numDuplicate); + assert(stats.getEarly() == numEarly); + assert(stats.getLate() == numLate); + assert(stats.getLost() == numLost); + assert(stats.getReceived() == numSent); + assert(stats.getRecovered() == 0); } // send 5 duplicates from after skip @@ -199,12 +196,12 @@ void SequenceNumberStatsTests::duplicateTest() { numDuplicate++; numLate++; - assert(stats.getNumDuplicate() == numDuplicate); - assert(stats.getNumEarly() == numEarly); - assert(stats.getNumLate() == numLate); - assert(stats.getNumLost() == numLost); - assert(stats.getNumReceived() == numSent); - assert(stats.getNumRecovered() == 0); + assert(stats.getUnreasonable() == numDuplicate); + assert(stats.getEarly() == numEarly); + assert(stats.getLate() == numLate); + assert(stats.getLost() == numLost); + assert(stats.getReceived() == numSent); + assert(stats.getRecovered() == 0); } } stats.reset(); @@ -278,3 +275,47 @@ void SequenceNumberStatsTests::pruneTest() { numLost = 0; } } + +void SequenceNumberStatsTests::resyncTest() { + + SequenceNumberStats stats(0); + + quint16 sequence; + + sequence = 89; + stats.sequenceNumberReceived(sequence); + + assert(stats.getUnreasonable() == 0); + + sequence = 2990; + for (int i = 0; i < 10; i++) { + stats.sequenceNumberReceived(sequence); + sequence += (quint16)1; + } + + assert(stats.getUnreasonable() == 0); + + + sequence = 0; + for (int R = 0; R < 7; R++) { + stats.sequenceNumberReceived(sequence); + sequence += (quint16)1; + } + + assert(stats.getUnreasonable() == 7); + + sequence = 6000; + for (int R = 0; R < 7; R++) { + stats.sequenceNumberReceived(sequence); + sequence += (quint16)1; + } + + assert(stats.getUnreasonable() == 14); + + sequence = 9000; + for (int i = 0; i < 10; i++) { + stats.sequenceNumberReceived(sequence); + sequence += (quint16)1; + } + assert(stats.getUnreasonable() == 0); +} diff --git a/tests/networking/src/SequenceNumberStatsTests.h b/tests/networking/src/SequenceNumberStatsTests.h index 53a0b66480..6b1fa3dde7 100644 --- a/tests/networking/src/SequenceNumberStatsTests.h +++ b/tests/networking/src/SequenceNumberStatsTests.h @@ -23,6 +23,7 @@ namespace SequenceNumberStatsTests { void earlyLateTest(); void duplicateTest(); void pruneTest(); + void resyncTest(); }; #endif // hifi_SequenceNumberStatsTests_h diff --git a/tests/octree/CMakeLists.txt b/tests/octree/CMakeLists.txt index c7a6500b6d..5d37b51abe 100644 --- a/tests/octree/CMakeLists.txt +++ b/tests/octree/CMakeLists.txt @@ -1,43 +1,10 @@ set(TARGET_NAME octree-tests) -set(ROOT_DIR ../..) -set(MACRO_DIR ${ROOT_DIR}/cmake/macros) +setup_hifi_project(Script Network) -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") - -find_package(Qt5Network REQUIRED) -find_package(Qt5Script REQUIRED) -find_package(Qt5Widgets REQUIRED) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} ${ROOT_DIR}) - -qt5_use_modules(${TARGET_NAME} Network Script Widgets) - -#include glm -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} ${ROOT_DIR}) +include_glm() # link in the shared libraries -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(models ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(octree ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(animation ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(fbx ${TARGET_NAME} ${ROOT_DIR}) -link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) - -IF (WIN32) - # add a definition for ssize_t so that windows doesn't bail - add_definitions(-Dssize_t=long) - - #target_link_libraries(${TARGET_NAME} Winmm Ws2_32) - target_link_libraries(${TARGET_NAME} wsock32.lib) -ENDIF(WIN32) - +link_hifi_libraries(animation fbx models networking octree shared) +link_shared_dependencies() \ No newline at end of file diff --git a/tests/physics/CMakeLists.txt b/tests/physics/CMakeLists.txt index 643d318f7d..96aaf48860 100644 --- a/tests/physics/CMakeLists.txt +++ b/tests/physics/CMakeLists.txt @@ -1,32 +1,10 @@ set(TARGET_NAME physics-tests) -set(ROOT_DIR ../..) -set(MACRO_DIR ${ROOT_DIR}/cmake/macros) +setup_hifi_project() -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") - -#find_package(Qt5Network REQUIRED) -#find_package(Qt5Script REQUIRED) -#find_package(Qt5Widgets REQUIRED) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} ${ROOT_DIR}) - -#qt5_use_modules(${TARGET_NAME} Network Script Widgets) - -#include glm -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} ${ROOT_DIR}) +include_glm() # link in the shared libraries -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) - -IF (WIN32) - #target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) +link_hifi_libraries(shared) +link_shared_dependencies() \ No newline at end of file diff --git a/tests/shared/CMakeLists.txt b/tests/shared/CMakeLists.txt index 0785314d36..fe3843e9eb 100644 --- a/tests/shared/CMakeLists.txt +++ b/tests/shared/CMakeLists.txt @@ -1,32 +1,10 @@ set(TARGET_NAME shared-tests) -set(ROOT_DIR ../..) -set(MACRO_DIR ${ROOT_DIR}/cmake/macros) +setup_hifi_project() -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") - -#find_package(Qt5Network REQUIRED) -#find_package(Qt5Script REQUIRED) -#find_package(Qt5Widgets REQUIRED) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -include(${MACRO_DIR}/AutoMTC.cmake) -auto_mtc(${TARGET_NAME} ${ROOT_DIR}) - -#qt5_use_modules(${TARGET_NAME} Network Script Widgets) - -#include glm -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} ${ROOT_DIR}) +include_glm() # link in the shared libraries -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) - -IF (WIN32) - #target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) +link_hifi_libraries(shared) +link_shared_dependencies() \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index c9c0690cc7..32a82627a3 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,4 +1,3 @@ - # add the tool directories add_subdirectory(bitstream2json) add_subdirectory(json2bitstream) diff --git a/tools/bitstream2json/CMakeLists.txt b/tools/bitstream2json/CMakeLists.txt index 576406e787..bc23a1e193 100644 --- a/tools/bitstream2json/CMakeLists.txt +++ b/tools/bitstream2json/CMakeLists.txt @@ -1,25 +1,8 @@ set(TARGET_NAME bitstream2json) +setup_hifi_project(Widgets Script) -set(ROOT_DIR ../..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") +include_glm() -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") +link_hifi_libraries(metavoxels) -find_package(Qt5 COMPONENTS Network Script Widgets) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") - -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") - -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) - -target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script) +link_shared_dependencies() \ No newline at end of file diff --git a/tools/json2bitstream/CMakeLists.txt b/tools/json2bitstream/CMakeLists.txt index 5ff4673298..91b56c18fd 100644 --- a/tools/json2bitstream/CMakeLists.txt +++ b/tools/json2bitstream/CMakeLists.txt @@ -1,25 +1,8 @@ set(TARGET_NAME json2bitstream) +setup_hifi_project(Widgets Script) -set(ROOT_DIR ../..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") +include_glm() -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") +link_hifi_libraries(metavoxels) -find_package(Qt5 COMPONENTS Network Script Widgets) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") - -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") - -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) - -target_link_libraries(${TARGET_NAME} Qt5::Network Qt5::Widgets Qt5::Script) +link_shared_dependencies() \ No newline at end of file diff --git a/tools/mtc/CMakeLists.txt b/tools/mtc/CMakeLists.txt index 582c5e3bfd..4dfa8421ff 100644 --- a/tools/mtc/CMakeLists.txt +++ b/tools/mtc/CMakeLists.txt @@ -1,7 +1,4 @@ set(TARGET_NAME mtc) +setup_hifi_project() -set(ROOT_DIR ../..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) \ No newline at end of file +link_shared_dependencies() \ No newline at end of file diff --git a/voxel-edit/CMakeLists.txt b/voxel-edit/CMakeLists.txt index 006dfb0599..b61ee6d132 100644 --- a/voxel-edit/CMakeLists.txt +++ b/voxel-edit/CMakeLists.txt @@ -1,40 +1,9 @@ set(TARGET_NAME voxel-edit) -set(ROOT_DIR ..) -set(MACRO_DIR "${ROOT_DIR}/cmake/macros") +setup_hifi_project() -# setup for find modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/modules/") +include_glm() -# set up the external glm library -include(${MACRO_DIR}/IncludeGLM.cmake) -include_glm(${TARGET_NAME} "${ROOT_DIR}") +link_hifi_libraries(networking octree shared voxels) -find_package(Qt5Script REQUIRED) - -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - -# link in the shared library -include(${MACRO_DIR}/LinkHifiLibrary.cmake) -link_hifi_library(shared ${TARGET_NAME} "${ROOT_DIR}") - -# link in the hifi octree library -link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}") - -# link in the hifi voxels library -link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}") - -# link in the hifi networking library -link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}") - -IF (WIN32) - target_link_libraries(${TARGET_NAME} Winmm Ws2_32) -ENDIF(WIN32) - -target_link_libraries(${TARGET_NAME} Qt5::Script) - -# add a definition for ssize_t so that windows doesn't bail -if (WIN32) - add_definitions(-Dssize_t=long) -endif () \ No newline at end of file +link_shared_dependencies() \ No newline at end of file