/* =========================================================================== Copyright (C) 2024 Gian 'myT' Schellenbaum This file is part of Challenge Quake 3 (CNQ3). Challenge Quake 3 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Challenge Quake 3 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Challenge Quake 3. If not, see . =========================================================================== */ // Cinematic Rendering Pipeline - accumulation depth of field #include "crp_local.h" #include "compshaders/crp/fullscreen.h" #include "compshaders/crp/accumdof_accum.h" #include "compshaders/crp/accumdof_norm.h" #include "compshaders/crp/accumdof_debug.h" #pragma pack(push, 4) struct DOFAccumRC { uint32_t textureIndex; }; struct DOFNormRC { uint32_t textureIndex; }; struct DOFDebugRC { float mvp[16]; // displaced view, to project to CS float invMvp[16]; // main view, to unproject to WS uint32_t colorTextureIndex; uint32_t depthTextureIndex; uint32_t debugMode; // 1: colorized coc, 2: constant intensity far field uint32_t tcScale; float focusDist; float maxNearCocCS; float maxFarCocCS; }; #pragma pack(pop) // the input is in [0,1]^2, the output polygon is centered at the origin static void MapUnitSquareToPolygon(const vec2_t square01, float apertureBladeCount, float apertureAngleRad, vec2_t polygon) { // needed to avoid inf/nan propagation through theta for samples // that are exactly in the middle of the quad on either axis // (i.e. square.x|y == 0.5f gets remapped to 0.0f) const float epsilon = 0.000001f; // morph into a square in [-1,1]^2 vec2_t square; square[0] = square01[0] * 2.0f - 1.0f; square[1] = square01[1] * 2.0f - 1.0f; // morph the square into a disk // "A Low Distortion Map Between Disk and Square" by Peter Shirley and Kenneth Chiu float radius, angle; vec2_t square2; square2[0] = square[0] * square[0]; square2[1] = square[1] * square[1]; if(square2[0] > square2[1]) { // left and right quadrants radius = square[0]; angle = (square[1] * M_PI_D4) / (square[0] + epsilon); } else { // top and bottom quadrants radius = square[1]; angle = M_PI_D2 - (square[0] * M_PI_D4) / (square[1] + epsilon); } if(radius < 0.0f) { radius = -radius; angle += M_PI; } // morph the disk into a polygon // "Graphics Gems from CryENGINE 3" by Tiago Sousa const float edgeCount = apertureBladeCount; if(edgeCount >= 3.0f) { const float num = cosf(M_PI / edgeCount); const float den0 = M_PI_M2 / edgeCount; const float den1 = (angle * edgeCount + M_PI) / M_PI_M2; const float den = angle - (den0 * floorf(den1)); radius *= num / cosf(den); angle += apertureAngleRad; } polygon[0] = cosf(angle) * radius; polygon[1] = sinf(angle) * radius; } static int GetResolutionScale() { switch(crp_accumDof_preview->integer) { case 0: return 1; case 1: return 2; case 2: return 4; default: Q_assert(0); return 1; } } void AccumDepthOfField::Init() { { GraphicsPipelineDesc desc("DOF Accumulate"); MakeFullScreenPipeline(desc, ShaderByteCode(g_accum_ps)); desc.AddRenderTarget(GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE, crp.renderTargetFormat); accumPipeline = CreateGraphicsPipeline(desc); } { GraphicsPipelineDesc desc("DOF Normalize"); MakeFullScreenPipeline(desc, ShaderByteCode(g_norm_ps)); desc.AddRenderTarget(0, crp.renderTargetFormat); normPipeline = CreateGraphicsPipeline(desc); } { GraphicsPipelineDesc desc("DOF Debug"); MakeFullScreenPipeline(desc, ShaderByteCode(g_debug_ps)); desc.AddRenderTarget(0, crp.renderTargetFormat); debugPipeline = CreateGraphicsPipeline(desc); } { TextureDesc desc("DOF accumulation", glConfig.vidWidth, glConfig.vidHeight); desc.initialState = ResourceStates::RenderTargetBit; desc.allowedState = ResourceStates::RenderTargetBit | ResourceStates::PixelShaderAccessBit; Vector4Clear(desc.clearColor); desc.usePreferredClearValue = true; desc.committedResource = true; desc.format = crp.renderTargetFormat; desc.shortLifeTime = true; accumTexture = CreateTexture(desc); } } void AccumDepthOfField::Begin(const drawSceneViewCommand_t& cmd) { CmdSetViewportAndScissor(0, 0, glConfig.vidWidth, glConfig.vidHeight); CmdBeginBarrier(); CmdTextureBarrier(accumTexture, ResourceStates::RenderTargetBit); CmdEndBarrier(); CmdClearColorTarget(accumTexture, vec4_zero); // project a point a few units in front of the main view onto a viewpoint with maximum displacement // the distance to the clip space center (i.e. the 2D origin) is the maximum expected CoC vec3_t testPoint; VectorMA(cmd.viewParms.world.viewOrigin, 16.0f, cmd.viewParms.orient.axis[0], testPoint); drawSceneViewCommand_t newCmd; FixCommand(newCmd, cmd, 0, 0); vec4_t eye, clip; R_TransformModelToClip(testPoint, newCmd.viewParms.world.modelMatrix, newCmd.viewParms.projectionMatrix, eye, clip); Vector4Scale(clip, 1.0f / clip[3], clip); maxNearCocCS = sqrtf(clip[0] * clip[0] + clip[1] * clip[1]); // same thing for a point far away VectorMA(cmd.viewParms.world.viewOrigin, 69420.0f, cmd.viewParms.orient.axis[0], testPoint); FixCommand(newCmd, cmd, 0, 0); R_TransformModelToClip(testPoint, newCmd.viewParms.world.modelMatrix, newCmd.viewParms.projectionMatrix, eye, clip); Vector4Scale(clip, 1.0f / clip[3], clip); maxFarCocCS = sqrtf(clip[0] * clip[0] + clip[1] * clip[1]); } uint32_t AccumDepthOfField::GetSampleCount() { switch(crp_accumDof_preview->integer) { case 1: return 3; case 2: return 5; default: break; } return 2 * crp_accumDof_samples->integer + 1; } void AccumDepthOfField::FixCommand(drawSceneViewCommand_t& newCmd, const drawSceneViewCommand_t& cmd, uint32_t x, uint32_t y) { const float radius = crp_accumDof_radius->value; const float tcScale = 1.0f / (float)(GetSampleCount() - 1); vec2_t square01; square01[0] = x * tcScale; square01[1] = y * tcScale; vec2_t polygon; MapUnitSquareToPolygon(square01, crp_dof_blades->value, DEG2RAD(crp_dof_angle->value), polygon); // 0=forward, 1=left, 2=up vec3_t axis[3]; VectorCopy(cmd.viewParms.orient.axis[0], axis[0]); VectorCopy(cmd.viewParms.orient.axis[1], axis[1]); VectorCopy(cmd.viewParms.orient.axis[2], axis[2]); vec3_t origin; VectorMA(cmd.viewParms.world.viewOrigin, radius * polygon[0], axis[1], origin); VectorMA(origin, radius * polygon[1], axis[2], origin); vec3_t focusPoint; VectorMA(cmd.viewParms.world.viewOrigin, crp_accumDof_focusDist->value, axis[0], focusPoint); VectorSubtract(focusPoint, origin, axis[0]); // forward VectorNormalize(axis[0]); CrossProduct(axis[2], axis[0], axis[1]); // left VectorNormalize(axis[1]); CrossProduct(axis[0], axis[1], axis[2]); // up VectorNormalize(axis[2]); newCmd = cmd; VectorCopy(origin, newCmd.viewParms.orient.origin); VectorCopy(origin, newCmd.viewParms.world.viewOrigin); R_CreateWorldModelMatrix(origin, axis, newCmd.viewParms.world.modelMatrix); newCmd.viewParms.viewportWidth /= GetResolutionScale(); newCmd.viewParms.viewportHeight /= GetResolutionScale(); if(x == 0 && y == 0) { memcpy(modelViewMatrix, newCmd.viewParms.world.modelMatrix, sizeof(modelViewMatrix)); memcpy(projMatrix, newCmd.viewParms.projectionMatrix, sizeof(projMatrix)); } } void AccumDepthOfField::Accumulate() { srp.renderMode = RenderMode::None; SCOPED_RENDER_PASS("DOF Accum", 0.5f, 1.0f, 0.5f); CmdBeginBarrier(); CmdTextureBarrier(crp.renderTarget, ResourceStates::PixelShaderAccessBit); CmdTextureBarrier(accumTexture, ResourceStates::RenderTargetBit); CmdEndBarrier(); DOFAccumRC rc = {}; rc.textureIndex = GetTextureIndexSRV(crp.renderTarget); CmdBindRenderTargets(1, &accumTexture, NULL); CmdBindPipeline(accumPipeline); CmdSetGraphicsRootConstants(0, sizeof(rc), &rc); CmdDraw(3, 0); } void AccumDepthOfField::Normalize() { srp.renderMode = RenderMode::None; { SCOPED_RENDER_PASS("DOF Norm", 0.5f, 1.0f, 0.5f); CmdBeginBarrier(); CmdTextureBarrier(accumTexture, ResourceStates::PixelShaderAccessBit); CmdTextureBarrier(crp.renderTarget, ResourceStates::RenderTargetBit); CmdEndBarrier(); DOFNormRC rc = {}; rc.textureIndex = GetTextureIndexSRV(accumTexture); CmdBindRenderTargets(1, &crp.renderTarget, NULL); CmdBindPipeline(normPipeline); CmdSetGraphicsRootConstants(0, sizeof(rc), &rc); CmdDraw(3, 0); } if(crp_accumDof_preview->integer) { crp.SwapRenderTargets(); const float scale = 1.0f / (float)GetResolutionScale(); const vec2_t tcScale = { scale, scale }; crp.Blit(crp.GetWriteRenderTarget(), crp.GetReadRenderTarget(), "DOF Upscale", true, tcScale, vec2_zero); } } void AccumDepthOfField::DrawDebug() { if(crp_dof_overlay->integer == 0) { return; } srp.renderMode = RenderMode::None; SCOPED_RENDER_PASS("DOF Debug", 0.5f, 1.0f, 0.5f); crp.SwapRenderTargets(); CmdBeginBarrier(); CmdTextureBarrier(crp.GetReadRenderTarget(), ResourceStates::PixelShaderAccessBit); CmdTextureBarrier(crp.depthTexture, ResourceStates::PixelShaderAccessBit); CmdTextureBarrier(crp.GetWriteRenderTarget(), ResourceStates::RenderTargetBit); CmdEndBarrier(); float mvp[16]; DOFDebugRC rc = {}; rc.colorTextureIndex = GetTextureIndexSRV(crp.GetReadRenderTarget()); rc.depthTextureIndex = GetTextureIndexSRV(crp.depthTexture); rc.debugMode = crp_dof_overlay->integer; rc.focusDist = crp_accumDof_focusDist->value; rc.maxNearCocCS = maxNearCocCS; rc.maxFarCocCS = maxFarCocCS; rc.tcScale = GetResolutionScale(); R_MultMatrix(modelViewMatrix, projMatrix, rc.mvp); R_MultMatrix(backEnd.viewParms.world.modelMatrix, backEnd.viewParms.projectionMatrix, mvp); R_InvMatrix(mvp, rc.invMvp); CmdBindRenderTargets(1, &crp.renderTarget, NULL); CmdBindPipeline(debugPipeline); CmdSetGraphicsRootConstants(0, sizeof(rc), &rc); CmdDraw(3, 0); }