// // Application.cpp // interface // // Created by Andrzej Kapolka on 5/10/13. // Copyright (c) 2013 High Fidelity, Inc. All rights reserved. #ifdef WIN32 #include #endif #include #include #include #include #include #include #include // include this before QGLWidget, which includes an earlier version of OpenGL #include "InterfaceConfig.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #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 "DataServerClient.h" #include "InterfaceVersion.h" #include "Menu.h" #include "Swatch.h" #include "Util.h" #include "devices/OculusManager.h" #include "devices/TV3DManager.h" #include "renderer/ProgramObject.h" #include "ui/TextRenderer.h" #include "InfoView.h" using namespace std; // Starfield information static unsigned STARFIELD_NUM_STARS = 50000; static unsigned STARFIELD_SEED = 1; static const int BANDWIDTH_METER_CLICK_MAX_DRAG_LENGTH = 6; // farther dragged clicks are ignored const int IDLE_SIMULATE_MSECS = 16; // How often should call simulate and other stuff // in the idle loop? (60 FPS is default) static QTimer* idleTimer = NULL; const int STARTUP_JITTER_SAMPLES = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL / 2; // Startup optimistically with small jitter buffer that // will start playback on the second received audio packet. const int MIRROR_VIEW_TOP_PADDING = 5; const int MIRROR_VIEW_LEFT_PADDING = 10; const int MIRROR_VIEW_WIDTH = 265; 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 QString CHECK_VERSION_URL = "http://highfidelity.io/latestVersion.xml"; const QString SKIP_FILENAME = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/hifi.skipversion"; const int STATS_PELS_PER_LINE = 20; void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString &message) { QString messageWithNewLine = message + "\n"; fprintf(stdout, "%s", messageWithNewLine.toLocal8Bit().constData()); Application::getInstance()->getLogger()->addMessage(messageWithNewLine.toLocal8Bit().constData()); } Application::Application(int& argc, char** argv, timeval &startup_time) : QApplication(argc, argv), _window(new QMainWindow(desktop())), _glWidget(new GLCanvas()), _nodeThread(new QThread(this)), _datagramProcessor(), _frameCount(0), _fps(120.0f), _justStarted(true), _voxelImporter(_window), _wantToKillLocalVoxels(false), _audioScope(256, 200, true), _avatarManager(), _profile(QString()), _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _mouseX(0), _mouseY(0), _lastMouseMove(usecTimestampNow()), _mouseHidden(false), _seenMouseMove(false), _touchAvgX(0.0f), _touchAvgY(0.0f), _isTouchPressed(false), _yawFromTouch(0.0f), _pitchFromTouch(0.0f), _mousePressed(false), _isHoverVoxel(false), _isHoverVoxelSounding(false), _mouseVoxelScale(1.0f / 1024.0f), _mouseVoxelScaleInitialized(false), _justEditedVoxel(false), _isHighlightVoxel(false), _nudgeStarted(false), _lookingAlongX(false), _lookingAwayFromOrigin(true), _chatEntryOn(false), _audio(&_audioScope, STARTUP_JITTER_SAMPLES), _enableProcessVoxelsThread(true), _voxelProcessor(), _voxelHideShowThread(&_voxels), _packetsPerSecond(0), _bytesPerSecond(0), _recentMaxPackets(0), _resetRecentMaxPacketsSoon(true), _swatch(NULL), _pasteMode(false), _logger(new FileLogger()), _persistThread(NULL), _statsExpanded(false) { _applicationStartupTime = startup_time; switchToResourcesParentIfRequired(); QFontDatabase::addApplicationFont("resources/styles/Inconsolata.otf"); _window->setWindowTitle("Interface"); qInstallMessageHandler(messageHandler); // call Menu getInstance static method to set up the menu _window->setMenuBar(Menu::getInstance()); unsigned int listenPort = 0; // bind to an ephemeral port by default const char** constArgv = const_cast(argv); const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); if (portStr) { listenPort = atoi(portStr); } // start the nodeThread so its event loop is running _nodeThread->start(); // make sure the node thread is given highest priority _nodeThread->setPriority(QThread::TimeCriticalPriority); // put the NodeList and datagram processing on the node thread NodeList* nodeList = NodeList::createInstance(NODE_TYPE_AGENT, listenPort); nodeList->moveToThread(_nodeThread); _datagramProcessor.moveToThread(_nodeThread); // connect the DataProcessor processDatagrams slot to the QUDPSocket readyRead() signal connect(&nodeList->getNodeSocket(), SIGNAL(readyRead()), &_datagramProcessor, SLOT(processDatagrams())); // put the audio processing on a separate thread QThread* audioThread = new QThread(this); _audio.moveToThread(audioThread); connect(audioThread, SIGNAL(started()), &_audio, SLOT(start())); audioThread->start(); connect(nodeList, SIGNAL(domainChanged(const QString&)), SLOT(domainChanged(const QString&))); 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))); // read the ApplicationInfo.ini file for Name/Version/Domain information QSettings applicationInfo("resources/info/ApplicationInfo.ini", QSettings::IniFormat); // set the associated application properties applicationInfo.beginGroup("INFO"); setApplicationName(applicationInfo.value("name").toString()); setApplicationVersion(BUILD_VERSION); setOrganizationName(applicationInfo.value("organizationName").toString()); setOrganizationDomain(applicationInfo.value("organizationDomain").toString()); qDebug() << "[VERSION] Build sequence: " << qPrintable(applicationVersion()); _settings = new QSettings(this); // Check to see if the user passed in a command line option for loading a local // Voxel File. _voxelsFilename = getCmdOption(argc, constArgv, "-i"); #ifdef _WIN32 WSADATA WsaData; int wsaresult = WSAStartup(MAKEWORD(2,2), &WsaData); #endif // tell the NodeList instance who to tell the domain server we care about nodeList->addSetOfNodeTypesToNodeInterestSet(QSet() << NODE_TYPE_AUDIO_MIXER << NODE_TYPE_AVATAR_MIXER << NODE_TYPE_VOXEL_SERVER << NODE_TYPE_PARTICLE_SERVER << NODE_TYPE_METAVOXEL_SERVER); // connect to the packet sent signal of the _voxelEditSender and the _particleEditSender connect(&_voxelEditSender, &VoxelEditPacketSender::packetSent, this, &Application::packetSent); connect(&_particleEditSender, &ParticleEditPacketSender::packetSent, this, &Application::packetSent); // move the silentNodeTimer to the _nodeThread QTimer* silentNodeTimer = new QTimer(); connect(silentNodeTimer, SIGNAL(timeout()), nodeList, SLOT(removeSilentNodes())); silentNodeTimer->moveToThread(_nodeThread); silentNodeTimer->start(NODE_SILENCE_THRESHOLD_USECS / 1000); QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); _networkAccessManager = new QNetworkAccessManager(this); QNetworkDiskCache* cache = new QNetworkDiskCache(_networkAccessManager); cache->setCacheDirectory(!cachePath.isEmpty() ? cachePath : "interfaceCache"); _networkAccessManager->setCache(cache); _window->setCentralWidget(_glWidget); restoreSizeAndPosition(); loadScripts(); QFontDatabase fontDatabase; fontDatabase.addApplicationFont("resources/styles/Inconsolata.otf"); _window->setVisible(true); _glWidget->setFocusPolicy(Qt::StrongFocus); _glWidget->setFocus(); // enable mouse tracking; otherwise, we only get drag events _glWidget->setMouseTracking(true); // initialization continues in initializeGL when OpenGL context is ready // Tell our voxel edit sender about our known jurisdictions _voxelEditSender.setVoxelServerJurisdictions(&_voxelServerJurisdictions); _particleEditSender.setServerJurisdictions(&_particleServerJurisdictions); Particle::setVoxelEditPacketSender(&_voxelEditSender); Particle::setParticleEditPacketSender(&_particleEditSender); // For now we're going to set the PPS for outbound packets to be super high, this is // probably not the right long term solution. But for now, we're going to do this to // allow you to move a particle around in your hand _particleEditSender.setPacketsPerSecond(3000); // super high!! // Set the sixense filtering _sixenseManager.setFilter(Menu::getInstance()->isOptionChecked(MenuOption::FilterSixense)); checkVersion(); } Application::~Application() { qInstallMessageHandler(NULL); // make sure we don't call the idle timer any more delete idleTimer; Menu::getInstance()->saveSettings(); _rearMirrorTools->saveSettings(_settings); _settings->sync(); // let the avatar mixer know we're out MyAvatar::sendKillAvatar(); // ask the datagram processing thread to quit and wait until it is done _nodeThread->quit(); _nodeThread->wait(); // ask the audio thread to quit and wait until it is done _audio.thread()->quit(); _audio.thread()->wait(); _voxelProcessor.terminate(); _voxelHideShowThread.terminate(); _voxelEditSender.terminate(); _particleEditSender.terminate(); if (_persistThread) { _persistThread->terminate(); _persistThread->deleteLater(); _persistThread = NULL; } storeSizeAndPosition(); saveScripts(); _sharedVoxelSystem.changeTree(new VoxelTree); VoxelTreeElement::removeDeleteHook(&_voxels); // we don't need to do this processing on shutdown Menu::getInstance()->deleteLater(); delete _logger; delete _settings; delete _glWidget; } void Application::restoreSizeAndPosition() { QSettings* settings = new QSettings(this); QRect available = desktop()->availableGeometry(); settings->beginGroup("Window"); int x = (int)loadSetting(settings, "x", 0); int y = (int)loadSetting(settings, "y", 0); _window->move(x, y); int width = (int)loadSetting(settings, "width", available.width()); int height = (int)loadSetting(settings, "height", available.height()); _window->resize(width, height); settings->endGroup(); } void Application::storeSizeAndPosition() { QSettings* settings = new QSettings(this); settings->beginGroup("Window"); settings->setValue("width", _window->rect().width()); settings->setValue("height", _window->rect().height()); settings->setValue("x", _window->pos().x()); settings->setValue("y", _window->pos().y()); settings->endGroup(); } void Application::initializeGL() { qDebug( "Created Display Window."); // initialize glut for shape drawing; Qt apparently initializes it on OS X #ifndef __APPLE__ int argc = 0; glutInit(&argc, 0); #endif #ifdef WIN32 GLenum err = glewInit(); if (GLEW_OK != err) { /* Problem: glewInit failed, something is seriously wrong. */ qDebug("Error: %s\n", glewGetErrorString(err)); } qDebug("Status: Using GLEW %s\n", glewGetString(GLEW_VERSION)); #endif // Before we render anything, let's set up our viewFrustumOffsetCamera with a sufficiently large // field of view and near and far clip to make it interesting. //viewFrustumOffsetCamera.setFieldOfView(90.0); _viewFrustumOffsetCamera.setNearClip(0.1f); _viewFrustumOffsetCamera.setFarClip(500.0f * TREE_SCALE); initDisplay(); qDebug( "Initialized Display."); init(); qDebug( "init() complete."); // create thread for parsing of voxel data independent of the main network and rendering threads _voxelProcessor.initialize(_enableProcessVoxelsThread); _voxelEditSender.initialize(_enableProcessVoxelsThread); _voxelHideShowThread.initialize(_enableProcessVoxelsThread); _particleEditSender.initialize(_enableProcessVoxelsThread); if (_enableProcessVoxelsThread) { qDebug("Voxel parsing thread created."); } // call our timer function every second QTimer* timer = new QTimer(this); connect(timer, SIGNAL(timeout()), SLOT(timer())); timer->start(1000); // call our idle function whenever we can idleTimer = new QTimer(this); connect(idleTimer, SIGNAL(timeout()), SLOT(idle())); idleTimer->start(0); _idleLoopStdev.reset(); if (_justStarted) { float startupTime = (usecTimestampNow() - usecTimestamp(&_applicationStartupTime)) / 1000000.0; _justStarted = false; qDebug("Startup time: %4.2f seconds.", startupTime); const char LOGSTASH_INTERFACE_START_TIME_KEY[] = "interface-start-time"; // ask the Logstash class to record the startup time Logging::stashValue(STAT_TYPE_TIMER, LOGSTASH_INTERFACE_START_TIME_KEY, startupTime); } // update before the first render update(0.0f); InfoView::showFirstTime(); } void Application::paintGL() { PerformanceWarning::setSuppressShortTimings(Menu::getInstance()->isOptionChecked(MenuOption::SuppressShortTimings)); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::paintGL()"); glEnable(GL_LINE_SMOOTH); if (OculusManager::isConnected()) { _myCamera.setUpShift (0.0f); _myCamera.setDistance (0.0f); _myCamera.setTightness (0.0f); // Camera is directly connected to head without smoothing _myCamera.setTargetPosition(_myAvatar.getHead().calculateAverageEyePosition()); _myCamera.setTargetRotation(_myAvatar.getHead().getOrientation()); } else if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { _myCamera.setTightness(0.0f); // In first person, camera follows head exactly without delay _myCamera.setTargetPosition(_myAvatar.getHead().calculateAverageEyePosition()); _myCamera.setTargetRotation(_myAvatar.getHead().getCameraOrientation()); } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { _myCamera.setTightness (0.0f); // Camera is directly connected to head without smoothing _myCamera.setTargetPosition(_myAvatar.getUprightHeadPosition()); _myCamera.setTargetRotation(_myAvatar.getHead().getCameraOrientation()); } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { _myCamera.setTightness(0.0f); float headHeight = _myAvatar.getHead().calculateAverageEyePosition().y - _myAvatar.getPosition().y; _myCamera.setDistance(MIRROR_FULLSCREEN_DISTANCE * _myAvatar.getScale()); _myCamera.setTargetPosition(_myAvatar.getPosition() + glm::vec3(0, headHeight, 0)); _myCamera.setTargetRotation(_myAvatar.getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PIf, 0.0f))); } // Update camera position _myCamera.update( 1.f/_fps ); // Note: whichCamera is used to pick between the normal camera myCamera for our // main camera, vs, an alternate camera. The alternate camera we support right now // is the viewFrustumOffsetCamera. But theoretically, we could use this same mechanism // to add other cameras. // // Why have two cameras? Well, one reason is that because in the case of the renderViewFrustum() // code, we want to keep the state of "myCamera" intact, so we can render what the view frustum of // myCamera is. But we also want to do meaningful camera transforms on OpenGL for the offset camera Camera whichCamera = _myCamera; if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayFrustum)) { ViewFrustumOffset viewFrustumOffset = Menu::getInstance()->getViewFrustumOffset(); // set the camera to third-person view but offset so we can see the frustum _viewFrustumOffsetCamera.setTargetPosition(_myCamera.getTargetPosition()); _viewFrustumOffsetCamera.setTargetRotation(_myCamera.getTargetRotation() * glm::quat(glm::radians(glm::vec3( viewFrustumOffset.pitch, viewFrustumOffset.yaw, viewFrustumOffset.roll)))); _viewFrustumOffsetCamera.setUpShift(viewFrustumOffset.up); _viewFrustumOffsetCamera.setDistance(viewFrustumOffset.distance); _viewFrustumOffsetCamera.initialize(); // force immediate snap to ideal position and orientation _viewFrustumOffsetCamera.update(1.f/_fps); whichCamera = _viewFrustumOffsetCamera; } if (Menu::getInstance()->isOptionChecked(MenuOption::Shadows)) { updateShadowMap(); } if (OculusManager::isConnected()) { OculusManager::display(whichCamera); } else if (TV3DManager::isConnected()) { _glowEffect.prepare(); TV3DManager::display(whichCamera); _glowEffect.render(); } else { _glowEffect.prepare(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); displaySide(whichCamera); glPopMatrix(); _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); } else if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { _rearMirrorTools->render(true); } displayOverlay(); } _frameCount++; } void Application::resetCamerasOnResizeGL(Camera& camera, int width, int height) { if (OculusManager::isConnected()) { OculusManager::configureCamera(camera, width, height); } else if (TV3DManager::isConnected()) { TV3DManager::configureCamera(camera, width, height); } else { camera.setAspectRatio((float)width / height); camera.setFieldOfView(Menu::getInstance()->getFieldOfView()); } } void Application::resizeGL(int width, int height) { resetCamerasOnResizeGL(_viewFrustumOffsetCamera, width, height); resetCamerasOnResizeGL(_myCamera, width, height); glViewport(0, 0, width, height); // shouldn't this account for the menu??? updateProjectionMatrix(); glLoadIdentity(); } void Application::updateProjectionMatrix() { updateProjectionMatrix(_myCamera); } void Application::updateProjectionMatrix(Camera& camera, bool updateViewFrustum) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); float left, right, bottom, top, nearVal, farVal; glm::vec4 nearClipPlane, farClipPlane; // Tell our viewFrustum about this change, using the application camera if (updateViewFrustum) { loadViewFrustum(camera, _viewFrustum); computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); // If we're in Display Frustum mode, then we want to use the slightly adjust near/far clip values of the // _viewFrustumOffsetCamera, so that we can see more of the application content in the application's frustum if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayFrustum)) { nearVal = _viewFrustumOffsetCamera.getNearClip(); farVal = _viewFrustumOffsetCamera.getFarClip(); } } else { ViewFrustum tempViewFrustum; loadViewFrustum(camera, tempViewFrustum); tempViewFrustum.computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); } glFrustum(left, right, bottom, top, nearVal, farVal); glMatrixMode(GL_MODELVIEW); } void Application::resetProfile(const QString& username) { // call the destructor on the old profile and construct a new one (&_profile)->~Profile(); new (&_profile) Profile(username); updateWindowTitle(); } void Application::controlledBroadcastToNodes(unsigned char* broadcastData, size_t dataBytes, const QSet& destinationNodeTypes) { foreach(NODE_TYPE type, destinationNodeTypes) { // Intercept data to voxel server when voxels are disabled if (type == NODE_TYPE_VOXEL_SERVER && !Menu::getInstance()->isOptionChecked(MenuOption::Voxels)) { continue; } // Perform the broadcast for one type int nReceivingNodes = NodeList::getInstance()->broadcastToNodes(broadcastData, dataBytes, QSet() << type); // Feed number of bytes to corresponding channel of the bandwidth meter, if any (done otherwise) BandwidthMeter::ChannelIndex channel; switch (type) { case NODE_TYPE_AGENT: case NODE_TYPE_AVATAR_MIXER: channel = BandwidthMeter::AVATARS; break; case NODE_TYPE_VOXEL_SERVER: channel = BandwidthMeter::VOXELS; break; default: continue; } _bandwidthMeter.outputStream(channel).updateValue(nReceivingNodes * dataBytes); } } void Application::keyPressEvent(QKeyEvent* event) { if (activeWindow() == _window) { if (_chatEntryOn) { if (_chatEntry.keyPressEvent(event)) { _myAvatar.setKeyState(event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete ? DELETE_KEY_DOWN : INSERT_KEY_DOWN); _myAvatar.setChatMessage(string(_chatEntry.getContents().size(), SOLID_BLOCK_CHAR)); } else { _myAvatar.setChatMessage(_chatEntry.getContents()); _chatEntry.clear(); _chatEntryOn = false; setMenuShortcutsEnabled(true); } return; } bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); switch (event->key()) { break; case Qt::Key_Shift: if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelSelectMode)) { _pasteMode = true; } break; case Qt::Key_BracketLeft: case Qt::Key_BracketRight: case Qt::Key_BraceLeft: case Qt::Key_BraceRight: case Qt::Key_ParenLeft: case Qt::Key_ParenRight: case Qt::Key_Less: case Qt::Key_Greater: case Qt::Key_Comma: case Qt::Key_Period: Menu::getInstance()->handleViewFrustumOffsetKeyModifier(event->key()); break; case Qt::Key_Apostrophe: _audioScope.inputPaused = !_audioScope.inputPaused; break; case Qt::Key_L: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::LodTools); } else if (isMeta) { Menu::getInstance()->triggerOption(MenuOption::Log); } break; case Qt::Key_E: if (_nudgeStarted) { _nudgeGuidePosition.y += _mouseVoxel.s; } else { if (!_myAvatar.getDriveKeys(UP)) { _myAvatar.jump(); } _myAvatar.setDriveKeys(UP, 1); } break; case Qt::Key_Asterisk: Menu::getInstance()->triggerOption(MenuOption::Stars); break; case Qt::Key_C: if (_nudgeStarted) { _nudgeGuidePosition.y -= _mouseVoxel.s; } else { _myAvatar.setDriveKeys(DOWN, 1); } break; case Qt::Key_W: if (_nudgeStarted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x += _mouseVoxel.s; } else { _nudgeGuidePosition.x -= _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z += _mouseVoxel.s; } else { _nudgeGuidePosition.z -= _mouseVoxel.s; } } } else { _myAvatar.setDriveKeys(FWD, 1); } break; case Qt::Key_S: if (isShifted && !isMeta) { _voxels.collectStatsForTreesAndVBOs(); } else if (isShifted && isMeta) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); } else if (_nudgeStarted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x -= _mouseVoxel.s; } else { _nudgeGuidePosition.x += _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z -= _mouseVoxel.s; } else { _nudgeGuidePosition.z += _mouseVoxel.s; } } } else { _myAvatar.setDriveKeys(BACK, 1); } break; case Qt::Key_Space: resetSensors(); break; case Qt::Key_G: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::Gravity); } else { Menu::getInstance()->triggerOption(MenuOption::VoxelGetColorMode); } break; case Qt::Key_A: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::Atmosphere); } else if (_nudgeStarted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z -= _mouseVoxel.s; } else { _nudgeGuidePosition.z += _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x += _mouseVoxel.s; } else { _nudgeGuidePosition.x -= _mouseVoxel.s; } } } else { _myAvatar.setDriveKeys(ROT_LEFT, 1); } break; case Qt::Key_D: if (_nudgeStarted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z += _mouseVoxel.s; } else { _nudgeGuidePosition.z -= _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x -= _mouseVoxel.s; } else { _nudgeGuidePosition.x += _mouseVoxel.s; } } } else { _myAvatar.setDriveKeys(ROT_RIGHT, 1); } break; case Qt::Key_Return: case Qt::Key_Enter: if (_nudgeStarted) { nudgeVoxels(); } else { _chatEntryOn = true; _myAvatar.setKeyState(NO_KEY_DOWN); _myAvatar.setChatMessage(string()); setMenuShortcutsEnabled(false); } break; case Qt::Key_Up: if (_nudgeStarted && !isShifted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x += _mouseVoxel.s; } else { _nudgeGuidePosition.x -= _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z += _mouseVoxel.s; } else { _nudgeGuidePosition.z -= _mouseVoxel.s; } } } else if (_nudgeStarted && isShifted) { _nudgeGuidePosition.y += _mouseVoxel.s; } else { _myAvatar.setDriveKeys(isShifted ? UP : FWD, 1); } break; case Qt::Key_Down: if (_nudgeStarted && !isShifted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x -= _mouseVoxel.s; } else { _nudgeGuidePosition.x += _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z -= _mouseVoxel.s; } else { _nudgeGuidePosition.z += _mouseVoxel.s; } } } else if (_nudgeStarted && isShifted) { _nudgeGuidePosition.y -= _mouseVoxel.s; } else { _myAvatar.setDriveKeys(isShifted ? DOWN : BACK, 1); } break; case Qt::Key_Left: if (_nudgeStarted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z -= _mouseVoxel.s; } else { _nudgeGuidePosition.z += _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x += _mouseVoxel.s; } else { _nudgeGuidePosition.x -= _mouseVoxel.s; } } } else { _myAvatar.setDriveKeys(isShifted ? LEFT : ROT_LEFT, 1); } break; case Qt::Key_Right: if (_nudgeStarted) { if (_lookingAlongX) { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.z += _mouseVoxel.s; } else { _nudgeGuidePosition.z -= _mouseVoxel.s; } } else { if (_lookingAwayFromOrigin) { _nudgeGuidePosition.x -= _mouseVoxel.s; } else { _nudgeGuidePosition.x += _mouseVoxel.s; } } } else { _myAvatar.setDriveKeys(isShifted ? RIGHT : ROT_RIGHT, 1); } break; case Qt::Key_I: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(0.002f, 0, 0)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0.001, 0)); } updateProjectionMatrix(); break; case Qt::Key_K: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(-0.002f, 0, 0)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, -0.001, 0)); } updateProjectionMatrix(); break; case Qt::Key_J: if (isShifted) { _viewFrustum.setFocalLength(_viewFrustum.getFocalLength() - 0.1f); if (TV3DManager::isConnected()) { TV3DManager::configureCamera(_myCamera, _glWidget->width(),_glWidget->height()); } } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(-0.001, 0, 0)); } updateProjectionMatrix(); break; case Qt::Key_M: if (isShifted) { _viewFrustum.setFocalLength(_viewFrustum.getFocalLength() + 0.1f); if (TV3DManager::isConnected()) { TV3DManager::configureCamera(_myCamera, _glWidget->width(),_glWidget->height()); } } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0.001, 0, 0)); } updateProjectionMatrix(); break; case Qt::Key_U: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(0, 0, -0.002f)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0, -0.001)); } updateProjectionMatrix(); break; case Qt::Key_Y: if (isShifted) { _myCamera.setEyeOffsetOrientation(glm::normalize( glm::quat(glm::vec3(0, 0, 0.002f)) * _myCamera.getEyeOffsetOrientation())); } else { _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0, 0.001)); } updateProjectionMatrix(); break; case Qt::Key_H: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::Mirror); } else { Menu::getInstance()->triggerOption(MenuOption::FullscreenMirror); } break; case Qt::Key_F: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::DisplayFrustum); } break; case Qt::Key_V: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::Voxels); } else { Menu::getInstance()->triggerOption(MenuOption::VoxelAddMode); _nudgeStarted = false; } break; case Qt::Key_P: Menu::getInstance()->triggerOption(MenuOption::FirstPerson); break; case Qt::Key_R: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::FrustumRenderMode); } else { Menu::getInstance()->triggerOption(MenuOption::VoxelDeleteMode); _nudgeStarted = false; } break; case Qt::Key_B: Menu::getInstance()->triggerOption(MenuOption::VoxelColorMode); _nudgeStarted = false; break; case Qt::Key_O: Menu::getInstance()->triggerOption(MenuOption::VoxelSelectMode); _nudgeStarted = false; break; case Qt::Key_Slash: Menu::getInstance()->triggerOption(MenuOption::Stats); break; case Qt::Key_Backspace: case Qt::Key_Delete: if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelDeleteMode) || Menu::getInstance()->isOptionChecked(MenuOption::VoxelSelectMode)) { deleteVoxelUnderCursor(); } break; case Qt::Key_Plus: _myAvatar.increaseSize(); break; case Qt::Key_Minus: _myAvatar.decreaseSize(); break; case Qt::Key_Equal: _myAvatar.resetSize(); break; case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: _swatch.handleEvent(event->key(), Menu::getInstance()->isOptionChecked(MenuOption::VoxelGetColorMode)); break; case Qt::Key_At: Menu::getInstance()->goToUser(); break; default: event->ignore(); break; } } } void Application::keyReleaseEvent(QKeyEvent* event) { if (activeWindow() == _window) { if (_chatEntryOn) { _myAvatar.setKeyState(NO_KEY_DOWN); return; } switch (event->key()) { case Qt::Key_Shift: _pasteMode = false; break; case Qt::Key_E: _myAvatar.setDriveKeys(UP, 0); break; case Qt::Key_C: _myAvatar.setDriveKeys(DOWN, 0); break; case Qt::Key_W: _myAvatar.setDriveKeys(FWD, 0); break; case Qt::Key_S: _myAvatar.setDriveKeys(BACK, 0); break; case Qt::Key_A: _myAvatar.setDriveKeys(ROT_LEFT, 0); break; case Qt::Key_D: _myAvatar.setDriveKeys(ROT_RIGHT, 0); break; case Qt::Key_Up: _myAvatar.setDriveKeys(FWD, 0); _myAvatar.setDriveKeys(UP, 0); break; case Qt::Key_Down: _myAvatar.setDriveKeys(BACK, 0); _myAvatar.setDriveKeys(DOWN, 0); break; case Qt::Key_Left: _myAvatar.setDriveKeys(LEFT, 0); _myAvatar.setDriveKeys(ROT_LEFT, 0); break; case Qt::Key_Right: _myAvatar.setDriveKeys(RIGHT, 0); _myAvatar.setDriveKeys(ROT_RIGHT, 0); break; default: event->ignore(); break; } } } void Application::mouseMoveEvent(QMouseEvent* event) { _lastMouseMove = usecTimestampNow(); if (_mouseHidden) { getGLWidget()->setCursor(Qt::ArrowCursor); _mouseHidden = false; _seenMouseMove = true; } int deltaX = event->x() - _mouseX; int deltaY = event->y() - _mouseY; _mouseX = event->x(); _mouseY = event->y(); if (activeWindow() == _window) { // orbit behavior if (_mousePressed && !Menu::getInstance()->isVoxelModeActionChecked()) { if (_avatarManager.getLookAtTargetAvatar()) { _myAvatar.orbit(_avatarManager.getLookAtTargetAvatar()->getPosition(), deltaX, deltaY); return; } if (_isHoverVoxel) { _myAvatar.orbit(getMouseVoxelWorldCoordinates(_hoverVoxel), deltaX, deltaY); return; } } // detect drag glm::vec3 mouseVoxelPos(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z); if (!_justEditedVoxel && mouseVoxelPos != _lastMouseVoxelPos) { if (event->buttons().testFlag(Qt::LeftButton)) { maybeEditVoxelUnderCursor(); } else if (event->buttons().testFlag(Qt::RightButton) && Menu::getInstance()->isVoxelModeActionChecked()) { deleteVoxelUnderCursor(); } } _pieMenu.mouseMoveEvent(_mouseX, _mouseY); } } const bool MAKE_SOUND_ON_VOXEL_HOVER = false; const bool MAKE_SOUND_ON_VOXEL_CLICK = true; const float HOVER_VOXEL_FREQUENCY = 7040.f; const float HOVER_VOXEL_DECAY = 0.999f; void Application::mousePressEvent(QMouseEvent* event) { if (activeWindow() == _window) { if (event->button() == Qt::LeftButton) { _mouseX = event->x(); _mouseY = event->y(); _mouseDragStartedX = _mouseX; _mouseDragStartedY = _mouseY; _mouseVoxelDragging = _mouseVoxel; _mousePressed = true; maybeEditVoxelUnderCursor(); if (_audio.mousePressEvent(_mouseX, _mouseY)) { // stop propagation return; } if (_rearMirrorTools->mousePressEvent(_mouseX, _mouseY)) { // stop propagation return; } if (!_palette.isActive() && (!_isHoverVoxel || _avatarManager.getLookAtTargetAvatar())) { // disable for now // _pieMenu.mousePressEvent(_mouseX, _mouseY); } if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelSelectMode) && _pasteMode) { pasteVoxels(); } if (MAKE_SOUND_ON_VOXEL_CLICK && _isHoverVoxel && !_isHoverVoxelSounding) { _hoverVoxelOriginalColor[0] = _hoverVoxel.red; _hoverVoxelOriginalColor[1] = _hoverVoxel.green; _hoverVoxelOriginalColor[2] = _hoverVoxel.blue; _hoverVoxelOriginalColor[3] = 1; const float RED_CLICK_FREQUENCY = 1000.f; const float GREEN_CLICK_FREQUENCY = 1250.f; const float BLUE_CLICK_FREQUENCY = 1330.f; const float MIDDLE_A_FREQUENCY = 440.f; float frequency = MIDDLE_A_FREQUENCY + (_hoverVoxel.red / 255.f * RED_CLICK_FREQUENCY + _hoverVoxel.green / 255.f * GREEN_CLICK_FREQUENCY + _hoverVoxel.blue / 255.f * BLUE_CLICK_FREQUENCY) / 3.f; _audio.startCollisionSound(1.0, frequency, 0.0, HOVER_VOXEL_DECAY, false); _isHoverVoxelSounding = true; const float PERCENTAGE_TO_MOVE_TOWARD = 0.90f; glm::vec3 newTarget = getMouseVoxelWorldCoordinates(_hoverVoxel); glm::vec3 myPosition = _myAvatar.getPosition(); // If there is not an action tool set (add, delete, color), move to this voxel if (Menu::getInstance()->isOptionChecked(MenuOption::ClickToFly) && !(Menu::getInstance()->isOptionChecked(MenuOption::VoxelAddMode) || Menu::getInstance()->isOptionChecked(MenuOption::VoxelDeleteMode) || Menu::getInstance()->isOptionChecked(MenuOption::VoxelColorMode))) { _myAvatar.setMoveTarget(myPosition + (newTarget - myPosition) * PERCENTAGE_TO_MOVE_TOWARD); } } } else if (event->button() == Qt::RightButton && Menu::getInstance()->isVoxelModeActionChecked()) { deleteVoxelUnderCursor(); } } } void Application::mouseReleaseEvent(QMouseEvent* event) { if (activeWindow() == _window) { if (event->button() == Qt::LeftButton) { _mouseX = event->x(); _mouseY = event->y(); _mousePressed = false; checkBandwidthMeterClick(); if (Menu::getInstance()->isOptionChecked(MenuOption::Stats)) { checkStatsClick(); } _pieMenu.mouseReleaseEvent(_mouseX, _mouseY); } } } void Application::touchUpdateEvent(QTouchEvent* event) { bool validTouch = false; if (activeWindow() == _window) { const QList& tPoints = event->touchPoints(); _touchAvgX = 0.0f; _touchAvgY = 0.0f; int numTouches = tPoints.count(); if (numTouches > 1) { for (int i = 0; i < numTouches; ++i) { _touchAvgX += tPoints[i].pos().x(); _touchAvgY += tPoints[i].pos().y(); } _touchAvgX /= (float)(numTouches); _touchAvgY /= (float)(numTouches); validTouch = true; } } if (!_isTouchPressed) { _touchDragStartedAvgX = _touchAvgX; _touchDragStartedAvgY = _touchAvgY; } _isTouchPressed = validTouch; } void Application::touchBeginEvent(QTouchEvent* event) { touchUpdateEvent(event); _lastTouchAvgX = _touchAvgX; _lastTouchAvgY = _touchAvgY; } void Application::touchEndEvent(QTouchEvent* event) { _touchDragStartedAvgX = _touchAvgX; _touchDragStartedAvgY = _touchAvgY; _isTouchPressed = false; } const bool USE_MOUSEWHEEL = false; void Application::wheelEvent(QWheelEvent* event) { // Wheel Events disabled for now because they are also activated by touch look pitch up/down. if (USE_MOUSEWHEEL && (activeWindow() == _window)) { if (!Menu::getInstance()->isVoxelModeActionChecked()) { event->ignore(); return; } if (event->delta() > 0) { increaseVoxelSize(); } else { decreaseVoxelSize(); } } } void Application::sendPingPackets() { unsigned char pingPacket[MAX_PACKET_SIZE]; int length = NodeList::getInstance()->fillPingPacket(pingPacket); getInstance()->controlledBroadcastToNodes(pingPacket, length, QSet() << NODE_TYPE_VOXEL_SERVER << NODE_TYPE_PARTICLE_SERVER << NODE_TYPE_AUDIO_MIXER << NODE_TYPE_AVATAR_MIXER << NODE_TYPE_METAVOXEL_SERVER); } // Every second, check the frame rates and other stuff void Application::timer() { gettimeofday(&_timerEnd, NULL); if (Menu::getInstance()->isOptionChecked(MenuOption::TestPing)) { sendPingPackets(); } _fps = (float)_frameCount / ((float)diffclock(&_timerStart, &_timerEnd) / 1000.f); _packetsPerSecond = (float) _datagramProcessor.getPacketCount() / ((float)diffclock(&_timerStart, &_timerEnd) / 1000.f); _bytesPerSecond = (float) _datagramProcessor.getByteCount() / ((float)diffclock(&_timerStart, &_timerEnd) / 1000.f); _frameCount = 0; _datagramProcessor.resetCounters(); gettimeofday(&_timerStart, NULL); // ask the node list to check in with the domain server NodeList::getInstance()->sendDomainServerCheckIn(); // send unmatched DataServerClient packets DataServerClient::resendUnmatchedPackets(); // give the MyAvatar object position, orientation to the Profile so it can propagate to the data-server _profile.updatePosition(_myAvatar.getPosition()); _profile.updateOrientation(_myAvatar.getOrientation()); } static glm::vec3 getFaceVector(BoxFace face) { switch (face) { case MIN_X_FACE: return glm::vec3(-1, 0, 0); case MAX_X_FACE: return glm::vec3(1, 0, 0); case MIN_Y_FACE: return glm::vec3(0, -1, 0); case MAX_Y_FACE: return glm::vec3(0, 1, 0); case MIN_Z_FACE: return glm::vec3(0, 0, -1); default: // quiet windows warnings case MAX_Z_FACE: return glm::vec3(0, 0, 1); } } void Application::idle() { // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing // details if we're in ExtraDebugging mode. However, the ::update() and it's subcomponents will show their timing // details normally. bool showWarnings = getLogger()->extraDebugging(); PerformanceWarning warn(showWarnings, "Application::idle()"); timeval check; gettimeofday(&check, NULL); // Only run simulation code if more than IDLE_SIMULATE_MSECS have passed since last time we ran double timeSinceLastUpdate = diffclock(&_lastTimeUpdated, &check); if (timeSinceLastUpdate > IDLE_SIMULATE_MSECS) { { PerformanceWarning warn(showWarnings, "Application::idle()... update()"); const float BIGGEST_DELTA_TIME_SECS = 0.25f; update(glm::clamp((float)timeSinceLastUpdate / 1000.f, 0.f, BIGGEST_DELTA_TIME_SECS)); } { PerformanceWarning warn(showWarnings, "Application::idle()... updateGL()"); _glWidget->updateGL(); } { PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); _lastTimeUpdated = check; _idleLoopStdev.addValue(timeSinceLastUpdate); // Record standard deviation and reset counter if needed const int STDEV_SAMPLES = 500; if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); _idleLoopStdev.reset(); } // After finishing all of the above work, restart the idle timer, allowing 2ms to process events. idleTimer->start(2); } } } void Application::checkBandwidthMeterClick() { // ... to be called upon button release if (Menu::getInstance()->isOptionChecked(MenuOption::Bandwidth) && glm::compMax(glm::abs(glm::ivec2(_mouseX - _mouseDragStartedX, _mouseY - _mouseDragStartedY))) <= BANDWIDTH_METER_CLICK_MAX_DRAG_LENGTH && _bandwidthMeter.isWithinArea(_mouseX, _mouseY, _glWidget->width(), _glWidget->height())) { // The bandwidth meter is visible, the click didn't get dragged too far and // we actually hit the bandwidth meter Menu::getInstance()->bandwidthDetails(); } } void Application::setFullscreen(bool fullscreen) { _window->setWindowState(fullscreen ? (_window->windowState() | Qt::WindowFullScreen) : (_window->windowState() & ~Qt::WindowFullScreen)); } void Application::setEnable3DTVMode(bool enable3DTVMode) { resizeGL(_glWidget->width(),_glWidget->height()); } void Application::setRenderVoxels(bool voxelRender) { _voxelEditSender.setShouldSend(voxelRender); if (!voxelRender) { doKillLocalVoxels(); } } void Application::doKillLocalVoxels() { _wantToKillLocalVoxels = true; } void Application::removeVoxel(glm::vec3 position, float scale) { VoxelDetail voxel; voxel.x = position.x / TREE_SCALE; voxel.y = position.y / TREE_SCALE; voxel.z = position.z / TREE_SCALE; voxel.s = scale / TREE_SCALE; _voxelEditSender.sendVoxelEditMessage(PACKET_TYPE_VOXEL_ERASE, voxel); // delete it locally to see the effect immediately (and in case no voxel server is present) _voxels.deleteVoxelAt(voxel.x, voxel.y, voxel.z, voxel.s); } void Application::makeVoxel(glm::vec3 position, float scale, unsigned char red, unsigned char green, unsigned char blue, bool isDestructive) { VoxelDetail voxel; voxel.x = position.x / TREE_SCALE; voxel.y = position.y / TREE_SCALE; voxel.z = position.z / TREE_SCALE; voxel.s = scale / TREE_SCALE; voxel.red = red; voxel.green = green; voxel.blue = blue; PACKET_TYPE message = isDestructive ? PACKET_TYPE_VOXEL_SET_DESTRUCTIVE : PACKET_TYPE_VOXEL_SET; _voxelEditSender.sendVoxelEditMessage(message, voxel); // create the voxel locally so it appears immediately _voxels.createVoxel(voxel.x, voxel.y, voxel.z, voxel.s, voxel.red, voxel.green, voxel.blue, isDestructive); } glm::vec3 Application::getMouseVoxelWorldCoordinates(const VoxelDetail& mouseVoxel) { return glm::vec3((mouseVoxel.x + mouseVoxel.s / 2.f) * TREE_SCALE, (mouseVoxel.y + mouseVoxel.s / 2.f) * TREE_SCALE, (mouseVoxel.z + mouseVoxel.s / 2.f) * TREE_SCALE); } const float NUDGE_PRECISION_MIN = 1 / pow(2.0, 12.0); void Application::decreaseVoxelSize() { if (_nudgeStarted) { if (_mouseVoxelScale >= NUDGE_PRECISION_MIN) { _mouseVoxelScale /= 2; } } else { _mouseVoxelScale /= 2; } } void Application::increaseVoxelSize() { if (_nudgeStarted) { if (_mouseVoxelScale < _nudgeVoxel.s) { _mouseVoxelScale *= 2; } } else { _mouseVoxelScale *= 2; } } const int MAXIMUM_EDIT_VOXEL_MESSAGE_SIZE = 1500; struct SendVoxelsOperationArgs { const unsigned char* newBaseOctCode; }; bool Application::sendVoxelsOperation(OctreeElement* element, void* extraData) { VoxelTreeElement* voxel = (VoxelTreeElement*)element; SendVoxelsOperationArgs* args = (SendVoxelsOperationArgs*)extraData; if (voxel->isColored()) { const unsigned char* nodeOctalCode = voxel->getOctalCode(); unsigned char* codeColorBuffer = NULL; int codeLength = 0; int bytesInCode = 0; int codeAndColorLength; // If the newBase is NULL, then don't rebase if (args->newBaseOctCode) { codeColorBuffer = rebaseOctalCode(nodeOctalCode, args->newBaseOctCode, true); codeLength = numberOfThreeBitSectionsInCode(codeColorBuffer); bytesInCode = bytesRequiredForCodeLength(codeLength); codeAndColorLength = bytesInCode + SIZE_OF_COLOR_DATA; } else { codeLength = numberOfThreeBitSectionsInCode(nodeOctalCode); bytesInCode = bytesRequiredForCodeLength(codeLength); codeAndColorLength = bytesInCode + SIZE_OF_COLOR_DATA; codeColorBuffer = new unsigned char[codeAndColorLength]; memcpy(codeColorBuffer, nodeOctalCode, bytesInCode); } // copy the colors over codeColorBuffer[bytesInCode + RED_INDEX] = voxel->getColor()[RED_INDEX]; codeColorBuffer[bytesInCode + GREEN_INDEX] = voxel->getColor()[GREEN_INDEX]; codeColorBuffer[bytesInCode + BLUE_INDEX] = voxel->getColor()[BLUE_INDEX]; getInstance()->_voxelEditSender.queueVoxelEditMessage(PACKET_TYPE_VOXEL_SET_DESTRUCTIVE, codeColorBuffer, codeAndColorLength); delete[] codeColorBuffer; } return true; // keep going } void Application::exportVoxels() { QString desktopLocation = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); QString suggestedName = desktopLocation.append("/voxels.svo"); QString fileNameString = QFileDialog::getSaveFileName(_glWidget, tr("Export Voxels"), suggestedName, tr("Sparse Voxel Octree Files (*.svo)")); QByteArray fileNameAscii = fileNameString.toLocal8Bit(); const char* fileName = fileNameAscii.data(); VoxelTreeElement* selectedNode = _voxels.getVoxelAt(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); if (selectedNode) { VoxelTree exportTree; _voxels.copySubTreeIntoNewTree(selectedNode, &exportTree, true); exportTree.writeToSVOFile(fileName); } // restore the main window's active state _window->activateWindow(); } void Application::importVoxels() { if (_voxelImporter.exec()) { qDebug("[DEBUG] Import succeeded."); } else { qDebug("[DEBUG] Import failed."); } // restore the main window's active state _window->activateWindow(); } void Application::cutVoxels() { copyVoxels(); deleteVoxelUnderCursor(); } void Application::copyVoxels() { // switch to and clear the clipboard first... _sharedVoxelSystem.killLocalVoxels(); if (_sharedVoxelSystem.getTree() != &_clipboard) { _clipboard.eraseAllOctreeElements(); _sharedVoxelSystem.changeTree(&_clipboard); } // then copy onto it if there is something to copy VoxelTreeElement* selectedNode = _voxels.getVoxelAt(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); if (selectedNode) { _voxels.copySubTreeIntoNewTree(selectedNode, &_sharedVoxelSystem, true); } } void Application::pasteVoxelsToOctalCode(const unsigned char* octalCodeDestination) { // Recurse the clipboard tree, where everything is root relative, and send all the colored voxels to // the server as an set voxel message, this will also rebase the voxels to the new location SendVoxelsOperationArgs args; args.newBaseOctCode = octalCodeDestination; _sharedVoxelSystem.getTree()->recurseTreeWithOperation(sendVoxelsOperation, &args); if (_sharedVoxelSystem.getTree() != &_clipboard) { _sharedVoxelSystem.killLocalVoxels(); _sharedVoxelSystem.changeTree(&_clipboard); } _voxelEditSender.releaseQueuedMessages(); } void Application::pasteVoxels() { unsigned char* calculatedOctCode = NULL; VoxelTreeElement* selectedNode = _voxels.getVoxelAt(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); // we only need the selected voxel to get the newBaseOctCode, which we can actually calculate from the // voxel size/position details. If we don't have an actual selectedNode then use the mouseVoxel to create a // target octalCode for where the user is pointing. const unsigned char* octalCodeDestination; if (selectedNode) { octalCodeDestination = selectedNode->getOctalCode(); } else { octalCodeDestination = calculatedOctCode = pointToVoxel(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); } pasteVoxelsToOctalCode(octalCodeDestination); if (calculatedOctCode) { delete[] calculatedOctCode; } } void Application::findAxisAlignment() { glm::vec3 direction = _myAvatar.getMouseRayDirection(); if (fabs(direction.z) > fabs(direction.x)) { _lookingAlongX = false; if (direction.z < 0) { _lookingAwayFromOrigin = false; } else { _lookingAwayFromOrigin = true; } } else { _lookingAlongX = true; if (direction.x < 0) { _lookingAwayFromOrigin = false; } else { _lookingAwayFromOrigin = true; } } } void Application::nudgeVoxels() { VoxelTreeElement* selectedNode = _voxels.getVoxelAt(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); if (!Menu::getInstance()->isOptionChecked(MenuOption::VoxelSelectMode) && selectedNode) { Menu::getInstance()->triggerOption(MenuOption::VoxelSelectMode); } if (!_nudgeStarted && selectedNode) { _nudgeVoxel = _mouseVoxel; _nudgeStarted = true; _nudgeGuidePosition = glm::vec3(_nudgeVoxel.x, _nudgeVoxel.y, _nudgeVoxel.z); findAxisAlignment(); } else { // calculate nudgeVec glm::vec3 nudgeVec(_nudgeGuidePosition.x - _nudgeVoxel.x, _nudgeGuidePosition.y - _nudgeVoxel.y, _nudgeGuidePosition.z - _nudgeVoxel.z); VoxelTreeElement* nodeToNudge = _voxels.getVoxelAt(_nudgeVoxel.x, _nudgeVoxel.y, _nudgeVoxel.z, _nudgeVoxel.s); if (nodeToNudge) { _voxels.getTree()->nudgeSubTree(nodeToNudge, nudgeVec, _voxelEditSender); _nudgeStarted = false; } } } void Application::deleteVoxels() { deleteVoxelUnderCursor(); } void Application::initDisplay() { glEnable(GL_BLEND); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); glShadeModel(GL_SMOOTH); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_DEPTH_TEST); } void Application::init() { _sharedVoxelSystemViewFrustum.setPosition(glm::vec3(TREE_SCALE / 2.0f, TREE_SCALE / 2.0f, 3.0f * TREE_SCALE / 2.0f)); _sharedVoxelSystemViewFrustum.setNearClip(TREE_SCALE / 2.0f); _sharedVoxelSystemViewFrustum.setFarClip(3.0f * TREE_SCALE / 2.0f); _sharedVoxelSystemViewFrustum.setFieldOfView(90); _sharedVoxelSystemViewFrustum.setOrientation(glm::quat()); _sharedVoxelSystemViewFrustum.calculate(); _sharedVoxelSystem.setViewFrustum(&_sharedVoxelSystemViewFrustum); VoxelTreeElement::removeUpdateHook(&_sharedVoxelSystem); _sharedVoxelSystem.init(); VoxelTree* tmpTree = _sharedVoxelSystem.getTree(); _sharedVoxelSystem.changeTree(&_clipboard); delete tmpTree; _voxelImporter.init(_settings); _environment.init(); _glowEffect.init(); _ambientOcclusionEffect.init(); _voxelShader.init(); _pointShader.init(); _headMouseX = _mouseX = _glWidget->width() / 2; _headMouseY = _mouseY = _glWidget->height() / 2; QCursor::setPos(_headMouseX, _headMouseY); _myAvatar.init(); _myAvatar.setPosition(START_LOCATION); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); _myCamera.setModeShiftRate(1.0f); _myAvatar.setDisplayingLookatVectors(false); _mirrorCamera.setMode(CAMERA_MODE_MIRROR); _mirrorCamera.setAspectRatio((float)MIRROR_VIEW_WIDTH / (float)MIRROR_VIEW_HEIGHT); _mirrorCamera.setFieldOfView(30); OculusManager::connect(); if (OculusManager::isConnected()) { QMetaObject::invokeMethod(Menu::getInstance()->getActionForOption(MenuOption::Fullscreen), "trigger", Qt::QueuedConnection); } TV3DManager::connect(); if (TV3DManager::isConnected()) { QMetaObject::invokeMethod(Menu::getInstance()->getActionForOption(MenuOption::Fullscreen), "trigger", Qt::QueuedConnection); } gettimeofday(&_timerStart, NULL); gettimeofday(&_lastTimeUpdated, NULL); Menu::getInstance()->loadSettings(); if (Menu::getInstance()->getAudioJitterBufferSamples() != 0) { _audio.setJitterBufferSamples(Menu::getInstance()->getAudioJitterBufferSamples()); } qDebug("Loaded settings"); if (!_profile.getUsername().isEmpty()) { // we have a username for this avatar, ask the data-server for the mesh URL for this avatar DataServerClient::getValueForKeyAndUserString(DataServerKey::FaceMeshURL, _profile.getUserString(), &_profile); DataServerClient::getValueForKeyAndUserString(DataServerKey::SkeletonURL, _profile.getUserString(), &_profile); } // Set up VoxelSystem after loading preferences so we can get the desired max voxel count _voxels.setMaxVoxels(Menu::getInstance()->getMaxVoxels()); _voxels.setUseVoxelShader(Menu::getInstance()->isOptionChecked(MenuOption::UseVoxelShader)); _voxels.setVoxelsAsPoints(Menu::getInstance()->isOptionChecked(MenuOption::VoxelsAsPoints)); _voxels.setDisableFastVoxelPipeline(false); _voxels.init(); _particles.init(); _particles.setViewFrustum(getViewFrustum()); _metavoxels.init(); _particleCollisionSystem.init(&_particleEditSender, _particles.getTree(), _voxels.getTree(), &_audio); _palette.init(_glWidget->width(), _glWidget->height()); _palette.addAction(Menu::getInstance()->getActionForOption(MenuOption::VoxelAddMode), 0, 0); _palette.addAction(Menu::getInstance()->getActionForOption(MenuOption::VoxelDeleteMode), 0, 1); _palette.addTool(&_swatch); _palette.addAction(Menu::getInstance()->getActionForOption(MenuOption::VoxelColorMode), 0, 2); _palette.addAction(Menu::getInstance()->getActionForOption(MenuOption::VoxelGetColorMode), 0, 3); _palette.addAction(Menu::getInstance()->getActionForOption(MenuOption::VoxelSelectMode), 0, 4); _pieMenu.init("./resources/images/hifi-interface-tools-v2-pie.svg", _glWidget->width(), _glWidget->height()); _audio.init(_glWidget); _rearMirrorTools = new RearMirrorTools(_glWidget, _mirrorViewRect, _settings); connect(_rearMirrorTools, SIGNAL(closeView()), SLOT(closeMirrorView())); connect(_rearMirrorTools, SIGNAL(restoreView()), SLOT(restoreMirrorView())); connect(_rearMirrorTools, SIGNAL(shrinkView()), SLOT(shrinkMirrorView())); connect(_rearMirrorTools, SIGNAL(resetView()), SLOT(resetSensors())); updateLocalOctreeCache(true); } void Application::closeMirrorView() { if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { Menu::getInstance()->triggerOption(MenuOption::Mirror);; } } void Application::restoreMirrorView() { if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { Menu::getInstance()->triggerOption(MenuOption::Mirror);; } if (!Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { Menu::getInstance()->triggerOption(MenuOption::FullscreenMirror); } } void Application::shrinkMirrorView() { if (!Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { Menu::getInstance()->triggerOption(MenuOption::Mirror);; } if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { Menu::getInstance()->triggerOption(MenuOption::FullscreenMirror); } } const float MAX_AVATAR_EDIT_VELOCITY = 1.0f; const float MAX_VOXEL_EDIT_DISTANCE = 50.0f; const float HEAD_SPHERE_RADIUS = 0.07f; bool Application::isLookingAtMyAvatar(Avatar* avatar) { glm::vec3 theirLookat = avatar->getHead().getLookAtPosition(); glm::vec3 myHeadPosition = _myAvatar.getHead().getPosition(); if (pointInSphere(theirLookat, myHeadPosition, HEAD_SPHERE_RADIUS * _myAvatar.getScale())) { return true; } return false; } void Application::renderHighlightVoxel(VoxelDetail voxel) { glDisable(GL_LIGHTING); glPushMatrix(); glScalef(TREE_SCALE, TREE_SCALE, TREE_SCALE); const float EDGE_EXPAND = 1.02f; glColor3ub(voxel.red + 128, voxel.green + 128, voxel.blue + 128); glTranslatef(voxel.x + voxel.s * 0.5f, voxel.y + voxel.s * 0.5f, voxel.z + voxel.s * 0.5f); glLineWidth(2.0f); glutWireCube(voxel.s * EDGE_EXPAND); glPopMatrix(); } void Application::updateMouseRay() { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMouseRay()"); _viewFrustum.computePickRay(_mouseX / (float)_glWidget->width(), _mouseY / (float)_glWidget->height(), _mouseRayOrigin, _mouseRayDirection); // adjust for mirroring if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { glm::vec3 mouseRayOffset = _mouseRayOrigin - _viewFrustum.getPosition(); _mouseRayOrigin -= 2.0f * (_viewFrustum.getDirection() * glm::dot(_viewFrustum.getDirection(), mouseRayOffset) + _viewFrustum.getRight() * glm::dot(_viewFrustum.getRight(), mouseRayOffset)); _mouseRayDirection -= 2.0f * (_viewFrustum.getDirection() * glm::dot(_viewFrustum.getDirection(), _mouseRayDirection) + _viewFrustum.getRight() * glm::dot(_viewFrustum.getRight(), _mouseRayDirection)); } // tell my avatar if the mouse is being pressed... _myAvatar.setMousePressed(_mousePressed); // tell my avatar the posiion and direction of the ray projected ino the world based on the mouse position _myAvatar.setMouseRay(_mouseRayOrigin, _mouseRayDirection); } void Application::updateFaceshift() { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateFaceshift()"); // Update faceshift _faceshift.update(); // Copy angular velocity if measured by faceshift, to the head if (_faceshift.isActive()) { _myAvatar.getHead().setAngularVelocity(_faceshift.getHeadAngularVelocity()); } } void Application::updateMyAvatarLookAtPosition(glm::vec3& lookAtSpot) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); const float FAR_AWAY_STARE = TREE_SCALE; if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { lookAtSpot = _myCamera.getPosition(); } else if (_mouseHidden) { // if the mouse cursor is hidden, just look straight ahead glm::vec3 rayOrigin, rayDirection; _viewFrustum.computePickRay(0.5f, 0.5f, rayOrigin, rayDirection); lookAtSpot = rayOrigin + rayDirection * FAR_AWAY_STARE; } else { // just look in direction of the mouse ray lookAtSpot = _mouseRayOrigin + _mouseRayDirection * FAR_AWAY_STARE; } if (_faceshift.isActive()) { // deflect using Faceshift gaze data glm::vec3 origin = _myAvatar.getHead().calculateAverageEyePosition(); float pitchSign = (_myCamera.getMode() == CAMERA_MODE_MIRROR) ? -1.0f : 1.0f; float deflection = Menu::getInstance()->getFaceshiftEyeDeflection(); lookAtSpot = origin + _myCamera.getRotation() * glm::quat(glm::radians(glm::vec3( _faceshift.getEstimatedEyePitch() * pitchSign * deflection, _faceshift.getEstimatedEyeYaw() * deflection, 0.0f))) * glm::inverse(_myCamera.getRotation()) * (lookAtSpot - origin); } _myAvatar.getHead().setLookAtPosition(lookAtSpot); } void Application::updateHoverVoxels(float deltaTime, float& distance, BoxFace& face) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateHoverVoxels()"); // If we have clicked on a voxel, update it's color if (_isHoverVoxelSounding) { VoxelTreeElement* hoveredNode = _voxels.getVoxelAt(_hoverVoxel.x, _hoverVoxel.y, _hoverVoxel.z, _hoverVoxel.s); if (hoveredNode) { float bright = _audio.getCollisionSoundMagnitude(); nodeColor clickColor = { 255 * bright + _hoverVoxelOriginalColor[0] * (1.f - bright), _hoverVoxelOriginalColor[1] * (1.f - bright), _hoverVoxelOriginalColor[2] * (1.f - bright), 1 }; hoveredNode->setColor(clickColor); if (bright < 0.01f) { hoveredNode->setColor(_hoverVoxelOriginalColor); _isHoverVoxelSounding = false; } } else { // Voxel is not found, clear all _isHoverVoxelSounding = false; _isHoverVoxel = false; } } else { // Check for a new hover voxel glm::vec4 oldVoxel(_hoverVoxel.x, _hoverVoxel.y, _hoverVoxel.z, _hoverVoxel.s); // only do this work if MAKE_SOUND_ON_VOXEL_HOVER or MAKE_SOUND_ON_VOXEL_CLICK is enabled, // and make sure the tree is not already busy... because otherwise you'll have to wait. if (!(_voxels.treeIsBusy() || _mousePressed)) { { PerformanceWarning warn(showWarnings, "Application::updateHoverVoxels() _voxels.findRayIntersection()"); _isHoverVoxel = _voxels.findRayIntersection(_mouseRayOrigin, _mouseRayDirection, _hoverVoxel, distance, face); } if (MAKE_SOUND_ON_VOXEL_HOVER && _isHoverVoxel && glm::vec4(_hoverVoxel.x, _hoverVoxel.y, _hoverVoxel.z, _hoverVoxel.s) != oldVoxel) { _hoverVoxelOriginalColor[0] = _hoverVoxel.red; _hoverVoxelOriginalColor[1] = _hoverVoxel.green; _hoverVoxelOriginalColor[2] = _hoverVoxel.blue; _hoverVoxelOriginalColor[3] = 1; _audio.startCollisionSound(1.0, HOVER_VOXEL_FREQUENCY * _hoverVoxel.s * TREE_SCALE, 0.0, HOVER_VOXEL_DECAY, false); _isHoverVoxelSounding = true; } } } } void Application::updateMouseVoxels(float deltaTime, float& distance, BoxFace& face) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMouseVoxels()"); _mouseVoxel.s = 0.0f; bool wasInitialized = _mouseVoxelScaleInitialized; if (Menu::getInstance()->isVoxelModeActionChecked() && (fabs(_myAvatar.getVelocity().x) + fabs(_myAvatar.getVelocity().y) + fabs(_myAvatar.getVelocity().z)) / 3 < MAX_AVATAR_EDIT_VELOCITY) { if (_voxels.findRayIntersection(_mouseRayOrigin, _mouseRayDirection, _mouseVoxel, distance, face)) { if (distance < MAX_VOXEL_EDIT_DISTANCE) { // set the voxel scale to that of the first moused-over voxel if (!wasInitialized) { _mouseVoxelScale = _mouseVoxel.s; } _mouseVoxelScaleInitialized = true; // find the nearest voxel with the desired scale if (_mouseVoxelScale > _mouseVoxel.s) { // choose the larger voxel that encompasses the one selected _mouseVoxel.x = _mouseVoxelScale * floorf(_mouseVoxel.x / _mouseVoxelScale); _mouseVoxel.y = _mouseVoxelScale * floorf(_mouseVoxel.y / _mouseVoxelScale); _mouseVoxel.z = _mouseVoxelScale * floorf(_mouseVoxel.z / _mouseVoxelScale); _mouseVoxel.s = _mouseVoxelScale; } else { glm::vec3 faceVector = getFaceVector(face); if (_mouseVoxelScale < _mouseVoxel.s) { // find the closest contained voxel glm::vec3 pt = (_mouseRayOrigin + _mouseRayDirection * distance) / (float)TREE_SCALE - faceVector * (_mouseVoxelScale * 0.5f); _mouseVoxel.x = _mouseVoxelScale * floorf(pt.x / _mouseVoxelScale); _mouseVoxel.y = _mouseVoxelScale * floorf(pt.y / _mouseVoxelScale); _mouseVoxel.z = _mouseVoxelScale * floorf(pt.z / _mouseVoxelScale); _mouseVoxel.s = _mouseVoxelScale; } if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelAddMode)) { // use the face to determine the side on which to create a neighbor _mouseVoxel.x += faceVector.x * _mouseVoxel.s; _mouseVoxel.y += faceVector.y * _mouseVoxel.s; _mouseVoxel.z += faceVector.z * _mouseVoxel.s; } } } else { _mouseVoxel.s = 0.0f; } } else if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelAddMode) || Menu::getInstance()->isOptionChecked(MenuOption::VoxelSelectMode)) { // place the voxel a fixed distance away float worldMouseVoxelScale = _mouseVoxelScale * TREE_SCALE; glm::vec3 pt = _mouseRayOrigin + _mouseRayDirection * (2.0f + worldMouseVoxelScale * 0.5f); _mouseVoxel.x = _mouseVoxelScale * floorf(pt.x / worldMouseVoxelScale); _mouseVoxel.y = _mouseVoxelScale * floorf(pt.y / worldMouseVoxelScale); _mouseVoxel.z = _mouseVoxelScale * floorf(pt.z / worldMouseVoxelScale); _mouseVoxel.s = _mouseVoxelScale; } if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelDeleteMode)) { // red indicates deletion _mouseVoxel.red = 255; _mouseVoxel.green = _mouseVoxel.blue = 0; } else if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelSelectMode)) { if (_nudgeStarted) { _mouseVoxel.red = _mouseVoxel.green = _mouseVoxel.blue = 255; } else { // yellow indicates selection _mouseVoxel.red = _mouseVoxel.green = 255; _mouseVoxel.blue = 0; } } else { // _addVoxelMode->isChecked() || _colorVoxelMode->isChecked() QColor paintColor = Menu::getInstance()->getActionForOption(MenuOption::VoxelPaintColor)->data().value(); _mouseVoxel.red = paintColor.red(); _mouseVoxel.green = paintColor.green(); _mouseVoxel.blue = paintColor.blue(); } // if we just edited, use the currently selected voxel as the "last" for drag detection if (_justEditedVoxel) { _lastMouseVoxelPos = glm::vec3(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z); _justEditedVoxel = false; } } } void Application::updateHandAndTouch(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateHandAndTouch()"); // Update from Touch if (_isTouchPressed) { float TOUCH_YAW_SCALE = -0.25f; float TOUCH_PITCH_SCALE = -12.5f; float FIXED_TOUCH_TIMESTEP = 0.016f; _yawFromTouch += ((_touchAvgX - _lastTouchAvgX) * TOUCH_YAW_SCALE * FIXED_TOUCH_TIMESTEP); _pitchFromTouch += ((_touchAvgY - _lastTouchAvgY) * TOUCH_PITCH_SCALE * FIXED_TOUCH_TIMESTEP); _lastTouchAvgX = _touchAvgX; _lastTouchAvgY = _touchAvgY; } } void Application::updateLeap(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateLeap()"); } void Application::updateSixense(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateSixense()"); _sixenseManager.update(deltaTime); } void Application::updateSerialDevices(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateSerialDevices()"); } void Application::updateThreads(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateThreads()"); // parse voxel packets if (!_enableProcessVoxelsThread) { _voxelProcessor.threadRoutine(); _voxelHideShowThread.threadRoutine(); _voxelEditSender.threadRoutine(); _particleEditSender.threadRoutine(); if (_persistThread) { _persistThread->threadRoutine(); } } } void Application::updateMyAvatarSimulation(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMyAvatarSimulation()"); if (Menu::getInstance()->isOptionChecked(MenuOption::Gravity)) { _myAvatar.setGravity(_environment.getGravity(_myAvatar.getPosition())); } else { _myAvatar.setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); } if (Menu::getInstance()->isOptionChecked(MenuOption::TransmitterDrive) && _myTransmitter.isConnected()) { _myAvatar.simulate(deltaTime, &_myTransmitter); } else { _myAvatar.simulate(deltaTime, NULL); } } void Application::updateParticles(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateParticles()"); if (Menu::getInstance()->isOptionChecked(MenuOption::ParticleCloud)) { _cloud.simulate(deltaTime); } } void Application::updateMetavoxels(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMetavoxels()"); if (Menu::getInstance()->isOptionChecked(MenuOption::Metavoxels)) { _metavoxels.simulate(deltaTime); } } void Application::updateTransmitter(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateTransmitter()"); // no transmitter drive implies transmitter pick if (!Menu::getInstance()->isOptionChecked(MenuOption::TransmitterDrive) && _myTransmitter.isConnected()) { _transmitterPickStart = _myAvatar.getChestPosition(); glm::vec3 direction = _myAvatar.getOrientation() * glm::quat(glm::radians(_myTransmitter.getEstimatedRotation())) * IDENTITY_FRONT; // check against voxels, avatars const float MAX_PICK_DISTANCE = 100.0f; float minDistance = MAX_PICK_DISTANCE; VoxelDetail detail; float distance; BoxFace face; if (_voxels.findRayIntersection(_transmitterPickStart, direction, detail, distance, face)) { minDistance = min(minDistance, distance); } _transmitterPickEnd = _transmitterPickStart + direction * minDistance; } else { _transmitterPickStart = _transmitterPickEnd = glm::vec3(); } } void Application::updateCamera(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateCamera()"); if (!OculusManager::isConnected() && !TV3DManager::isConnected()) { if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { if (_myCamera.getMode() != CAMERA_MODE_MIRROR) { _myCamera.setMode(CAMERA_MODE_MIRROR); _myCamera.setModeShiftRate(100.0f); } } else if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); _myCamera.setModeShiftRate(1.0f); } } else { if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); _myCamera.setModeShiftRate(1.0f); } } if (Menu::getInstance()->isOptionChecked(MenuOption::OffAxisProjection)) { float xSign = _myCamera.getMode() == CAMERA_MODE_MIRROR ? 1.0f : -1.0f; if (_faceshift.isActive()) { const float EYE_OFFSET_SCALE = 0.025f; glm::vec3 position = _faceshift.getHeadTranslation() * EYE_OFFSET_SCALE; _myCamera.setEyeOffsetPosition(glm::vec3(position.x * xSign, position.y, -position.z)); updateProjectionMatrix(); } } } } void Application::updateDialogs(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); // Update bandwidth dialog, if any BandwidthDialog* bandwidthDialog = Menu::getInstance()->getBandwidthDialog(); if (bandwidthDialog) { bandwidthDialog->update(); } VoxelStatsDialog* voxelStatsDialog = Menu::getInstance()->getVoxelStatsDialog(); if (voxelStatsDialog) { voxelStatsDialog->update(); } } void Application::updateAudio(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateAudio()"); // Update audio stats for procedural sounds _audio.setLastAcceleration(_myAvatar.getThrust()); _audio.setLastVelocity(_myAvatar.getVelocity()); } void Application::updateCursor(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateCursor()"); // watch mouse position, if it hasn't moved, hide the cursor bool underMouse = _glWidget->underMouse(); if (!_mouseHidden) { uint64_t now = usecTimestampNow(); int elapsed = now - _lastMouseMove; const int HIDE_CURSOR_TIMEOUT = 1 * 1000 * 1000; // 1 second if (elapsed > HIDE_CURSOR_TIMEOUT && (underMouse || !_seenMouseMove)) { getGLWidget()->setCursor(Qt::BlankCursor); _mouseHidden = true; } } else { // if the mouse is hidden, but we're not inside our window, then consider ourselves to be moving if (!underMouse && _seenMouseMove) { _lastMouseMove = usecTimestampNow(); getGLWidget()->setCursor(Qt::ArrowCursor); _mouseHidden = false; } } } void Application::update(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::update()"); // check what's under the mouse and update the mouse voxel updateMouseRay(); // Set where I am looking based on my mouse ray (so that other people can see) glm::vec3 lookAtSpot; updateFaceshift(); _avatarManager.updateLookAtTargetAvatar(lookAtSpot); updateMyAvatarLookAtPosition(lookAtSpot); // Find the voxel we are hovering over, and respond if clicked float distance; BoxFace face; updateHoverVoxels(deltaTime, distance, face); // clicking on voxels and making sounds updateMouseVoxels(deltaTime, distance, face); // UI/UX related to voxels updateHandAndTouch(deltaTime); // Update state for touch sensors updateLeap(deltaTime); // Leap finger-sensing device updateSixense(deltaTime); // Razer Hydra controllers updateSerialDevices(deltaTime); // Read serial port interface devices updateAvatar(deltaTime); // Sample hardware, update view frustum if needed, and send avatar data to mixer/nodes updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... _avatarManager.updateAvatars(deltaTime); //loop through all the other avatars and simulate them... updateMyAvatarSimulation(deltaTime); // Simulate myself updateParticles(deltaTime); // Simulate particle cloud movements updateMetavoxels(deltaTime); // update metavoxels updateTransmitter(deltaTime); // transmitter drive or pick updateCamera(deltaTime); // handle various camera tweaks like off axis projection updateDialogs(deltaTime); // update various stats dialogs if present updateAudio(deltaTime); // Update audio stats for procedural sounds updateCursor(deltaTime); // Handle cursor updates _particles.update(); // update the particles... // collide the particles... QVector avatars; avatars.push_back(&_myAvatar); _avatarManager.getAvatarBasePointers(avatars); _particleCollisionSystem.update(avatars); } void Application::updateAvatar(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateAvatar()"); // rotate body yaw for yaw received from multitouch _myAvatar.setOrientation(_myAvatar.getOrientation() * glm::quat(glm::vec3(0, _yawFromTouch, 0))); _yawFromTouch = 0.f; // apply pitch from touch _myAvatar.getHead().setMousePitch(_myAvatar.getHead().getMousePitch() + _myAvatar.getHand().getPitchUpdate() + _pitchFromTouch); _myAvatar.getHand().setPitchUpdate(0.f); _pitchFromTouch = 0.0f; // Update my avatar's state from gyros _myAvatar.updateFromGyros(Menu::getInstance()->isOptionChecked(MenuOption::TurnWithHead)); // Update head mouse from faceshift if active if (_faceshift.isActive()) { glm::vec3 headVelocity = _faceshift.getHeadAngularVelocity(); // sets how quickly head angular rotation moves the head mouse const float HEADMOUSE_FACESHIFT_YAW_SCALE = 40.f; const float HEADMOUSE_FACESHIFT_PITCH_SCALE = 30.f; _headMouseX -= headVelocity.y * HEADMOUSE_FACESHIFT_YAW_SCALE; _headMouseY -= headVelocity.x * HEADMOUSE_FACESHIFT_PITCH_SCALE; } // Constrain head-driven mouse to edges of screen _headMouseX = glm::clamp(_headMouseX, 0, _glWidget->width()); _headMouseY = glm::clamp(_headMouseY, 0, _glWidget->height()); if (OculusManager::isConnected()) { float yaw, pitch, roll; OculusManager::getEulerAngles(yaw, pitch, roll); _myAvatar.getHead().setYaw(yaw); _myAvatar.getHead().setPitch(pitch); _myAvatar.getHead().setRoll(roll); } // Get audio loudness data from audio input device _myAvatar.getHead().setAudioLoudness(_audio.getLastInputLoudness()); // send head/hand data to the avatar mixer and voxel server unsigned char broadcastString[MAX_PACKET_SIZE]; unsigned char* endOfBroadcastStringWrite = broadcastString; endOfBroadcastStringWrite += populateTypeAndVersion(endOfBroadcastStringWrite, PACKET_TYPE_HEAD_DATA); // pack the NodeList owner UUID endOfBroadcastStringWrite += NodeList::getInstance()->packOwnerUUID(endOfBroadcastStringWrite); endOfBroadcastStringWrite += _myAvatar.getBroadcastData(endOfBroadcastStringWrite); controlledBroadcastToNodes(broadcastString, endOfBroadcastStringWrite - broadcastString, QSet() << NODE_TYPE_AVATAR_MIXER); // Update _viewFrustum with latest camera and view frustum data... // NOTE: we get this from the view frustum, to make it simpler, since the // loadViewFrumstum() method will get the correct details from the camera // We could optimize this to not actually load the viewFrustum, since we don't // actually need to calculate the view frustum planes to send these details // to the server. loadViewFrustum(_myCamera, _viewFrustum); // Update my voxel servers with my current voxel query... queryOctree(NODE_TYPE_VOXEL_SERVER, PACKET_TYPE_VOXEL_QUERY, _voxelServerJurisdictions); queryOctree(NODE_TYPE_PARTICLE_SERVER, PACKET_TYPE_PARTICLE_QUERY, _particleServerJurisdictions); } void Application::queryOctree(NODE_TYPE serverType, PACKET_TYPE packetType, NodeToJurisdictionMap& jurisdictions) { // if voxels are disabled, then don't send this at all... if (!Menu::getInstance()->isOptionChecked(MenuOption::Voxels)) { return; } bool wantExtraDebugging = getLogger()->extraDebugging(); // These will be the same for all servers, so we can set them up once and then reuse for each server we send to. _voxelQuery.setWantLowResMoving(!Menu::getInstance()->isOptionChecked(MenuOption::DisableLowRes)); _voxelQuery.setWantColor(!Menu::getInstance()->isOptionChecked(MenuOption::DisableColorVoxels)); _voxelQuery.setWantDelta(!Menu::getInstance()->isOptionChecked(MenuOption::DisableDeltaSending)); _voxelQuery.setWantOcclusionCulling(Menu::getInstance()->isOptionChecked(MenuOption::EnableOcclusionCulling)); _voxelQuery.setWantCompression(Menu::getInstance()->isOptionChecked(MenuOption::EnableVoxelPacketCompression)); _voxelQuery.setCameraPosition(_viewFrustum.getPosition()); _voxelQuery.setCameraOrientation(_viewFrustum.getOrientation()); _voxelQuery.setCameraFov(_viewFrustum.getFieldOfView()); _voxelQuery.setCameraAspectRatio(_viewFrustum.getAspectRatio()); _voxelQuery.setCameraNearClip(_viewFrustum.getNearClip()); _voxelQuery.setCameraFarClip(_viewFrustum.getFarClip()); _voxelQuery.setCameraEyeOffsetPosition(_viewFrustum.getEyeOffsetPosition()); _voxelQuery.setOctreeSizeScale(Menu::getInstance()->getVoxelSizeScale()); _voxelQuery.setBoundaryLevelAdjust(Menu::getInstance()->getBoundaryLevelAdjust()); unsigned char voxelQueryPacket[MAX_PACKET_SIZE]; // Iterate all of the nodes, and get a count of how many voxel servers we have... int totalServers = 0; int inViewServers = 0; int unknownJurisdictionServers = 0; foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { // only send to the NodeTypes that are serverType if (node->getActiveSocket() != NULL && node->getType() == serverType) { totalServers++; // get the server bounds for this server QUuid nodeUUID = node->getUUID(); // if we haven't heard from this voxel server, go ahead and send it a query, so we // can get the jurisdiction... if (jurisdictions.find(nodeUUID) == jurisdictions.end()) { unknownJurisdictionServers++; } else { const JurisdictionMap& map = (jurisdictions)[nodeUUID]; unsigned char* rootCode = map.getRootOctalCode(); if (rootCode) { VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode, rootDetails); AABox serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); serverBounds.scale(TREE_SCALE); ViewFrustum::location serverFrustumLocation = _viewFrustum.boxInFrustum(serverBounds); if (serverFrustumLocation != ViewFrustum::OUTSIDE) { inViewServers++; } } } } } if (wantExtraDebugging && unknownJurisdictionServers > 0) { qDebug("Servers: total %d, in view %d, unknown jurisdiction %d", totalServers, inViewServers, unknownJurisdictionServers); } int perServerPPS = 0; const int SMALL_BUDGET = 10; int perUnknownServer = SMALL_BUDGET; int totalPPS = Menu::getInstance()->getMaxVoxelPacketsPerSecond(); // determine PPS based on number of servers if (inViewServers >= 1) { // set our preferred PPS to be exactly evenly divided among all of the voxel servers... and allocate 1 PPS // for each unknown jurisdiction server perServerPPS = (totalPPS / inViewServers) - (unknownJurisdictionServers * perUnknownServer); } else { if (unknownJurisdictionServers > 0) { perUnknownServer = (totalPPS / unknownJurisdictionServers); } } if (wantExtraDebugging && unknownJurisdictionServers > 0) { qDebug("perServerPPS: %d perUnknownServer: %d", perServerPPS, perUnknownServer); } NodeList* nodeList = NodeList::getInstance(); foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { // only send to the NodeTypes that are serverType if (node->getActiveSocket() != NULL && node->getType() == serverType) { // get the server bounds for this server QUuid nodeUUID = node->getUUID(); bool inView = false; bool unknownView = false; // if we haven't heard from this voxel server, go ahead and send it a query, so we // can get the jurisdiction... if (jurisdictions.find(nodeUUID) == jurisdictions.end()) { unknownView = true; // assume it's in view if (wantExtraDebugging) { qDebug() << "no known jurisdiction for node " << *node << ", assume it's visible."; } } else { const JurisdictionMap& map = (jurisdictions)[nodeUUID]; unsigned char* rootCode = map.getRootOctalCode(); if (rootCode) { VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode, rootDetails); AABox serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); serverBounds.scale(TREE_SCALE); ViewFrustum::location serverFrustumLocation = _viewFrustum.boxInFrustum(serverBounds); if (serverFrustumLocation != ViewFrustum::OUTSIDE) { inView = true; } else { inView = false; } } else { if (wantExtraDebugging) { qDebug() << "Jurisdiction without RootCode for node " << *node << ". That's unusual!"; } } } if (inView) { _voxelQuery.setMaxOctreePacketsPerSecond(perServerPPS); } else if (unknownView) { if (wantExtraDebugging) { qDebug() << "no known jurisdiction for node " << *node << ", give it budget of " << perUnknownServer << " to send us jurisdiction."; } // set the query's position/orientation to be degenerate in a manner that will get the scene quickly // If there's only one server, then don't do this, and just let the normal voxel query pass through // as expected... this way, we will actually get a valid scene if there is one to be seen if (totalServers > 1) { _voxelQuery.setCameraPosition(glm::vec3(-0.1,-0.1,-0.1)); const glm::quat OFF_IN_NEGATIVE_SPACE = glm::quat(-0.5, 0, -0.5, 1.0); _voxelQuery.setCameraOrientation(OFF_IN_NEGATIVE_SPACE); _voxelQuery.setCameraNearClip(0.1f); _voxelQuery.setCameraFarClip(0.1f); if (wantExtraDebugging) { qDebug() << "Using 'minimal' camera position for node" << *node; } } else { if (wantExtraDebugging) { qDebug() << "Using regular camera position for node" << *node; } } _voxelQuery.setMaxOctreePacketsPerSecond(perUnknownServer); } else { _voxelQuery.setMaxOctreePacketsPerSecond(0); } // set up the packet for sending... unsigned char* endOfVoxelQueryPacket = voxelQueryPacket; // insert packet type/version and node UUID endOfVoxelQueryPacket += populateTypeAndVersion(endOfVoxelQueryPacket, packetType); QByteArray ownerUUID = nodeList->getOwnerUUID().toRfc4122(); memcpy(endOfVoxelQueryPacket, ownerUUID.constData(), ownerUUID.size()); endOfVoxelQueryPacket += ownerUUID.size(); // encode the query data... endOfVoxelQueryPacket += _voxelQuery.getBroadcastData(endOfVoxelQueryPacket); int packetLength = endOfVoxelQueryPacket - voxelQueryPacket; // make sure we still have an active socket if (node->getActiveSocket()) { nodeList->getNodeSocket().writeDatagram((char*) voxelQueryPacket, packetLength, node->getActiveSocket()->getAddress(), node->getActiveSocket()->getPort()); } // Feed number of bytes to corresponding channel of the bandwidth meter _bandwidthMeter.outputStream(BandwidthMeter::VOXELS).updateValue(packetLength); } } } ///////////////////////////////////////////////////////////////////////////////////// // loadViewFrustum() // // Description: this will load the view frustum bounds for EITHER the head // or the "myCamera". // void Application::loadViewFrustum(Camera& camera, ViewFrustum& viewFrustum) { // We will use these below, from either the camera or head vectors calculated above glm::vec3 position(camera.getPosition()); float fov = camera.getFieldOfView(); float nearClip = camera.getNearClip(); float farClip = camera.getFarClip(); float aspectRatio = camera.getAspectRatio(); glm::quat rotation = camera.getRotation(); // Set the viewFrustum up with the correct position and orientation of the camera viewFrustum.setPosition(position); viewFrustum.setOrientation(rotation); // Also make sure it's got the correct lens details from the camera viewFrustum.setAspectRatio(aspectRatio); viewFrustum.setFieldOfView(fov); viewFrustum.setNearClip(nearClip); viewFrustum.setFarClip(farClip); viewFrustum.setEyeOffsetPosition(camera.getEyeOffsetPosition()); viewFrustum.setEyeOffsetOrientation(camera.getEyeOffsetOrientation()); // Ask the ViewFrustum class to calculate our corners viewFrustum.calculate(); } glm::vec3 Application::getSunDirection() { return glm::normalize(_environment.getClosestData(_myCamera.getPosition()).getSunLocation() - _myCamera.getPosition()); } void Application::updateShadowMap() { QOpenGLFramebufferObject* fbo = _textureCache.getShadowFramebufferObject(); fbo->bind(); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0, 0, fbo->width(), fbo->height()); glm::vec3 lightDirection = -getSunDirection(); glm::quat rotation = glm::inverse(rotationBetween(IDENTITY_FRONT, lightDirection)); glm::vec3 translation = glm::vec3(); float nearScale = 0.0f; const float MAX_SHADOW_DISTANCE = 2.0f; float farScale = (MAX_SHADOW_DISTANCE - _viewFrustum.getNearClip()) / (_viewFrustum.getFarClip() - _viewFrustum.getNearClip()); loadViewFrustum(_myCamera, _viewFrustum); glm::vec3 points[] = { rotation * (glm::mix(_viewFrustum.getNearTopLeft(), _viewFrustum.getFarTopLeft(), nearScale) + translation), rotation * (glm::mix(_viewFrustum.getNearTopRight(), _viewFrustum.getFarTopRight(), nearScale) + translation), rotation * (glm::mix(_viewFrustum.getNearBottomLeft(), _viewFrustum.getFarBottomLeft(), nearScale) + translation), rotation * (glm::mix(_viewFrustum.getNearBottomRight(), _viewFrustum.getFarBottomRight(), nearScale) + translation), rotation * (glm::mix(_viewFrustum.getNearTopLeft(), _viewFrustum.getFarTopLeft(), farScale) + translation), rotation * (glm::mix(_viewFrustum.getNearTopRight(), _viewFrustum.getFarTopRight(), farScale) + translation), rotation * (glm::mix(_viewFrustum.getNearBottomLeft(), _viewFrustum.getFarBottomLeft(), farScale) + translation), rotation * (glm::mix(_viewFrustum.getNearBottomRight(), _viewFrustum.getFarBottomRight(), farScale) + translation) }; glm::vec3 minima(FLT_MAX, FLT_MAX, FLT_MAX), maxima(-FLT_MAX, -FLT_MAX, -FLT_MAX); for (int i = 0; i < sizeof(points) / sizeof(points[0]); i++) { minima = glm::min(minima, points[i]); maxima = glm::max(maxima, points[i]); } // stretch out our extents in z so that we get all of the avatars minima.z -= _viewFrustum.getFarClip() * 0.5f; maxima.z += _viewFrustum.getFarClip() * 0.5f; // save the combined matrix for rendering _shadowMatrix = glm::transpose(glm::translate(0.5f, 0.5f, 0.5f) * glm::scale(0.5f, 0.5f, 0.5f) * glm::ortho(minima.x, maxima.x, minima.y, maxima.y, -maxima.z, -minima.z) * glm::mat4_cast(rotation) * glm::translate(translation)); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(minima.x, maxima.x, minima.y, maxima.y, -maxima.z, -minima.z); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); glm::vec3 axis = glm::axis(rotation); glRotatef(glm::angle(rotation), axis.x, axis.y, axis.z); // store view matrix without translation, which we'll use for precision-sensitive objects glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat*)&_untranslatedViewMatrix); _viewMatrixTranslation = translation; glTranslatef(translation.x, translation.y, translation.z); _avatarManager.renderAvatars(true); _particles.render(); glPopMatrix(); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); fbo->release(); glViewport(0, 0, _glWidget->width(), _glWidget->height()); } const GLfloat WHITE_SPECULAR_COLOR[] = { 1.0f, 1.0f, 1.0f, 1.0f }; const GLfloat NO_SPECULAR_COLOR[] = { 0.0f, 0.0f, 0.0f, 1.0f }; void Application::setupWorldLight() { // Setup 3D lights (after the camera transform, so that they are positioned in world space) glEnable(GL_COLOR_MATERIAL); glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); glm::vec3 sunDirection = getSunDirection(); GLfloat light_position0[] = { sunDirection.x, sunDirection.y, sunDirection.z, 0.0 }; glLightfv(GL_LIGHT0, GL_POSITION, light_position0); GLfloat ambient_color[] = { 0.7f, 0.7f, 0.8f }; glLightfv(GL_LIGHT0, GL_AMBIENT, ambient_color); GLfloat diffuse_color[] = { 0.8f, 0.7f, 0.7f }; glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse_color); glLightfv(GL_LIGHT0, GL_SPECULAR, WHITE_SPECULAR_COLOR); glMaterialfv(GL_FRONT, GL_SPECULAR, WHITE_SPECULAR_COLOR); glMateriali(GL_FRONT, GL_SHININESS, 96); } void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide()"); // transform by eye offset // flip x if in mirror mode (also requires reversing winding order for backface culling) if (whichCamera.getMode() == CAMERA_MODE_MIRROR) { glScalef(-1.0f, 1.0f, 1.0f); glFrontFace(GL_CW); } else { glFrontFace(GL_CCW); } glm::vec3 eyeOffsetPos = whichCamera.getEyeOffsetPosition(); glm::quat eyeOffsetOrient = whichCamera.getEyeOffsetOrientation(); glm::vec3 eyeOffsetAxis = glm::axis(eyeOffsetOrient); glRotatef(-glm::angle(eyeOffsetOrient), eyeOffsetAxis.x, eyeOffsetAxis.y, eyeOffsetAxis.z); glTranslatef(-eyeOffsetPos.x, -eyeOffsetPos.y, -eyeOffsetPos.z); // transform view according to whichCamera // could be myCamera (if in normal mode) // or could be viewFrustumOffsetCamera if in offset mode glm::quat rotation = whichCamera.getRotation(); glm::vec3 axis = glm::axis(rotation); glRotatef(-glm::angle(rotation), axis.x, axis.y, axis.z); // store view matrix without translation, which we'll use for precision-sensitive objects glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat*)&_untranslatedViewMatrix); _viewMatrixTranslation = -whichCamera.getPosition(); glTranslatef(_viewMatrixTranslation.x, _viewMatrixTranslation.y, _viewMatrixTranslation.z); // Setup 3D lights (after the camera transform, so that they are positioned in world space) setupWorldLight(); if (!selfAvatarOnly && Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... stars..."); if (!_stars.isStarsLoaded()) { _stars.generate(STARFIELD_NUM_STARS, STARFIELD_SEED); } // should be the first rendering pass - w/o depth buffer / lighting // compute starfield alpha based on distance from atmosphere float alpha = 1.0f; if (Menu::getInstance()->isOptionChecked(MenuOption::Atmosphere)) { const EnvironmentData& closestData = _environment.getClosestData(whichCamera.getPosition()); float height = glm::distance(whichCamera.getPosition(), closestData.getAtmosphereCenter()); if (height < closestData.getAtmosphereInnerRadius()) { alpha = 0.0f; } else if (height < closestData.getAtmosphereOuterRadius()) { alpha = (height - closestData.getAtmosphereInnerRadius()) / (closestData.getAtmosphereOuterRadius() - closestData.getAtmosphereInnerRadius()); } } // finally render the starfield _stars.render(whichCamera.getFieldOfView(), whichCamera.getAspectRatio(), whichCamera.getNearClip(), alpha); } // draw the sky dome if (!selfAvatarOnly && Menu::getInstance()->isOptionChecked(MenuOption::Atmosphere)) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... atmosphere..."); _environment.renderAtmospheres(whichCamera); } glEnable(GL_LIGHTING); glEnable(GL_DEPTH_TEST); // Enable to show line from me to the voxel I am touching //renderThrustAtVoxel(_voxelThrust); if (!selfAvatarOnly) { // draw a red sphere float originSphereRadius = 0.05f; glColor3f(1,0,0); glPushMatrix(); glutSolidSphere(originSphereRadius, 15, 15); glPopMatrix(); // disable specular lighting for ground and voxels glMaterialfv(GL_FRONT, GL_SPECULAR, NO_SPECULAR_COLOR); // Draw Cloud Particles if (Menu::getInstance()->isOptionChecked(MenuOption::ParticleCloud)) { _cloud.render(); } // Draw voxels if (Menu::getInstance()->isOptionChecked(MenuOption::Voxels)) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... voxels..."); if (!Menu::getInstance()->isOptionChecked(MenuOption::DontRenderVoxels)) { _voxels.render(Menu::getInstance()->isOptionChecked(MenuOption::VoxelTextures)); } } // also, metavoxels if (Menu::getInstance()->isOptionChecked(MenuOption::Metavoxels)) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... metavoxels..."); _metavoxels.render(); } // render particles... _particles.render(); // render the ambient occlusion effect if enabled if (Menu::getInstance()->isOptionChecked(MenuOption::AmbientOcclusion)) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... AmbientOcclusion..."); _ambientOcclusionEffect.render(); } // restore default, white specular glMaterialfv(GL_FRONT, GL_SPECULAR, WHITE_SPECULAR_COLOR); // Render the highlighted voxel if (_isHighlightVoxel) { renderHighlightVoxel(_highlightVoxel); } // indicate what we'll be adding/removing in mouse mode, if anything if (_mouseVoxel.s != 0 && whichCamera.getMode() != CAMERA_MODE_MIRROR) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... voxels TOOLS UX..."); glDisable(GL_LIGHTING); glPushMatrix(); glScalef(TREE_SCALE, TREE_SCALE, TREE_SCALE); const float CUBE_EXPANSION = 1.01f; if (_nudgeStarted) { renderNudgeGuide(_nudgeGuidePosition.x, _nudgeGuidePosition.y, _nudgeGuidePosition.z, _nudgeVoxel.s); renderNudgeGrid(_nudgeVoxel.x, _nudgeVoxel.y, _nudgeVoxel.z, _nudgeVoxel.s, _mouseVoxel.s); glPushMatrix(); glTranslatef(_nudgeVoxel.x + _nudgeVoxel.s * 0.5f, _nudgeVoxel.y + _nudgeVoxel.s * 0.5f, _nudgeVoxel.z + _nudgeVoxel.s * 0.5f); glColor3ub(255, 255, 255); glLineWidth(4.0f); glutWireCube(_nudgeVoxel.s * CUBE_EXPANSION); glPopMatrix(); } else { renderMouseVoxelGrid(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); } if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelAddMode)) { // use a contrasting color so that we can see what we're doing glColor3ub(_mouseVoxel.red + 128, _mouseVoxel.green + 128, _mouseVoxel.blue + 128); } else { glColor3ub(_mouseVoxel.red, _mouseVoxel.green, _mouseVoxel.blue); } if (_nudgeStarted) { // render nudge guide cube glTranslatef(_nudgeGuidePosition.x + _nudgeVoxel.s*0.5f, _nudgeGuidePosition.y + _nudgeVoxel.s*0.5f, _nudgeGuidePosition.z + _nudgeVoxel.s*0.5f); glLineWidth(4.0f); glutWireCube(_nudgeVoxel.s * CUBE_EXPANSION); } else { glTranslatef(_mouseVoxel.x + _mouseVoxel.s*0.5f, _mouseVoxel.y + _mouseVoxel.s*0.5f, _mouseVoxel.z + _mouseVoxel.s*0.5f); glLineWidth(4.0f); glutWireCube(_mouseVoxel.s * CUBE_EXPANSION); } glLineWidth(1.0f); glPopMatrix(); glEnable(GL_LIGHTING); } if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelSelectMode) && _pasteMode && whichCamera.getMode() != CAMERA_MODE_MIRROR) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... PASTE Preview..."); glPushMatrix(); glTranslatef(_mouseVoxel.x * TREE_SCALE, _mouseVoxel.y * TREE_SCALE, _mouseVoxel.z * TREE_SCALE); glScalef(_mouseVoxel.s, _mouseVoxel.s, _mouseVoxel.s); _sharedVoxelSystem.render(true); glPopMatrix(); } } _avatarManager.renderAvatars(whichCamera.getMode() == CAMERA_MODE_MIRROR, selfAvatarOnly); if (!selfAvatarOnly) { // Render the world box if (whichCamera.getMode() != CAMERA_MODE_MIRROR && Menu::getInstance()->isOptionChecked(MenuOption::Stats)) { renderWorldBox(); } // brad's frustum for debugging if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayFrustum) && whichCamera.getMode() != CAMERA_MODE_MIRROR) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... renderViewFrustum..."); renderViewFrustum(_viewFrustum); } // render voxel fades if they exist if (_voxelFades.size() > 0) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... voxel fades..."); for(std::vector::iterator fade = _voxelFades.begin(); fade != _voxelFades.end();) { fade->render(); if(fade->isDone()) { fade = _voxelFades.erase(fade); } else { ++fade; } } } // render transmitter pick ray, if non-empty if (_transmitterPickStart != _transmitterPickEnd) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... transmitter pick ray..."); Glower glower; const float TRANSMITTER_PICK_COLOR[] = { 1.0f, 1.0f, 0.0f }; glColor3fv(TRANSMITTER_PICK_COLOR); glLineWidth(3.0f); glBegin(GL_LINES); glVertex3f(_transmitterPickStart.x, _transmitterPickStart.y, _transmitterPickStart.z); glVertex3f(_transmitterPickEnd.x, _transmitterPickEnd.y, _transmitterPickEnd.z); glEnd(); glLineWidth(1.0f); glPushMatrix(); glTranslatef(_transmitterPickEnd.x, _transmitterPickEnd.y, _transmitterPickEnd.z); const float PICK_END_RADIUS = 0.025f; glutSolidSphere(PICK_END_RADIUS, 8, 8); glPopMatrix(); } // give external parties a change to hook in emit renderingInWorldInterface(); } } void Application::loadTranslatedViewMatrix(const glm::vec3& translation) { glLoadMatrixf((const GLfloat*)&_untranslatedViewMatrix); glTranslatef(translation.x + _viewMatrixTranslation.x, translation.y + _viewMatrixTranslation.y, translation.z + _viewMatrixTranslation.z); } void Application::computeOffAxisFrustum(float& left, float& right, float& bottom, float& top, float& nearVal, float& farVal, glm::vec4& nearClipPlane, glm::vec4& farClipPlane) const { _viewFrustum.computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); } void Application::displayOverlay() { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displayOverlay()"); // Render 2D overlay: I/O level bar graphs and text glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); gluOrtho2D(0, _glWidget->width(), _glWidget->height(), 0); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); // Display a single screen-size quad to create an alpha blended 'collision' flash if (_audio.getCollisionFlashesScreen()) { float collisionSoundMagnitude = _audio.getCollisionSoundMagnitude(); const float VISIBLE_COLLISION_SOUND_MAGNITUDE = 0.5f; if (collisionSoundMagnitude > VISIBLE_COLLISION_SOUND_MAGNITUDE) { renderCollisionOverlay(_glWidget->width(), _glWidget->height(), _audio.getCollisionSoundMagnitude()); } } if (Menu::getInstance()->isOptionChecked(MenuOption::Stats)) { displayStatsBackground(0x33333399, 0, _glWidget->height() - 68, 296, 68); _audio.render(_glWidget->width(), _glWidget->height()); if (Menu::getInstance()->isOptionChecked(MenuOption::Oscilloscope)) { int oscilloscopeTop = Menu::getInstance()->isOptionChecked(MenuOption::Mirror) ? 130 : 25; _audioScope.render(25, oscilloscopeTop); } } //noiseTest(_glWidget->width(), _glWidget->height()); if (Menu::getInstance()->isOptionChecked(MenuOption::HeadMouse)) { // Display small target box at center or head mouse target that can also be used to measure LOD glColor3f(1.0, 1.0, 1.0); glDisable(GL_LINE_SMOOTH); const int PIXEL_BOX = 16; glBegin(GL_LINES); glVertex2f(_headMouseX - PIXEL_BOX/2, _headMouseY); glVertex2f(_headMouseX + PIXEL_BOX/2, _headMouseY); glVertex2f(_headMouseX, _headMouseY - PIXEL_BOX/2); glVertex2f(_headMouseX, _headMouseY + PIXEL_BOX/2); glEnd(); glEnable(GL_LINE_SMOOTH); glColor3f(1.f, 0.f, 0.f); glPointSize(3.0f); glDisable(GL_POINT_SMOOTH); glBegin(GL_POINTS); glVertex2f(_headMouseX - 1, _headMouseY + 1); glEnd(); // If Faceshift is active, show eye pitch and yaw as separate pointer if (_faceshift.isActive()) { const float EYE_TARGET_PIXELS_PER_DEGREE = 40.0; int eyeTargetX = (_glWidget->width() / 2) - _faceshift.getEstimatedEyeYaw() * EYE_TARGET_PIXELS_PER_DEGREE; int eyeTargetY = (_glWidget->height() / 2) - _faceshift.getEstimatedEyePitch() * EYE_TARGET_PIXELS_PER_DEGREE; glColor3f(0.0, 1.0, 1.0); glDisable(GL_LINE_SMOOTH); glBegin(GL_LINES); glVertex2f(eyeTargetX - PIXEL_BOX/2, eyeTargetY); glVertex2f(eyeTargetX + PIXEL_BOX/2, eyeTargetY); glVertex2f(eyeTargetX, eyeTargetY - PIXEL_BOX/2); glVertex2f(eyeTargetX, eyeTargetY + PIXEL_BOX/2); glEnd(); } } // Show hand transmitter data if detected if (_myTransmitter.isConnected()) { _myTransmitter.renderLevels(_glWidget->width(), _glWidget->height()); } // Display stats and log text onscreen glLineWidth(1.0f); glPointSize(1.0f); if (Menu::getInstance()->isOptionChecked(MenuOption::Stats)) { // Onscreen text about position, servers, etc displayStats(); // Bandwidth meter if (Menu::getInstance()->isOptionChecked(MenuOption::Bandwidth)) { displayStatsBackground(0x33333399, _glWidget->width() - 296, _glWidget->height() - 68, 296, 68); _bandwidthMeter.render(_glWidget->width(), _glWidget->height()); } } // testing rendering coverage map if (Menu::getInstance()->isOptionChecked(MenuOption::CoverageMapV2)) { renderCoverageMapV2(); } if (Menu::getInstance()->isOptionChecked(MenuOption::CoverageMap)) { renderCoverageMap(); } // Show chat entry field if (_chatEntryOn) { _chatEntry.render(_glWidget->width(), _glWidget->height()); } // Show on-screen msec timer if (Menu::getInstance()->isOptionChecked(MenuOption::FrameTimer)) { char frameTimer[10]; uint64_t mSecsNow = floor(usecTimestampNow() / 1000.0 + 0.5); sprintf(frameTimer, "%d\n", (int)(mSecsNow % 1000)); int timerBottom = (Menu::getInstance()->isOptionChecked(MenuOption::Stats) && Menu::getInstance()->isOptionChecked(MenuOption::Bandwidth)) ? 80 : 20; drawtext(_glWidget->width() - 100, _glWidget->height() - timerBottom, 0.30f, 0, 1.0f, 0, frameTimer, 0, 0, 0); drawtext(_glWidget->width() - 102, _glWidget->height() - timerBottom - 2, 0.30f, 0, 1.0f, 0, frameTimer, 1, 1, 1); } _palette.render(_glWidget->width(), _glWidget->height()); QAction* paintColorAction = NULL; if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelGetColorMode) && (paintColorAction = Menu::getInstance()->getActionForOption(MenuOption::VoxelPaintColor))->data().value() != _swatch.getColor()) { QColor color = paintColorAction->data().value(); TextRenderer textRenderer(SANS_FONT_FAMILY, 11, 50); const char line1[] = "Assign this color to a swatch"; const char line2[] = "by choosing a key from 1 to 8."; int left = (_glWidget->width() - POPUP_WIDTH - 2 * POPUP_MARGIN) / 2; int top = _glWidget->height() / 40; glBegin(GL_POLYGON); glColor3f(0.0f, 0.0f, 0.0f); for (double a = M_PI; a < 1.5f * M_PI; a += POPUP_STEP) { glVertex2f(left + POPUP_MARGIN * cos(a) , top + POPUP_MARGIN * sin(a)); } for (double a = 1.5f * M_PI; a < 2.0f * M_PI; a += POPUP_STEP) { glVertex2f(left + POPUP_WIDTH + POPUP_MARGIN * cos(a), top + POPUP_MARGIN * sin(a)); } for (double a = 0.0f; a < 0.5f * M_PI; a += POPUP_STEP) { glVertex2f(left + POPUP_WIDTH + POPUP_MARGIN * cos(a), top + POPUP_HEIGHT + POPUP_MARGIN * sin(a)); } for (double a = 0.5f * M_PI; a < 1.0f * M_PI; a += POPUP_STEP) { glVertex2f(left + POPUP_MARGIN * cos(a) , top + POPUP_HEIGHT + POPUP_MARGIN * sin(a)); } glEnd(); glBegin(GL_QUADS); glColor3f(color.redF(), color.greenF(), color.blueF()); glVertex2f(left , top); glVertex2f(left + SWATCH_WIDTH, top); glVertex2f(left + SWATCH_WIDTH, top + SWATCH_HEIGHT); glVertex2f(left , top + SWATCH_HEIGHT); glEnd(); glColor3f(1.0f, 1.0f, 1.0f); textRenderer.draw(left + SWATCH_WIDTH + POPUP_MARGIN, top + FIRST_LINE_OFFSET , line1); textRenderer.draw(left + SWATCH_WIDTH + POPUP_MARGIN, top + SECOND_LINE_OFFSET, line2); } else { _swatch.checkColor(); } if (_pieMenu.isDisplayed()) { _pieMenu.render(); } glPopMatrix(); } // translucent background box that makes stats more readable void Application::displayStatsBackground(unsigned int rgba, int x, int y, int width, int height) { glBegin(GL_QUADS); glColor4f(((rgba >> 24) & 0xff) / 255.0f, ((rgba >> 16) & 0xff) / 255.0f, ((rgba >> 8) & 0xff) / 255.0f, (rgba & 0xff) / 255.0f); glVertex3f(x, y, 0); glVertex3f(x + width, y, 0); glVertex3f(x + width, y + height, 0); glVertex3f(x , y + height, 0); glEnd(); glColor4f(1, 1, 1, 1); } // display expanded or contracted stats void Application::displayStats() { unsigned int backgroundColor = 0x33333399; int verticalOffset = 0, horizontalOffset = 0, lines = 0; bool mirrorEnabled = Menu::getInstance()->isOptionChecked(MenuOption::Mirror); QLocale locale(QLocale::English); std::stringstream voxelStats; glPointSize(1.0f); int totalAvatars = 0, totalServers = 0; foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { node->getType() == NODE_TYPE_AGENT ? totalAvatars++ : totalServers++; } if (mirrorEnabled) { horizontalOffset += MIRROR_VIEW_WIDTH + MIRROR_VIEW_LEFT_PADDING * 2; } lines = _statsExpanded ? 5 : 3; displayStatsBackground(backgroundColor, horizontalOffset, 0, 165, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; char serverNodes[30]; sprintf(serverNodes, "Servers: %d", totalServers); char avatarNodes[30]; sprintf(avatarNodes, "Avatars: %d", totalAvatars); char framesPerSecond[30]; sprintf(framesPerSecond, "Framerate: %3.0f FPS", _fps); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0f, 2, serverNodes, .93f, .93f, .93f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0f, 2, avatarNodes, .93f, .93f, .93f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, framesPerSecond, .93f, .93f, .93f); if (_statsExpanded) { char packetsPerSecond[30]; sprintf(packetsPerSecond, "Pkts/sec: %d", _packetsPerSecond); char averageMegabitsPerSecond[30]; sprintf(averageMegabitsPerSecond, "Avg Mbps: %3.2f", (float)_bytesPerSecond * 8.f / 1000000.f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, packetsPerSecond, .93f, .93f, .93f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, averageMegabitsPerSecond, .93f, .93f, .93f); } verticalOffset = 0; horizontalOffset += 161; if (Menu::getInstance()->isOptionChecked(MenuOption::TestPing)) { int pingAudio = 0, pingAvatar = 0, pingVoxel = 0, pingVoxelMax = 0; NodeList* nodeList = NodeList::getInstance(); SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NODE_TYPE_AUDIO_MIXER); SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NODE_TYPE_AVATAR_MIXER); pingAudio = audioMixerNode ? audioMixerNode->getPingMs() : 0; pingAvatar = avatarMixerNode ? avatarMixerNode->getPingMs() : 0; // Now handle voxel servers, since there could be more than one, we average their ping times unsigned long totalPingVoxel = 0; int voxelServerCount = 0; foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getType() == NODE_TYPE_VOXEL_SERVER) { totalPingVoxel += node->getPingMs(); voxelServerCount++; if (pingVoxelMax < node->getPingMs()) { pingVoxelMax = node->getPingMs(); } } } if (voxelServerCount) { pingVoxel = totalPingVoxel/voxelServerCount; } lines = _statsExpanded ? 4 : 3; displayStatsBackground(backgroundColor, horizontalOffset, 0, 175, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; char audioPing[30]; sprintf(audioPing, "Audio ping: %d", pingAudio); char avatarPing[30]; sprintf(avatarPing, "Avatar ping: %d", pingAvatar); char voxelAvgPing[30]; sprintf(voxelAvgPing, "Voxel avg ping: %d", pingVoxel); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, audioPing, .93f, .93f, .93f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, avatarPing, .93f, .93f, .93f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, voxelAvgPing, .93f, .93f, .93f); if (_statsExpanded) { char voxelMaxPing[30]; sprintf(voxelMaxPing, "Voxel max ping: %d", pingVoxelMax); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, voxelMaxPing, .93f, .93f, .93f); } verticalOffset = 0; horizontalOffset += 171; } glm::vec3 avatarPos = _myAvatar.getPosition(); lines = _statsExpanded ? 4 : 3; displayStatsBackground(backgroundColor, horizontalOffset, 0, _glWidget->width() - (mirrorEnabled ? 301 : 411) - horizontalOffset, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; char avatarPosition[200]; if (mirrorEnabled) { // shorthand formatting sprintf(avatarPosition, "Pos: %.0f,%.0f,%.0f", avatarPos.x, avatarPos.y, avatarPos.z); } else { // longhand way sprintf(avatarPosition, "Position: %.3f, %.3f, %.3f", avatarPos.x, avatarPos.y, avatarPos.z); } char avatarVelocity[30]; sprintf(avatarVelocity, "Velocity: %.1f", glm::length(_myAvatar.getVelocity())); char avatarBodyYaw[30]; sprintf(avatarBodyYaw, "Yaw: %.2f", _myAvatar.getBodyYaw()); char avatarMixerStats[200]; verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, avatarPosition, .93f, .93f, .93f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, avatarVelocity, .93f, .93f, .93f); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, avatarBodyYaw, .93f, .93f, .93f); if (_statsExpanded) { SharedNodePointer avatarMixer = NodeList::getInstance()->soloNodeOfType(NODE_TYPE_AVATAR_MIXER); if (avatarMixer) { sprintf(avatarMixerStats, "Avatar Mixer: %.f kbps, %.f pps", roundf(avatarMixer->getAverageKilobitsPerSecond()), roundf(avatarMixer->getAveragePacketsPerSecond())); } else { sprintf(avatarMixerStats, "No Avatar Mixer"); } verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, avatarMixerStats, .93f, .93f, .93f); } verticalOffset = 0; horizontalOffset = _glWidget->width() - (mirrorEnabled ? 300 : 410); lines = _statsExpanded ? 11 : 3; displayStatsBackground(backgroundColor, horizontalOffset, 0, _glWidget->width() - horizontalOffset, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; if (_statsExpanded) { // Local Voxel Memory Usage voxelStats.str(""); voxelStats << "Voxels Memory Nodes: " << VoxelTreeElement::getTotalMemoryUsage() / 1000000.f << "MB"; verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); voxelStats.str(""); voxelStats << "Geometry RAM: " << _voxels.getVoxelMemoryUsageRAM() / 1000000.f << "MB / " << "VBO: " << _voxels.getVoxelMemoryUsageVBO() / 1000000.f << "MB"; if (_voxels.hasVoxelMemoryUsageGPU()) { voxelStats << " / GPU: " << _voxels.getVoxelMemoryUsageGPU() / 1000000.f << "MB"; } verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); // Voxel Rendering voxelStats.str(""); voxelStats.precision(4); voxelStats << "Voxel Rendering Slots Max: " << _voxels.getMaxVoxels() / 1000.f << "K"; verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); } voxelStats.str(""); voxelStats.precision(4); voxelStats << "Drawn: " << _voxels.getVoxelsWritten() / 1000.f << "K " << "Abandoned: " << _voxels.getAbandonedVoxels() / 1000.f << "K "; verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); // iterate all the current voxel stats, and list their sending modes, and total voxel counts std::stringstream sendingMode(""); sendingMode << "Octree Sending Mode: ["; int serverCount = 0; int movingServerCount = 0; unsigned long totalNodes = 0; unsigned long totalInternal = 0; unsigned long totalLeaves = 0; for(NodeToVoxelSceneStatsIterator i = _octreeServerSceneStats.begin(); i != _octreeServerSceneStats.end(); i++) { //const QUuid& uuid = i->first; VoxelSceneStats& stats = i->second; serverCount++; if (_statsExpanded) { if (serverCount > 1) { sendingMode << ","; } if (stats.isMoving()) { sendingMode << "M"; movingServerCount++; } else { sendingMode << "S"; } } // calculate server node totals totalNodes += stats.getTotalElements(); if (_statsExpanded) { totalInternal += stats.getTotalInternal(); totalLeaves += stats.getTotalLeaves(); } } if (_statsExpanded) { if (serverCount == 0) { sendingMode << "---"; } sendingMode << "] " << serverCount << " servers"; if (movingServerCount > 0) { sendingMode << " "; } else { sendingMode << " "; } verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)sendingMode.str().c_str(), .93f, .93f, .93f); } // Incoming packets int voxelPacketsToProcess = _voxelProcessor.packetsToProcessCount(); if (_statsExpanded) { voxelStats.str(""); QString packetsString = locale.toString((int)voxelPacketsToProcess); QString maxString = locale.toString((int)_recentMaxPackets); voxelStats << "Voxel Packets to Process: " << packetsString.toLocal8Bit().constData() << " [Recent Max: " << maxString.toLocal8Bit().constData() << "]"; verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); } if (_resetRecentMaxPacketsSoon && voxelPacketsToProcess > 0) { _recentMaxPackets = 0; _resetRecentMaxPacketsSoon = false; } if (voxelPacketsToProcess == 0) { _resetRecentMaxPacketsSoon = true; } else { if (voxelPacketsToProcess > _recentMaxPackets) { _recentMaxPackets = voxelPacketsToProcess; } } verticalOffset += (_statsExpanded ? STATS_PELS_PER_LINE : 0); QString serversTotalString = locale.toString((uint)totalNodes); // consider adding: .rightJustified(10, ' '); // Server Voxels voxelStats.str(""); voxelStats << "Server voxels: " << serversTotalString.toLocal8Bit().constData(); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); if (_statsExpanded) { QString serversInternalString = locale.toString((uint)totalInternal); QString serversLeavesString = locale.toString((uint)totalLeaves); voxelStats.str(""); voxelStats << "Internal: " << serversInternalString.toLocal8Bit().constData() << " " << "Leaves: " << serversLeavesString.toLocal8Bit().constData() << ""; verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); } unsigned long localTotal = VoxelTreeElement::getNodeCount(); QString localTotalString = locale.toString((uint)localTotal); // consider adding: .rightJustified(10, ' '); // Local Voxels voxelStats.str(""); voxelStats << "Local voxels: " << localTotalString.toLocal8Bit().constData(); verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); if (_statsExpanded) { unsigned long localInternal = VoxelTreeElement::getInternalNodeCount(); unsigned long localLeaves = VoxelTreeElement::getLeafNodeCount(); QString localInternalString = locale.toString((uint)localInternal); QString localLeavesString = locale.toString((uint)localLeaves); voxelStats.str(""); voxelStats << "Internal: " << localInternalString.toLocal8Bit().constData() << " " << "Leaves: " << localLeavesString.toLocal8Bit().constData() << ""; verticalOffset += STATS_PELS_PER_LINE; drawtext(horizontalOffset, verticalOffset, 0.10f, 0, 1.0, 2, (char*)voxelStats.str().c_str(), .93f, .93f, .93f); } } // called on mouse click release // check for clicks over stats in order to expand or contract them void Application::checkStatsClick() { if (0 != glm::compMax(glm::abs(glm::ivec2(_mouseX - _mouseDragStartedX, _mouseY - _mouseDragStartedY)))) { // not worried about dragging on stats return; } int statsHeight = 0, statsWidth = 0, statsX = 0, statsY = 0, lines = 0; if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { statsX += MIRROR_VIEW_WIDTH; } // top-left stats click lines = _statsExpanded ? 5 : 3; statsHeight = lines * STATS_PELS_PER_LINE + 10; statsWidth = 165; if (_mouseX > statsX && _mouseX < statsX + statsWidth && _mouseY > statsY && _mouseY < statsY + statsHeight) { toggleStatsExpanded(); return; } // ping stats click lines = _statsExpanded ? 4 : 3; statsX += statsWidth; statsHeight = lines * STATS_PELS_PER_LINE + 10; statsWidth = 175; if (_mouseX > statsX && _mouseX < statsX + statsWidth && _mouseY > statsY && _mouseY < statsY + statsHeight) { toggleStatsExpanded(); return; } // top-center stats panel click lines = _statsExpanded ? 4 : 3; statsX += statsWidth; statsHeight = lines * STATS_PELS_PER_LINE + 10; statsWidth = _glWidget->width() - 411 - statsX; if (_mouseX > statsX && _mouseX < statsX + statsWidth && _mouseY > statsY && _mouseY < statsY + statsHeight) { toggleStatsExpanded(); return; } // top-right stats click lines = _statsExpanded ? 11 : 3; statsX = _glWidget->width() - 410; statsHeight = lines * STATS_PELS_PER_LINE + 10; statsWidth = _glWidget->width() - statsX; if (_mouseX > statsX && _mouseX < statsX + statsWidth && _mouseY > statsY && _mouseY < statsY + statsHeight) { toggleStatsExpanded(); return; } } void Application::toggleStatsExpanded() { _statsExpanded = !_statsExpanded; } void Application::renderThrustAtVoxel(const glm::vec3& thrust) { if (_mousePressed) { glColor3f(1, 0, 0); glLineWidth(2.0f); glBegin(GL_LINES); glm::vec3 voxelTouched = getMouseVoxelWorldCoordinates(_mouseVoxelDragging); glVertex3f(voxelTouched.x, voxelTouched.y, voxelTouched.z); glVertex3f(voxelTouched.x + thrust.x, voxelTouched.y + thrust.y, voxelTouched.z + thrust.z); glEnd(); } } glm::vec2 Application::getScaledScreenPoint(glm::vec2 projectedPoint) { float horizontalScale = _glWidget->width() / 2.0f; float verticalScale = _glWidget->height() / 2.0f; // -1,-1 is 0,windowHeight // 1,1 is windowWidth,0 // -1,1 1,1 // +-----------------------+ // | | | // | | | // | -1,0 | | // |-----------+-----------| // | 0,0 | // | | | // | | | // | | | // +-----------------------+ // -1,-1 1,-1 glm::vec2 screenPoint((projectedPoint.x + 1.0) * horizontalScale, ((projectedPoint.y + 1.0) * -verticalScale) + _glWidget->height()); return screenPoint; } // render the coverage map on screen void Application::renderCoverageMapV2() { glDisable(GL_LIGHTING); glLineWidth(2.0); glBegin(GL_LINES); glColor3f(0,1,1); renderCoverageMapsV2Recursively(&_voxels.myCoverageMapV2); glEnd(); glEnable(GL_LIGHTING); } void Application::renderCoverageMapsV2Recursively(CoverageMapV2* map) { // render ourselves... if (map->isCovered()) { BoundingBox box = map->getBoundingBox(); glm::vec2 firstPoint = getScaledScreenPoint(box.getVertex(0)); glm::vec2 lastPoint(firstPoint); for (int i = 1; i < box.getVertexCount(); i++) { glm::vec2 thisPoint = getScaledScreenPoint(box.getVertex(i)); glVertex2f(lastPoint.x, lastPoint.y); glVertex2f(thisPoint.x, thisPoint.y); lastPoint = thisPoint; } glVertex2f(lastPoint.x, lastPoint.y); glVertex2f(firstPoint.x, firstPoint.y); } else { // iterate our children and call render on them. for (int i = 0; i < CoverageMapV2::NUMBER_OF_CHILDREN; i++) { CoverageMapV2* childMap = map->getChild(i); if (childMap) { renderCoverageMapsV2Recursively(childMap); } } } } // render the coverage map on screen void Application::renderCoverageMap() { glDisable(GL_LIGHTING); glLineWidth(2.0); glBegin(GL_LINES); glColor3f(0,0,1); renderCoverageMapsRecursively(&_voxels.myCoverageMap); glEnd(); glEnable(GL_LIGHTING); } void Application::renderCoverageMapsRecursively(CoverageMap* map) { for (int i = 0; i < map->getPolygonCount(); i++) { OctreeProjectedPolygon* polygon = map->getPolygon(i); if (polygon->getProjectionType() == (PROJECTION_RIGHT | PROJECTION_NEAR | PROJECTION_BOTTOM)) { glColor3f(.5,0,0); // dark red } else if (polygon->getProjectionType() == (PROJECTION_NEAR | PROJECTION_RIGHT)) { glColor3f(.5,.5,0); // dark yellow } else if (polygon->getProjectionType() == (PROJECTION_NEAR | PROJECTION_LEFT)) { glColor3f(.5,.5,.5); // gray } else if (polygon->getProjectionType() == (PROJECTION_NEAR | PROJECTION_LEFT | PROJECTION_BOTTOM)) { glColor3f(.5,0,.5); // dark magenta } else if (polygon->getProjectionType() == (PROJECTION_NEAR | PROJECTION_BOTTOM)) { glColor3f(.75,0,0); // red } else if (polygon->getProjectionType() == (PROJECTION_NEAR | PROJECTION_TOP)) { glColor3f(1,0,1); // magenta } else if (polygon->getProjectionType() == (PROJECTION_NEAR | PROJECTION_LEFT | PROJECTION_TOP)) { glColor3f(0,0,1); // Blue } else if (polygon->getProjectionType() == (PROJECTION_NEAR | PROJECTION_RIGHT | PROJECTION_TOP)) { glColor3f(0,1,0); // green } else if (polygon->getProjectionType() == (PROJECTION_NEAR)) { glColor3f(1,1,0); // yellow } else if (polygon->getProjectionType() == (PROJECTION_FAR | PROJECTION_RIGHT | PROJECTION_BOTTOM)) { glColor3f(0,.5,.5); // dark cyan } else { glColor3f(1,0,0); } glm::vec2 firstPoint = getScaledScreenPoint(polygon->getVertex(0)); glm::vec2 lastPoint(firstPoint); for (int i = 1; i < polygon->getVertexCount(); i++) { glm::vec2 thisPoint = getScaledScreenPoint(polygon->getVertex(i)); glVertex2f(lastPoint.x, lastPoint.y); glVertex2f(thisPoint.x, thisPoint.y); lastPoint = thisPoint; } glVertex2f(lastPoint.x, lastPoint.y); glVertex2f(firstPoint.x, firstPoint.y); } // iterate our children and call render on them. for (int i = 0; i < CoverageMapV2::NUMBER_OF_CHILDREN; i++) { CoverageMap* childMap = map->getChild(i); if (childMap) { renderCoverageMapsRecursively(childMap); } } } // renderViewFrustum() // // Description: this will render the view frustum bounds for EITHER the head // or the "myCamera". // // Frustum rendering mode. For debug purposes, we allow drawing the frustum in a couple of different ways. // We can draw it with each of these parts: // * Origin Direction/Up/Right vectors - these will be drawn at the point of the camera // * Near plane - this plane is drawn very close to the origin point. // * Right/Left planes - these two planes are drawn between the near and far planes. // * Far plane - the plane is drawn in the distance. // Modes - the following modes, will draw the following parts. // * All - draws all the parts listed above // * Planes - draws the planes but not the origin vectors // * Origin Vectors - draws the origin vectors ONLY // * Near Plane - draws only the near plane // * Far Plane - draws only the far plane void Application::renderViewFrustum(ViewFrustum& viewFrustum) { // Load it with the latest details! loadViewFrustum(_myCamera, viewFrustum); glm::vec3 position = viewFrustum.getOffsetPosition(); glm::vec3 direction = viewFrustum.getOffsetDirection(); glm::vec3 up = viewFrustum.getOffsetUp(); glm::vec3 right = viewFrustum.getOffsetRight(); // Get ready to draw some lines glDisable(GL_LIGHTING); glColor4f(1.0, 1.0, 1.0, 1.0); glLineWidth(1.0); glBegin(GL_LINES); if (Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_ALL || Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_VECTORS) { // Calculate the origin direction vectors glm::vec3 lookingAt = position + (direction * 0.2f); glm::vec3 lookingAtUp = position + (up * 0.2f); glm::vec3 lookingAtRight = position + (right * 0.2f); // Looking At = white glColor3f(1,1,1); glVertex3f(position.x, position.y, position.z); glVertex3f(lookingAt.x, lookingAt.y, lookingAt.z); // Looking At Up = purple glColor3f(1,0,1); glVertex3f(position.x, position.y, position.z); glVertex3f(lookingAtUp.x, lookingAtUp.y, lookingAtUp.z); // Looking At Right = cyan glColor3f(0,1,1); glVertex3f(position.x, position.y, position.z); glVertex3f(lookingAtRight.x, lookingAtRight.y, lookingAtRight.z); } if (Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_ALL || Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_PLANES || Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_NEAR_PLANE) { // Drawing the bounds of the frustum // viewFrustum.getNear plane - bottom edge glColor3f(1,0,0); glVertex3f(viewFrustum.getNearBottomLeft().x, viewFrustum.getNearBottomLeft().y, viewFrustum.getNearBottomLeft().z); glVertex3f(viewFrustum.getNearBottomRight().x, viewFrustum.getNearBottomRight().y, viewFrustum.getNearBottomRight().z); // viewFrustum.getNear plane - top edge glVertex3f(viewFrustum.getNearTopLeft().x, viewFrustum.getNearTopLeft().y, viewFrustum.getNearTopLeft().z); glVertex3f(viewFrustum.getNearTopRight().x, viewFrustum.getNearTopRight().y, viewFrustum.getNearTopRight().z); // viewFrustum.getNear plane - right edge glVertex3f(viewFrustum.getNearBottomRight().x, viewFrustum.getNearBottomRight().y, viewFrustum.getNearBottomRight().z); glVertex3f(viewFrustum.getNearTopRight().x, viewFrustum.getNearTopRight().y, viewFrustum.getNearTopRight().z); // viewFrustum.getNear plane - left edge glVertex3f(viewFrustum.getNearBottomLeft().x, viewFrustum.getNearBottomLeft().y, viewFrustum.getNearBottomLeft().z); glVertex3f(viewFrustum.getNearTopLeft().x, viewFrustum.getNearTopLeft().y, viewFrustum.getNearTopLeft().z); } if (Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_ALL || Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_PLANES || Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_FAR_PLANE) { // viewFrustum.getFar plane - bottom edge glColor3f(0,1,0); glVertex3f(viewFrustum.getFarBottomLeft().x, viewFrustum.getFarBottomLeft().y, viewFrustum.getFarBottomLeft().z); glVertex3f(viewFrustum.getFarBottomRight().x, viewFrustum.getFarBottomRight().y, viewFrustum.getFarBottomRight().z); // viewFrustum.getFar plane - top edge glVertex3f(viewFrustum.getFarTopLeft().x, viewFrustum.getFarTopLeft().y, viewFrustum.getFarTopLeft().z); glVertex3f(viewFrustum.getFarTopRight().x, viewFrustum.getFarTopRight().y, viewFrustum.getFarTopRight().z); // viewFrustum.getFar plane - right edge glVertex3f(viewFrustum.getFarBottomRight().x, viewFrustum.getFarBottomRight().y, viewFrustum.getFarBottomRight().z); glVertex3f(viewFrustum.getFarTopRight().x, viewFrustum.getFarTopRight().y, viewFrustum.getFarTopRight().z); // viewFrustum.getFar plane - left edge glVertex3f(viewFrustum.getFarBottomLeft().x, viewFrustum.getFarBottomLeft().y, viewFrustum.getFarBottomLeft().z); glVertex3f(viewFrustum.getFarTopLeft().x, viewFrustum.getFarTopLeft().y, viewFrustum.getFarTopLeft().z); } if (Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_ALL || Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_PLANES) { // RIGHT PLANE IS CYAN // right plane - bottom edge - viewFrustum.getNear to distant glColor3f(0,1,1); glVertex3f(viewFrustum.getNearBottomRight().x, viewFrustum.getNearBottomRight().y, viewFrustum.getNearBottomRight().z); glVertex3f(viewFrustum.getFarBottomRight().x, viewFrustum.getFarBottomRight().y, viewFrustum.getFarBottomRight().z); // right plane - top edge - viewFrustum.getNear to distant glVertex3f(viewFrustum.getNearTopRight().x, viewFrustum.getNearTopRight().y, viewFrustum.getNearTopRight().z); glVertex3f(viewFrustum.getFarTopRight().x, viewFrustum.getFarTopRight().y, viewFrustum.getFarTopRight().z); // LEFT PLANE IS BLUE // left plane - bottom edge - viewFrustum.getNear to distant glColor3f(0,0,1); glVertex3f(viewFrustum.getNearBottomLeft().x, viewFrustum.getNearBottomLeft().y, viewFrustum.getNearBottomLeft().z); glVertex3f(viewFrustum.getFarBottomLeft().x, viewFrustum.getFarBottomLeft().y, viewFrustum.getFarBottomLeft().z); // left plane - top edge - viewFrustum.getNear to distant glVertex3f(viewFrustum.getNearTopLeft().x, viewFrustum.getNearTopLeft().y, viewFrustum.getNearTopLeft().z); glVertex3f(viewFrustum.getFarTopLeft().x, viewFrustum.getFarTopLeft().y, viewFrustum.getFarTopLeft().z); // focal plane - bottom edge glColor3f(1.0f, 0.0f, 1.0f); float focalProportion = (viewFrustum.getFocalLength() - viewFrustum.getNearClip()) / (viewFrustum.getFarClip() - viewFrustum.getNearClip()); glm::vec3 focalBottomLeft = glm::mix(viewFrustum.getNearBottomLeft(), viewFrustum.getFarBottomLeft(), focalProportion); glm::vec3 focalBottomRight = glm::mix(viewFrustum.getNearBottomRight(), viewFrustum.getFarBottomRight(), focalProportion); glVertex3f(focalBottomLeft.x, focalBottomLeft.y, focalBottomLeft.z); glVertex3f(focalBottomRight.x, focalBottomRight.y, focalBottomRight.z); // focal plane - top edge glm::vec3 focalTopLeft = glm::mix(viewFrustum.getNearTopLeft(), viewFrustum.getFarTopLeft(), focalProportion); glm::vec3 focalTopRight = glm::mix(viewFrustum.getNearTopRight(), viewFrustum.getFarTopRight(), focalProportion); glVertex3f(focalTopLeft.x, focalTopLeft.y, focalTopLeft.z); glVertex3f(focalTopRight.x, focalTopRight.y, focalTopRight.z); // focal plane - left edge glVertex3f(focalBottomLeft.x, focalBottomLeft.y, focalBottomLeft.z); glVertex3f(focalTopLeft.x, focalTopLeft.y, focalTopLeft.z); // focal plane - right edge glVertex3f(focalBottomRight.x, focalBottomRight.y, focalBottomRight.z); glVertex3f(focalTopRight.x, focalTopRight.y, focalTopRight.z); } glEnd(); glEnable(GL_LIGHTING); if (Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_ALL || Menu::getInstance()->getFrustumDrawMode() == FRUSTUM_DRAW_MODE_KEYHOLE) { // Draw the keyhole float keyholeRadius = viewFrustum.getKeyholeRadius(); if (keyholeRadius > 0.0f) { glPushMatrix(); glColor4f(1, 1, 0, 1); glTranslatef(position.x, position.y, position.z); // where we actually want it! glutWireSphere(keyholeRadius, 20, 20); glPopMatrix(); } } } bool Application::maybeEditVoxelUnderCursor() { if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelAddMode) || Menu::getInstance()->isOptionChecked(MenuOption::VoxelColorMode)) { if (_mouseVoxel.s != 0) { makeVoxel(glm::vec3(_mouseVoxel.x * TREE_SCALE, _mouseVoxel.y * TREE_SCALE, _mouseVoxel.z * TREE_SCALE), _mouseVoxel.s * TREE_SCALE, _mouseVoxel.red, _mouseVoxel.green, _mouseVoxel.blue, Menu::getInstance()->isOptionChecked(MenuOption::DestructiveAddVoxel)); // remember the position for drag detection _justEditedVoxel = true; } } else if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelDeleteMode)) { deleteVoxelUnderCursor(); VoxelFade fade(VoxelFade::FADE_OUT, 1.0f, 1.0f, 1.0f); const float VOXEL_BOUNDS_ADJUST = 0.01f; float slightlyBigger = _mouseVoxel.s * VOXEL_BOUNDS_ADJUST; fade.voxelDetails.x = _mouseVoxel.x - slightlyBigger; fade.voxelDetails.y = _mouseVoxel.y - slightlyBigger; fade.voxelDetails.z = _mouseVoxel.z - slightlyBigger; fade.voxelDetails.s = _mouseVoxel.s + slightlyBigger + slightlyBigger; _voxelFades.push_back(fade); } else if (Menu::getInstance()->isOptionChecked(MenuOption::VoxelGetColorMode)) { eyedropperVoxelUnderCursor(); } else { return false; } return true; } void Application::deleteVoxelUnderCursor() { if (_mouseVoxel.s != 0) { // sending delete to the server is sufficient, server will send new version so we see updates soon enough _voxelEditSender.sendVoxelEditMessage(PACKET_TYPE_VOXEL_ERASE, _mouseVoxel); // delete it locally to see the effect immediately (and in case no voxel server is present) _voxels.deleteVoxelAt(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); } // remember the position for drag detection _justEditedVoxel = true; } void Application::eyedropperVoxelUnderCursor() { VoxelTreeElement* selectedNode = _voxels.getVoxelAt(_mouseVoxel.x, _mouseVoxel.y, _mouseVoxel.z, _mouseVoxel.s); if (selectedNode && selectedNode->isColored()) { QColor selectedColor(selectedNode->getColor()[RED_INDEX], selectedNode->getColor()[GREEN_INDEX], selectedNode->getColor()[BLUE_INDEX]); if (selectedColor.isValid()) { QAction* voxelPaintColorAction = Menu::getInstance()->getActionForOption(MenuOption::VoxelPaintColor); voxelPaintColorAction->setData(selectedColor); voxelPaintColorAction->setIcon(Swatch::createIcon(selectedColor)); } } } void Application::resetSensors() { _headMouseX = _mouseX = _glWidget->width() / 2; _headMouseY = _mouseY = _glWidget->height() / 2; _faceshift.reset(); if (OculusManager::isConnected()) { OculusManager::reset(); } QCursor::setPos(_headMouseX, _headMouseY); _myAvatar.reset(); _myTransmitter.resetLevels(); _myAvatar.setVelocity(glm::vec3(0,0,0)); _myAvatar.setThrust(glm::vec3(0,0,0)); QMetaObject::invokeMethod(&_audio, "reset", Qt::QueuedConnection); } static void setShortcutsEnabled(QWidget* widget, bool enabled) { foreach (QAction* action, widget->actions()) { QKeySequence shortcut = action->shortcut(); if (!shortcut.isEmpty() && (shortcut[0] & (Qt::CTRL | Qt::ALT | Qt::META)) == 0) { // it's a shortcut that may coincide with a "regular" key, so switch its context action->setShortcutContext(enabled ? Qt::WindowShortcut : Qt::WidgetShortcut); } } foreach (QObject* child, widget->children()) { if (child->isWidgetType()) { setShortcutsEnabled(static_cast(child), enabled); } } } void Application::setMenuShortcutsEnabled(bool enabled) { setShortcutsEnabled(_window->menuBar(), enabled); } void Application::updateWindowTitle(){ QString title = ""; QString buildVersion = " (build " + applicationVersion() + ")"; QString username = _profile.getUsername(); if(!username.isEmpty()){ title += username; title += " @ "; } title += NodeList::getInstance()->getDomainHostname(); title += buildVersion; qDebug("Application title set to: %s", title.toStdString().c_str()); _window->setWindowTitle(title); } void Application::domainChanged(const QString& domainHostname) { // update the user's last domain in their Profile (which will propagate to data-server) _profile.updateDomain(domainHostname); updateWindowTitle(); // reset the environment so that we don't erroneously end up with multiple _environment.resetToDefault(); // reset our node to stats and node to jurisdiction maps... since these must be changing... _voxelServerJurisdictions.clear(); _octreeServerSceneStats.clear(); _particleServerJurisdictions.clear(); // reset the particle renderer _particles.clear(); // reset our persist thread qDebug() << "Domain changed to" << domainHostname << ". Swapping persist cache."; updateLocalOctreeCache(); } void Application::nodeKilled(SharedNodePointer node) { if (node->getType() == NODE_TYPE_VOXEL_SERVER) { QUuid nodeUUID = node->getUUID(); // see if this is the first we've heard of this node... if (_voxelServerJurisdictions.find(nodeUUID) != _voxelServerJurisdictions.end()) { unsigned char* rootCode = _voxelServerJurisdictions[nodeUUID].getRootOctalCode(); VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode, rootDetails); printf("voxel server going away...... v[%f, %f, %f, %f]\n", rootDetails.x, rootDetails.y, rootDetails.z, rootDetails.s); // Add the jurisditionDetails object to the list of "fade outs" if (!Menu::getInstance()->isOptionChecked(MenuOption::DontFadeOnVoxelServerChanges)) { VoxelFade fade(VoxelFade::FADE_OUT, NODE_KILLED_RED, NODE_KILLED_GREEN, NODE_KILLED_BLUE); fade.voxelDetails = rootDetails; const float slightly_smaller = 0.99f; fade.voxelDetails.s = fade.voxelDetails.s * slightly_smaller; _voxelFades.push_back(fade); } // If the voxel server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server _voxelServerJurisdictions.erase(nodeUUID); } // also clean up scene stats for that server _voxelSceneStatsLock.lockForWrite(); if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { _octreeServerSceneStats.erase(nodeUUID); } _voxelSceneStatsLock.unlock(); } else if (node->getType() == NODE_TYPE_PARTICLE_SERVER) { QUuid nodeUUID = node->getUUID(); // see if this is the first we've heard of this node... if (_particleServerJurisdictions.find(nodeUUID) != _particleServerJurisdictions.end()) { unsigned char* rootCode = _particleServerJurisdictions[nodeUUID].getRootOctalCode(); VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode, rootDetails); printf("particle server going away...... v[%f, %f, %f, %f]\n", rootDetails.x, rootDetails.y, rootDetails.z, rootDetails.s); // Add the jurisditionDetails object to the list of "fade outs" if (!Menu::getInstance()->isOptionChecked(MenuOption::DontFadeOnVoxelServerChanges)) { VoxelFade fade(VoxelFade::FADE_OUT, NODE_KILLED_RED, NODE_KILLED_GREEN, NODE_KILLED_BLUE); fade.voxelDetails = rootDetails; const float slightly_smaller = 0.99f; fade.voxelDetails.s = fade.voxelDetails.s * slightly_smaller; _voxelFades.push_back(fade); } // If the voxel server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server _particleServerJurisdictions.erase(nodeUUID); } // also clean up scene stats for that server _voxelSceneStatsLock.lockForWrite(); if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { _octreeServerSceneStats.erase(nodeUUID); } _voxelSceneStatsLock.unlock(); } else if (node->getType() == NODE_TYPE_AVATAR_MIXER) { // our avatar mixer has gone away - clear the hash of avatars _avatarManager.clearHash(); } } void Application::trackIncomingVoxelPacket(unsigned char* messageData, ssize_t messageLength, const HifiSockAddr& senderSockAddr, bool wasStatsPacket) { // Attempt to identify the sender from it's address. SharedNodePointer serverNode = NodeList::getInstance()->nodeWithAddress(senderSockAddr); if (serverNode) { QUuid nodeUUID = serverNode->getUUID(); // now that we know the node ID, let's add these stats to the stats for that node... _voxelSceneStatsLock.lockForWrite(); if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { VoxelSceneStats& stats = _octreeServerSceneStats[nodeUUID]; stats.trackIncomingOctreePacket(messageData, messageLength, wasStatsPacket, serverNode->getClockSkewUsec()); } _voxelSceneStatsLock.unlock(); } } int Application::parseOctreeStats(unsigned char* messageData, ssize_t messageLength, const HifiSockAddr& senderSockAddr) { // But, also identify the sender, and keep track of the contained jurisdiction root for this server SharedNodePointer server = NodeList::getInstance()->nodeWithAddress(senderSockAddr); // parse the incoming stats datas stick it in a temporary object for now, while we // determine which server it belongs to VoxelSceneStats temp; int statsMessageLength = temp.unpackFromMessage(messageData, messageLength); // quick fix for crash... why would voxelServer be NULL? if (server) { QUuid nodeUUID = server->getUUID(); // now that we know the node ID, let's add these stats to the stats for that node... _voxelSceneStatsLock.lockForWrite(); if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { _octreeServerSceneStats[nodeUUID].unpackFromMessage(messageData, messageLength); } else { _octreeServerSceneStats[nodeUUID] = temp; } _voxelSceneStatsLock.unlock(); VoxelPositionSize rootDetails; voxelDetailsForCode(temp.getJurisdictionRoot(), rootDetails); // see if this is the first we've heard of this node... NodeToJurisdictionMap* jurisdiction = NULL; if (server->getType() == NODE_TYPE_VOXEL_SERVER) { jurisdiction = &_voxelServerJurisdictions; } else { jurisdiction = &_particleServerJurisdictions; } if (jurisdiction->find(nodeUUID) == jurisdiction->end()) { printf("stats from new server... v[%f, %f, %f, %f]\n", rootDetails.x, rootDetails.y, rootDetails.z, rootDetails.s); // Add the jurisditionDetails object to the list of "fade outs" if (!Menu::getInstance()->isOptionChecked(MenuOption::DontFadeOnVoxelServerChanges)) { VoxelFade fade(VoxelFade::FADE_OUT, NODE_ADDED_RED, NODE_ADDED_GREEN, NODE_ADDED_BLUE); fade.voxelDetails = rootDetails; const float slightly_smaller = 0.99f; fade.voxelDetails.s = fade.voxelDetails.s * slightly_smaller; _voxelFades.push_back(fade); } } // store jurisdiction details for later use // This is bit of fiddling is because JurisdictionMap assumes it is the owner of the values used to construct it // but VoxelSceneStats thinks it's just returning a reference to it's contents. So we need to make a copy of the // details from the VoxelSceneStats to construct the JurisdictionMap JurisdictionMap jurisdictionMap; jurisdictionMap.copyContents(temp.getJurisdictionRoot(), temp.getJurisdictionEndNodes()); (*jurisdiction)[nodeUUID] = jurisdictionMap; } return statsMessageLength; } void Application::packetSent(quint64 length) { _bandwidthMeter.outputStream(BandwidthMeter::VOXELS).updateValue(length); } void Application::loadScripts(){ // loads all saved scripts QSettings* settings = new QSettings(this); int size = settings->beginReadArray("Settings"); for(int i=0; isetArrayIndex(i); QString string = settings->value("script").toString(); loadScript(string); } settings->endArray(); } void Application::saveScripts(){ // saves all current running scripts QSettings* settings = new QSettings(this); settings->beginWriteArray("Settings"); for(int i=0; i<_activeScripts.size(); ++i){ settings->setArrayIndex(i); settings->setValue("script", _activeScripts.at(i)); } settings->endArray(); } void Application::removeScriptName(const QString& fileNameString) { _activeScripts.removeOne(fileNameString); } void Application::loadScript(const QString& fileNameString){ _activeScripts.append(fileNameString); QByteArray fileNameAscii = fileNameString.toLocal8Bit(); const char* fileName = fileNameAscii.data(); std::ifstream file(fileName, std::ios::in|std::ios::binary|std::ios::ate); if(!file.is_open()) { qDebug("Error loading file %s", fileName); return; } qDebug("Loading file %s...", fileName); // get file length.... unsigned long fileLength = file.tellg(); file.seekg( 0, std::ios::beg ); // read the entire file into a buffer, WHAT!? Why not. char* entireFile = new char[fileLength+1]; file.read((char*)entireFile, fileLength); file.close(); entireFile[fileLength] = 0;// null terminate QString script(entireFile); delete[] entireFile; // start the script on a new thread... bool wantMenuItems = true; // tells the ScriptEngine object to add menu items for itself ScriptEngine* scriptEngine = new ScriptEngine(script, wantMenuItems, fileName, Menu::getInstance(), &_controllerScriptingInterface); scriptEngine->setupMenuItems(); // setup the packet senders and jurisdiction listeners of the script engine's scripting interfaces so // we can use the same ones from the application. scriptEngine->getVoxelsScriptingInterface()->setPacketSender(&_voxelEditSender); scriptEngine->getParticlesScriptingInterface()->setPacketSender(&_particleEditSender); scriptEngine->getParticlesScriptingInterface()->setParticleTree(_particles.getTree()); // hook our avatar object into this script engine scriptEngine->setAvatarData(&_myAvatar, "MyAvatar"); QThread* workerThread = new QThread(this); // when the worker thread is started, call our engine's run.. connect(workerThread, SIGNAL(started()), scriptEngine, SLOT(run())); // when the thread is terminated, add both scriptEngine and thread to the deleteLater queue connect(scriptEngine, SIGNAL(finished(const QString&)), scriptEngine, SLOT(deleteLater())); connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater())); connect(scriptEngine, SIGNAL(finished(const QString&)), this, SLOT(removeScriptName(const QString&))); // when the application is about to quit, stop our script engine so it unwinds properly connect(this, SIGNAL(aboutToQuit()), scriptEngine, SLOT(stop())); scriptEngine->moveToThread(workerThread); // Starts an event loop, and emits workerThread->started() workerThread->start(); // restore the main window's active state _window->activateWindow(); } void Application::loadDialog() { // shut down and stop any existing script QString desktopLocation = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); QString suggestedName = desktopLocation.append("/script.js"); QString fileNameString = QFileDialog::getOpenFileName(_glWidget, tr("Open Script"), suggestedName, tr("JavaScript Files (*.js)")); loadScript(fileNameString); } void Application::toggleLogDialog() { if (! _logDialog) { _logDialog = new LogDialog(_glWidget, getLogger()); _logDialog->show(); } else { _logDialog->close(); } } void Application::initAvatarAndViewFrustum() { updateAvatar(0.f); } QString Application::getLocalVoxelCacheFileName() { QString fileName = QStandardPaths::writableLocation(QStandardPaths::DataLocation); QDir logDir(fileName); if (!logDir.exists(fileName)) { logDir.mkdir(fileName); } fileName.append(QString("/hifi.voxelscache.")); fileName.append(_profile.getLastDomain()); fileName.append(QString(".svo")); return fileName; } void Application::updateLocalOctreeCache(bool firstTime) { // only do this if we've already got a persistThread or we're told this is the first time if (firstTime || _persistThread) { if (_persistThread) { _persistThread->terminate(); _persistThread->deleteLater(); _persistThread = NULL; } QString localVoxelCacheFileName = getLocalVoxelCacheFileName(); const int LOCAL_CACHE_PERSIST_INTERVAL = 1000 * 10; // every 10 seconds if (!Menu::getInstance()->isOptionChecked(MenuOption::DisableLocalVoxelCache)) { _persistThread = new OctreePersistThread(_voxels.getTree(), localVoxelCacheFileName.toLocal8Bit().constData(),LOCAL_CACHE_PERSIST_INTERVAL); qDebug() << "updateLocalOctreeCache()... localVoxelCacheFileName=" << localVoxelCacheFileName; } if (_persistThread) { _voxels.beginLoadingLocalVoxelCache(); // while local voxels are importing, don't do individual node VBO updates connect(_persistThread, SIGNAL(loadCompleted()), &_voxels, SLOT(localVoxelCacheLoaded())); _persistThread->initialize(true); } } } void Application::checkVersion() { QNetworkRequest latestVersionRequest((QUrl(CHECK_VERSION_URL))); latestVersionRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); connect(Application::getInstance()->getNetworkAccessManager()->get(latestVersionRequest), SIGNAL(finished()), SLOT(parseVersionXml())); } void Application::parseVersionXml() { #ifdef Q_OS_WIN32 QString operatingSystem("win"); #endif #ifdef Q_OS_MAC QString operatingSystem("mac"); #endif #ifdef Q_OS_LINUX QString operatingSystem("ubuntu"); #endif QString releaseDate; QString releaseNotes; QString latestVersion; QUrl downloadUrl; QObject* sender = QObject::sender(); QXmlStreamReader xml(qobject_cast(sender)); while (!xml.atEnd() && !xml.hasError()) { QXmlStreamReader::TokenType token = xml.readNext(); if (token == QXmlStreamReader::StartElement) { if (xml.name() == "ReleaseDate") { xml.readNext(); releaseDate = xml.text().toString(); } if (xml.name() == "ReleaseNotes") { xml.readNext(); releaseNotes = xml.text().toString(); } if (xml.name() == "Version") { xml.readNext(); latestVersion = xml.text().toString(); } if (xml.name() == operatingSystem) { xml.readNext(); downloadUrl = QUrl(xml.text().toString()); } } } if (!shouldSkipVersion(latestVersion) && applicationVersion() != latestVersion) { new UpdateDialog(_glWidget, releaseNotes, latestVersion, downloadUrl); } sender->deleteLater(); } bool Application::shouldSkipVersion(QString latestVersion) { QFile skipFile(SKIP_FILENAME); skipFile.open(QIODevice::ReadWrite); QString skipVersion(skipFile.readAll()); return (skipVersion == latestVersion || applicationVersion() == "dev"); } void Application::skipVersion(QString latestVersion) { QFile skipFile(SKIP_FILENAME); skipFile.open(QIODevice::WriteOnly | QIODevice::Truncate); skipFile.seek(0); skipFile.write(latestVersion.toStdString().c_str()); }