diff options
Diffstat (limited to 'silx/gui/plot3d/scene/viewport.py')
-rw-r--r-- | silx/gui/plot3d/scene/viewport.py | 603 |
1 files changed, 0 insertions, 603 deletions
diff --git a/silx/gui/plot3d/scene/viewport.py b/silx/gui/plot3d/scene/viewport.py deleted file mode 100644 index 6de640e..0000000 --- a/silx/gui/plot3d/scene/viewport.py +++ /dev/null @@ -1,603 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2015-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a class to control a viewport on the rendering window. - -The :class:`Viewport` describes a Viewport rendering a scene. -The attribute :attr:`scene` is the root group of the scene tree. -:class:`RenderContext` handles the current state during rendering. -""" - -from __future__ import absolute_import, division, unicode_literals - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "24/04/2018" - - -import string -import numpy - -from silx.gui.colors import rgba - -from ..._glutils import gl - -from . import camera -from . import event -from . import transform -from .function import DirectionalLight, ClippingPlane, Fog - - -class RenderContext(object): - """Handle a current rendering context. - - An instance of this class is passed to rendering method through - the scene during render. - - User should NEVER use an instance of this class beyond the method - it is passed to as an argument (i.e., do not keep a reference to it). - - :param Viewport viewport: The viewport doing the rendering. - :param Context glContext: The operating system OpenGL context in use. - """ - - _FRAGMENT_SHADER_SRC = string.Template(""" - void scene_post(vec4 cameraPosition) { - gl_FragColor = $fogCall(gl_FragColor, cameraPosition); - } - """) - - def __init__(self, viewport, glContext): - self._viewport = viewport - self._glContext = glContext - self._transformStack = [viewport.camera.extrinsic] - self._clipPlane = ClippingPlane(normal=(0., 0., 0.)) - - # cache - self.__cache = {} - - def cache(self, key, factory, *args, **kwargs): - """Lazy-loading cache to store values in the context for rendering - - :param key: The key to retrieve - :param factory: A callback taking args and kwargs as arguments - and returning the value to store. - :return: The stored or newly allocated value - """ - if key not in self.__cache: - self.__cache[key] = factory(*args, **kwargs) - return self.__cache[key] - - @property - def viewport(self): - """Viewport doing the current rendering""" - return self._viewport - - @property - def glCtx(self): - """The OpenGL context in use""" - return self._glContext - - @property - def objectToCamera(self): - """The current transform from object to camera coords. - - Do not modify. - """ - return self._transformStack[-1] - - @property - def projection(self): - """Projection transform. - - Do not modify. - """ - return self.viewport.camera.intrinsic - - @property - def objectToNDC(self): - """The transform from object to NDC (this includes projection). - - Do not modify. - """ - return transform.StaticTransformList( - (self.projection, self.objectToCamera)) - - def pushTransform(self, transform_, multiply=True): - """Push a :class:`Transform` on the transform stack. - - :param Transform transform_: The transform to add to the stack. - :param bool multiply: - True (the default) to multiply with the top of the stack, - False to push the transform as is without multiplication. - """ - if multiply: - assert len(self._transformStack) >= 1 - transform_ = transform.StaticTransformList( - (self._transformStack[-1], transform_)) - - self._transformStack.append(transform_) - - def popTransform(self): - """Pop the transform on top of the stack. - - :return: The Transform that is popped from the stack. - """ - assert len(self._transformStack) > 1 - return self._transformStack.pop() - - @property - def clipper(self): - """The current clipping plane (ClippingPlane)""" - return self._clipPlane - - def setClipPlane(self, point=(0., 0., 0.), normal=(0., 0., 0.)): - """Set the clipping plane to use - - For now only handles a single clipping plane. - - :param point: A point of the plane - :type point: 3-tuple of float - :param normal: Normal vector of the plane or (0, 0, 0) for no clipping - :type normal: 3-tuple of float - """ - self._clipPlane = ClippingPlane(point, normal) - - def setupProgram(self, program): - """Sets-up uniforms of a program using the context shader functions. - - :param GLProgram program: The program to set-up. - It MUST be in use and using the context function. - """ - self.clipper.setupProgram(self, program) - self.viewport.fog.setupProgram(self, program) - - @property - def fragDecl(self): - """Fragment shader declaration for scene shader functions""" - return '\n'.join(( - self.clipper.fragDecl, - self.viewport.fog.fragDecl, - self._FRAGMENT_SHADER_SRC.substitute( - fogCall=self.viewport.fog.fragCall))) - - @property - def fragCallPre(self): - """Fragment shader call for scene shader functions (to do first) - - It takes the camera position (vec4) as argument. - """ - return self.clipper.fragCall - - @property - def fragCallPost(self): - """Fragment shader call for scene shader functions (to do last) - - It takes the camera position (vec4) as argument. - """ - return "scene_post" - - -class Viewport(event.Notifier): - """Rendering a single scene through a camera in part of a framebuffer. - - :param int framebuffer: The framebuffer ID this viewport is rendering into - """ - - def __init__(self, framebuffer=0): - from . import Group # Here to avoid cyclic import - super(Viewport, self).__init__() - self._dirty = True - self._origin = 0, 0 - self._size = 1, 1 - self._framebuffer = int(framebuffer) - self.scene = Group() # The stuff to render, add overlaid scenes? - self.scene._setParent(self) - self.scene.addListener(self._changed) - self._background = 0., 0., 0., 1. - self._camera = camera.Camera(fovy=30., near=1., far=100., - position=(0., 0., 12.)) - self._camera.addListener(self._changed) - self._transforms = transform.TransformList([self._camera]) - - self._light = DirectionalLight(direction=(0., 0., -1.), - ambient=(0.3, 0.3, 0.3), - diffuse=(0.7, 0.7, 0.7)) - self._light.addListener(self._changed) - self._fog = Fog() - self._fog.isOn = False - self._fog.addListener(self._changed) - - @property - def transforms(self): - """Proxy of camera transforms. - - Do not modify the list. - """ - return self._transforms - - def _changed(self, *args, **kwargs): - """Callback handling scene updates""" - self._dirty = True - self.notify() - - @property - def dirty(self): - """True if scene is dirty and needs redisplay.""" - return self._dirty - - def resetDirty(self): - """Mark the scene as not being dirty. - - To call after rendering. - """ - self._dirty = False - - @property - def background(self): - """Viewport's background color (4-tuple of float in [0, 1] or None) - - The background color is used to clear to viewport. - If None, the viewport is not cleared - """ - return self._background - - @background.setter - def background(self, color): - if color is not None: - color = rgba(color) - if self._background != color: - self._background = color - self._changed() - - @property - def camera(self): - """The camera used to render the scene.""" - return self._camera - - @property - def light(self): - """The light used to render the scene.""" - return self._light - - @property - def fog(self): - """The fog function used to render the scene""" - return self._fog - - @property - def origin(self): - """Origin (ox, oy) of the viewport in pixels""" - return self._origin - - @origin.setter - def origin(self, origin): - ox, oy = origin - origin = int(ox), int(oy) - if origin != self._origin: - self._origin = origin - self._changed() - - @property - def size(self): - """Size (width, height) of the viewport in pixels""" - return self._size - - @size.setter - def size(self, size): - w, h = size - size = int(w), int(h) - if size != self._size: - self._size = size - - self.camera.intrinsic.size = size - self._changed() - - @property - def shape(self): - """Shape (height, width) of the viewport in pixels. - - This is a convenient wrapper to the inverse of size. - """ - return self._size[1], self._size[0] - - @shape.setter - def shape(self, shape): - self.size = shape[1], shape[0] - - @property - def framebuffer(self): - """The framebuffer ID this viewport is rendering into (int)""" - return self._framebuffer - - @framebuffer.setter - def framebuffer(self, framebuffer): - self._framebuffer = int(framebuffer) - - def render(self, glContext): - """Perform the rendering of the viewport - - :param Context glContext: The context used for rendering""" - # Get a chance to run deferred delete - glContext.cleanGLGarbage() - - # OpenGL set-up: really need to be done once - ox, oy = self.origin - w, h = self.size - gl.glViewport(ox, oy, w, h) - - gl.glEnable(gl.GL_SCISSOR_TEST) - gl.glScissor(ox, oy, w, h) - - gl.glEnable(gl.GL_BLEND) - gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) - - gl.glEnable(gl.GL_DEPTH_TEST) - gl.glDepthFunc(gl.GL_LEQUAL) - gl.glDepthRange(0., 1.) - - # gl.glEnable(gl.GL_POLYGON_OFFSET_FILL) - # gl.glPolygonOffset(1., 1.) - - gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) - gl.glEnable(gl.GL_LINE_SMOOTH) - - if self.background is None: - gl.glClear(gl.GL_STENCIL_BUFFER_BIT | - gl.GL_DEPTH_BUFFER_BIT) - else: - gl.glClearColor(*self.background) - - # Prepare OpenGL - gl.glClear(gl.GL_COLOR_BUFFER_BIT | - gl.GL_STENCIL_BUFFER_BIT | - gl.GL_DEPTH_BUFFER_BIT) - - ctx = RenderContext(self, glContext) - self.scene.render(ctx) - self.scene.postRender(ctx) - - def adjustCameraDepthExtent(self): - """Update camera depth extent to fit the scene bounds. - - Only near and far planes are updated. - The scene might still not be fully visible - (e.g., if spanning behind the viewpoint with perspective projection). - """ - bounds = self.scene.bounds(transformed=True) - if bounds is None: - bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)), - dtype=numpy.float32) - bounds = self.camera.extrinsic.transformBounds(bounds) - - if isinstance(self.camera.intrinsic, transform.Perspective): - # This needs to be reworked - zbounds = - bounds[:, 2] - zextent = max(numpy.fabs(zbounds[0] - zbounds[1]), 0.0001) - near = max(zextent / 1000., 0.95 * zbounds[1]) - far = max(near + 0.1, 1.05 * zbounds[0]) - - self.camera.intrinsic.setDepthExtent(near, far) - elif isinstance(self.camera.intrinsic, transform.Orthographic): - # Makes sure z bounds are included - border = max(abs(bounds[:, 2])) - self.camera.intrinsic.setDepthExtent(-border, border) - else: - raise RuntimeError('Unsupported camera', self.camera.intrinsic) - - def resetCamera(self): - """Change camera to have the whole scene in the viewing frustum. - - It updates the camera position and depth extent. - Camera sight direction and up are not affected. - """ - bounds = self.scene.bounds(transformed=True) - if bounds is None: - bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)), - dtype=numpy.float32) - self.camera.resetCamera(bounds) - - def orbitCamera(self, direction, angle=1.): - """Rotate the camera around center of the scene. - - :param str direction: Direction of movement relative to image plane. - In: 'up', 'down', 'left', 'right'. - :param float angle: he angle in degrees of the rotation. - """ - bounds = self.scene.bounds(transformed=True) - if bounds is None: - bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)), - dtype=numpy.float32) - center = 0.5 * (bounds[0] + bounds[1]) - self.camera.orbit(direction, center, angle) - - def moveCamera(self, direction, step=0.1): - """Move the camera relative to the image plane. - - :param str direction: Direction relative to image plane. - One of: 'up', 'down', 'left', 'right', - 'forward', 'backward'. - :param float step: The ratio of data to step for each pan. - """ - bounds = self.scene.bounds(transformed=True) - if bounds is None: - bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)), - dtype=numpy.float32) - bounds = self.camera.extrinsic.transformBounds(bounds) - center = 0.5 * (bounds[0] + bounds[1]) - ndcCenter = self.camera.intrinsic.transformPoint( - center, perspectiveDivide=True) - - step *= 2. # NDC has size 2 - - if direction == 'up': - ndcCenter[1] -= step - elif direction == 'down': - ndcCenter[1] += step - - elif direction == 'right': - ndcCenter[0] -= step - elif direction == 'left': - ndcCenter[0] += step - - elif direction == 'forward': - ndcCenter[2] += step - elif direction == 'backward': - ndcCenter[2] -= step - - else: - raise ValueError('Unsupported direction: %s' % direction) - - newCenter = self.camera.intrinsic.transformPoint( - ndcCenter, direct=False, perspectiveDivide=True) - - self.camera.move(direction, numpy.linalg.norm(newCenter - center)) - - def windowToNdc(self, winX, winY, checkInside=True): - """Convert position from window to normalized device coordinates. - - If window coordinates are int, they are moved half a pixel - to be positioned at the center of pixel. - - :param winX: X window coord, origin left. - :param winY: Y window coord, origin top. - :param bool checkInside: If True, returns None if position is - outside viewport. - :return: (x, y) Normalize device coordinates in [-1, 1] or None. - Origin center, x to the right, y goes upward. - """ - ox, oy = self._origin - width, height = self.size - - # If int, move it to the center of pixel - if isinstance(winX, int): - winX += 0.5 - if isinstance(winY, int): - winY += 0.5 - - x, y = winX - ox, winY - oy - - if checkInside and (x < 0. or x > width or y < 0. or y > height): - return None # Out of viewport - - ndcx = 2. * x / float(width) - 1. - ndcy = 1. - 2. * y / float(height) - return ndcx, ndcy - - def ndcToWindow(self, ndcX, ndcY, checkInside=True): - """Convert position from normalized device coordinates (NDC) to window. - - :param float ndcX: X NDC coord. - :param float ndcY: Y NDC coord. - :param bool checkInside: If True, returns None if position is - outside viewport. - :return: (x, y) window coordinates or None. - Origin top-left, x to the right, y goes downward. - """ - if (checkInside and - (ndcX < -1. or ndcX > 1. or ndcY < -1. or ndcY > 1.)): - return None # Outside viewport - - ox, oy = self._origin - width, height = self.size - - winx = ox + width * 0.5 * (ndcX + 1.) - winy = oy + height * 0.5 * (1. - ndcY) - return winx, winy - - def _pickNdcZGL(self, x, y, offset=0): - """Retrieve depth from depth buffer and return corresponding NDC Z. - - :param int x: In pixels in window coordinates, origin left. - :param int y: In pixels in window coordinates, origin top. - :param int offset: Number of pixels to look at around the given pixel - - :return: Normalize device Z coordinate of depth in [-1, 1] - or None if outside viewport. - :rtype: float or None - """ - ox, oy = self._origin - width, height = self.size - - x = int(x) - y = height - int(y) # Invert y coord - - if x < ox or x > ox + width or y < oy or y > oy + height: - # Outside viewport - return None - - # Get depth from depth buffer in [0., 1.] - # Bind used framebuffer to get depth - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebuffer) - - if offset == 0: # Fast path - # glReadPixels is not GL|ES friendly - depth = gl.glReadPixels( - x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)[0] - else: - offset = abs(int(offset)) - size = 2*offset + 1 - depthPatch = gl.glReadPixels( - x - offset, y - offset, - size, size, - gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT) - depthPatch = depthPatch.ravel() # Work in 1D - - # TODO cache sortedIndices to avoid computing it each time - # Compute distance of each pixels to the center of the patch - offsetToCenter = numpy.arange(- offset, offset + 1, dtype=numpy.float32) ** 2 - sqDistToCenter = numpy.add.outer(offsetToCenter, offsetToCenter) - - # Use distance to center to sort values from the patch - sortedIndices = numpy.argsort(sqDistToCenter.ravel()) - sortedValues = depthPatch[sortedIndices] - - # Take first depth that is not 1 in the sorted values - hits = sortedValues[sortedValues != 1.] - depth = 1. if len(hits) == 0 else hits[0] - - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) - - # Z in NDC in [-1., 1.] - return float(depth) * 2. - 1. - - def _getXZYGL(self, x, y): - ndc = self.windowToNdc(x, y) - if ndc is None: - return None # Outside viewport - ndcz = self._pickNdcZGL(x, y) - ndcpos = numpy.array((ndc[0], ndc[1], ndcz, 1.), dtype=numpy.float32) - - camerapos = self.camera.intrinsic.transformPoint( - ndcpos, direct=False, perspectiveDivide=True) - - scenepos = self.camera.extrinsic.transformPoint(camerapos, - direct=False) - return scenepos[:3] - - def pick(self, x, y): - pass - # ndcX, ndcY = self.windowToNdc(x, y) - # ndcNearPt = ndcX, ndcY, -1. - # ndcFarPT = ndcX, ndcY, 1. |