diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt index d1acdbcbf011aab7de96f060aeb55ede83c516eb..460766ad31045c5ac37d6aa8ccffc614a8921fdc 100644 --- a/projects/CMakeLists.txt +++ b/projects/CMakeLists.txt @@ -8,4 +8,5 @@ add_subdirectory(sph) add_subdirectory(voxelization) add_subdirectory(mesh_shader) add_subdirectory(saf_r) -add_subdirectory(indirect_dispatch) \ No newline at end of file +add_subdirectory(indirect_dispatch) +add_subdirectory(path_tracer) \ No newline at end of file diff --git a/projects/path_tracer/.gitignore b/projects/path_tracer/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ff3dff30031efafa24269a9ac0ef93f64f63ded1 --- /dev/null +++ b/projects/path_tracer/.gitignore @@ -0,0 +1 @@ +saf_r \ No newline at end of file diff --git a/projects/path_tracer/CMakeLists.txt b/projects/path_tracer/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..2b8edc208c70a5e8e74c6c28221f783a68a3ec6c --- /dev/null +++ b/projects/path_tracer/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.16) +project(path_tracer) + +# setting c++ standard for the project +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# this should fix the execution path to load local files from the project +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + +# adding source files to the project +add_executable(path_tracer + src/main.cpp) + +# this should fix the execution path to load local files from the project (for MSVC) +if(MSVC) + set_target_properties(path_tracer PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + set_target_properties(path_tracer PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + + # in addition to setting the output directory, the working directory has to be set + # by default visual studio sets the working directory to the build directory, when using the debugger + set_target_properties(path_tracer PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) +endif() + +# including headers of dependencies and the VkCV framework +target_include_directories(path_tracer SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include}) + +# linking with libraries from all dependencies and the VkCV framework +target_link_libraries(path_tracer vkcv vkcv_testing vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui) diff --git a/projects/path_tracer/shaders/clearImage.comp b/projects/path_tracer/shaders/clearImage.comp new file mode 100644 index 0000000000000000000000000000000000000000..97998e945112d166be7d00df98ee44ea8322a633 --- /dev/null +++ b/projects/path_tracer/shaders/clearImage.comp @@ -0,0 +1,17 @@ +#version 440 +#extension GL_GOOGLE_include_directive : enable + +layout(set=0, binding=0, rgba32f) uniform image2D outImage; + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +void main(){ + + ivec2 outImageRes = imageSize(outImage); + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + + if(any(greaterThanEqual(coord, outImageRes))) + return; + + imageStore(outImage, coord, vec4(0)); +} \ No newline at end of file diff --git a/projects/path_tracer/shaders/combineImages.comp b/projects/path_tracer/shaders/combineImages.comp new file mode 100644 index 0000000000000000000000000000000000000000..d1a4e85caf175dfc3125afd847d7458ddec2fef1 --- /dev/null +++ b/projects/path_tracer/shaders/combineImages.comp @@ -0,0 +1,21 @@ +#version 440 +#extension GL_GOOGLE_include_directive : enable + +layout(set=0, binding=0, rgba32f) uniform image2D newImage; +layout(set=0, binding=1, rgba32f) uniform image2D meanImage; + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +void main(){ + + ivec2 outImageRes = imageSize(meanImage); + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + + if(any(greaterThanEqual(coord, outImageRes))) + return; + + vec4 colorNew = imageLoad(newImage, coord); + vec4 colorMean = imageLoad(meanImage, coord); + + imageStore(meanImage, coord, colorNew + colorMean); +} \ No newline at end of file diff --git a/projects/path_tracer/shaders/path_tracer.comp b/projects/path_tracer/shaders/path_tracer.comp new file mode 100644 index 0000000000000000000000000000000000000000..f08bdfd123ede964befe5feed4ba9f438dc0a498 --- /dev/null +++ b/projects/path_tracer/shaders/path_tracer.comp @@ -0,0 +1,430 @@ +#version 450 core +#extension GL_ARB_separate_shader_objects : enable + +const float pi = 3.1415926535897932384626433832795; +const float hitBias = 0.0001; // used to offset hits to avoid self intersection +const float denomMin = 0.001; + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +struct Material { + vec3 emission; + float ks; // specular percentage + vec3 albedo; + float r; // roughness + vec3 f0; + float padding; +}; + +struct Sphere{ + vec3 center; + float radius; + int materialIndex; + float padding[3]; +}; + +struct Plane{ + vec3 center; + int materialIndex; + vec3 N; + float padding1; + vec2 extent; + vec2 padding2; +}; + +layout(std430, binding = 0) buffer spheres{ + Sphere inSpheres[]; +}; + +layout(std430, binding = 1) buffer planes{ + Plane inPlanes[]; +}; + +layout(std430, binding = 2) buffer materials{ + Material inMaterials[]; +}; + +layout(set=0, binding = 3, rgba32f) uniform image2D outImage; + +layout( push_constant ) uniform constants{ + mat4 viewToWorld; + vec3 skyColor; + int sphereCount; + int planeCount; + int frameIndex; +}; + +// ---- Intersection functions ---- + +struct Ray{ + vec3 origin; + vec3 direction; +}; + +struct Intersection{ + bool hit; + float distance; + vec3 pos; + vec3 N; + Material material; +}; + +// https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection +Intersection raySphereIntersect(Ray ray, Sphere sphere){ + + Intersection intersection; + intersection.hit = false; + + vec3 L = sphere.center - ray.origin; + float tca = dot(L, ray.direction); + float d2 = dot(L, L) - tca * tca; + + if (d2 > sphere.radius * sphere.radius){ + return intersection; + } + float thc = float(sqrt(sphere.radius * sphere.radius - d2)); + float t0 = tca - thc; + float t1 = tca + thc; + + if (t0 < 0) + t0 = t1; + + if (t0 < 0) + return intersection; + + intersection.hit = true; + intersection.distance = t0; + intersection.pos = ray.origin + ray.direction * intersection.distance; + intersection.N = normalize(intersection.pos - sphere.center); + intersection.material = inMaterials[sphere.materialIndex]; + + return intersection; +} + +struct Basis{ + vec3 right; + vec3 up; + vec3 forward; +}; + +Basis buildBasisAroundNormal(vec3 N){ + Basis basis; + basis.up = N; + basis.right = abs(basis.up.x) < 0.99 ? vec3(1, 0, 0) : vec3(0, 0, 1); + basis.forward = normalize(cross(basis.up, basis.right)); + basis.right = cross(basis.up, basis.forward); + return basis; +} + +// see: https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection +Intersection rayPlaneIntersect(Ray ray, Plane plane){ + + Intersection intersection; + intersection.hit = false; + + vec3 toPlane = plane.center - ray.origin; + float denom = dot(ray.direction, plane.N); + if(abs(denom) < 0.001) + return intersection; + + intersection.distance = dot(toPlane, plane.N) / denom; + + if(intersection.distance < 0) + return intersection; + + intersection.pos = ray.origin + ray.direction * intersection.distance; + + vec3 centerToIntersection = intersection.pos - plane.center; + Basis planeBasis = buildBasisAroundNormal(plane.N); + float projectedRight = dot(centerToIntersection, planeBasis.right); + float projectedUp = dot(centerToIntersection, planeBasis.forward); + + intersection.hit = abs(projectedRight) <= plane.extent.x && abs(projectedUp) <= plane.extent.y; + intersection.N = plane.N; + intersection.material = inMaterials[plane.materialIndex]; + + return intersection; +} + +Intersection sceneIntersect(Ray ray) { + float minDistance = 100000; // lets start with something big + + Intersection intersection; + intersection.hit = false; + + for (int i = 0; i < sphereCount; i++) { + Intersection sphereIntersection = raySphereIntersect(ray, inSpheres[i]); + if (sphereIntersection.hit && sphereIntersection.distance < minDistance) { + intersection = sphereIntersection; + minDistance = intersection.distance; + } + } + for (int i = 0; i < planeCount; i++){ + Intersection planeIntersection = rayPlaneIntersect(ray, inPlanes[i]); + if (planeIntersection.hit && planeIntersection.distance < minDistance) { + intersection = planeIntersection; + minDistance = intersection.distance; + } + } + return intersection; +} + +vec3 biasHitPosition(vec3 hitPos, vec3 rayDirection, vec3 N){ + // return hitPos + N * hitBias; // works as long as no refraction/transmission is used and camera is outside sphere + return hitPos + sign(dot(rayDirection, N)) * N * hitBias; +} + +// ---- noise/hash functions for pseudorandom variables ---- + +// extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences +vec2 r2Sequence(uint n){ + n = n % 42000; + const float g = 1.32471795724474602596; + return fract(vec2( + n / g, + n / (g*g))); +} + +// random() and helpers from: https://www.shadertoy.com/view/XlycWh +float g_seed = 0; + +uint base_hash(uvec2 p) { + p = 1103515245U*((p >> 1U)^(p.yx)); + uint h32 = 1103515245U*((p.x)^(p.y>>3U)); + return h32^(h32 >> 16); +} + +vec2 hash2(inout float seed) { + uint n = base_hash(floatBitsToUint(vec2(seed+=.1,seed+=.1))); + uvec2 rz = uvec2(n, n*48271U); + return vec2(rz.xy & uvec2(0x7fffffffU))/float(0x7fffffff); +} + +void initRandom(ivec2 coord){ + g_seed = float(base_hash(coord)/float(0xffffffffU)+frameIndex); +} + +vec2 random(){ + return hash2(g_seed); +} + +// ---- shading ---- + +vec3 lambertBRDF(vec3 albedo){ + return albedo / pi; +} + +vec3 computeDiffuseBRDF(Material material){ + return lambertBRDF(material.albedo); +} + +float distributionGGX(float r, float NoH){ + float r2 = r*r; + float denom = pi * pow(NoH*NoH * (r2-1) + 1, 2); + return r2 / max(denom, denomMin); +} + +float geometryGGXSmith(float r, float NoL){ + float r2 = r*r; + float denom = NoL + sqrt(r2 + (1-r2) * NoL*NoL); + return 2 * NoL / max(denom, denomMin); +} + +float geometryGGX(float r, float NoV, float NoL){ + return geometryGGXSmith(r, NoV) * geometryGGXSmith(r, NoL); +} + +vec3 fresnelSchlick(vec3 f0, float NoH){ + return f0 + (1 - f0) * pow(1 - NoH, 5); +} + +vec3 computeSpecularBRDF(vec3 f0, float r, float NoV, float NoL, float NoH){ + float denom = 4 * NoV * NoL; + float D = distributionGGX(r, NoH); + float G = geometryGGX(r, NoV, NoL); + vec3 F = fresnelSchlick(f0, NoH); + return D * F * G / max(denom, denomMin); +} + +// ---- pathtracing and main ---- + +// distributions: https://link.springer.com/content/pdf/10.1007/978-1-4842-4427-2_16.pdf +float cosineDistributionPDF(float NoL){ + return NoL / pi; +} + +vec3 sampleCosineDistribution(vec2 xi){ + float phi = 2 * pi * xi.y; + return vec3( + sqrt(xi.x) * cos(phi), + sqrt(1 - xi.x), + sqrt(xi.x) * sin(phi)); +} + +float uniformDistributionPDF(){ + return 1.f / (2 * pi); +} + +vec3 sampleUniformDistribution(vec2 xi){ + float phi = 2 * pi * xi.y; + return vec3( + sqrt(xi.x) * cos(phi), + 1 - xi.x, + sqrt(xi.x) * sin(phi)); +} + +float ggxDistributionPDF(float r, float NoH){ + return distributionGGX(r, NoH) * NoH; +} + +float ggxDistributionPDFReflected(float r, float NoH, float NoV){ + float jacobian = 0.25 / max(NoV, denomMin); + return ggxDistributionPDF(r, NoH) * jacobian; +} + +vec3 sampleGGXDistribution(vec2 xi, float r){ + float phi = 2 * pi * xi.y; + float cosTheta = sqrt((1 - xi.x) / ((r*r - 1) * xi.x + 1)); + float sinTheta = sqrt(1 - cosTheta*cosTheta); + return vec3( + cos(phi) * sinTheta, + cosTheta, + sin(phi) * sinTheta); +} + +vec3 sampleTangentToWorldSpace(vec3 tangentSpaceSample, vec3 N){ + Basis tangentBasis = buildBasisAroundNormal(N); + return + tangentBasis.right * tangentSpaceSample.x + + tangentBasis.up * tangentSpaceSample.y + + tangentBasis.forward * tangentSpaceSample.z; +} + +vec3 castRay(Ray ray) { + + vec3 throughput = vec3(1); + vec3 color = vec3(0); + + const int maxDepth = 10; + for(int i = 0; i < maxDepth; i++){ + + Intersection intersection = sceneIntersect(ray); + + vec3 hitLighting = vec3(0); + vec3 brdf = vec3(1); + + // V is where the ray came from and will lead back to the camera (over multiple bounces) + vec3 V = -normalize(ray.direction); + vec3 R = reflect(-V, intersection.N); + float NoV = max(dot(intersection.N, V), 0); + + intersection.material.r *= intersection.material.r; // remapping for perceuptual linearity + intersection.material.r = max(intersection.material.r, 0.01); + + float kd = 1 - intersection.material.ks; + bool sampleDiffuse = random().x < kd; + + vec3 sampleTangentSpace; + float pdf; + if(sampleDiffuse){ + sampleTangentSpace = sampleCosineDistribution(random()); + ray.direction = sampleTangentToWorldSpace(sampleTangentSpace, intersection.N); + + float NoL = max(dot(intersection.N, ray.direction), 0); + pdf = cosineDistributionPDF(NoL); + } + else{ + #define IMPORTANCE + + #ifdef IMPORTANCE + sampleTangentSpace = sampleGGXDistribution(random(), intersection.material.r); + ray.direction = sampleTangentToWorldSpace(sampleTangentSpace, R); + vec3 L = normalize(ray.direction); + pdf = ggxDistributionPDFReflected(intersection.material.r, max(sampleTangentSpace.y, 0.01), max(dot(intersection.N, V), 0.01)); + #else + sampleTangentSpace = sampleUniformDistribution(random()); + ray.direction = sampleTangentToWorldSpace(sampleTangentSpace, intersection.N); + pdf = uniformDistributionPDF(); + #endif + } + + ray.origin = biasHitPosition(intersection.pos, ray.direction, intersection.N); + + // L is where the ray is going, as that is the direction where light will from + vec3 L = normalize(ray.direction); + vec3 H = normalize(L + V); + + float NoL = max(dot(intersection.N, L), 0); + float NoH = max(dot(intersection.N, H), 0); + + if(intersection.hit){ + vec3 diffuseBRDF = computeDiffuseBRDF(intersection.material); + + vec3 specularBRDF = computeSpecularBRDF(intersection.material.f0, intersection.material.r, NoV, NoL, NoH); + brdf = mix(diffuseBRDF, specularBRDF, intersection.material.ks); + + + hitLighting = intersection.material.emission * max(sign(NoV), 0); // objects only emit in direction of normal + } + else{ + hitLighting = skyColor; + } + + color += hitLighting * throughput; + throughput *= brdf * NoL / max(pdf, denomMin); + + if(!intersection.hit) + break; + } + + return color; +} + +// coord must be in pixel coordinates, but already shifted to pixel center +vec3 computeCameraRay(vec2 coord){ + + ivec2 outImageRes = imageSize(outImage); + float fovDegree = 45; + float fov = fovDegree * pi / 180; + + vec2 uv = coord / vec2(outImageRes); + vec2 ndc = 2 * uv - 1; + + float tanFovHalf = tan(fov / 2.f); + float aspectRatio = outImageRes.x / float(outImageRes.y); + float x = ndc.x * tanFovHalf * aspectRatio; + float y = -ndc.y * tanFovHalf; + + // view direction goes through pixel on image plane with z=1 + vec3 directionViewSpace = normalize(vec3(x, y, 1)); + vec3 directionWorldSpace = mat3(viewToWorld) * directionViewSpace; + return directionWorldSpace; +} + +void main(){ + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + vec2 pixelSize = 1.f / coord; + initRandom(coord); + + Ray cameraRay; + cameraRay.origin = viewToWorld[3].xyz; + vec2 coordCentered = coord + 0.5; + + vec3 color = vec3(0); + + const int samplesPerPixel = 1; + for(int i = 0; i < samplesPerPixel; i++){ + vec2 jitter = r2Sequence(i + frameIndex) - 0.5; + cameraRay.direction = computeCameraRay(coordCentered + jitter); + color += castRay(cameraRay); + } + color /= samplesPerPixel; + + vec4 final = vec4(color, 1); + + // occasional NaNs in reflection, should be fixed properly + if(any(isnan(color))) + final = vec4(0); + + imageStore(outImage, coord, final); +} \ No newline at end of file diff --git a/projects/path_tracer/shaders/presentImage.comp b/projects/path_tracer/shaders/presentImage.comp new file mode 100644 index 0000000000000000000000000000000000000000..a52159c0c6173779b091e5d4153b15b0a6361780 --- /dev/null +++ b/projects/path_tracer/shaders/presentImage.comp @@ -0,0 +1,23 @@ +#version 440 +#extension GL_GOOGLE_include_directive : enable + +layout(set=0, binding=0, rgba32f) uniform image2D inImage; +layout(set=0, binding=1, rgba8) uniform image2D outImage; + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +void main(){ + + ivec2 outImageRes = imageSize(outImage); + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + + if(any(greaterThanEqual(coord, outImageRes))) + return; + + vec4 colorRaw = imageLoad(inImage, coord); + vec3 colorNormalized = colorRaw.rgb / colorRaw.a; + vec3 colorTonemapped = colorNormalized / (1 + dot(colorNormalized, vec3(0.71, 0.21, 0.08))); // reinhard tonemapping + vec3 colorGammaCorrected = pow(colorTonemapped, vec3(1.f / 2.2)); + + imageStore(outImage, coord, vec4(colorGammaCorrected, 0)); +} \ No newline at end of file diff --git a/projects/path_tracer/src/main.cpp b/projects/path_tracer/src/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c3b21f78cea479a463fb6224605a202d35d8e581 --- /dev/null +++ b/projects/path_tracer/src/main.cpp @@ -0,0 +1,451 @@ +#include <vkcv/Core.hpp> +#include <vkcv/camera/CameraManager.hpp> +#include <vkcv/asset/asset_loader.hpp> +#include <vkcv/shader/GLSLCompiler.hpp> +#include "vkcv/gui/GUI.hpp" +#include <chrono> +#include <vector> + +int main(int argc, const char** argv) { + + // structs must match shader version + struct Material { + Material(const glm::vec3& emission, const glm::vec3& albedo, float ks, float roughness, const glm::vec3& f0) + : emission(emission), albedo(albedo), ks(ks), roughness(roughness), f0(f0){} + + glm::vec3 emission; + float ks; + glm::vec3 albedo; + float roughness; + glm::vec3 f0; + float padding; + }; + + struct Sphere { + Sphere(const glm::vec3& c, const float& r, const int m) : center(c), radius(r), materialIndex(m) {} + + glm::vec3 center; + float radius; + uint32_t materialIndex; + float padding[3]; + }; + + struct Plane { + Plane(const glm::vec3& c, const glm::vec3& n, const glm::vec2 e, int m) + : center(c), normal(n), extent(e), materialIndex(m) {} + + glm::vec3 center; + uint32_t materialIndex; + glm::vec3 normal; + float padding1; + glm::vec2 extent; + glm::vec2 padding3; + }; + + const char* applicationName = "Path Tracer"; + + const int initialWidth = 1280; + const int initialHeight = 720; + vkcv::Window window = vkcv::Window::create( + applicationName, + initialWidth, + initialHeight, + true); + + vkcv::Core core = vkcv::Core::create( + window, + applicationName, + VK_MAKE_VERSION(0, 0, 1), + { vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute }, + { "VK_KHR_swapchain" } + ); + + // images + vkcv::ImageHandle outputImage = core.createImage( + vk::Format::eR32G32B32A32Sfloat, + initialWidth, + initialHeight, + 1, + false, + true).getHandle(); + + vkcv::ImageHandle meanImage = core.createImage( + vk::Format::eR32G32B32A32Sfloat, + initialWidth, + initialHeight, + 1, + false, + true).getHandle(); + + vkcv::shader::GLSLCompiler compiler; + + // path tracing shader + vkcv::ShaderProgram traceShaderProgram{}; + + compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/path_tracer.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) { + traceShaderProgram.addShader(shaderStage, path); + }); + + const vkcv::DescriptorBindings& traceDescriptorBindings = traceShaderProgram.getReflectedDescriptors().at(0); + vkcv::DescriptorSetLayoutHandle traceDescriptorSetLayout = core.createDescriptorSetLayout(traceDescriptorBindings); + vkcv::DescriptorSetHandle traceDescriptorSet = core.createDescriptorSet(traceDescriptorSetLayout); + + // image combine shader + vkcv::ShaderProgram imageCombineShaderProgram{}; + + compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/combineImages.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) { + imageCombineShaderProgram.addShader(shaderStage, path); + }); + + const vkcv::DescriptorBindings& imageCombineDescriptorBindings = imageCombineShaderProgram.getReflectedDescriptors().at(0); + vkcv::DescriptorSetLayoutHandle imageCombineDescriptorSetLayout = core.createDescriptorSetLayout(imageCombineDescriptorBindings); + vkcv::DescriptorSetHandle imageCombineDescriptorSet = core.createDescriptorSet(imageCombineDescriptorSetLayout); + vkcv::PipelineHandle imageCombinePipeline = core.createComputePipeline( + imageCombineShaderProgram, + { core.getDescriptorSetLayout(imageCombineDescriptorSetLayout).vulkanHandle }); + + vkcv::DescriptorWrites imageCombineDescriptorWrites; + imageCombineDescriptorWrites.storageImageWrites = { + vkcv::StorageImageDescriptorWrite(0, outputImage), + vkcv::StorageImageDescriptorWrite(1, meanImage) + }; + core.writeDescriptorSet(imageCombineDescriptorSet, imageCombineDescriptorWrites); + + // image present shader + vkcv::ShaderProgram presentShaderProgram{}; + + compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/presentImage.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) { + presentShaderProgram.addShader(shaderStage, path); + }); + + const vkcv::DescriptorBindings& presentDescriptorBindings = presentShaderProgram.getReflectedDescriptors().at(0); + vkcv::DescriptorSetLayoutHandle presentDescriptorSetLayout = core.createDescriptorSetLayout(presentDescriptorBindings); + vkcv::DescriptorSetHandle presentDescriptorSet = core.createDescriptorSet(presentDescriptorSetLayout); + vkcv::PipelineHandle presentPipeline = core.createComputePipeline( + presentShaderProgram, + { core.getDescriptorSetLayout(presentDescriptorSetLayout).vulkanHandle }); + + // clear shader + vkcv::ShaderProgram clearShaderProgram{}; + + compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/clearImage.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) { + clearShaderProgram.addShader(shaderStage, path); + }); + + const vkcv::DescriptorBindings& imageClearDescriptorBindings = clearShaderProgram.getReflectedDescriptors().at(0); + vkcv::DescriptorSetLayoutHandle imageClearDescriptorSetLayout = core.createDescriptorSetLayout(imageClearDescriptorBindings); + vkcv::DescriptorSetHandle imageClearDescriptorSet = core.createDescriptorSet(imageClearDescriptorSetLayout); + vkcv::PipelineHandle imageClearPipeline = core.createComputePipeline( + clearShaderProgram, + { core.getDescriptorSetLayout(imageClearDescriptorSetLayout).vulkanHandle }); + + vkcv::DescriptorWrites imageClearDescriptorWrites; + imageClearDescriptorWrites.storageImageWrites = { + vkcv::StorageImageDescriptorWrite(0, meanImage) + }; + core.writeDescriptorSet(imageClearDescriptorSet, imageClearDescriptorWrites); + + // buffers + typedef std::pair<std::string, Material> MaterialSetting; + + std::vector<MaterialSetting> materialSettings; + materialSettings.emplace_back(MaterialSetting("white", Material(glm::vec3(0), glm::vec3(0.65), 0, 0.25, glm::vec3(0.04)))); + materialSettings.emplace_back(MaterialSetting("red", Material(glm::vec3(0), glm::vec3(0.5, 0.0, 0.0), 0, 0.25, glm::vec3(0.04)))); + materialSettings.emplace_back(MaterialSetting("green", Material(glm::vec3(0), glm::vec3(0.0, 0.5, 0.0), 0, 0.25, glm::vec3(0.04)))); + materialSettings.emplace_back(MaterialSetting("light", Material(glm::vec3(20), glm::vec3(0), 0, 0.25, glm::vec3(0.04)))); + materialSettings.emplace_back(MaterialSetting("sphere", Material(glm::vec3(0), glm::vec3(0.65), 1, 0.25, glm::vec3(0.04)))); + materialSettings.emplace_back(MaterialSetting("ground", Material(glm::vec3(0), glm::vec3(0.65), 0, 0.25, glm::vec3(0.04)))); + + const uint32_t whiteMaterialIndex = 0; + const uint32_t redMaterialIndex = 1; + const uint32_t greenMaterialIndex = 2; + const uint32_t lightMaterialIndex = 3; + const uint32_t sphereMaterialIndex = 4; + const uint32_t groundMaterialIndex = 5; + + std::vector<Sphere> spheres; + spheres.emplace_back(Sphere(glm::vec3(0, -1.5, 0), 0.5, sphereMaterialIndex)); + + std::vector<Plane> planes; + planes.emplace_back(Plane(glm::vec3( 0, -2, 0), glm::vec3( 0, 1, 0), glm::vec2(2), groundMaterialIndex)); + planes.emplace_back(Plane(glm::vec3( 0, 2, 0), glm::vec3( 0, -1, 0), glm::vec2(2), whiteMaterialIndex)); + planes.emplace_back(Plane(glm::vec3( 2, 0, 0), glm::vec3(-1, 0, 0), glm::vec2(2), redMaterialIndex)); + planes.emplace_back(Plane(glm::vec3(-2, 0, 0), glm::vec3( 1, 0, 0), glm::vec2(2), greenMaterialIndex)); + planes.emplace_back(Plane(glm::vec3( 0, 0, 2), glm::vec3( 0, 0, -1), glm::vec2(2), whiteMaterialIndex)); + planes.emplace_back(Plane(glm::vec3( 0, 1.9, 0), glm::vec3( 0, -1, 0), glm::vec2(1), lightMaterialIndex)); + + vkcv::Buffer<Sphere> sphereBuffer = core.createBuffer<Sphere>( + vkcv::BufferType::STORAGE, + spheres.size()); + sphereBuffer.fill(spheres); + + vkcv::Buffer<Plane> planeBuffer = core.createBuffer<Plane>( + vkcv::BufferType::STORAGE, + planes.size()); + planeBuffer.fill(planes); + + vkcv::Buffer<Material> materialBuffer = core.createBuffer<Material>( + vkcv::BufferType::STORAGE, + materialSettings.size()); + + vkcv::DescriptorWrites traceDescriptorWrites; + traceDescriptorWrites.storageBufferWrites = { + vkcv::BufferDescriptorWrite(0, sphereBuffer.getHandle()), + vkcv::BufferDescriptorWrite(1, planeBuffer.getHandle()), + vkcv::BufferDescriptorWrite(2, materialBuffer.getHandle())}; + traceDescriptorWrites.storageImageWrites = { + vkcv::StorageImageDescriptorWrite(3, outputImage)}; + core.writeDescriptorSet(traceDescriptorSet, traceDescriptorWrites); + + vkcv::PipelineHandle tracePipeline = core.createComputePipeline( + traceShaderProgram, + { core.getDescriptorSetLayout(traceDescriptorSetLayout).vulkanHandle }); + + if (!tracePipeline) + { + vkcv_log(vkcv::LogLevel::ERROR, "Could not create graphics pipeline. Exiting."); + return EXIT_FAILURE; + } + + vkcv::camera::CameraManager cameraManager(window); + uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT); + + cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -2)); + + auto startTime = std::chrono::system_clock::now(); + float time = 0; + int frameIndex = 0; + bool clearMeanImage = true; + bool updateMaterials = true; + + float cameraPitchPrevious = 0; + float cameraYawPrevious = 0; + glm::vec3 cameraPositionPrevious = glm::vec3(0); + + uint32_t widthPrevious = initialWidth; + uint32_t heightPrevious = initialHeight; + + vkcv::gui::GUI gui(core, window); + + bool renderUI = true; + window.e_key.add([&renderUI](int key, int scancode, int action, int mods) { + if (key == GLFW_KEY_I && action == GLFW_PRESS) { + renderUI = !renderUI; + } + }); + + glm::vec3 skyColor = glm::vec3(0.2, 0.7, 0.8); + float skyColorMultiplier = 1; + + while (window.isWindowOpen()) + { + vkcv::Window::pollEvents(); + + uint32_t swapchainWidth, swapchainHeight; // No resizing = No problem + if (!core.beginFrame(swapchainWidth, swapchainHeight)) { + continue; + } + + if (swapchainWidth != widthPrevious || swapchainHeight != heightPrevious) { + + // resize images + outputImage = core.createImage( + vk::Format::eR32G32B32A32Sfloat, + swapchainWidth, + swapchainHeight, + 1, + false, + true).getHandle(); + + meanImage = core.createImage( + vk::Format::eR32G32B32A32Sfloat, + swapchainWidth, + swapchainHeight, + 1, + false, + true).getHandle(); + + // update descriptor sets + traceDescriptorWrites.storageImageWrites = { + vkcv::StorageImageDescriptorWrite(3, outputImage) }; + core.writeDescriptorSet(traceDescriptorSet, traceDescriptorWrites); + + vkcv::DescriptorWrites imageCombineDescriptorWrites; + imageCombineDescriptorWrites.storageImageWrites = { + vkcv::StorageImageDescriptorWrite(0, outputImage), + vkcv::StorageImageDescriptorWrite(1, meanImage) + }; + core.writeDescriptorSet(imageCombineDescriptorSet, imageCombineDescriptorWrites); + + vkcv::DescriptorWrites imageClearDescriptorWrites; + imageClearDescriptorWrites.storageImageWrites = { + vkcv::StorageImageDescriptorWrite(0, meanImage) + }; + core.writeDescriptorSet(imageClearDescriptorSet, imageClearDescriptorWrites); + + widthPrevious = swapchainWidth; + heightPrevious = swapchainHeight; + + clearMeanImage = true; + } + + auto end = std::chrono::system_clock::now(); + auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - startTime); + startTime = end; + + time += 0.000001f * static_cast<float>(deltatime.count()); + + cameraManager.update(0.000001 * static_cast<double>(deltatime.count())); + + const vkcv::CommandStreamHandle cmdStream = core.createCommandStream(vkcv::QueueType::Graphics); + + uint32_t fullscreenDispatchCount[3] = { + static_cast<uint32_t> (std::ceil(swapchainWidth / 8.f)), + static_cast<uint32_t> (std::ceil(swapchainHeight / 8.f)), + 1 }; + + if (updateMaterials) { + std::vector<Material> materials; + for (const auto& settings : materialSettings) { + materials.push_back(settings.second); + } + materialBuffer.fill(materials); + updateMaterials = false; + clearMeanImage = true; + } + + float cameraPitch; + float cameraYaw; + cameraManager.getActiveCamera().getAngles(cameraPitch, cameraYaw); + + if (glm::abs(cameraPitch - cameraPitchPrevious) > 0.01 || glm::abs(cameraYaw - cameraYawPrevious) > 0.01) + clearMeanImage = true; // camera rotated + + cameraPitchPrevious = cameraPitch; + cameraYawPrevious = cameraYaw; + + glm::vec3 cameraPosition = cameraManager.getActiveCamera().getPosition(); + + if(glm::distance(cameraPosition, cameraPositionPrevious) > 0.0001) + clearMeanImage = true; // camera moved + + cameraPositionPrevious = cameraPosition; + + if (clearMeanImage) { + core.prepareImageForStorage(cmdStream, meanImage); + + core.recordComputeDispatchToCmdStream(cmdStream, + imageClearPipeline, + fullscreenDispatchCount, + { vkcv::DescriptorSetUsage(0, core.getDescriptorSet(imageClearDescriptorSet).vulkanHandle) }, + vkcv::PushConstants(0)); + + clearMeanImage = false; + } + + // path tracing + struct RaytracingPushConstantData { + glm::mat4 viewToWorld; + glm::vec3 skyColor; + int32_t sphereCount; + int32_t planeCount; + int32_t frameIndex; + }; + + RaytracingPushConstantData raytracingPushData; + raytracingPushData.viewToWorld = glm::inverse(cameraManager.getActiveCamera().getView()); + raytracingPushData.skyColor = skyColor * skyColorMultiplier; + raytracingPushData.sphereCount = spheres.size(); + raytracingPushData.planeCount = planes.size(); + raytracingPushData.frameIndex = frameIndex; + + vkcv::PushConstants pushConstantsCompute(sizeof(RaytracingPushConstantData)); + pushConstantsCompute.appendDrawcall(raytracingPushData); + + uint32_t traceDispatchCount[3] = { + static_cast<uint32_t> (std::ceil(swapchainWidth / 16.f)), + static_cast<uint32_t> (std::ceil(swapchainHeight / 16.f)), + 1 }; + + core.prepareImageForStorage(cmdStream, outputImage); + + core.recordComputeDispatchToCmdStream(cmdStream, + tracePipeline, + traceDispatchCount, + { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(traceDescriptorSet).vulkanHandle) }, + pushConstantsCompute); + + core.prepareImageForStorage(cmdStream, meanImage); + core.recordImageMemoryBarrier(cmdStream, outputImage); + + // combine images + core.recordComputeDispatchToCmdStream(cmdStream, + imageCombinePipeline, + fullscreenDispatchCount, + { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(imageCombineDescriptorSet).vulkanHandle) }, + vkcv::PushConstants(0)); + + core.recordImageMemoryBarrier(cmdStream, meanImage); + + // present image + const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle(); + + vkcv::DescriptorWrites presentDescriptorWrites; + presentDescriptorWrites.storageImageWrites = { + vkcv::StorageImageDescriptorWrite(0, meanImage), + vkcv::StorageImageDescriptorWrite(1, swapchainInput) }; + core.writeDescriptorSet(presentDescriptorSet, presentDescriptorWrites); + + core.prepareImageForStorage(cmdStream, swapchainInput); + + core.recordComputeDispatchToCmdStream(cmdStream, + presentPipeline, + fullscreenDispatchCount, + { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(presentDescriptorSet).vulkanHandle) }, + vkcv::PushConstants(0)); + + core.prepareSwapchainImageForPresent(cmdStream); + core.submitCommandStream(cmdStream); + + if (renderUI) { + gui.beginGUI(); + + ImGui::Begin("Settings"); + + clearMeanImage |= ImGui::ColorEdit3("Sky color", &skyColor.x); + clearMeanImage |= ImGui::InputFloat("Sky color multiplier", &skyColorMultiplier); + + if (ImGui::CollapsingHeader("Materials")) { + + for (auto& setting : materialSettings) { + if (ImGui::CollapsingHeader(setting.first.c_str())) { + + const glm::vec3 emission = setting.second.emission; + float emissionStrength = glm::max(glm::max(glm::max(emission.x, emission.y), emission.z), 1.f); + glm::vec3 emissionColor = emission / emissionStrength; + + updateMaterials |= ImGui::ColorEdit3((std::string("Emission color ") + setting.first).c_str(), &emissionColor.x); + updateMaterials |= ImGui::InputFloat((std::string("Emission strength ") + setting.first).c_str(), &emissionStrength); + + setting.second.emission = emissionStrength * emissionColor; + + updateMaterials |= ImGui::ColorEdit3((std::string("Albedo color ") + setting.first).c_str(), &setting.second.albedo.x); + updateMaterials |= ImGui::ColorEdit3((std::string("F0 ") + setting.first).c_str(), &setting.second.f0.x); + updateMaterials |= ImGui::DragFloat(( std::string("ks ") + setting.first).c_str(), &setting.second.ks, 0.01, 0, 1); + updateMaterials |= ImGui::DragFloat(( std::string("roughness ") + setting.first).c_str(), &setting.second.roughness, 0.01, 0, 1); + + } + } + } + + ImGui::End(); + + gui.endGUI(); + } + + core.endFrame(); + + frameIndex++; + } + return 0; +}