VTK: problem about coordinate convertion: display vs world - python

I am using VTK in my project, and I meet a problem about the coordinate convertion: from display coordinate vs world coordiante.
First, I show a cone and set the window size to (500, 500). Then, I calcualte the world point using the (250, 250) display point. The display point (250, 250) is located in the cone.
picker = vtk.vtkPropPicker()
picker.Pick(250, 250, 0, self.ren)
if picker.GetActor():
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(250, 250, 0)
position = coordinate.GetComputedWorldValue(self.ren)
self.position = position
I show this point as a red point.
Then, I rotate the screen, and I want to convert the world point as the display point. In my thought, the red point would move along with the cone.
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToWorld()
point = self.position
coordinate.SetValue(point[0], point[1], point[2])
displayCoor = coordinate.GetComputedDisplayValue(self.ren)
However, the red point is seperated from the cone. Is there something wrong with the coordinate convertion?
The whole code is:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys, os
import numpy as np
import vtk, qimage2ndarray
class vtkLabel(QLabel):
def __init__(self):
super(vtkLabel, self).__init__()
self.setFixedSize(500, 500)
cone = vtk.vtkConeSource()
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(cone.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
ren = vtk.vtkRenderer()
ren.AddActor(actor)
renWin = vtk.vtkRenderWindow()
renWin.AddRenderer(ren)
renWin.SetOffScreenRendering(1)
imageFilter = vtk.vtkWindowToImageFilter()
imageFilter.SetInput(renWin)
self.ren = ren
self.renWin = renWin
self.imageFilter = imageFilter
def mousePressEvent(self, QMouseEvent):
super().mousePressEvent(QMouseEvent)
pos = QMouseEvent.pos()
x = pos.x()
y = pos.y()
self.lastPos = [x, y]
def mouseReleaseEvent(self, QMouseEvent):
super().mouseReleaseEvent(QMouseEvent)
self.lastPos = None
def mouseMoveEvent(self, QMouseEvent):
super().mouseMoveEvent(QMouseEvent)
pos = QMouseEvent.pos()
x = pos.x()
y = pos.y()
if self.lastPos != None:
if QMouseEvent.buttons() == Qt.RightButton:
dy = self.lastPos[1] - y
center = self.ren.GetCenter()
dyf = dy/center[1]
import math
val = math.pow(1.1, dyf)
self.ren.GetActiveCamera().Dolly(val)
if QMouseEvent.buttons() == Qt.LeftButton:
dx = x - self.lastPos[0]
dy = self.lastPos[1] - y
size = self.renWin.GetSize()
delta_elevation = -20.0 / size[1]
delta_azimuth = -20.0 / size[0]
rxf = dx * delta_azimuth
ryf = dy * delta_elevation
camera = self.ren.GetActiveCamera()
camera.Azimuth(rxf)
camera.Elevation(ryf)
camera.OrthogonalizeViewUp()
self.ren.ResetCameraClippingRange()
self.ren.UpdateLightsGeometryToFollowCamera()
self.ren.Render()
self.calculationForDisplay()
def resizeEvent(self, QMouseEvent):
super().resizeEvent(QMouseEvent)
self.renWin.SetSize(self.width(), self.height())
self.calculationForDisplay()
picker = vtk.vtkPropPicker()
picker.Pick(250, 250, 0, self.ren)
if picker.GetActor():
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(250, 250, 0)
position = coordinate.GetComputedWorldValue(self.ren)
self.position = position
print('the point is one of the cone point')
else:
raise RuntimeError('the point is not one of the cone point')
def calculationForDisplay(self):
self.renWin.Render()
self.imageFilter.Modified()
self.imageFilter.Update()
displayImg = self.imageFilter.GetOutput()
dims = displayImg.GetDimensions()
from vtk.util.numpy_support import vtk_to_numpy
numImg = vtk_to_numpy(displayImg.GetPointData().GetScalars())
numImg = numImg.reshape(dims[1], dims[0], 3)
numImg = numImg.transpose(0, 1, 2)
numImg = np.flipud(numImg)
displayQImg = qimage2ndarray.array2qimage(numImg)
pixmap = QPixmap.fromImage(displayQImg)
self.pixmap = pixmap
self.update()
def paintEvent(self, QPaintEvent):
super(vtkLabel, self).paintEvent(QPaintEvent)
painter = QPainter(self)
width = self.width()
height = self.height()
painter.drawPixmap(QPoint(0, 0), self.pixmap, QRect(0, 0, width, height))
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToWorld()
point = self.position
coordinate.SetValue(point[0], point[1], point[2])
displayCoor = coordinate.GetComputedDisplayValue(self.ren)
x = displayCoor[0]
y = displayCoor[1]
y = self.height() - y
painter.setPen(QPen(QColor(255, 0, 0), 5))
painter.drawPoint(x, y)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setFixedSize(500, 500)
self.imageLabel = vtkLabel()
self.setCentralWidget(self.imageLabel)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Update:
Finally, I know what's wrong with my code.
In the resizeEvent, I obtain the world point by:
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(250, 250, 0)
position = coordinate.GetComputedWorldValue(self.ren)
Actually, the z should not be 0. We should use the following code to obtain the point:
picker = vtk.vtkPropPicker()
picker.Pick(250, 250, 0, self.ren)
pos = picker.GetPickPosition()
Then, everything is OK.

Related

Make graphics item move around another item instead of passing through

I have a graphics scene with QGraphicsEllipseitem circles that are movable. I am trying to have the one I am dragging move around the other circle instead of allowing them to overlap aka collide. So far I was able to stop the collision but its not moving around smoothly it snaps to a corner. I dont know how to fix it.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import math
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
self.scene().views()[0].parent().movearound()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.gscene = QGraphicsScene(0, 0, 1000, 1000)
gview = QGraphicsView(self.gscene)
self.setCentralWidget(gview)
self.circle1 = Circleitem (123, brush=QColor(255,255,0))
self.circle2 =Circleitem(80, brush=QColor(0,255,0))
self.gscene.addItem(self.circle1)
self.gscene.addItem(self.circle2)
self.circle1.setPos(500, 500)
self.circle2.setPos(300, 300)
self.show()
def movearound(self):
if self.gscene.selectedItems()[0] == self.circle1:
moveditem = self.circle1
stillitem = self.circle2
else:
moveditem = self.circle2
stillitem = self.circle1
if len(self.gscene.collidingItems(moveditem)) != 0:
xdist = moveditem.x() - stillitem.x()
ydist = moveditem.y() - stillitem.y()
totaldist = moveditem.rect().width()/2 + stillitem.rect().width()/2
totaldist *= math.sqrt(1 + pow(math.pi, 2)/10)/2
if ( abs(xdist) < totaldist or abs(ydist) < totaldist ):
if xdist > 0:
x = stillitem.x() + totaldist
else:
x = stillitem.x() - totaldist
if ydist > 0:
y = stillitem.y() + totaldist
else:
y = stillitem.y() - totaldist
moveditem.setPos(x, y)
app = QApplication([])
win = MainWindow()
app.exec()
It is simpler to keep the logic in Circleitem.mouseMoveEvent and use QLineF to find the distance and new position.
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
colliding = self.collidingItems()
if colliding:
item = colliding[0] # Add offset if points are equal so length > 0
line = QLineF(item.pos(), self.pos() + QPoint(self.pos() == item.pos(), 0))
min_distance = (self.rect().width() + item.rect().width()) / 2
if line.length() < min_distance:
line.setLength(min_distance)
self.setPos(line.p2())

Getting mouse position relative to image limits

I display an image within a custom QLabel and get clicks on this label. I'm interested in clicks within the image only, and in a position expressed by a number between 0 and 1, 0 being the leftmost or topmost pixel and 1 the rightmost or bottommost pixel, regardless of the image actual size.
I can't get the image rectangle to compute the position. When I call self.pixmap.rect(), width and height are the original image dimensions, not the dimensions of the image which was scaled to fit into the label.
What am I doing doing wrong?
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QGridLayout
from PyQt5.QtGui import QPixmap
class Window(QWidget):
def __init__(self):
super().__init__()
image_path = '092.jpg'
image_area = Image_Area(QPixmap(image_path))
image_area.left_click.connect(self.image_clicked)
layout = QGridLayout()
layout.addWidget(image_area, 0, 0)
self.setLayout(layout)
# Left click on image
def image_clicked(self, x, y):
print(f'{x:.3f},{y:.3f}')
class Image_Area(QLabel):
left_click = pyqtSignal(float, float)
def __init__(self, pixmap):
super().__init__()
self.pixmap = pixmap
self.setPixmap(pixmap)
self.setScaledContents(False)
self.setMinimumSize(100, 100)
def resizeEvent(self, e):
if self.pixmap != None:
width, height = int(self.width()), int(self.height())
scaled_pixmap = self.pixmap.scaled(width, height, Qt.KeepAspectRatio)
self.setPixmap(scaled_pixmap)
return super().resizeEvent(e)
def mousePressEvent(self, e):
x, y, = e.x(), e.y()
if self.pixmap:
# Get pixmap rectangle
r = self.pixmap.rect()
x0, y0 = r.x(), r.y()
x1, y1 = x0+r.width(), y0+r.height()
# Check we clicked on the pixmap
if x >= x0 and x < x1 and y >= y0 and y < y1:
# emit position relative to pixmap bottom-left corner
x_relative = (x - x0) / (x1 - x0)
y_relative = (y - y0) / (y1 - y0)
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)
app = QApplication([])
window = Window()
window.show()
app.exec()
Update: After the answer was provided, I realized the computation of the click position relative to the image origin was wrong, here is an accurate version for the persons interested:
def mousePressEvent(self, e):
# Mouse position is in label coordinates
x, y, = e.x(), e.y()
if self.pixmap():
# pixmap is not a widget, we don't have its location,
# so we assume the pixmap is centered in the label and
# compute the location with respect to label
label_size = self.size()
pixmap_size = self.pixmap().size()
width = pixmap_size.width()
height = pixmap_size.height()
x0 = int((label_size.width() - width) / 2)
y0 = int((label_size.height() - height) / 2)
# Check we clicked on the pixmap
if (x >= x0 and x < (x0 + width) and
y >= y0 and y < (y0 + height)):
# emit position relative to pixmap top-left corner
x_relative = (x - x0) / width
y_relative = (y - y0) / height
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)
You have to use the QPixmap established in the QLabel that can be obtained through the pixmap() method. The problem is that you are obfuscating the access to that method since you have an attribute with a similar name. So the solution is to use pixmap() and rename the attribute pixmap.
class Image_Area(QLabel):
left_click = pyqtSignal(float, float)
def __init__(self, pixmap):
super().__init__()
self._pixmap = pixmap
self.setPixmap(pixmap)
self.setScaledContents(False)
self.setMinimumSize(100, 100)
def resizeEvent(self, e):
if self._pixmap is not None:
scaled_pixmap = self._pixmap.scaled(self.size(), Qt.KeepAspectRatio)
self.setPixmap(scaled_pixmap)
return super().resizeEvent(e)
def mousePressEvent(self, e):
x, y, = (
e.x(),
e.y(),
)
if self.pixmap:
r = self.pixmap().rect()
x0, y0 = r.x(), r.y()
x1, y1 = x0 + r.width(), y0 + r.height()
if x >= x0 and x < x1 and y >= y0 and y < y1:
x_relative = (x - x0) / (x1 - x0)
y_relative = (y - y0) / (y1 - y0)
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)

