diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index d4a83d00f0..75265bdf10 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -123,6 +123,30 @@ void broadcastIdentityPacket() { } } +void broadcastBillboardPacket(const SharedNodePointer& sendingNode) { + AvatarMixerClientData* nodeData = static_cast(sendingNode->getLinkedData()); + QByteArray packet = byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard); + packet.append(sendingNode->getUUID().toRfc4122()); + packet.append(nodeData->getBillboard()); + + NodeList* nodeList = NodeList::getInstance(); + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { + if (node->getType() == NodeType::Agent && node != sendingNode) { + nodeList->writeDatagram(packet, node); + } + } +} + +void broadcastBillboardPackets() { + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getLinkedData() && node->getType() == NodeType::Agent) { + AvatarMixerClientData* nodeData = static_cast(node->getLinkedData()); + broadcastBillboardPacket(node); + nodeData->setHasSentBillboardBetweenKeyFrames(false); + } + } +} + void AvatarMixer::nodeKilled(SharedNodePointer killedNode) { if (killedNode->getType() == NodeType::Agent && killedNode->getLinkedData()) { @@ -170,6 +194,23 @@ void AvatarMixer::readPendingDatagrams() { nodeList->broadcastToNodes(identityPacket, NodeSet() << NodeType::Agent); } } + break; + } + case PacketTypeAvatarBillboard: { + + // check if we have a matching node in our list + SharedNodePointer avatarNode = nodeList->sendingNodeForPacket(receivedPacket); + + if (avatarNode && avatarNode->getLinkedData()) { + AvatarMixerClientData* nodeData = static_cast(avatarNode->getLinkedData()); + if (nodeData->hasBillboardChangedAfterParsing(receivedPacket) + && !nodeData->hasSentBillboardBetweenKeyFrames()) { + // this avatar changed their billboard and we haven't sent a packet in this keyframe + broadcastBillboardPacket(avatarNode); + nodeData->setHasSentBillboardBetweenKeyFrames(true); + } + } + break; } case PacketTypeKillAvatar: { nodeList->processKillNode(receivedPacket); @@ -185,6 +226,7 @@ void AvatarMixer::readPendingDatagrams() { } const qint64 AVATAR_IDENTITY_KEYFRAME_MSECS = 5000; +const qint64 AVATAR_BILLBOARD_KEYFRAME_MSECS = 5000; void AvatarMixer::run() { commonInit(AVATAR_MIXER_LOGGING_NAME, NodeType::AvatarMixer); @@ -202,6 +244,9 @@ void AvatarMixer::run() { QElapsedTimer identityTimer; identityTimer.start(); + QElapsedTimer billboardTimer; + billboardTimer.start(); + while (!_isFinished) { QCoreApplication::processEvents(); @@ -219,6 +264,11 @@ void AvatarMixer::run() { // restart the timer so we do it again in AVATAR_IDENTITY_KEYFRAME_MSECS identityTimer.restart(); } + + if (billboardTimer.elapsed() >= AVATAR_BILLBOARD_KEYFRAME_MSECS) { + broadcastBillboardPackets(); + billboardTimer.restart(); + } int usecToSleep = usecTimestamp(&startTime) + (++nextFrame * AVATAR_DATA_SEND_INTERVAL_USECS) - usecTimestampNow(); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 0261613532..388d6f6488 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -9,7 +9,8 @@ #include "AvatarMixerClientData.h" AvatarMixerClientData::AvatarMixerClientData() : - _hasSentIdentityBetweenKeyFrames(false) + _hasSentIdentityBetweenKeyFrames(false), + _hasSentBillboardBetweenKeyFrames(false) { } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 8e046d9212..7240288306 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -21,9 +21,15 @@ public: bool hasSentIdentityBetweenKeyFrames() const { return _hasSentIdentityBetweenKeyFrames; } void setHasSentIdentityBetweenKeyFrames(bool hasSentIdentityBetweenKeyFrames) { _hasSentIdentityBetweenKeyFrames = hasSentIdentityBetweenKeyFrames; } + + bool hasSentBillboardBetweenKeyFrames() const { return _hasSentBillboardBetweenKeyFrames; } + void setHasSentBillboardBetweenKeyFrames(bool hasSentBillboardBetweenKeyFrames) + { _hasSentBillboardBetweenKeyFrames = hasSentBillboardBetweenKeyFrames; } + private: bool _hasSentIdentityBetweenKeyFrames; + bool _hasSentBillboardBetweenKeyFrames; }; #endif /* defined(__hifi__AvatarMixerClientData__) */ diff --git a/examples/alternate-gameoflife.js b/examples/alternate-gameoflife.js new file mode 100644 index 0000000000..8ab992138a --- /dev/null +++ b/examples/alternate-gameoflife.js @@ -0,0 +1,137 @@ +// Add your JavaScript for assignment below this line + +// The following is an example of Conway's Game of Life (http://en.wikipedia.org/wiki/Conway's_Game_of_Life) + +var gameZ = 100; +var NUMBER_OF_CELLS_EACH_DIMENSION = 64; +var NUMBER_OF_CELLS = NUMBER_OF_CELLS_EACH_DIMENSION * NUMBER_OF_CELLS_EACH_DIMENSION; + +var currentCells = []; +var nextCells = []; + +var METER_LENGTH = 1; +var cellScale = (NUMBER_OF_CELLS_EACH_DIMENSION * METER_LENGTH) / NUMBER_OF_CELLS_EACH_DIMENSION; + +// randomly populate the cell start values +for (var i = 0; i < NUMBER_OF_CELLS_EACH_DIMENSION; i++) { + // create the array to hold this row + currentCells[i] = []; + + // create the array to hold this row in the nextCells array + nextCells[i] = []; + + for (var j = 0; j < NUMBER_OF_CELLS_EACH_DIMENSION; j++) { + currentCells[i][j] = Math.floor(Math.random() * 2); + + // put the same value in the nextCells array for first board draw + nextCells[i][j] = currentCells[i][j]; + } +} + +function isNeighbourAlive(i, j) { + if (i < 0 || i >= NUMBER_OF_CELLS_EACH_DIMENSION + || i < 0 || j >= NUMBER_OF_CELLS_EACH_DIMENSION) { + return 0; + } else { + return currentCells[i][j]; + } +} + +function updateCells() { + var i = 0; + var j = 0; + + for (i = 0; i < NUMBER_OF_CELLS_EACH_DIMENSION; i++) { + for (j = 0; j < NUMBER_OF_CELLS_EACH_DIMENSION; j++) { + // figure out the number of live neighbours for the i-j cell + var liveNeighbours = + isNeighbourAlive(i + 1, j - 1) + isNeighbourAlive(i + 1, j) + isNeighbourAlive(i + 1, j + 1) + + isNeighbourAlive(i, j - 1) + isNeighbourAlive(i, j + 1) + + isNeighbourAlive(i - 1, j - 1) + isNeighbourAlive(i - 1, j) + isNeighbourAlive(i - 1, j + 1); + + if (currentCells[i][j]) { + // live cell + + if (liveNeighbours < 2) { + // rule #1 - under-population - this cell will die + // mark it zero to mark the change + nextCells[i][j] = 0; + } else if (liveNeighbours < 4) { + // rule #2 - this cell lives + // mark it -1 to mark no change + nextCells[i][j] = -1; + } else { + // rule #3 - overcrowding - this cell dies + // mark it zero to mark the change + nextCells[i][j] = 0; + } + } else { + // dead cell + if (liveNeighbours == 3) { + // rule #4 - reproduction - this cell revives + // mark it one to mark the change + nextCells[i][j] = 1; + } else { + // this cell stays dead + // mark it -1 for no change + nextCells[i][j] = -1; + } + } + + if (Math.random() < 0.001) { + // Random mutation to keep things interesting in there. + nextCells[i][j] = 1; + } + } + } + + for (i = 0; i < NUMBER_OF_CELLS_EACH_DIMENSION; i++) { + for (j = 0; j < NUMBER_OF_CELLS_EACH_DIMENSION; j++) { + if (nextCells[i][j] != -1) { + // there has been a change to this cell, change the value in the currentCells array + currentCells[i][j] = nextCells[i][j]; + } + } + } +} + +function sendNextCells() { + for (var i = 0; i < NUMBER_OF_CELLS_EACH_DIMENSION; i++) { + for (var j = 0; j < NUMBER_OF_CELLS_EACH_DIMENSION; j++) { + if (nextCells[i][j] != -1) { + // 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; + + // queue a packet to add a voxel for the new cell + var color = (nextCells[i][j] == 1) ? 255 : 1; + if (color == 255) { + Voxels.setVoxel(x, y, gameZ, cellScale, color, color, color); + } else { + Voxels.setVoxel(x, y, gameZ, cellScale, 255, 0, 0); + } + } + } + } +} + +var sentFirstBoard = false; + +function step() { + 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; + } + + sendNextCells(); +} + +print("here"); +//Script.willSendVisualDataCallback.connect(step); +Voxels.setPacketsPerSecond(200); +print("now here"); diff --git a/examples/cubes.svo b/examples/cubes.svo new file mode 100644 index 0000000000..dee8f428d5 Binary files /dev/null and b/examples/cubes.svo differ diff --git a/examples/editVoxels.js b/examples/editVoxels.js index ce444885af..4e24f6021e 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -738,7 +738,7 @@ function endOrbitMode(event) { var cameraOrientation = Camera.getOrientation(); var eulers = Quat.safeEulerAngles(cameraOrientation); MyAvatar.position = Camera.getPosition(); - MyAvatar.orientation = cameraOrientation; + MyAvatar.headOrientation = cameraOrientation; Camera.stopLooking(); Camera.setMode(oldMode); Camera.setOrientation(cameraOrientation); diff --git a/examples/hifi-interface-tools.svg b/examples/hifi-interface-tools.svg new file mode 100644 index 0000000000..311514581f --- /dev/null +++ b/examples/hifi-interface-tools.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/invader.svo b/examples/invader.svo new file mode 100644 index 0000000000..638bfc782f Binary files /dev/null and b/examples/invader.svo differ diff --git a/examples/invader2.svo b/examples/invader2.svo new file mode 100644 index 0000000000..bacabfe662 Binary files /dev/null and b/examples/invader2.svo differ diff --git a/examples/overlaysExample.js b/examples/overlaysExample.js index b57f82e7e9..d97ec9e8fd 100644 --- a/examples/overlaysExample.js +++ b/examples/overlaysExample.js @@ -156,6 +156,13 @@ var line3d = Overlays.addOverlay("line3d", { lineWidth: 5 }); +// this will display the content of your clipboard at the origin of the domain +var clipboardPreview = Overlays.addOverlay("clipboard", { + position: { x: 0, y: 0, z: 0}, + size: 1 / 32, + visible: true + }); + // When our script shuts down, we should clean up all of our overlays function scriptEnding() { @@ -170,6 +177,7 @@ function scriptEnding() { Overlays.deleteOverlay(solidCube); Overlays.deleteOverlay(sphere); Overlays.deleteOverlay(line3d); + Overlays.deleteOverlay(clipboardPreview); } Script.scriptEnding.connect(scriptEnding); diff --git a/examples/slider.jpg b/examples/slider.jpg new file mode 100644 index 0000000000..0d9652d212 Binary files /dev/null and b/examples/slider.jpg differ diff --git a/examples/slider.png b/examples/slider.png new file mode 100644 index 0000000000..b4ba7d90b8 Binary files /dev/null and b/examples/slider.png differ diff --git a/examples/swatches.svg b/examples/swatches.svg new file mode 100644 index 0000000000..ee144170c5 --- /dev/null +++ b/examples/swatches.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/examples/testing-hifi-interface-tools.svg b/examples/testing-hifi-interface-tools.svg new file mode 100644 index 0000000000..b7bdbbcb58 --- /dev/null +++ b/examples/testing-hifi-interface-tools.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/testing-swatches b/examples/testing-swatches new file mode 100644 index 0000000000..ee144170c5 --- /dev/null +++ b/examples/testing-swatches @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/examples/testing-swatches.svg b/examples/testing-swatches.svg new file mode 100644 index 0000000000..ee144170c5 --- /dev/null +++ b/examples/testing-swatches.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/examples/thumb.jpg b/examples/thumb.jpg new file mode 100644 index 0000000000..ab33cec764 Binary files /dev/null and b/examples/thumb.jpg differ diff --git a/examples/thumb.png b/examples/thumb.png new file mode 100644 index 0000000000..fd5f87caca Binary files /dev/null and b/examples/thumb.png differ diff --git a/examples/touchVsMouse.js b/examples/touchVsMouse.js new file mode 100644 index 0000000000..c5007e076b --- /dev/null +++ b/examples/touchVsMouse.js @@ -0,0 +1,36 @@ +// +// toucheVsMouse.js +// hifi +// +// Created by Brad Hefta-Gaub on 1/28/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// This is an example script that demonstrates use of the Controller class +// +// + +function touchBeginEvent(event) { + print("touchBeginEvent event.x,y=" + event.x + ", " + event.y); +} + +function touchEndEvent(event) { + print("touchEndEvent event.x,y=" + event.x + ", " + event.y); +} + +function touchUpdateEvent(event) { + print("touchUpdateEvent event.x,y=" + event.x + ", " + event.y); +} +function mouseMoveEvent(event) { + print("mouseMoveEvent event.x,y=" + event.x + ", " + event.y); +} + +// Map the mouse events to our functions +Controller.touchBeginEvent.connect(touchBeginEvent); +Controller.touchUpdateEvent.connect(touchUpdateEvent); +Controller.touchEndEvent.connect(touchEndEvent); + +Controller.mouseMoveEvent.connect(mouseMoveEvent); + +function scriptEnding() { +} +Script.scriptEnding.connect(scriptEnding); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e5b16a8891..7738be1d3b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -98,6 +98,7 @@ const int MIRROR_VIEW_HEIGHT = 215; const float MIRROR_FULLSCREEN_DISTANCE = 0.35f; const float MIRROR_REARVIEW_DISTANCE = 0.65f; const float MIRROR_REARVIEW_BODY_DISTANCE = 2.3f; +const float MIRROR_FIELD_OF_VIEW = 30.0f; const QString CHECK_VERSION_URL = "http://highfidelity.io/latestVersion.xml"; const QString SKIP_FILENAME = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/hifi.skipversion"; @@ -255,6 +256,11 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : connect(identityPacketTimer, &QTimer::timeout, _myAvatar, &MyAvatar::sendIdentityPacket); identityPacketTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); + // send the billboard packet for our avatar every few seconds + QTimer* billboardPacketTimer = new QTimer(); + connect(billboardPacketTimer, &QTimer::timeout, _myAvatar, &MyAvatar::sendBillboardPacket); + billboardPacketTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS); + QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); _networkAccessManager = new QNetworkAccessManager(this); @@ -527,73 +533,8 @@ void Application::paintGL() { _glowEffect.render(); if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { - - bool eyeRelativeCamera = false; - if (_rearMirrorTools->getZoomLevel() == BODY) { - _mirrorCamera.setDistance(MIRROR_REARVIEW_BODY_DISTANCE * _myAvatar->getScale()); - _mirrorCamera.setTargetPosition(_myAvatar->getChestPosition()); - } else { // HEAD zoom level - _mirrorCamera.setDistance(MIRROR_REARVIEW_DISTANCE * _myAvatar->getScale()); - if (_myAvatar->getSkeletonModel().isActive() && _myAvatar->getHead()->getFaceModel().isActive()) { - // as a hack until we have a better way of dealing with coordinate precision issues, reposition the - // face/body so that the average eye position lies at the origin - eyeRelativeCamera = true; - _mirrorCamera.setTargetPosition(glm::vec3()); - - } else { - _mirrorCamera.setTargetPosition(_myAvatar->getHead()->calculateAverageEyePosition()); - } - } - - _mirrorCamera.setTargetRotation(_myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PIf, 0.0f))); - _mirrorCamera.update(1.0f/_fps); - - // set the bounds of rear mirror view - glViewport(_mirrorViewRect.x(), _glWidget->height() - _mirrorViewRect.y() - _mirrorViewRect.height(), - _mirrorViewRect.width(), _mirrorViewRect.height()); - glScissor(_mirrorViewRect.x(), _glWidget->height() - _mirrorViewRect.y() - _mirrorViewRect.height(), - _mirrorViewRect.width(), _mirrorViewRect.height()); - bool updateViewFrustum = false; - updateProjectionMatrix(_mirrorCamera, updateViewFrustum); - glEnable(GL_SCISSOR_TEST); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - // render rear mirror view - glPushMatrix(); - if (eyeRelativeCamera) { - // save absolute translations - glm::vec3 absoluteSkeletonTranslation = _myAvatar->getSkeletonModel().getTranslation(); - glm::vec3 absoluteFaceTranslation = _myAvatar->getHead()->getFaceModel().getTranslation(); - - // get the eye positions relative to the neck and use them to set the face translation - glm::vec3 leftEyePosition, rightEyePosition; - _myAvatar->getHead()->getFaceModel().setTranslation(glm::vec3()); - _myAvatar->getHead()->getFaceModel().getEyePositions(leftEyePosition, rightEyePosition); - _myAvatar->getHead()->getFaceModel().setTranslation((leftEyePosition + rightEyePosition) * -0.5f); - - // get the neck position relative to the body and use it to set the skeleton translation - glm::vec3 neckPosition; - _myAvatar->getSkeletonModel().setTranslation(glm::vec3()); - _myAvatar->getSkeletonModel().getNeckPosition(neckPosition); - _myAvatar->getSkeletonModel().setTranslation(_myAvatar->getHead()->getFaceModel().getTranslation() - - neckPosition); - - displaySide(_mirrorCamera, true); - - // restore absolute translations - _myAvatar->getSkeletonModel().setTranslation(absoluteSkeletonTranslation); - _myAvatar->getHead()->getFaceModel().setTranslation(absoluteFaceTranslation); - } else { - displaySide(_mirrorCamera, true); - } - glPopMatrix(); - - _rearMirrorTools->render(false); - - // reset Viewport and projection matrix - glViewport(0, 0, _glWidget->width(), _glWidget->height()); - glDisable(GL_SCISSOR_TEST); - updateProjectionMatrix(_myCamera, updateViewFrustum); + renderRearViewMirror(_mirrorViewRect); + } else if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { _rearMirrorTools->render(true); } @@ -2743,6 +2684,24 @@ void Application::setupWorldLight() { glMateriali(GL_FRONT, GL_SHININESS, 96); } +QImage Application::renderAvatarBillboard() { + _textureCache.getPrimaryFramebufferObject()->bind(); + + glDisable(GL_BLEND); + + const int BILLBOARD_SIZE = 64; + renderRearViewMirror(QRect(0, _glWidget->height() - BILLBOARD_SIZE, BILLBOARD_SIZE, BILLBOARD_SIZE), true); + + QImage image(BILLBOARD_SIZE, BILLBOARD_SIZE, QImage::Format_ARGB32); + glReadPixels(0, 0, BILLBOARD_SIZE, BILLBOARD_SIZE, GL_BGRA, GL_UNSIGNED_BYTE, image.bits()); + + glEnable(GL_BLEND); + + _textureCache.getPrimaryFramebufferObject()->release(); + + return image; +} + void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide()"); // transform by eye offset @@ -3660,6 +3619,84 @@ void Application::renderCoverageMapsRecursively(CoverageMap* map) { } } +void Application::renderRearViewMirror(const QRect& region, bool billboard) { + bool eyeRelativeCamera = false; + if (billboard) { + _mirrorCamera.setFieldOfView(BILLBOARD_FIELD_OF_VIEW); + _mirrorCamera.setDistance(BILLBOARD_DISTANCE * _myAvatar->getScale()); + _mirrorCamera.setTargetPosition(_myAvatar->getPosition()); + + } else if (_rearMirrorTools->getZoomLevel() == BODY) { + _mirrorCamera.setFieldOfView(MIRROR_FIELD_OF_VIEW); + _mirrorCamera.setDistance(MIRROR_REARVIEW_BODY_DISTANCE * _myAvatar->getScale()); + _mirrorCamera.setTargetPosition(_myAvatar->getChestPosition()); + + } else { // HEAD zoom level + _mirrorCamera.setFieldOfView(MIRROR_FIELD_OF_VIEW); + _mirrorCamera.setDistance(MIRROR_REARVIEW_DISTANCE * _myAvatar->getScale()); + if (_myAvatar->getSkeletonModel().isActive() && _myAvatar->getHead()->getFaceModel().isActive()) { + // as a hack until we have a better way of dealing with coordinate precision issues, reposition the + // face/body so that the average eye position lies at the origin + eyeRelativeCamera = true; + _mirrorCamera.setTargetPosition(glm::vec3()); + + } else { + _mirrorCamera.setTargetPosition(_myAvatar->getHead()->calculateAverageEyePosition()); + } + } + _mirrorCamera.setAspectRatio((float)region.width() / region.height()); + + _mirrorCamera.setTargetRotation(_myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PIf, 0.0f))); + _mirrorCamera.update(1.0f/_fps); + + // set the bounds of rear mirror view + glViewport(region.x(), _glWidget->height() - region.y() - region.height(), region.width(), region.height()); + glScissor(region.x(), _glWidget->height() - region.y() - region.height(), region.width(), region.height()); + bool updateViewFrustum = false; + updateProjectionMatrix(_mirrorCamera, updateViewFrustum); + glEnable(GL_SCISSOR_TEST); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // render rear mirror view + glPushMatrix(); + if (eyeRelativeCamera) { + // save absolute translations + glm::vec3 absoluteSkeletonTranslation = _myAvatar->getSkeletonModel().getTranslation(); + glm::vec3 absoluteFaceTranslation = _myAvatar->getHead()->getFaceModel().getTranslation(); + + // get the eye positions relative to the neck and use them to set the face translation + glm::vec3 leftEyePosition, rightEyePosition; + _myAvatar->getHead()->getFaceModel().setTranslation(glm::vec3()); + _myAvatar->getHead()->getFaceModel().getEyePositions(leftEyePosition, rightEyePosition); + _myAvatar->getHead()->getFaceModel().setTranslation((leftEyePosition + rightEyePosition) * -0.5f); + + // get the neck position relative to the body and use it to set the skeleton translation + glm::vec3 neckPosition; + _myAvatar->getSkeletonModel().setTranslation(glm::vec3()); + _myAvatar->getSkeletonModel().getNeckPosition(neckPosition); + _myAvatar->getSkeletonModel().setTranslation(_myAvatar->getHead()->getFaceModel().getTranslation() - + neckPosition); + + displaySide(_mirrorCamera, true); + + // restore absolute translations + _myAvatar->getSkeletonModel().setTranslation(absoluteSkeletonTranslation); + _myAvatar->getHead()->getFaceModel().setTranslation(absoluteFaceTranslation); + } else { + displaySide(_mirrorCamera, true); + } + glPopMatrix(); + + if (!billboard) { + _rearMirrorTools->render(false); + } + + // reset Viewport and projection matrix + glViewport(0, 0, _glWidget->width(), _glWidget->height()); + glDisable(GL_SCISSOR_TEST); + updateProjectionMatrix(_myCamera, updateViewFrustum); +} + // renderViewFrustum() // // Description: this will render the view frustum bounds for EITHER the head diff --git a/interface/src/Application.h b/interface/src/Application.h index abe51b8bb1..cb1ba2f949 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -96,6 +97,9 @@ static const float NODE_KILLED_BLUE = 0.0f; static const QString SNAPSHOT_EXTENSION = ".jpg"; +static const float BILLBOARD_FIELD_OF_VIEW = 30.0f; +static const float BILLBOARD_DISTANCE = 5.0f; + class Application : public QApplication { Q_OBJECT @@ -185,6 +189,8 @@ public: void setupWorldLight(); + QImage renderAvatarBillboard(); + void displaySide(Camera& whichCamera, bool selfAvatarOnly = false); /// Loads a view matrix that incorporates the specified model translation without the precision issues that can @@ -200,6 +206,8 @@ public: void computeOffAxisFrustum(float& left, float& right, float& bottom, float& top, float& nearVal, float& farVal, glm::vec4& nearClipPlane, glm::vec4& farClipPlane) const; + + VoxelShader& getVoxelShader() { return _voxelShader; } PointShader& getPointShader() { return _pointShader; } FileLogger* getLogger() { return _logger; } @@ -328,7 +336,7 @@ private: void displayStats(); void checkStatsClick(); void toggleStatsExpanded(); - void renderAvatars(bool forceRenderHead, bool selfAvatarOnly = false); + void renderRearViewMirror(const QRect& region, bool billboard = false); void renderViewFrustum(ViewFrustum& viewFrustum); void checkBandwidthMeterClick(); diff --git a/interface/src/DatagramProcessor.cpp b/interface/src/DatagramProcessor.cpp index d8447168cd..e5fd37af4f 100644 --- a/interface/src/DatagramProcessor.cpp +++ b/interface/src/DatagramProcessor.cpp @@ -98,7 +98,8 @@ void DatagramProcessor::processDatagrams() { break; case PacketTypeBulkAvatarData: case PacketTypeKillAvatar: - case PacketTypeAvatarIdentity: { + case PacketTypeAvatarIdentity: + case PacketTypeAvatarBillboard: { // update having heard from the avatar-mixer and record the bytes received SharedNodePointer avatarMixer = nodeList->sendingNodeForPacket(incomingPacket); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index dcecd0258d..f0f950adc1 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -26,6 +26,7 @@ #include "Physics.h" #include "world.h" #include "devices/OculusManager.h" +#include "renderer/TextureCache.h" #include "ui/TextRenderer.h" using namespace std; @@ -107,6 +108,10 @@ glm::quat Avatar::getWorldAlignedOrientation () const { return computeRotationFromBodyToWorldUp() * getOrientation(); } +float Avatar::getLODDistance() const { + return glm::distance(Application::getInstance()->getCamera()->getPosition(), _position) / _scale; +} + void Avatar::simulate(float deltaTime) { if (_scale != _targetScale) { setScale(_targetScale); @@ -116,9 +121,9 @@ void Avatar::simulate(float deltaTime) { glm::vec3 oldVelocity = getVelocity(); getHand()->simulate(deltaTime, false); + _skeletonModel.setLODDistance(getLODDistance()); _skeletonModel.simulate(deltaTime); Head* head = getHead(); - head->setBodyRotation(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll)); glm::vec3 headPosition; if (!_skeletonModel.getHeadPosition(headPosition)) { headPosition = _position; @@ -282,9 +287,11 @@ glm::quat Avatar::computeRotationFromBodyToWorldUp(float proportion) const { } void Avatar::renderBody(bool forceRenderHead) { - // Render the body's voxels and head - glm::vec3 pos = getPosition(); - //printf("Render other at %.3f, %.2f, %.2f\n", pos.x, pos.y, pos.z); + const float BILLBOARD_DISTANCE = 40.0f; + if (!_billboard.isEmpty() && getLODDistance() >= BILLBOARD_DISTANCE) { + renderBillboard(); + return; + } _skeletonModel.render(1.0f); if (forceRenderHead) { getHead()->render(1.0f); @@ -292,6 +299,62 @@ void Avatar::renderBody(bool forceRenderHead) { getHand()->render(false); } +void Avatar::renderBillboard() { + if (!_billboardTexture) { + QImage image = QImage::fromData(_billboard).convertToFormat(QImage::Format_ARGB32); + + _billboardTexture.reset(new Texture()); + glBindTexture(GL_TEXTURE_2D, _billboardTexture->getID()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 1, + GL_BGRA, GL_UNSIGNED_BYTE, image.constBits()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + } else { + glBindTexture(GL_TEXTURE_2D, _billboardTexture->getID()); + } + + glEnable(GL_ALPHA_TEST); + glAlphaFunc(GL_GREATER, 0.5f); + + glEnable(GL_TEXTURE_2D); + glDisable(GL_LIGHTING); + + glPushMatrix(); + glTranslatef(_position.x, _position.y, _position.z); + + // rotate about vertical to face the camera + glm::quat rotation = getOrientation(); + glm::vec3 cameraVector = glm::inverse(rotation) * (Application::getInstance()->getCamera()->getPosition() - _position); + rotation = rotation * glm::angleAxis(glm::degrees(atan2f(-cameraVector.x, -cameraVector.z)), 0.0f, 1.0f, 0.0f); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::angle(rotation), axis.x, axis.y, axis.z); + + // compute the size from the billboard camera parameters and scale + float size = _scale * BILLBOARD_DISTANCE * tanf(glm::radians(BILLBOARD_FIELD_OF_VIEW / 2.0f)); + glScalef(size, size, size); + + glColor3f(1.0f, 1.0f, 1.0f); + + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); + glVertex2f(-1.0f, -1.0f); + glTexCoord2f(1.0f, 0.0f); + glVertex2f(1.0f, -1.0f); + glTexCoord2f(1.0f, 1.0f); + glVertex2f(1.0f, 1.0f); + glTexCoord2f(0.0f, 1.0f); + glVertex2f(-1.0f, 1.0f); + glEnd(); + + glPopMatrix(); + + glDisable(GL_TEXTURE_2D); + glEnable(GL_LIGHTING); + glDisable(GL_ALPHA_TEST); + + glBindTexture(GL_TEXTURE_2D, 0); +} + void Avatar::renderDisplayName() { if (_displayName.isEmpty() || _displayNameAlpha == 0.0f) { @@ -502,6 +565,13 @@ void Avatar::setDisplayName(const QString& displayName) { _displayNameBoundingRect = textRenderer(DISPLAYNAME)->metrics().tightBoundingRect(displayName); } +void Avatar::setBillboard(const QByteArray& billboard) { + AvatarData::setBillboard(billboard); + + // clear out any existing billboard texture + _billboardTexture.reset(); +} + int Avatar::parseData(const QByteArray& packet) { // change in position implies movement glm::vec3 oldPosition = _position; diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 6fa0f203e9..b9433f15dc 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -62,6 +63,8 @@ enum ScreenTintLayer { // Grayson as he's building a street around here for demo dinner 2 const glm::vec3 START_LOCATION(0.485f * TREE_SCALE, 0.f, 0.5f * TREE_SCALE); +class Texture; + class Avatar : public AvatarData { Q_OBJECT @@ -87,6 +90,9 @@ public: Head* getHead() { return static_cast(_headData); } Hand* getHand() { return static_cast(_handData); } glm::quat getWorldAlignedOrientation() const; + + /// Returns the distance to use as a LOD parameter. + float getLODDistance() const; Node* getOwningAvatarMixer() { return _owningAvatarMixer.data(); } void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } @@ -114,6 +120,7 @@ public: virtual void setFaceModelURL(const QUrl& faceModelURL); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setDisplayName(const QString& displayName); + virtual void setBillboard(const QByteArray& billboard); void setShowDisplayName(bool showDisplayName); @@ -167,8 +174,10 @@ protected: private: bool _initialized; + QScopedPointer _billboardTexture; void renderBody(bool forceRenderHead); + void renderBillboard(); void renderDisplayName(); }; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index b873b10017..f65566fe14 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -133,6 +133,9 @@ void AvatarManager::processAvatarMixerDatagram(const QByteArray& datagram, const case PacketTypeAvatarIdentity: processAvatarIdentityPacket(datagram); break; + case PacketTypeAvatarBillboard: + processAvatarBillboardPacket(datagram); + break; case PacketTypeKillAvatar: processKillAvatar(datagram); break; @@ -212,6 +215,20 @@ void AvatarManager::processAvatarIdentityPacket(const QByteArray &packet) { } } +void AvatarManager::processAvatarBillboardPacket(const QByteArray& packet) { + int headerSize = numBytesForPacketHeader(packet); + QUuid nodeUUID = QUuid::fromRfc4122(QByteArray::fromRawData(packet.constData() + headerSize, NUM_BYTES_RFC4122_UUID)); + + AvatarSharedPointer matchingAvatar = _avatarHash.value(nodeUUID); + if (matchingAvatar) { + Avatar* avatar = static_cast(matchingAvatar.data()); + QByteArray billboard = packet.mid(headerSize + NUM_BYTES_RFC4122_UUID); + if (avatar->getBillboard() != billboard) { + avatar->setBillboard(billboard); + } + } +} + void AvatarManager::processKillAvatar(const QByteArray& datagram) { // read the node id QUuid nodeUUID = QUuid::fromRfc4122(datagram.mid(numBytesForPacketHeader(datagram), NUM_BYTES_RFC4122_UUID)); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 35dbf8dbb0..115423e618 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -42,6 +42,7 @@ private: void processAvatarDataPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); void processAvatarIdentityPacket(const QByteArray& packet); + void processAvatarBillboardPacket(const QByteArray& packet); void processKillAvatar(const QByteArray& datagram); void simulateAvatarFades(float deltaTime); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 5c6100764a..de3f3cd3d8 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -28,7 +28,6 @@ Head::Head(Avatar* owningAvatar) : _gravity(0.0f, -1.0f, 0.0f), _lastLoudness(0.0f), _audioAttack(0.0f), - _bodyRotation(0.0f, 0.0f, 0.0f), _angularVelocity(0,0,0), _renderLookatVectors(false), _saccade(0.0f, 0.0f, 0.0f), @@ -158,6 +157,9 @@ void Head::simulate(float deltaTime, bool isMine) { glm::clamp(sqrt(_averageLoudness * JAW_OPEN_SCALE) - JAW_OPEN_DEAD_ZONE, 0.0f, 1.0f), _blendshapeCoefficients); } + if (!isMine) { + _faceModel.setLODDistance(static_cast(_owningAvatar)->getLODDistance()); + } _faceModel.simulate(deltaTime); // the blend face may have custom eye meshes @@ -180,12 +182,8 @@ void Head::setScale (float scale) { _scale = scale; } -glm::quat Head::getOrientation() const { - return glm::quat(glm::radians(_bodyRotation)) * glm::quat(glm::radians(glm::vec3(_pitch, _yaw, _roll))); -} - glm::quat Head::getTweakedOrientation() const { - return glm::quat(glm::radians(_bodyRotation)) * glm::quat(glm::radians(glm::vec3(getTweakedPitch(), getTweakedYaw(), getTweakedRoll() ))); + return _owningAvatar->getOrientation() * glm::quat(glm::radians(glm::vec3(getTweakedPitch(), getTweakedYaw(), getTweakedRoll() ))); } glm::quat Head::getCameraOrientation () const { diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index c88e654d95..39a2f4eeb6 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -40,13 +40,11 @@ public: void render(float alpha); void setScale(float scale); void setPosition(glm::vec3 position) { _position = position; } - void setBodyRotation(glm::vec3 bodyRotation) { _bodyRotation = bodyRotation; } void setGravity(glm::vec3 gravity) { _gravity = gravity; } void setAverageLoudness(float averageLoudness) { _averageLoudness = averageLoudness; } void setReturnToCenter (bool returnHeadToCenter) { _returnHeadToCenter = returnHeadToCenter; } void setRenderLookatVectors(bool onOff) { _renderLookatVectors = onOff; } - glm::quat getOrientation() const; glm::quat getTweakedOrientation() const; glm::quat getCameraOrientation () const; const glm::vec3& getAngularVelocity() const { return _angularVelocity; } @@ -97,7 +95,6 @@ private: glm::vec3 _gravity; float _lastLoudness; float _audioAttack; - glm::vec3 _bodyRotation; glm::vec3 _angularVelocity; bool _renderLookatVectors; glm::vec3 _saccade; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 90ef18848b..3bb156edcf 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -9,6 +9,8 @@ #include #include +#include + #include #include @@ -57,7 +59,8 @@ MyAvatar::MyAvatar() : _thrustMultiplier(1.0f), _moveTarget(0,0,0), _moveTargetStepCounter(0), - _lookAtTargetAvatar() + _lookAtTargetAvatar(), + _billboardValid(false) { for (int i = 0; i < MAX_DRIVE_KEYS; i++) { _driveKeys[i] = 0.0f; @@ -321,7 +324,6 @@ void MyAvatar::simulate(float deltaTime) { _skeletonModel.simulate(deltaTime); Head* head = getHead(); - head->setBodyRotation(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll)); glm::vec3 headPosition; if (!_skeletonModel.getHeadPosition(headPosition)) { headPosition = _position; @@ -332,7 +334,9 @@ void MyAvatar::simulate(float deltaTime) { // Zero thrust out now that we've added it to velocity in this frame _thrust = glm::vec3(0, 0, 0); - + + // consider updating our billboard + maybeUpdateBillboard(); } const float MAX_PITCH = 90.0f; @@ -705,6 +709,16 @@ glm::vec3 MyAvatar::getUprightHeadPosition() const { return _position + getWorldAlignedOrientation() * glm::vec3(0.0f, getPelvisToHeadLength(), 0.0f); } +void MyAvatar::setFaceModelURL(const QUrl& faceModelURL) { + Avatar::setFaceModelURL(faceModelURL); + _billboardValid = false; +} + +void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { + Avatar::setSkeletonModelURL(skeletonModelURL); + _billboardValid = false; +} + void MyAvatar::renderBody(bool forceRenderHead) { // Render the body's voxels and head _skeletonModel.render(1.0f); @@ -1124,6 +1138,20 @@ void MyAvatar::updateChatCircle(float deltaTime) { _position = glm::mix(_position, targetPosition, APPROACH_RATE); } +void MyAvatar::maybeUpdateBillboard() { + if (_billboardValid || !(_skeletonModel.isLoadedWithTextures() && getHead()->getFaceModel().isLoadedWithTextures())) { + return; + } + QImage image = Application::getInstance()->renderAvatarBillboard(); + _billboard.clear(); + QBuffer buffer(&_billboard); + buffer.open(QIODevice::WriteOnly); + image.save(&buffer, "PNG"); + _billboardValid = true; + + sendBillboardPacket(); +} + void MyAvatar::setGravity(glm::vec3 gravity) { _gravity = gravity; getHead()->setGravity(_gravity); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 538ca4e0b2..2b5be47419 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -84,6 +84,9 @@ public: void updateLookAtTargetAvatar(glm::vec3& eyePosition); void clearLookAtTargetAvatar(); + virtual void setFaceModelURL(const QUrl& faceModelURL); + virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); + public slots: void goHome(); void increaseSize(); @@ -118,6 +121,8 @@ private: glm::vec3 _transmitterPickStart; glm::vec3 _transmitterPickEnd; + bool _billboardValid; + // private methods void renderBody(bool forceRenderHead); void updateThrust(float deltaTime); @@ -128,6 +133,7 @@ private: void applyHardCollision(const glm::vec3& penetration, float elasticity, float damping); void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void updateChatCircle(float deltaTime); + void maybeUpdateBillboard(); }; #endif diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index f1916db4d1..c3aae7146a 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -17,8 +17,8 @@ using namespace std; Model::Model(QObject* parent) : QObject(parent), - _pupilDilation(0.0f) -{ + _lodDistance(0.0f), + _pupilDilation(0.0f) { // we may have been created in the network thread, but we live in the main thread moveToThread(Application::getInstance()->thread()); } @@ -46,6 +46,21 @@ void Model::initSkinProgram(ProgramObject& program, Model::SkinLocations& locati program.release(); } +bool Model::isLoadedWithTextures() const { + if (!isActive()) { + return false; + } + foreach (const NetworkMesh& mesh, _geometry->getMeshes()) { + foreach (const NetworkMeshPart& part, mesh.parts) { + if (part.diffuseTexture && !part.diffuseTexture->isLoaded() || + part.normalTexture && !part.normalTexture->isLoaded()) { + return false; + } + } + } + return true; +} + void Model::init() { if (!_program.isLinked()) { switchToResourcesParentIfRequired(); @@ -92,8 +107,7 @@ void Model::reset() { void Model::simulate(float deltaTime) { // update our LOD if (_geometry) { - QSharedPointer geometry = _geometry->getLODOrFallback(glm::distance(_translation, - Application::getInstance()->getCamera()->getPosition()), _lodHysteresis); + QSharedPointer geometry = _geometry->getLODOrFallback(_lodDistance, _lodHysteresis); if (_geometry != geometry) { deleteGeometry(); _dilatedTextures.clear(); diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 28189d0379..cf6e1fea6d 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -46,6 +46,8 @@ public: bool isActive() const { return _geometry && _geometry->isLoaded(); } + bool isLoadedWithTextures() const; + void init(); void reset(); void simulate(float deltaTime); @@ -54,6 +56,9 @@ public: Q_INVOKABLE void setURL(const QUrl& url, const QUrl& fallback = QUrl()); const QUrl& getURL() const { return _url; } + /// Sets the distance parameter used for LOD computations. + void setLODDistance(float distance) { _lodDistance = distance; } + /// Returns the extents of the model in its bind pose. Extents getBindExtents() const; @@ -228,13 +233,14 @@ private: void renderMeshes(float alpha, bool translucent); QSharedPointer _baseGeometry; ///< reference required to prevent collection of base + float _lodDistance; float _lodHysteresis; float _pupilDilation; std::vector _blendshapeCoefficients; QUrl _url; - + QVector _blendedVertexBufferIDs; QVector > > _dilatedTextures; bool _resetStates; diff --git a/interface/src/renderer/TextureCache.cpp b/interface/src/renderer/TextureCache.cpp index dc6883a5d0..8bfef5a742 100644 --- a/interface/src/renderer/TextureCache.cpp +++ b/interface/src/renderer/TextureCache.cpp @@ -258,9 +258,11 @@ NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) : _reply(NULL), _attempts(0), _averageColor(1.0f, 1.0f, 1.0f, 1.0f), - _translucent(false) { + _translucent(false), + _loaded(false) { if (!url.isValid()) { + _loaded = true; return; } _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); @@ -298,6 +300,7 @@ void NetworkTexture::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTo _reply->disconnect(this); _reply->deleteLater(); _reply = NULL; + _loaded = true; QImage image = QImage::fromData(entirety).convertToFormat(QImage::Format_ARGB32); @@ -345,6 +348,8 @@ void NetworkTexture::handleReplyError() { QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeRequest())); debug << " -- retrying..."; + } else { + _loaded = true; } } diff --git a/interface/src/renderer/TextureCache.h b/interface/src/renderer/TextureCache.h index e560acf6f7..ca7bf67a32 100644 --- a/interface/src/renderer/TextureCache.h +++ b/interface/src/renderer/TextureCache.h @@ -118,6 +118,8 @@ public: NetworkTexture(const QUrl& url, bool normalMap); ~NetworkTexture(); + bool isLoaded() const { return _loaded; } + /// Returns the average color over the entire texture. const glm::vec4& getAverageColor() const { return _averageColor; } @@ -142,6 +144,7 @@ private: int _attempts; glm::vec4 _averageColor; bool _translucent; + bool _loaded; }; /// Caches derived, dilated textures. diff --git a/interface/src/ui/ClipboardOverlay.cpp b/interface/src/ui/ClipboardOverlay.cpp new file mode 100644 index 0000000000..7eee318ec9 --- /dev/null +++ b/interface/src/ui/ClipboardOverlay.cpp @@ -0,0 +1,50 @@ +// +// ClipboardOverlay.cpp +// hifi +// +// Created by Clément Brisset on 2/20/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "ClipboardOverlay.h" +#include "../Application.h" + +static int lastVoxelCount = 0; + +ClipboardOverlay::ClipboardOverlay() { +} + +ClipboardOverlay::~ClipboardOverlay() { +} + +void ClipboardOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + + VoxelSystem* voxelSystem = Application::getInstance()->getSharedVoxelSystem(); + VoxelTree* clipboard = Application::getInstance()->getClipboard(); + if (voxelSystem->getTree() != clipboard) { + voxelSystem->changeTree(clipboard); + } + + glPushMatrix(); + glTranslatef(_position.x, _position.y, _position.z); + glScalef(_size, _size, _size); + + // We only force the redraw when the clipboard content has changed + if (lastVoxelCount != clipboard->getOctreeElementsCount()) { + voxelSystem->forceRedrawEntireTree(); + lastVoxelCount = clipboard->getOctreeElementsCount(); + } + + voxelSystem->render(); + + glPopMatrix(); +} \ No newline at end of file diff --git a/interface/src/ui/ClipboardOverlay.h b/interface/src/ui/ClipboardOverlay.h new file mode 100644 index 0000000000..49daa73c20 --- /dev/null +++ b/interface/src/ui/ClipboardOverlay.h @@ -0,0 +1,25 @@ +// +// ClipboardOverlay.h +// hifi +// +// Created by Clément Brisset on 2/20/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__ClipboardOverlay__ +#define __interface__ClipboardOverlay__ + +#include "Volume3DOverlay.h" + +class ClipboardOverlay : public Volume3DOverlay { + Q_OBJECT + +public: + ClipboardOverlay(); + ~ClipboardOverlay(); + + virtual void render(); +}; + + +#endif /* defined(__interface__ClipboardOverlay__) */ diff --git a/interface/src/ui/Overlays.cpp b/interface/src/ui/Overlays.cpp index c35c4fc5ec..84944332f1 100644 --- a/interface/src/ui/Overlays.cpp +++ b/interface/src/ui/Overlays.cpp @@ -12,7 +12,7 @@ #include "Overlays.h" #include "Sphere3DOverlay.h" #include "TextOverlay.h" - +#include "ClipboardOverlay.h" unsigned int Overlays::_nextOverlayID = 1; @@ -73,6 +73,12 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope thisOverlay->setProperties(properties); created = true; is3D = true; + } else if (type == "clipboard") { + thisOverlay = new ClipboardOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; } if (created) { diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 3c50f2622c..2cb719c446 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -305,6 +305,15 @@ QByteArray AvatarData::identityByteArray() { return identityData; } +bool AvatarData::hasBillboardChangedAfterParsing(const QByteArray& packet) { + QByteArray newBillboard = packet.mid(numBytesForPacketHeader(packet)); + if (newBillboard == _billboard) { + return false; + } + _billboard = newBillboard; + return true; +} + void AvatarData::setFaceModelURL(const QUrl& faceModelURL) { _faceModelURL = faceModelURL.isEmpty() ? DEFAULT_HEAD_MODEL_URL : faceModelURL; @@ -323,6 +332,12 @@ void AvatarData::setDisplayName(const QString& displayName) { qDebug() << "Changing display name for avatar to" << displayName; } +void AvatarData::setBillboard(const QByteArray& billboard) { + _billboard = billboard; + + qDebug() << "Changing billboard for avatar."; +} + void AvatarData::setClampedTargetScale(float targetScale) { targetScale = glm::clamp(targetScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); @@ -344,3 +359,10 @@ void AvatarData::sendIdentityPacket() { NodeList::getInstance()->broadcastToNodes(identityPacket, NodeSet() << NodeType::AvatarMixer); } + +void AvatarData::sendBillboardPacket() { + QByteArray billboardPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard); + billboardPacket.append(_billboard); + + NodeList::getInstance()->broadcastToNodes(billboardPacket, NodeSet() << NodeType::AvatarMixer); +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 07c774d5e6..c5cd72c184 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -29,6 +29,7 @@ typedef unsigned long long quint64; #include #include +#include #include #include #include @@ -54,6 +55,7 @@ static const float MIN_AVATAR_SCALE = .005f; const float MAX_AUDIO_LOUDNESS = 1000.0; // close enough for mouth animation const int AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS = 1000; +const int AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS = 5000; const QUrl DEFAULT_HEAD_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_head.fst"); const QUrl DEFAULT_BODY_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_body.fst"); @@ -78,6 +80,7 @@ class AvatarData : public NodeData { Q_PROPERTY(QString chatMessage READ getQStringChatMessage WRITE setChatMessage) Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) + Q_PROPERTY(glm::quat headOrientation READ getHeadOrientation WRITE setHeadOrientation) Q_PROPERTY(float headPitch READ getHeadPitch WRITE setHeadPitch) Q_PROPERTY(float audioLoudness READ getAudioLoudness WRITE setAudioLoudness) @@ -109,6 +112,9 @@ public: glm::quat getOrientation() const { return glm::quat(glm::radians(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll))); } void setOrientation(const glm::quat& orientation); + glm::quat getHeadOrientation() const { return _headData->getOrientation(); } + void setHeadOrientation(const glm::quat& orientation) { _headData->setOrientation(orientation); } + // access to Head().set/getMousePitch float getHeadPitch() const { return _headData->getPitch(); } void setHeadPitch(float value) { _headData->setPitch(value); }; @@ -151,6 +157,8 @@ public: bool hasIdentityChangedAfterParsing(const QByteArray& packet); QByteArray identityByteArray(); + bool hasBillboardChangedAfterParsing(const QByteArray& packet); + const QUrl& getFaceModelURL() const { return _faceModelURL; } QString getFaceModelURLString() const { return _faceModelURL.toString(); } const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; } @@ -159,6 +167,9 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setDisplayName(const QString& displayName); + virtual void setBillboard(const QByteArray& billboard); + const QByteArray& getBillboard() const { return _billboard; } + QString getFaceModelURLFromScript() const { return _faceModelURL.toString(); } void setFaceModelURLFromScript(const QString& faceModelString) { setFaceModelURL(faceModelString); } @@ -169,6 +180,7 @@ public: public slots: void sendIdentityPacket(); + void sendBillboardPacket(); protected: glm::vec3 _position; @@ -204,6 +216,8 @@ protected: float _displayNameTargetAlpha; float _displayNameAlpha; + QByteArray _billboard; + private: // privatize the copy constructor and assignment operator so they cannot be called AvatarData(const AvatarData&); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 62e8276bd3..68a5c2c826 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -6,6 +6,11 @@ // Copyright (c) 2013 High Fidelity, Inc. All rights reserved. // +#include + +#include + +#include "AvatarData.h" #include "HeadData.h" HeadData::HeadData(AvatarData* owningAvatar) : @@ -26,6 +31,24 @@ HeadData::HeadData(AvatarData* owningAvatar) : } +glm::quat HeadData::getOrientation() const { + return _owningAvatar->getOrientation() * glm::quat(glm::radians(glm::vec3(_pitch, _yaw, _roll))); +} + +void HeadData::setOrientation(const glm::quat& orientation) { + // rotate body about vertical axis + glm::quat bodyOrientation = _owningAvatar->getOrientation(); + glm::vec3 newFront = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FRONT); + bodyOrientation = bodyOrientation * glm::angleAxis(glm::degrees(atan2f(-newFront.x, -newFront.z)), 0.0f, 1.0f, 0.0f); + _owningAvatar->setOrientation(bodyOrientation); + + // the rest goes to the head + glm::vec3 eulers = safeEulerAngles(glm::inverse(bodyOrientation) * orientation); + _pitch = eulers.x; + _yaw = eulers.y; + _roll = eulers.z; +} + void HeadData::addYaw(float yaw) { setYaw(_yaw + yaw); } diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 04d5fe5b46..618de89b31 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -13,6 +13,7 @@ #include #include +#include const float MIN_HEAD_YAW = -110; const float MAX_HEAD_YAW = 110; @@ -42,6 +43,9 @@ public: float getRoll() const { return _roll; } void setRoll(float roll) { _roll = glm::clamp(roll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); } + glm::quat getOrientation() const; + void setOrientation(const glm::quat& orientation); + float getAudioLoudness() const { return _audioLoudness; } void setAudioLoudness(float audioLoudness) { _audioLoudness = audioLoudness; } diff --git a/libraries/shared/src/PacketHeaders.h b/libraries/shared/src/PacketHeaders.h index c7496b7f4a..f6d745cb44 100644 --- a/libraries/shared/src/PacketHeaders.h +++ b/libraries/shared/src/PacketHeaders.h @@ -53,7 +53,8 @@ enum PacketType { PacketTypeParticleErase, PacketTypeParticleAddResponse, PacketTypeMetavoxelData, - PacketTypeAvatarIdentity + PacketTypeAvatarIdentity, + PacketTypeAvatarBillboard }; typedef char PacketVersion;