glvolume_funcs.py 10.3 KB
Newer Older
Paul McCarthy's avatar
Paul McCarthy committed
1
2
#!/usr/bin/env python
#
3
# glvolume_funcs.py - OpenGL 1.4 functions used by the GLVolume class.
Paul McCarthy's avatar
Paul McCarthy committed
4
5
6
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
7
8
"""This module provides functions which are used by the :class:`.GLVolume`
class to render :class:`.Image` overlays in an OpenGL 1.4 compatible manner.
9
10
11

An :class:`.ARBPShader` is used to manage the ``glvolume`` vertex/fragment
programs.
Paul McCarthy's avatar
Paul McCarthy committed
12
13
"""

14

Paul McCarthy's avatar
Paul McCarthy committed
15
16
import logging

17
18
import numpy                as np
import OpenGL.GL            as gl
Paul McCarthy's avatar
Paul McCarthy committed
19

20
21
22
import fsl.transform.affine as affine
import fsleyes.gl.shaders   as shaders
import fsleyes.gl.routines  as glroutines
Paul McCarthy's avatar
Paul McCarthy committed
23
import fsleyes.gl.glvolume  as glvolume
Paul McCarthy's avatar
Paul McCarthy committed
24
25
26
27
28


log = logging.getLogger(__name__)


29
def init(self):
30
    """Calls :func:`compileShaders` and :func:`updateShaderState`."""
31

32
    self.shader = None
Paul McCarthy's avatar
Paul McCarthy committed
33

34
35
36
    compileShaders(   self)
    updateShaderState(self)

Paul McCarthy's avatar
Paul McCarthy committed
37

38
39
40
def destroy(self):
    """Deletes handles to the vertex/fragment programs."""

41
42
43
    if self.shader is not None:
        self.shader.destroy()
        self.shader = None
44
45


Paul McCarthy's avatar
Paul McCarthy committed
46
def compileShaders(self):
47
    """Creates a :class:`.ARBPShader` instance. """
Paul McCarthy's avatar
Paul McCarthy committed
48

49
    if self.shader is not None:
50
        self.shader.destroy()
Paul McCarthy's avatar
Paul McCarthy committed
51

52
53
54
    if self.threedee: frag = 'glvolume_3d'
    else:             frag = 'glvolume'

55
    vertSrc  = shaders.getVertexShader(  'glvolume')
56
    fragSrc  = shaders.getFragmentShader(frag)
57
    texes    = {
58
59
60
        'imageTexture'     : 0,
        'colourTexture'    : 1,
        'negColourTexture' : 2,
61
62
        'clipTexture'      : 3,
        'modulateTexture'  : 4
63
    }
Paul McCarthy's avatar
Paul McCarthy committed
64

65
66
67
68
    constants = {
        'kill_fragments_early' : not self.threedee,
        'texture_is_2d'        : self.imageTexture.ndim == 2
    }
69

70
    if self.threedee:
71
72
73
74
75
76

        if   self.opts.clipMode == 'intersection': clipMode = 1
        elif self.opts.clipMode == 'union':        clipMode = 2
        elif self.opts.clipMode == 'complement':   clipMode = 3
        else:                                      clipMode = 0

77
        constants['numSteps']        = self.opts.numInnerSteps
78
        constants['clipMode']        = clipMode
79
        constants['numClipPlanes']   = self.opts.numClipPlanes
80
81
        texes[    'startingTexture'] = 5
        texes[    'depthTexture']    = 6
82

83
84
85
    self.shader = shaders.ARBPShader(vertSrc,
                                     fragSrc,
                                     shaders.getShaderDir(),
86
                                     texes,
87
                                     constants)
Paul McCarthy's avatar
Paul McCarthy committed
88

Paul McCarthy's avatar
Paul McCarthy committed
89

Paul McCarthy's avatar
Paul McCarthy committed
90
def updateShaderState(self):
91
    """Sets all variables required by the vertex and fragment programs. """
92

93
    if not self.ready():
94
        return
Paul McCarthy's avatar
Paul McCarthy committed
95

96
97
    opts   = self.opts
    shader = self.shader
Paul McCarthy's avatar
Paul McCarthy committed
98
99

    # enable the vertex and fragment programs
100
    shader.load()
Paul McCarthy's avatar
Paul McCarthy committed
101
102
103
104
105
106
107

    # The voxValXform transformation turns
    # an image texture value into a raw
    # voxel value. The colourMapXform
    # transformation turns a raw voxel value
    # into a value between 0 and 1, suitable
    # for looking up an appropriate colour
108
    # in the 1D colour map texture.
109
    voxValXform = affine.concat(self.colourTexture.getCoordinateTransform(),
110
                                self.imageTexture.voxValXform)
111
    voxValXform = [voxValXform[0, 0], voxValXform[0, 3], 0, 0]
Paul McCarthy's avatar
Paul McCarthy committed
112

Paul McCarthy's avatar
Paul McCarthy committed
113
114
    # And the clipping range, normalised
    # to the image texture value range
