Skip to content

Latest commit

 

History

History
 
 

ray_tracing_jitter_cam

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Jitter Camera - Tutorial

Tutorial (Setup)

This is an extension of the Vulkan ray tracing tutorial.

In this extension, we will implement antialiasing by jittering the offset of each ray for each pixel over time, instead of always shooting each ray from the middle of its pixel.

Random Functions

We will use some simple functions for random number generation, which suffice for this example.

Create a new shader file random.glsl with the following code. Add it to the shaders directory and rerun CMake, and include this new file in raytrace.rgen:

// Generate a random unsigned int from two unsigned int values, using 16 pairs
// of rounds of the Tiny Encryption Algorithm. See Zafar, Olano, and Curtis,
// "GPU Random Numbers via the Tiny Encryption Algorithm"
uint tea(uint val0, uint val1)
{
  uint v0 = val0;
  uint v1 = val1;
  uint s0 = 0;

  for(uint n = 0; n < 16; n++)
  {
    s0 += 0x9e3779b9;
    v0 += ((v1 << 4) + 0xa341316c) ^ (v1 + s0) ^ ((v1 >> 5) + 0xc8013ea4);
    v1 += ((v0 << 4) + 0xad90777d) ^ (v0 + s0) ^ ((v0 >> 5) + 0x7e95761e);
  }

  return v0;
}

// Generate a random unsigned int in [0, 2^24) given the previous RNG state
// using the Numerical Recipes linear congruential generator
uint lcg(inout uint prev)
{
  uint LCG_A = 1664525u;
  uint LCG_C = 1013904223u;
  prev       = (LCG_A * prev + LCG_C);
  return prev & 0x00FFFFFF;
}

// Generate a random float in [0, 1) given the previous RNG state
float rnd(inout uint prev)
{
  return (float(lcg(prev)) / float(0x01000000));
}

Frame Number

Since our jittered samples will be accumulated across frames, we need to know which frame we are currently rendering. A frame number of 0 will indicate a new frame, and we will accumulate the data for larger frame numbers.

Note that the uniform image is read/write, which makes it possible to accumulate previous frames.

Add the frame member to the PushConstantRay struct in shaders/host_device.h:

struct PushConstantRay
{
  vec4  clearColor;
  vec3  lightPosition;
  float lightIntensity;
  int   lightType;
  int   frame;
};

Random and Jitter

In raytrace.rgen, at the beginning of main(), initialize the random seed:

  // Initialize the random number
  uint seed = tea(gl_LaunchIDEXT.y * gl_LaunchSizeEXT.x + gl_LaunchIDEXT.x, pcRay.frame);

Then we need two random numbers to vary the X and Y inside the pixel, except for frame 0, where we always shoot in the center.

float r1 = rnd(seed);
float r2 = rnd(seed);
// Subpixel jitter: send the ray through a different position inside the pixel
// each time, to provide antialiasing.
vec2 subpixel_jitter = pcRay.frame == 0 ? vec2(0.5f, 0.5f) : vec2(r1, r2);

Now we only need to change how we compute the pixel center:

const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + subpixel_jitter;

Storing or Updating

At the end of main(), if the frame number is equal to 0, we write directly to the image. Otherwise, we combine the new image with the previous frame frames.

  // Do accumulation over time
  if(pcRay.frame > 0)
  {
    float a         = 1.0f / float(pcRay.frame + 1);
    vec3  old_color = imageLoad(image, ivec2(gl_LaunchIDEXT.xy)).xyz;
    imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(mix(old_color, prd.hitValue, a), 1.f));
  }
  else
  {
    // First frame, replace the value in the buffer
    imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(prd.hitValue, 1.f));
  }

Application Frame Update

We need to increment the current rendering frame, but we also need to reset it when something in the scene is changing.

Add two new functions to the HelloVulkan class:

  void resetFrame();
  void updateFrame();

The implementation of updateFrame resets the frame counter if the camera has changed; otherwise, it increments the frame counter.

