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/camera.py2
-rw-r--r--silx/gui/plot3d/scene/core.py11
-rw-r--r--silx/gui/plot3d/scene/cutplane.py18
-rw-r--r--silx/gui/plot3d/scene/function.py87
-rw-r--r--silx/gui/plot3d/scene/interaction.py60
-rw-r--r--silx/gui/plot3d/scene/primitives.py130
-rw-r--r--silx/gui/plot3d/scene/utils.py73
-rw-r--r--silx/gui/plot3d/scene/viewport.py75
8 files changed, 283 insertions, 173 deletions
diff --git a/silx/gui/plot3d/scene/camera.py b/silx/gui/plot3d/scene/camera.py
index acc5899..90de7ed 100644
--- a/silx/gui/plot3d/scene/camera.py
+++ b/silx/gui/plot3d/scene/camera.py
@@ -292,6 +292,8 @@ class Camera(transform.Transform):
center = 0.5 * (bounds[0] + bounds[1])
radius = numpy.linalg.norm(0.5 * (bounds[1] - bounds[0]))
+ if radius == 0.: # bounds are all collapsed
+ radius = 1.
if isinstance(self.intrinsic, transform.Perspective):
# Get the viewpoint distance from the bounds center
diff --git a/silx/gui/plot3d/scene/core.py b/silx/gui/plot3d/scene/core.py
index a293f28..43838fe 100644
--- a/silx/gui/plot3d/scene/core.py
+++ b/silx/gui/plot3d/scene/core.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# 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
@@ -112,6 +112,15 @@ class Base(event.Notifier):
return root if isinstance(root, Viewport) else None
@property
+ def root(self):
+ """The root node of the scene.
+
+ If attached to a :class:`Viewport`, this is the item right under it
+ """
+ path = self.path
+ return path[1] if isinstance(path[0], Viewport) else path[0]
+
+ @property
def objectToNDCTransform(self):
"""Transform from object to normalized device coordinates.
diff --git a/silx/gui/plot3d/scene/cutplane.py b/silx/gui/plot3d/scene/cutplane.py
index 08a9899..81c74c7 100644
--- a/silx/gui/plot3d/scene/cutplane.py
+++ b/silx/gui/plot3d/scene/cutplane.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-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
@@ -79,19 +79,20 @@ class ColormapMesh3D(Geometry):
uniform float alpha;
$colormapDecl
-
- $clippingDecl
+ $sceneDecl
$lightingFunction
void main(void)
{
+ $scenePreCall(vCameraPosition);
+
float value = texture3D(data, vTexCoords).r;
vec4 color = $colormapCall(value);
color.a = alpha;
- $clippingCall(vCameraPosition);
-
gl_FragColor = $lightingCall(color, vPosition, vNormal);
+
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -186,8 +187,9 @@ class ColormapMesh3D(Geometry):
def renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall,
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
colormapDecl=self.colormap.decl,
@@ -216,7 +218,7 @@ class ColormapMesh3D(Geometry):
gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
- ctx.clipper.setupProgram(ctx, program)
+ ctx.setupProgram(program)
self._texture.bind()
self._draw(program)
diff --git a/silx/gui/plot3d/scene/function.py b/silx/gui/plot3d/scene/function.py
index 2921d48..7651f75 100644
--- a/silx/gui/plot3d/scene/function.py
+++ b/silx/gui/plot3d/scene/function.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# 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
@@ -60,6 +60,91 @@ class ProgramFunction(object):
pass
+class Fog(event.Notifier, ProgramFunction):
+ """Linear fog over the whole scene content.
+
+ The background of the viewport is used as fog color,
+ otherwise it defaults to white.
+ """
+ # TODO: add more controls (set fog range), add more fog modes
+
+ _fragDecl = """
+ /* (1/(far - near) or 0, near) z in [0 (camera), -inf[ */
+ uniform vec2 fogExtentInfo;
+
+ /* Color to use as fog color */
+ uniform vec3 fogColor;
+
+ vec4 fog(vec4 color, vec4 cameraPosition) {
+ /* d = (pos - near) / (far - near) */
+ float distance = fogExtentInfo.x * (cameraPosition.z/cameraPosition.w - fogExtentInfo.y);
+ float fogFactor = clamp(distance, 0.0, 1.0);
+ vec3 rgb = mix(color.rgb, fogColor, fogFactor);
+ return vec4(rgb.r, rgb.g, rgb.b, color.a);
+ }
+ """
+
+ _fragDeclNoop = """
+ vec4 fog(vec4 color, vec4 cameraPosition) {
+ return color;
+ }
+ """
+
+ def __init__(self):
+ super(Fog, self).__init__()
+ self._isOn = True
+
+ @property
+ def isOn(self):
+ """True to enable fog, False to disable (bool)"""
+ return self._isOn
+
+ @isOn.setter
+ def isOn(self, isOn):
+ isOn = bool(isOn)
+ if self._isOn != isOn:
+ self._isOn = bool(isOn)
+ self.notify()
+
+ @property
+ def fragDecl(self):
+ return self._fragDecl if self.isOn else self._fragDeclNoop
+
+ @property
+ def fragCall(self):
+ return "fog"
+
+ @staticmethod
+ def _zExtentCamera(viewport):
+ """Return (far, near) planes Z in camera coordinates.
+
+ :param Viewport viewport:
+ :return: (far, near) position in camera coords (from 0 to -inf)
+ """
+ # Provide scene z extent in camera coords
+ bounds = viewport.camera.extrinsic.transformBounds(
+ viewport.scene.bounds(transformed=True, dataBounds=True))
+ return bounds[:, 2]
+
+ def setupProgram(self, context, program):
+ if not self.isOn:
+ return
+
+ far, near = context.cache(key='zExtentCamera',
+ factory=self._zExtentCamera,
+ viewport=context.viewport)
+ extent = far - near
+ gl.glUniform2f(program.uniforms['fogExtentInfo'],
+ 0.9/extent if extent != 0. else 0.,
+ near)
+
+ # Use background color as fog color
+ bgColor = context.viewport.background
+ if bgColor is None:
+ bgColor = 1., 1., 1.
+ gl.glUniform3f(program.uniforms['fogColor'], *bgColor[:3])
+
+
class ClippingPlane(ProgramFunction):
"""Description of a clipping plane and rendering.
diff --git a/silx/gui/plot3d/scene/interaction.py b/silx/gui/plot3d/scene/interaction.py
index e5cfb6d..14a54dc 100644
--- a/silx/gui/plot3d/scene/interaction.py
+++ b/silx/gui/plot3d/scene/interaction.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# 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
@@ -43,11 +43,11 @@ from . import transform
_logger = logging.getLogger(__name__)
-# ClickOrDrag #################################################################
-
-# TODO merge with silx.gui.plot.Interaction.ClickOrDrag
class ClickOrDrag(StateMachine):
- """Click or drag interaction for a given button."""
+ """Click or drag interaction for a given button.
+
+ """
+ #TODO: merge this class with silx.gui.plot.Interaction.ClickOrDrag
DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
@@ -126,23 +126,29 @@ class ClickOrDrag(StateMachine):
pass
-# CameraRotate ################################################################
-
-class CameraRotate(ClickOrDrag):
+class CameraSelectRotate(ClickOrDrag):
"""Camera rotation using an arcball-like interaction."""
- def __init__(self, viewport, orbitAroundCenter=True, button=RIGHT_BTN):
+ def __init__(self, viewport, orbitAroundCenter=True, button=RIGHT_BTN,
+ selectCB=None):
self._viewport = viewport
self._orbitAroundCenter = orbitAroundCenter
+ self._selectCB = selectCB
self._reset()
- super(CameraRotate, self).__init__(button)
+ super(CameraSelectRotate, self).__init__(button)
def _reset(self):
self._origin, self._center = None, None
self._startExtrinsic = None
def click(self, x, y):
- pass # No interaction yet
+ if self._selectCB is not None:
+ ndcZ = self._viewport._pickNdcZGL(x, y)
+ position = self._viewport._getXZYGL(x, y)
+ # This assume no object lie on the far plane
+ # Alternative, change the depth range so that far is < 1
+ if ndcZ != 1. and position is not None:
+ self._selectCB((x, y, ndcZ), position)
def beginDrag(self, x, y):
centerPos = None
@@ -205,8 +211,6 @@ class CameraRotate(ClickOrDrag):
self._reset()
-# CameraSelectPan #############################################################
-
class CameraSelectPan(ClickOrDrag):
"""Picking on click and pan camera on drag."""
@@ -259,8 +263,6 @@ class CameraSelectPan(ClickOrDrag):
self._lastPosNdc = None
-# CameraWheel #################################################################
-
class CameraWheel(object):
"""StateMachine like class, just handling wheel events."""
@@ -371,8 +373,6 @@ class CameraWheel(object):
return True
-# FocusManager ################################################################
-
class FocusManager(StateMachine):
"""Manages focus across multiple event handlers
@@ -449,8 +449,6 @@ class FocusManager(StateMachine):
handler.cancel()
-# CameraControl ###############################################################
-
class RotateCameraControl(FocusManager):
"""Combine wheel and rotate state machine for left button
and pan when ctrl is pressed
@@ -460,7 +458,8 @@ class RotateCameraControl(FocusManager):
mode='center', scaleTransform=None,
selectCB=None):
handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraRotate(viewport, orbitAroundCenter, LEFT_BTN))
+ CameraSelectRotate(
+ viewport, orbitAroundCenter, LEFT_BTN, selectCB))
ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
CameraSelectPan(viewport, LEFT_BTN, selectCB))
super(RotateCameraControl, self).__init__(handlers, ctrlHandlers)
@@ -476,7 +475,8 @@ class PanCameraControl(FocusManager):
handlers = (CameraWheel(viewport, mode, scaleTransform),
CameraSelectPan(viewport, LEFT_BTN, selectCB))
ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraRotate(viewport, orbitAroundCenter, LEFT_BTN))
+ CameraSelectRotate(
+ viewport, orbitAroundCenter, LEFT_BTN, selectCB))
super(PanCameraControl, self).__init__(handlers, ctrlHandlers)
@@ -488,12 +488,11 @@ class CameraControl(FocusManager):
selectCB=None):
handlers = (CameraWheel(viewport, mode, scaleTransform),
CameraSelectPan(viewport, LEFT_BTN, selectCB),
- CameraRotate(viewport, orbitAroundCenter, RIGHT_BTN))
+ CameraSelectRotate(
+ viewport, orbitAroundCenter, RIGHT_BTN, selectCB))
super(CameraControl, self).__init__(handlers)
-# PlaneRotate #################################################################
-
class PlaneRotate(ClickOrDrag):
"""Plane rotation using arcball interaction.
@@ -603,8 +602,6 @@ class PlaneRotate(ClickOrDrag):
self._reset()
-# PlanePan ###################################################################
-
class PlanePan(ClickOrDrag):
"""Pan a plane along its normal on drag."""
@@ -668,8 +665,6 @@ class PlanePan(ClickOrDrag):
self._beginPlanePoint = None
-# PlaneControl ################################################################
-
class PlaneControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine for plane control."""
def __init__(self, viewport, plane,
@@ -686,9 +681,9 @@ class PanPlaneRotateCameraControl(FocusManager):
mode='center', scaleTransform=None):
handlers = (CameraWheel(viewport, mode, scaleTransform),
PlanePan(viewport, plane, LEFT_BTN),
- CameraRotate(viewport,
- orbitAroundCenter=False,
- button=RIGHT_BTN))
+ CameraSelectRotate(viewport,
+ orbitAroundCenter=False,
+ button=RIGHT_BTN))
super(PanPlaneRotateCameraControl, self).__init__(handlers)
@@ -701,5 +696,6 @@ class PanPlaneZoomOnWheelControl(FocusManager):
handlers = (CameraWheel(viewport, mode, scaleTransform),
PlanePan(viewport, plane, LEFT_BTN))
ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraRotate(viewport, orbitAroundCenter, LEFT_BTN))
+ CameraSelectRotate(
+ viewport, orbitAroundCenter, LEFT_BTN))
super(PanPlaneZoomOnWheelControl, self).__init__(handlers, ctrlHandlers)
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
index ca06e30..08724ba 100644
--- a/silx/gui/plot3d/scene/primitives.py
+++ b/silx/gui/plot3d/scene/primitives.py
@@ -29,8 +29,10 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
-
-import collections
+try:
+ from collections import abc
+except ImportError: # Python2 support
+ import collections as abc
import ctypes
from functools import reduce
import logging
@@ -47,7 +49,7 @@ from . import event
from . import core
from . import transform
from . import utils
-from .function import Colormap
+from .function import Colormap, Fog
_logger = logging.getLogger(__name__)
@@ -146,7 +148,7 @@ class Geometry(core.Elem):
:param bool copy: True to make a copy of the array, False to use as is
"""
# Convert single value (int, float, numpy types) to tuple
- if not isinstance(array, collections.Iterable):
+ if not isinstance(array, abc.Iterable):
array = (array, )
# Makes sure it is an array
@@ -361,9 +363,11 @@ class Geometry(core.Elem):
if attribute.ndim == 1: # Single value
min_ = attribute
max_ = attribute
- else: # Array of values, compute min/max
+ elif len(attribute) > 0: # Array of values, compute min/max
min_ = numpy.nanmin(attribute, axis=0)
max_ = numpy.nanmax(attribute, axis=0)
+ else:
+ min_, max_ = numpy.zeros((2, attribute.shape[1]), dtype=numpy.float32)
toCopy = min(len(min_), 3-index)
if toCopy != len(min_):
@@ -451,13 +455,14 @@ class Lines(Geometry):
varying vec3 vNormal;
varying vec4 vColor;
- $clippingDecl
+ $sceneDecl
$lightingFunction
void main(void)
{
- $clippingCall(vCameraPosition);
+ $scenePreCall(vCameraPosition);
gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -492,8 +497,9 @@ class Lines(Geometry):
fraglightfunction = ctx.viewport.light.fragmentShaderFunctionNoop
fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall,
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost,
lightingFunction=fraglightfunction,
lightingCall=ctx.viewport.light.fragmentCall)
prog = ctx.glCtx.prog(self._shaders[0], fragment)
@@ -509,7 +515,7 @@ class Lines(Geometry):
ctx.objectToCamera.matrix,
safe=True)
- ctx.clipper.setupProgram(ctx, prog)
+ ctx.setupProgram(prog)
with gl.enabled(gl.GL_LINE_SMOOTH, self._smooth):
self._draw(prog)
@@ -560,18 +566,21 @@ class DashedLines(Lines):
uniform vec2 dash;
- $clippingDecl
+ $sceneDecl
$lightingFunction
void main(void)
{
+ $scenePreCall(vCameraPosition);
+
/* Discard off dash fragments */
float lineDist = distance(vOriginFragCoord, gl_FragCoord.xy);
if (mod(lineDist, dash.x + dash.y) > dash.x) {
discard;
}
- $clippingCall(vCameraPosition);
gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
+
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -627,8 +636,9 @@ class DashedLines(Lines):
context.viewport.light.fragmentShaderFunctionNoop
fragment = self._shaders[1].substitute(
- clippingDecl=context.clipper.fragDecl,
- clippingCall=context.clipper.fragCall,
+ sceneDecl=context.fragDecl,
+ scenePreCall=context.fragCallPre,
+ scenePostCall=context.fragCallPost,
lightingFunction=fraglightfunction,
lightingCall=context.viewport.light.fragmentCall)
program = context.glCtx.prog(self._shaders[0], fragment)
@@ -648,7 +658,7 @@ class DashedLines(Lines):
program.uniforms['viewportSize'], *context.viewport.size)
gl.glUniform2f(program.uniforms['dash'], *self.dash)
- context.clipper.setupProgram(context, program)
+ context.setupProgram(program)
self._draw(program)
@@ -1236,14 +1246,12 @@ class _Points(Geometry):
varying $valueType vValue;
$valueToColorDecl
-
- $clippingDecl
-
+ $sceneDecl
$alphaSymbolDecl
void main(void)
{
- $clippingCall(vCameraPosition);
+ $scenePreCall(vCameraPosition);
float alpha = alphaSymbol(gl_PointCoord, vSize);
@@ -1252,6 +1260,8 @@ class _Points(Geometry):
if (gl_FragColor.a == 0.0) {
discard;
}
+
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -1305,8 +1315,9 @@ class _Points(Geometry):
vertexShader = self._shaders[0].substitute(
valueType=valueType)
fragmentShader = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall,
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost,
valueType=valueType,
valueToColorDecl=valueToColorDecl,
valueToColorCall=valueToColorCall,
@@ -1324,7 +1335,7 @@ class _Points(Geometry):
ctx.objectToCamera.matrix,
safe=True)
- ctx.clipper.setupProgram(ctx, program)
+ ctx.setupProgram(program)
self._renderGL2PreDrawHook(ctx, program)
@@ -1475,15 +1486,17 @@ class GridPoints(Geometry):
in vec4 vCameraPosition;
in float vNormValue;
- out vec4 fragColor;
+ out vec4 gl_FragColor;
- $clippingDecl
+ $sceneDecl
void main(void)
{
- $clippingCall(vCameraPosition);
+ $scenePreCall(vCameraPosition);
+
+ gl_FragColor = vec4(0.5 * vNormValue + 0.5, 0.0, 0.0, 1.0);
- fragColor = vec4(0.5 * vNormValue + 0.5, 0.0, 0.0, 1.0);
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -1497,7 +1510,7 @@ class GridPoints(Geometry):
def __init__(self, values=0., shape=None, sizes=1., indices=None,
minValue=None, maxValue=None):
- if isinstance(values, collections.Iterable):
+ if isinstance(values, abc.Iterable):
values = numpy.array(values, copy=False)
# Test if gl_VertexID will overflow
@@ -1532,8 +1545,9 @@ class GridPoints(Geometry):
def renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall)
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost)
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -1546,7 +1560,7 @@ class GridPoints(Geometry):
ctx.objectToCamera.matrix,
safe=True)
- ctx.clipper.setupProgram(ctx, prog)
+ ctx.setupProgram(prog)
gl.glUniform3i(prog.uniforms['gridDims'],
self._shape[2] if len(self._shape) == 3 else 1,
@@ -1632,12 +1646,12 @@ class Spheres(Geometry):
varying float vViewDepth;
varying float vViewRadius;
- $clippingDecl
+ $sceneDecl
$lightingFunction
void main(void)
{
- $clippingCall(vCameraPosition);
+ $scenePreCall(vCameraPosition);
/* Get normal from point coords */
vec3 normal;
@@ -1658,6 +1672,8 @@ class Spheres(Geometry):
float viewDepth = vViewDepth + vViewRadius * normal.z;
vec2 clipZW = viewDepth * projMat[2].zw + projMat[3].zw;
gl_FragDepth = 0.5 * (clipZW.x / clipZW.y) + 0.5;
+
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -1676,8 +1692,9 @@ class Spheres(Geometry):
def renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall,
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall)
prog = ctx.glCtx.prog(self._shaders[0], fragment)
@@ -1694,7 +1711,7 @@ class Spheres(Geometry):
ctx.objectToCamera.matrix,
safe=True)
- ctx.clipper.setupProgram(ctx, prog)
+ ctx.setupProgram(prog)
gl.glUniform2f(prog.uniforms['screenSize'], *ctx.viewport.size)
@@ -1748,14 +1765,16 @@ class Mesh3D(Geometry):
varying vec3 vNormal;
varying vec4 vColor;
- $clippingDecl
+ $sceneDecl
$lightingFunction
void main(void)
{
- $clippingCall(vCameraPosition);
+ $scenePreCall(vCameraPosition);
gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
+
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -1798,8 +1817,9 @@ class Mesh3D(Geometry):
fragLightFunction = ctx.viewport.light.fragmentShaderFunctionNoop
fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall,
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost,
lightingFunction=fragLightFunction,
lightingCall=ctx.viewport.light.fragmentCall)
prog = ctx.glCtx.prog(self._shaders[0], fragment)
@@ -1818,7 +1838,7 @@ class Mesh3D(Geometry):
ctx.objectToCamera.matrix,
safe=True)
- ctx.clipper.setupProgram(ctx, prog)
+ ctx.setupProgram(prog)
self._draw(prog)
@@ -1860,15 +1880,17 @@ class ColormapMesh3D(Geometry):
varying float vValue;
$colormapDecl
- $clippingDecl
+ $sceneDecl
$lightingFunction
void main(void)
{
- $clippingCall(vCameraPosition);
+ $scenePreCall(vCameraPosition);
vec4 color = $colormapCall(vValue);
gl_FragColor = $lightingCall(color, vPosition, vNormal);
+
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -1933,8 +1955,9 @@ class ColormapMesh3D(Geometry):
def _renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall,
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
colormapDecl=self.colormap.decl,
@@ -1943,7 +1966,7 @@ class ColormapMesh3D(Geometry):
program.use()
ctx.viewport.light.setupProgram(ctx, program)
- ctx.clipper.setupProgram(ctx, program)
+ ctx.setupProgram(program)
self.colormap.setupProgram(ctx, program)
if self.culling is not None:
@@ -2001,20 +2024,20 @@ class _Image(Geometry):
uniform float alpha;
$imageDecl
-
- $clippingDecl
-
+ $sceneDecl
$lightingFunction
void main(void)
{
+ $scenePreCall(vCameraPosition);
+
vec4 color = imageColor(data, vTexCoords);
color.a = alpha;
- $clippingCall(vCameraPosition);
-
vec3 normal = vec3(0.0, 0.0, 1.0);
gl_FragColor = $lightingCall(color, vPosition, normal);
+
+ $scenePostCall(vCameraPosition);
}
"""))
@@ -2133,8 +2156,9 @@ class _Image(Geometry):
def _renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall,
+ sceneDecl=ctx.fragDecl,
+ scenePreCall=ctx.fragCallPre,
+ scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
imageDecl=self._shaderImageColorDecl()
@@ -2159,7 +2183,7 @@ class _Image(Geometry):
gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
- ctx.clipper.setupProgram(ctx, program)
+ ctx.setupProgram(program)
self._texture.bind()
diff --git a/silx/gui/plot3d/scene/utils.py b/silx/gui/plot3d/scene/utils.py
index 1224f5e..bddbcac 100644
--- a/silx/gui/plot3d/scene/utils.py
+++ b/silx/gui/plot3d/scene/utils.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# 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
@@ -544,77 +544,6 @@ def segmentVolumeIntersect(segment, nbins):
return bins
-def segmentTrianglesIntersection(segment, triangles):
- """Check for segment/triangles intersection.
-
- This is based on signed tetrahedron volume comparison.
-
- See A. Kensler, A., Shirley, P.
- Optimizing Ray-Triangle Intersection via Automated Search.
- Symposium on Interactive Ray Tracing, vol. 0, p33-38 (2006)
-
- :param numpy.ndarray segment:
- Segment end points as a 2x3 array of coordinates
- :param numpy.ndarray triangles:
- Nx3x3 array of triangles
- :return: (triangle indices, segment parameter, barycentric coord)
- Indices of intersected triangles, "depth" along the segment
- of the intersection point and barycentric coordinates of intersection
- point in the triangle.
- :rtype: List[numpy.ndarray]
- """
- # TODO triangles from vertices + indices
- # TODO early rejection? e.g., check segment bbox vs triangle bbox
- segment = numpy.asarray(segment)
- assert segment.ndim == 2
- assert segment.shape == (2, 3)
-
- triangles = numpy.asarray(triangles)
- assert triangles.ndim == 3
- assert triangles.shape[1] == 3
-
- # Test line/triangles intersection
- d = segment[1] - segment[0]
- t0s0 = segment[0] - triangles[:, 0, :]
- edge01 = triangles[:, 1, :] - triangles[:, 0, :]
- edge02 = triangles[:, 2, :] - triangles[:, 0, :]
-
- dCrossEdge02 = numpy.cross(d, edge02)
- t0s0CrossEdge01 = numpy.cross(t0s0, edge01)
- volume = numpy.sum(dCrossEdge02 * edge01, axis=1)
- del edge01
- subVolumes = numpy.empty((len(triangles), 3), dtype=triangles.dtype)
- subVolumes[:, 1] = numpy.sum(dCrossEdge02 * t0s0, axis=1)
- del dCrossEdge02
- subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1)
- subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2]
- intersect = numpy.logical_or(
- numpy.all(subVolumes >= 0., axis=1), # All positive
- numpy.all(subVolumes <= 0., axis=1)) # All negative
- intersect = numpy.where(intersect)[0] # Indices of intersected triangles
-
- # Get barycentric coordinates
- barycentric = subVolumes[intersect] / volume[intersect].reshape(-1, 1)
- del subVolumes
-
- # Test segment/triangles intersection
- volAlpha = numpy.sum(t0s0CrossEdge01[intersect] * edge02[intersect], axis=1)
- t = volAlpha / volume[intersect] # segment parameter of intersected triangles
- del t0s0CrossEdge01
- del edge02
- del volAlpha
- del volume
-
- inSegmentMask = numpy.logical_and(t >= 0., t <= 1.)
- intersect = intersect[inSegmentMask]
- t = t[inSegmentMask]
- barycentric = barycentric[inSegmentMask]
-
- # Sort intersecting triangles by t
- indices = numpy.argsort(t)
- return intersect[indices], t[indices], barycentric[indices]
-
-
# Plane #######################################################################
class Plane(event.Notifier):
diff --git a/silx/gui/plot3d/scene/viewport.py b/silx/gui/plot3d/scene/viewport.py
index 41aa999..6de640e 100644
--- a/silx/gui/plot3d/scene/viewport.py
+++ b/silx/gui/plot3d/scene/viewport.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# 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
@@ -36,6 +36,7 @@ __license__ = "MIT"
__date__ = "24/04/2018"
+import string
import numpy
from silx.gui.colors import rgba
@@ -45,7 +46,7 @@ from ..._glutils import gl
from . import camera
from . import event
from . import transform
-from .function import DirectionalLight, ClippingPlane
+from .function import DirectionalLight, ClippingPlane, Fog
class RenderContext(object):
@@ -61,12 +62,33 @@ class RenderContext(object):
: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"""
@@ -127,8 +149,7 @@ class RenderContext(object):
@property
def clipper(self):
- """The current clipping plane
- """
+ """The current clipping plane (ClippingPlane)"""
return self._clipPlane
def setClipPlane(self, point=(0., 0., 0.), normal=(0., 0., 0.)):
@@ -143,6 +164,40 @@ class RenderContext(object):
"""
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.
@@ -170,6 +225,9 @@ class Viewport(event.Notifier):
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):
@@ -224,6 +282,11 @@ class Viewport(event.Notifier):
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
@@ -351,8 +414,8 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
+ dtype=numpy.float32)
self.camera.resetCamera(bounds)
def orbitCamera(self, direction, angle=1.):