In section 5.3.1 we explained why inverse mapping is needed to achieve seamless distor- tion, i.e. working from a distorted voxel space to obtain non-distorted coordinates. In practice, we can adapt this idea to use texture mapping: in this case the distorted voxel coordinates map to original texture coordinates and the original voxel coordinates map to distorted texture coordinates. Hence this method is based on the principle that it is
Chapter 6 94 Hardware-accelerated Methods
possible to achieve volume distortion by simply altering the texture coordinates of each fragment being drawn on screen. As the fragment shader gets the interpolated, original texture coordinates, it is simple to create a function that applies the bifocal distortion ac- cording to the region that the fragment is located in. This was first attempted with a 2D texture, to verify the feasibility of the process (listing 6.1).
The GLSL code starts by defining some uniform variables, which are values that can be changed by the user’s program. Those variables are called uniform because they cannot be changed inside a primitive - in contrast with varying variables which represent interpolated values. The following are the uniform variables needed:
uniform sampler2D t e x U n i t ; / / t h e t e x t u r e u n i t
uniform v e c 2 f o c u s ; / / c e n t r e o f f o c u s r e g i o n
uniform v e c 2 x l i m i t s ; / / r e g i o n l i m i t s i n x ( xmin , xmax )
uniform v e c 2 y l i m i t s ; / / r e g i o n l i m i t s i n y ( ymin , ymax )
uniform v e c 2 s c a l e x ; / / s c a l i n g f a c t o r s f o r c o n t e x t r e g i o n s ( x )
uniform v e c 2 s c a l e y ; / / s c a l i n g f a c t o r s f o r c o n t e x t r e g i o n s ( y )
uniform f l o a t mag ; / / m a g n i f i c a t i o n f a c t o r
In the above code section, the vec2 variables are arrays with two elements. Likewise, it is possible to use vec3 or vec4 if needed. The sampler2D type is used to specify a texture unit, which is necessary to later perform texture lookup. Now the first step is to get the original texture coordinates. This is done by accessing a varying variable called glTexCoord[0],which stores the interpolated texture coordinate for the first set - OpenGL supports multiple sets of texture coordinates.
v e c 2 t e x = gl_TexCoord [ 0 ] . xy ;
Then a series of tests is performed to identify if those coordinates lie within the limits of the focus region in each coordinate direction (defined with xlimits and ylimits) - each test will give a result less than zero in that case. Thus for the x direction:
f l o a t x t e s t = ( t e x . x − x l i m i t s [ 0 ] ) ∗ ( t e x . x − x l i m i t s [ 1 ] ) ; Now it is just a matter of using the result to identify the correct region: either before, within or after the focus region. Once that is done, the texture coordinate for the x axis is adjusted accordingly, following the model outlined in section 5.3.1.1.
i f ( x t e s t <=0) t e x . x = ( t e x . x − f o c u s . x + mag ∗ f o c u s . x ) / mag ; e l s e i f ( t e x . x < x l i m i t s [ 0 ] ) t e x . x = t e x . x / s c a l e x [ 0 ] ;
e l s e t e x . x = 1−((1− t e x . x ) / s c a l e x [ 1 ] ) ;
Then the same procedure is repeated to obtain the new y texture coordinate: f l o a t y t e s t = ( t e x . y − y l i m i t s [ 0 ] ) ∗ ( t e x . y − y l i m i t s [ 1 ] ) ;
Chapter 6 95 Hardware-accelerated Methods
i f ( y t e s t <=0) t e x . y = ( t e x . y − f o c u s . y + mag ∗ f o c u s . y ) / mag ; e l s e i f ( t e x . y < y l i m i t s [ 0 ] ) t e x . y = t e x . y = t e x . y / s c a l e y [ 0 ] ; e l s e t e x . y = 1−((1− t e x . y ) / s c a l e y [ 1 ] ) ;
Finally, the last step is just to fetch the texture colour using the the new texture coor- dinates. The variable gl_FragColor is used to store the fragment colour so it will show on the screen.
g l _ F r a g C o l o r = t e x t u r e 2 D ( t e x U n i t , t e x ) ;
Listing 6.1 presents the complete code and the results of this method are shown in figure 6.3 - they reproduce exactly the application of the mathematical method outlined in section 5.3.1.1. The 2D example of figure 6.3 is useful in itself (and has wider application as the figure shows) but the power of our approach is its application to 3D.
Listing 6.1: Distortion through direct computation of new texture coordinates with a frag- ment shader (2D)
uniform sampler2D t e x U n i t ; / / t h e v o l u m e t e x t u r e u n i t
uniform v e c 2 f o c u s ; / / c e n t r e o f f o c u s r e g i o n
uniform v e c 2 x l i m i t s ; / / r e g i o n l i m i t s i n x ( xmin , xmax )
uniform v e c 2 y l i m i t s ; / / r e g i o n l i m i t s i n y ( ymin , ymax )
uniform v e c 2 s c a l e x ; / / s c a l i n g f a c t o r s f o r c o n t e x t r e g i o n s ( x ) uniform v e c 2 s c a l e y ; / / s c a l i n g f a c t o r s f o r c o n t e x t r e g i o n s ( y ) uniform f l o a t mag ; / / m a g n i f i c a t i o n f a c t o r v o i d main ( v o i d ) { v e c 2 t e x = gl_TexCoord [ 0 ] . xy ; f l o a t x t e s t = ( t e x . x − x l i m i t s [ 0 ] ) ∗ ( t e x . x − x l i m i t s [ 1 ] ) ; i f ( x t e s t <=0) t e x . x = ( t e x . x − f o c u s . x + mag ∗ f o c u s . x ) / mag ; e l s e i f ( t e x . x < x l i m i t s [ 0 ] ) t e x . x = t e x . x / s c a l e x [ 0 ] ; e l s e t e x . x = 1−((1− t e x . x ) / s c a l e x [ 1 ] ) ; f l o a t y t e s t = ( t e x . y − y l i m i t s [ 0 ] ) ∗ ( t e x . y − y l i m i t s [ 1 ] ) ; i f ( y t e s t <=0) t e x . y = ( t e x . y − f o c u s . y + mag ∗ f o c u s . y ) / mag ; e l s e i f ( t e x . y < y l i m i t s [ 0 ] ) t e x . y = t e x . y = t e x . y / s c a l e y [ 0 ] ; e l s e t e x . y = 1−((1− t e x . y ) / s c a l e y [ 1 ] ) ; g l _ F r a g C o l o r = t e x t u r e 2 D ( t e x U n i t , t e x ) ; }
Chapter 6 96 Hardware-accelerated Methods
(a) normal (b) distorted
Figure 6.3: 2D distortion using fragment shader (direct computation approach). Thus logically, the next step is to attempt the same procedure with a 3D texture. It is simple to adapt the previous code to work with a 3D texture (see listing 6.2): first the sampler2Duniform variable becomes a sampler3D one, then focus must be specified by a 3D coordinate and finally, two uniforms are added - one for the z limits (zlimits) and an- other for the z scaling factors (scalez). Also in order to present a semi-transparent volume, the colour information must have an alpha component. This is accomplished through the use of a lookup table, in the form of a one dimensional RGBA texture (sampler1D). Later the code is changed accordingly, so the fetched texture data is used as the index for this lookup table.
The rest of the code remains mostly the same, but a section is added after the com- putation of the y texture coordinate, in order to do the same procedure to the z texture coordinate:
f l o a t z t e s t = ( t e x . z − z l i m i t s [ 0 ] ) ∗ ( t e x . z − z l i m i t s [ 1 ] ) ; i f ( z t e s t <=0) t e x . z = ( t e x . z − f o c u s . z + mag ∗ f o c u s . z ) / mag ; e l s e i f ( t e x . z < z l i m i t s [ 0 ] ) t e x . z = t e x . z = t e x . z / s c a l e z [ 0 ] ; e l s e t e x . z = 1−((1− t e x . z ) / s c a l e z [ 1 ] ) ;
The last step is to use the fetched texture data as the index for accessing the lookup table:
f l o a t l u m i n a n c e = t e x t u r e 3 D ( t e x U n i t , t e x ) . r ; g l _ F r a g C o l o r = t e x t u r e 1 D ( c o l o u r U n i t , l u m i n a n c e ) ;
Chapter 6 97 Hardware-accelerated Methods
Listing 6.2: Directly computing new texture coordinates (3D)
uniform sampler3D t e x U n i t ; / / t h e v o l u m e t e x t u r e u n i t
uniform sampler1D c o l o u r U n i t ; / / t h e c o l o u r m a p t e x t u r e u n i t
uniform v e c 3 f o c u s ; / / c e n t r e o f f o c u s r e g i o n
uniform v e c 2 x l i m i t s ; / / r e g i o n l i m i t s i n x ( xmin , xmax )
uniform v e c 2 y l i m i t s ; / / r e g i o n l i m i t s i n y ( ymin , ymax )
uniform v e c 2 z l i m i t s ; / / r e g i o n l i m i t s i n z ( zmin , zmax )
uniform v e c 2 s c a l e x ; / / s c a l i n g f a c t o r s f o r c o n t e x t r e g i o n s ( x ) uniform v e c 2 s c a l e y ; / / s c a l i n g f a c t o r s f o r c o n t e x t r e g i o n s ( y ) uniform v e c 2 s c a l e z ; / / s c a l i n g f a c t o r s f o r c o n t e x t r e g i o n s ( z ) uniform f l o a t mag ; / / m a g n i f i c a t i o n f a c t o r v o i d main ( v o i d ) { v e c 3 t e x = gl_TexCoord [ 0 ] . xyz ; f l o a t x t e s t = ( t e x . x − x l i m i t s [ 0 ] ) ∗ ( t e x . x − x l i m i t s [ 1 ] ) ; i f ( x t e s t <=0) t e x . x = ( t e x . x − f o c u s . x + mag ∗ f o c u s . x ) / mag ; e l s e i f ( t e x . x < x l i m i t s [ 0 ] ) t e x . x = t e x . x / s c a l e x [ 0 ] ; e l s e t e x . x = 1−((1− t e x . x ) / s c a l e x [ 1 ] ) ; f l o a t z t e s t = ( t e x . z − z l i m i t s [ 0 ] ) ∗ ( t e x . z − z l i m i t s [ 1 ] ) ; i f ( z t e s t <=0) t e x . z = ( t e x . z − f o c u s . z + mag ∗ f o c u s . z ) / mag ; e l s e i f ( t e x . z < z l i m i t s [ 0 ] ) t e x . z = t e x . z / s c a l e z [ 0 ] ; e l s e t e x . z = 1−((1− t e x . z ) / s c a l e z [ 1 ] ) ; f l o a t y t e s t = ( t e x . y − y l i m i t s [ 0 ] ) ∗ ( t e x . y − y l i m i t s [ 1 ] ) ; i f ( y t e s t <=0) t e x . y = ( t e x . y − f o c u s . y + mag ∗ f o c u s . y ) / mag ; e l s e i f ( t e x . y < y l i m i t s [ 0 ] ) t e x . y = t e x . y = t e x . y / s c a l e y [ 0 ] ; e l s e t e x . y = 1−((1− t e x . y ) / s c a l e y [ 1 ] ) ; f l o a t l u m i n a n c e = t e x t u r e 3 D ( t e x U n i t , t e x ) . r ; v e c 4 c o l o u r = t e x t u r e 1 D ( c o l o u r U n i t , l u m i n a n c e ) ; g l _ F r a g C o l o r = c o l o u r ; }
Figure 6.4 presents the results, both in 2D and 3D of applying the technique to a medical dataset. The 3D view was generated by drawing all the slices (128 in this case) from the volume, in back to front order towards the viewer - this is the common approach used by a volume renderer. Although the 2D view is rendered with an adequate speed, the 3D view is not (see table 6.2 in section 6.9 for details). This happens because the number of fragments is much greater and the fragment shader has to compute the new texture coordinates for every single fragment. Being simply a direct application of the distortion
Chapter 6 98 Hardware-accelerated Methods
formulae, in practice this approach is not really suitable for execution on a GPU. It could be made faster by reducing the number of slices being drawn - but at the expense of less quality in the final image. An advantage of this method is that any function, be it distortion-oriented or not, could be potentially implemented by changing the code - but this is not simple nor flexible enough for practical use (and as we demonstrated, does not offer adequate performance).
(a) 2D view (b) 3D view
Figure 6.4: 3D distortion using fragment shader (direct computation approach).