import importlib
from OpenGL import GL
from OpenGL.GL import shaders
import numpy as np
from ...Qt import QtGui, QT_LIB
from ..GLGraphicsItem import GLGraphicsItem
if QT_LIB in ["PyQt5", "PySide2"]:
QtOpenGL = QtGui
else:
QtOpenGL = importlib.import_module(f"{QT_LIB}.QtOpenGL")
__all__ = ['GLVolumeItem']
[docs]
class GLVolumeItem(GLGraphicsItem):
"""
**Bases:** :class:`GLGraphicsItem <pyqtgraph.opengl.GLGraphicsItem.GLGraphicsItem>`
Displays volumetric data.
"""
_shaderProgram = None
[docs]
def __init__(self, data, sliceDensity=1, smooth=True, glOptions='translucent', parentItem=None):
"""
============== =======================================================================================
**Arguments:**
data Volume data to be rendered. *Must* be 4D numpy array (x, y, z, RGBA) with dtype=ubyte.
sliceDensity Density of slices to render through the volume. A value of 1 means one slice per voxel.
smooth (bool) If True, the volume slices are rendered with linear interpolation
============== =======================================================================================
"""
super().__init__()
self.setGLOptions(glOptions)
self.sliceDensity = sliceDensity
self.smooth = smooth
self.data = None
self._needUpload = False
self.texture = None
self.m_vbo_position = QtOpenGL.QOpenGLBuffer(QtOpenGL.QOpenGLBuffer.Type.VertexBuffer)
self.setParentItem(parentItem)
self.setData(data)
def setData(self, data):
self.data = data
self._needUpload = True
self.update()
def _uploadData(self):
if self.texture is None:
self.texture = GL.glGenTextures(1)
GL.glBindTexture(GL.GL_TEXTURE_3D, self.texture)
filt = GL.GL_LINEAR if self.smooth else GL.GL_NEAREST
GL.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_MIN_FILTER, filt)
GL.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_MAG_FILTER, filt)
GL.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_BORDER)
GL.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_BORDER)
GL.glTexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_R, GL.GL_CLAMP_TO_BORDER)
shape = self.data.shape
context = QtGui.QOpenGLContext.currentContext()
if not context.isOpenGLES():
## Test texture dimensions first
GL.glTexImage3D(GL.GL_PROXY_TEXTURE_3D, 0, GL.GL_RGBA, shape[0], shape[1], shape[2], 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, None)
if GL.glGetTexLevelParameteriv(GL.GL_PROXY_TEXTURE_3D, 0, GL.GL_TEXTURE_WIDTH) == 0:
raise Exception("OpenGL failed to create 3D texture (%dx%dx%d); too large for this hardware." % shape[:3])
data = np.ascontiguousarray(self.data.transpose((2,1,0,3)))
GL.glTexImage3D(GL.GL_TEXTURE_3D, 0, GL.GL_RGBA, shape[0], shape[1], shape[2], 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, data)
GL.glBindTexture(GL.GL_TEXTURE_3D, 0)
all_vertices = []
self.lists = {}
for ax in [0,1,2]:
for d in [-1, 1]:
vertices = self.drawVolume(ax, d)
self.lists[(ax,d)] = (len(all_vertices), len(vertices))
all_vertices.extend(vertices)
pos = np.array(all_vertices, dtype=np.float32)
vbo = self.m_vbo_position
if not vbo.isCreated():
vbo.create()
vbo.bind()
vbo.allocate(pos, pos.nbytes)
vbo.release()
self._needUpload = False
@staticmethod
def getShaderProgram():
klass = GLVolumeItem
if klass._shaderProgram is not None:
return klass._shaderProgram
ctx = QtGui.QOpenGLContext.currentContext()
fmt = ctx.format()
if ctx.isOpenGLES():
if fmt.version() >= (3, 0):
glsl_version = "#version 300 es\n"
sources = SHADER_CORE
else:
glsl_version = ""
sources = SHADER_LEGACY
else:
if fmt.version() >= (3, 1):
glsl_version = "#version 140\n"
sources = SHADER_CORE
else:
glsl_version = ""
sources = SHADER_LEGACY
compiled = [shaders.compileShader([glsl_version, v], k) for k, v in sources.items()]
program = shaders.compileProgram(*compiled)
GL.glBindAttribLocation(program, 0, "a_position")
GL.glBindAttribLocation(program, 1, "a_texcoord")
GL.glLinkProgram(program)
klass._shaderProgram = program
return program
def paint(self):
if self.data is None:
return
if self._needUpload:
self._uploadData()
self.setupGLState()
mat_mvp = self.mvpMatrix()
mat_mvp = np.array(mat_mvp.data(), dtype=np.float32)
# calculate camera coordinates in this model's local space.
# (in eye space, the camera is at the origin)
modelview = self.modelViewMatrix()
cam_local = modelview.inverted()[0].map(QtGui.QVector3D())
# in local space, the model spans (0,0,0) to data.shape
center = QtGui.QVector3D(*[x/2. for x in self.data.shape[:3]])
cam = cam_local - center
cam = np.array([cam.x(), cam.y(), cam.z()])
ax = np.argmax(abs(cam))
d = 1 if cam[ax] > 0 else -1
offset, num_vertices = self.lists[(ax,d)]
program = self.getShaderProgram()
loc_pos, loc_tex = 0, 1
self.m_vbo_position.bind()
GL.glVertexAttribPointer(loc_pos, 3, GL.GL_FLOAT, False, 6*4, None)
GL.glVertexAttribPointer(loc_tex, 3, GL.GL_FLOAT, False, 6*4, GL.GLvoidp(3*4))
self.m_vbo_position.release()
enabled_locs = [loc_pos, loc_tex]
GL.glBindTexture(GL.GL_TEXTURE_3D, self.texture)
for loc in enabled_locs:
GL.glEnableVertexAttribArray(loc)
with program:
loc = GL.glGetUniformLocation(program, "u_mvp")
GL.glUniformMatrix4fv(loc, 1, False, mat_mvp)
GL.glDrawArrays(GL.GL_TRIANGLES, offset, num_vertices)
for loc in enabled_locs:
GL.glDisableVertexAttribArray(loc)
GL.glBindTexture(GL.GL_TEXTURE_3D, 0)
def drawVolume(self, ax, d):
imax = [0,1,2]
imax.remove(ax)
tp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]]
vp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]]
nudge = [0.5/x for x in self.data.shape]
tp[0][imax[0]] = 0+nudge[imax[0]]
tp[0][imax[1]] = 0+nudge[imax[1]]
tp[1][imax[0]] = 1-nudge[imax[0]]
tp[1][imax[1]] = 0+nudge[imax[1]]
tp[2][imax[0]] = 1-nudge[imax[0]]
tp[2][imax[1]] = 1-nudge[imax[1]]
tp[3][imax[0]] = 0+nudge[imax[0]]
tp[3][imax[1]] = 1-nudge[imax[1]]
vp[0][imax[0]] = 0
vp[0][imax[1]] = 0
vp[1][imax[0]] = self.data.shape[imax[0]]
vp[1][imax[1]] = 0
vp[2][imax[0]] = self.data.shape[imax[0]]
vp[2][imax[1]] = self.data.shape[imax[1]]
vp[3][imax[0]] = 0
vp[3][imax[1]] = self.data.shape[imax[1]]
slices = self.data.shape[ax] * self.sliceDensity
r = list(range(slices))
if d == -1:
r = r[::-1]
vertices = []
tzVals = np.linspace(nudge[ax], 1.0-nudge[ax], slices)
vzVals = np.linspace(0, self.data.shape[ax], slices)
for i in r:
z = tzVals[i]
w = vzVals[i]
tp[0][ax] = z
tp[1][ax] = z
tp[2][ax] = z
tp[3][ax] = z
vp[0][ax] = w
vp[1][ax] = w
vp[2][ax] = w
vp[3][ax] = w
# assuming 0-1-2-3 are the BL, BR, TR, TL vertices of a quad
for idx in [0, 1, 3, 1, 2, 3]: # 2 triangles per quad
vtx = tuple(vp[idx]) + tuple(tp[idx])
vertices.append(vtx)
return vertices
SHADER_LEGACY = {
GL.GL_VERTEX_SHADER : """
uniform mat4 u_mvp;
attribute vec4 a_position;
attribute vec3 a_texcoord;
varying vec3 v_texcoord;
void main() {
gl_Position = u_mvp * a_position;
v_texcoord = a_texcoord;
}
""",
GL.GL_FRAGMENT_SHADER : """
uniform sampler3D u_texture;
varying vec3 v_texcoord;
void main()
{
gl_FragColor = texture3D(u_texture, v_texcoord);
}
""",
}
SHADER_CORE = {
GL.GL_VERTEX_SHADER : """
uniform mat4 u_mvp;
in vec4 a_position;
in vec3 a_texcoord;
out vec3 v_texcoord;
void main() {
gl_Position = u_mvp * a_position;
v_texcoord = a_texcoord;
}
""",
GL.GL_FRAGMENT_SHADER : """
#ifdef GL_ES
precision mediump float;
precision lowp sampler3D;
#endif
uniform sampler3D u_texture;
in vec3 v_texcoord;
out vec4 fragColor;
void main()
{
fragColor = texture(u_texture, v_texcoord);
}
""",
}