115
116
117
    invClip     = 1 if opts.invertClipping    else -1
    useNegCmap  = 1 if opts.useNegativeCmap   else  0
    imageIsClip = 1 if opts.clipImage is None else -1
118
119

    # modalpha not applied in 3D
120
121
    modAlpha    = 1 if opts.modulateAlpha         else -1
    imageIsMod  = 1 if opts.modulateImage is None else -1
122
    modXform    = self.getModulateValueXform()
123
124
125

    imgXform = self.imageTexture.invVoxValXform
    if opts.clipImage is None: clipXform = imgXform
Paul McCarthy's avatar
Paul McCarthy committed
126
    else:                      clipXform = self.clipTexture.invVoxValXform
127

128
129
130
    clipLo  = opts.clippingRange[0] * clipXform[0, 0] + clipXform[0, 3]
    clipHi  = opts.clippingRange[1] * clipXform[0, 0] + clipXform[0, 3]
    texZero = 0.0                   * imgXform[ 0, 0] + imgXform[ 0, 3]
Paul McCarthy's avatar
Paul McCarthy committed
131

132
    clipping = [clipLo, clipHi, invClip, imageIsClip]
133
    modulate = [modXform[0, 0], modXform[0, 3], modAlpha, imageIsMod]
Paul McCarthy's avatar
Paul McCarthy committed
134
    negCmap  = [useNegCmap, texZero, modAlpha, 0]
135

136
137
138
139
140
141
    # disable clip image/modalpha for 3D
    if self.threedee:
        clipping[3] =  1
        modulate[2] = -1
        modulate[3] =  1

142
    changed  = False
143
144
    changed |= shader.setFragParam('voxValXform', voxValXform)
    changed |= shader.setFragParam('clipping',    clipping)
145
    changed |= shader.setFragParam('modulate',    modulate)
146
147
148
    changed |= shader.setFragParam('negCmap',     negCmap)

    if self.threedee:
149
        clipPlanes  = np.zeros((5, 4), dtype=np.float32)
150
151
152
153
        d2tmat      = opts.getTransform('display', 'texture')

        for i in range(opts.numClipPlanes):
            origin, normal   = self.get3DClipPlane(i)
154
155
            origin           = affine.transform(origin, d2tmat)
            normal           = affine.transformNormal(normal, d2tmat)
156
157
158
            clipPlanes[i, :] = glroutines.planeEquation2(origin, normal)

        changed |= shader.setFragParam('clipPlanes', clipPlanes)
159

160
    self.shader.unload()
Paul McCarthy's avatar
Paul McCarthy committed
161

162
    return changed
163

Paul McCarthy's avatar
Paul McCarthy committed
164

165
def preDraw(self, xform=None, bbox=None):
166
    """Prepares to draw a slice from the given :class:`.GLVolume` instance. """
Paul McCarthy's avatar
Paul McCarthy committed
167

168
    self.shader.load()
169
    self.shader.loadAtts()
Paul McCarthy's avatar
Paul McCarthy committed
170

171
    if isinstance(self, glvolume.GLVolume):
172
        clipCoordXform = self.getAuxTextureXform('clip')
173
        modCoordXform  = self.getAuxTextureXform('modulate')
174
        self.shader.setVertParam('clipCoordXform', clipCoordXform)
175
        self.shader.setVertParam('modCoordXform',  modCoordXform)
Paul McCarthy's avatar
Paul McCarthy committed
176

Paul McCarthy's avatar
Paul McCarthy committed
177

178
def draw2D(self, zpos, axes, xform=None, bbox=None):
179
    """Draws a 2D slice of the image at the given Z location. """
180

181
    vertices, voxCoords, texCoords = self.generateVertices2D(
182
183
184
        zpos, axes, bbox=bbox)

    if xform is not None:
185
        vertices = affine.transform(vertices, xform)
Paul McCarthy's avatar
Paul McCarthy committed
186

187
188
    vertices = np.array(vertices, dtype=np.float32).ravel('C')

189
190
    # Voxel coordinates are calculated
    # in the vertex program
191
    self.shader.setAtt('texCoord', texCoords)
Paul McCarthy's avatar
Paul McCarthy committed
192

193
194
195
    with glroutines.enabled((gl.GL_VERTEX_ARRAY)):
        gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
        gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6)
Paul McCarthy's avatar
Paul McCarthy committed
196

Paul McCarthy's avatar
Paul McCarthy committed
197

198
199
200
201
202
203
204
205
206
207
208
def draw3D(self, xform=None, bbox=None):
    """Draws the image in 3D on the canvas.

    :arg self:    The :class:`.GLVolume` object which is managing the image
                  to be drawn.

    :arg xform:   A 4*4 transformation matrix to be applied to the vertex
                  data.

    :arg bbox:    An optional bounding box.
    """
209
210
211
212
    opts    = self.opts
    canvas  = self.canvas
    display = self.display
    shader  = self.shader
213
    shape   = self.image.shape
214
    proj    = canvas.projectionMatrix
