// // ApplicationOverlay.cpp // interface/src/ui/overlays // // Created by Benjamin Arnold on 5/27/14. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "InterfaceConfig.h" #include #include #include #include "Application.h" #include "ApplicationOverlay.h" #include "devices/OculusManager.h" #include "ui/Stats.h" // Used to fade the UI const float FADE_SPEED = 0.08f; // Used to animate the magnification windows const float MAG_SPEED = 0.08f; const quint64 MSECS_TO_USECS = 1000ULL; const float WHITE_TEXT[] = { 0.93f, 0.93f, 0.93f }; const float RETICLE_COLOR[] = { 0.0f, 198.0f / 255.0f, 244.0f / 255.0f }; const float reticleSize = TWO_PI / 100.0f; const float CONNECTION_STATUS_BORDER_COLOR[] = { 1.0f, 0.0f, 0.0f }; const float CONNECTION_STATUS_BORDER_LINE_WIDTH = 4.0f; static const float MOUSE_PITCH_RANGE = 1.0f * PI; static const float MOUSE_YAW_RANGE = 0.5f * TWO_PI; // Return a point's cartesian coordinates on a sphere from pitch and yaw glm::vec3 getPoint(float yaw, float pitch) { return glm::vec3(glm::cos(-pitch) * (-glm::sin(yaw)), glm::sin(-pitch), glm::cos(-pitch) * (-glm::cos(yaw))); } //Checks if the given ray intersects the sphere at the origin. result will store a multiplier that should //be multiplied by dir and added to origin to get the location of the collision bool raySphereIntersect(const glm::vec3 &dir, const glm::vec3 &origin, float r, float* result) { //Source: http://wiki.cgsociety.org/index.php/Ray_Sphere_Intersection //Compute A, B and C coefficients float a = glm::dot(dir, dir); float b = 2 * glm::dot(dir, origin); float c = glm::dot(origin, origin) - (r * r); //Find discriminant float disc = b * b - 4 * a * c; // if discriminant is negative there are no real roots, so return // false as ray misses sphere if (disc < 0) { return false; } // compute q as described above float distSqrt = sqrtf(disc); float q; if (b < 0) { q = (-b - distSqrt) / 2.0; } else { q = (-b + distSqrt) / 2.0; } // compute t0 and t1 float t0 = q / a; float t1 = c / q; // make sure t0 is smaller than t1 if (t0 > t1) { // if t0 is bigger than t1 swap them around float temp = t0; t0 = t1; t1 = temp; } // if t1 is less than zero, the object is in the ray's negative direction // and consequently the ray misses the sphere if (t1 < 0) { return false; } // if t0 is less than zero, the intersection point is at t1 if (t0 < 0) { *result = t1; return true; } else { // else the intersection point is at t0 *result = t0; return true; } } void renderReticle(glm::quat orientation, float alpha) { glm::vec3 topLeft = getPoint(reticleSize / 2.0f, -reticleSize / 2.0f); glm::vec3 topRight = getPoint(-reticleSize / 2.0f, -reticleSize / 2.0f); glm::vec3 bottomLeft = getPoint(reticleSize / 2.0f, reticleSize / 2.0f); glm::vec3 bottomRight = getPoint(-reticleSize / 2.0f, reticleSize / 2.0f); glPushMatrix(); { glm::vec3 axis = glm::axis(orientation); glRotatef(glm::degrees(glm::angle(orientation)), axis.x, axis.y, axis.z); glBegin(GL_QUADS); { glColor4f(RETICLE_COLOR[0], RETICLE_COLOR[1], RETICLE_COLOR[2], alpha); glTexCoord2f(0.0f, 0.0f); glVertex3f(topLeft.x, topLeft.y, topLeft.z); glTexCoord2f(1.0f, 0.0f); glVertex3f(bottomLeft.x, bottomLeft.y, bottomLeft.z); glTexCoord2f(1.0f, 1.0f); glVertex3f(bottomRight.x, bottomRight.y, bottomRight.z); glTexCoord2f(0.0f, 1.0f); glVertex3f(topRight.x, topRight.y, topRight.z); } glEnd(); } glPopMatrix(); } ApplicationOverlay::ApplicationOverlay() : _textureFov(glm::radians(DEFAULT_OCULUS_UI_ANGULAR_SIZE)), _textureAspectRatio(1.0f), _lastMouseMove(0), _alpha(1.0f), _oculusUIRadius(1.0f), _crosshairTexture(0) { memset(_reticleActive, 0, sizeof(_reticleActive)); memset(_magActive, 0, sizeof(_reticleActive)); memset(_magSizeMult, 0, sizeof(_magSizeMult)); } ApplicationOverlay::~ApplicationOverlay() { } // Renders the overlays either to a texture or to the screen void ApplicationOverlay::renderOverlay(bool renderToTexture) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "ApplicationOverlay::displayOverlay()"); Application* application = Application::getInstance(); Overlays& overlays = application->getOverlays(); GLCanvas::SharedPointer glCanvas = DependencyManager::get(); MyAvatar* myAvatar = application->getAvatar(); _textureFov = glm::radians(Menu::getInstance()->getOculusUIAngularSize()); _textureAspectRatio = (float)glCanvas->getDeviceWidth() / (float)glCanvas->getDeviceHeight(); //Handle fading and deactivation/activation of UI if (Menu::getInstance()->isOptionChecked(MenuOption::UserInterface)) { _alpha += FADE_SPEED; if (_alpha > 1.0f) { _alpha = 1.0f; } } else { _alpha -= FADE_SPEED; if (_alpha <= 0.0f) { _alpha = 0.0f; } } // Render 2D overlay glMatrixMode(GL_PROJECTION); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); if (renderToTexture) { _overlays.buildFramebufferObject(); _overlays.bind(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } glPushMatrix(); { glLoadIdentity(); gluOrtho2D(0, glCanvas->width(), glCanvas->height(), 0); renderAudioMeter(); if (Menu::getInstance()->isOptionChecked(MenuOption::HeadMouse)) { myAvatar->renderHeadMouse(glCanvas->width(), glCanvas->height()); } renderStatsAndLogs(); // give external parties a change to hook in emit application->renderingOverlay(); overlays.render2D(); renderPointers(); renderDomainConnectionStatusBorder(); } glPopMatrix(); glMatrixMode(GL_MODELVIEW); glEnable(GL_DEPTH_TEST); glEnable(GL_LIGHTING); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); if (renderToTexture) { _overlays.release(); } } // Draws the FBO texture for the screen void ApplicationOverlay::displayOverlayTexture() { if (_alpha == 0.0f) { return; } GLCanvas::SharedPointer glCanvas = DependencyManager::get(); glEnable(GL_TEXTURE_2D); glActiveTexture(GL_TEXTURE0); _overlays.bindTexture(); glMatrixMode(GL_PROJECTION); glPushMatrix(); { glLoadIdentity(); gluOrtho2D(0, glCanvas->getDeviceWidth(), glCanvas->getDeviceHeight(), 0); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); glEnable(GL_BLEND); glBegin(GL_QUADS); { glColor4f(1.0f, 1.0f, 1.0f, _alpha); glTexCoord2f(0, 0); glVertex2i(0, glCanvas->getDeviceHeight()); glTexCoord2f(1, 0); glVertex2i(glCanvas->getDeviceWidth(), glCanvas->getDeviceHeight()); glTexCoord2f(1, 1); glVertex2i(glCanvas->getDeviceWidth(), 0); glTexCoord2f(0, 1); glVertex2i(0, 0); } glEnd(); } glPopMatrix(); glDisable(GL_TEXTURE_2D); } // Draws the FBO texture for Oculus rift. void ApplicationOverlay::displayOverlayTextureOculus(Camera& whichCamera) { if (_alpha == 0.0f) { return; } glEnable(GL_TEXTURE_2D); glActiveTexture(GL_TEXTURE0); _overlays.bindTexture(); glEnable(GL_BLEND); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); glDisable(GL_LIGHTING); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 0.01f); //Update and draw the magnifiers MyAvatar* myAvatar = Application::getInstance()->getAvatar(); const glm::quat& orientation = myAvatar->getOrientation(); const glm::vec3& position = myAvatar->getDefaultEyePosition(); const float scale = myAvatar->getScale() * _oculusUIRadius; glMatrixMode(GL_MODELVIEW); glPushMatrix(); { glTranslatef(position.x, position.y, position.z); glm::mat4 rotation = glm::toMat4(orientation); glMultMatrixf(&rotation[0][0]); glScalef(scale, scale, scale); for (int i = 0; i < NUMBER_OF_RETICLES; i++) { if (_magActive[i]) { _magSizeMult[i] += MAG_SPEED; if (_magSizeMult[i] > 1.0f) { _magSizeMult[i] = 1.0f; } } else { _magSizeMult[i] -= MAG_SPEED; if (_magSizeMult[i] < 0.0f) { _magSizeMult[i] = 0.0f; } } if (_magSizeMult[i] > 0.0f) { //Render magnifier, but dont show border for mouse magnifier glm::vec2 projection = screenToOverlay(glm::vec2(_reticlePosition[MOUSE].x(), _reticlePosition[MOUSE].y())); renderMagnifier(projection, _magSizeMult[i], i != MOUSE); } } glDepthMask(GL_FALSE); glDisable(GL_ALPHA_TEST); glColor4f(1.0f, 1.0f, 1.0f, _alpha); static float textureFOV = 0.0f, textureAspectRatio = 1.0f; if (textureFOV != _textureFov || textureAspectRatio != _textureAspectRatio) { textureFOV = _textureFov; textureAspectRatio = _textureAspectRatio; _overlays.buildVBO(_textureFov, _textureAspectRatio, 80, 80); } _overlays.render(); renderPointersOculus(myAvatar->getDefaultEyePosition()); glDepthMask(GL_TRUE); _overlays.releaseTexture(); glDisable(GL_TEXTURE_2D); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); glEnable(GL_LIGHTING); } glPopMatrix(); } // Draws the FBO texture for 3DTV. void ApplicationOverlay::displayOverlayTexture3DTV(Camera& whichCamera, float aspectRatio, float fov) { if (_alpha == 0.0f) { return; } Application* application = Application::getInstance(); MyAvatar* myAvatar = application->getAvatar(); const glm::vec3& viewMatrixTranslation = application->getViewMatrixTranslation(); glActiveTexture(GL_TEXTURE0); glEnable(GL_BLEND); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); _overlays.bindTexture(); glEnable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); glEnable(GL_TEXTURE_2D); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); // Transform to world space glm::quat rotation = whichCamera.getRotation(); glm::vec3 axis2 = glm::axis(rotation); glRotatef(-glm::degrees(glm::angle(rotation)), axis2.x, axis2.y, axis2.z); glTranslatef(viewMatrixTranslation.x, viewMatrixTranslation.y, viewMatrixTranslation.z); // Translate to the front of the camera glm::vec3 pos = whichCamera.getPosition(); glm::quat rot = myAvatar->getOrientation(); glm::vec3 axis = glm::axis(rot); glTranslatef(pos.x, pos.y, pos.z); glRotatef(glm::degrees(glm::angle(rot)), axis.x, axis.y, axis.z); glColor4f(1.0f, 1.0f, 1.0f, _alpha); //Render const GLfloat distance = 1.0f; const GLfloat halfQuadHeight = distance * tan(fov); const GLfloat halfQuadWidth = halfQuadHeight * aspectRatio; const GLfloat quadWidth = halfQuadWidth * 2.0f; const GLfloat quadHeight = halfQuadHeight * 2.0f; GLfloat x = -halfQuadWidth; GLfloat y = -halfQuadHeight; glDisable(GL_DEPTH_TEST); glBegin(GL_QUADS); glTexCoord2f(0.0f, 1.0f); glVertex3f(x, y + quadHeight, -distance); glTexCoord2f(1.0f, 1.0f); glVertex3f(x + quadWidth, y + quadHeight, -distance); glTexCoord2f(1.0f, 0.0f); glVertex3f(x + quadWidth, y, -distance); glTexCoord2f(0.0f, 0.0f); glVertex3f(x, y, -distance); glEnd(); GLCanvas::SharedPointer glCanvas = DependencyManager::get(); if (_crosshairTexture == 0) { _crosshairTexture = glCanvas->bindTexture(QImage(PathUtils::resourcesPath() + "images/sixense-reticle.png")); } //draw the mouse pointer glBindTexture(GL_TEXTURE_2D, _crosshairTexture); const float reticleSize = 40.0f / glCanvas->width() * quadWidth; x -= reticleSize / 2.0f; y += reticleSize / 2.0f; const float mouseX = (application->getMouseX() / (float)glCanvas->width()) * quadWidth; const float mouseY = (1.0 - (application->getMouseY() / (float)glCanvas->height())) * quadHeight; glBegin(GL_QUADS); glColor3f(RETICLE_COLOR[0], RETICLE_COLOR[1], RETICLE_COLOR[2]); glTexCoord2d(0.0f, 0.0f); glVertex3f(x + mouseX, y + mouseY, -distance); glTexCoord2d(1.0f, 0.0f); glVertex3f(x + mouseX + reticleSize, y + mouseY, -distance); glTexCoord2d(1.0f, 1.0f); glVertex3f(x + mouseX + reticleSize, y + mouseY - reticleSize, -distance); glTexCoord2d(0.0f, 1.0f); glVertex3f(x + mouseX, y + mouseY - reticleSize, -distance); glEnd(); glEnable(GL_DEPTH_TEST); glPopMatrix(); glDepthMask(GL_TRUE); glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); glEnable(GL_LIGHTING); glColor4f(1.0f, 1.0f, 1.0f, 1.0f); } void ApplicationOverlay::computeOculusPickRay(float x, float y, glm::vec3& origin, glm::vec3& direction) const { const float pitch = (0.5f - y) * MOUSE_PITCH_RANGE; const float yaw = (0.5f - x) * MOUSE_YAW_RANGE; const glm::quat orientation(glm::vec3(pitch, yaw, 0.0f)); const glm::vec3 localDirection = orientation * IDENTITY_FRONT; //Rotate the UI pick ray by the avatar orientation const MyAvatar* myAvatar = Application::getInstance()->getAvatar(); origin = myAvatar->getDefaultEyePosition(); direction = myAvatar->getOrientation() * localDirection; } //Caculate the click location using one of the sixense controllers. Scale is not applied QPoint ApplicationOverlay::getPalmClickLocation(const PalmData *palm) const { Application* application = Application::getInstance(); GLCanvas::SharedPointer glCanvas = DependencyManager::get(); MyAvatar* myAvatar = application->getAvatar(); glm::vec3 tip = myAvatar->getLaserPointerTipPosition(palm); glm::vec3 eyePos = myAvatar->getHead()->getEyePosition(); glm::quat invOrientation = glm::inverse(myAvatar->getOrientation()); //direction of ray goes towards camera glm::vec3 dir = invOrientation * glm::normalize(application->getCamera()->getPosition() - tip); glm::vec3 tipPos = invOrientation * (tip - eyePos); QPoint rv; if (OculusManager::isConnected()) { float t; //We back the ray up by dir to ensure that it will not start inside the UI. glm::vec3 adjustedPos = tipPos - dir; //Find intersection of crosshair ray. if (raySphereIntersect(dir, adjustedPos, _oculusUIRadius * myAvatar->getScale(), &t)){ glm::vec3 collisionPos = adjustedPos + dir * t; //Normalize it in case its not a radius of 1 collisionPos = glm::normalize(collisionPos); //If we hit the back hemisphere, mark it as not a collision if (collisionPos.z > 0) { rv.setX(INT_MAX); rv.setY(INT_MAX); } else { float u = asin(collisionPos.x) / (_textureFov)+0.5f; float v = 1.0 - (asin(collisionPos.y) / (_textureFov)+0.5f); rv.setX(u * glCanvas->width()); rv.setY(v * glCanvas->height()); } } else { //if they did not click on the overlay, just set the coords to INT_MAX rv.setX(INT_MAX); rv.setY(INT_MAX); } } else { glm::dmat4 projection; application->getProjectionMatrix(&projection); glm::vec4 clipSpacePos = glm::vec4(projection * glm::dvec4(tipPos, 1.0)); glm::vec3 ndcSpacePos; if (clipSpacePos.w != 0) { ndcSpacePos = glm::vec3(clipSpacePos) / clipSpacePos.w; } rv.setX(((ndcSpacePos.x + 1.0) / 2.0) * glCanvas->width()); rv.setY((1.0 - ((ndcSpacePos.y + 1.0) / 2.0)) * glCanvas->height()); } return rv; } //Finds the collision point of a world space ray bool ApplicationOverlay::calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction, glm::vec3& result) const { Application* application = Application::getInstance(); MyAvatar* myAvatar = application->getAvatar(); glm::quat orientation = myAvatar->getOrientation(); glm::vec3 relativePosition = orientation * (position - myAvatar->getDefaultEyePosition()); glm::vec3 relativeDirection = orientation * direction; float t; if (raySphereIntersect(relativeDirection, relativePosition, _oculusUIRadius * myAvatar->getScale(), &t)){ result = position + direction * t; return true; } return false; } //Renders optional pointers void ApplicationOverlay::renderPointers() { Application* application = Application::getInstance(); GLCanvas::SharedPointer glCanvas = DependencyManager::get(); //lazily load crosshair texture if (_crosshairTexture == 0) { _crosshairTexture = glCanvas->bindTexture(QImage(PathUtils::resourcesPath() + "images/sixense-reticle.png")); } glEnable(GL_TEXTURE_2D); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _crosshairTexture); if (OculusManager::isConnected() && !application->getLastMouseMoveWasSimulated()) { //If we are in oculus, render reticle later if (_lastMouseMove == 0) { _lastMouseMove = usecTimestampNow(); } QPoint position = QPoint(application->getTrueMouseX(), application->getTrueMouseY()); static const int MAX_IDLE_TIME = 3; if (_reticlePosition[MOUSE] != position) { _lastMouseMove = usecTimestampNow(); } else if (usecTimestampNow() - _lastMouseMove > MAX_IDLE_TIME * USECS_PER_SECOND) { float pitch, yaw, roll; OculusManager::getEulerAngles(yaw, pitch, roll); glm::vec2 screenPos = sphericalToScreen(glm::vec2(yaw, -pitch)); position = QPoint(screenPos.x, screenPos.y); glCanvas->cursor().setPos(glCanvas->mapToGlobal(position)); } _reticlePosition[MOUSE] = position; _reticleActive[MOUSE] = true; _magActive[MOUSE] = true; _reticleActive[LEFT_CONTROLLER] = false; _reticleActive[RIGHT_CONTROLLER] = false; } else if (application->getLastMouseMoveWasSimulated() && Menu::getInstance()->isOptionChecked(MenuOption::SixenseMouseInput)) { _lastMouseMove = 0; //only render controller pointer if we aren't already rendering a mouse pointer _reticleActive[MOUSE] = false; _magActive[MOUSE] = false; renderControllerPointers(); } glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); } void ApplicationOverlay::renderControllerPointers() { Application* application = Application::getInstance(); GLCanvas::SharedPointer glCanvas = DependencyManager::get(); MyAvatar* myAvatar = application->getAvatar(); //Static variables used for storing controller state static quint64 pressedTime[NUMBER_OF_RETICLES] = { 0ULL, 0ULL, 0ULL }; static bool isPressed[NUMBER_OF_RETICLES] = { false, false, false }; static bool stateWhenPressed[NUMBER_OF_RETICLES] = { false, false, false }; const HandData* handData = Application::getInstance()->getAvatar()->getHandData(); for (unsigned int palmIndex = 2; palmIndex < 4; palmIndex++) { const int index = palmIndex - 1; const PalmData* palmData = NULL; if (palmIndex >= handData->getPalms().size()) { return; } if (handData->getPalms()[palmIndex].isActive()) { palmData = &handData->getPalms()[palmIndex]; } else { continue; } int controllerButtons = palmData->getControllerButtons(); //Check for if we should toggle or drag the magnification window if (controllerButtons & BUTTON_3) { if (isPressed[index] == false) { //We are now dragging the window isPressed[index] = true; //set the pressed time in us pressedTime[index] = usecTimestampNow(); stateWhenPressed[index] = _magActive[index]; } } else if (isPressed[index]) { isPressed[index] = false; //If the button was only pressed for < 250 ms //then disable it. const int MAX_BUTTON_PRESS_TIME = 250 * MSECS_TO_USECS; if (usecTimestampNow() < pressedTime[index] + MAX_BUTTON_PRESS_TIME) { _magActive[index] = !stateWhenPressed[index]; } } //if we have the oculus, we should make the cursor smaller since it will be //magnified if (OculusManager::isConnected()) { QPoint point = getPalmClickLocation(palmData); _reticlePosition[index] = point; //When button 2 is pressed we drag the mag window if (isPressed[index]) { _magActive[index] = true; } // If oculus is enabled, we draw the crosshairs later continue; } int mouseX, mouseY; if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseLasers)) { QPoint res = getPalmClickLocation(palmData); mouseX = res.x(); mouseY = res.y(); } else { // Get directon relative to avatar orientation glm::vec3 direction = glm::inverse(myAvatar->getOrientation()) * palmData->getFingerDirection(); // Get the angles, scaled between (-0.5,0.5) float xAngle = (atan2(direction.z, direction.x) + M_PI_2); float yAngle = 0.5f - ((atan2(direction.z, direction.y) + M_PI_2)); // Get the pixel range over which the xAngle and yAngle are scaled float cursorRange = glCanvas->width() * SixenseManager::getInstance().getCursorPixelRangeMult(); mouseX = (glCanvas->width() / 2.0f + cursorRange * xAngle); mouseY = (glCanvas->height() / 2.0f + cursorRange * yAngle); } //If the cursor is out of the screen then don't render it if (mouseX < 0 || mouseX >= glCanvas->width() || mouseY < 0 || mouseY >= glCanvas->height()) { _reticleActive[index] = false; continue; } _reticleActive[index] = true; const float reticleSize = 40.0f; mouseX -= reticleSize / 2.0f; mouseY += reticleSize / 2.0f; glBegin(GL_QUADS); glColor3f(RETICLE_COLOR[0], RETICLE_COLOR[1], RETICLE_COLOR[2]); glTexCoord2d(0.0f, 0.0f); glVertex2i(mouseX, mouseY); glTexCoord2d(1.0f, 0.0f); glVertex2i(mouseX + reticleSize, mouseY); glTexCoord2d(1.0f, 1.0f); glVertex2i(mouseX + reticleSize, mouseY - reticleSize); glTexCoord2d(0.0f, 1.0f); glVertex2i(mouseX, mouseY - reticleSize); glEnd(); } } void ApplicationOverlay::renderPointersOculus(const glm::vec3& eyePos) { glBindTexture(GL_TEXTURE_2D, _crosshairTexture); glDisable(GL_DEPTH_TEST); glMatrixMode(GL_MODELVIEW); //Controller Pointers MyAvatar* myAvatar = Application::getInstance()->getAvatar(); for (int i = 0; i < (int)myAvatar->getHand()->getNumPalms(); i++) { PalmData& palm = myAvatar->getHand()->getPalms()[i]; if (palm.isActive()) { glm::vec3 tip = myAvatar->getLaserPointerTipPosition(&palm); glm::vec3 tipDirection = glm::normalize(glm::inverse(myAvatar->getOrientation()) * (tip - eyePos)); float pitch = -glm::asin(tipDirection.y); float yawSign = glm::sign(-tipDirection.x); float yaw = glm::acos(-tipDirection.z) * ((yawSign == 0.0f) ? 1.0f : yawSign); glm::quat orientation = glm::quat(glm::vec3(pitch, yaw, 0.0f)); renderReticle(orientation, _alpha); } } //Mouse Pointer if (_reticleActive[MOUSE]) { glm::vec2 projection = screenToSpherical(glm::vec2(_reticlePosition[MOUSE].x(), _reticlePosition[MOUSE].y())); glm::quat orientation(glm::vec3(-projection.y, projection.x, 0.0f)); renderReticle(orientation, _alpha); } glEnable(GL_DEPTH_TEST); } //Renders a small magnification of the currently bound texture at the coordinates void ApplicationOverlay::renderMagnifier(glm::vec2 magPos, float sizeMult, bool showBorder) const { GLCanvas::SharedPointer glCanvas = DependencyManager::get(); const int widgetWidth = glCanvas->width(); const int widgetHeight = glCanvas->height(); const float halfWidth = (MAGNIFY_WIDTH / _textureAspectRatio) * sizeMult / 2.0f; const float halfHeight = MAGNIFY_HEIGHT * sizeMult / 2.0f; // Magnification Texture Coordinates const float magnifyULeft = (magPos.x - halfWidth) / (float)widgetWidth; const float magnifyURight = (magPos.x + halfWidth) / (float)widgetWidth; const float magnifyVTop = 1.0f - (magPos.y - halfHeight) / (float)widgetHeight; const float magnifyVBottom = 1.0f - (magPos.y + halfHeight) / (float)widgetHeight; const float newHalfWidth = halfWidth * MAGNIFY_MULT; const float newHalfHeight = halfHeight * MAGNIFY_MULT; //Get yaw / pitch value for the corners const glm::vec2 topLeftYawPitch = overlayToSpherical(glm::vec2(magPos.x - newHalfWidth, magPos.y - newHalfHeight)); const glm::vec2 bottomRightYawPitch = overlayToSpherical(glm::vec2(magPos.x + newHalfWidth, magPos.y + newHalfHeight)); const glm::vec3 bottomLeft = getPoint(topLeftYawPitch.x, bottomRightYawPitch.y); const glm::vec3 bottomRight = getPoint(bottomRightYawPitch.x, bottomRightYawPitch.y); const glm::vec3 topLeft = getPoint(topLeftYawPitch.x, topLeftYawPitch.y); const glm::vec3 topRight = getPoint(bottomRightYawPitch.x, topLeftYawPitch.y); glPushMatrix(); { if (showBorder) { glDisable(GL_TEXTURE_2D); glLineWidth(1.0f); //Outer Line glBegin(GL_LINE_STRIP); { glColor4f(1.0f, 0.0f, 0.0f, _alpha); glVertex3f(topLeft.x, topLeft.y, topLeft.z); glVertex3f(bottomLeft.x, bottomLeft.y, bottomLeft.z); glVertex3f(bottomRight.x, bottomRight.y, bottomRight.z); glVertex3f(topRight.x, topRight.y, topRight.z); glVertex3f(topLeft.x, topLeft.y, topLeft.z); } glEnd(); glEnable(GL_TEXTURE_2D); } glColor4f(1.0f, 1.0f, 1.0f, _alpha); glBegin(GL_QUADS); { glTexCoord2f(magnifyULeft, magnifyVBottom); glVertex3f(bottomLeft.x, bottomLeft.y, bottomLeft.z); glTexCoord2f(magnifyURight, magnifyVBottom); glVertex3f(bottomRight.x, bottomRight.y, bottomRight.z); glTexCoord2f(magnifyURight, magnifyVTop); glVertex3f(topRight.x, topRight.y, topRight.z); glTexCoord2f(magnifyULeft, magnifyVTop); glVertex3f(topLeft.x, topLeft.y, topLeft.z); } glEnd(); } glPopMatrix(); } void ApplicationOverlay::renderAudioMeter() { Application* application = Application::getInstance(); GLCanvas::SharedPointer glCanvas = DependencyManager::get(); Audio* audio = application->getAudio(); // 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(glCanvas->width(), glCanvas->height(), audio->getCollisionSoundMagnitude()); } } // Audio VU Meter and Mute Icon const int MUTE_ICON_SIZE = 24; const int AUDIO_METER_INSET = 2; const int MUTE_ICON_PADDING = 10; const int AUDIO_METER_WIDTH = MIRROR_VIEW_WIDTH - MUTE_ICON_SIZE - AUDIO_METER_INSET - MUTE_ICON_PADDING; const int AUDIO_METER_SCALE_WIDTH = AUDIO_METER_WIDTH - 2 * AUDIO_METER_INSET; const int AUDIO_METER_HEIGHT = 8; const int AUDIO_METER_GAP = 5; const int AUDIO_METER_X = MIRROR_VIEW_LEFT_PADDING + MUTE_ICON_SIZE + AUDIO_METER_INSET + AUDIO_METER_GAP; int audioMeterY; bool smallMirrorVisible = Menu::getInstance()->isOptionChecked(MenuOption::Mirror) && !OculusManager::isConnected(); bool boxed = smallMirrorVisible && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); if (boxed) { audioMeterY = MIRROR_VIEW_HEIGHT + AUDIO_METER_GAP + MUTE_ICON_PADDING; } else { audioMeterY = AUDIO_METER_GAP + MUTE_ICON_PADDING; } const float AUDIO_METER_BLUE[] = { 0.0, 0.0, 1.0 }; const float AUDIO_METER_GREEN[] = { 0.0, 1.0, 0.0 }; const float AUDIO_METER_RED[] = { 1.0, 0.0, 0.0 }; const float AUDIO_GREEN_START = 0.25 * AUDIO_METER_SCALE_WIDTH; const float AUDIO_RED_START = 0.80 * AUDIO_METER_SCALE_WIDTH; const float CLIPPING_INDICATOR_TIME = 1.0f; const float AUDIO_METER_AVERAGING = 0.5; const float LOG2 = log(2.0f); const float METER_LOUDNESS_SCALE = 2.8f / 5.0f; const float LOG2_LOUDNESS_FLOOR = 11.0f; float audioLevel = 0.0f; float loudness = audio->getLastInputLoudness() + 1.0f; _trailingAudioLoudness = AUDIO_METER_AVERAGING * _trailingAudioLoudness + (1.0f - AUDIO_METER_AVERAGING) * loudness; float log2loudness = log(_trailingAudioLoudness) / LOG2; if (log2loudness <= LOG2_LOUDNESS_FLOOR) { audioLevel = (log2loudness / LOG2_LOUDNESS_FLOOR) * METER_LOUDNESS_SCALE * AUDIO_METER_SCALE_WIDTH; } else { audioLevel = (log2loudness - (LOG2_LOUDNESS_FLOOR - 1.0f)) * METER_LOUDNESS_SCALE * AUDIO_METER_SCALE_WIDTH; } if (audioLevel > AUDIO_METER_SCALE_WIDTH) { audioLevel = AUDIO_METER_SCALE_WIDTH; } bool isClipping = ((audio->getTimeSinceLastClip() > 0.0f) && (audio->getTimeSinceLastClip() < CLIPPING_INDICATOR_TIME)); if ((audio->getTimeSinceLastClip() > 0.0f) && (audio->getTimeSinceLastClip() < CLIPPING_INDICATOR_TIME)) { const float MAX_MAGNITUDE = 0.7f; float magnitude = MAX_MAGNITUDE * (1 - audio->getTimeSinceLastClip() / CLIPPING_INDICATOR_TIME); renderCollisionOverlay(glCanvas->width(), glCanvas->height(), magnitude, 1.0f); } audio->renderToolBox(MIRROR_VIEW_LEFT_PADDING + AUDIO_METER_GAP, audioMeterY, boxed); audio->renderScope(glCanvas->width(), glCanvas->height()); audio->renderStats(WHITE_TEXT, glCanvas->width(), glCanvas->height()); glBegin(GL_QUADS); if (isClipping) { glColor3f(1, 0, 0); } else { glColor3f(0.475f, 0.475f, 0.475f); } audioMeterY += AUDIO_METER_HEIGHT; glColor3f(0, 0, 0); // Draw audio meter background Quad glVertex2i(AUDIO_METER_X, audioMeterY); glVertex2i(AUDIO_METER_X + AUDIO_METER_WIDTH, audioMeterY); glVertex2i(AUDIO_METER_X + AUDIO_METER_WIDTH, audioMeterY + AUDIO_METER_HEIGHT); glVertex2i(AUDIO_METER_X, audioMeterY + AUDIO_METER_HEIGHT); if (audioLevel > AUDIO_RED_START) { if (!isClipping) { glColor3fv(AUDIO_METER_RED); } else { glColor3f(1, 1, 1); } // Draw Red Quad glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + AUDIO_RED_START, audioMeterY + AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + AUDIO_RED_START, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); audioLevel = AUDIO_RED_START; } if (audioLevel > AUDIO_GREEN_START) { if (!isClipping) { glColor3fv(AUDIO_METER_GREEN); } else { glColor3f(1, 1, 1); } // Draw Green Quad glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + AUDIO_GREEN_START, audioMeterY + AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + AUDIO_GREEN_START, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); audioLevel = AUDIO_GREEN_START; } // Draw Blue Quad if (!isClipping) { glColor3fv(AUDIO_METER_BLUE); } else { glColor3f(1, 1, 1); } // Draw Blue (low level) quad glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET, audioMeterY + AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glEnd(); } void ApplicationOverlay::renderStatsAndLogs() { Application* application = Application::getInstance(); GLCanvas::SharedPointer glCanvas = DependencyManager::get(); const OctreePacketProcessor& octreePacketProcessor = application->getOctreePacketProcessor(); BandwidthMeter* bandwidthMeter = application->getBandwidthMeter(); NodeBounds& nodeBoundsDisplay = application->getNodeBoundsDisplay(); // Display stats and log text onscreen glLineWidth(1.0f); glPointSize(1.0f); if (Menu::getInstance()->isOptionChecked(MenuOption::Stats)) { // let's set horizontal offset to give stats some margin to mirror int horizontalOffset = MIRROR_VIEW_WIDTH + MIRROR_VIEW_LEFT_PADDING * 2; int voxelPacketsToProcess = octreePacketProcessor.packetsToProcessCount(); // Onscreen text about position, servers, etc Stats::getInstance()->display(WHITE_TEXT, horizontalOffset, application->getFps(), application->getPacketsPerSecond(), application->getBytesPerSecond(), voxelPacketsToProcess); // Bandwidth meter if (Menu::getInstance()->isOptionChecked(MenuOption::Bandwidth)) { Stats::drawBackground(0x33333399, glCanvas->width() - 296, glCanvas->height() - 68, 296, 68); bandwidthMeter->render(glCanvas->width(), glCanvas->height()); } } // Show on-screen msec timer if (Menu::getInstance()->isOptionChecked(MenuOption::FrameTimer)) { char frameTimer[10]; quint64 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(glCanvas->width() - 100, glCanvas->height() - timerBottom, 0.30f, 0.0f, 0, frameTimer, WHITE_TEXT); } nodeBoundsDisplay.drawOverlay(); } void ApplicationOverlay::renderDomainConnectionStatusBorder() { NodeList* nodeList = NodeList::getInstance(); if (nodeList && !nodeList->getDomainHandler().isConnected()) { GLCanvas::SharedPointer glCanvas = DependencyManager::get(); int right = glCanvas->width(); int bottom = glCanvas->height(); glColor3f(CONNECTION_STATUS_BORDER_COLOR[0], CONNECTION_STATUS_BORDER_COLOR[1], CONNECTION_STATUS_BORDER_COLOR[2]); glLineWidth(CONNECTION_STATUS_BORDER_LINE_WIDTH); glBegin(GL_LINE_LOOP); glVertex2i(0, 0); glVertex2i(0, bottom); glVertex2i(right, bottom); glVertex2i(right, 0); glEnd(); } } ApplicationOverlay::TexturedHemisphere::TexturedHemisphere() : _vertices(0), _indices(0), _framebufferObject(NULL), _vbo(0, 0) { } ApplicationOverlay::TexturedHemisphere::~TexturedHemisphere() { cleanupVBO(); if (_framebufferObject != NULL) { delete _framebufferObject; } } void ApplicationOverlay::TexturedHemisphere::bind() { _framebufferObject->bind(); } void ApplicationOverlay::TexturedHemisphere::release() { _framebufferObject->release(); } void ApplicationOverlay::TexturedHemisphere::bindTexture() { glBindTexture(GL_TEXTURE_2D, _framebufferObject->texture()); } void ApplicationOverlay::TexturedHemisphere::releaseTexture() { glBindTexture(GL_TEXTURE_2D, 0); } void ApplicationOverlay::TexturedHemisphere::buildVBO(const float fov, const float aspectRatio, const int slices, const int stacks) { if (fov >= PI) { qDebug() << "TexturedHemisphere::buildVBO(): FOV greater or equal than Pi will create issues"; } // Cleanup old VBO if necessary cleanupVBO(); //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm // Compute number of vertices needed _vertices = slices * stacks; // Compute vertices positions and texture UV coordinate TextureVertex* vertexData = new TextureVertex[_vertices]; TextureVertex* vertexPtr = &vertexData[0]; for (int i = 0; i < stacks; i++) { float stacksRatio = (float)i / (float)(stacks - 1); // First stack is 0.0f, last stack is 1.0f // abs(theta) <= fov / 2.0f float pitch = -fov * (stacksRatio - 0.5f); for (int j = 0; j < slices; j++) { float slicesRatio = (float)j / (float)(slices - 1); // First slice is 0.0f, last slice is 1.0f // abs(phi) <= fov * aspectRatio / 2.0f float yaw = -fov * aspectRatio * (slicesRatio - 0.5f); vertexPtr->position = getPoint(yaw, pitch); vertexPtr->uv.x = slicesRatio; vertexPtr->uv.y = stacksRatio; vertexPtr++; } } // Create and write to buffer glGenBuffers(1, &_vbo.first); glBindBuffer(GL_ARRAY_BUFFER, _vbo.first); static const int BYTES_PER_VERTEX = sizeof(TextureVertex); glBufferData(GL_ARRAY_BUFFER, _vertices * BYTES_PER_VERTEX, vertexData, GL_STATIC_DRAW); delete[] vertexData; // Compute number of indices needed static const int VERTEX_PER_TRANGLE = 3; static const int TRIANGLE_PER_RECTANGLE = 2; int numberOfRectangles = (slices - 1) * (stacks - 1); _indices = numberOfRectangles * TRIANGLE_PER_RECTANGLE * VERTEX_PER_TRANGLE; // Compute indices order GLushort* indexData = new GLushort[_indices]; GLushort* indexPtr = indexData; for (int i = 0; i < stacks - 1; i++) { for (int j = 0; j < slices - 1; j++) { GLushort bottomLeftIndex = i * slices + j; GLushort bottomRightIndex = bottomLeftIndex + 1; GLushort topLeftIndex = bottomLeftIndex + slices; GLushort topRightIndex = topLeftIndex + 1; *(indexPtr++) = topLeftIndex; *(indexPtr++) = bottomLeftIndex; *(indexPtr++) = topRightIndex; *(indexPtr++) = topRightIndex; *(indexPtr++) = bottomLeftIndex; *(indexPtr++) = bottomRightIndex; } } // Create and write to buffer glGenBuffers(1, &_vbo.second); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _vbo.second); static const int BYTES_PER_INDEX = sizeof(GLushort); glBufferData(GL_ELEMENT_ARRAY_BUFFER, _indices * BYTES_PER_INDEX, indexData, GL_STATIC_DRAW); delete[] indexData; } void ApplicationOverlay::TexturedHemisphere::cleanupVBO() { if (_vbo.first != 0) { glDeleteBuffers(1, &_vbo.first); _vbo.first = 0; } if (_vbo.second != 0) { glDeleteBuffers(1, &_vbo.second); _vbo.second = 0; } } void ApplicationOverlay::TexturedHemisphere::buildFramebufferObject() { QSize size = DependencyManager::get()->getDeviceSize(); if (_framebufferObject != NULL && size == _framebufferObject->size()) { // Already build return; } if (_framebufferObject != NULL) { delete _framebufferObject; } _framebufferObject = new QOpenGLFramebufferObject(size); bindTexture(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); GLfloat borderColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); releaseTexture(); } //Renders a hemisphere with texture coordinates. void ApplicationOverlay::TexturedHemisphere::render() { if (_framebufferObject == NULL || _vbo.first == 0 || _vbo.second == 0) { qDebug() << "TexturedHemisphere::render(): Incorrect initialisation"; return; } glBindBuffer(GL_ARRAY_BUFFER, _vbo.first); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _vbo.second); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); static const int STRIDE = sizeof(TextureVertex); static const void* VERTEX_POINTER = 0; static const void* TEX_COORD_POINTER = (void*)sizeof(glm::vec3); glVertexPointer(3, GL_FLOAT, STRIDE, VERTEX_POINTER); glTexCoordPointer(2, GL_FLOAT, STRIDE, TEX_COORD_POINTER); glDrawRangeElements(GL_TRIANGLES, 0, _vertices - 1, _indices, GL_UNSIGNED_SHORT, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } glm::vec2 ApplicationOverlay::screenToSpherical(glm::vec2 screenPos) const { QSize screenSize = DependencyManager::get()->getDeviceSize(); float yaw = -(screenPos.x / screenSize.width() - 0.5f) * MOUSE_YAW_RANGE; float pitch = (screenPos.y / screenSize.height() - 0.5f) * MOUSE_PITCH_RANGE; return glm::vec2(yaw, pitch); } glm::vec2 ApplicationOverlay::sphericalToScreen(glm::vec2 sphericalPos) const { QSize screenSize = DependencyManager::get()->getDeviceSize(); float x = (-sphericalPos.x / MOUSE_YAW_RANGE + 0.5f) * screenSize.width(); float y = (sphericalPos.y / MOUSE_PITCH_RANGE + 0.5f) * screenSize.height(); return glm::vec2(x, y); } glm::vec2 ApplicationOverlay::sphericalToOverlay(glm::vec2 sphericalPos) const { QSize screenSize = DependencyManager::get()->getDeviceSize(); float x = (-sphericalPos.x / (_textureFov * _textureAspectRatio) + 0.5f) * screenSize.width(); float y = (sphericalPos.y / _textureFov + 0.5f) * screenSize.height(); return glm::vec2(x, y); } glm::vec2 ApplicationOverlay::overlayToSpherical(glm::vec2 overlayPos) const { QSize screenSize = DependencyManager::get()->getDeviceSize(); float yaw = -(overlayPos.x / screenSize.width() - 0.5f) * _textureFov * _textureAspectRatio; float pitch = (overlayPos.y / screenSize.height() - 0.5f) * _textureFov; return glm::vec2(yaw, pitch); } glm::vec2 ApplicationOverlay::screenToOverlay(glm::vec2 screenPos) const { return sphericalToOverlay(screenToSpherical(screenPos)); } glm::vec2 ApplicationOverlay::overlayToScreen(glm::vec2 overlayPos) const { return sphericalToScreen(overlayToSpherical(overlayPos)); }