diff --git a/CMakeLists.txt b/CMakeLists.txt index 72d57ab..41d5001 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,7 +75,8 @@ set(WGSL_SHADER_FILES reference_path_tracer.wgsl texture_blit.wgsl hybrid_renderer_gbuffer_pass.wgsl - hybrid_renderer_debug_pass.wgsl) + hybrid_renderer_debug_pass.wgsl + hybrid_renderer_sky_pass.wgsl) set(SHADER_SOURCE_HEADER_FILE src/pt/shader_source.hpp) diff --git a/src/pt/aligned_sky_state.hpp b/src/pt/aligned_sky_state.hpp new file mode 100644 index 0000000..1da45eb --- /dev/null +++ b/src/pt/aligned_sky_state.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include <common/assert.hpp> +#include <common/units/angle.hpp> + +#include <glm/glm.hpp> +#include <hw-skymodel/hw_skymodel.h> + +#include <array> +#include <cstring> +#include <numbers> + +namespace nlrs +{ +struct Sky +{ + float turbidity = 1.0f; + std::array<float, 3> albedo = {1.0f, 1.0f, 1.0f}; + float sunZenithDegrees = 30.0f; + float sunAzimuthDegrees = 0.0f; + + bool operator==(const Sky&) const noexcept = default; +}; + +// A 16-byte aligned sky state for the hw-skymodel library. Matches the layout of the following WGSL +// struct: +// +// struct SkyState { +// params: array<f32, 27>, +// skyRadiances: array<f32, 3>, +// solarRadiances: array<f32, 3>, +// sunDirection: vec3<f32>, +// }; +struct AlignedSkyState +{ + float params[27]; // offset: 0 + float skyRadiances[3]; // offset: 27 + float solarRadiances[3]; // offset: 30 + float padding1[3]; // offset: 33 + glm::vec3 sunDirection; // offset: 36 + float padding2; // offset: 39 + + inline AlignedSkyState(const Sky& sky) + : params{0}, + skyRadiances{0}, + solarRadiances{0}, + padding1{0.f, 0.f, 0.f}, + sunDirection(0.f), + padding2(0.0f) + { + const float sunZenith = Angle::degrees(sky.sunZenithDegrees).asRadians(); + const float sunAzimuth = Angle::degrees(sky.sunAzimuthDegrees).asRadians(); + + sunDirection = glm::normalize(glm::vec3( + std::sin(sunZenith) * std::cos(sunAzimuth), + std::cos(sunZenith), + -std::sin(sunZenith) * std::sin(sunAzimuth))); + + const sky_params skyParams{ + .elevation = 0.5f * std::numbers::pi_v<float> - sunZenith, + .turbidity = sky.turbidity, + .albedo = {sky.albedo[0], sky.albedo[1], sky.albedo[2]}}; + + sky_state skyState; + NLRS_ASSERT(sky_state_new(&skyParams, &skyState) == sky_state_result_success); + + std::memcpy(params, skyState.params, sizeof(skyState.params)); + std::memcpy(skyRadiances, skyState.sky_radiances, sizeof(skyState.sky_radiances)); + std::memcpy(solarRadiances, skyState.solar_radiances, sizeof(skyState.solar_radiances)); + } +}; +} // namespace nlrs diff --git a/src/pt/hybrid_renderer.cpp b/src/pt/hybrid_renderer.cpp index 926e15f..daae70c 100644 --- a/src/pt/hybrid_renderer.cpp +++ b/src/pt/hybrid_renderer.cpp @@ -71,7 +71,8 @@ HybridRenderer::HybridRenderer( mGbufferBindGroupLayout(), mGbufferBindGroup(), mGbufferPass(gpuContext, rendererDesc), - mDebugPass() + mDebugPass(), + mSkyPass(gpuContext) { { const std::array<WGPUTextureFormat, 1> depthFormats{ @@ -169,8 +170,10 @@ HybridRenderer::~HybridRenderer() void HybridRenderer::render( const GpuContext& gpuContext, - const WGPUTextureView textureView, - const glm::mat4& viewProjectionMat) + const glm::mat4& viewProjectionMat, + const glm::vec3& cameraPosition, + const Sky& sky, + const WGPUTextureView textureView) { wgpuDeviceTick(gpuContext.device); @@ -190,7 +193,7 @@ void HybridRenderer::render( mAlbedoTextureView, mNormalTextureView); - mDebugPass.render(mGbufferBindGroup, encoder, textureView); + mSkyPass.render(gpuContext, viewProjectionMat, cameraPosition, sky, encoder, textureView); const WGPUCommandBuffer cmdBuffer = [encoder]() { const WGPUCommandBufferDescriptor cmdBufferDesc{ @@ -1020,4 +1023,240 @@ void HybridRenderer::DebugPass::resize(const GpuContext& gpuContext, const Exten wgpuQueueWriteBuffer( gpuContext.queue, mUniformBuffer.ptr(), 0, &uniformData.x, sizeof(Extent2<float>)); } + +HybridRenderer::SkyPass::SkyPass(const GpuContext& gpuContext) + : mCurrentSky{}, + mVertexBuffer{ + gpuContext.device, + "Sky vertex buffer", + GpuBufferUsage::Vertex | GpuBufferUsage::CopyDst, + std::span<const float[2]>(quadVertexData)}, + mSkyStateBuffer{ + gpuContext.device, + "Sky state buffer", + GpuBufferUsage::ReadOnlyStorage | GpuBufferUsage::CopyDst, + sizeof(AlignedSkyState)}, + mSkyStateBindGroup{}, + mUniformBuffer{ + gpuContext.device, + "Sky uniform buffer", + GpuBufferUsage::Uniform | GpuBufferUsage::CopyDst, + sizeof(Uniforms)}, + mUniformBindGroup{}, + mPipeline(nullptr) +{ + { + const AlignedSkyState skyState{mCurrentSky}; + wgpuQueueWriteBuffer( + gpuContext.queue, mSkyStateBuffer.ptr(), 0, &skyState, sizeof(AlignedSkyState)); + } + + const GpuBindGroupLayout skyStateBindGroupLayout{ + gpuContext.device, + "Sky pass sky state bind group layout", + mSkyStateBuffer.bindGroupLayoutEntry(0, WGPUShaderStage_Fragment, sizeof(AlignedSkyState))}; + + mSkyStateBindGroup = GpuBindGroup{ + gpuContext.device, + "Sky pass sky state bind group", + skyStateBindGroupLayout.ptr(), + mSkyStateBuffer.bindGroupEntry(0)}; + + const GpuBindGroupLayout uniformBindGroupLayout{ + gpuContext.device, + "Sky passs uniform bind group layout", + mUniformBuffer.bindGroupLayoutEntry(0, WGPUShaderStage_Fragment, sizeof(Uniforms))}; + + mUniformBindGroup = GpuBindGroup{ + gpuContext.device, + "Sky pass uniform bind group", + uniformBindGroupLayout.ptr(), + mUniformBuffer.bindGroupEntry(0)}; + + { + // Pipeline layout + + const WGPUBindGroupLayout bindGroupLayouts[] = { + skyStateBindGroupLayout.ptr(), uniformBindGroupLayout.ptr()}; + + const WGPUPipelineLayoutDescriptor pipelineLayoutDesc{ + .nextInChain = nullptr, + .label = "Sky pass pipeline layout", + .bindGroupLayoutCount = std::size(bindGroupLayouts), + .bindGroupLayouts = bindGroupLayouts, + }; + + const WGPUPipelineLayout pipelineLayout = + wgpuDeviceCreatePipelineLayout(gpuContext.device, &pipelineLayoutDesc); + + // Vertex layout + + const WGPUVertexAttribute vertexAttributes[] = {WGPUVertexAttribute{ + .format = WGPUVertexFormat_Float32x2, + .offset = 0, + .shaderLocation = 0, + }}; + + const WGPUVertexBufferLayout vertexBufferLayout{ + .arrayStride = sizeof(float[2]), + .stepMode = WGPUVertexStepMode_Vertex, + .attributeCount = std::size(vertexAttributes), + .attributes = vertexAttributes, + }; + + // Shader module + + const WGPUShaderModule shaderModule = [&gpuContext]() -> WGPUShaderModule { + const WGPUShaderModuleWGSLDescriptor wgslDesc = { + .chain = + WGPUChainedStruct{ + .next = nullptr, + .sType = WGPUSType_ShaderModuleWGSLDescriptor, + }, + .code = HYBRID_RENDERER_SKY_PASS_SOURCE, + }; + + const WGPUShaderModuleDescriptor moduleDesc{ + .nextInChain = &wgslDesc.chain, + .label = "Sky pass shader", + }; + + return wgpuDeviceCreateShaderModule(gpuContext.device, &moduleDesc); + }(); + NLRS_ASSERT(shaderModule != nullptr); + + // Fragment state + + const WGPUBlendState blendState{ + .color = + WGPUBlendComponent{ + .operation = WGPUBlendOperation_Add, + .srcFactor = WGPUBlendFactor_One, + .dstFactor = WGPUBlendFactor_OneMinusSrcAlpha, + }, + .alpha = + WGPUBlendComponent{ + .operation = WGPUBlendOperation_Add, + .srcFactor = WGPUBlendFactor_Zero, + .dstFactor = WGPUBlendFactor_One, + }, + }; + + const WGPUColorTargetState colorTargets[] = {WGPUColorTargetState{ + .nextInChain = nullptr, + .format = Window::SWAP_CHAIN_FORMAT, + .blend = &blendState, + .writeMask = WGPUColorWriteMask_All}}; + + const WGPUFragmentState fragmentState{ + .nextInChain = nullptr, + .module = shaderModule, + .entryPoint = "fsMain", + .constantCount = 0, + .constants = nullptr, + .targetCount = std::size(colorTargets), + .targets = colorTargets, + }; + + // Pipeline + + const WGPURenderPipelineDescriptor pipelineDesc{ + .nextInChain = nullptr, + .label = "Sky pass render pipeline", + .layout = pipelineLayout, + .vertex = + WGPUVertexState{ + .nextInChain = nullptr, + .module = shaderModule, + .entryPoint = "vsMain", + .constantCount = 0, + .constants = nullptr, + .bufferCount = 1, + .buffers = &vertexBufferLayout, + }, + .primitive = + WGPUPrimitiveState{ + .nextInChain = nullptr, + .topology = WGPUPrimitiveTopology_TriangleList, + .stripIndexFormat = WGPUIndexFormat_Undefined, + .frontFace = WGPUFrontFace_CCW, + .cullMode = WGPUCullMode_Back, + }, + .depthStencil = nullptr, + .multisample = + WGPUMultisampleState{ + .nextInChain = nullptr, + .count = 1, + .mask = ~0u, + .alphaToCoverageEnabled = false, + }, + .fragment = &fragmentState, + }; + + mPipeline = wgpuDeviceCreateRenderPipeline(gpuContext.device, &pipelineDesc); + wgpuPipelineLayoutRelease(pipelineLayout); + } +} + +HybridRenderer::SkyPass::~SkyPass() +{ + renderPipelineSafeRelease(mPipeline); + mPipeline = nullptr; +} + +void HybridRenderer::SkyPass::render( + const GpuContext& gpuContext, + const glm::mat4& viewProjectionMat, + const glm::vec3& cameraPosition, + const Sky& sky, + const WGPUCommandEncoder cmdEncoder, + const WGPUTextureView textureView) +{ + if (mCurrentSky != sky) + { + mCurrentSky = sky; + const AlignedSkyState skyState{sky}; + wgpuQueueWriteBuffer( + gpuContext.queue, mSkyStateBuffer.ptr(), 0, &skyState, sizeof(AlignedSkyState)); + } + + { + const Uniforms uniforms{glm::inverse(viewProjectionMat), glm::vec4(cameraPosition, 1.f)}; + wgpuQueueWriteBuffer( + gpuContext.queue, mUniformBuffer.ptr(), 0, &uniforms, sizeof(Uniforms)); + } + + const WGPURenderPassEncoder renderPass = [cmdEncoder, textureView]() -> WGPURenderPassEncoder { + const WGPURenderPassColorAttachment colorAttachment{ + .nextInChain = nullptr, + .view = textureView, + .depthSlice = WGPU_DEPTH_SLICE_UNDEFINED, + .resolveTarget = nullptr, + .loadOp = WGPULoadOp_Clear, + .storeOp = WGPUStoreOp_Store, + .clearValue = WGPUColor{0.0, 0.0, 0.0, 1.0}, + }; + + const WGPURenderPassDescriptor renderPassDesc{ + .nextInChain = nullptr, + .label = "Sky pass render pass", + .colorAttachmentCount = 1, + .colorAttachments = &colorAttachment, + .depthStencilAttachment = nullptr, + .occlusionQuerySet = nullptr, + .timestampWrites = nullptr, + }; + + return wgpuCommandEncoderBeginRenderPass(cmdEncoder, &renderPassDesc); + }(); + NLRS_ASSERT(renderPass != nullptr); + + wgpuRenderPassEncoderSetPipeline(renderPass, mPipeline); + wgpuRenderPassEncoderSetBindGroup(renderPass, 0, mSkyStateBindGroup.ptr(), 0, nullptr); + wgpuRenderPassEncoderSetBindGroup(renderPass, 1, mUniformBindGroup.ptr(), 0, nullptr); + wgpuRenderPassEncoderSetVertexBuffer( + renderPass, 0, mVertexBuffer.ptr(), 0, mVertexBuffer.byteSize()); + wgpuRenderPassEncoderDraw(renderPass, 6, 1, 0, 0); + wgpuRenderPassEncoderEnd(renderPass); +} } // namespace nlrs diff --git a/src/pt/hybrid_renderer.hpp b/src/pt/hybrid_renderer.hpp index 31fe5ac..92fc2b8 100644 --- a/src/pt/hybrid_renderer.hpp +++ b/src/pt/hybrid_renderer.hpp @@ -1,5 +1,6 @@ #pragma once +#include "aligned_sky_state.hpp" #include "gpu_bind_group.hpp" #include "gpu_bind_group_layout.hpp" #include "gpu_buffer.hpp" @@ -43,7 +44,12 @@ class HybridRenderer HybridRenderer(HybridRenderer&&) = delete; HybridRenderer& operator=(HybridRenderer&&) = delete; - void render(const GpuContext&, WGPUTextureView, const glm::mat4& viewProjectionMatrix); + void render( + const GpuContext& gpuContext, + const glm::mat4& viewProjectionMatrix, + const glm::vec3& cameraPosition, + const Sky& sky, + WGPUTextureView targetTextureView); void resize(const GpuContext&, const Extent2u&); private: @@ -124,6 +130,42 @@ class HybridRenderer void resize(const GpuContext&, const Extent2u&); }; + struct SkyPass + { + private: + Sky mCurrentSky; + GpuBuffer mVertexBuffer; + GpuBuffer mSkyStateBuffer; + GpuBindGroup mSkyStateBindGroup; + GpuBuffer mUniformBuffer; + GpuBindGroup mUniformBindGroup; + WGPURenderPipeline mPipeline; + + struct Uniforms + { + glm::mat4 inverseViewProjection; + glm::vec4 cameraPosition; + }; + + public: + SkyPass(const GpuContext&); + ~SkyPass(); + + SkyPass(const SkyPass&) = delete; + SkyPass& operator=(const SkyPass&) = delete; + + SkyPass(SkyPass&&) = delete; + SkyPass& operator=(SkyPass&&) = delete; + + void render( + const GpuContext& gpuContext, + const glm::mat4& viewProjectionMatrix, + const glm::vec3& cameraPosition, + const Sky& sky, + WGPUCommandEncoder cmdEncoder, + WGPUTextureView textureView); + }; + WGPUTexture mDepthTexture; WGPUTextureView mDepthTextureView; WGPUTexture mAlbedoTexture; @@ -134,5 +176,6 @@ class HybridRenderer GpuBindGroup mGbufferBindGroup; GbufferPass mGbufferPass; DebugPass mDebugPass; + SkyPass mSkyPass; }; } // namespace nlrs diff --git a/src/pt/hybrid_renderer_sky_pass.wgsl b/src/pt/hybrid_renderer_sky_pass.wgsl new file mode 100644 index 0000000..b03c252 --- /dev/null +++ b/src/pt/hybrid_renderer_sky_pass.wgsl @@ -0,0 +1,112 @@ +struct VertexInput { + @location(0) position: vec2f, +} + +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, +} + +@vertex +fn vsMain(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = vec4f(in.position, 0.0, 1.0); + out.texCoord = 0.5 * in.position + vec2f(0.5); + + return out; +} + +struct SkyState { + params: array<f32, 27>, + skyRadiances: array<f32, 3>, + solarRadiances: array<f32, 3>, + sunDirection: vec3<f32>, +}; + +struct Uniforms { + inverseViewProjectionMat: mat4x4f, + cameraEye: vec4f +} + +@group(0) @binding(0) var<storage, read> skyState: SkyState; +@group(1) @binding(0) var<uniform> uniforms: Uniforms; + +const CHANNEL_R = 0u; +const CHANNEL_G = 1u; +const CHANNEL_B = 2u; + +@fragment +fn fsMain(in: VertexOutput) -> @location(0) vec4f { + let uv = in.texCoord; + let world = worldFromUv(uv); + let v = normalize((world - uniforms.cameraEye).xyz); + let s = skyState.sunDirection; + + let theta = acos(v.y); + let gamma = acos(clamp(dot(v, s), -1f, 1f)); + let color = vec3f( + skyRadiance(theta, gamma, CHANNEL_R), + skyRadiance(theta, gamma, CHANNEL_G), + skyRadiance(theta, gamma, CHANNEL_B) + ); + + let exposure = 1f / pow(2f, 4); + return vec4(acesFilmic(exposure * color), 1.0); +} + +fn worldFromUv(uv: vec2f) -> vec4f { + let ndc = vec4(2.0 * uv - vec2(1.0), 0.0, 1.0); + let worldInvW = uniforms.inverseViewProjectionMat * ndc; + let world = worldInvW / worldInvW.w; + return world; +} + +const PI = 3.1415927f; +const DEGREES_TO_RADIANS = PI / 180f; +const TERRESTRIAL_SOLAR_RADIUS = 0.255f * DEGREES_TO_RADIANS; + +@must_use +fn skyRadiance(theta: f32, gamma: f32, channel: u32) -> f32 { + // Sky dome radiance + let r = skyState.skyRadiances[channel]; + let idx = 9u * channel; + let p0 = skyState.params[idx + 0u]; + let p1 = skyState.params[idx + 1u]; + let p2 = skyState.params[idx + 2u]; + let p3 = skyState.params[idx + 3u]; + let p4 = skyState.params[idx + 4u]; + let p5 = skyState.params[idx + 5u]; + let p6 = skyState.params[idx + 6u]; + let p7 = skyState.params[idx + 7u]; + let p8 = skyState.params[idx + 8u]; + + let cosGamma = cos(gamma); + let cosGamma2 = cosGamma * cosGamma; + let cosTheta = abs(cos(theta)); + + let expM = exp(p4 * gamma); + let rayM = cosGamma2; + let mieMLhs = 1.0 + cosGamma2; + let mieMRhs = pow(1.0 + p8 * p8 - 2.0 * p8 * cosGamma, 1.5f); + let mieM = mieMLhs / mieMRhs; + let zenith = sqrt(cosTheta); + let radianceLhs = 1.0 + p0 * exp(p1 / (cosTheta + 0.01)); + let radianceRhs = p2 + p3 * expM + p5 * rayM + p6 * mieM + p7 * zenith; + let radianceDist = radianceLhs * radianceRhs; + + // Solar radiance + let solarDiskRadius = gamma / TERRESTRIAL_SOLAR_RADIUS; + let solarRadiance = select(0f, skyState.solarRadiances[channel], solarDiskRadius <= 1f); + + return r * radianceDist + solarRadiance; +} + +@must_use +fn acesFilmic(x: vec3f) -> vec3f { + let a = 2.51f; + let b = 0.03f; + let c = 2.43f; + let d = 0.59f; + let e = 0.14f; + return saturate((x * (a * x + b)) / (x * (c * x + d) + e)); +} diff --git a/src/pt/main.cpp b/src/pt/main.cpp index 25ae810..4623f17 100644 --- a/src/pt/main.cpp +++ b/src/pt/main.cpp @@ -423,10 +423,23 @@ try renderer.render(gpuContext, textureBlitter.textureView()); break; case RendererType_Hybrid: + { const glm::mat4 viewProjectionMat = appState.cameraController.viewProjectionMatrix(); - hybridRenderer.render(gpuContext, textureBlitter.textureView(), viewProjectionMat); + const nlrs::Sky sky{ + appState.ui.skyTurbidity, + appState.ui.skyAlbedo, + appState.ui.sunZenithDegrees, + appState.ui.sunAzimuthDegrees, + }; + hybridRenderer.render( + gpuContext, + viewProjectionMat, + appState.cameraController.position(), + sky, + textureBlitter.textureView()); break; } + } textureBlitter.render(gpuContext, gui, swapChain); }; diff --git a/src/pt/reference_path_tracer.cpp b/src/pt/reference_path_tracer.cpp index 29123ce..271c0c5 100644 --- a/src/pt/reference_path_tracer.cpp +++ b/src/pt/reference_path_tracer.cpp @@ -7,7 +7,6 @@ #include <common/bvh.hpp> #include <common/gltf_model.hpp> -#include <hw-skymodel/hw_skymodel.h> #include <fmt/core.h> #include <glm/glm.hpp> @@ -27,9 +26,6 @@ namespace nlrs { -inline constexpr float PI = std::numbers::pi_v<float>; -inline constexpr float DEGREES_TO_RADIANS = PI / 180.0f; - namespace { struct FrameDataLayout @@ -96,47 +92,6 @@ struct SamplingStateLayout } }; -struct SkyStateLayout -{ - float params[27]; // offset: 0 - float skyRadiances[3]; // offset: 27 - float solarRadiances[3]; // offset: 30 - float padding1[3]; // offset: 33 - glm::vec3 sunDirection; // offset: 36 - float padding2; // offset: 39 - - SkyStateLayout(const Sky& sky) - : params{0}, - skyRadiances{0}, - solarRadiances{0}, - padding1{0.f, 0.f, 0.f}, - sunDirection(0.f), - padding2(0.0f) - { - const float sunZenith = sky.sunZenithDegrees * DEGREES_TO_RADIANS; - const float sunAzimuth = sky.sunAzimuthDegrees * DEGREES_TO_RADIANS; - - sunDirection = glm::normalize(glm::vec3( - std::sin(sunZenith) * std::cos(sunAzimuth), - std::cos(sunZenith), - -std::sin(sunZenith) * std::sin(sunAzimuth))); - - const sky_params skyParams{ - .elevation = 0.5f * PI - sunZenith, - .turbidity = sky.turbidity, - .albedo = {sky.albedo[0], sky.albedo[1], sky.albedo[2]}}; - - sky_state skyState; - [[maybe_unused]] const auto r = sky_state_new(&skyParams, &skyState); - // TODO: exceptional error handling - assert(r == sky_state_result_success); - - std::memcpy(params, skyState.params, sizeof(skyState.params)); - std::memcpy(skyRadiances, skyState.sky_radiances, sizeof(skyState.sky_radiances)); - std::memcpy(solarRadiances, skyState.solar_radiances, sizeof(skyState.solar_radiances)); - } -}; - struct RenderParamsLayout { FrameDataLayout frameData; @@ -187,7 +142,7 @@ ReferencePathTracer::ReferencePathTracer( gpuContext.device, "sky state buffer", GpuBufferUsage::ReadOnlyStorage | GpuBufferUsage::CopyDst, - sizeof(SkyStateLayout)), + sizeof(AlignedSkyState)), mRenderParamsBindGroup(), mBvhNodeBuffer( gpuContext.device, @@ -624,9 +579,9 @@ void ReferencePathTracer::render(const GpuContext& gpuContext, WGPUTextureView t 0, &mCurrentPostProcessingParams, sizeof(PostProcessingParameters)); - const SkyStateLayout skyStateLayout{mCurrentRenderParams.sky}; + const AlignedSkyState skyState{mCurrentRenderParams.sky}; wgpuQueueWriteBuffer( - gpuContext.queue, mSkyStateBuffer.ptr(), 0, &skyStateLayout, sizeof(SkyStateLayout)); + gpuContext.queue, mSkyStateBuffer.ptr(), 0, &skyState, sizeof(AlignedSkyState)); } const WGPUCommandEncoder encoder = [&gpuContext]() { diff --git a/src/pt/reference_path_tracer.hpp b/src/pt/reference_path_tracer.hpp index a666e34..2bc1c75 100644 --- a/src/pt/reference_path_tracer.hpp +++ b/src/pt/reference_path_tracer.hpp @@ -1,5 +1,6 @@ #pragma once +#include "aligned_sky_state.hpp" #include "gpu_bind_group.hpp" #include "gpu_buffer.hpp" @@ -29,16 +30,6 @@ struct SamplingParams bool operator==(const SamplingParams&) const noexcept = default; }; -struct Sky -{ - float turbidity = 1.0f; - std::array<float, 3> albedo = {1.0f, 1.0f, 1.0f}; - float sunZenithDegrees = 30.0f; - float sunAzimuthDegrees = 0.0f; - - bool operator==(const Sky&) const noexcept = default; -}; - struct RenderParameters { Extent2u framebufferSize; diff --git a/src/pt/shader_source.hpp b/src/pt/shader_source.hpp index f077574..1ef269c 100644 --- a/src/pt/shader_source.hpp +++ b/src/pt/shader_source.hpp @@ -775,4 +775,118 @@ fn fsMain(in: VertexOutput) -> @location(0) vec4f { } )"; +const char* const HYBRID_RENDERER_SKY_PASS_SOURCE = R"(struct VertexInput { + @location(0) position: vec2f, +} + +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, +} + +@vertex +fn vsMain(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = vec4f(in.position, 0.0, 1.0); + out.texCoord = 0.5 * in.position + vec2f(0.5); + + return out; +} + +struct SkyState { + params: array<f32, 27>, + skyRadiances: array<f32, 3>, + solarRadiances: array<f32, 3>, + sunDirection: vec3<f32>, +}; + +struct Uniforms { + inverseViewProjectionMat: mat4x4f, + cameraEye: vec4f +} + +@group(0) @binding(0) var<storage, read> skyState: SkyState; +@group(1) @binding(0) var<uniform> uniforms: Uniforms; + +const CHANNEL_R = 0u; +const CHANNEL_G = 1u; +const CHANNEL_B = 2u; + +@fragment +fn fsMain(in: VertexOutput) -> @location(0) vec4f { + let uv = in.texCoord; + let world = worldFromUv(uv); + let v = normalize((world - uniforms.cameraEye).xyz); + let s = skyState.sunDirection; + + let theta = acos(v.y); + let gamma = acos(clamp(dot(v, s), -1f, 1f)); + let color = vec3f( + skyRadiance(theta, gamma, CHANNEL_R), + skyRadiance(theta, gamma, CHANNEL_G), + skyRadiance(theta, gamma, CHANNEL_B) + ); + + let exposure = 1f / pow(2f, 4); + return vec4(acesFilmic(exposure * color), 1.0); +} + +fn worldFromUv(uv: vec2f) -> vec4f { + let ndc = vec4(2.0 * uv - vec2(1.0), 0.0, 1.0); + let worldInvW = uniforms.inverseViewProjectionMat * ndc; + let world = worldInvW / worldInvW.w; + return world; +} + +const PI = 3.1415927f; +const DEGREES_TO_RADIANS = PI / 180f; +const TERRESTRIAL_SOLAR_RADIUS = 0.255f * DEGREES_TO_RADIANS; + +@must_use +fn skyRadiance(theta: f32, gamma: f32, channel: u32) -> f32 { + // Sky dome radiance + let r = skyState.skyRadiances[channel]; + let idx = 9u * channel; + let p0 = skyState.params[idx + 0u]; + let p1 = skyState.params[idx + 1u]; + let p2 = skyState.params[idx + 2u]; + let p3 = skyState.params[idx + 3u]; + let p4 = skyState.params[idx + 4u]; + let p5 = skyState.params[idx + 5u]; + let p6 = skyState.params[idx + 6u]; + let p7 = skyState.params[idx + 7u]; + let p8 = skyState.params[idx + 8u]; + + let cosGamma = cos(gamma); + let cosGamma2 = cosGamma * cosGamma; + let cosTheta = abs(cos(theta)); + + let expM = exp(p4 * gamma); + let rayM = cosGamma2; + let mieMLhs = 1.0 + cosGamma2; + let mieMRhs = pow(1.0 + p8 * p8 - 2.0 * p8 * cosGamma, 1.5f); + let mieM = mieMLhs / mieMRhs; + let zenith = sqrt(cosTheta); + let radianceLhs = 1.0 + p0 * exp(p1 / (cosTheta + 0.01)); + let radianceRhs = p2 + p3 * expM + p5 * rayM + p6 * mieM + p7 * zenith; + let radianceDist = radianceLhs * radianceRhs; + + // Solar radiance + let solarDiskRadius = gamma / TERRESTRIAL_SOLAR_RADIUS; + let solarRadiance = select(0f, skyState.solarRadiances[channel], solarDiskRadius <= 1f); + + return r * radianceDist + solarRadiance; +} + +@must_use +fn acesFilmic(x: vec3f) -> vec3f { + let a = 2.51f; + let b = 0.03f; + let c = 2.43f; + let d = 0.59f; + let e = 0.14f; + return saturate((x * (a * x + b)) / (x * (c * x + d) + e)); +} +)"; + } // namespace nlrs