def createSceneGraph(self): sceneGraph = scenenode.Node() self.worldScene = self.createWorldScene() self.worldScene.setVisibleLayers(self.layerToggleGroup.getVisibleLayers()) clearNode = ClearNode() self.skyNode = sky.SkyNode() self.loadableChunksNode = loadablechunks.LoadableChunksNode(self.dimension) self.matrixNode = MatrixNode() self._updateMatrices() self.matrixNode.addChild(self.loadableChunksNode) self.matrixNode.addChild(self.worldScene) self.matrixNode.addChild(self.overlayNode) sceneGraph.addChild(clearNode) sceneGraph.addChild(self.skyNode) sceneGraph.addChild(self.matrixNode) sceneGraph.addChild(self.compassOrthoNode) if self.cursorNode: self.matrixNode.addChild(self.cursorNode) return sceneGraph
class WorldView(QGLWidget): """ Superclass for the following views: IsoWorldView: Display the world using an isometric viewing angle, without perspective. Click and drag to pan the viewing area. Use the mouse wheel or a UI control to zoom. CameraWorldView: Display the world using a first-person viewing angle with perspective. Click and drag to pan the camera. Use WASD or click and drag to move the camera. CutawayWorldView: Display a single slice of the world. Click and drag to move sideways. Use the mouse wheel or a UI widget to move forward or backward. Use a UI widget to zoom. FourUpWorldView: Display up to four other world views at once. Default to three cutaways and one isometric view. """ viewportMoved = QtCore.Signal(QtGui.QWidget) cursorMoved = QtCore.Signal(QtGui.QMouseEvent) urlsDropped = QtCore.Signal(QtCore.QMimeData, Vector, faces.Face) mapItemDropped = QtCore.Signal(QtCore.QMimeData, Vector, faces.Face) mouseBlockPos = Vector(0, 0, 0) mouseBlockFace = faces.FaceYIncreasing def __init__(self, dimension, textureAtlas=None, geometryCache=None, sharedGLWidget=None): """ :param dimension: :type dimension: WorldEditorDimension :param textureAtlas: :type textureAtlas: TextureAtlas :param geometryCache: :type geometryCache: GeometryCache :param sharedGLWidget: :type sharedGLWidget: QGLWidget :return: :rtype: """ QGLWidget.__init__(self, shareWidget=sharedGLWidget) self.dimension = None self.worldScene = None self.loadableChunksNode = None self.textureAtlas = None validateWidgetQGLContext(self) self.setAcceptDrops(True) self.setSizePolicy(QtGui.QSizePolicy.Policy.Expanding, QtGui.QSizePolicy.Policy.Expanding) self.setFocusPolicy(Qt.ClickFocus) self.layerToggleGroup = LayerToggleGroup() self.layerToggleGroup.layerToggled.connect(self.setLayerVisible) self.mouseRay = Ray(Vector(0, 1, 0), Vector(0, -1, 0)) self.setMouseTracking(True) self.lastAutoUpdate = time.time() self.autoUpdateInterval = 0.5 # frequency of screen redraws in response to loaded chunks self.compassNode = self.createCompass() self.compassOrthoNode = OrthoNode((1, float(self.height()) / self.width())) self.compassOrthoNode.addChild(self.compassNode) self.viewActions = [] self.pressedKeys = set() self.setTextureAtlas(textureAtlas) if geometryCache is None and sharedGLWidget is not None: geometryCache = sharedGLWidget.geometryCache if geometryCache is None: geometryCache = GeometryCache() self.geometryCache = geometryCache self.matrixNode = None self.skyNode = None self.overlayNode = scenenode.Node() self.sceneGraph = None self.renderGraph = None self.frameSamples = deque(maxlen=500) self.frameSamples.append(time.time()) self.cursorNode = None self.setDimension(dimension) acceptableMimeTypes = [ MimeFormats.MapItem, ] def dragEnterEvent(self, event): # xxx show drop preview as scene node print("DRAG ENTER. FORMATS:\n%s" % event.mimeData().formats()) for mimeType in self.acceptableMimeTypes: if event.mimeData().hasFormat(mimeType): event.acceptProposedAction() return if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): mimeData = event.mimeData() x = event.pos().x() y = event.pos().y() ray = self.rayAtPosition(x, y) dropPosition, face = self.rayCastInView(ray) if mimeData.hasFormat(MimeFormats.MapItem): self.mapItemDropped.emit(mimeData, dropPosition, face) elif mimeData.hasUrls: self.urlsDropped.emit(mimeData, dropPosition, face) def setDimension(self, dimension): """ :param dimension: :type dimension: WorldEditorDimension :return: :rtype: """ log.info("Changing %s to dimension %s", self, dimension) self.dimension = dimension self.makeCurrent() if self.renderGraph: self.renderGraph.destroy() self.sceneGraph = self.createSceneGraph() self.renderGraph = rendernode.createRenderNode(self.sceneGraph) self.resetLoadOrder() self.update() def setTextureAtlas(self, textureAtlas): self.textureAtlas = textureAtlas if self.worldScene: self.worldScene.setTextureAtlas(textureAtlas) if textureAtlas is not None: self.makeCurrent() textureAtlas.load() self.resetLoadOrder() def destroy(self): self.makeCurrent() self.renderGraph.destroy() super(WorldView, self).destroy() def __str__(self): try: if self.dimension: dimName = displayName(self.dimension.worldEditor.filename) + ": " + self.dimension.dimName else: dimName = "None" except Exception as e: return "%s trying to get node name" % e return "%s(%r)" % (self.__class__.__name__, dimName) def createCompass(self): return compass.CompassNode() def createWorldScene(self): return worldscene.WorldScene(self.dimension, self.textureAtlas, self.geometryCache) def createSceneGraph(self): sceneGraph = scenenode.Node() self.worldScene = self.createWorldScene() self.worldScene.setVisibleLayers(self.layerToggleGroup.getVisibleLayers()) clearNode = ClearNode() self.skyNode = sky.SkyNode() self.loadableChunksNode = loadablechunks.LoadableChunksNode(self.dimension) self.matrixNode = MatrixNode() self._updateMatrices() self.matrixNode.addChild(self.loadableChunksNode) self.matrixNode.addChild(self.worldScene) self.matrixNode.addChild(self.overlayNode) sceneGraph.addChild(clearNode) sceneGraph.addChild(self.skyNode) sceneGraph.addChild(self.matrixNode) sceneGraph.addChild(self.compassOrthoNode) if self.cursorNode: self.matrixNode.addChild(self.cursorNode) return sceneGraph def initializeGL(self, *args, **kwargs): GL.glEnableClientState(GL.GL_VERTEX_ARRAY) GL.glAlphaFunc(GL.GL_NOTEQUAL, 0) GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA) GL.glEnable(GL.GL_DEPTH_TEST) def setToolCursor(self, cursorNode): if self.cursorNode: self.matrixNode.removeChild(self.cursorNode) self.cursorNode = cursorNode if cursorNode: self.matrixNode.addChild(cursorNode) def setToolOverlays(self, overlayNodes): self.overlayNode.clear() for node in overlayNodes: self.overlayNode.addChild(node) def _updateMatrices(self): self.updateMatrices() self.updateFrustum() #min = self.unprojectPoint(0, 0)[0] #max = self.unprojectPoint(self.width(), self.height())[0] #self.visibleBox = BoundingBox(min, (0, 0, 0)).union(BoundingBox(max, (0, 0, 0))) def updateMatrices(self): """ Subclasses must implement updateMatrices to set the projection and modelview matrices. Should set self.matrixNode.projection and self.matrixNode.modelview """ raise NotImplementedError def updateFrustum(self): matrix = self.matrixNode.projection * self.matrixNode.modelview self.frustum = Frustum.fromViewingMatrix(numpy.array(matrix.data())) def getViewCorners(self): """ Returns corners: bottom left, near bottom left, far top left, near top left, far bottom right, near bottom right, far top right, near top right, far :return: :rtype: list[QVector4D] """ corners = [QtGui.QVector4D(x, y, z, 1.) for x, y, z in itertools.product((-1., 1.), (-1., 1.), (0., 1. ))] matrix = self.matrixNode.projection * self.matrixNode.modelview matrix, inverted = matrix.inverted() worldCorners = [matrix.map(corner) for corner in corners] worldCorners = [Vector(*((corner / corner.w()).toTuple()[:3])) for corner in worldCorners] return worldCorners def getViewBounds(self): """ Get the corners of the viewing area, intersected with the world's bounds. xxx raycast to intersect with terrain height too :return: :rtype: """ corners = self.getViewCorners() # Convert the 4 corners into rays extending from the near point, then interpolate each ray at the # current dimension's height limits pairs = [] for i in range(0, 8, 2): pairs.append(corners[i:i+2]) rays = [Ray.fromPoints(p1, p2) for p1, p2 in pairs] bounds = self.dimension.bounds pointPairs = [(r.atHeight(bounds.maxy), r.atHeight(bounds.miny)) for r in rays] return sum(pointPairs, ()) def resizeGL(self, width, height): GL.glViewport(0, 0, width, height) self._updateMatrices() def resizeEvent(self, event): center = self.viewCenter() self.compassOrthoNode.size = (1, float(self.height()) / self.width()) super(WorldView, self).resizeEvent(event) # log.info("WorldView: resized. moving to %s", center) # self.centerOnPoint(center) _centerPoint = Vector(0, 0, 0) @property def centerPoint(self): return self._centerPoint @centerPoint.setter def centerPoint(self, value): value = Vector(*value) if value != self._centerPoint: self._centerPoint = value self._updateMatrices() log.debug("update(): centerPoint %s %s", self, value) self.update() self.resetLoadOrder() self.viewportMoved.emit(self) scaleChanged = QtCore.Signal(float) _scale = 1. / 16 @property def scale(self): return self._scale @scale.setter def scale(self, value): self._scale = value self._updateMatrices() log.debug("update(): scale %s %s", self, value) self.update() self.scaleChanged.emit(value) self.viewportMoved.emit(self) def centerOnPoint(self, pos, distance=None): """Center the view on the given position""" # delta = self.viewCenter() - self.centerPoint # self.centerPoint = pos - delta self.centerPoint = pos self.update() def viewCenter(self): """Return the world position at the center of the view.""" #return self.unprojectAtHeight(self.width() / 2, self.height() / 2, 64.) # ray = self.rayAtPosition(self.width() / 2, self.height() / 2) # try: # point, face = raycast.rayCastInBounds(ray, self.dimension, 600) # except (raycast.MaxDistanceError, ValueError): # point = ray.atHeight(0) # return point or ray.point return self.centerPoint def _anglesToVector(self, yaw, pitch): def nanzero(x): if math.isnan(x): return 0 else: return x dx = -math.sin(math.radians(yaw)) * math.cos(math.radians(pitch)) dy = -math.sin(math.radians(pitch)) dz = math.cos(math.radians(yaw)) * math.cos(math.radians(pitch)) return Vector(*map(nanzero, [dx, dy, dz])) dragStart = None def keyPressEvent(self, event): self.augmentKeyEvent(event) self.pressedKeys.add(event.key()) for action in self.viewActions: if action.matchKeyEvent(event): action.keyPressEvent(event) def keyReleaseEvent(self, event): self.augmentKeyEvent(event) self.pressedKeys.discard(event.key()) for action in self.viewActions: if action.matchKeyEvent(event): action.keyReleaseEvent(event) def mousePressEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.button & event.button(): if action.matchModifiers(event): if not action.key or action.key in self.pressedKeys: action.mousePressEvent(event) def mouseMoveEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if not action.button or action.button == event.buttons() or action.button & event.buttons(): # Important! mouseMove checks event.buttons(), press and release check event.button() if action.matchModifiers(event): if not action.key or action.key in self.pressedKeys: action.mouseMoveEvent(event) self.cursorMoved.emit(event) self.update() def mouseReleaseEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.button & event.button(): if action.matchModifiers(event): if not action.key or action.key in self.pressedKeys: action.mouseReleaseEvent(event) wheelPos = 0 def wheelEvent(self, event): self.augmentMouseEvent(event) for action in self.viewActions: if action.acceptsMouseWheel and ((action.modifiers & event.modifiers()) or action.modifiers == event.modifiers()): self.wheelPos += event.delta() # event.delta reports eighths of a degree. a standard wheel tick is 15 degrees, or 120 eighths. # keep count of wheel position and emit an event for each 15 degrees turned. # xxx will we ever need sub-click precision for wheel events? clicks = 0 while self.wheelPos >= 120: self.wheelPos -= 120 clicks += 1 while self.wheelPos <= -120: self.wheelPos += 120 clicks -= 1 if action.button == action.WHEEL_UP and clicks > 0: for i in range(abs(clicks)): action.keyPressEvent(event) if action.button == action.WHEEL_DOWN and clicks < 0: for i in range(abs(clicks)): action.keyPressEvent(event) def augmentMouseEvent(self, event): x, y = event.x(), event.y() return self.augmentEvent(x, y, event) def augmentKeyEvent(self, event): globalPos = QtGui.QCursor.pos() mousePos = self.mapFromGlobal(globalPos) x = mousePos.x() y = mousePos.y() # xxx fake position of mouse event -- need to use mcedit internal event object already event.x = lambda: x event.y = lambda: y return self.augmentEvent(x, y, event) @profiler.function def augmentEvent(self, x, y, event): ray = self.rayAtPosition(x, y) event.ray = ray event.view = self position, face = self.rayCastInView(ray) self.mouseBlockPos = event.blockPosition = position self.mouseBlockFace = event.blockFace = face self.mouseRay = ray def rayCastInView(self, ray): try: result = raycast.rayCastInBounds(ray, self.dimension, maxDistance=200) position, face = result except (raycast.MaxDistanceError, ValueError): # GL.glReadBuffer(GL.GL_BACK) # pixel = GL.glReadPixels(x, self.height() - y, 1, 1, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT) # depth = -1 + 2 * pixel[0, 0] # p = self.pointsAtPositions((x, y, depth))[0] # # face = faces.FaceYIncreasing # position = p.intfloor() defaultDistance = 200 position = (ray.point + ray.vector * defaultDistance).intfloor() face = faces.FaceUp return position, face maxFPS = 30 @profiler.function def glDraw(self, *args, **kwargs): frameInterval = 1.0 / self.maxFPS if time.time() - self.frameSamples[-1] < frameInterval: return super(WorldView, self).glDraw(*args, **kwargs) def paintGL(self): with profiler.context("paintGL: %s" % self): self.frameSamples.append(time.time()) if self.textureAtlas: self.textureAtlas.update() with profiler.context("renderScene"): rendernode.renderScene(self.renderGraph) @property def fps(self): samples = 3 if len(self.frameSamples) <= samples: return 0.0 return (samples - 1) / (self.frameSamples[-1] - self.frameSamples[-samples]) def rayAtPosition(self, x, y): """ Given coordinates in screen space, return a ray in 3D space. Parameters: x and y are coordinates local to this QWidget :rtype: Ray """ p0, p1 = self.pointsAtPositions((x, y, 0.0), (x, y, 1.0)) return Ray(p0, (p1 - p0).normalize()) def rayAtCenter(self): return self.rayAtPosition(self.width()/2, self.height()/2) def pointsAtPositions(self, *screenPoints): w = float(self.width()) h = float(self.height()) matrix = self.matrixNode.projection * self.matrixNode.modelview inverse, ok = matrix.inverted() if not ok or 0 in (w, h): return [Vector(0, 0, 0) for i in screenPoints] def _unproject(): for x, y, z in screenPoints: x = float(x) y = float(y) y = h - y x = 2 * x / w - 1 y = 2 * y / h - 1 def v(p): return Vector(p.x(), p.y(), p.z()) yield v(inverse.map(QtGui.QVector3D(x, y, z))) return list(_unproject()) def unprojectAtHeight(self, x, y, h): """ Given coordinates in screen space, find the corresponding point in 3D space. Like rayAtPosition, but the third parameter is a height value in 3D space. """ ray = self.rayAtPosition(x, y) return ray.atHeight(h) _chunkIter = None def resetLoadOrder(self): self._chunkIter = None def makeChunkIter(self): x, y, z = self.viewCenter() return iterateChunks(x, z, 1 + max(self.width() * self.scale, self.height() * self.scale) // 32) def requestChunk(self): if self._chunkIter is None: self._chunkIter = self.makeChunkIter() try: for c in self._chunkIter: if self.worldScene.wantsChunk(c): return c except StopIteration: pass def wantsChunk(self, c): if not self.worldScene.wantsChunk(c): return False if hasattr(self, 'frustum'): point = [ c[0] * 16 + 8, self.dimension.bounds.miny + self.dimension.bounds.height / 2, c[1] * 16 + 8, 1.0 ] return self.frustum.visible1(point=point, radius=self.dimension.bounds.height / 2) return True def chunkNotPresent(self, cPos): self.worldScene.chunkNotPresent(cPos) self.loadableChunksNode.dirty = True # gross. def recieveChunk(self, chunk): t = time.time() if self.lastAutoUpdate + self.autoUpdateInterval < t: self.lastAutoUpdate = t log.debug("update(): receivedChunk %s %s", self, chunk) self.update() with profiler.getProfiler().context("preloadCulling"): if hasattr(self, 'frustum'): cx, cz = chunk.chunkPosition points = [(cx * 16 + 8, h + 8, cz * 16 + 8, 1.0) for h in chunk.sectionPositions()] points = numpy.array(points) visibleSections = self.frustum.visible(points, radius=8 * math.sqrt(2)) else: visibleSections = None return self.worldScene.workOnChunk(chunk, visibleSections) def chunkInvalid(self, (cx, cz)): self.worldScene.invalidateChunk(cx, cz) self.resetLoadOrder()