215
216
    src     = self.renderTexture1
    dest    = self.renderTexture2
Paul McCarthy's avatar
Paul McCarthy committed
217
    w, h    = src.shape
218

219
    vertices, voxCoords, texCoords = self.generateVertices3D(bbox)
220
    rayStep, texform               = opts.calculateRayCastSettings(xform, proj)
221

222
    rayStep = affine.transformNormal(
223
        rayStep, self.imageTexture.texCoordXform(shape))
224
    texform = affine.concat(
225
        texform, self.imageTexture.invTexCoordXform(shape))
226

227
    if xform is not None:
228
        vertices = affine.transform(vertices, xform)
229

230
231
    vertices = np.array(vertices, dtype=np.float32).ravel('C')

232
    outerLoop  = opts.getNumOuterSteps()
233
234
235
236
237
    screenSize = [
        1.0 / w,
        1.0 / h,
        1 if opts.blendByIntensity else -1,
        0]
238
239
240
    rayStep    = list(rayStep)   + [0]
    texform    = texform[2, :]
    settings   = [
241
        opts.blendFactor,
242
243
244
        0,
        0,
        display.alpha / 100.0]
245
246

    gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
247

248
249
250
251
    shader.setAtt(      'texCoord',        texCoords)
    shader.setFragParam('rayStep',         rayStep)
    shader.setFragParam('screenSize',      screenSize)
    shader.setFragParam('tex2ScreenXform', texform)
252

253
254
255
    # Disable blending - we want each
    # loop to replace the contents of
    # the texture, not blend into it!
256
    with glroutines.enabled((gl.GL_VERTEX_ARRAY)), \
257
         glroutines.disabled((gl.GL_BLEND)):
258

259
260
261
262
263
264
265
266
        # The depth value for a fragment will
        # not necessary be set at the same
        # time that the fragment colour is
        # set, so we need to use <= for depth
        # testing so that unset depth values
        # do not cause depth clipping.
        gl.glDepthFunc(gl.GL_LEQUAL)

267
        for i in range(outerLoop):
268

269
            settings    = list(settings)
270
            dtex        = src.depthTexture
271
            settings[1] = i * opts.numInnerSteps
272

273
274
            if i == outerLoop - 1: settings[2] =  1
            else:                  settings[2] = -1
275

276
            shader.setFragParam('settings', settings)
277

278
            dest.bindAsRenderTarget()
279
280
            src .bindTexture(gl.GL_TEXTURE5)
            dtex.bindTexture(gl.GL_TEXTURE6)
281

282
            gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
283
            gl.glDrawArrays(gl.GL_TRIANGLES, 0, 36)
284

285
286
            src .unbindTexture()
            dtex.unbindTexture()
287
288
289
290
            dest.unbindAsRenderTarget()

            dest, src = src, dest

291
292
    gl.glDepthFunc(gl.GL_LESS)

293
294
    shader.unloadAtts()
    shader.unload()
295

296
297
    self.renderTexture1 = src
    self.renderTexture2 = dest
298

299

300
def drawAll(self, axes, zposes, xforms):
Paul McCarthy's avatar
Paul McCarthy committed
301
302
303
304
305
306
307
    """Draws mutltiple slices of the given image at the given Z position,
    applying the corresponding transformation to each of the slices.
    """

    nslices   = len(zposes)
    vertices  = np.zeros((nslices * 6, 3), dtype=np.float32)
    texCoords = np.zeros((nslices * 6, 3), dtype=np.float32)
308
    indices   = np.arange(nslices * 6,     dtype=np.uint32)
Paul McCarthy's avatar
Paul McCarthy committed
309
310

    for i, (zpos, xform) in enumerate(zip(zposes, xforms)):
Paul McCarthy's avatar
Paul McCarthy committed
311

312
        v, vc, tc = self.generateVertices2D(zpos, axes)
Paul McCarthy's avatar
Paul McCarthy committed
313

314
        vertices[ i * 6: i * 6 + 6, :] = affine.transform(v, xform)
Paul McCarthy's avatar
Paul McCarthy committed
315
316
        texCoords[i * 6: i * 6 + 6, :] = tc

317
    vertices = vertices.ravel('C')
318

319
    self.shader.setAtt('texCoord', texCoords)
Paul McCarthy's avatar
Paul McCarthy committed
320

321
322
323
324
325
326
    with glroutines.enabled((gl.GL_VERTEX_ARRAY)):
        gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
        gl.glDrawElements(gl.GL_TRIANGLES,
                          nslices * 6,
                          gl.GL_UNSIGNED_INT,
                          indices)
Paul McCarthy's avatar
Paul McCarthy committed
327
328


329
def postDraw(self, xform=None, bbox=None):
Paul McCarthy's avatar
Paul McCarthy committed
330
331
332
    """Cleans up the GL state after drawing from the given :class:`.GLVolume`
    instance.
    """
333
334
    self.shader.unloadAtts()
    self.shader.unload()
335
336

    if self.threedee:
337
        self.drawClipPlanes(xform=xform)