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/hybrid_renderer.cpp b/src/pt/hybrid_renderer.cpp index 926e15f..037e3df 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 depthFormats{ @@ -170,7 +171,8 @@ HybridRenderer::~HybridRenderer() void HybridRenderer::render( const GpuContext& gpuContext, const WGPUTextureView textureView, - const glm::mat4& viewProjectionMat) + const glm::mat4& viewProjectionMat, + const Sky& sky) { wgpuDeviceTick(gpuContext.device); @@ -190,7 +192,7 @@ void HybridRenderer::render( mAlbedoTextureView, mNormalTextureView); - mDebugPass.render(mGbufferBindGroup, encoder, textureView); + mSkyPass.render(gpuContext, sky, encoder, textureView); const WGPUCommandBuffer cmdBuffer = [encoder]() { const WGPUCommandBufferDescriptor cmdBufferDesc{ @@ -1020,4 +1022,213 @@ void HybridRenderer::DebugPass::resize(const GpuContext& gpuContext, const Exten wgpuQueueWriteBuffer( gpuContext.queue, mUniformBuffer.ptr(), 0, &uniformData.x, sizeof(Extent2)); } + +HybridRenderer::SkyPass::SkyPass(const GpuContext& gpuContext) + : mCurrentSky{}, + mVertexBuffer{ + gpuContext.device, + "Sky vertex buffer", + GpuBufferUsage::Vertex | GpuBufferUsage::CopyDst, + std::span(quadVertexData)}, + mSkyStateBuffer{ + gpuContext.device, + "Sky state buffer", + GpuBufferUsage::ReadOnlyStorage | GpuBufferUsage::CopyDst, + sizeof(AlignedSkyState)}, + mSkyStateBindGroup{}, + mPipeline(nullptr) +{ + { + const AlignedSkyState skyState{mCurrentSky}; + wgpuQueueWriteBuffer( + gpuContext.queue, mSkyStateBuffer.ptr(), 0, &skyState, sizeof(AlignedSkyState)); + } + + const GpuBindGroupLayout skyStateBindGroupLayout{ + gpuContext.device, + "Sky state bind group layout", + mSkyStateBuffer.bindGroupLayoutEntry(0, WGPUShaderStage_Fragment, sizeof(AlignedSkyState))}; + + mSkyStateBindGroup = GpuBindGroup{ + gpuContext.device, + "Sky state bind group", + skyStateBindGroupLayout.ptr(), + mSkyStateBuffer.bindGroupEntry(0)}; + + { + // Pipeline layout + + const WGPUBindGroupLayout bindGroupLayouts[] = {skyStateBindGroupLayout.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 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 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); + 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..77e8168 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,11 @@ class HybridRenderer HybridRenderer(HybridRenderer&&) = delete; HybridRenderer& operator=(HybridRenderer&&) = delete; - void render(const GpuContext&, WGPUTextureView, const glm::mat4& viewProjectionMatrix); + void render( + const GpuContext&, + WGPUTextureView, + const glm::mat4& viewProjectionMatrix, + const Sky&); void resize(const GpuContext&, const Extent2u&); private: @@ -124,6 +129,32 @@ class HybridRenderer void resize(const GpuContext&, const Extent2u&); }; + struct SkyPass + { + private: + Sky mCurrentSky; + GpuBuffer mVertexBuffer; + GpuBuffer mSkyStateBuffer; + GpuBindGroup mSkyStateBindGroup; + WGPURenderPipeline mPipeline; + + 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 Sky& sky, + WGPUCommandEncoder cmdEncoder, + WGPUTextureView textureView); + }; + WGPUTexture mDepthTexture; WGPUTextureView mDepthTextureView; WGPUTexture mAlbedoTexture; @@ -134,5 +165,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..669e18a --- /dev/null +++ b/src/pt/hybrid_renderer_sky_pass.wgsl @@ -0,0 +1,111 @@ +struct VertexInput { + @location(0) position: vec2f, +} + +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, +} + +@group(0) @binding(0) var skyState: SkyState; + +@vertex +fn vsMain(in: VertexInput) -> VertexOutput { + let uv = 0.5 * in.position + vec2f(0.5); + var out: VertexOutput; + out.position = vec4f(in.position, 0.0, 1.0); + out.texCoord = vec2f(uv.x, 1.0 - uv.y); // flip y axis + + return out; +} + +struct SkyState { + params: array, + skyRadiances: array, + solarRadiances: array, + sunDirection: vec3, +}; + +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 x = uv.x * 2.0 - 1.0; + let y = uv.y * 2.0 - 1.0; + + var color = vec3f(0f); + + let radiusSqr = x * x + y * y; + if radiusSqr < 1f { + // Ray direction + let z = sqrt(1f - radiusSqr); + let v = vec3f(x, z, -y); + let s = skyState.sunDirection; + + // Compute the sky radiance + let theta = acos(v.y); + let gamma = acos(clamp(dot(v, s), -1f, 1f)); + + color = vec3f( + skyRadiance(theta, gamma, 0), + skyRadiance(theta, gamma, 1), + skyRadiance(theta, gamma, 2) + ); + } + + let exposure = 1f / pow(2f, 4); + return vec4(acesFilmic(exposure * color), 1.0); +} + +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..ccd17a6 100644 --- a/src/pt/main.cpp +++ b/src/pt/main.cpp @@ -423,10 +423,18 @@ 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, textureBlitter.textureView(), viewProjectionMat, sky); break; } + } textureBlitter.render(gpuContext, gui, swapChain); }; diff --git a/src/pt/shader_source.hpp b/src/pt/shader_source.hpp index 69208fa..2fa0c76 100644 --- a/src/pt/shader_source.hpp +++ b/src/pt/shader_source.hpp @@ -777,4 +777,117 @@ 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, +} + +@group(0) @binding(0) var skyState: SkyState; + +@vertex +fn vsMain(in: VertexInput) -> VertexOutput { + let uv = 0.5 * in.position + vec2f(0.5); + var out: VertexOutput; + out.position = vec4f(in.position, 0.0, 1.0); + out.texCoord = vec2f(uv.x, 1.0 - uv.y); // flip y axis + + return out; +} + +struct SkyState { + params: array, + skyRadiances: array, + solarRadiances: array, + sunDirection: vec3, +}; + +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 x = uv.x * 2.0 - 1.0; + let y = uv.y * 2.0 - 1.0; + + var color = vec3f(0f); + + let radiusSqr = x * x + y * y; + if radiusSqr < 1f { + // Ray direction + let z = sqrt(1f - radiusSqr); + let v = vec3f(x, z, -y); + let s = skyState.sunDirection; + + // Compute the sky radiance + let theta = acos(v.y); + let gamma = acos(clamp(dot(v, s), -1f, 1f)); + + color = vec3f( + skyRadiance(theta, gamma, 0), + skyRadiance(theta, gamma, 1), + skyRadiance(theta, gamma, 2) + ); + } + + let exposure = 1f / pow(2f, 4); + return vec4(acesFilmic(exposure * color), 1.0); +} + +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