summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/scene
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/scene')
-rw-r--r--silx/gui/plot3d/scene/axes.py19
-rw-r--r--silx/gui/plot3d/scene/function.py128
-rw-r--r--silx/gui/plot3d/scene/interaction.py29
-rw-r--r--silx/gui/plot3d/scene/primitives.py7
-rw-r--r--silx/gui/plot3d/scene/viewport.py15
-rw-r--r--silx/gui/plot3d/scene/window.py11
6 files changed, 134 insertions, 75 deletions
diff --git a/silx/gui/plot3d/scene/axes.py b/silx/gui/plot3d/scene/axes.py
index 528e4f7..520ef3e 100644
--- a/silx/gui/plot3d/scene/axes.py
+++ b/silx/gui/plot3d/scene/axes.py
@@ -52,6 +52,8 @@ class LabelledAxes(primitives.GroupBBox):
self._font = text.Font()
+ self._boxVisibility = True
+
# TODO offset labels from anchor in pixels
self._xlabel = text.Text2D(font=self._font)
@@ -145,6 +147,21 @@ class LabelledAxes(primitives.GroupBBox):
def zlabel(self, text):
self._zlabel.text = text
+ @property
+ def boxVisible(self):
+ """Returns bounding box, axes labels and grid visibility."""
+ return self._boxVisibility
+
+ @boxVisible.setter
+ def boxVisible(self, visible):
+ self._boxVisibility = bool(visible)
+ for child in self._children:
+ if child == self._tickLines:
+ if self._ticksForBounds is not None:
+ child.visible = self._boxVisibility
+ elif child != self._group:
+ child.visible = self._boxVisibility
+
def _updateTicks(self):
"""Check if ticks need update and update them if needed."""
bounds = self._group.bounds(transformed=False, dataBounds=True)
@@ -187,7 +204,7 @@ class LabelledAxes(primitives.GroupBBox):
zcoords[:, 3, 1] += ticklength[1] # Z ticks on YZ plane
self._tickLines.setPositions(coords.reshape(-1, 3))
- self._tickLines.visible = True
+ self._tickLines.visible = self._boxVisibility
# Update labels
color = self.tickColor
diff --git a/silx/gui/plot3d/scene/function.py b/silx/gui/plot3d/scene/function.py
index 80ac820..73cdb72 100644
--- a/silx/gui/plot3d/scene/function.py
+++ b/silx/gui/plot3d/scene/function.py
@@ -35,6 +35,7 @@ import contextlib
import logging
import numpy
+from ... import _glutils
from ..._glutils import gl
from . import event
@@ -296,18 +297,10 @@ class DirectionalLight(event.Notifier, ProgramFunction):
class Colormap(event.Notifier, ProgramFunction):
# TODO use colors for out-of-bound values, for <=0 with log, for nan
- # TODO texture-based colormap
decl = """
- #define CMAP_GRAY 0
- #define CMAP_R_GRAY 1
- #define CMAP_RED 2
- #define CMAP_GREEN 3
- #define CMAP_BLUE 4
- #define CMAP_TEMP 5
-
uniform struct {
- int id;
+ sampler2D texture;
bool isLog;
float min;
float oneOverRange;
@@ -328,60 +321,24 @@ class Colormap(event.Notifier, ProgramFunction):
value = clamp(cmap.oneOverRange * (value - cmap.min), 0.0, 1.0);
}
- if (cmap.id == CMAP_GRAY) {
- return vec4(value, value, value, 1.0);
- }
- else if (cmap.id == CMAP_R_GRAY) {
- float invValue = 1.0 - value;
- return vec4(invValue, invValue, invValue, 1.0);
- }
- else if (cmap.id == CMAP_RED) {
- return vec4(value, 0.0, 0.0, 1.0);
- }
- else if (cmap.id == CMAP_GREEN) {
- return vec4(0.0, value, 0.0, 1.0);
- }
- else if (cmap.id == CMAP_BLUE) {
- return vec4(0.0, 0.0, value, 1.0);
- }
- else if (cmap.id == CMAP_TEMP) {
- //red: 0.5->0.75: 0->1
- //green: 0.->0.25: 0->1; 0.75->1.: 1->0
- //blue: 0.25->0.5: 1->0
- return vec4(
- clamp(4.0 * value - 2.0, 0.0, 1.0),
- 1.0 - clamp(4.0 * abs(value - 0.5) - 1.0, 0.0, 1.0),
- 1.0 - clamp(4.0 * value - 1.0, 0.0, 1.0),
- 1.0);
- }
- else {
- /* Unknown colormap */
- return vec4(0.0, 0.0, 0.0, 1.0);
- }
+ vec4 color = texture2D(cmap.texture, vec2(value, 0.5));
+ return color;
}
"""
call = "colormap"
- _COLORMAPS = {
- 'gray': 0,
- 'reversed gray': 1,
- 'red': 2,
- 'green': 3,
- 'blue': 4,
- 'temperature': 5
- }
-
- COLORMAPS = tuple(_COLORMAPS.keys())
- """Tuple of supported colormap names."""
-
NORMS = 'linear', 'log'
"""Tuple of supported normalizations."""
- def __init__(self, name='gray', norm='linear', range_=(1., 10.)):
+ _COLORMAP_TEXTURE_UNIT = 1
+ """Texture unit to use for storing the colormap"""
+
+ def __init__(self, colormap=None, norm='linear', range_=(1., 10.)):
"""Shader function to apply a colormap to a value.
- :param str name: Name of the colormap.
+ :param colormap: RGB(A) color look-up table (default: gray)
+ :param colormap: numpy.ndarray of numpy.uint8 of dimension Nx3 or Nx4
:param str norm: Normalization to apply: 'linear' (default) or 'log'.
:param range_: Range of value to map to the colormap.
:type range_: 2-tuple of float (begin, end).
@@ -389,24 +346,35 @@ class Colormap(event.Notifier, ProgramFunction):
super(Colormap, self).__init__()
# Init privates to default
- self._name, self._norm, self._range = 'gray', 'linear', (1., 10.)
+ self._colormap, self._norm, self._range = None, 'linear', (1., 10.)
+
+ self._texture = None
+ self._update_texture = True
+
+ if colormap is None:
+ # default colormap
+ colormap = numpy.empty((256, 3), dtype=numpy.uint8)
+ colormap[:] = numpy.arange(256,
+ dtype=numpy.uint8)[:, numpy.newaxis]
# Set to param values through properties to go through asserts
- self.name = name
+ self.colormap = colormap
self.norm = norm
self.range_ = range_
@property
- def name(self):
- """Name of the colormap in use."""
- return self._name
-
- @name.setter
- def name(self, name):
- if name != self._name:
- assert name in self.COLORMAPS
- self._name = name
- self.notify()
+ def colormap(self):
+ """Color look-up table to use."""
+ return numpy.array(self._colormap, copy=True)
+
+ @colormap.setter
+ def colormap(self, colormap):
+ colormap = numpy.array(colormap, copy=True)
+ assert colormap.ndim == 2
+ assert colormap.shape[1] in (3, 4)
+ self._colormap = colormap
+ self._update_texture = True
+ self.notify()
@property
def norm(self):
@@ -459,7 +427,15 @@ class Colormap(event.Notifier, ProgramFunction):
:param GLProgram program: The program to set-up.
It MUST be in use and using this function.
"""
- gl.glUniform1i(program.uniforms['cmap.id'], self._COLORMAPS[self.name])
+ self.prepareGL2(context) # TODO see how to handle
+
+ if self._texture is None: # No colormap
+ return
+
+ self._texture.bind()
+
+ gl.glUniform1i(program.uniforms['cmap.texture'],
+ self._texture.texUnit)
gl.glUniform1i(program.uniforms['cmap.isLog'], self._norm == 'log')
min_, max_ = self.range_
@@ -469,3 +445,23 @@ class Colormap(event.Notifier, ProgramFunction):
gl.glUniform1f(program.uniforms['cmap.min'], min_)
gl.glUniform1f(program.uniforms['cmap.oneOverRange'],
(1. / (max_ - min_)) if max_ != min_ else 0.)
+
+ def prepareGL2(self, context):
+ if self._texture is None or self._update_texture:
+ if self._texture is not None:
+ self._texture.discard()
+
+ colormap = numpy.empty(
+ (16, self._colormap.shape[0], self._colormap.shape[1]),
+ dtype=self._colormap.dtype)
+ colormap[:] = self._colormap
+
+ format_ = gl.GL_RGBA if colormap.shape[-1] == 4 else gl.GL_RGB
+
+ self._texture = _glutils.Texture(
+ format_, colormap, format_,
+ texUnit=self._COLORMAP_TEXTURE_UNIT,
+ minFilter=gl.GL_NEAREST,
+ magFilter=gl.GL_NEAREST,
+ wrap=gl.GL_CLAMP_TO_EDGE)
+ self._update_texture = False
diff --git a/silx/gui/plot3d/scene/interaction.py b/silx/gui/plot3d/scene/interaction.py
index 68bfc13..2911b2c 100644
--- a/silx/gui/plot3d/scene/interaction.py
+++ b/silx/gui/plot3d/scene/interaction.py
@@ -440,6 +440,26 @@ class FocusManager(StateMachine):
# CameraControl ###############################################################
+class RotateCameraControl(FocusManager):
+ """Combine wheel and rotate state machine."""
+ def __init__(self, viewport,
+ orbitAroundCenter=False,
+ mode='center', scaleTransform=None):
+ handlers = (CameraWheel(viewport, mode, scaleTransform),
+ CameraRotate(viewport, orbitAroundCenter, LEFT_BTN))
+ super(RotateCameraControl, self).__init__(handlers)
+
+
+class PanCameraControl(FocusManager):
+ """Combine wheel, selectPan and rotate state machine."""
+ def __init__(self, viewport,
+ mode='center', scaleTransform=None,
+ selectCB=None):
+ handlers = (CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB))
+ super(PanCameraControl, self).__init__(handlers)
+
+
class CameraControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine."""
def __init__(self, viewport,
@@ -650,3 +670,12 @@ class PanPlaneRotateCameraControl(FocusManager):
orbitAroundCenter=False,
button=RIGHT_BTN))
super(PanPlaneRotateCameraControl, self).__init__(handlers)
+
+
+class PanPlaneZoomOnWheelControl(FocusManager):
+ """Combine zoom on wheel and pan plane state machines."""
+ def __init__(self, viewport, plane,
+ mode='center', scaleTransform=None):
+ handlers = (CameraWheel(viewport, mode, scaleTransform),
+ PlanePan(viewport, plane, LEFT_BTN))
+ super(PanPlaneZoomOnWheelControl, self).__init__(handlers)
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
index ca2616a..fc38e09 100644
--- a/silx/gui/plot3d/scene/primitives.py
+++ b/silx/gui/plot3d/scene/primitives.py
@@ -292,8 +292,11 @@ class Geometry(core.Elem):
self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
# Support vertex with to 2 to 4 coordinates
positions = self._attributes['position']
- self.__bounds[0, :positions.shape[1]] = positions.min(axis=0)[:3]
- self.__bounds[1, :positions.shape[1]] = positions.max(axis=0)[:3]
+ self.__bounds[0, :positions.shape[1]] = \
+ numpy.nanmin(positions, axis=0)[:3]
+ self.__bounds[1, :positions.shape[1]] = \
+ numpy.nanmax(positions, axis=0)[:3]
+ self.__bounds[numpy.isnan(self.__bounds)] = 0. # Avoid NaNs
return self.__bounds.copy()
def prepareGL2(self, ctx):
diff --git a/silx/gui/plot3d/scene/viewport.py b/silx/gui/plot3d/scene/viewport.py
index 83cda43..72e1ea3 100644
--- a/silx/gui/plot3d/scene/viewport.py
+++ b/silx/gui/plot3d/scene/viewport.py
@@ -314,6 +314,9 @@ class Viewport(event.Notifier):
(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):
@@ -337,7 +340,11 @@ class Viewport(event.Notifier):
It updates the camera position and depth extent.
Camera sight direction and up are not affected.
"""
- self.camera.resetCamera(self.scene.bounds(transformed=True))
+ 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.
@@ -347,6 +354,9 @@ class Viewport(event.Notifier):
: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)
@@ -359,6 +369,9 @@ class Viewport(event.Notifier):
: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(
diff --git a/silx/gui/plot3d/scene/window.py b/silx/gui/plot3d/scene/window.py
index ad7e6e5..3c63c7a 100644
--- a/silx/gui/plot3d/scene/window.py
+++ b/silx/gui/plot3d/scene/window.py
@@ -244,6 +244,7 @@ class Window(event.Notifier):
void main(void) {
gl_FragColor = texture2D(texture, textureCoord);
+ gl_FragColor.a = 1.0;
}
""")
@@ -304,12 +305,11 @@ class Window(event.Notifier):
self._viewports.removeListener(self._updated)
self._viewports = event.NotifierList(iterable)
self._viewports.addListener(self._updated)
- self._dirty = True
+ self._updated(self)
def _updated(self, source, *args, **kwargs):
- if source is not self:
- self._dirty = True
- self.notify(*args, **kwargs)
+ self._dirty = True
+ self.notify(*args, **kwargs)
framebufferid = property(lambda self: self._framebufferid,
doc="Framebuffer ID used to perform rendering")
@@ -323,11 +323,12 @@ class Window(event.Notifier):
height, width = self.shape
image = numpy.empty((height, width, 3), dtype=numpy.uint8)
+ previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebufferid)
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
gl.glReadPixels(
0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, image)
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)
# glReadPixels gives bottom to top,
# while images are stored as top to bottom