Creating Custom Time Picker Widget

I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

How to implement zoom towards mouse like in 3dsMax?

I'm trying to mimick the 3dsmax behaviour when you zoom in/out by moving the mouse wheel. In 3ds max this zooming will be towards the mouse position. So far I've come up with this little mcve:
import math
from ctypes import c_void_p
import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from glm import *
class Camera():
def __init__(
self,
eye=None, target=None, up=None,
fov=None, near=0.1, far=100000,
**kwargs
):
self.eye = vec3(eye) or vec3(0, 0, 1)
self.target = vec3(target) or vec3(0, 0, 0)
self.up = vec3(up) or vec3(0, 1, 0)
self.original_up = vec3(self.up)
self.fov = fov or radians(45)
self.near = near
self.far = far
def update(self, aspect):
self.view = lookAt(self.eye, self.target, self.up)
self.projection = perspective(self.fov, aspect, self.near, self.far)
def zoom(self, *args):
delta = -args[1] * 0.1
distance = length(self.target - self.eye)
self.eye = self.target + (self.eye - self.target) * (delta + 1)
def zoom_towards_cursor(self, *args):
x = args[2]
y = args[3]
v = glGetIntegerv(GL_VIEWPORT)
viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
height = viewport.z
p0 = vec3(x, height - y, 0.0)
p1 = vec3(x, height - y, 1.0)
v1 = unProject(p0, self.view, self.projection, viewport)
v2 = unProject(p1, self.view, self.projection, viewport)
world_from = vec3(
(-v1.z * (v2.x - v1.x)) / (v2.z - v1.z) + v1.x,
(-v1.z * (v2.y - v1.y)) / (v2.z - v1.z) + v1.y,
0.0
)
self.eye.z = self.eye.z * (1.0 + 0.1 * args[1])
view = lookAt(self.eye, self.target, self.up)
v1 = unProject(p0, view, self.projection, viewport)
v2 = unProject(p1, view, self.projection, viewport)
world_to = vec3(
(v1.z * (v2.x - v1.x)) / (v2.z - v1.z) + v1.x,
(-v1.z * (v2.y - v1.y)) / (v2.z - v1.z) + v1.y,
0.0
)
offset = world_to - world_from
print(self.eye.z, world_from, world_to, offset)
self.eye += offset
self.target += offset
class GlutController():
def __init__(self, camera):
self.camera = camera
self.zoom = self.camera.zoom
def glut_mouse_wheel(self, *args):
self.zoom(*args)
class MyWindow:
def __init__(self, w, h):
self.width = w
self.height = h
glutInit()
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
glutInitWindowSize(w, h)
glutCreateWindow('OpenGL Window')
self.startup()
glutReshapeFunc(self.reshape)
glutDisplayFunc(self.display)
glutMouseWheelFunc(self.controller.glut_mouse_wheel)
glutKeyboardFunc(self.keyboard_func)
glutIdleFunc(self.idle_func)
def keyboard_func(self, *args):
try:
key = args[0].decode("utf8")
if key == "\x1b":
glutLeaveMainLoop()
if key in ['1']:
self.controller.zoom = self.camera.zoom
print("Using normal zoom")
elif key in ['2']:
self.controller.zoom = self.camera.zoom_towards_cursor
print("Using zoom towards mouse")
except Exception as e:
import traceback
traceback.print_exc()
def startup(self):
glEnable(GL_DEPTH_TEST)
aspect = self.width / self.height
params = {
"eye": vec3(10, 10, 10),
"target": vec3(0, 0, 0),
"up": vec3(0, 1, 0)
}
self.cameras = [
Camera(**params)
]
self.camera = self.cameras[0]
self.model = mat4(1)
self.controller = GlutController(self.camera)
def run(self):
glutMainLoop()
def idle_func(self):
glutPostRedisplay()
def reshape(self, w, h):
glViewport(0, 0, w, h)
self.width = w
self.height = h
def display(self):
self.camera.update(self.width / self.height)
glClearColor(0.2, 0.3, 0.3, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(degrees(self.camera.fov), self.width / self.height, self.camera.near, self.camera.far)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
e = self.camera.eye
t = self.camera.target
u = self.camera.up
gluLookAt(e.x, e.y, e.z, t.x, t.y, t.z, u.x, u.y, u.z)
glColor3f(1, 1, 1)
glBegin(GL_LINES)
for i in range(-5, 6):
if i == 0:
continue
glVertex3f(-5, 0, i)
glVertex3f(5, 0, i)
glVertex3f(i, 0, -5)
glVertex3f(i, 0, 5)
glEnd()
glBegin(GL_LINES)
glColor3f(1, 1, 1)
glVertex3f(-5, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, -5)
glVertex3f(0, 0, 0)
glColor3f(1, 0, 0)
glVertex3f(0, 0, 0)
glVertex3f(5, 0, 0)
glColor3f(0, 1, 0)
glVertex3f(0, 0, 0)
glVertex3f(0, 5, 0)
glColor3f(0, 0, 1)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, 5)
glEnd()
glutSwapBuffers()
if __name__ == '__main__':
window = MyWindow(800, 600)
window.run()
In this snippet you can switch between 2 zooming modes by pressing keys '1' or '2' keys.
When pressing '1' I'm doing an standard zooming, so far so good.
Problem is when pressing '2', in this case I've tried to adapt code from this thread to python/pyopengl/pygml but because I didn't understand very well the underlying maths of that answer I don't know very well how to fix the bad behaviour.
How would you fix the posted code so it will zoom in/out towards the mouse properly like 3dsmax?
A possible solution is to move the camera along a ray, from the camera position through the cursor (mouse) position and to move the target position in parallel.
self.eye = self.eye + ray_cursor * delta
self.target = self.target + ray_cursor * delta
For this the window position of the cursor has to be "un-projected" (unProject).
Calculate the cursor position in world space (e.g. on the far plane):
pt_wnd = vec3(x, height - y, 1.0)
pt_world = unProject(pt_wnd, self.view, self.projection, viewport)
The ray from the eye position through the cursor is given by the the normalized vector from the eye position to the world space cursor position:
ray_cursor = normalize(pt_world - self.eye)
There is an issue in your code when you get the window height from the viewport rectangle, because the height is the .w component rather than the .z component:
v = glGetIntegerv(GL_VIEWPORT)
viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
width = viewport.z
height = viewport.w
Full code listing of the function zoom_towards_cursor:
def zoom_towards_cursor(self, *args):
x = args[2]
y = args[3]
v = glGetIntegerv(GL_VIEWPORT)
viewport = vec4(float(v[0]), float(v[1]), float(v[2]), float(v[3]))
width = viewport.z
height = viewport.w
pt_wnd = vec3(x, height - y, 1.0)
pt_world = unProject(pt_wnd, self.view, self.projection, viewport)
ray_cursor = normalize(pt_world - self.eye)
delta = -args[1]
self.eye = self.eye + ray_cursor * delta
self.target = self.target + ray_cursor * delta
See also Python OpenGL 4.6, GLM navigation
Preview:

Sprite in Pyglet not doing what I want

I am trying to have a circle that, when clicked, moves somewhere else on the screen. However, when I click the circle, nothing happens.
#IMPORT STUFF
import pyglet as pg
from random import randint
mouse = pg.window.mouse
#VARS
window = pg.window.Window(width = 640, height = 480)
score = 0
circleImg = pg.image.load("circle.png")
circle = pg.sprite.Sprite(circleImg, randint(1, window.width), randint(1, window.height))
text = pg.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
#DETECT MOUSE PRESS ON CIRCLE
#window.event
def on_mouse_press(x, y, button, modifiers):
if x == circle.x and y == circle.y:
circle.x = randint(1, window.width)
circle.y = randint(1, window.height)
#window.event
def on_draw():
window.clear()
text.draw()
circle.draw()
pg.app.run()
import pyglet
from pyglet.gl import *
from random import randint
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
class Circle(pyglet.sprite.Sprite):
def __init__(self, radiance=5, x=0, y=0):
self.texture = pyglet.image.load('circle.png')
super(Circle, self).__init__(self.texture)
def click(self, x, y):
if x >= self.x and y >= self.y:
if x <= self.x + self.texture.width and y <= self.y + self.texture.height:
return self
mouse = pyglet.window.mouse
#VARS
window = pyglet.window.Window(width = 640, height = 480)
score = 0
#circleImg = pyglet.image.load("circle.png")
#circle = pyglet.sprite.Sprite(circleImg, randint(1, window.width), randint(1, window.height))
circle = Circle(x=50, y=50)
text = pyglet.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
#DETECT MOUSE PRESS ON CIRCLE
#window.event
def on_mouse_press(x, y, button, modifiers):
if circle.click(x, y):
print('Clicked in circle')
circle.x = randint(0, window.width - 10)
circle.y = randint(0, window.height - 10)
#window.event
def on_draw():
window.clear()
text.draw()
circle.draw()
pyglet.app.run()
A short description of what this does is it creates a custom class called Circle that inherits the Sprite class. It loads the circle.png as a texture with a alpha channel that gets blended by the GL library.
We add a custom function called click that checks if the lowest x,y coordinates are higher than the circles lowest x,y, then we check if the cursor is below x+width and same for y of the image region.
If that's the case, we return the circle sprite class as a True value in case we want to use the sprite.
Future enhancements:
You should draw the circle using gl functions, hence why I've defined radiance in the class definitions. However radiance here is never used, it's a placeholder for the future.
This is so you can use math to defined if you actually clicked within the circle, but this is beyond my scope of quick answers.. I would have to do a lot of debugging myself in order to get the math to add up (it's not my strong side).
What makes it work now is that we use the image width, height, x and y data to crudely check if we're within the image, aka "the circle".
trying to draw over sprite or change picture pyglet
As a bonus, I'll add this answer to the list of enhancements because it contains some stuff that might be useful. One would be to replace 90% of your code with a custom pyglet.window.Window class to replace global variables and decorators and stuff.
And it would look something like this:
import pyglet
from pyglet.gl import *
from random import randint
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
key = pyglet.window.key
class Circle(pyglet.sprite.Sprite):
def __init__(self, radiance=5, x=0, y=0):
self.texture = pyglet.image.load('circle.png')
super(Circle, self).__init__(self.texture)
def click(self, x, y):
if x >= self.x and y >= self.y:
if x <= self.x + self.texture.width and y <= self.y + self.texture.height:
return self
class MainScreen(pyglet.window.Window):
def __init__ (self):
super(MainScreen, self).__init__(800, 600, fullscreen = False)
self.x, self.y = 0, 0
self.bg = pyglet.sprite.Sprite(pyglet.image.load('background.jpg'))
self.sprites = {}
self.sprites['circle'] = Circle(x=50, y=50)
self.sprites['label'] = pyglet.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_mouse_press(self, x, y, button, modifiers):
if self.sprites['circle'].click(x, y):
print('Clicked in circle')
self.sprites['circle'].x = randint(0, self.width - 10)
self.sprites['circle'].y = randint(0, self.height - 10)
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
def render(self):
self.clear()
self.bg.draw()
for sprite_name, sprite_obj in self.sprites.items():
sprite_obj.draw()
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
x = MainScreen()
x.run()
I'm not familiar with pyglet, but I'm guessing the problem is that you're checking whether x == circle.x etc, which means it only moves when you click the single pixel at the exact centre of the circle. Try some kind of maximum distance from the centre (e.g. a hypotenuse math.sqrt( (x-circle.x)**2 + (y-circle.y)**2) < circle.radius

Categories

Resources