//--------------------------------------------------------------------------------------------------
// If the camera matrix or the the fov has changed, resets the frame.
// otherwise, increments frame.
//
void HelloVulkan::updateFrame()
{
  static glm::mat4 refCamMatrix;
  static float         refFov{CameraManip.getFov()};

  const auto& m   = CameraManip.getMatrix();
  const auto  fov = CameraManip.getFov();

  if(refCamMatrix != m || refFov != fov)
  {
    resetFrame();
    refCamMatrix = m;
    refFov       = fov;
  }
  m_pcRay.frame++;
}

Since resetFrame will be called before updateFrame increments the frame counter, resetFrame will set the frame counter to -1:

void HelloVulkan::resetFrame()
{
  m_pcRay.frame = -1;
}

At the begining of HelloVulkan::raytrace, call

  updateFrame();

The application will now antialias the image when ray tracing is enabled.

Adding resetFrame() in HelloVulkan::onResize() will also take care of clearing the buffer while resizing the window.

Resetting Frame on UI Change

The frame number should also be reset when any parts of the scene change, such as the light direction or the background color. In renderUI() in main.cpp, check for UI changes and reset the frame number when they happen:

void renderUI(HelloVulkan& helloVk)
{
  bool changed = false;

  changed |= ImGuiH::CameraWidget();
  if(ImGui::CollapsingHeader("Light"))
  {
    auto& pc = helloVk.m_pushConstant;
    changed |= ImGui::RadioButton("Point", &pc.lightType, 0);
    ImGui::SameLine();
    changed |= ImGui::RadioButton("Infinite", &pc.lightType, 1);

    changed |= ImGui::SliderFloat3("Position", &pc.lightPosition.x, -20.f, 20.f);
    changed |= ImGui::SliderFloat("Intensity", &pc.lightIntensity, 0.f, 150.f);
  }

  if(changed)
    helloVk.resetFrame();
}

We also need to check for UI changes inside the main loop inside main():

  bool changed = false;
  // Edit 3 floats representing a color
  changed |= ImGui::ColorEdit3("Clear color", reinterpret_cast<float*>(&clearColor));
  // Switch between raster and ray tracing
  changed |= ImGui::Checkbox("Ray Tracer mode", &useRaytracer);
  if(changed)
    helloVk.resetFrame();

Quality

After enough samples, the quality of the rendering will be sufficiently high that it might make sense to avoid accumulating further images.

Add a member variable to HelloVulkan

int m_maxFrames{100};

and also add a way to control it in renderUI(), making sure that m_maxFrames cannot be set below 1:

changed |= ImGui::SliderInt("Max Frames", &helloVk.m_maxFrames, 1, 100);

Then in raytrace(), immediately after the call to updateFrame(), return if the current frame has exceeded the max frame.

  if(m_pcRay.frame >= m_maxFrames)
    return;

Since the output image won't be modified by the ray tracer, we will simply display the last good image, reducing GPU usage when the target quality has been reached.

More Samples in RayGen

To improve efficiency, we can perform multiple samples directly in the ray generation shader. This will be faster than calling raytrace() the equivalent number of times.

To do this, add a constant to raytrace.rgen (this could alternatively be added to the push constant block and controlled by the application):

const int NBSAMPLES = 10;

In main(), after initializing the random number seed, create a loop that encloses the lines from the generation of r1 and r2 to the traceRayEXT call, and accumulates the colors returned by traceRayEXT. At the end of the loop, divide by the number of samples that were taken.

  vec3 hitValues = vec3(0);

  for(int smpl = 0; smpl < NBSAMPLES; smpl++)
  {
    float r1 = rnd(seed);
    float r2 = rnd(seed);
    // ...
    // TraceRayEXT( ... );
    hitValues += prd.hitValue;
  }
  prd.hitValue = hitValues / NBSAMPLES;

For a given value of m_maxFrames and NBSAMPLE, the image will have m_maxFrames * NBSAMPLE antialiasing samples.

For instance, if m_maxFrames = 10 and NBSAMPLE = 10, this will be equivalent in quality to an image using m_maxFrames = 100 and NBSAMPLE = 1.

However, using NBSAMPLE=10 in the ray generation shader will be faster than calling raytrace() with NBSAMPLE=1 10 times in a row.