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 Ψ x t at some fixed time t . 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, t 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.

Color in the pixels corresponding to | Ψ x t | 2 .

The display code takes the waveFunction texture as input and draws a representative curve. We represent | Ψ x t | 2 by coloring the pixels on an xResolution x yResolution canvas that correspond to the values of | Ψ x t | 2 at each x .

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 | Ψ | 2 > | Ψ max | 2 × ( t - 0.5 δ t ) and | Ψ | 2 | Ψ max | 2 × ( t + 0.5 δ t ) Where t is the t texture coordinate of the fragment, and δ t 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);
  }
      
Ψ x  vs  x

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 Ψ x = 0 and nothing in the physics changes that. Next up we will look at injecting a moving particle into our simulation.

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.