diff options
author | Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> | 2019-07-09 10:20:20 +0200 |
---|---|---|
committer | Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> | 2019-07-09 10:20:20 +0200 |
commit | 654a6ac93513c3cc1ef97cacd782ff674c6f4559 (patch) | |
tree | 3b986e4972de7c57fa465820367602fc34bcb0d3 /silx/gui/plot3d/scene | |
parent | a763e5d1b3921b3194f3d4e94ab9de3fbe08bbdd (diff) |
New upstream version 0.11.0+dfsg
Diffstat (limited to 'silx/gui/plot3d/scene')
-rw-r--r-- | silx/gui/plot3d/scene/camera.py | 2 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/core.py | 11 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/cutplane.py | 18 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/function.py | 87 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/interaction.py | 60 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/primitives.py | 130 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/utils.py | 73 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/viewport.py | 75 |
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.): |