#include "FractoriumPch.h" #include "GLWidget.h" #include "Fractorium.h" //#define OLDDRAG 1 /// /// Constructor which passes parent widget to the base and initializes OpenGL profile. /// This will need to change in the future to implement all drawing as shader programs. /// /// The parent widget GLWidget::GLWidget(QWidget* p) : QOpenGLWidget(p) { QSurfaceFormat qsf; m_Init = false; m_Drawing = false; m_TexWidth = 0; m_TexHeight = 0; m_OutputTexID = 0; m_Fractorium = nullptr; qsf.setSwapInterval(1);//Vsync. qsf.setSwapBehavior(QSurfaceFormat::DoubleBuffer); qsf.setVersion(2, 0); setFormat(qsf); } /// /// Empty destructor. /// GLWidget::~GLWidget() { } /// /// A manual initialization that must be called immediately after the main window is shown /// and the virtual initializeGL() is called. /// void GLWidget::InitGL() { if (!m_Init) { int w = m_Fractorium->ui.GLParentScrollArea->width(); int h = m_Fractorium->ui.GLParentScrollArea->height(); SetDimensions(w, h); m_Fractorium->m_WidthSpin->setValue(w); m_Fractorium->m_HeightSpin->setValue(h); //Start with a flock of 10 random embers. Can't do this until now because the window wasn't maximized yet, so the sizes would have been off. m_Fractorium->OnActionNewFlock(false); m_Fractorium->m_Controller->DelayedStartRenderTimer(); m_Init = true; } } /// /// Draw the final rendered image as a texture on a quad that is the same size as the window. /// Different action is taken based on whether a CPU or OpenCL renderer is used. /// For CPU, the output image buffer must be copied to OpenGL every time it's drawn. /// For OpenCL, the output image and the texture are the same thing, so no copying is necessary /// and all image memory remains on the card. /// void GLWidget::DrawQuad() { GLint texWidth = 0, texHeight = 0; glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); RendererBase* renderer = m_Fractorium->m_Controller->Renderer(); vector* finalImage = m_Fractorium->m_Controller->FinalImage(); //Ensure all allocation has taken place first. if (m_OutputTexID != 0 && finalImage && !finalImage->empty()) { glBindTexture(GL_TEXTURE_2D, m_OutputTexID);//The texture to draw to. //Only draw if the dimensions match exactly. if (m_TexWidth == width() && m_TexHeight == height() && ((m_TexWidth * m_TexHeight * 4) == GLint(finalImage->size()))) { glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, 1, 1, 0, -1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); //Copy data from CPU to OpenGL if using a CPU renderer. This is not needed when using OpenCL. if (renderer->RendererType() == eRendererType::CPU_RENDERER) glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_TexWidth, m_TexHeight, GL_RGBA, GL_UNSIGNED_BYTE, finalImage->data()); glBegin(GL_QUADS);//This will need to be converted to a shader at some point in the future. glTexCoord2f(0.0, 0.0); glVertex2f(0.0, 0.0); glTexCoord2f(0.0, 1.0); glVertex2f(0.0, 1.0); glTexCoord2f(1.0, 1.0); glVertex2f(1.0, 1.0); glTexCoord2f(1.0, 0.0); glVertex2f(1.0, 0.0); glEnd(); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); } glBindTexture(GL_TEXTURE_2D, 0);//Stop using this texture. } glDisable(GL_BLEND); glDisable(GL_TEXTURE_2D); } /// /// Set drag and drag modifier states to nothing. /// void GLEmberControllerBase::ClearDrag() { m_DragModifier = 0; m_DragState = eDragState::DragNone; } /// /// Wrapper around Allocate() call on the GL widget. /// bool GLEmberControllerBase::Allocate(bool force) { return m_GL->Allocate(force); } /// /// Helpers to set/get/clear which keys are pressed while dragging. /// /// bool bool GLEmberControllerBase::GetAlt() { return (m_DragModifier & et(eDragModifier::DragModAlt)) == et(eDragModifier::DragModAlt); } bool GLEmberControllerBase::GetShift() { return (m_DragModifier & et(eDragModifier::DragModShift)) == et(eDragModifier::DragModShift); } bool GLEmberControllerBase::GetControl() { return (m_DragModifier & et(eDragModifier::DragModControl)) == et(eDragModifier::DragModControl); } void GLEmberControllerBase::SetAlt() { m_DragModifier |= et(eDragModifier::DragModAlt); } void GLEmberControllerBase::SetShift() { m_DragModifier |= et(eDragModifier::DragModShift); } void GLEmberControllerBase::SetControl() { m_DragModifier |= et(eDragModifier::DragModControl); } void GLEmberControllerBase::ClearAlt() { m_DragModifier &= ~et(eDragModifier::DragModAlt); } void GLEmberControllerBase::ClearShift() { m_DragModifier &= ~et(eDragModifier::DragModShift); } void GLEmberControllerBase::ClearControl() { m_DragModifier &= ~et(eDragModifier::DragModControl); } /// /// Clear the OpenGL output window to be the background color of the current ember. /// Both buffers must be cleared, else artifacts will show up. /// template void GLEmberController::ClearWindow() { Ember* ember = m_FractoriumEmberController->CurrentEmber(); m_GL->makeCurrent(); m_GL->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); m_GL->glClearColor(ember->m_Background.r, ember->m_Background.g, ember->m_Background.b, 1.0); } /// /// Set the currently selected xform. /// The currently selected xform is drawn with a circle around it, with all others only showing their axes. /// /// The xform. template void GLEmberController::SetSelectedXform(Xform* xform) { //By doing this check, it prevents triggering unnecessary events when selecting an xform on this window with the mouse, //which will set the combo box on the main window, which will trigger this call. However, if the xform has been selected //here with the mouse, the window has already repainted, so there's no need to do it again. if (m_SelectedXform != xform || m_HoverXform != xform) { m_HoverXform = xform; m_SelectedXform = xform; if (m_GL->m_Init) //m_GL->update(); m_GL->repaint();//Force immediate redraw with repaint() instead of update(). } } /// /// Setters for main window pointers. /// void GLWidget::SetMainWindow(Fractorium* f) { m_Fractorium = f; } /// /// Getters for OpenGL state. /// bool GLWidget::Init() { return m_Init; } bool GLWidget::Drawing() { return m_Drawing; } GLint GLWidget::MaxTexSize() { return m_MaxTexSize; } GLuint GLWidget::OutputTexID() { return m_OutputTexID; } /// /// Initialize OpenGL, called once at startup after the main window constructor finishes. /// Although it seems an awkward place to put some of this setup code, the dimensions of the /// main window and its widgets are not fully initialized before this is called. /// Once this is done, the render timer is started after a short delay. /// Rendering is then clear to begin. /// void GLWidget::initializeGL() { if (!m_Init && initializeOpenGLFunctions() && m_Fractorium) { glClearColor(0.0, 0.0, 0.0, 1.0); glEnable(GL_TEXTURE_2D); glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_MaxTexSize); glDisable(GL_TEXTURE_2D); m_Fractorium->m_WidthSpin->setMaximum(m_MaxTexSize); m_Fractorium->m_HeightSpin->setMaximum(m_MaxTexSize); } } /// /// The main drawing/update function. /// First the quad will be drawn, then the remaining affine circles. /// void GLWidget::paintGL() { FractoriumEmberControllerBase* controller = m_Fractorium->m_Controller.get(); //Ensure there is a renderer and that it's supposed to be drawing, signified by the running timer. if (controller && controller->Renderer() && controller->RenderTimerRunning()) { RendererBase* renderer = controller->Renderer(); m_Drawing = true; controller->GLController()->DrawImage(); //Affine drawing. bool pre = m_Fractorium->ui.PreAffineGroupBox->isChecked(); bool post = m_Fractorium->ui.PostAffineGroupBox->isChecked(); float unitX = fabs(renderer->UpperRightX(false) - renderer->LowerLeftX(false)) / 2.0f; float unitY = fabs(renderer->UpperRightY(false) - renderer->LowerLeftY(false)) / 2.0f; glEnable(GL_DEPTH_TEST); glEnable(GL_BLEND); glEnable(GL_LINE_SMOOTH); glEnable(GL_POINT_SMOOTH); glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(-unitX, unitX, -unitY, unitY, -1, 1);//Projection matrix: OpenGL camera is always centered, just move the ember internally inside the renderer. glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); glDisable(GL_DEPTH_TEST); controller->GLController()->DrawAffines(pre, post); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND); glDisable(GL_LINE_SMOOTH); glDisable(GL_POINT_SMOOTH); m_Drawing = false; } } /// /// Draw the image on the quad. /// template void GLEmberController::DrawImage() { RendererBase* renderer = m_FractoriumEmberController->Renderer(); Ember* ember = m_FractoriumEmberController->CurrentEmber(); m_GL->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); m_GL->glClearColor(ember->m_Background.r, ember->m_Background.g, ember->m_Background.b, 1.0); m_GL->glDisable(GL_DEPTH_TEST); renderer->EnterFinalAccum();//Lock, may not be necessary, but just in case. renderer->EnterResize(); if (SizesMatch())//Ensure all sizes are correct. If not, do nothing. { vector* finalImage = m_FractoriumEmberController->FinalImage(); if ((renderer->RendererType() == eRendererType::OPENCL_RENDERER) || finalImage)//Final image only matters for CPU renderer. if ((renderer->RendererType() == eRendererType::OPENCL_RENDERER) || finalImage->size() == renderer->FinalBufferSize()) m_GL->DrawQuad();//Output image is drawn here. } renderer->LeaveResize();//Unlock, may not be necessary. renderer->LeaveFinalAccum(); } /// /// Draw the affine circles. /// /// True to draw pre affines, else don't. /// True to draw post affines, else don't. template void GLEmberController::DrawAffines(bool pre, bool post) { QueryVMP();//Resolves to float or double specialization function depending on T. Ember* ember = m_FractoriumEmberController->CurrentEmber(); bool dragging = m_DragState == eDragState::DragDragging; //Draw grid if control key is pressed. if (m_GL->hasFocus() && GetControl()) { m_GL->glLineWidth(1.0f); m_GL->DrawGrid(); } //When dragging, only draw the selected xform's affine and hide all others. if (!m_Fractorium->m_Settings->ShowAllXforms() && dragging) { if (m_SelectedXform) DrawAffine(m_SelectedXform, m_AffineType == eAffineType::AffinePre, true); } else//Show all while dragging, or not dragging just hovering/mouse move. { if (pre && m_Fractorium->DrawAllPre())//Draw all pre affine if specified. { for (size_t i = 0; i < ember->TotalXformCount(); i++) { Xform* xform = ember->GetTotalXform(i); bool selected = dragging ? (m_SelectedXform == xform) : (m_HoverXform == xform); DrawAffine(xform, true, selected); } } else if (pre && m_HoverXform)//Only draw current pre affine. { DrawAffine(m_HoverXform, true, true); } if (post && m_Fractorium->DrawAllPost())//Draw all post affine if specified. { for (size_t i = 0; i < ember->TotalXformCount(); i++) { Xform* xform = ember->GetTotalXform(i); bool selected = dragging ? (m_SelectedXform == xform) : (m_HoverXform == xform); DrawAffine(xform, false, selected); } } else if (post && m_HoverXform)//Only draw current post affine. { DrawAffine(m_HoverXform, false, true); } } if (dragging)//Draw large yellow dot on select or drag. { m_GL->glPointSize(6.0f); m_GL->glBegin(GL_POINTS); m_GL->glColor4f(1.0f, 1.0f, 0.5f, 1.0f); m_GL->glVertex2f(m_DragHandlePos.x, m_DragHandlePos.y); m_GL->glEnd(); m_GL->glPointSize(1.0f);//Restore point size. } else if (m_HoverType != eHoverType::HoverNone && m_HoverXform == m_SelectedXform)//Draw large turquoise dot on hover if they are hovering over the selected xform. { m_GL->glPointSize(6.0f); m_GL->glBegin(GL_POINTS); m_GL->glColor4f(0.5f, 1.0f, 1.0f, 1.0f); m_GL->glVertex2f(m_HoverHandlePos.x, m_HoverHandlePos.y); m_GL->glEnd(); m_GL->glPointSize(1.0f); } } /// /// Set drag modifiers based on key press. /// /// The event bool GLEmberControllerBase::KeyPress_(QKeyEvent* e) { #ifdef OLDDRAG if (e->key() == Qt::Key_Shift) SetShift(); else if (e->key() == Qt::Key_Control || e->key() == Qt::Key_C) SetControl(); else if (e->key() == Qt::Key_Alt || e->key() == Qt::Key_A) SetAlt(); else return false; return true; #else if (e->key() == Qt::Key_Control) { SetControl(); return true; } #endif return false; } /// /// Call controller KeyPress_(). /// /// The event void GLWidget::keyPressEvent(QKeyEvent* e) { if (!GLController() || !GLController()->KeyPress_(e)) QOpenGLWidget::keyPressEvent(e); update(); } /// /// Set drag modifiers based on key release. /// /// The event bool GLEmberControllerBase::KeyRelease_(QKeyEvent* e) { #ifdef OLDDRAG if (e->key() == Qt::Key_Shift) ClearShift(); else if (e->key() == Qt::Key_Control || e->key() == Qt::Key_C) ClearControl(); else if (e->key() == Qt::Key_Alt || e->key() == Qt::Key_A) ClearAlt(); else return false; return true; #else if (e->key() == Qt::Key_Control) { ClearControl(); return true; } #endif return false; } /// /// Call controller KeyRelease_(). /// /// The event void GLWidget::keyReleaseEvent(QKeyEvent* e) { if (!GLController() || !GLController()->KeyRelease_(e)) QOpenGLWidget::keyReleaseEvent(e); update(); } /// /// Determine if the mouse click was over an affine circle /// and set the appropriate selection information to be used /// on subsequent mouse move events. /// If nothing was selected, then reset the selection and drag states. /// /// The event template void GLEmberController::MousePress(QMouseEvent* e) { v3T mouseFlipped(e->x() * m_GL->devicePixelRatio(), m_Viewport[3] - e->y() * m_GL->devicePixelRatio(), 0);//Must flip y because in OpenGL, 0,0 is bottom left, but in windows, it's top left. Ember* ember = m_FractoriumEmberController->CurrentEmber(); RendererBase* renderer = m_FractoriumEmberController->Renderer(); //Ensure everything has been initialized. if (!renderer) return; m_MouseDownPos = glm::ivec2(e->x() * m_GL->devicePixelRatio(), e->y() * m_GL->devicePixelRatio());//Capture the raster coordinates of where the mouse was clicked. m_MouseWorldPos = WindowToWorld(mouseFlipped, false);//Capture the world cartesian coordinates of where the mouse is. m_BoundsDown.w = renderer->LowerLeftX(false);//Need to capture these because they'll be changing if scaling. m_BoundsDown.x = renderer->LowerLeftY(false); m_BoundsDown.y = renderer->UpperRightX(false); m_BoundsDown.z = renderer->UpperRightY(false); #ifndef OLDDRAG Qt::KeyboardModifiers mod = e->modifiers(); if (mod.testFlag(Qt::ShiftModifier)) SetShift(); //if (mod.testFlag(Qt::ControlModifier))// || mod.testFlag(Qt::Key_C)) // m_DragModifier |= DragModControl; if (mod.testFlag(Qt::AltModifier))// || mod.testFlag(Qt::Key_A)) SetAlt(); #endif if (m_DragState == eDragState::DragNone)//Only take action if the user wasn't already dragging. { m_MouseDownWorldPos = m_MouseWorldPos;//Set the mouse down position to the current position. if (e->button() & Qt::LeftButton) { int xformIndex = UpdateHover(mouseFlipped);//Determine if an affine circle was clicked. if (m_HoverXform && xformIndex != -1) { m_SelectedXform = m_HoverXform; m_DragSrcTransform = Affine2D(m_AffineType == eAffineType::AffinePre ? m_SelectedXform->m_Affine : m_SelectedXform->m_Post);//Copy the affine of the xform that was selected. m_DragHandlePos = m_HoverHandlePos; m_DragHandleOffset = m_DragHandlePos - m_MouseWorldPos; m_DragState = eDragState::DragDragging; //The user has selected an xform by clicking on it, so update the main GUI by selecting this xform in the combo box. m_Fractorium->CurrentXform(xformIndex); //Draw large yellow dot on select or drag. m_GL->glPointSize(6.0f); m_GL->glBegin(GL_POINTS); m_GL->glColor4f(1.0f, 1.0f, 0.5f, 1.0f); m_GL->glVertex2f(m_DragHandlePos.x, m_DragHandlePos.y); m_GL->glEnd(); m_GL->glPointSize(1.0f);//Restore point size. m_GL->repaint(); } else//Nothing was selected. { //m_SelectedXform = nullptr; m_DragState = eDragState::DragNone; } } else if (e->button() == Qt::MiddleButton)//Middle button does whole image translation. { m_CenterDownX = ember->m_CenterX;//Capture where the center of the image is because this value will change when panning. m_CenterDownY = ember->m_CenterY; m_DragState = eDragState::DragPanning; } else if (e->button() == Qt::RightButton)//Right button does whole image rotation and scaling. { UpdateHover(mouseFlipped); m_SelectedXform = m_HoverXform; m_CenterDownX = ember->m_CenterX;//Capture these because they will change when rotating and scaling. m_CenterDownY = ember->m_CenterY; m_RotationDown = ember->m_Rotate; m_ScaleDown = ember->m_PixelsPerUnit; m_DragState = eDragState::DragRotateScale; } } } /// /// Call controller MousePress(). /// /// The event void GLWidget::mousePressEvent(QMouseEvent* e) { setFocus();//Must do this so that this window gets keyboard events. if (GLEmberControllerBase* controller = GLController()) controller->MousePress(e); QOpenGLWidget::mousePressEvent(e); } /// /// Reset the selection and dragging state, but re-calculate the /// hovering state because the mouse might still be over an affine circle. /// /// The event template void GLEmberController::MouseRelease(QMouseEvent* e) { v3T mouseFlipped(e->x() * m_GL->devicePixelRatio(), m_Viewport[3] - e->y() * m_GL->devicePixelRatio(), 0);//Must flip y because in OpenGL, 0,0 is bottom left, but in windows, it's top left. m_MouseWorldPos = WindowToWorld(mouseFlipped, false); if (m_DragState == eDragState::DragDragging && (e->button() & Qt::LeftButton)) UpdateHover(mouseFlipped); m_DragState = eDragState::DragNone; #ifndef OLDDRAG m_DragModifier = 0; #endif m_GL->repaint();//Force immediate redraw. } /// /// Call controller MouseRelease(). /// /// The event void GLWidget::mouseReleaseEvent(QMouseEvent* e) { setFocus();//Must do this so that this window gets keyboard events. if (GLEmberControllerBase* controller = GLController()) controller->MouseRelease(e); QOpenGLWidget::mouseReleaseEvent(e); } /// /// If dragging, update relevant values and reset entire rendering process. /// If hovering, update display. /// /// The event template void GLEmberController::MouseMove(QMouseEvent* e) { bool draw = true; glm::ivec2 mouse(e->x() * m_GL->devicePixelRatio(), e->y() * m_GL->devicePixelRatio()); v3T mouseFlipped(e->x() * m_GL->devicePixelRatio(), m_Viewport[3] - e->y() * m_GL->devicePixelRatio(), 0);//Must flip y because in OpenGL, 0,0 is bottom left, but in windows, it's top left. Ember* ember = m_FractoriumEmberController->CurrentEmber(); //First check to see if the mouse actually moved. if (mouse == m_MousePos) return; m_MousePos = mouse; m_MouseWorldPos = WindowToWorld(mouseFlipped, false); //v3T mouseDelta = m_MouseWorldPos - m_MouseDownWorldPos;//Determine how far the mouse has moved in world cartesian coordinates. //Update status bar on main window, regardless of whether anything is being dragged. if (m_Fractorium->m_Controller->RenderTimerRunning()) m_Fractorium->SetCoordinateStatus(e->x() * m_GL->devicePixelRatio(), e->y() * m_GL->devicePixelRatio(), m_MouseWorldPos.x, m_MouseWorldPos.y); if (m_SelectedXform && m_DragState == eDragState::DragDragging)//Dragging and affine. { bool pre = m_AffineType == eAffineType::AffinePre; Affine2D* affine = pre ? &m_SelectedXform->m_Affine : &m_SelectedXform->m_Post;//Determine pre or post affine. if (m_HoverType == eHoverType::HoverTranslation) *affine = CalcDragTranslation(); else if (m_HoverType == eHoverType::HoverXAxis) *affine = CalcDragXAxis(); else if (m_HoverType == eHoverType::HoverYAxis) *affine = CalcDragYAxis(); m_FractoriumEmberController->FillAffineWithXform(m_SelectedXform, pre);//Update the spinners in the affine tab of the main window. m_FractoriumEmberController->UpdateRender();//Restart the rendering process. } else if (m_DragState == eDragState::DragPanning)//Translating the whole image. { T x = -(m_MouseWorldPos.x - m_MouseDownWorldPos.x); T y = (m_MouseWorldPos.y - m_MouseDownWorldPos.y); Affine2D rotMat; rotMat.C(m_CenterDownX); rotMat.F(m_CenterDownY); rotMat.Rotate(ember->m_Rotate); v2T v1(x, y); v2T v2 = rotMat.TransformVector(v1); ember->m_CenterX = v2.x; ember->m_CenterY = ember->m_RotCenterY = v2.y; m_FractoriumEmberController->SetCenter(ember->m_CenterX, ember->m_CenterY);//Will restart the rendering process. } else if (m_DragState == eDragState::DragRotateScale)//Rotating and scaling the whole image. { T rot = CalcRotation(); T scale = CalcScale(); ember->m_Rotate = NormalizeDeg180(m_RotationDown + rot); m_Fractorium->SetRotation(ember->m_Rotate, true); ember->m_PixelsPerUnit = m_ScaleDown + scale; m_Fractorium->SetScale(ember->m_PixelsPerUnit);//Will restart the rendering process. } else { //If the user doesn't already have a key down, and they aren't dragging, clear the keys to be safe. //This is done because if they do an alt+tab between windows, it thinks the alt key is down. if (e->modifiers() == Qt::NoModifier) ClearDrag(); //Check if they weren't dragging and weren't hovering over any affine. //In that case, nothing needs to be done. if (UpdateHover(mouseFlipped) == -1) draw = false; //Xform* previousHover = m_HoverXform; // //if (UpdateHover(mouseFlipped) == -1) // m_HoverXform = m_SelectedXform; // //if (m_HoverXform == previousHover) // draw = false; } //Only update if the user was dragging or hovered over a point. //Use repaint() to update immediately for a more responsive feel. if ((m_DragState != eDragState::DragNone) || draw) m_GL->update(); //m_GL->repaint(); } /// /// Call controller MouseMove(). /// /// The event void GLWidget::mouseMoveEvent(QMouseEvent* e) { setFocus();//Must do this so that this window gets keyboard events. if (GLEmberControllerBase* controller = GLController()) controller->MouseMove(e); QOpenGLWidget::mouseMoveEvent(e); } /// /// Mouse wheel changes the scale (pixels per unit) which /// will zoom in the image in our out, while sacrificing quality. /// If the user needs to preserve quality, they can use the zoom spinner /// on the main window. /// /// The event template void GLEmberController::Wheel(QWheelEvent* e) { Ember* ember = m_FractoriumEmberController->CurrentEmber(); if (m_Fractorium && !(e->buttons() & Qt::MiddleButton))//Middle button does whole image translation, so ignore the mouse wheel while panning to avoid inadvertent zooming. m_Fractorium->SetScale(ember->m_PixelsPerUnit + (e->angleDelta().y() >= 0 ? 50 : -50)); } /// /// Call controller Wheel(). /// /// The event void GLWidget::wheelEvent(QWheelEvent* e) { if (GLEmberControllerBase* controller = GLController()) controller->Wheel(e); //Do not call QOpenGLWidget::wheelEvent(e) because this should only affect the scale and not the position of the scroll bars. } /// /// Respond to a resize event which will set the double click default value /// in the width and height spinners. /// Note, this does not change the size of the ember being rendered or /// the OpenGL texture it's being drawn on. /// /// The event //void GLWidget::resizeEvent(QResizeEvent* e) //{ // if (m_Fractorium) // { // } // // QOpenGLWidget::resizeEvent(e); //} /// /// Set the dimensions of the drawing area. /// This will be called from the main window's SyncSizes() function. /// /// Width in pixels /// Height in pixels void GLWidget::SetDimensions(int w, int h) { setFixedSize(w, h); //resize(w, h); //m_Fractorium->ui.GLParentScrollAreaContents->setFixedSize(w, h); } /// /// Set up texture memory to match the size of the window. /// If first allocation, generate, bind and set parameters. /// If subsequent call, only take action if dimensions don't match the window. In such case, /// first deallocate, then reallocate. /// /// True if success, else false. bool GLWidget::Allocate(bool force) { bool alloc = false; bool doResize = force || m_TexWidth != width() || m_TexHeight != height(); bool doIt = doResize || m_OutputTexID == 0; if (doIt) { m_TexWidth = width(); m_TexHeight = height(); glEnable(GL_TEXTURE_2D); glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); if (doResize) Deallocate(); glGenTextures(1, &m_OutputTexID); glBindTexture(GL_TEXTURE_2D, m_OutputTexID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//Fractron had this as GL_LINEAR_MIPMAP_LINEAR for OpenCL and Cuda. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_TexWidth, m_TexHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); alloc = true; } if (alloc) { glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); } return m_OutputTexID != 0; } /// /// Deallocate texture memory. /// /// True if anything deleted, else false. bool GLWidget::Deallocate() { bool deleted = false; if (m_OutputTexID != 0) { glBindTexture(GL_TEXTURE_2D, m_OutputTexID); glDeleteTextures(1, &m_OutputTexID); m_OutputTexID = 0; deleted = true; } return deleted; } /// /// Set the viewport to match the window dimensions. /// If the dimensions already match, no action is taken. /// void GLWidget::SetViewport() { if (m_Init && (m_ViewWidth != m_TexWidth || m_ViewHeight != m_TexHeight)) { glViewport(0, 0, GLint(m_TexWidth), GLint(m_TexHeight)); m_ViewWidth = m_TexWidth; m_ViewHeight = m_TexHeight; } } /// /// Determine whether the dimensions of the renderer's current ember match /// the dimensions of the widget, texture and viewport. /// Since this uses the renderer's dimensions, this /// must be called after the renderer has set the current ember. /// /// True if all sizes match, else false. template bool GLEmberController::SizesMatch() { Ember* ember = m_FractoriumEmberController->CurrentEmber(); return (ember && ember->m_FinalRasW == m_GL->width() && ember->m_FinalRasH == m_GL->height() && m_GL->width() == m_GL->m_TexWidth && m_GL->height() == m_GL->m_TexHeight && m_GL->m_TexWidth == m_GL->m_ViewWidth && m_GL->m_TexHeight == m_GL->m_ViewHeight); } /// /// Draw the grid in response to the control key being pressed. /// The frequency of the grid lines will change depending on the zoom. /// Calculated with the frame always centered, the renderer just moves the camera. /// void GLWidget::DrawGrid() { RendererBase* renderer = m_Fractorium->m_Controller->Renderer(); float unitX = fabs(renderer->UpperRightX(false) - renderer->LowerLeftX(false)) / 2.0f; float unitY = fabs(renderer->UpperRightY(false) - renderer->LowerLeftY(false)) / 2.0f; float rad = std::max(unitX, unitY); float xLow = floor(-unitX); float xHigh = ceil(unitX); float yLow = floor(-unitY); float yHigh = ceil(unitY); glBegin(GL_LINES); if (rad <= 8.0f) { glColor4f(0.5f, 0.5f, 0.5f, 0.5f); for (float fx = xLow; fx <= xHigh; fx += GridStep) { glVertex2f(fx, yLow); glVertex2f(fx, yHigh); } for (float fy = yLow; fy < yHigh; fy += GridStep) { glVertex2f(xLow, fy); glVertex2f(xHigh, fy); } } if (unitX <= 64.0f) { glColor4f(0.5f, 0.5f, 0.5f, 1.0f); for (float fx = xLow; fx <= xHigh; fx += 1.0f) { glVertex2f(fx, yLow); glVertex2f(fx, yHigh); } for (float fy = yLow; fy < yHigh; fy += 1.0f) { glVertex2f(xLow, fy); glVertex2f(xHigh, fy); } } glColor4f(1.0f, 0.0f, 0.0f, 1.0f); glVertex2f(0.0f, 0.0f); glVertex2f(xHigh, 0.0f); glColor4f(0.5f, 0.0f, 0.0f, 1.0f); glVertex2f(0.0f, 0.0f); glVertex2f(xLow, 0.0f); glColor4f(0.0f, 1.0f, 0.0f, 1.0f); glVertex2f(0.0f, 0.0f); glVertex2f(0.0f, yHigh); glColor4f(0.0f, 0.5f, 0.0f, 1.0f); glVertex2f(0.0f, 0.0f); glVertex2f(0.0f, yLow); glEnd(); } /// /// Draw the unit square. /// void GLWidget::DrawUnitSquare() { glLineWidth(1.0f); glBegin(GL_LINES); glColor4f(1.0f, 1.0f, 1.0f, 0.25f); glVertex2f(-1, -1); glVertex2f( 1, -1); glVertex2f(-1, 1); glVertex2f( 1, 1); glVertex2f(-1, -1); glVertex2f(-1, 1); glVertex2f( 1, -1); glVertex2f( 1, 1); glColor4f(1.0f, 0.0f, 0.0f, 0.5f); glVertex2f(-1, 0); glVertex2f( 1, 0); glColor4f(0.0f, 1.0f, 0.0f, 0.5f); glVertex2f( 0, -1); glVertex2f( 0, 1); glEnd(); } /// /// Draw the pre or post affine circle for the passed in xform. /// For drawing affine transforms, multiply the identity model view matrix by the /// affine for each xform, so that all points are considered to be "1". /// /// A pointer to the xform whose affine will be drawn /// True for pre affine, else false for post. /// True if selected (draw enclosing circle), else false (only draw axes). template void GLEmberController::DrawAffine(Xform* xform, bool pre, bool selected) { Ember* ember = m_FractoriumEmberController->CurrentEmber(); bool final = ember->IsFinalXform(xform); int index = ember->GetXformIndex(xform); size_t size = ember->m_Palette.m_Entries.size(); v4T color = ember->m_Palette.m_Entries[Clamp(xform->m_ColorX * size, 0, size - 1)]; Affine2D* affine = pre ? &xform->m_Affine : &xform->m_Post; //For some incredibly strange reason, even though glm and OpenGL use matrices with a column-major //data layout, nothing will work here unless they are flipped to row major order. This is how it was //done in Fractron. m4T mat = affine->ToMat4RowMajor(); m_GL->glPushMatrix(); m_GL->glLoadIdentity(); MultMatrix(mat); m_GL->glLineWidth(3.0f);//One 3px wide, colored black, except green on x axis for post affine. m_GL->DrawAffineHelper(index, selected, pre, final, true); m_GL->glLineWidth(1.0f);//Again 1px wide, colored white, to give a white middle with black outline effect. m_GL->DrawAffineHelper(index, selected, pre, final, false); m_GL->glPointSize(5.0f);//Three black points, one in the center and two on the circle. Drawn big 5px first to give a black outline. m_GL->glBegin(GL_POINTS); m_GL->glColor4f(0.0f, 0.0f, 0.0f, selected ? 1.0f : 0.5f); m_GL->glVertex2f(0.0f, 0.0f); m_GL->glVertex2f(1.0f, 0.0f); m_GL->glVertex2f(0.0f, 1.0f); m_GL->glEnd(); m_GL->glLineWidth(2.0f);//Draw lines again for y axis only, without drawing the circle, using the color of the selected xform. m_GL->glBegin(GL_LINES); m_GL->glColor4f(color.r, color.g, color.b, 1.0f); m_GL->glVertex2f(0.0f, 0.0f); m_GL->glVertex2f(0.0f, 1.0f); m_GL->glEnd(); m_GL->glPointSize(3.0f);//Draw smaller white points, to give a black outline effect. m_GL->glBegin(GL_POINTS); m_GL->glColor4f(1.0f, 1.0f, 1.0f, selected ? 1.0f : 0.5f); m_GL->glVertex2f(0.0f, 0.0f); m_GL->glVertex2f(1.0f, 0.0f); m_GL->glVertex2f(0.0f, 1.0f); m_GL->glEnd(); m_GL->glPopMatrix(); } /// /// Draw the axes, and optionally the surrounding circle /// of an affine transform. /// /// /// True if selected (draw enclosing circle), else false (only draw axes). /// /// /// void GLWidget::DrawAffineHelper(int index, bool selected, bool pre, bool final, bool background) { float px = 1.0f; float py = 0.0f; QColor col = final ? m_Fractorium->m_FinalXformComboColor : m_Fractorium->m_XformComboColors[index % XFORM_COLOR_COUNT]; glBegin(GL_LINES); //Circle part. if (!background) { glColor4f(col.redF(), col.greenF(), col.blueF(), 1.0f);//Draw pre affine transform with white. } else { glColor4f(0.0f, 0.0f, 0.0f, 1.0f);//Draw pre affine transform outline with white. } if (selected) { for (size_t i = 1; i <= 64; i++)//The circle. { float theta = float(M_PI) * 2.0f * float(i % 64) / 64.0f; float fx = float(cos(theta)); float fy = float(sin(theta)); glVertex2f(px, py); glVertex2f(fx, fy); px = fx; py = fy; } } //Lines from center to circle. if (!background) { glColor4f(col.redF(), col.greenF(), col.blueF(), 1.0f); } else { if (pre) glColor4f(0.0f, 0.0f, 0.0f, 1.0f);//Draw pre affine transform outline with white. else glColor4f(0.0f, 0.75f, 0.0f, 1.0f);//Draw post affine transform outline with green. } //The lines from the center to the circle. glVertex2f(0.0f, 0.0f);//X axis. glVertex2f(1.0f, 0.0f); if (background) glColor4f(0.0f, 0.0f, 0.0f, 1.0f); glVertex2f(0.0f, 0.0f);//Y axis. glVertex2f(0.0f, 1.0f); glEnd(); } /// /// Determine the index of the xform being hovered over if any. /// Give precedence to the currently selected xform, if any. /// /// The mouse raster coordinates to check /// The index of the xform being hovered over, else -1 if no hover. template int GLEmberController::UpdateHover(v3T& glCoords) { bool pre = m_Fractorium->ui.PreAffineGroupBox->isChecked(); bool post = m_Fractorium->ui.PostAffineGroupBox->isChecked(); bool preAll = pre && m_Fractorium->DrawAllPre(); bool postAll = post && m_Fractorium->DrawAllPost(); uint bestIndex = -1; T bestDist = 10; Ember* ember = m_FractoriumEmberController->CurrentEmber(); m_HoverType = eHoverType::HoverNone; //If there's a selected/current xform, check it first so it gets precedence over the others. if (m_SelectedXform) { //These checks prevent highlighting the pre/post selected xform circle, when one is set to show all, and the other //is set to show current, and the user hovers over another xform, but doesn't select it, then moves the mouse //back over the hidden circle for the pre/post that was set to only show current. bool checkSelPre = preAll || (pre && m_HoverXform == m_SelectedXform); bool checkSelPost = postAll || (post && m_HoverXform == m_SelectedXform); if (CheckXformHover(m_SelectedXform, glCoords, bestDist, checkSelPre, checkSelPost)) { m_HoverXform = m_SelectedXform; bestIndex = ember->GetTotalXformIndex(m_SelectedXform); } } //Check all xforms. for (size_t i = 0; i < ember->TotalXformCount(); i++) { Xform* xform = ember->GetTotalXform(i); if (preAll || (pre && m_HoverXform == xform))//Only check pre affine if they are shown. { if (CheckXformHover(xform, glCoords, bestDist, true, false)) { m_HoverXform = xform; bestIndex = i; } } if (postAll || (post && m_HoverXform == xform))//Only check post affine if they are shown. { if (CheckXformHover(xform, glCoords, bestDist, false, true)) { m_HoverXform = xform; bestIndex = i; } } } return bestIndex; } /// /// Determine the passed in xform's pre/post affine transforms are being hovered over. /// Meant to be called in succession when checking all xforms for hover, and the best /// hover distance is recorded in the bestDist reference parameter. /// Mouse coordinates will be converted internally to world cartesian coordinates for checking. /// /// A pointer to the xform to check for hover /// The mouse raster coordinates to check /// Reference to hold the best distance found so far /// True to check pre affine, else don't. /// True to check post affine, else don't. /// True if hovering and the distance is smaller than the bestDist parameter template bool GLEmberController::CheckXformHover(Xform* xform, v3T& glCoords, T& bestDist, bool pre, bool post) { bool preFound = false, postFound = false; float dist = 0.0f; v3T pos; Ember* ember = m_FractoriumEmberController->CurrentEmber(); if (pre) { v3T translation(xform->m_Affine.C()/* - ember->m_CenterX*/, /*ember->m_CenterY + */xform->m_Affine.F(), 0); v3T transScreen = glm::project(translation, m_Modelview, m_Projection, m_Viewport); v3T xAxis(xform->m_Affine.A(), xform->m_Affine.D(), 0); v3T xAxisScreen = glm::project(translation + xAxis, m_Modelview, m_Projection, m_Viewport); v3T yAxis(xform->m_Affine.B(), xform->m_Affine.E(), 0); v3T yAxisScreen = glm::project(translation + yAxis, m_Modelview, m_Projection, m_Viewport); pos = translation; dist = glm::distance(glCoords, transScreen); if (dist < bestDist) { bestDist = dist; m_HoverType = eHoverType::HoverTranslation; m_HoverHandlePos = pos; preFound = true; } pos = translation + xAxis; dist = glm::distance(glCoords, xAxisScreen); if (dist < bestDist) { bestDist = dist; m_HoverType = eHoverType::HoverXAxis; m_HoverHandlePos = pos; preFound = true; } pos = translation + yAxis; dist = glm::distance(glCoords, yAxisScreen); if (dist < bestDist) { bestDist = dist; m_HoverType = eHoverType::HoverYAxis; m_HoverHandlePos = pos; preFound = true; } if (preFound) m_AffineType = eAffineType::AffinePre; } if (post) { v3T translation(xform->m_Post.C()/* - ember->m_CenterX*/, /*ember->m_CenterY + */xform->m_Post.F(), 0); v3T transScreen = glm::project(translation, m_Modelview, m_Projection, m_Viewport); v3T xAxis(xform->m_Post.A(), xform->m_Post.D(), 0); v3T xAxisScreen = glm::project(translation + xAxis, m_Modelview, m_Projection, m_Viewport); v3T yAxis(xform->m_Post.B(), xform->m_Post.E(), 0); v3T yAxisScreen = glm::project(translation + yAxis, m_Modelview, m_Projection, m_Viewport); pos = translation; dist = glm::distance(glCoords, transScreen); if (dist < bestDist) { bestDist = dist; m_HoverType = eHoverType::HoverTranslation; m_HoverHandlePos = pos; postFound = true; } pos = translation + xAxis; dist = glm::distance(glCoords, xAxisScreen); if (dist < bestDist) { bestDist = dist; m_HoverType = eHoverType::HoverXAxis; m_HoverHandlePos = pos; postFound = true; } pos = translation + yAxis; dist = glm::distance(glCoords, yAxisScreen); if (dist < bestDist) { bestDist = dist; m_HoverType = eHoverType::HoverYAxis; m_HoverHandlePos = pos; postFound = true; } if (postFound) m_AffineType = eAffineType::AffinePost; } return preFound || postFound; } /// /// Calculate the new affine transform when dragging with the x axis with the left mouse button. /// The value returned will depend on whether any modifier keys were held down. /// None: Rotate and scale only. /// Local Pivot: /// Shift: Rotate only about affine center. /// Alt: Free transform. /// Shift + Alt: Rotate single axis about affine center. /// Control: Rotate and scale, snapping to grid. /// Control + Shift: Rotate only, snapping to grid. /// Control + Alt: Free transform, snapping to grid. /// Control + Shift + Alt: Rotate single axis about affine center, snapping to grid. /// World Pivot: /// Shift + Alt: Rotate single axis about world center. /// Control + Shift + Alt: Rotate single axis about world center, snapping to grid. /// All others are the same as local pivot. /// /// The new affine transform to be assigned to the selected xform template Affine2D GLEmberController::CalcDragXAxis() { v3T t3, newAxis, newPos; Affine2D result = m_DragSrcTransform; bool worldPivotShiftAlt = !m_Fractorium->LocalPivot() && GetShift() && GetAlt(); if (worldPivotShiftAlt) t3 = v3T(0, 0, 0); else t3 = v3T(m_DragSrcTransform.O(), 0); if (GetShift()) { v3T targetAxis = m_MouseWorldPos - t3; v3T norm = glm::normalize(targetAxis); if (GetControl()) norm = SnapToNormalizedAngle(norm, 24); if (worldPivotShiftAlt) newAxis = norm * glm::length(m_DragSrcTransform.O() + m_DragSrcTransform.X()); else newAxis = norm * glm::length(m_DragSrcTransform.X()); } else { if (GetControl()) newPos = SnapToGrid(m_MouseWorldPos); else newPos = m_MouseWorldPos + m_DragHandleOffset; newAxis = newPos - t3; } if (GetAlt()) { if (worldPivotShiftAlt) result.X(v2T(newAxis) - m_DragSrcTransform.O()); else result.X(v2T(newAxis)); } else { result.RotateScaleXTo(v2T(newAxis)); } m_DragHandlePos = v3T(result.O() + result.X(), 0); return result; } /// /// Calculate the new affine transform when dragging with the y axis with the left mouse button. /// The value returned will depend on whether any modifier keys were held down. /// None: Rotate and scale only. /// Local Pivot: /// Shift: Rotate only about affine center. /// Alt: Free transform. /// Shift + Alt: Rotate single axis about affine center. /// Control: Rotate and scale, snapping to grid. /// Control + Shift: Rotate only, snapping to grid. /// Control + Alt: Free transform, snapping to grid. /// Control + Shift + Alt: Rotate single axis about affine center, snapping to grid. /// World Pivot: /// Shift + Alt: Rotate single axis about world center. /// Control + Shift + Alt: Rotate single axis about world center, snapping to grid. /// All others are the same as local pivot. /// /// The new affine transform to be assigned to the selected xform template Affine2D GLEmberController::CalcDragYAxis() { v3T t3, newAxis, newPos; Affine2D result = m_DragSrcTransform; bool worldPivotShiftAlt = !m_Fractorium->LocalPivot() && GetShift() && GetAlt(); if (worldPivotShiftAlt) t3 = v3T(0, 0, 0); else t3 = v3T(m_DragSrcTransform.O(), 0); if (GetShift()) { v3T targetAxis = m_MouseWorldPos - t3; v3T norm = glm::normalize(targetAxis); if (GetControl()) norm = SnapToNormalizedAngle(norm, 24); if (worldPivotShiftAlt) newAxis = norm * glm::length(m_DragSrcTransform.O() + m_DragSrcTransform.Y()); else newAxis = norm * glm::length(m_DragSrcTransform.Y()); } else { if (GetControl()) newPos = SnapToGrid(m_MouseWorldPos); else newPos = m_MouseWorldPos + m_DragHandleOffset; newAxis = newPos - t3; } if (GetAlt()) { if (worldPivotShiftAlt) result.Y(v2T(newAxis) - m_DragSrcTransform.O()); else result.Y(v2T(newAxis)); } else { result.RotateScaleYTo(v2T(newAxis)); } m_DragHandlePos = v3T(result.O() + result.Y(), 0); return result; } /// /// Calculate the new affine transform when dragging the center with the left mouse button. /// The value returned will depend on whether any modifier keys were held down. /// None: Free transform. /// Local Pivot: /// Shift: Rotate about world center, keeping orientation the same. /// Control: Free transform, snapping to grid. /// Control + Shift: Rotate about world center, keeping orientation the same, snapping to grid. /// World Pivot: /// Shift: Rotate about world center, rotating orientation. /// Control + Shift: Rotate about world center, rotating orientation, snapping to grid. /// All others are the same as local pivot. /// /// The new affine transform to be assigned to the selected xform template Affine2D GLEmberController::CalcDragTranslation() { v3T newPos, newX, newY; Affine2D result = m_DragSrcTransform; bool worldPivotShift = !m_Fractorium->LocalPivot() && GetShift(); if (GetShift()) { v3T norm = glm::normalize(m_MouseWorldPos); if (GetControl()) norm = SnapToNormalizedAngle(norm, 12); newPos = glm::length(m_DragSrcTransform.O()) * norm; if (worldPivotShift) { T startAngle = atan2(m_DragSrcTransform.O().y, m_DragSrcTransform.O().x); T endAngle = atan2(newPos.y, newPos.x); T angle = startAngle - endAngle; result.Rotate(angle * RAD_2_DEG); } } else { if (GetControl()) newPos = SnapToGrid(m_MouseWorldPos); else newPos = m_MouseWorldPos + m_DragHandleOffset; } result.O(v2T(newPos)); m_DragHandlePos = newPos; return result; } /// /// Thin wrapper to check if all controllers are ok and return a pointer to the GLController. /// /// A pointer to the GLController if everything is ok, else false. GLEmberControllerBase* GLWidget::GLController() { if (m_Fractorium && m_Fractorium->ControllersOk()) return m_Fractorium->m_Controller->GLController(); return nullptr; } template class GLEmberController; #ifdef DO_DOUBLE template class GLEmberController; #endif