Simple GPU Path Tracing, Part. 1 : Project Setup
- Simple GPU Path Tracing : Introduction
- Simple GPU Path Tracing, Part. 1 : Project Setup
- Simple GPU Path Tracing, Part. 1.1 : Adding a cuda backend to the project
- Simple GPU Path Tracing, Part. 2.0 : Scene Representation
- Simple GPU Path Tracing, Part. 2.1 : Acceleration structure
- Simple GPU Path Tracing, Part. 3.0 : Path Tracing Basics
- Simple GPU Path Tracing, Part. 3.1 : Matte Material
- Simple GPU Path Tracing, Part. 3.2 : Physically Based Material
- Simple GPU Path Tracing, Part. 3.4 : Small Improvements, Camera and wrap up
- Simple GPU Path Tracing, Part. 4.0 : Mesh Loading
- Simple GPU Path Tracing, Part. 4.1 : Textures
- Simple GPU Path Tracing, Part. 4.2 : Normal Mapping & GLTF Textures
- Simple GPU Path Tracing, Part. 5.0 : Sampling lights
- Simple GPU Path Tracing, Part 6 : GUI
- Simple GPU Path Tracing, Part 7.0 : Transparency
- Simple GPU Path Tracing, Part 7.1 : Volumetric materials
- Simple GPU Path Tracing, Part 7.2 : Refractive material
- Simple GPU Path Tracing, Part 8 : Denoising
- Simple GPU Path Tracing, Part 9 : Environment Lighting
- Simple GPU Path Tracing, Part 10 : Little Optimizations
- Simple GPU Path Tracing, Part 11 : Multiple Importance Sampling
In this part, we'll be setting up the project.
The final goal is to have a window that displays the result image of a simple compute shader, with both OpenGL and Cuda backends.
Expected result for this postHere's the commit containing all the code in this post.
CMake configuration
I'm using a single and simple CMake file that will links all the libraries, sets all the include directories, and more importantly sets all the source files for compiling.
Remember we're going to have a cuda backend so it's important to call find_package(CUDA REQUIRED) and enable_language(cuda).
Also, instead of add_executable, we use add_cuda_executable. this will compile with nvcc instead of msvc, and therefore compile all the .cu files that contain cuda kernels correctly.
Here's a link to the final CMakeFile of this part.
Le'ts also create a little .bat file that will build the project in debug mode :
BuildDebug.bat :
msbuild build/INSTALL.vcxproj /p:Configuration=Debug /m /p:CL_MPCount=5
Now, if we run BuildDebug.bat, it should build the project and install the needed files into the install folder that we set in cmake.
Application Wrapper + Window Creation
Cool, let's get coding now. First thing to do is getting a window up and running.
I'm using an "application" class that will manage the application lifetime.
This allows us to have a simple main() function, and the application class will hold all the required variables and functions for running our app as class members :
application *App = application::Get();
App->Init();
App->Run();
App->Cleanup();
As you can see, it's a singleton class. We get the instance, and then start the application.
Let's quickly see what's inside each function :
void application::Init()
{
Window = std::make_shared<window>(800, 600);
}
This just creates a window
void application::Run()
{
while(!Window->ShouldClose())
{
Window->PollEvents();
}
}
Run() just contains the main window loop, and check for events.
I'm using a wrapper class for the window. It creates a window with GLFW library, and also initializes openGL with glew.
Dear ImGui
Now we will start using ImGui. The final goal for this chapter is to get an image on the screen, and ImGui will be handy for this task. We could otherwise draw the image on the screen using a GL quad mesh and texture it, but why bother when it can be a one liner with ImGui.
Working with the dearimgui library is a really nice experience. Everything is carefully designed and explained in the example files. So, to initialize ImGui, I just checked the example included in their source code for opengl3 and glfw, and pasted some code in my project.
Here's the init code :
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(Window->Handle, true);
ImGui_ImplOpenGL3_Init("#version 460");
Then at the start of each frame, we call StartFrame() :
void application::StartFrame()
{
glViewport(0, 0, Window->Width, Window->Height);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
}
This will clear the openGL back buffers, and call some ImGui functions to tell it that we're starting a new frame
At the end of each frame, we call EndFrame():
void application::EndFrame()
{
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
Window->Present();
}
Nothing spectacular here too. Window->Present() will call glfwSwapBuffers to swap the openGL back buffers.
Writing the shader for OpenGL
Now, we're ready to write some openGL shader code. Remember, we just want a simple shader that writes into an image, and we want to display that image on the screen.
To achieve that, I'll add two helper classes to the project :
- shaderGL, that wraps an openGL shader object. Nothing fancy here, just basic boilerplate openGL code.
- textureGL that wraps an openGL texture object. It's a very minimal wrapper too, but I don't really need more than that at this point.
I've added a new function called InitGpuObjects(), that gets called in app::Init().
that's where we can create our texture and our shader :
void application::InitGpuObjects()
{
PathTracingShader = std::make_shared<shaderGL>("resources/shaders/PathTrace.glsl");
RenderTexture = std::make_shared<textureGL>(Window->Width, Window->Height, 4);
}
Alright, now that we have that, we need to run that shader, and display the result on the screen !
To run the shader :
PathTracingShader->Use();
PathTracingShader->SetTexture(0, RenderTexture->TextureID, GL_READ_WRITE);
PathTracingShader->Dispatch(Window->Width / 16 + 1, Window->Height / 16 +1, 1);
And to display the texture on the screen :
ImGui::SetNextWindowPos(ImVec2(0,0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(Window->Width, Window->Height), ImGuiCond_Always);
ImGui::Begin("RenderWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize
|ImGuiWindowFlags_NoTitleBar);
ImGui::Image((ImTextureID)RenderTexture->TextureID,
ImVec2(Window->Width, Window->Height));
ImGui::End();
And now, here's the compute shader :
#version 460
layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
layout (binding = 0, rgba32f) uniform image2D RenderImage;
void main()
{
ivec2 ImageSize = imageSize(RenderImage);
vec2 UV = vec2(gl_GlobalInvocationID.xy) / vec2(ImageSize);
imageStore(RenderImage, ivec2(gl_GlobalInvocationID.xy), vec4(UV, 0, 1));
}
If you don't know about compute shaders, they're "general purpose" programs that are useful for computing massively parallel tasks, like setting pixels on an image for example.
Compute shaders are executed by 1d, 2d, or 3d "blocks" or "groups" of threads.
Each block can be of an arbitrary size. For filling up a 2d image, we will logically use 2d blocks, and in our case we use 16x16 blocks. (see local_size_x and local_size_y in the above code).
we then "Dispatch" the shader to run it, giving it a number of groups to execute.
so if we have a 2d image of size 256x256, and our block size is 16, we need to dispatch 16 groups in the x dimension, 16 groups in the y dimension, and 1 group in the z dimension.
Here's a very useful schema for understanding the execution of compute shaders (It's based on d3d compute shaders, but can be applied to openGL too).
Here's a more in depth introduction to openGL compute shaders.
In this shader, we find the UV coordinate of the current pixel using gl_GlobalInvocationID, and set that uv value as the colour for this pixel.
Here's the result :
Amazing ! now we can view the result of our compute shader, live on the window.
In the next post, we'll see how to add a cuda backend to the project, because... why not ?
Links
- Add Cuda to a cmake project
Modern CMake and cuda
NVidia Cuda Compiler
Singleton design pattern
glBindImageTexture
Explanation of Groups, threads, and executino scheme of compute shaders in D3D
Compute shaders tutorial in openGL
next Post : Simple GPU Path Tracing, Part. 1.1 : Adding a cuda backend to the project
Commentaires
Enregistrer un commentaire