Displaying GPGPU Results
Our Schrödinger example shows how to carry out physical simulations on the GPU. Now that we have some results, we want to see what they look like. This will take our GPU code closer to the design intention of OpenGL, but still apply it in an unusual way.
At the end of each step of our Schrödinger solver, the waveFunction
texture holds values for
at some fixed time
. We will map these values into curves
on a canvas.
This adds an additional layer of complexity to our project. Originally, we used a canvas sized according to our problem and never attached to the DOM. Now we must use a canvas sized according to our display, and it must be attached to the DOM. Luckily, by adjusting the viewport we can have the best of both approaches. During computation steps we adjust the viewport to process fragments corresponding to the problem grid, and during rendering steps we adjust the viewport to process fragments corresponding to the pixels on the screen.
The computational steps render onto a FBO as in the previous discussions. But now, we are careful to get a WebGLRenderingContext sized to our computational domain with the GPGPUtility method:
this.getComputeContext = function() {
if (problemWidth != canvasWidth
|| problemHeight != canvasHeight) {
gl.viewport(0, 0, problemWidth, problemHeight);
}
return gl;
}
With this rendering context, for this problem, the texture coordinate s
ranges from
0 to 1, while t
remains constant. This corresponds to pixels ranging from 0 to
xResolution
along the x axis, while y remains fixed. Also note that
we have two t's. One t
is a texture coordinate, the other, italic,
is the time variable in the simulation.
When we draw to the screen, we get a rendering context that matches the full canvas size.
this.getRenderingContext = function() {
if (problemWidth != canvasWidth
|| problemHeight != canvasHeight) {
gl.viewport(0, 0, canvasWidth, canvasHeight);
}
return gl;
}
With this context, both s
and
t
vary corresponding to the coordinate ranges along the
x and y axes. For each fragment, or pixel, we read the
waveFunction
texture and compare
its value with the value represented by the pixel. If they match, we set the color for the pixel, otherwise we
leave the pixel blank. Because of the way the
waveFunction
texture was setup, we retrieve the same
value for a given s
, no matter the value of t
.
The display code takes the waveFunction
texture as input and draws a representative
curve. We represent
by coloring the pixels on an
xResolution
x yResolution
canvas that correspond to
the values of
at each
.
The rendering step works entirely with the texture coordinates s
and t
.
S
ranges from 0 to 1 as we progress from from left to right, and
t
ranges from 0 to 1 as we move from
the bottom to the top of the page.
We color the pixel if and Where is the t texture coordinate of the fragment, and is the change in texture coordinates from one fragment to the next. We can make a straightforward translation into a shader.
uniform vec4 color;
// Y scale for the plot
uniform float psiMax;
// waveFunction.r is the real part waveFunction.g is the imiginary part.
uniform sampler2D waveFunction;
// The number of points along the y axis.
uniform int yResolution;
varying vec2 vTextureCoord;
void main()
{
vec2 psi;
float absPsi2;
float psiMax2;
psiMax2 = psiMax*psiMax;
psi = texture2D(waveFunction, vTextureCoord).rg;
absPsi2 = psi.r*psi.r + psi.g*psi.g;
if (absPsi2 > psiMax2*(vTextureCoord.t-0.5/float(yResolution))
&& absPsi2 <= psiMax2*(vTextureCoord.t+0.5/float(yResolution))
{
gl_FragColor = color;
}
else
{
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
}
}
We can, though, do better. Remember my comment back in the Speed Bumps section about using built in functions such as step or smoothstep to avoid conditionals.
void main()
{
float absPsi2;
float halfDeltaT;
vec2 psi;
float psiMax2;
psiMax2 = psiMax*psiMax;
halfDeltaT = 0.5/float(yResolution);
psi = texture2D(waveFunction, vTextureCoord).rg;
absPsi2 = psi.r*psi.r + psi.g*psi.g;
gl_FragColor = color*step(psiMax2*(vTextureCoord.t-halfDeltaT), absPsi2)
- color*step(psiMax2*(vTextureCoord.t+halfDeltaT), absPsi2);
}
This is a rather flat wave function, but if you think about it this is what we expect. The time dependent Schrödinger equation as we are using it describes the evolution of a wave function in time. We start out with and nothing in the physics changes that. Next up we will look at injecting a moving particle into our simulation.