diff --git a/EXE/dlss5-feed.addon32 b/EXE/dlss5-feed.addon32 new file mode 100644 index 00000000..94d4b019 Binary files /dev/null and b/EXE/dlss5-feed.addon32 differ diff --git a/EXE/dlss5-feed.cfg b/EXE/dlss5-feed.cfg new file mode 100644 index 00000000..220ca2e5 --- /dev/null +++ b/EXE/dlss5-feed.cfg @@ -0,0 +1,17 @@ +enabled=1 +mode=2 +hdr=-1 +depth_inverted=-1 +flags=-1 +reset_every=1 +log_frames=3 +host_window=0 +work_resolution=100 +work_upscale=0 +work_sharpness=0.30 +async_home=1 +mv_scale_x=1.000 +mv_scale_y=1.000 +cast_key=0 +cast_scale=100 +cast_mode=0 diff --git a/EXE/host64/dlss5-feed-host64.exe b/EXE/host64/dlss5-feed-host64.exe new file mode 100644 index 00000000..335bdee3 Binary files /dev/null and b/EXE/host64/dlss5-feed-host64.exe differ diff --git a/EXE/host64/dxgi.dll b/EXE/host64/dxgi.dll new file mode 100644 index 00000000..bc5a0843 Binary files /dev/null and b/EXE/host64/dxgi.dll differ diff --git a/EXE/host64/nvngx_dlss.dll b/EXE/host64/nvngx_dlss.dll new file mode 100644 index 00000000..a22c98a7 Binary files /dev/null and b/EXE/host64/nvngx_dlss.dll differ diff --git a/EXE/host64/nvngx_dlssnr.dll b/EXE/host64/nvngx_dlssnr.dll new file mode 100644 index 00000000..6d58c2ea Binary files /dev/null and b/EXE/host64/nvngx_dlssnr.dll differ diff --git a/EXE/host64/renodx-dlss5.addon64 b/EXE/host64/renodx-dlss5.addon64 new file mode 100644 index 00000000..7f7a5b58 Binary files /dev/null and b/EXE/host64/renodx-dlss5.addon64 differ diff --git a/EXE/reshade-shaders/Shaders/DLSS5_Feed.fx b/EXE/reshade-shaders/Shaders/DLSS5_Feed.fx new file mode 100644 index 00000000..fa756447 --- /dev/null +++ b/EXE/reshade-shaders/Shaders/DLSS5_Feed.fx @@ -0,0 +1,817 @@ +/* + DLSS5_Feed.fx - companion effect for the "DLSS 5 Feed" ReShade add-on (dlss5-feed.addon64/32). + + It turns what ReShade already has into the guide textures DLSS needs, in the exact layout + the add-on expects: + + DLSS5_MV RG16F motion vectors in PIXELS, pointing from the current pixel to where it was + in the previous frame (DLSS convention). Vectors that fail validation + (below) are zeroed. + DLSS5_Depth R32F the game's raw hardware depth (not linearised), sampled at backbuffer size, + with ReShade's RESHADE_DEPTH_INPUT_* orientation fixes applied. + DLSS5_Mask R8 "bias current colour" mask for DLSS: 1 where the motion vector could not + be trusted, so DLSS leans on the current frame there instead of warping + history in. Optional -- an add-on that does not know it ignores it. + + MOTION VECTOR PROVIDER -- set the DLSS5_MV_PROVIDER preprocessor definition (ReShade overlay: + this effect's "Preprocessor definitions", or the global list) and enable that provider's + technique ABOVE this one in the effect list: + + 0 texMotionVectors the community-standard shared texture: qUINT_motionvectors, + dh_uber_motion, ReshadeMotionEstimation (DRME -- NOTE: DRME does not + compile on ReShade 6.8, "cannot sample from texture that is also used + as render target"; it then silently writes nothing) [default] + 1 Launchpad iMMERSE Launchpad (MartysMods_LAUNCHPAD.fx): Deferred::MotionVectorsTex. + Launchpad only runs its optical flow when asked to, so this mode also + files that per-frame request (Launchpad's IPC buffer, see below). + 2 VORT vort_Motion.fx (MIT): MotVectTexVort -- the recommended provider + 3 LumeniteFX Kernel lumenite_Kernel.fx ("LUMENITE: Kernel"): Kernel::tFlow -- pyramidal + optical flow with per-level median + a-trous filtering and previous- + frame seeding. 1/8 resolution, upsampled here. Needs no depth buffer. + 4 LumeniteFX QuantMotion + lumenite_QuantMotion.fx: QuantMotion::tFlow -- the light cut of 3. + + This is the same mechanism dh_uber_rt (USE_MARTY_LAUNCHPAD_MOTION / USE_VORT_MOTION) and + vort (V_MV_MODE) use: the selected provider's OUTPUT texture is declared here exactly as the + provider declares it, so ReShade binds the same resource, and only that one is allocated. + Every provider above hands out delta UV with prev_uv = uv + mv. Nothing of any provider is + included or bundled: this file contains no third-party code and includes no third-party + files beyond ReShade's own headers. + + VALIDATION -- why it exists. A game's motion vectors are geometric: a static wall under a + flickering light has vectors of exactly zero. Every provider above is OPTICAL FLOW: it + matches pixels, so a lighting change (flicker, flames, particles) is answered with a vector + that points at whatever happened to match -- confidently wrong, and DLSS then warps its + history in from there. That is the "warping around flames" and the "bad dither when the + light flickers". The fix is the one every production TAA uses: reproject and CHECK. + For each pixel, three tests against the previous frame at uv + mv: + - luma: the previous luma must fall inside the current 3x3 neighbourhood's range + (flicker moves the whole range, so a stale match falls outside); + - depth: the previous linear depth must match the current one (disocclusions); + - consistency: the previous frame's vector at that spot must resemble this one + (real motion is smooth frame to frame; flow on fire is erratic). + A vector failing any test is zeroed (the surface is treated as static -- the right answer + for a lit wall) and the pixel is flagged in DLSS5_Mask so DLSS trusts the current frame there. + + The add-on runs DLSS + DLSS 5 neural rendering right after the "DLSS5_Feed" technique has + rendered, so anything placed below it in the list is applied on top of the neural output. +*/ + +#include "ReShade.fxh" + +// Expose ReShade's completed frame to the add-on as an SRV. The 64-bit D3D11 path +// uses this only when its work-resolution control is below 100%; no extra pass or +// copy is introduced by this declaration. +texture DLSS5_ColorInput : COLOR; +sampler sDLSS5_ColorInput { Texture = DLSS5_ColorInput; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Point; MagFilter = Point; }; + +#ifndef DLSS5_MV_PROVIDER + #define DLSS5_MV_PROVIDER 0 +#endif + +// --------------------------------------------------------------------------------------------- +// The selected provider's output, declared byte for byte like the provider itself does. +// --------------------------------------------------------------------------------------------- + +#if DLSS5_MV_PROVIDER == 1 + // iMMERSE Launchpad (MartysMods/mmx_deferred.fxh) + namespace Deferred { + texture MotionVectorsTex { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RG16F; }; + // Launchpad's request buffer. Launchpad only computes optical flow when a consumer asked + // for it during the previous frame (it reads this 1x1 RGBA8 at the top of its technique + // and clears it at the bottom; bit 4 = optical flow, written through the render-target + // write mask). Being below Launchpad in the list, our request lands for the next frame. + // Declared like Launchpad declares it; the two shaders that write it below are ours. + namespace IPC { + texture2D PredicationBuffer { Format = RGBA8; }; + } + } + sampler sDLSS5_ProviderMV { Texture = Deferred::MotionVectorsTex; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Point; MagFilter = Point; }; + float4 DLSS5_IpcRequestVS(in uint id : SV_VertexID) : SV_Position { return float4(0.0, 0.0, 0.0, 1.0); } + float4 DLSS5_IpcRequestPS(in float4 vpos : SV_Position) : SV_Target0 { return 1.0; } + #define DLSS5_MV_PROVIDER_NAME "Launchpad (Deferred::MotionVectorsTex)" + #define DLSS5_MV_REQUEST_PASS pass IpcRequestOpticalFlow { PrimitiveTopology = POINTLIST; VertexCount = 1; VertexShader = DLSS5_IpcRequestVS; PixelShader = DLSS5_IpcRequestPS; RenderTarget = Deferred::IPC::PredicationBuffer; RenderTargetWriteMask = 4; } +#elif DLSS5_MV_PROVIDER == 2 + // VORT (Includes/vort_MotionUtils.fxh, V_MV_MODE 1) + texture2D MotVectTexVort { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RG16F; }; + sampler sDLSS5_ProviderMV { Texture = MotVectTexVort; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Point; MagFilter = Point; }; + #define DLSS5_MV_PROVIDER_NAME "VORT (MotVectTexVort)" +#elif DLSS5_MV_PROVIDER == 3 + // LumeniteFX Kernel (lumenite_Kernel.fx), as lumenite_RTAO/TRAA re-declare it. 1/8 resolution. + namespace Kernel { + texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; + texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; + } + sampler sDLSS5_ProviderMV { Texture = Kernel::tFlow; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Linear; MagFilter = Linear; }; + sampler sDLSS5_ProviderMVPoint { Texture = Kernel::tFlow; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Point; MagFilter = Point; }; + sampler sDLSS5_ProviderConfidence{ Texture = Kernel::tConfidence; AddressU = Clamp; AddressV = Clamp; }; + #define DLSS5_MV_PROVIDER_NAME "LumeniteFX Kernel (Kernel::tFlow, 1/8 res)" + #define DLSS5_MV_LOWRES 1 +#elif DLSS5_MV_PROVIDER == 4 + // LumeniteFX QuantMotion (lumenite_QuantMotion.fx), as lumenite_QuantAO re-declares it. 1/8 resolution. + namespace QuantMotion { + texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; + texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; + } + sampler sDLSS5_ProviderMV { Texture = QuantMotion::tFlow; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Linear; MagFilter = Linear; }; + sampler sDLSS5_ProviderMVPoint { Texture = QuantMotion::tFlow; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Point; MagFilter = Point; }; + sampler sDLSS5_ProviderConfidence{ Texture = QuantMotion::tConfidence; AddressU = Clamp; AddressV = Clamp; }; + #define DLSS5_MV_PROVIDER_NAME "LumeniteFX QuantMotion (QuantMotion::tFlow, 1/8 res)" + #define DLSS5_MV_LOWRES 1 +#else + // The community-standard shared texture (ReshadeMotionEstimation, qUINT, dh_uber_motion, ...) + texture texMotionVectors < pooled = false; > { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RG16F; }; + sampler sDLSS5_ProviderMV { Texture = texMotionVectors; AddressU = Clamp; AddressV = Clamp; MipFilter = Point; MinFilter = Point; MagFilter = Point; }; + #define DLSS5_MV_PROVIDER_NAME "texMotionVectors (DRME, qUINT, dh_uber_motion, ...)" +#endif + +#ifndef DLSS5_MV_LOWRES + #define DLSS5_MV_LOWRES 0 +#endif +#ifndef DLSS5_MV_REQUEST_PASS + #define DLSS5_MV_REQUEST_PASS +#endif + +// --------------------------------------------------------------------------------------------- + +uniform int MV_PROVIDER_INFO < + ui_type = "radio"; + ui_label = " "; + ui_text = "Motion vector provider: " DLSS5_MV_PROVIDER_NAME "\n" + "Change it with the DLSS5_MV_PROVIDER preprocessor definition:\n" + " 0 texMotionVectors (DRME, qUINT, dh_uber_motion) 1 Launchpad 2 VORT\n" + " 3 LumeniteFX Kernel 4 LumeniteFX QuantMotion\n" + "Enable that provider's technique ABOVE DLSS 5 Feed."; +>; + +#if DLSS5_MV_LOWRES +uniform int MV_LOWRES_FILTER < + ui_type = "combo"; + ui_items = "Bilinear\0Point (nearest)\0"; + ui_label = "Low-res provider filter"; + ui_tooltip = "How the provider's 1/8-resolution flow is brought up to full resolution.\n" + "Bilinear smooths across flow cells; point keeps each 8x8 cell's vector as-is."; +> = 0; +#endif + +// --------------------------------------------------------------------------------------------- +// Geometry vectors. A game's motion vectors for static geometry come from camera motion and +// depth, not from pixels. We have depth; the camera motion is fitted each frame from the +// provider's flow over a sparse grid (robust two-pass least squares on a 9-term screen-space +// model: affine + quadratic rotation terms + inverse-depth parallax terms), and every pixel +// then gets the vector that model predicts from its depth -- correct under flicker, correct +// while moving. The provider's flow is only used where it disagrees with the model AND wins a +// structure test: a genuinely moving object. Flames and flicker lose that test and keep the +// geometric vector, so nothing warps. +// --------------------------------------------------------------------------------------------- + +uniform bool GEOM_ENABLE < + ui_category = "Geometry vectors (camera model + depth) -- EXPERIMENTAL"; + ui_label = "Use geometry vectors (experimental, off by default)"; + ui_tooltip = "Fit the camera motion from the provider's flow + depth each frame and derive every static\n" + "pixel's vector from it. The provider is then only consulted for moving objects.\n" + "EXPERIMENTAL: the per-frame fit is still noisy, and anything not part of the 3D world\n" + "(the HUD) gets camera vectors it should not have -- expect jitter there.\n" + "Off = the per-pixel validation below is applied to the provider's flow directly."; +> = false; + +uniform float GEOM_PARALLAX < + ui_category = "Geometry vectors (camera model + depth)"; + ui_type = "drag"; ui_min = 0.001; ui_max = 0.5; ui_step = 0.001; + ui_label = "Parallax depth scale"; + ui_tooltip = "The model's inverse-depth term is s / (depth + s) with linear depth in 0..1. Smaller = more\n" + "parallax resolution near the camera. Usually fine as is."; +> = 0.02; + +uniform float GEOM_OUTLIER_PX < + ui_category = "Geometry vectors (camera model + depth)"; + ui_type = "drag"; ui_min = 0.5; ui_max = 32.0; ui_step = 0.5; + ui_label = "Fit: outlier rejection (px)"; + ui_tooltip = "Second fitting pass ignores samples whose flow is further than this from the first pass's\n" + "prediction -- moving objects, flames, the first-person weapon."; +> = 4.0; + +uniform float GEOM_AGREE_PX < + ui_category = "Geometry vectors (camera model + depth)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 16.0; ui_step = 0.1; + ui_label = "Agreement (px)"; + ui_tooltip = "If the provider's flow is within this many pixels (+10% of the vector) of the model, the\n" + "model's vector is used as-is. Beyond it, the structure test decides moving object vs junk."; +> = 1.5; + +uniform float GEOM_DYNAMIC_MARGIN < + ui_category = "Geometry vectors (camera model + depth)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 0.9; ui_step = 0.01; + ui_label = "Moving-object margin"; + ui_tooltip = "For the provider's flow to override the model on a disagreeing pixel, its reprojection must\n" + "explain the pixel's structure at least this much (relative) better than the model's does.\n" + "Higher = more conservative (fewer things count as moving objects)."; +> = 0.25; + +uniform float GEOM_MASK_REJECTED < + ui_category = "Geometry vectors (camera model + depth)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 1.0; ui_step = 0.05; + ui_label = "Mask strength on rejected flow"; + ui_tooltip = "Where the provider disagreed with the model but did not win the structure test (fire, smoke,\n" + "flicker), the geometric vector is used; this is how strongly DLSS is additionally asked to\n" + "favour the current frame there. 0 = pure history (smoothest), 1 = mostly current frame."; +> = 0.35; + +uniform bool MV_VALIDATE < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_label = "Validate motion vectors against the previous frame"; + ui_tooltip = "Optical-flow providers answer a lighting change (flicker, flames) with a vector that\n" + "points at whatever happened to match. Reprojecting and checking catches those:\n" + "the vector is zeroed and DLSS is told to trust the current frame there (DLSS5_Mask)."; +> = true; + +uniform bool VALIDATE_STATIC < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_label = "Static-hypothesis test (zeroes the vector, keeps history)"; + ui_tooltip = "For each pixel, asks which explains it better: 'did not move' or the provider's vector.\n" + "Both are scored on illumination-normalised 3x3 structure (local mean removed), so a\n" + "flickering light does not count as motion. When 'did not move' wins, the vector is zeroed\n" + "and the pixel is NOT masked -- a static wall wants its full history, which is what smooths\n" + "the flicker. This is the test for the flickering-wall case."; +> = true; + +uniform float STATIC_BIAS < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; + ui_label = "Static bias"; + ui_tooltip = "How much worse (relative) the static explanation may score than the vector's and still win.\n" + "0 = the vector must strictly beat 'did not move'. Higher favours zero vectors."; +> = 0.15; + +uniform float STATIC_MIN_CONTRAST < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 0.1; ui_step = 0.001; + ui_label = "Static test: minimum patch contrast"; + ui_tooltip = "Below this 3x3 contrast (mean absolute deviation of luma) a patch has no structure to judge\n" + "motion by, and the test abstains -- the provider's vector stands. Raise it if flat surfaces\n" + "trail while moving (yellow on plain motion in the debug view); lower it if the\n" + "flickering wall stops being caught."; +> = 0.012; + +uniform bool VALIDATE_LUMA < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_label = "Luma test (mask only)"; + ui_tooltip = "The reprojected previous luma must fall inside the current 3x3 neighbourhood's luma range.\n" + "A failure only raises the mask (DLSS leans on the current frame); it never zeroes the vector,\n" + "because a lighting change does not prove the surface did not move. Off by default: on a\n" + "flickering surface it asks DLSS to drop exactly the history that would smooth the flicker."; +> = false; + +uniform float LUMA_TOLERANCE < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; + ui_label = "Luma tolerance"; + ui_tooltip = "How far outside the current 3x3 neighbourhood's luma range the reprojected previous luma\n" + "may fall (relative to that range's maximum). Lower = stricter."; +> = 0.25; + +uniform bool VALIDATE_DEPTH < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_label = "Depth test (zeroes the vector)"; + ui_tooltip = "The reprojected previous linear depth must match the current one: a mismatch means the vector\n" + "points at a different surface (disocclusion), so it is zeroed and masked. Sky is exempt."; +> = true; + +uniform float DEPTH_TOLERANCE < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 0.5; ui_step = 0.005; + ui_label = "Depth tolerance"; + ui_tooltip = "Allowed relative difference between the reprojected previous linear depth and the current one."; +> = 0.10; + +uniform bool VALIDATE_MV < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_label = "Consistency test (zeroes the vector)"; + ui_tooltip = "This frame's vector must resemble the previous frame's vector at the spot it points to.\n" + "Real motion is smooth frame to frame; optical flow on fire, smoke or a flickering wall is not.\n" + "A failure zeroes the vector and masks the pixel."; +> = true; + +uniform float MV_CONSISTENCY < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 16.0; ui_step = 0.1; + ui_label = "Vector consistency (px)"; + ui_tooltip = "Allowed change, in pixels, between this frame's vector and the previous frame's vector at\n" + "the reprojected spot, plus 50% of the vector length. Raise it if plain camera motion\n" + "shows blue in the 'Validation tests' debug view."; +> = 1.4; + +uniform float MASK_STRENGTH < + ui_category = "Validation (flicker / flames / disocclusion)"; + ui_type = "drag"; ui_min = 0.0; ui_max = 1.0; ui_step = 0.05; + ui_label = "Bias-current-colour mask strength"; + ui_tooltip = "How strongly a distrusted pixel asks DLSS to favour the current frame (DLSS5_Mask).\n" + "1 = fully; 0 = only zero the vector, do not mask."; +> = 1.0; + +uniform float2 MV_SIGN < + ui_type = "drag"; + ui_min = -1.0; ui_max = 1.0; ui_step = 2.0; + ui_label = "Motion vector sign (x, y)"; + ui_tooltip = "Flip a component if the DLAA output doubles/smears in that direction while moving.\n" + "Default (1, 1) matches the convention every supported provider uses (prev_uv = uv + mv)."; +> = float2(1.0, 1.0); + +uniform float MV_SCALE < + ui_type = "drag"; + ui_min = 0.0; ui_max = 4.0; ui_step = 0.01; + ui_label = "Motion vector scale"; + ui_tooltip = "1.0 = the provider's estimate as-is. Diagnostic only."; +> = 1.0; + +uniform int DEBUG_VIEW < + ui_type = "combo"; + ui_items = "Motion vectors (colour = direction, brightness = speed)\0" + "Raw depth\0" + "Provider confidence (LumeniteFX only; white = confident)\0" + "Validation mask (white = vector distrusted, DLSS uses current frame)\0" + "Validation mask over the image\0" + "Validation tests over the image (red = luma, green = depth, blue = consistency, yellow = static wins)\0" + "Geometry model vectors (colour = direction, brightness = speed)\0" + "Geometry decision over the image (green = model, red = provider won as moving object, blue = provider rejected)\0" + "Geometry fit quality (grey = inlier share; top strip = fit error, black 0 px .. white 8 px)\0"; + ui_label = "Debug view (DLSS5_Feed_Debug technique)"; +> = 0; + +// Outputs for the add-on +texture DLSS5_MV { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RG16F; }; +texture DLSS5_Depth { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R32F; }; +texture DLSS5_Mask { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R8; }; +sampler sDLSS5_MV { Texture = DLSS5_MV; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_Depth { Texture = DLSS5_Depth; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_Mask { Texture = DLSS5_Mask; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; + +// Previous-frame history for validation (written at the end of the technique) +texture DLSS5_PrevLuma { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; }; +texture DLSS5_PrevDepth { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; }; +texture DLSS5_PrevMV { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RG16F; }; +// Luma may be interpolated (a smooth quantity); depth and vectors must NOT be -- bilinear +// across an object edge mixes two surfaces' values and fails the test on every edge in motion. +sampler sDLSS5_PrevLuma { Texture = DLSS5_PrevLuma; AddressU = Clamp; AddressV = Clamp; MinFilter = LINEAR; MagFilter = LINEAR; MipFilter = POINT; }; +sampler sDLSS5_PrevDepth { Texture = DLSS5_PrevDepth; AddressU = Clamp; AddressV = Clamp; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_PrevMV { Texture = DLSS5_PrevMV; AddressU = Clamp; AddressV = Clamp; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; + +// Camera-model fit: a sparse sample grid of (x, y, w, valid | u, v), and the solved model as +// six 1x1 RGBA32F texels (18 parameters + fit statistics). +#define DLSS5_FIT_W 40 +#define DLSS5_FIT_H 23 +texture DLSS5_FitA { Width = DLSS5_FIT_W; Height = DLSS5_FIT_H; Format = RGBA32F; }; +texture DLSS5_FitB { Width = DLSS5_FIT_W; Height = DLSS5_FIT_H; Format = RGBA32F; }; +sampler sDLSS5_FitA { Texture = DLSS5_FitA; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_FitB { Texture = DLSS5_FitB; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +texture DLSS5_Cam0 { Width = 1; Height = 1; Format = RGBA32F; }; +texture DLSS5_Cam1 { Width = 1; Height = 1; Format = RGBA32F; }; +texture DLSS5_Cam2 { Width = 1; Height = 1; Format = RGBA32F; }; +texture DLSS5_Cam3 { Width = 1; Height = 1; Format = RGBA32F; }; +texture DLSS5_Cam4 { Width = 1; Height = 1; Format = RGBA32F; }; +texture DLSS5_Cam5 { Width = 1; Height = 1; Format = RGBA32F; }; +sampler sDLSS5_Cam0 { Texture = DLSS5_Cam0; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_Cam1 { Texture = DLSS5_Cam1; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_Cam2 { Texture = DLSS5_Cam2; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_Cam3 { Texture = DLSS5_Cam3; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_Cam4 { Texture = DLSS5_Cam4; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; +sampler sDLSS5_Cam5 { Texture = DLSS5_Cam5; MinFilter = POINT; MagFilter = POINT; MipFilter = POINT; }; + +// --------------------------------------------------------------------------------------------- + +// The selected provider's vector at uv, as delta UV (prev_uv = uv + mv). +float2 ProviderMV(float2 uv) +{ + float4 c = float4(uv, 0.0, 0.0); +#if DLSS5_MV_LOWRES + return MV_LOWRES_FILTER == 0 ? tex2Dlod(sDLSS5_ProviderMV, c).xy : tex2Dlod(sDLSS5_ProviderMVPoint, c).xy; +#else + return tex2Dlod(sDLSS5_ProviderMV, c).xy; +#endif +} + +float Luma(float2 uv) +{ + return dot(tex2Dlod(sDLSS5_ColorInput, float4(uv, 0.0, 0.0)).rgb, float3(0.299, 0.587, 0.114)); +} + +// Illumination-normalised 3x3 structure difference between the current frame at uv_cur and +// the previous frame at uv_prev: each patch has its own mean removed first, so a brightness +// change (flicker) contributes nothing and only the pattern is compared. +// Also returns the current patch's contrast (mean absolute deviation): a patch with no +// structure cannot decide anything, and the caller must not pretend it can. +float PatchError(float2 uv_cur, float2 uv_prev, out float contrast) +{ + const float2 px = BUFFER_PIXEL_SIZE; + float c[9], p[9]; + float mc = 0.0, mp = 0.0; + [unroll] for (int i = 0; i < 9; ++i) + { + const float2 o = float2(i % 3 - 1, i / 3 - 1) * px; + c[i] = Luma(uv_cur + o); + p[i] = tex2Dlod(sDLSS5_PrevLuma, float4(uv_prev + o, 0.0, 0.0)).x; + mc += c[i]; mp += p[i]; + } + mc /= 9.0; mp /= 9.0; + float err = 0.0; + contrast = 0.0; + [unroll] for (int j = 0; j < 9; ++j) + { + err += abs((c[j] - mc) - (p[j] - mp)); + contrast += abs(c[j] - mc); + } + contrast /= 9.0; + return err / 9.0; +} + +// Per-test failure (0 = fine, 1 = failed, soft in between): x = luma, y = depth, z = consistency, +// w = the static hypothesis won. Luma failing says "this pixel's appearance changed"; depth or +// consistency failing says "this vector points at the wrong thing"; static winning says "no +// vector explains this pixel better than zero". Only y, z and w justify zeroing the vector, and +// only x, y, z justify asking DLSS to distrust history. +float4 ValidateTests(float2 uv, float2 mv) +{ + const float2 puv = uv + mv; + float4 bad = 0.0; + // Reprojecting off-screen: nothing to compare against. Keep the vector (DLSS handles + // it) and let the mask lean on the current frame. + if (any(puv < 0.0) || any(puv > 1.0)) return float4(1.0, 0.0, 0.0, 0.0); + + // 0. Static hypothesis: does "did not move" explain this pixel at least as well as the + // vector does? Scored on mean-removed structure, so flicker is not motion. Skipped for + // vectors under half a pixel (nothing to decide). + if (VALIDATE_STATIC && length(mv * BUFFER_SCREEN_SIZE) > 0.5) + { + float sc, unused; + const float es = PatchError(uv, uv, sc); + const float ef = PatchError(uv, puv, unused); + // Only a patch with structure can tell the two apart. Below the contrast floor the + // scores tie for lack of evidence, and a tie must go to the provider (its flow is + // propagated from textured neighbours -- the right guess for a moving flat wall). + // With structure, static wins only if it beats the vector by a share of that contrast. + if (sc >= STATIC_MIN_CONTRAST) + bad.w = es + 0.25 * sc <= ef * (1.0 + STATIC_BIAS) ? 1.0 : 0.0; + } + + // 1. Luma: current 3x3 range vs the previous luma at the reprojected spot. + if (VALIDATE_LUMA) + { + const float2 px = BUFFER_PIXEL_SIZE; + float lc = Luma(uv), lmin = lc, lmax = lc; + [unroll] for (int y = -1; y <= 1; ++y) + [unroll] for (int x = -1; x <= 1; ++x) + { + const float l = Luma(uv + float2(x, y) * px); + lmin = min(lmin, l); lmax = max(lmax, l); + } + const float lp = tex2Dlod(sDLSS5_PrevLuma, float4(puv, 0.0, 0.0)).x; + const float margin = LUMA_TOLERANCE * max(lmax, 0.05) + 2.0 / 255.0; + bad.x = saturate(max(lmin - lp, lp - lmax) / margin); + } + + // 2. Depth: previous linear depth at the reprojected spot vs the current one (sky exempt). + const float dc = ReShade::GetLinearizedDepth(uv); + if (VALIDATE_DEPTH && dc < 0.999) + { + const float dp = tex2Dlod(sDLSS5_PrevDepth, float4(puv, 0.0, 0.0)).x; + const float tol = DEPTH_TOLERANCE * max(dc, 1e-3); + bad.y = saturate((abs(dp - dc) - tol) / (tol + 1e-5)); + } + + // 3. Consistency: the previous frame's vector where this pixel came from vs this one. + if (VALIDATE_MV && MV_CONSISTENCY > 0.0) + { + const float2 pmv = tex2Dlod(sDLSS5_PrevMV, float4(puv, 0.0, 0.0)).xy; + const float diff = length((mv - pmv) * BUFFER_SCREEN_SIZE); + const float allow = MV_CONSISTENCY + 0.5 * length(mv * BUFFER_SCREEN_SIZE); + bad.z = saturate((diff - allow) / allow); + } + return bad; +} + +// --------------------------------------------------------------------------------------------- +// Camera model. Screen position x, y in -0.5..0.5, inverse-depth term w = s / (depth + s). +// Basis (9 terms): 1, x, y, x^2, xy, y^2, w, xw, yw -- the small-rotation flow field of a +// pinhole camera is quadratic in the image position, and translation adds terms in 1/Z. +// Both flow components share the basis; the fit solves them together (two right-hand sides). +// --------------------------------------------------------------------------------------------- + +#define DLSS5_BASIS(B, x, y, w) \ + B[0] = 1.0; B[1] = x; B[2] = y; B[3] = x * x; B[4] = x * y; B[5] = y * y; B[6] = w; B[7] = x * w; B[8] = y * w; + +float ParallaxW(float d) { return GEOM_PARALLAX / (d + GEOM_PARALLAX); } + +// The model's predicted delta-UV at uv for linear depth d. +float2 PredictMV(float2 uv, float d) +{ + const float4 c = float4(0.5, 0.5, 0.0, 0.0); + const float4 p0 = tex2Dlod(sDLSS5_Cam0, c), p1 = tex2Dlod(sDLSS5_Cam1, c), p2 = tex2Dlod(sDLSS5_Cam2, c); + const float4 p3 = tex2Dlod(sDLSS5_Cam3, c), p4 = tex2Dlod(sDLSS5_Cam4, c); + const float x = uv.x - 0.5, y = uv.y - 0.5, w = ParallaxW(d); + float B[9]; DLSS5_BASIS(B, x, y, w) + // u: p0.xyzw p1.xyzw p2.x v: p2.yzw p3.xyzw p4.xy + const float u = p0.x * B[0] + p0.y * B[1] + p0.z * B[2] + p0.w * B[3] + p1.x * B[4] + p1.y * B[5] + p1.z * B[6] + p1.w * B[7] + p2.x * B[8]; + const float v = p2.y * B[0] + p2.z * B[1] + p2.w * B[2] + p3.x * B[3] + p3.y * B[4] + p3.z * B[5] + p3.w * B[6] + p4.x * B[7] + p4.y * B[8]; + return float2(u, v); +} + +bool FitIsUsable() +{ + const float4 s = tex2Dlod(sDLSS5_Cam5, float4(0.5, 0.5, 0.0, 0.0)); // x = inlier share, y = rms px, z = samples used + return s.z >= 40.0 && s.x >= 0.25; +} + +// Pass 1: sample the provider's flow and the depth on a sparse grid. +void PS_FitSamples(float4 vpos : SV_Position, float2 uv : TEXCOORD, out float4 A : SV_Target0, out float4 B : SV_Target1) +{ + const float2 suv = (floor(vpos.xy) + 0.5) / float2(DLSS5_FIT_W, DLSS5_FIT_H); + const float d = ReShade::GetLinearizedDepth(suv); + const float2 mv = ProviderMV(suv); + const bool valid = d > 0.001 && all(abs(mv * BUFFER_SCREEN_SIZE) < 512.0); + A = float4(suv.x - 0.5, suv.y - 0.5, ParallaxW(d), valid ? 1.0 : 0.0); + B = float4(mv, 0.0, 0.0); +} + +// Pass 2 (one pixel): robust least squares. Pass one fits everything; pass two refits on the +// samples the first fit explains to within GEOM_OUTLIER_PX, which drops moving objects, +// flames and the weapon from the camera estimate. +void PS_FitSolve(float4 vpos : SV_Position, float2 uv : TEXCOORD, + out float4 P0 : SV_Target0, out float4 P1 : SV_Target1, out float4 P2 : SV_Target2, + out float4 P3 : SV_Target3, out float4 P4 : SV_Target4, out float4 P5 : SV_Target5) +{ + float p[18]; + [unroll] for (int z = 0; z < 18; ++z) p[z] = 0.0; + float inlier = 0.0, rms = 0.0, used = 0.0; + const int total = DLSS5_FIT_W * DLSS5_FIT_H; + + [loop] for (int it = 0; it < 2; ++it) + { + float M[45]; // upper triangle of the 9x9 normal matrix + float ru[9], rv[9]; + [unroll] for (int z0 = 0; z0 < 45; ++z0) M[z0] = 0.0; + [unroll] for (int z1 = 0; z1 < 9; ++z1) { ru[z1] = 0.0; rv[z1] = 0.0; } + int n = 0; + float se = 0.0; + + [loop] for (int s = 0; s < total; ++s) + { + const int2 cell = int2(s % DLSS5_FIT_W, s / DLSS5_FIT_W); + const float4 a = tex2Dfetch(sDLSS5_FitA, cell); + const float4 b = tex2Dfetch(sDLSS5_FitB, cell); + if (a.w < 0.5) continue; + float B[9]; DLSS5_BASIS(B, a.x, a.y, a.z) + if (it > 0) + { + float pu = 0.0, pv = 0.0; + [unroll] for (int i0 = 0; i0 < 9; ++i0) { pu += p[i0] * B[i0]; pv += p[9 + i0] * B[i0]; } + const float r = length((float2(pu, pv) - b.xy) * BUFFER_SCREEN_SIZE); + if (r > GEOM_OUTLIER_PX) continue; + se += r * r; + } + ++n; + int k = 0; + [unroll] for (int i = 0; i < 9; ++i) + { + ru[i] += B[i] * b.x; + rv[i] += B[i] * b.y; + [unroll] for (int j = i; j < 9; ++j) { M[k] += B[i] * B[j]; ++k; } + } + } + if (n < 40) break; // not enough evidence: keep whatever the previous pass produced + + // Augmented 9 x (9 + 2) system, Gauss-Jordan with partial pivoting, tiny ridge for + // the degenerate cases (flat depth makes w collinear with 1; a still camera makes + // everything zero). + float G[99]; + { + int k2 = 0; + [unroll] for (int i = 0; i < 9; ++i) + { + [unroll] for (int j = i; j < 9; ++j) { G[i * 11 + j] = M[k2]; G[j * 11 + i] = M[k2]; ++k2; } + G[i * 11 + i] += 1e-5 * n; + G[i * 11 + 9] = ru[i]; + G[i * 11 + 10] = rv[i]; + } + } + bool singular = false; + [loop] for (int col = 0; col < 9; ++col) + { + int piv = col; + float best = abs(G[col * 11 + col]); + [loop] for (int r0 = col + 1; r0 < 9; ++r0) + { + const float v0 = abs(G[r0 * 11 + col]); + if (v0 > best) { best = v0; piv = r0; } + } + if (best < 1e-12) { singular = true; break; } + if (piv != col) + [unroll] for (int c0 = 0; c0 < 11; ++c0) { const float t = G[col * 11 + c0]; G[col * 11 + c0] = G[piv * 11 + c0]; G[piv * 11 + c0] = t; } + const float inv = 1.0 / G[col * 11 + col]; + [unroll] for (int c1 = 0; c1 < 11; ++c1) G[col * 11 + c1] *= inv; + [loop] for (int r1 = 0; r1 < 9; ++r1) + { + if (r1 == col) continue; + const float f = G[r1 * 11 + col]; + if (f == 0.0) continue; + [unroll] for (int c2 = 0; c2 < 11; ++c2) G[r1 * 11 + c2] -= f * G[col * 11 + c2]; + } + } + if (singular) break; + [unroll] for (int i2 = 0; i2 < 9; ++i2) { p[i2] = G[i2 * 11 + 9]; p[9 + i2] = G[i2 * 11 + 10]; } + used = n; + inlier = float(n) / float(total); + if (it > 0) rms = sqrt(se / max(n, 1)); + } + + P0 = float4(p[0], p[1], p[2], p[3]); + P1 = float4(p[4], p[5], p[6], p[7]); + P2 = float4(p[8], p[9], p[10], p[11]); + P3 = float4(p[12], p[13], p[14], p[15]); + P4 = float4(p[16], p[17], 0.0, 0.0); + P5 = float4(inlier, rms, used, 0.0); +} + +// Per-pixel decision: x = final delta-UV vector, .z = 0 model / 1 provider (moving object) / +// 2 provider rejected, .w = mask contribution from that decision. +float4 GeometryDecide(float2 uv, float d, float2 flow) +{ + const float2 pred = PredictMV(uv, d); + const float r = length((flow - pred) * BUFFER_SCREEN_SIZE); + const float agree = GEOM_AGREE_PX + 0.1 * length(pred * BUFFER_SCREEN_SIZE); + if (r <= agree) return float4(pred, 0.0, 0.0); + + float cp, cf; + const float ep = PatchError(uv, uv + pred, cp); + const float ef = PatchError(uv, uv + flow, cf); + const bool dynamic = cp >= STATIC_MIN_CONTRAST && ef <= ep * (1.0 - GEOM_DYNAMIC_MARGIN) - 1.0 / 255.0; + if (dynamic) return float4(flow, 1.0, 0.0); + return float4(pred, 2.0, GEOM_MASK_REJECTED * saturate((r - agree) / (4.0 * agree))); +} + +float RawDepth(float2 uv) +{ + // Raw hardware depth, exactly as the game wrote it -- the same orientation/offset + // corrections ReShade.fxh applies in GetLinearizedDepth(), minus the linearisation + // (DLSS must receive the raw values; the add-on tells it whether the range is reversed). + float2 t = uv; +#if RESHADE_DEPTH_INPUT_IS_UPSIDE_DOWN + t.y = 1.0 - t.y; +#endif + t.x /= RESHADE_DEPTH_INPUT_X_SCALE; + t.y /= RESHADE_DEPTH_INPUT_Y_SCALE; +#if RESHADE_DEPTH_INPUT_X_PIXEL_OFFSET + t.x -= RESHADE_DEPTH_INPUT_X_PIXEL_OFFSET * BUFFER_RCP_WIDTH; +#else + t.x -= RESHADE_DEPTH_INPUT_X_OFFSET / 2.000000001; +#endif +#if RESHADE_DEPTH_INPUT_Y_PIXEL_OFFSET + t.y += RESHADE_DEPTH_INPUT_Y_PIXEL_OFFSET * BUFFER_RCP_HEIGHT; +#else + t.y += RESHADE_DEPTH_INPUT_Y_OFFSET / 2.000000001; +#endif + return tex2Dlod(ReShade::DepthBuffer, float4(t, 0.0, 0.0)).x; +} + +void PS_MotionVectors(float4 vpos : SV_Position, float2 uv : TEXCOORD, + out float2 mv_out : SV_Target0, out float mask : SV_Target1, + out float depth : SV_Target2) +{ + // Providers hand out "delta UV": previous position = uv + mv. DLSS wants the same + // direction, in pixels. + const float2 flow = ProviderMV(uv); + float2 mv = flow; + float distrust = 0.0; + + if (GEOM_ENABLE && FitIsUsable()) + { + const float d = ReShade::GetLinearizedDepth(uv); + const float4 g = GeometryDecide(uv, d, flow); + mv = g.xy; + distrust = g.w; + // Disocclusion: the geometric vector on a newly revealed pixel points into the + // occluder's old position; the depth test catches that and asks for the current frame. + if (VALIDATE_DEPTH && d < 0.999) + { + const float2 puv = uv + mv; + if (all(puv >= 0.0) && all(puv <= 1.0)) + { + const float dp = tex2Dlod(sDLSS5_PrevDepth, float4(puv, 0.0, 0.0)).x; + const float tol = DEPTH_TOLERANCE * max(d, 1e-3); + distrust = max(distrust, saturate((abs(dp - d) - tol) / (tol + 1e-5))); + } + } + } + else if (MV_VALIDATE) + { + const float4 bad = ValidateTests(uv, flow); + const float zero_vector = max(bad.y, max(bad.z, bad.w)); // wrong target, or static explains it: treat as static + distrust = max(bad.x, max(bad.y, bad.z)); // appearance changed / wrong target: favour the current frame + mv = flow * (1.0 - zero_vector); + } + + mv_out = mv * float2(BUFFER_WIDTH, BUFFER_HEIGHT) * MV_SIGN * MV_SCALE; + mask = distrust * MASK_STRENGTH; + depth = RawDepth(uv); +} + +// End of the technique: this frame becomes next frame's history. The raw provider vector is +// stored (not the validated one), so one distrusted frame does not poison the next test. +void PS_StoreHistory(float4 vpos : SV_Position, float2 uv : TEXCOORD, + out float luma : SV_Target0, out float depth : SV_Target1, out float2 mv : SV_Target2) +{ + luma = Luma(uv); + depth = ReShade::GetLinearizedDepth(uv); + mv = ProviderMV(uv); +} + +float3 PS_Debug(float4 vpos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + if (DEBUG_VIEW == 1) + { + const float raw_depth = tex2Dlod(sDLSS5_Depth, float4(uv, 0.0, 0.0)).x; +#if RESHADE_DEPTH_INPUT_IS_REVERSED + const float proximity = raw_depth; +#else + const float proximity = 1.0 - raw_depth; +#endif + // Display-only contrast curve: DLSS5_Depth itself remains raw and untouched. + return pow(saturate(proximity), 0.125).xxx; + } + if (DEBUG_VIEW == 2) + { +#if DLSS5_MV_LOWRES + return saturate(tex2Dlod(sDLSS5_ProviderConfidence, float4(uv, 0.0, 0.0)).x).xxx; +#else + return (0.25).xxx; // this provider publishes no confidence map +#endif + } + if (DEBUG_VIEW == 3) + return tex2Dlod(sDLSS5_Mask, float4(uv, 0.0, 0.0)).xxx; + if (DEBUG_VIEW == 4) + { + const float m = tex2Dlod(sDLSS5_Mask, float4(uv, 0.0, 0.0)).x; + const float3 img = tex2Dlod(sDLSS5_ColorInput, float4(uv, 0.0, 0.0)).rgb; + return lerp(img, float3(1.0, 0.2, 0.1), m * 0.75); + } + if (DEBUG_VIEW == 5) + { + // Recomputed here against the same history the feed pass used this frame. + const float4 bad = ValidateTests(uv, ProviderMV(uv)); + const float3 img = tex2Dlod(sDLSS5_ColorInput, float4(uv, 0.0, 0.0)).rgb * 0.5; + return saturate(img + bad.xyz * 0.9 + bad.w * float3(0.6, 0.6, 0.0)); + } + if (DEBUG_VIEW == 6) + { + const float2 pv = PredictMV(uv, ReShade::GetLinearizedDepth(uv)) * BUFFER_SCREEN_SIZE; + const float angle = atan2(pv.y, pv.x), speed = length(pv); + const float3 rgb = saturate(3.0 * abs(2.0 * frac(angle / 6.283185 + float3(0.0, -1.0 / 3.0, 1.0 / 3.0)) - 1.0) - 1.0); + return lerp(0.5, rgb, saturate(speed / 16.0)); + } + if (DEBUG_VIEW == 7) + { + const float3 img = tex2Dlod(sDLSS5_ColorInput, float4(uv, 0.0, 0.0)).rgb * 0.5; + if (!FitIsUsable()) return img; // no usable fit this frame: nothing to show + const float4 g = GeometryDecide(uv, ReShade::GetLinearizedDepth(uv), ProviderMV(uv)); + const float3 tint = g.z < 0.5 ? float3(0.0, 0.5, 0.0) : g.z < 1.5 ? float3(0.9, 0.0, 0.0) : float3(0.0, 0.2, 0.9); + return saturate(img + tint); + } + if (DEBUG_VIEW == 8) + { + const float4 s = tex2Dlod(sDLSS5_Cam5, float4(0.5, 0.5, 0.0, 0.0)); + if (uv.y < 0.05) return saturate(s.y / 8.0).xxx; // fit error strip + return s.x.xxx; // inlier share + } + float2 mv = tex2Dlod(sDLSS5_MV, float4(uv, 0.0, 0.0)).xy; // pixels + float angle = atan2(mv.y, mv.x); + float speed = length(mv); + float3 rgb = saturate(3.0 * abs(2.0 * frac(angle / 6.283185 + float3(0.0, -1.0 / 3.0, 1.0 / 3.0)) - 1.0) - 1.0); + return lerp(0.5, rgb, saturate(speed / 16.0)); // 16 px/frame saturates the colour +} + +// --------------------------------------------------------------------------------------------- + +technique DLSS5_Feed +< + ui_label = "DLSS 5 Feed (place below your motion-vector provider)"; + ui_tooltip = "Prepares motion vectors + depth (+ a trust mask) for the DLSS 5 Feed add-on.\n\n" + "Provider: " DLSS5_MV_PROVIDER_NAME "\n" + "Change it with the DLSS5_MV_PROVIDER preprocessor definition (0 texMotionVectors,\n" + "1 Launchpad, 2 VORT, 3 LumeniteFX Kernel, 4 LumeniteFX QuantMotion) and enable\n" + "that provider's technique ABOVE this one."; +> +{ + pass FitSamples { VertexShader = PostProcessVS; PixelShader = PS_FitSamples; RenderTarget0 = DLSS5_FitA; RenderTarget1 = DLSS5_FitB; } + pass FitSolve { VertexShader = PostProcessVS; PixelShader = PS_FitSolve; RenderTarget0 = DLSS5_Cam0; RenderTarget1 = DLSS5_Cam1; RenderTarget2 = DLSS5_Cam2; RenderTarget3 = DLSS5_Cam3; RenderTarget4 = DLSS5_Cam4; RenderTarget5 = DLSS5_Cam5; } + pass Guides { VertexShader = PostProcessVS; PixelShader = PS_MotionVectors; RenderTarget0 = DLSS5_MV; RenderTarget1 = DLSS5_Mask; RenderTarget2 = DLSS5_Depth; } + pass History { VertexShader = PostProcessVS; PixelShader = PS_StoreHistory; RenderTarget0 = DLSS5_PrevLuma; RenderTarget1 = DLSS5_PrevDepth; RenderTarget2 = DLSS5_PrevMV; } + DLSS5_MV_REQUEST_PASS // Launchpad only: ask it to compute optical flow again next frame +} + +technique DLSS5_Feed_Debug +< + ui_label = "DLSS 5 Feed - debug view"; + ui_tooltip = "Shows the motion vectors / depth / mask the add-on will send to DLSS. Enable only for checking."; +> +{ + pass { VertexShader = PostProcessVS; PixelShader = PS_Debug; } +} diff --git a/EXE/reshade-shaders/Shaders/include/lumenite_ColorManagement.fxh b/EXE/reshade-shaders/Shaders/include/lumenite_ColorManagement.fxh new file mode 100644 index 00000000..716fd10e --- /dev/null +++ b/EXE/reshade-shaders/Shaders/include/lumenite_ColorManagement.fxh @@ -0,0 +1,252 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_ColorManagement.fxh + Version : 2026.05.05 + Author : Afzaal (Kaidō) + Description: Provides color management including color space detection, + color space transfers and tonemapping. + Supported colorbuffers: + - SDR (sRGB) + - HDR (scRGB / Linear) + - HDR (PQ / ST.2084) + - HDR (HLG) + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +#pragma once + +/*-------------------. +| :: PREPROCESSOR :: | +'-------------------*/ +#ifndef HDR_WHITELEVEL + #define HDR_WHITELEVEL 203 +#endif + +#if BUFFER_COLOR_SPACE > 0 + //already defined by ReShade +#else + #if BUFFER_COLOR_BIT_DEPTH == 8 + #undef BUFFER_COLOR_SPACE + #define BUFFER_COLOR_SPACE 1 //sRGB + #elif BUFFER_COLOR_BIT_DEPTH == 16 + #undef BUFFER_COLOR_SPACE + #define BUFFER_COLOR_SPACE 2 //scRGB + #elif __RENDERER__ < 0xb000 + #undef BUFFER_COLOR_SPACE + #define BUFFER_COLOR_SPACE 1 //D3D9/10 usually SDR + #endif +#endif + +/*------------------. +| :: UI UNIFORMS :: | +'------------------*/ +// uniform int SHOW_COLOR_SPACE < +// ui_category = "Color Management"; +// ui_type = "combo"; +// ui_label = "Colorspace"; +// ui_tooltip = "Shows the detected color space.\n1=sRGB, 2=scRGB, 3=PQ, 4=HLG"; +// hidden = true; +// #if BUFFER_COLOR_SPACE == 1 +// ui_items = "sRGB (Detected)\0"; +// #elif BUFFER_COLOR_SPACE == 2 +// ui_items = "scRGB (Detected)\0"; +// #elif BUFFER_COLOR_SPACE == 3 +// ui_items = "PQ / ST.2084 (Detected)\0"; +// #elif BUFFER_COLOR_SPACE == 4 +// ui_items = "HLG (Detected)\0"; +// #else +// ui_items = "Unknown (Defaulting to sRGB)\0"; +// #endif +// > = 0; + +#if BUFFER_COLOR_BIT_DEPTH > 8 || BUFFER_COLOR_SPACE > 1 + #define COLORSPACE_CONVERSION 1 //use approx. transfer function; 0 for accurate +#else + #define COLORSPACE_CONVERSION 2 //N/A for 8-bit +#endif + +#if BUFFER_COLOR_SPACE == 1 + #define TONEMAPPER 1 //reinhard tonemapper workflow for SDR (sRGB) colorbuffer; 0 for None +#else + #define TONEMAPPER 0 +#endif + +/*-------------------------. +| :: TRANSFER FUNCTIONS :: | +'-------------------------*/ +//sRGB +float3 sRGBtoLinearAccurate(float3 r) { + return (r <= 0.04045) ? (r / 12.92) : pow(abs(r + 0.055) / 1.055, 2.4); +} + +float3 sRGBtoLinearFast(float3 r) { + return max(r / 12.92, r * r); //gamma 2.0 approx +} + +float3 sRGBtoLinear(float3 r) { + if (COLORSPACE_CONVERSION == 1) return sRGBtoLinearFast(r); + else return sRGBtoLinearAccurate(r); +} + +float3 linearToSRGBAccurate(float3 r) { + return (r <= 0.0031308) ? (r * 12.92) : (1.055 * pow(abs(r), 1.0 / 2.4) - 0.055); +} + +float3 linearToSRGBFast(float3 r) { + return min(r * 12.92, sqrt(r)); //gamma 2.0 approx +} + +float3 linearToSRGB(float3 r) { + if (COLORSPACE_CONVERSION == 1) return linearToSRGBFast(r); + else return linearToSRGBAccurate(r); +} + +//PQ (ST.2084) +float3 PQtoLinearAccurate(float3 r) { + const float m1 = 1305.0/8192.0; + const float m2 = 2523.0/32.0; + const float c1 = 107.0/128.0; + const float c2 = 2413.0/128.0; + const float c3 = 2392.0/128.0; + float3 powr = pow(max(r, 0), 1.0/m2); + r = pow(max(max(powr - c1, 0) / (c2 - c3 * powr), 0), 1.0/m1); + //scale 10,000 nits down so Paper White (HDR_WHITELEVEL) maps to 1.0 + return r * 10000.0 / HDR_WHITELEVEL; +} + +float3 PQtoLinearFast(float3 r) { + float3 square = r * r; + float3 quad = square * square; + float3 oct = quad * quad; + r = max(max(square / 340.0, quad / 6.0), oct); + return r * 10000.0 / HDR_WHITELEVEL; +} + +float3 PQtoLinear(float3 r) { + if (COLORSPACE_CONVERSION == 1) return PQtoLinearFast(r); + else return PQtoLinearAccurate(r); +} + +float3 linearToPQAccurate(float3 r) { + const float m1 = 1305.0/8192.0; + const float m2 = 2523.0/32.0; + const float c1 = 107.0/128.0; + const float c2 = 2413.0/128.0; + const float c3 = 2392.0/128.0; + + r = r * (HDR_WHITELEVEL / 10000.0); //rescale 1.0 back to nits + float3 powr = pow(max(r, 0), m1); + r = pow(max((c1 + c2 * powr) / (1 + c3 * powr), 0), m2); + return r; +} + +float3 linearToPQFast(float3 r) { + r = r * (HDR_WHITELEVEL / 10000.0); + float3 squareroot = sqrt(r); + float3 quadroot = sqrt(squareroot); + float3 octroot = sqrt(quadroot); + r = min(octroot, min(sqrt(sqrt(6.0))*quadroot, sqrt(340.0)*squareroot)); + return r; +} + +float3 linearToPQ(float3 r) { + if (COLORSPACE_CONVERSION == 1) return linearToPQFast(r); + else return linearToPQAccurate(r); +} + +//HLG (Hybrid Log Gamma) +float3 linearToHLG(float3 r) { + r = r * HDR_WHITELEVEL / 1000.0; + const float a = 0.17883277; + const float b = 0.28466892; + const float c = 0.55991073; + float3 s = sqrt(3 * r); + return (s < 0.5) ? s : (log(12 * r - b) * a + c); +} + +float3 HLGtoLinear(float3 r) { + const float a = 0.17883277; + const float b = 0.28466892; + const float c = 0.55991073; + r = (r < 0.5) ? (r * r / 3.0) : ((exp((r - c) / a) + b) / 12.0); + return r * 1000.0 / HDR_WHITELEVEL; +} + +//YCoCg +float3 linearToYCoCg(float3 r) { + float y = (r.r + 2.0 * r.g + r.b) * 0.25; + float co = (r.r - r.b) * 0.5; + float cg = (r.g - (r.r + r.b) * 0.5) * 0.5; + return float3(y, co, cg); +} + +float3 YCoCgToLinear(float3 r) { + float y = r.x; + float co = r.y; + float cg = r.z; + float g = y + cg; + float rOut = y + co - cg; + float b = y - co - cg; + return float3(rOut, g, b); +} + +/*--------------. +| :: HELPERS :: | +'--------------*/ +float3 ToLinearColorspace(float3 r, bool tonemap) { + if (BUFFER_COLOR_SPACE == 2) r = r * (80.0 / HDR_WHITELEVEL); //scRGB + else if (BUFFER_COLOR_SPACE == 3) r = PQtoLinear(r); + else if (BUFFER_COLOR_SPACE == 4) r = HLGtoLinear(r); + else { + r = sRGBtoLinear(r); + if (TONEMAPPER == 1 && tonemap) r = r / max(1.0 - r, 0.001); //inverse reinhard + } + return r; +} + +float3 ToOutputColorspace(float3 r, bool tonemap) { + if (BUFFER_COLOR_SPACE == 2) r = r * (HDR_WHITELEVEL / 80.0); //scRGB + else if (BUFFER_COLOR_SPACE == 3) r = linearToPQ(r); + else if (BUFFER_COLOR_SPACE == 4) r = linearToHLG(r); + else { + if (TONEMAPPER == 1 && tonemap) r = r / (1.0 + r); //forward reinhard + r = linearToSRGB(r); + } + return r; +} + +//read the theoretical max value of the buffer (in linear scale) +float GetMaxColorValue() { + if (BUFFER_COLOR_SPACE == 4) return 1000.0 / HDR_WHITELEVEL; + if (BUFFER_COLOR_SPACE >= 2) return 10000.0 / HDR_WHITELEVEL; + return 1.0; +} + +float GetLuminance(float3 color) +{ + return dot(color, float3(0.2126, 0.7152, 0.0722)); +} + +float3 GetLinearColor(float2 uv, bool tonemap) +{ + float3 color = tex2Dlod(ReShade::BackBuffer, float4(uv, 0, 0)).rgb; + return ToLinearColorspace(color, tonemap); +} diff --git a/EXE/reshade-shaders/Shaders/include/lumenite_Compute.fxh b/EXE/reshade-shaders/Shaders/include/lumenite_Compute.fxh new file mode 100644 index 00000000..34f9f09c --- /dev/null +++ b/EXE/reshade-shaders/Shaders/include/lumenite_Compute.fxh @@ -0,0 +1,54 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_Compute.fxh + Version : 2026.05.09 + Author : Afzaal (Kaidō) + Description: Header file for supporting compute enabled platforms. + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +#pragma once + +#include "ReShade.fxh" + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define D3D9 0x9000 +#define D3D10 0xa000 +#define D3D11 0xb000 +#define D3D12 0xc000 +#define OPENGL 0x10000 +#define VULKAN 0x20000 + +#if __RENDERER__ >= D3D11 + #define _COMPUTE_ENABLED_ 1 +#else + #define _COMPUTE_ENABLED_ 0 +#endif + +struct CSInput +{ + uint3 dispatchID : SV_DispatchThreadID; //global pixel coord (x, y, 0) + uint3 groupID : SV_GroupID; //which tile/group in grid + uint3 localID : SV_GroupThreadID; //thread inside group [0..CS_W-1] + uint flatIndex : SV_GroupIndex; //localID flattened: y*CS_W + x +}; diff --git a/EXE/reshade-shaders/Shaders/include/lumenite_Helpers.fxh b/EXE/reshade-shaders/Shaders/include/lumenite_Helpers.fxh new file mode 100644 index 00000000..4359790a --- /dev/null +++ b/EXE/reshade-shaders/Shaders/include/lumenite_Helpers.fxh @@ -0,0 +1,94 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_Helpers.fxh + Version : 2026.05.30 + Author : Afzaal (Kaidō) + Description: Helper functions for Lumenite shaders. + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +#pragma once + +#include "ReShade.fxh" + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define PI 3.14159265359 +#define EPSILON 1e-6 +//R2 sequence constants +static const float PHI_2 = 1.324717957244746; +static const float2 R2_CONSTANT = float2(1.0/PHI_2, 1.0/(PHI_2*PHI_2)); + +/*--------------. +| :: UNIFORMS ::| +'--------------*/ +uniform float TIMER < source = "timer"; >; //ms since launch +uniform float FRAME_TIME < source = "frametime"; >; //ms last frame +uniform uint FRAME_COUNT < source = "framecount"; >; +uniform float2 MOUSE_POS < source = "mousepoint"; >; //in screen px +uniform bool MOUSE_DOWN < source = "mousebutton"; min = 0; max = 0; >; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +bool CheckerboardSkip(uint2 currentPos, float scale) +{ + //map current buffer pixel to full screen pixel. + //floor() to ensure we snap to the integer grid of the full screen + uint2 fullScreenPos = uint2(floor(currentPos.x * scale), floor(currentPos.y * scale)); + return (((fullScreenPos.x + fullScreenPos.y + (FRAME_COUNT & 1)) & 1) == 1); +} + +float GetDepth(float2 uv) +{ + return ReShade::GetLinearizedDepth(uv); +} + +bool IsOOB(float2 uv) { + return any(uv < 0.0) || any(uv > 1.0); +} + +//QUASI-MONTE CARLO SEQUENCE +//fast Hilbert curve math (a 1D index from 2D coords) +uint HilbertIndex(uint x, uint y) { + uint index = 0; + [unroll] for (uint s = 64 / 2; s > 0; s /= 2) { + uint rx = (x & s) > 0; + uint ry = (y & s) > 0; + index += s * s * ((3 * rx) ^ ry); + if (ry == 0) { + if (rx == 1) { + x = 64 - 1 - x; + y = 64 - 1 - y; + } + uint t = x; x = y; y = t; + } + } + return index; +} + +float2 GetStratifiedNoise(float2 vpos) { + uint2 screenPos = uint2(vpos.xy) % 64; //64x64 tiled pixel coords + uint hIndex = HilbertIndex(screenPos.x, screenPos.y); //Hilbert index (spatial) + uint totalIndex = hIndex + (uint(FRAME_COUNT % 64) * 288); //temporal offset: 288 (same as Intel XeGTAO implementation) + return frac(float(totalIndex) * R2_CONSTANT); +} diff --git a/EXE/reshade-shaders/Shaders/include/lumenite_Projections.fxh b/EXE/reshade-shaders/Shaders/include/lumenite_Projections.fxh new file mode 100644 index 00000000..04200a8e --- /dev/null +++ b/EXE/reshade-shaders/Shaders/include/lumenite_Projections.fxh @@ -0,0 +1,96 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_Projections.fxh + Version : 2026.04.11 + Author : Afzaal (Kaidō) + Description: Camera projection functions for Lumenite shaders. + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +#pragma once + +#include "ReShade.fxh" + +/*--------------. +| :: HELPERS :: | +'--------------*/ +//VERTEX SHADER +struct VSOUT +{ + float4 vpos : SV_Position; + float2 uv : TEXCOORD0; + float tan_half_fov_x : TEXCOORD1; + float tan_half_fov_y : TEXCOORD2; + float inv_tan_half_fov_x : TEXCOORD3; + float inv_tan_half_fov_y : TEXCOORD4; + float near_ratio : TEXCOORD5; + float diff_ratio : TEXCOORD6; +}; + +#define TAN_HALF_FOV_Y tan(radians(FOV * 0.5)) +#define ASPECT_RATIO_X_OVER_Y ((float)BUFFER_WIDTH / (float)BUFFER_HEIGHT) +#define TAN_HALF_FOV_X TAN_HALF_FOV_Y * ASPECT_RATIO_X_OVER_Y +#define INV_TAN_HALF_FOV_X rcp(TAN_HALF_FOV_X) +#define INV_TAN_HALF_FOV_Y rcp(TAN_HALF_FOV_Y) + +VSOUT VS(uint id : SV_VertexID) +{ + VSOUT o; + o.uv.x = (id == 2) ? 2.0 : 0.0; + o.uv.y = (id == 1) ? 2.0 : 0.0; + o.vpos = float4(mad(o.uv.x, 2.0, -1.0), mad(o.uv.y, -2.0, 1.0), 0.0, 1.0); + o.tan_half_fov_x = TAN_HALF_FOV_X; + o.tan_half_fov_y = TAN_HALF_FOV_Y; + o.inv_tan_half_fov_x = INV_TAN_HALF_FOV_X; + o.inv_tan_half_fov_y = INV_TAN_HALF_FOV_Y; + o.near_ratio = NEAR_PLANE / RESHADE_DEPTH_LINEARIZATION_FAR_PLANE; + o.diff_ratio = 1.0 - o.near_ratio; //lerp(a,b,t) = (a+t * (b-a)), precompute (b-a) or (1.0-near_ratio) here + return o; +} + +//PROJECTION FUNCTIONS +//normalized frustum +//left-handed viewspace +//normals point outwards +//Z+ goes into the screen +float3 UVToViewSpace(float2 uv, float linear_depth_vs, VSOUT ps_input) +{ + float projection_scale = mad(linear_depth_vs, ps_input.diff_ratio, ps_input.near_ratio); //faster lerp: a+t * diff + float3 view_pos; + float ndc_x = mad(uv.x, 2.0, -1.0); + float ndc_y = mad(uv.y, -2.0, 1.0); + view_pos.x = ndc_x * ps_input.tan_half_fov_x * projection_scale; + view_pos.y = ndc_y * ps_input.tan_half_fov_y * projection_scale; + view_pos.z = linear_depth_vs; + return view_pos; +} + +float2 ViewSpaceToUV(float3 view_pos, VSOUT ps_input) +{ + float inv_projection_scale = rcp(mad(view_pos.z, ps_input.diff_ratio, ps_input.near_ratio)); + float2 ndc; + ndc.x = view_pos.x * ps_input.inv_tan_half_fov_x * inv_projection_scale; + ndc.y = view_pos.y * ps_input.inv_tan_half_fov_y * inv_projection_scale; + float2 uv; + uv.x = mad(ndc.x, 0.5, 0.5); + uv.y = mad(ndc.y, -0.5, 0.5); + return uv; +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_AnamorphicBloom.fx b/EXE/reshade-shaders/Shaders/lumenite_AnamorphicBloom.fx new file mode 100644 index 00000000..b989da5a --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_AnamorphicBloom.fx @@ -0,0 +1,511 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_AnamorphicBloom.fx + Version : 2026.06.09 + Author : Afzaal (Kaidō) + Description: Artistic bloom approximating the Anamorphic lens aesthetic. + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#ifndef ANAMORPHIC_BLOOM + #define ANAMORPHIC_BLOOM 1 +#endif + +#ifndef ANAMORPHIC_STREAKS + #define ANAMORPHIC_STREAKS 0 +#endif + +#ifndef COLOR_FRINGING + #define COLOR_FRINGING 0 +#endif + +#define BLOOM_THRESHOLD_SCALER 10.0 + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" +#include "./include/lumenite_ColorManagement.fxh" +#include "./include/lumenite_Helpers.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +#if ANAMORPHIC_BLOOM + uniform bool BLOOM_SKIP_SKYBOX < + ui_type = "radio"; + ui_label = "Exclude Skybox (Bloom)"; + ui_tooltip = "Prevents sky pixels from contributing to bloom."; + ui_category = "Anamorphic Bloom"; + > = false; + + uniform bool BLOOM_SHARP < + ui_type = "radio"; + ui_label = "Add More Definition to Bloom Shape (Experimental)"; + ui_tooltip = "Enables a sharper 1D horizontal kernel. May flicker with camera movement."; + ui_category = "Anamorphic Bloom"; + > = false; + + uniform float BLOOM_INTENSITY < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; + ui_label = "Bloom Intensity"; + ui_tooltip = "Scales the intensity of the Bloom effect."; + ui_category = "Anamorphic Bloom"; + > = 1.0; + + uniform float BLOOM_THRESHOLD < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; + ui_label = "Bloom Threshold"; + ui_tooltip = "Higher values bloom more of the scene."; + ui_category = "Anamorphic Bloom"; + > = 0.7; + + uniform float BLOOM_STRETCH < + ui_type = "drag"; + ui_min = 0.0; ui_max = 7.5; ui_step = 0.01; + ui_label = "Bloom Stretch"; + ui_tooltip = "Adjusts the horizontal elongation of the Bloom effect."; + ui_category = "Anamorphic Bloom"; + > = 7.5; + + #if COLOR_FRINGING + uniform float BLOOM_CA < + ui_type = "drag"; + ui_min = 0.0; ui_max = 10.0; ui_step = 0.01; + ui_label = "Bloom Chromatic Shift"; + ui_tooltip = "Shifts R/B channels within the bloom passes."; + ui_category = "Anamorphic Bloom"; + > = 10.0; + #endif +#endif + +#if ANAMORPHIC_STREAKS + uniform bool STREAK_SKIP_SKYBOX < + ui_type = "radio"; + ui_label = "Exclude Skybox (Streaks)"; + ui_tooltip = "Prevents sky pixels from contributing to light streaks."; + ui_category = "Anamorphic Streaks"; + > = false; + + uniform float STREAK_INTENSITY < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; + ui_label = "Streak Intensity"; + ui_tooltip = "Scales the intensity of the light streaks."; + ui_category = "Anamorphic Streaks"; + > = 1.0; + + uniform float STREAK_THRESHOLD < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; + ui_label = "Streak Threshold"; + ui_tooltip = "Higher values considers more of the scene."; + ui_category = "Anamorphic Streaks"; + > = 0.5; + + uniform float STREAK_STRETCH < + ui_type = "drag"; + ui_min = 0.0; ui_max = 10.0; ui_step = 0.01; + ui_label = "Streak Stretch"; + ui_tooltip = "Adjusts the horizontal elongation of the light streaks."; + ui_category = "Anamorphic Streaks"; + > = 10.0; + + uniform float3 STREAK_TINT < + ui_type = "color"; + ui_label = "Tint"; + ui_tooltip = "Tints the light streaks with chosen color. Set to white (1, 1, 1) for pass-through."; + ui_category = "Anamorphic Streaks"; + > = float3(0.55, 0.55, 1.0); + + #if COLOR_FRINGING + uniform float STREAK_CA < + ui_type = "drag"; + ui_min = 0.0; ui_max = 10.0; ui_step = 0.01; + ui_label = "Streak Chromatic Shift"; + ui_tooltip = "Shifts R/B channels of the light streaks."; + ui_category = "Anamorphic Streaks"; + > = 10.0; + #endif +#endif + +uniform int USER_GUIDE < +ui_type = "radio"; + ui_category = ""; + ui_label = " "; + ui_text = "Exclude Skybox: Requires access to properly configured depth buffer."; +>; + +namespace LumeniteAnamorphicBloom { + +/*-------------. +| :: MACROS :: | +'-------------*/ +#if ANAMORPHIC_BLOOM + #define BLOOM_SHIFT (float2(BLOOM_CA * BUFFER_PIXEL_SIZE.x, 0.0)) //once per pass + + #if COLOR_FRINGING + #define SAMPLE_BLOOM_TEX(s, uv) float3( \ + tex2D(s, (uv) - BLOOM_SHIFT).r, \ + tex2D(s, (uv)).g, \ + tex2D(s, (uv) + BLOOM_SHIFT).b \ + ) + #else + #define SAMPLE_BLOOM_TEX(s, uv) tex2D(s, uv).rgb + #endif +#endif + +#if ANAMORPHIC_STREAKS + #define STREAK_SHIFT (STREAK_CA * BUFFER_PIXEL_SIZE.x) + + #if COLOR_FRINGING + #define SAMPLE_STREAK_TEX(s, uv, o) float3( \ + tex2D(s, uv + float2(o - STREAK_SHIFT, 0.0)).r, \ + tex2D(s, uv + float2(o, 0.0)).g, \ + tex2D(s, uv + float2(o + STREAK_SHIFT, 0.0)).b \ + ) + #else + #define SAMPLE_STREAK_TEX(s, uv, o) tex2D(s, uv + float2(o, 0.0)).rgb + #endif +#endif + +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ +texture2D tUnpackedColor { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler2D sUnpackedColor { Texture = tUnpackedColor; }; + +#if ANAMORPHIC_BLOOM + texture2D tBloomDown0 { Width = BUFFER_WIDTH/2; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sBloomDown0 { Texture = tBloomDown0; }; + + texture2D tBloomDown1 { Width = BUFFER_WIDTH/4; Height = BUFFER_HEIGHT/4; Format = RGBA16F; }; + sampler2D sBloomDown1 { Texture = tBloomDown1; }; + + texture2D tBloomDown2 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RGBA16F; }; + sampler2D sBloomDown2 { Texture = tBloomDown2; }; + + texture2D tBloomDown3 { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = RGBA16F; }; + sampler2D sBloomDown3 { Texture = tBloomDown3; }; + + texture2D tBloomDown4 { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/32; Format = RGBA16F; }; + sampler2D sBloomDown4 { Texture = tBloomDown4; }; + + texture2D tBloomUp3 { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = RGBA16F; }; + sampler2D sBloomUp3 { Texture = tBloomUp3; }; + + texture2D tBloomUp2 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RGBA16F; }; + sampler2D sBloomUp2 { Texture = tBloomUp2; }; + + texture2D tBloomUp1 { Width = BUFFER_WIDTH/4; Height = BUFFER_HEIGHT/4; Format = RGBA16F; }; + sampler2D sBloomUp1 { Texture = tBloomUp1; }; + + texture2D tBloomUp0 { Width = BUFFER_WIDTH/2; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sBloomUp0 { Texture = tBloomUp0; }; + + texture2D tBloomUp4 { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; + sampler2D sBloomUp4 { Texture = tBloomUp4; }; +#endif + +#if ANAMORPHIC_STREAKS + texture2D tStreakDown0 { Width = BUFFER_WIDTH/2; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakDown0 { Texture = tStreakDown0; }; + + texture2D tStreakDown1 { Width = BUFFER_WIDTH/4; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakDown1 { Texture = tStreakDown1; }; + + texture2D tStreakDown2 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakDown2 { Texture = tStreakDown2; }; + + texture2D tStreakDown3 { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakDown3 { Texture = tStreakDown3; }; + + texture2D tStreakDown4 { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakDown4 { Texture = tStreakDown4; }; + + texture2D tStreakUp3 { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakUp3 { Texture = tStreakUp3; }; + + texture2D tStreakUp2 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakUp2 { Texture = tStreakUp2; }; + + texture2D tStreakUp1 { Width = BUFFER_WIDTH/4; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakUp1 { Texture = tStreakUp1; }; + + texture2D tStreakUp0 { Width = BUFFER_WIDTH/2; Height = BUFFER_HEIGHT/2; Format = RGBA16F; }; + sampler2D sStreakUp0 { Texture = tStreakUp0; }; +#endif + +/*--------------. +| :: HELPERS :: | +'--------------*/ +#if ANAMORPHIC_BLOOM + float3 TentFilter13Anisotropic(sampler2D src, float2 uv, float2 radius) + { + float dx = radius.x; + float dy = radius.y; + + [branch] if (BLOOM_SHARP) + { + float3 center = SAMPLE_BLOOM_TEX(src, uv); + float3 innerLeft = SAMPLE_BLOOM_TEX(src, uv + float2(-dx, 0)); + float3 innerRight = SAMPLE_BLOOM_TEX(src, uv + float2( dx, 0)); + float3 outerLeft = SAMPLE_BLOOM_TEX(src, uv + float2(-2*dx, 0)); + float3 outerRight = SAMPLE_BLOOM_TEX(src, uv + float2( 2*dx, 0)); + return center * 0.25 + (innerLeft + innerRight) * 0.25 + (outerLeft + outerRight) * 0.125; + } + + float3 a = SAMPLE_BLOOM_TEX(src, uv + float2(-2*dx, 2*dy)).rgb; + float3 b = SAMPLE_BLOOM_TEX(src, uv + float2( 0, 2*dy)).rgb; + float3 c = SAMPLE_BLOOM_TEX(src, uv + float2( 2*dx, 2*dy)).rgb; + float3 d = SAMPLE_BLOOM_TEX(src, uv + float2(-2*dx, 0)).rgb; + float3 e = SAMPLE_BLOOM_TEX(src, uv + float2( 0, 0)).rgb; + float3 f = SAMPLE_BLOOM_TEX(src, uv + float2( 2*dx, 0)).rgb; + float3 g = SAMPLE_BLOOM_TEX(src, uv + float2(-2*dx, -2*dy)).rgb; + float3 h = SAMPLE_BLOOM_TEX(src, uv + float2( 0, -2*dy)).rgb; + float3 i = SAMPLE_BLOOM_TEX(src, uv + float2( 2*dx, -2*dy)).rgb; + float3 j = SAMPLE_BLOOM_TEX(src, uv + float2(-dx, dy)).rgb; + float3 k = SAMPLE_BLOOM_TEX(src, uv + float2( dx, dy)).rgb; + float3 l = SAMPLE_BLOOM_TEX(src, uv + float2(-dx, -dy)).rgb; + float3 m = SAMPLE_BLOOM_TEX(src, uv + float2( dx, -dy)).rgb; + + return e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125; + } + + float3 TentFilter9Anisotropic(sampler2D src, float2 uv, float2 radius) + { + float dx = radius.x; + float dy = radius.y; + + [branch] if (BLOOM_SHARP) + { + float3 center = SAMPLE_BLOOM_TEX(src, uv); + float3 left = SAMPLE_BLOOM_TEX(src, uv + float2(-dx, 0)); + float3 right = SAMPLE_BLOOM_TEX(src, uv + float2( dx, 0)); + return center * 0.5 + (left + right) * 0.25; + } + + float3 a = SAMPLE_BLOOM_TEX(src, uv + float2(-dx, dy)).rgb; + float3 b = SAMPLE_BLOOM_TEX(src, uv + float2( 0, dy)).rgb; + float3 c = SAMPLE_BLOOM_TEX(src, uv + float2( dx, dy)).rgb; + float3 d = SAMPLE_BLOOM_TEX(src, uv + float2(-dx, 0)).rgb; + float3 e = SAMPLE_BLOOM_TEX(src, uv + float2( 0, 0)).rgb; + float3 f = SAMPLE_BLOOM_TEX(src, uv + float2( dx, 0)).rgb; + float3 g = SAMPLE_BLOOM_TEX(src, uv + float2(-dx, -dy)).rgb; + float3 h = SAMPLE_BLOOM_TEX(src, uv + float2( 0, -dy)).rgb; + float3 i = SAMPLE_BLOOM_TEX(src, uv + float2( dx, -dy)).rgb; + + return (e * 4.0 + (b + d + f + h) * 2.0 + (a + c + g + i)) * 0.0625; + } +#endif + +#if ANAMORPHIC_STREAKS + float3 StreakFilter(sampler2D src, float2 uv, float radius) + { + float dx = BUFFER_PIXEL_SIZE.x * radius; + return SAMPLE_STREAK_TEX(src, uv, -dx * 2.0) * 0.1 + + SAMPLE_STREAK_TEX(src, uv, -dx) * 0.25 + + SAMPLE_STREAK_TEX(src, uv, 0.0) * 0.3 + + SAMPLE_STREAK_TEX(src, uv, dx) * 0.25 + + SAMPLE_STREAK_TEX(src, uv, dx * 2.0) * 0.1; + } +#endif + +/*--------------. +| :: SHADERS :: | +'--------------*/ +float4 PS_StoreUnpackedColor(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return float4(GetLinearColor(uv, false), 1); +} + +#if ANAMORPHIC_BLOOM + //downsample with anisotropic blur (13-tap) + float4 PS_BloomDownsample0(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 1.0) * BUFFER_PIXEL_SIZE; + float3 downsample = TentFilter13Anisotropic(sUnpackedColor, uv, radius); + if (BLOOM_SKIP_SKYBOX) downsample *= (GetDepth(uv) < 1.0); + downsample = downsample * smoothstep(0.0, max(1.0 - BLOOM_THRESHOLD, 0.07)*BLOOM_THRESHOLD_SCALER, GetLuminance(downsample)); + return float4(downsample, 1); + } + + float4 PS_BloomDownsample1(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 1.0) * BUFFER_PIXEL_SIZE * 2.0; + return float4(TentFilter13Anisotropic(sBloomDown0, uv, radius), 1); + } + + float4 PS_BloomDownsample2(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 1.0) * BUFFER_PIXEL_SIZE * 4.0; + return float4(TentFilter13Anisotropic(sBloomDown1, uv, radius), 1); + } + + float4 PS_BloomDownsample3(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 1.0) * BUFFER_PIXEL_SIZE * 8.0; + return float4(TentFilter13Anisotropic(sBloomDown2, uv, radius), 1); + } + + float4 PS_BloomDownsample4(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 1.0) * BUFFER_PIXEL_SIZE * 16.0; + return float4(TentFilter13Anisotropic(sBloomDown3, uv, radius), 1); + } + + //upsample with anisotropic blur (9-tap) + float4 PS_BloomUpsample0(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 0.0) * BUFFER_PIXEL_SIZE * 32.0 * float2(1.0, rcp(BUFFER_ASPECT_RATIO)); + float3 upsample = TentFilter9Anisotropic(sBloomDown4, uv, radius); + float3 previous = tex2D(sBloomDown3, uv).rgb; + return float4(upsample + previous, 1); + } + + float4 PS_BloomUpsample1(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 0.0) * BUFFER_PIXEL_SIZE * 16.0 * float2(1.0, rcp(BUFFER_ASPECT_RATIO)); + float3 upsample = TentFilter9Anisotropic(sBloomUp3, uv, radius); + float3 previous = tex2D(sBloomDown2, uv).rgb; + return float4(upsample + previous, 1); + } + + float4 PS_BloomUpsample2(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 0.0) * BUFFER_PIXEL_SIZE * 8.0 * float2(1.0, rcp(BUFFER_ASPECT_RATIO)); + float3 upsample = TentFilter9Anisotropic(sBloomUp2, uv, radius); + float3 previous = tex2D(sBloomDown1, uv).rgb; + return float4(upsample + previous, 1); + } + + float4 PS_BloomUpsample3(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 0.0) * BUFFER_PIXEL_SIZE * 4.0 * float2(1.0, rcp(BUFFER_ASPECT_RATIO)); + float3 upsample = TentFilter9Anisotropic(sBloomUp1, uv, radius); + float3 previous = tex2D(sBloomDown0, uv).rgb; + return float4(upsample + previous, 1); + } + + float4 PS_BloomUpsample4(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float2 radius = float2(BLOOM_STRETCH, 0.0) * BUFFER_PIXEL_SIZE * 2.0 * float2(1.0, rcp(BUFFER_ASPECT_RATIO)); + float3 upsample = TentFilter9Anisotropic(sBloomUp0, uv, radius); + float3 previous = tex2D(sBloomDown0, uv).rgb; + return float4(upsample + previous, 1); + } +#endif + +#if ANAMORPHIC_STREAKS + //thresholding pass + float4 PS_Prefilter(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target + { + float3 color = tex2D(sUnpackedColor, uv).rgb; + if (STREAK_SKIP_SKYBOX) color *= (GetDepth(uv) < 1.0); + float br = max(color.r, max(color.g, color.b)); + float nm = max(0.0, br - (1.0 - STREAK_THRESHOLD)); + return float4(color * (nm / max(br, 0.0001)), 1.0); + } + + float4 PS_StreakDownsample0(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakDown0, uv, 1.0 * STREAK_STRETCH), 1); } + float4 PS_StreakDownsample1(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakDown1, uv, 2.0 * STREAK_STRETCH), 1); } + float4 PS_StreakDownsample2(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakDown2, uv, 4.0 * STREAK_STRETCH), 1); } + float4 PS_StreakDownsample3(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakDown3, uv, 8.0 * STREAK_STRETCH), 1); } + + float4 PS_StreakUpsample0(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakUp3, uv, 8.0 * STREAK_STRETCH) + tex2D(sStreakDown2, uv).rgb, 1); } + float4 PS_StreakUpsample1(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakUp2, uv, 4.0 * STREAK_STRETCH) + tex2D(sStreakDown1, uv).rgb, 1); } + float4 PS_StreakUpsample2(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakUp1, uv, 2.0 * STREAK_STRETCH) + tex2D(sStreakDown0, uv).rgb, 1); } + float4 PS_StreakUpsample3(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target { return float4(StreakFilter(sStreakDown4, uv, 16.0 * STREAK_STRETCH) + tex2D(sStreakDown3, uv).rgb, 1); } +#endif + +float4 PS_ToDisplay(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 unpackedColor = tex2D(sUnpackedColor, uv).rgb; + float3 light = 0; + + #if ANAMORPHIC_BLOOM + light = tex2D(sBloomUp4, uv).rgb * BLOOM_INTENSITY; + #endif + + #if ANAMORPHIC_STREAKS + light = max(light, tex2D(sStreakUp0, uv).rgb * STREAK_TINT * STREAK_INTENSITY); + #endif + + float3 toDisplay; + #if (BUFFER_COLOR_SPACE == 1) + //sRGB colorspace + toDisplay = 1.0 - (1.0 - unpackedColor) * (1.0 - light); + #else + toDisplay = unpackedColor + light; + #endif + + toDisplay = ToOutputColorspace(toDisplay, false); + return float4(toDisplay, 1); +} + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ +technique Lumenite_AnamorphicBloom < + ui_label = "LUMENITE: AnamorphicBloom"; + ui_tooltip = "Artistic bloom & Lens Flare approximating the Anamorphic lens aesthetic."; +> +{ + pass { VertexShader = PostProcessVS; PixelShader = PS_StoreUnpackedColor; RenderTarget = tUnpackedColor; } + + //bloom pyramid + #if ANAMORPHIC_BLOOM + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomDownsample0; RenderTarget = tBloomDown0; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomDownsample1; RenderTarget = tBloomDown1; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomDownsample2; RenderTarget = tBloomDown2; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomDownsample3; RenderTarget = tBloomDown3; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomDownsample4; RenderTarget = tBloomDown4; } + + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomUpsample0; RenderTarget = tBloomUp3; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomUpsample1; RenderTarget = tBloomUp2; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomUpsample2; RenderTarget = tBloomUp1; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomUpsample3; RenderTarget = tBloomUp0; } + pass { VertexShader = PostProcessVS; PixelShader = PS_BloomUpsample4; RenderTarget = tBloomUp4; } + #endif + + //streak pyramid + #if ANAMORPHIC_STREAKS + pass { VertexShader = PostProcessVS; PixelShader = PS_Prefilter; RenderTarget = tStreakDown0; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakDownsample0; RenderTarget = tStreakDown1; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakDownsample1; RenderTarget = tStreakDown2; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakDownsample2; RenderTarget = tStreakDown3; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakDownsample3; RenderTarget = tStreakDown4; } + + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakUpsample3; RenderTarget = tStreakUp3; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakUpsample0; RenderTarget = tStreakUp2; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakUpsample1; RenderTarget = tStreakUp1; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StreakUpsample2; RenderTarget = tStreakUp0; } + #endif + + pass { VertexShader = PostProcessVS; PixelShader = PS_ToDisplay; } +} + +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_Kernel.fx b/EXE/reshade-shaders/Shaders/lumenite_Kernel.fx new file mode 100644 index 00000000..3ee4cb4a --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_Kernel.fx @@ -0,0 +1,753 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_Kernel.fx + Version : 2026.07.28 + Author : Afzaal (Kaidō) + Description: Pre-effect for various LumeniteFX shaders. + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define FOV 60.0 +#define NEAR_PLANE 0.01 + +#ifndef IMAGE_SPACE + #define IMAGE_SPACE 0 +#endif + +#ifndef DEBUG_KERNEL + #define DEBUG_KERNEL 0 +#endif + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" +#if DEBUG_KERNEL + #include "DrawText.fxh" +#endif +#include "./include/lumenite_Projections.fxh" +#include "./include/lumenite_Helpers.fxh" +#include "./include/lumenite_Compute.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +#if DEBUG_KERNEL +uniform int DEBUG_VIEW < + ui_type = "combo"; + ui_items = "Split View\0" + "Normals/Depth\0" + "Optical Flow\0" + "Motion Vectors\0" + "Motion Confidence\0" + ; + ui_label = "Debug View"; + ui_category = "Kernel"; +> = 0; +#endif + +namespace Kernel { + +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ + +texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; +sampler2D sFlow { Texture = tFlow; MagFilter = POINT; MinFilter = POINT; }; + +texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; +sampler2D sConfidence { Texture = tConfidence; }; + +texture tNormals { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; MipLevels = 4; }; +sampler sNormals { Texture = tNormals; }; + +texture2D tDepth { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 4; }; +sampler2D sDepth { Texture = tDepth; }; + +texture2D tCurrLuma { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 8; }; +sampler2D sCurrLuma { Texture = tCurrLuma; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tPrevLuma { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 8; }; +sampler2D sPrevLuma { Texture = tPrevLuma; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow128 { Width = BUFFER_WIDTH/128; Height = BUFFER_HEIGHT/128; Format = RG16F; }; +sampler2D sFlow128 { Texture = tFlow128; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow64A { Width = BUFFER_WIDTH/64; Height = BUFFER_HEIGHT/64; Format = RG16F; }; +sampler2D sFlow64A { Texture = tFlow64A; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; +texture2D tFlow64B { Width = BUFFER_WIDTH/64; Height = BUFFER_HEIGHT/64; Format = RG16F; }; +sampler2D sFlow64B { Texture = tFlow64B; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow32A { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/32; Format = RG16F; }; +sampler2D sFlow32A { Texture = tFlow32A; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; +texture2D tFlow32B { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/32; Format = RG16F; }; +sampler2D sFlow32B { Texture = tFlow32B; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow16A { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = RG16F; }; +sampler2D sFlow16A { Texture = tFlow16A; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; +texture2D tFlow16B { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = RG16F; }; +sampler2D sFlow16B { Texture = tFlow16B; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow8 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; +sampler2D sFlow8 { Texture = tFlow8; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tPrevFrameFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; +sampler2D sPrevFrameFlow { Texture = tPrevFrameFlow; MagFilter = POINT; MinFilter = POINT; }; + +texture2D tPrevConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; +sampler2D sPrevConfidence { Texture = tPrevConfidence; }; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +float3 GetColor(float2 uv) +{ + return tex2Dlod(ReShade::BackBuffer, float4(uv, 0, 0)).rgb; +} + +float3 DepthGradient(float t, float2 uv) +{ + //grayscale: close=dark, far=bright + float3 depth = saturate(t).xxx; + const float ditherBit = 8.0; + float gridPos = frac(dot(uv, (BUFFER_SCREEN_SIZE * float2(1.0 / 16.0, 10.0 / 36.0)) + 0.25)); + float ditherShift = 0.25 * (1.0 / (pow(2.0, ditherBit) - 1.0)); + float3 ditherShiftRGB = float3(ditherShift, -ditherShift, ditherShift); //subpixel dithering + ditherShiftRGB = lerp(2.0 * ditherShiftRGB, -2.0 * ditherShiftRGB, gridPos); + return depth + ditherShiftRGB; +} + +float3 MotionToColor(float2 motion) +{ + float angle = atan2(-motion.y, -motion.x) / 6.283 + 0.5; + float rawLength = length(motion) / (15.0 * BUFFER_PIXEL_SIZE.x); + float compressed = rawLength / (1.0 + rawLength * 1.4); //asymptotic squash + float boosted = pow(compressed, 0.5); //lift shadows + float magnitude = saturate(lerp(compressed, boosted, saturate(rawLength * 3.0))); + float3 hsv = float3(angle, 1, magnitude); + float4 K = float4(1, 2/3.0, 1/3.0, 3); + float3 p = abs(frac(hsv.xxx + K.xyz) * 6 - K.www); + return hsv.z * lerp(K.xxx, clamp(p - K.xxx, 0, 1), hsv.y) + 0.1; +} + +float SegmentDist(float2 p, float2 a, float2 b) //anti-aliased distance from point p to segment a-b +{ + float2 pa = p - a; + float2 ba = b - a; + float h = saturate(dot(pa, ba) / (dot(ba, ba) + EPSILON)); + return length(pa - ba * h); +} + +float4 DrawMotionVectors(float2 uv) +{ + static const int GATHER = 2; //cell radius searched (5x5); always MAX_LENGTH <= GATHER*GRID_SPACING + static const float GRID_SPACING = 16.0; //px between grid nodes + static const float DOT_RADIUS = 2.0; //px radius of node dots + static const float GRID_OPACITY = 0.20; //0..1 lattice visibility + static const float3 GRID_TINT = float3(0.55, 0.55, 0.60); + + static const float SHAFT_THICKNESS = 1.5; //px half-width of shaft (larger) + static const float HEAD_LENGTH = 6.0; //px length of arrowhead (larger) + static const float HEAD_HALF_WIDTH = 4.0; //px half-width of head base (larger) + static const float MIN_LENGTH = 7.0; //px shortest arrow + static const float MAX_LENGTH = 30.0; //px longest arrow (<= GATHER*GRID_SPACING) + static const float LENGTH_SCALE = 2.5; //arrow px per motion px (elongation gain) + static const float AA = 0.9; //px edge softness + + float3 baseColor = GetColor(uv); + float2 pixelPos = uv * BUFFER_SCREEN_SIZE; + + //dotted grid + float2 g = pixelPos / GRID_SPACING; + float2 nearest = round(g) * GRID_SPACING; //nearest node centre, px + float dDot = length(pixelPos - nearest); //px distance to that node + float gridCov = (1.0 - smoothstep(DOT_RADIUS - AA, DOT_RADIUS + AA, dDot)) * GRID_OPACITY; + + float bestCov = 0.0; + float3 bestColor = float3(0.0, 0.0, 0.0); + + //union of arrows from the (2*GATHER+1)^2 nearest nodes (roots on grid crossings) + float2 baseNode = round(g); + [unroll] for (int ny = -GATHER; ny <= GATHER; ny++) + [unroll] for (int nx = -GATHER; nx <= GATHER; nx++) + { + float2 rootPx = (baseNode + float2(nx, ny)) * GRID_SPACING; //node sits on a crossing + float2 rootUV = rootPx * BUFFER_PIXEL_SIZE; + + float2 motion = tex2Dlod(sFlow, float4(rootUV, 0, 0)).xy; + float2 motionPx = motion * BUFFER_SCREEN_SIZE; + float magPx = length(motionPx); + bool valid = (magPx >= 0.4) && (tex2Dlod(sDepth, float4(rootUV, 0, 0)).r < 0.999); + + float len = clamp(magPx * LENGTH_SCALE, MIN_LENGTH, MAX_LENGTH); //elongates with this node's motion + float2 fwd = -motionPx / (magPx + EPSILON); //negate for forward motion + float2 tip = rootPx + fwd * len; + float2 perp = float2(-fwd.y, fwd.x); + + //shaft + float2 shaftEnd = rootPx + fwd * max(len - HEAD_LENGTH, 0.0); + float dShaft = SegmentDist(pixelPos, rootPx, shaftEnd); + float covShaft = 1.0 - smoothstep(SHAFT_THICKNESS - AA, SHAFT_THICKNESS + AA, dShaft); + + //head + float2 toTip = pixelPos - tip; + float along = dot(toTip, -fwd); + float side = abs(dot(toTip, perp)); + float halfW = HEAD_HALF_WIDTH * saturate(along / HEAD_LENGTH); + float covAlong = smoothstep(-AA, AA, along) * (1.0 - smoothstep(HEAD_LENGTH - AA, HEAD_LENGTH + AA, along)); + float covHead = covAlong * (1.0 - smoothstep(halfW - AA, halfW + AA, side)); + + float cov = max(covShaft, covHead) * (valid ? 1.0 : 0.0); + if (cov > bestCov) { bestCov = cov; bestColor = MotionToColor(motion); } + } + + float3 outColor = lerp(baseColor, GRID_TINT, gridCov); //lattice underneath + outColor = lerp(outColor, bestColor, bestCov); //arrows on top + return float4(outColor, 1.0); +} + +float ZMSAD(sampler2D currLumaSrc, sampler2D prevLumaSrc, float2 posA, float2 posB, float2 texelSize, uint mip) +{ + static const int2 offsets[9] = { + int2(0, 3), + int2(0, 1), + int2(-3,0), int2(-1,0), int2(0, 0), int2(1,0), int2(3,0), + int2(0,-1), + int2(0,-3) + }; + + //gather samples and calculate the mean for each patch + float samplesA[9], samplesB[9]; + float meanA = 0.0, meanB = 0.0; + + [unroll] for(int i = 0; i < 9; i++) { + float2 offset = float2(offsets[i]) * texelSize; + samplesA[i] = tex2Dlod(currLumaSrc, float4(posA + offset, 0, mip)).r; + samplesB[i] = tex2Dlod(prevLumaSrc, float4(posB + offset, 0, mip)).r; + meanA += samplesA[i]; + meanB += samplesB[i]; + } + meanA /= 9.0; + meanB /= 9.0; + + //SAD on the normalized samples + float err = 0.0; + [unroll] for(int i = 0; i < 9; i++) + err += abs((samplesA[i] - meanA) - (samplesB[i] - meanB)); + + return ((err / 9.0) + EPSILON); +} + +float2 Median9(sampler2D flowSrc, float2 uv, float2 texelSize, uint mip) +{ + float2 v[9]; + int idx = 0; + [unroll] for(int dy = -1; dy <= 1; dy++) for(int dx = -1; dx <= 1; dx++) + v[idx++] = tex2Dlod(flowSrc, float4(uv + float2(dx, dy) * texelSize, 0, mip)).xy; + + //bubble sort ensures the Median lands in v[4], only needs 5 passes + //indices 4,5,6,7,8 contain the 5 largest items, so v[4] is the median + [unroll] for(int k = 0; k < 5; k++) for(int i = 0; i < 8 - k; i++) { //checks decrease as right side gets sorted + float2 a = v[i]; + float2 b = v[i+1]; + v[i] = min(a, b); + v[i+1] = max(a, b); + } + + return v[4]; +} + +float2 BilateralMedian9(sampler2D flowSrc, float2 uv, float2 texelSize, uint mip) +{ + static const int2 DENSE_3X3[9] = { + int2(-1,-1), int2(0,-1), int2(1,-1), + int2(-1, 0), int2(0, 0), int2(1, 0), + int2(-1, 1), int2(0, 1), int2(1, 1) + }; + float lumaC = tex2Dlod(sCurrLuma, float4(uv, 0, mip)).x; + float lumaW = tex2Dlod(sCurrLuma, float4(uv + float2(-1.0, 0.0) * texelSize, 0, mip)).x; + float lumaE = tex2Dlod(sCurrLuma, float4(uv + float2( 1.0, 0.0) * texelSize, 0, mip)).x; + float lumaN = tex2Dlod(sCurrLuma, float4(uv + float2( 0.0,-1.0) * texelSize, 0, mip)).x; + float lumaS = tex2Dlod(sCurrLuma, float4(uv + float2( 0.0, 1.0) * texelSize, 0, mip)).x; + //central-difference gradient, wider baseline than quad ddx/ddy, derived from real samples + float dxLuma = (lumaE - lumaW) * 0.5; + float dyLuma = (lumaS - lumaN) * 0.5; + float2 v[9]; + uint validCount = 0; + [unroll] for (int i = 0; i < 9; i++) { + int2 off = DENSE_3X3[i]; + float2 sampleUV = uv + float2(off) * texelSize; + //cardinals + center use sampled luma; diagonals get linear prediction + float sampleLuma = lumaC; //covers (0,0) + if (off.x == -1 && off.y == 0) sampleLuma = lumaW; + else if (off.x == 1 && off.y == 0) sampleLuma = lumaE; + else if (off.x == 0 && off.y == -1) sampleLuma = lumaN; + else if (off.x == 0 && off.y == 1) sampleLuma = lumaS; + else if (off.x != 0 && off.y != 0) sampleLuma = lumaC + float(off.x) * dxLuma + float(off.y) * dyLuma; + bool isValid = abs(lumaC - sampleLuma) <= 0.05; + v[i] = isValid ? tex2Dlod(flowSrc, float4(sampleUV, 0, 0)).xy : float2(1e38, 1e38); + validCount += uint(isValid); + } + if(validCount < 3u) return v[4]; + //right-to-left bubble: smallest reaches v[0] per pass; after 5 passes, v[0..4] sorted ascending + [unroll] for(int k = 0; k < 5; k++) for(int j = 7; j >= k; j--) { + float2 a = v[j]; + float2 b = v[j+1]; + v[j] = min(a, b); + v[j+1] = max(a, b); + } + uint medianIdx = validCount / 2u; + float2 result = v[1]; //fallback for validCount == 3 (medianIdx 1) + if (medianIdx == 2u) result = v[2]; + if (medianIdx == 3u) result = v[3]; + if (medianIdx == 4u) result = v[4]; + return result; +} + +float2 ATrousFilter(sampler2D motionSrc, float2 uv, uint dilation, uint mip) +{ + static const int2 offsets[8] = { int2(-1,-1), int2(0,-1), int2(1,-1), + int2(-1, 0), int2(1, 0), + int2(-1, 1), int2(0, 1), int2(1, 1) }; + float centerLuma = tex2Dlod(sCurrLuma, float4(uv, 0, mip)).r; + #if IMAGE_SPACE == 0 + float centerDepth = tex2Dlod(sDepth, float4(uv, 0, mip)).r; + #endif + float2 centerFlow = tex2Dlod(motionSrc, float4(uv, 0, 0)).xy; + float centerConf = max(tex2Dlod(sConfidence, float4(uv, 0, 0)).r, 0.01); //0.01 floor prevents NaN if conf hits 0 + float2 sum = centerFlow * centerConf; + float totalWeight = centerConf; + [unroll] for (int i = 0; i < 8; i++) { + float2 sampleUV = uv + float2(offsets[i]) * dilation * BUFFER_PIXEL_SIZE * 8.0; //*8 = stride of flow grid + float2 sampleFlow = tex2Dlod(motionSrc, float4(sampleUV, 0, 0)).xy; + + float sampleConf = tex2Dlod(sConfidence, float4(sampleUV, 0, 0)).r; + float confWeight = pow(sampleConf, 3.0); + + float discontinuityGate; + #if IMAGE_SPACE == 0 + float sampleDepth = tex2Dlod(sDepth, float4(sampleUV, 0, mip)).r; + float absDepthDiff = abs(centerDepth - sampleDepth); + float depthWeight = (absDepthDiff < 0.003) ? 1.0 : 0.0; + discontinuityGate = depthWeight; + #else + float2 flowDeltaPx = (sampleFlow - centerFlow) * BUFFER_SCREEN_SIZE; //measure flow disagreement in full-res px + float rawMotionGate = exp2(-dot(flowDeltaPx, flowDeltaPx) / (0.01 + EPSILON)); + float motionGate = lerp(1.0, rawMotionGate, saturate(centerConf)); //if center flow is unreliable; relax gate so confident neighbors repair it + discontinuityGate = motionGate; + #endif + + float sampleLuma = tex2Dlod(sCurrLuma, float4(sampleUV, 0, mip)).r; + float absLumaDiff = abs(centerLuma - sampleLuma); + float lumaWeight = saturate(1.0 - absLumaDiff * 10.0); //10.0: scale, 4.0: sharpness + + float weight = confWeight * lumaWeight * discontinuityGate; + sum += sampleFlow * weight; + totalWeight += weight; + } + return sum / (totalWeight + EPSILON); +} + +float2 UpscaleFlow(sampler2D coarseSrc, sampler2D currLumaSrc, sampler2D prevLumaSrc, float2 uv, float2 texelSize, uint mip) +{ + if(FRAME_COUNT == 0) return float2(0, 0); + + float2 coarseTexelSize = rcp(float2(tex2Dsize(coarseSrc, 0))); + //pool candidates for tournament selection. order matters here + float2 candidates[10]; + candidates[0] = tex2D(coarseSrc, uv).xy ; + candidates[1] = tex2D(coarseSrc, uv + float2(0, -coarseTexelSize.y)).xy ; + candidates[2] = tex2D(coarseSrc, uv + float2(0, coarseTexelSize.y)).xy ; + candidates[3] = tex2D(coarseSrc, uv - float2(coarseTexelSize.x, 0)).xy ; + candidates[4] = tex2D(coarseSrc, uv + float2(coarseTexelSize.x, 0)).xy ; + candidates[5] = tex2D(coarseSrc, uv + float2(-coarseTexelSize.x, -coarseTexelSize.y)).xy ; + candidates[6] = tex2D(coarseSrc, uv + float2( coarseTexelSize.x, -coarseTexelSize.y)).xy ; + candidates[7] = tex2D(coarseSrc, uv + float2(-coarseTexelSize.x, coarseTexelSize.y)).xy ; + candidates[8] = tex2D(coarseSrc, uv + float2(coarseTexelSize.x, coarseTexelSize.y)).xy ; + candidates[9] = tex2D(sPrevFrameFlow, uv).xy; + + float minCost = 1e6; + float2 prediction = candidates[0]; + [loop] for (int i = 0; i < 10; i++) { + float cost = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + candidates[i], texelSize, mip); + if (cost < minCost) { + minCost = cost; + prediction = candidates[i]; + } + } + + //refinement with parabolic fitting + float costLeft = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction - float2(texelSize.x, 0), texelSize, mip); + float costRight = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction + float2(texelSize.x, 0), texelSize, mip); + float costDown = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction - float2(0, texelSize.y), texelSize, mip); + float costUp = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction + float2(0, texelSize.y), texelSize, mip); + //sub-pixel offset (parabolic fitting) + float2 subpixelOffset; + subpixelOffset.x = (costLeft - costRight) / (4.0 * (costLeft + costRight - 2.0 * minCost) + EPSILON); //EPSILON for flat surface handling + subpixelOffset.y = (costDown - costUp) / (4.0 * (costDown + costUp - 2.0 * minCost) + EPSILON); + //clamp offset to a reasonable range + subpixelOffset = clamp(subpixelOffset, -0.5, 0.5); + + return (prediction+subpixelOffset*texelSize); +} + +/*--------------. +| :: SHADERS :: | +'--------------*/ +void PS_ReconstructNormals(VSOUT input, out float4 gbuffer : SV_Target0, out float depthC : SV_Target1) +{ + depthC = GetDepth(input.uv); + + const float2 offsetX = float2(BUFFER_PIXEL_SIZE.x, 0); + const float2 offsetY = float2(0, BUFFER_PIXEL_SIZE.y); + + float3 pC = UVToViewSpace(input.uv, depthC, input); + float3 pL = UVToViewSpace(input.uv - offsetX, GetDepth(input.uv - offsetX), input); + float3 pR = UVToViewSpace(input.uv + offsetX, GetDepth(input.uv + offsetX), input); + float3 pT = UVToViewSpace(input.uv - offsetY, GetDepth(input.uv - offsetY), input); + float3 pB = UVToViewSpace(input.uv + offsetY, GetDepth(input.uv + offsetY), input); + + float3 diffX2 = pR - pC; + float3 diffX1 = pC - pL; + float3 diffY2 = pB - pC; + float3 diffY1 = pC - pT; + + float lenSqX2 = dot(diffX2, diffX2); + float lenSqX1 = dot(diffX1, diffX1); + float lenSqY2 = dot(diffY2, diffY2); + float lenSqY1 = dot(diffY1, diffY1); + + float3 ddx = lenSqX2 < lenSqX1 ? diffX2 : diffX1; + float3 ddy = lenSqY2 < lenSqY1 ? diffY2 : diffY1; + float3 geoNormal = normalize(cross(ddx, ddy)); + gbuffer = float4(geoNormal, depthC); +} + +float PS_PackFeatures(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 color = GetColor(uv); + float luma = dot(color, float3(0.2126, 0.7152, 0.0722)); + return luma * rcp(1.0 + luma); +} + +float2 PS_ComputeFlow128(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + if(FRAME_COUNT == 0) return float2(0, 0); + + static const int SEARCH_RADIUS = 3; + static const uint mip = 5; + float2 texelSize = BUFFER_PIXEL_SIZE * exp2(mip); + + //candidate seeds for the coarsest level for tournament selection + float2 prevSeed = tex2D(sPrevFrameFlow, uv).xy; + float2 zeroSeed = float2(0, 0); + float prevCost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv + prevSeed, texelSize, mip); + float zeroCost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv + zeroSeed, texelSize, mip); + + float2 seed = (zeroCost < prevCost) ? zeroSeed : prevSeed; //pick better candidate as seed + float2 bestFlow = seed; + float minCost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv+seed, texelSize, mip); + //search in a grid AROUND the seed + for (int y = -SEARCH_RADIUS; y <= SEARCH_RADIUS; ++y) for (int x = -SEARCH_RADIUS; x <= SEARCH_RADIUS; ++x) { + if (x == 0 && y == 0) continue; + float2 candidateFlow = seed + float2(x, y) * texelSize; + float cost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv + candidateFlow, texelSize, mip); + if (cost < minCost) { + minCost = cost; + bestFlow = candidateFlow; + if (minCost < 0.01) //near-perfect match found + return bestFlow; + } + } + return bestFlow; +} + +float2 PS_UpscaleFlow64(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow128, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*16.0, 4); +} + +float2 PS_MedianPass64(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return Median9(sFlow64A, uv, BUFFER_PIXEL_SIZE*64.0, 6); +} + +float2 PS_UpscaleFlow32(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow64B, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*8.0, 3); +} + +float2 PS_MedianPass32(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return Median9(sFlow32A, uv, BUFFER_PIXEL_SIZE*32.0, 5); +} + +float2 PS_UpscaleFlow16(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow32B, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*4.0, 2); +} + +float2 PS_MedianPass16(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return Median9(sFlow16A, uv, BUFFER_PIXEL_SIZE*16.0, 4); +} + +float2 PS_UpscaleFlow8(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow16B, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*2.0, 1); +} + +float2 PS_MedianPass8A(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return BilateralMedian9(sFlow, uv, BUFFER_PIXEL_SIZE*8.0, 3); +} + +float2 PS_MedianPass8B(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return BilateralMedian9(sFlow8, uv, BUFFER_PIXEL_SIZE*8.0, 3); +} + +float2 PS_ATrousPassA(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target //stride 1 +{ + return ATrousFilter(sFlow, uv, 2, 3); +} + +float2 PS_ATrousPassB(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target //stride 2 +{ + float2 flow = ATrousFilter(sFlow8, uv, 4, 1); + //kill sub-pixel noise + float flowPixelMag = length(flow / BUFFER_PIXEL_SIZE); + float gate = saturate(1.0 - pow(1.0 - saturate(saturate(flowPixelMag) - 0.2), 10.0)); //SNAP TO REALITY + return flow*gate; +} + +float PS_Confidence(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + if(FRAME_COUNT == 0) return 0.0; //no confidence + + float2 flow = tex2D(sFlow, uv).xy; + float2 prevUV = uv + flow; //warp prev frame forward + if(IsOOB(prevUV)) return 0.0; + + //look at local contrast for pattern confidence + float sumX = 0, sumX2 = 0, sumY = 0, sumY2 = 0; + float2 lumaTexSize = BUFFER_PIXEL_SIZE * 4.0; + static const float2 offsets[5] = { + float2(0, 1), + float2(-1,0), float2(0, 0), float2(1,0), + float2(0,-1) + }; + [unroll] for(int i = 0; i < 5; i++) { + float valCurr = tex2Dlod(sCurrLuma, float4(uv + offsets[i] * lumaTexSize, 0, 2)).r; + float valPrev = tex2Dlod(sPrevLuma, float4(prevUV + offsets[i] * lumaTexSize, 0, 2)).r; + sumX += valCurr; sumX2 += valCurr * valCurr; + sumY += valPrev; sumY2 += valPrev * valPrev; + } + float varCurr = max(0.0, (sumX2 / 5.0) - (sumX / 5.0 * sumX / 5.0)); + float varPrev = max(0.0, (sumY2 / 5.0) - (sumY / 5.0 * sumY / 5.0)); + float patternConf = 1.0 - saturate(abs(sqrt(varCurr) - sqrt(varPrev)) / (sqrt(varCurr) + 0.01)); + + //look at neighborhood for flow consistency + float flowMagnitude = length(flow); + float2 flowTexelSize = BUFFER_PIXEL_SIZE * 8.0; + float2 flowN = tex2Dlod(sFlow, float4(uv + float2(0, -flowTexelSize.y), 0, 0)).xy; + float2 flowS = tex2Dlod(sFlow, float4(uv + float2(0, flowTexelSize.y), 0, 0)).xy; + float2 flowE = tex2Dlod(sFlow, float4(uv + float2( flowTexelSize.x, 0), 0, 0)).xy; + float2 flowW = tex2Dlod(sFlow, float4(uv + float2(-flowTexelSize.x, 0), 0, 0)).xy; + float2 avgNeighborFlow = (flowN + flowS + flowE + flowW) * 0.25; + float spatialDiff = distance(flow, avgNeighborFlow); + float spatialThreshold = flowMagnitude * 0.5 + BUFFER_PIXEL_SIZE.x; + float spatialConfidence = saturate(1.0 - (spatialDiff / (spatialThreshold + EPSILON))); + + //motion length penalty + float subpixelThreshold = length(BUFFER_PIXEL_SIZE); + float lengthConfidence = (flowMagnitude <= subpixelThreshold) ? 1.0 : rcp((flowMagnitude / subpixelThreshold) * 0.05 + 1.0); + //float panThreshold = BUFFER_PIXEL_SIZE.x * 30.0; + //float lengthConfidence = (flowMagnitude <= panThreshold) ? 1.0 : rcp(((flowMagnitude - panThreshold) / panThreshold) * 0.1 + 1.0); + + //current frame final confidence + float currentConf = spatialConfidence * lengthConfidence * patternConf; + + //temporal filter + float historyConf = tex2D(sPrevConfidence, prevUV).r; + + //DEPRECATED: linear EMA (a=0.15) 15% new + 85% history every frame + //unbiased (settles at the true mean), very stable but distrusts a real drop only as slowly as it trusts a rise + //return lerp(historyConf, currentConf, 0.15); //higher makes it react to changes quickly + + //Asymmetric EMA; a=0.5 only on a genuine drop (>0.05 below history) fast distrust, else a reasonable a=0.1 + //0.05 deadband keeps calm-region jitter on 0.1; only true occlusion/disocclusion bleeds confidence fast + float alpha = (currentConf < historyConf - 0.05) ? 0.5 : 0.1; + return lerp(historyConf, currentConf, alpha); +} + +void PS_StoreFlow(float4 pos : SV_Position, float2 uv : TEXCOORD, out float2 flow : SV_Target0, out float confidence : SV_Target1) +{ + flow = tex2D(sFlow, uv).xy; + confidence = tex2D(sConfidence, uv).r; +} + +float PS_StoreLuma(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return tex2D(sCurrLuma, uv).r; +} + +#if DEBUG_KERNEL +float4 PS_Debug(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 sceneColor = GetColor(uv); + switch(DEBUG_VIEW) + { + case 0: { + static const float LINE_PX = 1.5; //divider half-width, px + static const float3 LINE_TINT = float3(0.0, 0.0, 0.0); + static const float2 BOX_HALF = float2(0.16, 0.18); //centre inset half-extents, uv + + float2 pixelPos = uv * BUFFER_SCREEN_SIZE; + float2 centrePx = BUFFER_SCREEN_SIZE * 0.5; + float2 boxHalfPx = BOX_HALF * BUFFER_SCREEN_SIZE; + + //axis-aligned box distance + float2 dd = abs(pixelPos - centrePx) - boxHalfPx; + float boxSDF = length(max(dd, 0.0)) + min(max(dd.x, dd.y), 0.0); + + float3 view; + if (boxSDF < 0.0) + { + float2 boxUV = (uv - (0.5 - BOX_HALF)) / (2.0 * BOX_HALF); //full frame mapped into inset + view = DrawMotionVectors(boxUV).rgb; //centre: motion vectors + } + else + { + float2 quadUV = frac(uv * 2.0); //flow/confidence remap to full [0,1] frame + if (uv.y < 0.5) + view = (uv.x < 0.5) + ? tex2Dlod(sNormals, float4(uv, 0, 0)).rgb * 0.5 + 0.5 //TL: normals (spatial, raw uv) + : DepthGradient(tex2Dlod(sDepth, float4(uv, 0, 0)).r, uv); //TR: depth (spatial, raw uv) + else if (uv.x < 0.5) + view = MotionToColor(tex2Dlod(sFlow, float4(quadUV, 0, 0)).xy); //BL: optical flow field + else + { + float confidence = tex2Dlod(sConfidence, float4(quadUV, 0, 0)).x; //BR: motion confidence field + float3 confidenceColor = (confidence < 0.5) + ? lerp(float3(1.0, 0.0, 0.0), float3(1.0, 1.0, 0.0), confidence * 2.0) + : lerp(float3(1.0, 1.0, 0.0), float3(0.0, 1.0, 0.0), (confidence - 0.5) * 2.0); + view = lerp(GetColor(quadUV), confidenceColor, 0.9); + } + + //black dividers + float dCross = min(abs(pixelPos.x - centrePx.x), abs(pixelPos.y - centrePx.y)); + view = lerp(view, LINE_TINT, 1.0 - smoothstep(LINE_PX - 0.9, LINE_PX + 0.9, dCross)); + } + + //centre inset border + view = lerp(view, LINE_TINT, 1.0 - smoothstep(LINE_PX - 0.9, LINE_PX + 0.9, abs(boxSDF))); + //window labels + float2 texcoord = uv; //alias: the DrawText macro declares its own internal 'uv' + float labelMask = 0.0; + float labelSize = max(BUFFER_HEIGHT * 0.025, 12.0); //label height, px + int lblNormals[21] = { __R, __e, __c, __o, __n, __s, __t, __r, __u, __c, __t, __e, __d, __Space, __N, __o, __r, __m, __a, __l, __s }; + int lblDepth[16] = { __L, __i, __n, __e, __a, __r, __i, __z, __e, __d, __Space, __D, __e, __p, __t, __h }; + int lblFlow[10] = { __F, __l, __o, __w, __Space, __F, __i, __e, __l, __d }; + int lblConfidence[16] = { __C, __o, __n, __f, __i, __d, __e, __n, __c, __e, __Space, __F, __i, __e, __l, __d }; + int lblVectors[14] = { __M, __o, __t, __i, __o, __n, __Space, __V, __e, __c, __t, __o, __r, __s }; + + labelMask = 0.0; DrawText_String(float2(BUFFER_WIDTH * 0.25 - 21.0 * labelSize * 0.25, BUFFER_HEIGHT * 0.03), labelSize, 1.0, texcoord, lblNormals, 21, labelMask); view = lerp(view, float3(1.00, 1.00, 1.00), saturate(labelMask)); //TL white + labelMask = 0.0; DrawText_String(float2(BUFFER_WIDTH * 0.75 - 16.0 * labelSize * 0.25, BUFFER_HEIGHT * 0.03), labelSize, 1.0, texcoord, lblDepth, 16, labelMask); view = lerp(view, float3(0.55, 0.85, 1.00), saturate(labelMask)); //TR blue + labelMask = 0.0; DrawText_String(float2(BUFFER_WIDTH * 0.25 - 10.0 * labelSize * 0.25, BUFFER_HEIGHT * 0.53), labelSize, 1.0, texcoord, lblFlow, 10, labelMask); view = lerp(view, float3(1.00, 1.00, 1.00), saturate(labelMask)); //BL white + labelMask = 0.0; DrawText_String(float2(BUFFER_WIDTH * 0.75 - 16.0 * labelSize * 0.25, BUFFER_HEIGHT * 0.53), labelSize, 1.0, texcoord, lblConfidence, 16, labelMask); view = lerp(view, float3(1.00, 1.00, 1.00), saturate(labelMask)); //BR white + labelMask = 0.0; DrawText_String(float2(BUFFER_WIDTH * 0.50 - 14.0 * labelSize * 0.25, BUFFER_HEIGHT * (0.5 - BOX_HALF.y) + 8.0), labelSize, 1.0, texcoord, lblVectors, 14, labelMask); view = lerp(view, float3(1.00, 1.00, 1.00), saturate(labelMask)); //centre white + + view = lerp(view, float3(1.0, 1.0, 1.0), saturate(labelMask)); //white labels + return float4(view, 1.0); + } + case 1: { + float4 gbuffer = tex2D(sNormals, uv); + float3 normal = gbuffer.rgb; + float depth = gbuffer.a; + bool isLeftHalf = uv.x < 0.5; + float4 dbg; + if (isLeftHalf) + dbg = float4(normal * 0.5 + 0.5, 1.0); //left: normals + else + dbg = float4(DepthGradient(depth, uv), 1.0); //right: depth gradient + return dbg; + } + case 2: return float4(MotionToColor(tex2D(sFlow, uv).xy), 1); + case 3: return DrawMotionVectors(uv); + case 4: + { + float confidence = tex2D(sConfidence, uv).x; + float3 confidenceColor; + if (confidence < 0.5) + confidenceColor = lerp(float3(1.0, 0.0, 0.0), float3(1.0, 1.0, 0.0), confidence * 2.0); + else + confidenceColor = lerp(float3(1.0, 1.0, 0.0), float3(0.0, 1.0, 0.0), (confidence - 0.5) * 2.0); + return float4(lerp(sceneColor, confidenceColor, 0.9), 1.0); + } + default: return float4(sceneColor, 1.0); + } +} +#endif + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ +technique Lumenite_Kernel < + ui_label = "LUMENITE: Kernel 2.0"; + ui_tooltip = "Pre-effect for LumeniteFX shaders."; +> +{ + //normals + #if IMAGE_SPACE == 0 + pass { VertexShader = VS; PixelShader = PS_ReconstructNormals; RenderTarget0 = tNormals; RenderTarget1 = tDepth; } + #endif + + //optical flow + pass { VertexShader = PostProcessVS; PixelShader = PS_PackFeatures; RenderTarget = tCurrLuma; } + pass { VertexShader = PostProcessVS; PixelShader = PS_ComputeFlow128; RenderTarget = tFlow128; } + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow64; RenderTarget = tFlow64A; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass64; RenderTarget = tFlow64B; } + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow32; RenderTarget = tFlow32A; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass32; RenderTarget = tFlow32B; } + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow16; RenderTarget = tFlow16A; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass16; RenderTarget = tFlow16B; } + + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow8; RenderTarget = tFlow; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass8A; RenderTarget = tFlow8; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass8B; RenderTarget = tFlow; } + pass { VertexShader = PostProcessVS; PixelShader = PS_Confidence; RenderTarget = tConfidence; } + pass { VertexShader = PostProcessVS; PixelShader = PS_ATrousPassA; RenderTarget = tFlow8; } + pass { VertexShader = PostProcessVS; PixelShader = PS_ATrousPassB; RenderTarget = tFlow; } + + pass { VertexShader = PostProcessVS; PixelShader = PS_StoreFlow; RenderTarget0 = tPrevFrameFlow; RenderTarget1 = tPrevConfidence; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StoreLuma; RenderTarget = tPrevLuma; } + + //debug views +#if DEBUG_KERNEL + pass { VertexShader = PostProcessVS; PixelShader = PS_Debug; } +#endif +} + +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_LSAO.fx b/EXE/reshade-shaders/Shaders/lumenite_LSAO.fx new file mode 100644 index 00000000..f6261668 --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_LSAO.fx @@ -0,0 +1,368 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_LSAO.fx + Version : 2026.06.09 + Author : Afzaal (Kaidō) + Description: Large-Scale Ray Traced Ambient Occlusion (Screen Space). + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define FOV 60.0 +#define NEAR_PLANE 0.01 +#define AO_MAX_MARCH_STEPS 100 + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" +#include "./include/lumenite_Projections.fxh" +#include "./include/lumenite_Helpers.fxh" +#include "./include/lumenite_ColorManagement.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +uniform bool DEBUG_VIEW < + ui_label = "Show AO Mask"; + ui_tooltip = "Debug view for the AO. Shows raw AO."; + ui_category = "Ambient Occlusion"; +> = 0; + +uniform float DEPTH_BOUNDARY < + ui_type = "slider"; + ui_min = 0.001; ui_max = 0.999; ui_step = 0.001; + ui_label = "AO Range"; + ui_tooltip = "The Z+ range/depth in which the effect is applied."; + ui_category = "Ambient Occlusion"; + hidden = false; +> = 0.6; + +uniform float DEPTH_FADE_START < + ui_type = "slider"; + ui_min = 0.1; ui_max = 1.0; ui_step = 0.01; + ui_label = "Z+ Fade Start (%)"; + ui_tooltip = "Z+ fraction where effect starts fading out (relative to AO Range)"; + ui_category = "Ambient Occlusion"; + hidden = true; +> = 0.75; + +uniform float AO_INTENSITY < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; + ui_label = "AO Strength"; + ui_tooltip = "Controls the intensity of the ambient occlusion effect."; + ui_category = "Ambient Occlusion"; +> = 1.0; + +/*--------------. +| :: IMPORTS :: | +'--------------*/ +namespace Kernel { + texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; + sampler2D sFlow { Texture = tFlow; MagFilter = POINT; MinFilter = POINT; }; + + texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; + sampler2D sConfidence { Texture = tConfidence; }; + + texture tNormals { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; MipLevels = 4; }; + sampler sNormals { Texture = tNormals; }; + + texture2D tDepth { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 4; }; + sampler2D sDepth { Texture = tDepth; }; +} + +namespace LumeniteLSAO { + +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ +texture tAOTrace { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = R16F; }; +sampler sAOTrace { Texture = tAOTrace; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; + +texture tAO1 { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sAO1 { Texture = tAO1; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; + +texture tAO2 { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sAO2 { Texture = tAO2; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; +sampler sAO2Linear { Texture = tAO2; AddressU = CLAMP; AddressV = CLAMP; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; }; + +texture tPrevAO { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sPrevAO { Texture = tPrevAO; AddressU = CLAMP; AddressV = CLAMP; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; }; + +//HiZ mipchain +texture tHiZMip0 { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; }; +texture tHiZMip1 { Width = BUFFER_WIDTH/2; Height = BUFFER_HEIGHT/2; Format = R16F; }; +texture tHiZMip2 { Width = BUFFER_WIDTH/4; Height = BUFFER_HEIGHT/4; Format = R16F; }; +texture tHiZMip3 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; +texture tHiZMip4 { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = R16F; }; +texture tHiZMip5 { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/32; Format = R16F; }; + +sampler sHiZMip0 { Texture = tHiZMip0; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip1 { Texture = tHiZMip1; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip2 { Texture = tHiZMip2; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip3 { Texture = tHiZMip3; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip4 { Texture = tHiZMip4; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip5 { Texture = tHiZMip5; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +void BuildOrthonormalBasis(float3 n, out float3 b1, out float3 b2) +{ + if (n.z < -0.9999999) { + b1 = float3(0.0, -1.0, 0.0); + b2 = float3(-1.0, 0.0, 0.0); + } else { + float a = rcp(1.0 + n.z); + float b = -n.x * n.y * a; + b1 = float3(mad(-n.x * n.x, a, 1.0), b, -n.x); + b2 = float3(b, mad(-n.y * n.y, a, 1.0), -n.y); + } +} + +float3 GenerateHemisphereDirection(float3 normal, float2 rand, float3 tangent, float3 bitangent) +{ + float phi = rand.x * 6.28318530718; //2.0*PI as constant + float sinPhi, cosPhi; + sincos(phi, sinPhi, cosPhi); + float cosTheta = sqrt(1.0 - rand.y); + float sinTheta = sqrt(rand.y); + float3 result = normal * cosTheta; + result = mad(bitangent, sinTheta * sinPhi, result); + result = mad(tangent, sinTheta * cosPhi, result); + return result; +} + +float CalculateDepthFade(float depth) +{ + float fadeStartDepth = DEPTH_BOUNDARY * DEPTH_FADE_START; + float fadeRange = DEPTH_BOUNDARY - fadeStartDepth; + return 1.0 - saturate((depth - fadeStartDepth) / fadeRange); +} + +float2 ATrousFilter(sampler SourceSampler, float2 uv, uint dilation, bool adaptiveDilation) +{ + float4 gbuffer = tex2D(Kernel::sNormals, uv); + if (gbuffer.a == 0 || gbuffer.a >= DEPTH_BOUNDARY) return float2(1.0, 0.0); + + [branch] if (adaptiveDilation) { + float confidence = tex2Dlod(Kernel::sConfidence, float4(uv, 0, 0)).r; + dilation += uint(round((1.0 - confidence) * 2.0)); //scale filter kernel w. motion by up to a factor of 2 + } + + float2 centerData = tex2Dlod(SourceSampler, float4(uv, 0, 0)).rg; + float variance = max(0.0, centerData.g - (centerData.r * centerData.r)); //Moment - AO^2 + variance = max(variance, 0.0001); + float2 sum = centerData; + float totalWeight = 1.0; + for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) { + if (x == 0 && y == 0) continue; + float2 sampleUV = uv + float2(x, y) * dilation * (BUFFER_PIXEL_SIZE * 2.0); //don't forget the x2.0 to properly step half-res grid! + float2 sampleData = tex2Dlod(SourceSampler, float4(sampleUV, 0, 0)).rg; + float4 sampleGeo = tex2Dlod(Kernel::sNormals, float4(sampleUV, 0, 0)); + float depthWeight = exp(-abs(gbuffer.a - sampleGeo.a) / (gbuffer.a * 0.02 + 0.001)); + float normalWeight = pow(saturate(dot(gbuffer.rgb, sampleGeo.rgb)), 50.0); + float aoDiff = centerData.r - sampleData.r; + float aoWeight = exp(-(aoDiff * aoDiff) / (variance + 0.0001)); + float weight = depthWeight * normalWeight * aoWeight; + sum += sampleData * weight; + totalWeight += weight; + } + return sum / (totalWeight + EPSILON); +} + +float SamplePrevHiZ(float2 centerUV, sampler srcSampler, int srcMipLvl) { + float2 srcTexelSize = BUFFER_PIXEL_SIZE * pow(2, srcMipLvl); + float2 off[4] = { float2(-0.5, -0.5), float2(0.5, -0.5), float2(-0.5, 0.5), float2(0.5, 0.5) }; + float minDepth = 1.0; + [unroll] for(int i=0; i<4; i++) + minDepth = min(minDepth, tex2D(srcSampler, centerUV + off[i] * srcTexelSize).r); + return minDepth; +} + +/*--------------. +| :: SHADERS :: | +'--------------*/ +float PS_GenerateMip0(VSOUT input) : SV_Target +{ + float2 blockOriginUV = floor(input.uv / (BUFFER_PIXEL_SIZE * 2.0)) * (BUFFER_PIXEL_SIZE * 2.0); + float2 uvs[4] = { blockOriginUV + BUFFER_PIXEL_SIZE * float2(0.5, 0.5), + blockOriginUV + BUFFER_PIXEL_SIZE * float2(1.5, 0.5), + blockOriginUV + BUFFER_PIXEL_SIZE * float2(0.5, 1.5), + blockOriginUV + BUFFER_PIXEL_SIZE * float2(1.5, 1.5) }; + float d0 = tex2D(Kernel::sDepth, uvs[0]).r; + float d1 = tex2D(Kernel::sDepth, uvs[1]).r; + float d2 = tex2D(Kernel::sDepth, uvs[2]).r; + float d3 = tex2D(Kernel::sDepth, uvs[3]).r; + return min(min(d0, d1), min(d2, d3)); +} + +float PS_ReduceMip1 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip0, 0); } +float PS_ReduceMip2 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip1, 1); } +float PS_ReduceMip3 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip2, 2); } +float PS_ReduceMip4 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip3, 3); } +float PS_ReduceMip5 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip4, 4); } + +float PS_TraceAO(VSOUT input) : SV_Target +{ + float4 gbuffer = tex2D(Kernel::sNormals, input.uv); + float3 normal = gbuffer.rgb; + float depth = gbuffer.a; + if (depth == 0 || depth >= DEPTH_BOUNDARY) discard; + float3 startPos = UVToViewSpace(input.uv, depth, input); + float3 tangent, bitangent; + BuildOrthonormalBasis(normal, tangent, bitangent); + float2 noise = GetStratifiedNoise(input.vpos.xy); + float3 rayDir = GenerateHemisphereDirection(normal, noise, tangent, bitangent); + float totalRayLength = 0.7 * depth; + float baseStepSize = totalRayLength / (float)AO_MAX_MARCH_STEPS; + float stepSize = baseStepSize; + float3 currentPos = startPos + rayDir * stepSize; + float occlusion = 0.0; + float t = stepSize; + [loop] + for (int step = 0; step < AO_MAX_MARCH_STEPS; step++) { + if (t >= totalRayLength) break; + + float2 hitPos = ViewSpaceToUV(currentPos, input); + if (IsOOB(hitPos)) break; + + //select the appropriate Mip Level + float2 ray_screen_velocity = abs(rayDir.xy / currentPos.z) * float2(BUFFER_WIDTH, BUFFER_HEIGHT); + float footprint = max(ray_screen_velocity.x, ray_screen_velocity.y) * max(stepSize / currentPos.z, 1.0); + int mip = clamp(int(log2(max(footprint, 1.0))), 0, 5); + float HiZDepth; + if (mip==5) HiZDepth = tex2Dlod(sHiZMip5, float4(hitPos,0,0)).r; + else if (mip==4) HiZDepth = tex2Dlod(sHiZMip4, float4(hitPos,0,0)).r; + else if (mip==3) HiZDepth = tex2Dlod(sHiZMip3, float4(hitPos,0,0)).r; + else if (mip==2) HiZDepth = tex2Dlod(sHiZMip2, float4(hitPos,0,0)).r; + else if (mip==1) HiZDepth = tex2Dlod(sHiZMip1, float4(hitPos,0,0)).r; + else HiZDepth = tex2Dlod(sHiZMip0, float4(hitPos,0,0)).r; + + //skip some empty space + float currentStepSize = stepSize * max(1.0, float(mip) * 0.5); //stepsize mip scaling + if (currentPos.z < HiZDepth && (HiZDepth - currentPos.z) > currentStepSize) { + float leap = max(currentStepSize, (HiZDepth - currentPos.z) * 0.065); + currentPos += rayDir * leap; + t += leap; + continue; + } + + //hit test + float sceneDepth = tex2Dlod(Kernel::sDepth, float4(hitPos, 0, 0)).r; + float depthDiff = currentPos.z - sceneDepth; + float maxThickness = currentPos.z * 0.6; + if (depthDiff > (currentPos.z * 0.0001) && depthDiff < maxThickness) { + float3 scenePos = UVToViewSpace(hitPos, sceneDepth, input); + float hitDistance = length(scenePos - startPos); + float normalizedDist = hitDistance / totalRayLength; + occlusion = 1.0 - saturate(depthDiff / maxThickness); + occlusion = occlusion * occlusion; + occlusion = saturate(pow(saturate(1.0 - normalizedDist), 1.2) * occlusion * 1.4); + break; + } + + currentPos += rayDir * stepSize; + t += stepSize; + } + + float aoFactor = 1.0 - saturate(occlusion * AO_INTENSITY); + return aoFactor; +} + +float2 PS_TemporalFilter(VSOUT input) : SV_Target +{ + float depth = tex2D(Kernel::sDepth, input.uv).r; + //overwrite noise at boundary with clean White, prevents gaps + if (depth >= DEPTH_BOUNDARY) return float2(1.0, 1.0); //1.0 AO, 1.0 Moment + if (depth == 0) discard; + float ao = tex2D(sAOTrace, input.uv).r; + ao = lerp(1.0, ao, CalculateDepthFade(depth)); + float moment = ao * ao; + float2 flow = tex2D(Kernel::sFlow, input.uv).xy; + float confidence = tex2D(Kernel::sConfidence, input.uv).x; + confidence = saturate(confidence + log2(2.0 - confidence) * 0.6); //boost confidence + float2 rawHistory = tex2D(sPrevAO, input.uv + flow).rg; //history stores "1.0 - AO". 0.0 (Black Texture) -> Reads as 1.0 (White) + float prevAO = 1.0 - rawHistory.r; + float prevMoment = 1.0 - rawHistory.g; + float alpha = confidence * 0.98; + ao = lerp(ao, prevAO, alpha); + moment = lerp(moment, prevMoment, alpha); + //max(..., 0.001) to ensure we NEVER write exactly 0.0 again + //this tells the next frame "I contain data" + return float2(max(ao, 0.001), max(moment, 0.001)); +} + +float2 PS_StoreAO(VSOUT input) : SV_Target +{ + //must prevent history collision here + //if we store exactly 0.0 (means White), the next frame's blend pass thinks + //history is empty and resets it, causing shimmer + //so clamp to 0.0001 so the system knows "This is valid history data" + float2 data = tex2D(sAO1, input.uv).rg; + return float2(max(1.0 - data.r, 0.0001), max(1.0 - data.g, 0.0001)); //store inverted +} + +float2 PS_ATrousPass1(VSOUT input) : SV_Target { return ATrousFilter(sAO1, input.uv, 2, false); } + +float4 PS_ToDisplay(VSOUT input) : SV_Target +{ + float depth = tex2D(Kernel::sDepth, input.uv).r; + float ao = ATrousFilter(sAO2Linear, input.uv, 4, true).r; //stable AO mask (fades to 1.0) + if (DEBUG_VIEW) { + #if BUFFER_COLOR_SPACE > 1 + return float4(ToOutputColorspace(ao.xxx, true), 1.0); + #else + return float4(ao.xxx, 1.0); + #endif + } + if (depth == 0 || depth >= DEPTH_BOUNDARY) discard; + float3 base = GetLinearColor(input.uv, true); + base *= ao; + return float4(ToOutputColorspace(base, true), 1.0); +} + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ +technique Lumenite_LSAO < + ui_label = "LUMENITE: LSAO"; + ui_tooltip = "Large-Scale Ray Traced Ambient Occlusion (Screen Space)."; +> +{ + pass { VertexShader = VS; PixelShader = PS_GenerateMip0; RenderTarget = tHiZMip0; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip1; RenderTarget = tHiZMip1; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip2; RenderTarget = tHiZMip2; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip3; RenderTarget = tHiZMip3; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip4; RenderTarget = tHiZMip4; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip5; RenderTarget = tHiZMip5; } + + pass { VertexShader = VS; PixelShader = PS_TraceAO; RenderTarget = tAOTrace; } + pass { VertexShader = VS; PixelShader = PS_TemporalFilter; RenderTarget = tAO1; } + pass { VertexShader = VS; PixelShader = PS_StoreAO; RenderTarget = tPrevAO; } + pass { VertexShader = VS; PixelShader = PS_ATrousPass1; RenderTarget = tAO2; } + pass { VertexShader = VS; PixelShader = PS_ToDisplay; } +} + +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_QuantAO.fx b/EXE/reshade-shaders/Shaders/lumenite_QuantAO.fx new file mode 100644 index 00000000..11a7e68a --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_QuantAO.fx @@ -0,0 +1,403 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_QuantAO.fx + Version : 2026.06.09 + Author : Afzaal (Kaidō) + Description: Fast Ambient Occlusion (Screen Space). + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define FOV 60.0 +#define NEAR_PLANE 0.01 +#define AO_MAX_MARCH_STEPS 48 + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" +#include "./include/lumenite_Projections.fxh" +#include "./include/lumenite_Helpers.fxh" +#include "./include/lumenite_ColorManagement.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +uniform bool DEBUG_VIEW < + ui_label = "Show AO Mask"; + ui_tooltip = "Debug view for the AO. Shows raw AO."; + ui_category = "Ambient Occlusion"; +> = 0; + +uniform float DEPTH_BOUNDARY < + ui_type = "slider"; + ui_min = 0.001; ui_max = 0.999; ui_step = 0.001; + ui_label = "AO Range"; + ui_tooltip = "The Z+ range/depth in which the effect is applied."; + ui_category = "Ambient Occlusion"; + hidden = false; +> = 0.6; + +uniform float DEPTH_FADE_START < + ui_type = "slider"; + ui_min = 0.1; ui_max = 1.0; ui_step = 0.01; + ui_label = "Z+ Fade Start (%)"; + ui_tooltip = "Z+ fraction where effect starts fading out (relative to AO Range)"; + ui_category = "Ambient Occlusion"; + hidden = true; +> = 0.75; + +uniform float AO_INTENSITY < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; + ui_label = "AO Strength"; + ui_tooltip = "Controls the intensity of the ambient occlusion effect."; + ui_category = "Ambient Occlusion"; +> = 1.0; + +/*--------------. +| :: IMPORTS :: | +'--------------*/ +namespace QuantMotion { + texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; + sampler2D sFlow { Texture = tFlow; MagFilter = POINT; MinFilter = POINT; }; + + texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; + sampler2D sConfidence { Texture = tConfidence; }; +} + +namespace LumeniteQuantAO { + +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ +texture tNormals { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RGBA16F; }; +sampler sNormals { Texture = tNormals; }; + +texture2D tDepth { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = R16F; }; +sampler2D sDepth { Texture = tDepth; }; + +texture tAOTrace { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = R16F; }; +sampler sAOTrace { Texture = tAOTrace; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; + +texture tAO1 { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sAO1 { Texture = tAO1; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; + +texture tAO2 { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sAO2 { Texture = tAO2; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; + +texture tAO3 { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sAO3Linear { Texture = tAO3; AddressU = CLAMP; AddressV = CLAMP; MagFilter = LINEAR; MinFilter = LINEAR; }; + +texture tPrevAO { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sPrevAO { Texture = tPrevAO; AddressU = CLAMP; AddressV = CLAMP; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; }; + +//HiZ mipchain +texture tHiZMip0 { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; }; +texture tHiZMip1 { Width = BUFFER_WIDTH/2; Height = BUFFER_HEIGHT/2; Format = R16F; }; +texture tHiZMip2 { Width = BUFFER_WIDTH/4; Height = BUFFER_HEIGHT/4; Format = R16F; }; +texture tHiZMip3 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; +texture tHiZMip4 { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = R16F; }; +texture tHiZMip5 { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/32; Format = R16F; }; + +sampler sHiZMip0 { Texture = tHiZMip0; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip1 { Texture = tHiZMip1; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip2 { Texture = tHiZMip2; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip3 { Texture = tHiZMip3; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip4 { Texture = tHiZMip4; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; +sampler sHiZMip5 { Texture = tHiZMip5; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; }; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +void BuildOrthonormalBasis(float3 n, out float3 b1, out float3 b2) +{ + if (n.z < -0.9999999) { + b1 = float3(0.0, -1.0, 0.0); + b2 = float3(-1.0, 0.0, 0.0); + } else { + float a = rcp(1.0 + n.z); + float b = -n.x * n.y * a; + b1 = float3(mad(-n.x * n.x, a, 1.0), b, -n.x); + b2 = float3(b, mad(-n.y * n.y, a, 1.0), -n.y); + } +} + +float3 GenerateHemisphereDirection(float3 normal, float2 rand, float3 tangent, float3 bitangent) +{ + float phi = rand.x * 6.28318530718; //2.0*PI as constant + float sinPhi, cosPhi; + sincos(phi, sinPhi, cosPhi); + float cosTheta = sqrt(1.0 - rand.y); + float sinTheta = sqrt(rand.y); + float3 result = normal * cosTheta; + result = mad(bitangent, sinTheta * sinPhi, result); + result = mad(tangent, sinTheta * cosPhi, result); + return result; +} + +float CalculateDepthFade(float depth) +{ + float fadeStartDepth = DEPTH_BOUNDARY * DEPTH_FADE_START; + float fadeRange = DEPTH_BOUNDARY - fadeStartDepth; + return 1.0 - saturate((depth - fadeStartDepth) / fadeRange); +} + +float2 ATrousFilter(sampler SourceSampler, float2 uv, uint dilation, bool adaptiveDilation) +{ + float4 gbuffer = tex2D(sNormals, uv); + if (gbuffer.a == 0 || gbuffer.a >= DEPTH_BOUNDARY) return float2(1.0, 0.0); + + [branch] if (adaptiveDilation) { + float confidence = tex2Dlod(QuantMotion::sConfidence, float4(uv, 0, 0)).r; + dilation += uint(round((1.0 - confidence) * 2.0)); //scale filter kernel w. motion by up to a factor of 2 + } + + float2 centerData = tex2Dlod(SourceSampler, float4(uv, 0, 0)).rg; + float variance = max(0.0, centerData.g - (centerData.r * centerData.r)); //Moment - AO^2 + variance = max(variance, 0.0001); + float2 sum = centerData; + float totalWeight = 1.0; + for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) { + if (x == 0 && y == 0) continue; + float2 sampleUV = uv + float2(x, y) * dilation * (BUFFER_PIXEL_SIZE * 2.0); //don't forget the x2.0 to properly step half-res grid! + float2 sampleData = tex2Dlod(SourceSampler, float4(sampleUV, 0, 0)).rg; + float4 sampleGeo = tex2Dlod(sNormals, float4(sampleUV, 0, 0)); + float depthWeight = exp(-abs(gbuffer.a - sampleGeo.a) / (gbuffer.a * 0.02 + 0.001)); + float normalWeight = pow(saturate(dot(gbuffer.rgb, sampleGeo.rgb)), 50.0); + float aoDiff = centerData.r - sampleData.r; + float aoWeight = exp(-(aoDiff * aoDiff) / (variance + 0.0001)); + float weight = depthWeight * normalWeight * aoWeight; + sum += sampleData * weight; + totalWeight += weight; + } + return sum / (totalWeight + EPSILON); +} + +float SamplePrevHiZ(float2 centerUV, sampler srcSampler, int srcMipLvl) { + float2 srcTexelSize = BUFFER_PIXEL_SIZE * pow(2, srcMipLvl); + float2 off[4] = { float2(-0.5, -0.5), float2(0.5, -0.5), float2(-0.5, 0.5), float2(0.5, 0.5) }; + float minDepth = 1.0; + [unroll] for(int i=0; i<4; i++) + minDepth = min(minDepth, tex2D(srcSampler, centerUV + off[i] * srcTexelSize).r); + return minDepth; +} + +/*--------------. +| :: SHADERS :: | +'--------------*/ +void PS_ReconstructNormals(VSOUT input, out float4 gbuffer : SV_Target0, out float depthC : SV_Target1) +{ + depthC = GetDepth(input.uv); + + const float2 offsetX = float2(BUFFER_PIXEL_SIZE.x, 0); + const float2 offsetY = float2(0, BUFFER_PIXEL_SIZE.y); + + float3 pC = UVToViewSpace(input.uv, depthC, input); + float3 pL = UVToViewSpace(input.uv - offsetX, GetDepth(input.uv - offsetX), input); + float3 pR = UVToViewSpace(input.uv + offsetX, GetDepth(input.uv + offsetX), input); + float3 pT = UVToViewSpace(input.uv - offsetY, GetDepth(input.uv - offsetY), input); + float3 pB = UVToViewSpace(input.uv + offsetY, GetDepth(input.uv + offsetY), input); + + float3 diffX2 = pR - pC; + float3 diffX1 = pC - pL; + float3 diffY2 = pB - pC; + float3 diffY1 = pC - pT; + + float lenSqX2 = dot(diffX2, diffX2); + float lenSqX1 = dot(diffX1, diffX1); + float lenSqY2 = dot(diffY2, diffY2); + float lenSqY1 = dot(diffY1, diffY1); + + float3 ddx = lenSqX2 < lenSqX1 ? diffX2 : diffX1; + float3 ddy = lenSqY2 < lenSqY1 ? diffY2 : diffY1; + float3 geoNormal = normalize(cross(ddx, ddy)); + gbuffer = float4(geoNormal, depthC); +} + +float PS_GenerateMip0(VSOUT input) : SV_Target +{ + float2 blockOriginUV = floor(input.uv / (BUFFER_PIXEL_SIZE * 2.0)) * (BUFFER_PIXEL_SIZE * 2.0); + float2 uvs[4] = { blockOriginUV + BUFFER_PIXEL_SIZE * float2(0.5, 0.5), + blockOriginUV + BUFFER_PIXEL_SIZE * float2(1.5, 0.5), + blockOriginUV + BUFFER_PIXEL_SIZE * float2(0.5, 1.5), + blockOriginUV + BUFFER_PIXEL_SIZE * float2(1.5, 1.5) }; + float d0 = tex2D(sDepth, uvs[0]).r; + float d1 = tex2D(sDepth, uvs[1]).r; + float d2 = tex2D(sDepth, uvs[2]).r; + float d3 = tex2D(sDepth, uvs[3]).r; + return min(min(d0, d1), min(d2, d3)); +} + +float PS_ReduceMip1 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip0, 0); } +float PS_ReduceMip2 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip1, 1); } +float PS_ReduceMip3 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip2, 2); } +float PS_ReduceMip4 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip3, 3); } +float PS_ReduceMip5 (VSOUT input) : SV_Target { return SamplePrevHiZ(input.uv, sHiZMip4, 4); } + +float PS_TraceAO(VSOUT input) : SV_Target +{ + float4 gbuffer = tex2D(sNormals, input.uv); + float3 normal = gbuffer.rgb; + float depth = gbuffer.a; + if (depth == 0 || depth >= DEPTH_BOUNDARY) discard; + float3 startPos = UVToViewSpace(input.uv, depth, input); + float3 tangent, bitangent; + BuildOrthonormalBasis(normal, tangent, bitangent); + float2 noise = GetStratifiedNoise(input.vpos.xy); + float3 rayDir = GenerateHemisphereDirection(normal, noise, tangent, bitangent); + float totalRayLength = 0.7 * depth; + float baseStepSize = totalRayLength / (float)AO_MAX_MARCH_STEPS; + float stepSize = baseStepSize; + float3 currentPos = startPos + rayDir * stepSize; + float occlusion = 0.0; + float t = stepSize; + [loop] + for (int step = 0; step < AO_MAX_MARCH_STEPS; step++) { + if (t >= totalRayLength) break; + + float2 hitPos = ViewSpaceToUV(currentPos, input); + if (IsOOB(hitPos)) break; + + //select the appropriate Mip Level + float2 ray_screen_velocity = abs(rayDir.xy / currentPos.z) * float2(BUFFER_WIDTH, BUFFER_HEIGHT); + float footprint = max(ray_screen_velocity.x, ray_screen_velocity.y) * max(stepSize / currentPos.z, 1.0); + int mip = clamp(int(log2(max(footprint, 1.0))), 0, 5); + float HiZDepth; + if (mip==5) HiZDepth = tex2Dlod(sHiZMip5, float4(hitPos,0,0)).r; + else if (mip==4) HiZDepth = tex2Dlod(sHiZMip4, float4(hitPos,0,0)).r; + else if (mip==3) HiZDepth = tex2Dlod(sHiZMip3, float4(hitPos,0,0)).r; + else if (mip==2) HiZDepth = tex2Dlod(sHiZMip2, float4(hitPos,0,0)).r; + else if (mip==1) HiZDepth = tex2Dlod(sHiZMip1, float4(hitPos,0,0)).r; + else HiZDepth = tex2Dlod(sHiZMip0, float4(hitPos,0,0)).r; + + //skip some empty space + float currentStepSize = stepSize * max(1.0, float(mip) * 0.5); //stepsize mip scaling + if (currentPos.z < HiZDepth && (HiZDepth - currentPos.z) > currentStepSize) { + float leap = max(currentStepSize, (HiZDepth - currentPos.z) * 0.065); + currentPos += rayDir * leap; + t += leap; + continue; + } + + //hit test + float sceneDepth = tex2Dlod(sDepth, float4(hitPos, 0, 0)).r; + float depthDiff = currentPos.z - sceneDepth; + float maxThickness = currentPos.z * 0.6; + if (depthDiff > (currentPos.z * 0.0001) && depthDiff < maxThickness) { + float3 scenePos = UVToViewSpace(hitPos, sceneDepth, input); + float hitDistance = length(scenePos - startPos); + float normalizedDist = hitDistance / totalRayLength; + occlusion = 1.0 - saturate(depthDiff / maxThickness); + occlusion = occlusion * occlusion; + occlusion = saturate(pow(saturate(1.0 - normalizedDist), 1.2) * occlusion * 1.4); + break; + } + + currentPos += rayDir * stepSize; + t += stepSize; + } + + float aoFactor = 1.0 - saturate(occlusion * AO_INTENSITY); + return aoFactor; +} + +float2 PS_TemporalFilter(VSOUT input) : SV_Target +{ + float depth = tex2D(sDepth, input.uv).r; + //overwrite noise at boundary with clean White, prevents gaps + if (depth >= DEPTH_BOUNDARY) return float2(1.0, 1.0); //1.0 AO, 1.0 Moment + if (depth == 0) discard; + float ao = tex2D(sAOTrace, input.uv).r; + ao = lerp(1.0, ao, CalculateDepthFade(depth)); + float moment = ao * ao; + float2 flow = tex2D(QuantMotion::sFlow, input.uv).xy; + float confidence = tex2D(QuantMotion::sConfidence, input.uv).x; + confidence = saturate(confidence + log2(2.0 - confidence) * 0.55); //boost confidence + float2 rawHistory = tex2D(sPrevAO, input.uv + flow).rg; //history stores "1.0 - AO". 0.0 (Black Texture) -> Reads as 1.0 (White) + float prevAO = 1.0 - rawHistory.r; + float prevMoment = 1.0 - rawHistory.g; + float alpha = confidence * 0.98; + ao = lerp(ao, prevAO, alpha); + moment = lerp(moment, prevMoment, alpha); + //max(..., 0.001) to ensure we NEVER write exactly 0.0 again + //this tells the next frame "I contain data" + return float2(max(ao, 0.001), max(moment, 0.001)); +} + +float2 PS_StoreAO(VSOUT input) : SV_Target +{ + //must prevent history collision here + //if we store exactly 0.0 (means White), the next frame's blend pass thinks + //history is empty and resets it, causing shimmer + //so clamp to 0.0001 so the system knows "This is valid history data" + float2 data = tex2D(sAO1, input.uv).rg; + return float2(max(1.0 - data.r, 0.0001), max(1.0 - data.g, 0.0001)); //store inverted +} + +float2 PS_ATrousPass1(VSOUT input) : SV_Target { return ATrousFilter(sAO1, input.uv, 2, false); } + +float2 PS_ATrousPass2(VSOUT input) : SV_Target { return ATrousFilter(sAO2, input.uv, 4, true); } + +float4 PS_ToDisplay(VSOUT input) : SV_Target +{ + float depth = tex2D(sDepth, input.uv).r; + float ao = tex2D(sAO3Linear, input.uv).r; + if (DEBUG_VIEW) { + #if BUFFER_COLOR_SPACE > 1 + return float4(ToOutputColorspace(ao.xxx, true), 1.0); + #else + return float4(ao.xxx, 1.0); + #endif + } + if (depth == 0 || depth >= DEPTH_BOUNDARY) discard; + float3 base = GetLinearColor(input.uv, true); + base *= ao; + return float4(ToOutputColorspace(base, true), 1.0); +} + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ +technique Lumenite_QuantAO < + ui_label = "LUMENITE: QuantAO"; + ui_tooltip = "Fast Ambient Occlusion (Screen Space)."; +> +{ + pass { VertexShader = VS; PixelShader = PS_ReconstructNormals; RenderTarget0 = tNormals; RenderTarget1 = tDepth; } + pass { VertexShader = VS; PixelShader = PS_GenerateMip0; RenderTarget = tHiZMip0; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip1; RenderTarget = tHiZMip1; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip2; RenderTarget = tHiZMip2; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip3; RenderTarget = tHiZMip3; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip4; RenderTarget = tHiZMip4; } + pass { VertexShader = VS; PixelShader = PS_ReduceMip5; RenderTarget = tHiZMip5; } + + pass { VertexShader = VS; PixelShader = PS_TraceAO; RenderTarget = tAOTrace; } + pass { VertexShader = VS; PixelShader = PS_TemporalFilter; RenderTarget = tAO1; } + pass { VertexShader = VS; PixelShader = PS_StoreAO; RenderTarget = tPrevAO; } + pass { VertexShader = VS; PixelShader = PS_ATrousPass1; RenderTarget = tAO2; } + pass { VertexShader = VS; PixelShader = PS_ATrousPass2; RenderTarget = tAO3; } + pass { VertexShader = VS; PixelShader = PS_ToDisplay; } +} + +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_QuantMotion.fx b/EXE/reshade-shaders/Shaders/lumenite_QuantMotion.fx new file mode 100644 index 00000000..1e07c243 --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_QuantMotion.fx @@ -0,0 +1,444 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : QuantMotion.fx + Version : 2026.06.16 + Author : Afzaal (Kaidō) + Description: Superfast motion vectors for low-end hardware. + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define EPSILON 1e-6 + +#ifndef DEBUG_FLOW + #define DEBUG_FLOW 0 +#endif + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +uniform uint FRAME_COUNT < source = "framecount"; >; + +namespace QuantMotion { + +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ +texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; +sampler2D sFlow { Texture = tFlow; MagFilter = POINT; MinFilter = POINT; }; + +texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; +sampler2D sConfidence { Texture = tConfidence; }; + +texture2D tCurrLuma { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 8; }; +sampler2D sCurrLuma { Texture = tCurrLuma; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tPrevLuma { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 8; }; +sampler2D sPrevLuma { Texture = tPrevLuma; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow128 { Width = BUFFER_WIDTH/128; Height = BUFFER_HEIGHT/128; Format = RG16F; }; +sampler2D sFlow128 { Texture = tFlow128; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow64A { Width = BUFFER_WIDTH/64; Height = BUFFER_HEIGHT/64; Format = RG16F; }; +sampler2D sFlow64A { Texture = tFlow64A; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; +texture2D tFlow64B { Width = BUFFER_WIDTH/64; Height = BUFFER_HEIGHT/64; Format = RG16F; }; +sampler2D sFlow64B { Texture = tFlow64B; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow32A { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/32; Format = RG16F; }; +sampler2D sFlow32A { Texture = tFlow32A; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; +texture2D tFlow32B { Width = BUFFER_WIDTH/32; Height = BUFFER_HEIGHT/32; Format = RG16F; }; +sampler2D sFlow32B { Texture = tFlow32B; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow16A { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = RG16F; }; +sampler2D sFlow16A { Texture = tFlow16A; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; +texture2D tFlow16B { Width = BUFFER_WIDTH/16; Height = BUFFER_HEIGHT/16; Format = RG16F; }; +sampler2D sFlow16B { Texture = tFlow16B; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tFlow8 { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; +sampler2D sFlow8 { Texture = tFlow8; MagFilter = POINT; MinFilter = POINT; AddressU = CLAMP; AddressV = CLAMP; AddressW = CLAMP; }; + +texture2D tPrevFrameFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; +sampler2D sPrevFrameFlow { Texture = tPrevFrameFlow; MagFilter = POINT; MinFilter = POINT; }; + +texture2D tPrevConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; +sampler2D sPrevConfidence { Texture = tPrevConfidence; }; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +bool IsOOB(float2 uv) { + return any(uv < 0.0) || any(uv > 1.0); +} + +float3 GetColor(float2 uv) +{ + return tex2Dlod(ReShade::BackBuffer, float4(uv, 0, 0)).rgb; +} + +float3 MotionToColor(float2 motion) +{ + float angle = atan2(-motion.y, -motion.x) / 6.283 + 0.5; + float rawLength = length(motion) / (15.0 * BUFFER_PIXEL_SIZE.x); + float compressed = rawLength / (1.0 + rawLength * 1.4); //asymptotic squash + float boosted = pow(compressed, 0.5); //lift shadows + float magnitude = saturate(lerp(compressed, boosted, saturate(rawLength * 3.0))); + float3 hsv = float3(angle, 1, magnitude); + float4 K = float4(1, 2/3.0, 1/3.0, 3); + float3 p = abs(frac(hsv.xxx + K.xyz) * 6 - K.www); + return hsv.z * lerp(K.xxx, clamp(p - K.xxx, 0, 1), hsv.y) + 0.1; +} + +float ZMSAD(sampler2D currLumaSrc, sampler2D prevLumaSrc, float2 posA, float2 posB, float2 texelSize, uint mip) +{ + static const int2 offsets[9] = { + int2(0, 3), + int2(0, 1), + int2(-3,0), int2(-1,0), int2(0, 0), int2(1,0), int2(3,0), + int2(0,-1), + int2(0,-3) + }; + + //gather samples and calculate the mean for each patch + float samplesA[9], samplesB[9]; + float meanA = 0.0, meanB = 0.0; + + [unroll] for(int i = 0; i < 9; i++) { + float2 offset = float2(offsets[i]) * texelSize; + samplesA[i] = tex2Dlod(currLumaSrc, float4(posA + offset, 0, mip)).r; + samplesB[i] = tex2Dlod(prevLumaSrc, float4(posB + offset, 0, mip)).r; + meanA += samplesA[i]; + meanB += samplesB[i]; + } + meanA /= 9.0; + meanB /= 9.0; + + //SAD on the normalized samples + float err = 0.0; + [unroll] for(int i = 0; i < 9; i++) + err += abs((samplesA[i] - meanA) - (samplesB[i] - meanB)); + + return ((err / 9.0) + EPSILON); +} + +float2 Median9(sampler2D flowSrc, float2 uv, float2 texelSize, uint mip) +{ + float2 v[9]; + int idx = 0; + [unroll] for(int dy = -1; dy <= 1; dy++) for(int dx = -1; dx <= 1; dx++) + v[idx++] = tex2Dlod(flowSrc, float4(uv + float2(dx, dy) * texelSize, 0, mip)).xy; + + //bubble sort ensures the Median lands in v[4], only needs 5 passes + //indices 4,5,6,7,8 contain the 5 largest items, so v[4] is the median + [unroll] for(int k = 0; k < 5; k++) for(int i = 0; i < 8 - k; i++) { //checks decrease as right side gets sorted + float2 a = v[i]; + float2 b = v[i+1]; + v[i] = min(a, b); + v[i+1] = max(a, b); + } + + return v[4]; +} + +float2 BilateralMedian9(sampler2D flowSrc, float2 uv, float2 texelSize, uint mip) +{ + static const int2 DENSE_3X3[9] = { + int2(-1,-1), int2(0,-1), int2(1,-1), + int2(-1, 0), int2(0, 0), int2(1, 0), + int2(-1, 1), int2(0, 1), int2(1, 1) + }; + float lumaC = tex2Dlod(sCurrLuma, float4(uv, 0, 0)).x; + float lumaW = tex2Dlod(sCurrLuma, float4(uv + float2(-1.0, 0.0) * texelSize, 0, 0)).x; + float lumaE = tex2Dlod(sCurrLuma, float4(uv + float2( 1.0, 0.0) * texelSize, 0, 0)).x; + float lumaN = tex2Dlod(sCurrLuma, float4(uv + float2( 0.0,-1.0) * texelSize, 0, 0)).x; + float lumaS = tex2Dlod(sCurrLuma, float4(uv + float2( 0.0, 1.0) * texelSize, 0, 0)).x; + //central-difference gradient, wider baseline than quad ddx/ddy, derived from real samples + float dxLuma = (lumaE - lumaW) * 0.5; + float dyLuma = (lumaS - lumaN) * 0.5; + float2 v[9]; + uint validCount = 0; + [unroll] for (int i = 0; i < 9; i++) { + int2 off = DENSE_3X3[i]; + float2 sampleUV = uv + float2(off) * texelSize; + //cardinals + center use sampled luma; diagonals get linear prediction + float sampleLuma = lumaC; //covers (0,0) + if (off.x == -1 && off.y == 0) sampleLuma = lumaW; + else if (off.x == 1 && off.y == 0) sampleLuma = lumaE; + else if (off.x == 0 && off.y == -1) sampleLuma = lumaN; + else if (off.x == 0 && off.y == 1) sampleLuma = lumaS; + else if (off.x != 0 && off.y != 0) sampleLuma = lumaC + float(off.x) * dxLuma + float(off.y) * dyLuma; + bool isValid = abs(lumaC - sampleLuma) <= 0.05; + v[i] = isValid ? tex2Dlod(flowSrc, float4(sampleUV, 0, mip)).xy : float2(1e38, 1e38); + validCount += uint(isValid); + } + if(validCount < 3u) return v[4]; + //right-to-left bubble: smallest reaches v[0] per pass; after 5 passes, v[0..4] sorted ascending + [unroll] for(int k = 0; k < 5; k++) for(int j = 7; j >= k; j--) { + float2 a = v[j]; + float2 b = v[j+1]; + v[j] = min(a, b); + v[j+1] = max(a, b); + } + uint medianIdx = validCount / 2u; + float2 result = v[1]; //fallback for validCount == 3 (medianIdx 1) + if (medianIdx == 2u) result = v[2]; + if (medianIdx == 3u) result = v[3]; + if (medianIdx == 4u) result = v[4]; + return result; +} + +float2 ATrousFilter(sampler2D motionSrc, float2 uv, uint dilation, uint mip) +{ + static const int2 offsets[8] = { int2(-1,-1), int2(0,-1), int2(1,-1), + int2(-1, 0), int2(1, 0), + int2(-1, 1), int2(0, 1), int2(1, 1) }; + float2 centerFlow = tex2Dlod(motionSrc, float4(uv, 0, 0)).xy; + float centerConf = max(tex2Dlod(sConfidence, float4(uv, 0, 0)).r, 0.01); //0.01 floor prevents NaN if conf hits 0 + float2 sum = centerFlow * centerConf; + float totalWeight = centerConf; + [unroll] for (int i = 0; i < 8; i++) { + float2 sampleUV = uv + float2(offsets[i]) * dilation * BUFFER_PIXEL_SIZE * 8.0; //*8 = stride of flow grid + float2 sampleFlow = tex2Dlod(motionSrc, float4(sampleUV, 0, 0)).xy; + float2 flowDelta = (sampleFlow - centerFlow) / BUFFER_PIXEL_SIZE * 8.0; + float flowWeight = exp(-dot(flowDelta, flowDelta) * 0.125); + float weight = flowWeight; + sum += sampleFlow * weight; + totalWeight += weight; + } + return sum / (totalWeight + EPSILON); +} + +float2 UpscaleFlow(sampler2D coarseSrc, sampler2D currLumaSrc, sampler2D prevLumaSrc, float2 uv, float2 texelSize, uint mip) +{ + if(FRAME_COUNT == 0) return float2(0, 0); + + float2 coarseTexelSize = rcp(float2(tex2Dsize(coarseSrc, 0))); + //pool candidates for tournament selection. order matters here + float2 candidates[6]; + candidates[0] = tex2D(coarseSrc, uv).xy ; + candidates[1] = tex2D(coarseSrc, uv + float2(0, -coarseTexelSize.y)).xy ; + candidates[2] = tex2D(coarseSrc, uv + float2(0, coarseTexelSize.y)).xy ; + candidates[3] = tex2D(coarseSrc, uv - float2(coarseTexelSize.x, 0)).xy ; + candidates[4] = tex2D(coarseSrc, uv + float2(coarseTexelSize.x, 0)).xy ; + candidates[5] = tex2D(sPrevFrameFlow, uv).xy; + + float minCost = 1e6; + float2 prediction = candidates[0]; + [loop] for (int i = 0; i < 6; i++) { + float cost = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + candidates[i], texelSize, mip); + if (cost < minCost) { + minCost = cost; + prediction = candidates[i]; + } + } + + //refinement with parabolic fitting + float costLeft = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction - float2(texelSize.x, 0), texelSize, mip); + float costRight = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction + float2(texelSize.x, 0), texelSize, mip); + float costDown = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction - float2(0, texelSize.y), texelSize, mip); + float costUp = ZMSAD(currLumaSrc, prevLumaSrc, uv, uv + prediction + float2(0, texelSize.y), texelSize, mip); + //sub-pixel offset (parabolic fitting) + float2 subpixelOffset; + subpixelOffset.x = (costLeft - costRight) / (4.0 * (costLeft + costRight - 2.0 * minCost) + EPSILON); //EPSILON for flat surface handling + subpixelOffset.y = (costDown - costUp) / (4.0 * (costDown + costUp - 2.0 * minCost) + EPSILON); + //clamp offset to a reasonable range + subpixelOffset = clamp(subpixelOffset, -0.5, 0.5); + + return (prediction+subpixelOffset*texelSize); +} + +/*--------------. +| :: SHADERS :: | +'--------------*/ +float PS_PackFeatures(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float luma = dot(GetColor(uv), float3(0.2126, 0.7152, 0.0722)); + return luma * rcp(1.0 + luma); +} + +float2 PS_ComputeFlow128(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + if(FRAME_COUNT == 0) return float2(0, 0); + + static const int SEARCH_RADIUS = 3; + static const uint mip = 5; + float2 texelSize = BUFFER_PIXEL_SIZE * exp2(mip); + + //candidate seeds for the coarsest level for tournament selection + float2 prevSeed = tex2D(sPrevFrameFlow, uv).xy; + float2 zeroSeed = float2(0, 0); + float prevCost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv + prevSeed, texelSize, mip); + float zeroCost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv + zeroSeed, texelSize, mip); + + float2 seed = (zeroCost < prevCost) ? zeroSeed : prevSeed; //pick better candidate as seed + float2 bestFlow = seed; + float minCost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv+seed, texelSize, mip); + //search in a grid AROUND the seed + for (int y = -SEARCH_RADIUS; y <= SEARCH_RADIUS; ++y) for (int x = -SEARCH_RADIUS; x <= SEARCH_RADIUS; ++x) { + if (x == 0 && y == 0) continue; + float2 candidateFlow = seed + float2(x, y) * texelSize; + float cost = ZMSAD(sCurrLuma, sPrevLuma, uv, uv + candidateFlow, texelSize, mip); + if (cost < minCost) { + minCost = cost; + bestFlow = candidateFlow; + if (minCost < 0.01) //near-perfect match found + return bestFlow; + } + } + return bestFlow; +} + +float2 PS_UpscaleFlow64(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow128, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*16.0, 4); +} + +float2 PS_MedianPass64(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return Median9(sFlow64A, uv, BUFFER_PIXEL_SIZE*64.0, 6); +} + +float2 PS_UpscaleFlow32(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow64B, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*8.0, 3); +} + +float2 PS_MedianPass32(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return Median9(sFlow32A, uv, BUFFER_PIXEL_SIZE*32.0, 5); +} + +float2 PS_UpscaleFlow16(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow32B, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*4.0, 2); +} + +float2 PS_MedianPass16(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return Median9(sFlow16A, uv, BUFFER_PIXEL_SIZE*16.0, 4); +} + +float2 PS_UpscaleFlow8(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return UpscaleFlow(sFlow16B, sCurrLuma, sPrevLuma, uv, BUFFER_PIXEL_SIZE*2.0, 1); +} + +float2 PS_MedianPass8(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return BilateralMedian9(sFlow8, uv, BUFFER_PIXEL_SIZE*8.0, 3); +} + +float2 PS_ATrousPassA(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target //stride 1 +{ + return ATrousFilter(sFlow, uv, 2, 3); +} + +float2 PS_ATrousPassB(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target //stride 2 +{ + float2 flow = ATrousFilter(sFlow8, uv, 4, 1); + //kill sub-pixel noise + float flowPixelMag = length(flow / BUFFER_PIXEL_SIZE); + float gate = saturate(1.0 - pow(1.0 - saturate(saturate(flowPixelMag) - 0.2), 10.0)); //SNAP TO REALITY + return flow*gate; +} + +float PS_Confidence(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + if(FRAME_COUNT == 0) return 0.0; //no confidence + float2 flow = tex2D(sFlow, uv).xy; + float2 prevUV = uv + flow; //warp prev frame forward + if(IsOOB(prevUV)) return 0.0; + float currLuma = tex2Dlod(sCurrLuma, float4(uv, 0, 3)).r; + float prevLuma = tex2Dlod(sPrevLuma, float4(prevUV, 0, 3)).r; + float lumaError = abs(currLuma - prevLuma); + if(lumaError > 0.1) return 0.0; //no confidence + float subpixelThreshold = length(BUFFER_PIXEL_SIZE); + float flowMagnitude = length(flow); + if (flowMagnitude <= subpixelThreshold) return 0.9; //if flow is subpixel, high confidence + float motionPenalty = flowMagnitude / subpixelThreshold; + float lengthConfidence = rcp(motionPenalty * 0.07 + 1.0); + float photometricConfidence = exp(-lumaError * 8.0 * lengthConfidence); + //current frame final confidence + float currentConf = lengthConfidence * photometricConfidence; + //temporal filter + float historyConf = tex2D(sPrevConfidence, prevUV).r; + float alpha = (currentConf < historyConf - 0.05) ? 0.5 : 0.1; //drop fast (kill speckles promptly), regain slowly (stay stable) + return lerp(historyConf, currentConf, alpha); +} + +void PS_StoreFlow(float4 pos : SV_Position, float2 uv : TEXCOORD, out float2 flow : SV_Target0, out float confidence : SV_Target1) +{ + flow = tex2D(sFlow, uv).xy; + confidence = tex2D(sConfidence, uv).r; +} + +float PS_StoreLuma(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return tex2D(sCurrLuma, uv).r; +} + +#if DEBUG_FLOW +float4 PS_Debug(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + return float4(MotionToColor(tex2D(sFlow, uv).xy), 1); +} +#endif + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ +technique Lumenite_QuantMotion < + ui_label = "LUMENITE: QuantMotion"; + ui_tooltip = "Superfast motion vectors for ReShade."; +> +{ + //optical flow + pass { VertexShader = PostProcessVS; PixelShader = PS_PackFeatures; RenderTarget = tCurrLuma; } + pass { VertexShader = PostProcessVS; PixelShader = PS_ComputeFlow128; RenderTarget = tFlow128; } + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow64; RenderTarget = tFlow64A; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass64; RenderTarget = tFlow64B; } + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow32; RenderTarget = tFlow32A; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass32; RenderTarget = tFlow32B; } + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow16; RenderTarget = tFlow16A; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass16; RenderTarget = tFlow16B; } + + pass { VertexShader = PostProcessVS; PixelShader = PS_UpscaleFlow8; RenderTarget = tFlow8; } + pass { VertexShader = PostProcessVS; PixelShader = PS_MedianPass8; RenderTarget = tFlow; } + pass { VertexShader = PostProcessVS; PixelShader = PS_Confidence; RenderTarget = tConfidence; } + pass { VertexShader = PostProcessVS; PixelShader = PS_ATrousPassA; RenderTarget = tFlow8; } + pass { VertexShader = PostProcessVS; PixelShader = PS_ATrousPassB; RenderTarget = tFlow; } + + pass { VertexShader = PostProcessVS; PixelShader = PS_StoreFlow; RenderTarget0 = tPrevFrameFlow; RenderTarget1 = tPrevConfidence; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StoreLuma; RenderTarget = tPrevLuma; } + + //debug views +#if DEBUG_FLOW + pass { VertexShader = PostProcessVS; PixelShader = PS_Debug; } +#endif +} + +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_RTAO.fx b/EXE/reshade-shaders/Shaders/lumenite_RTAO.fx new file mode 100644 index 00000000..c60a5d57 --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_RTAO.fx @@ -0,0 +1,300 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_RTAO.fx + Version : 2026.05.30 + Author : Afzaal (Kaidō) + Description: Ray Traced Ambient Occlusion (Screen Space). + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define FOV 60.0 +#define NEAR_PLANE 0.01 +#define INITIAL_STEP_SCALE 0.9 +#define STEP_GROWTH_FACTOR 1.2 +#define AO_MAX_MARCH_STEPS 15 + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" +#include "./include/lumenite_Projections.fxh" +#include "./include/lumenite_Helpers.fxh" +#include "./include/lumenite_ColorManagement.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +uniform bool DEBUG_VIEW < + ui_label = "Show AO Mask"; + ui_tooltip = "Debug view for the AO. Shows raw AO."; + ui_category = "Ambient Occlusion"; +> = 0; + +uniform float DEPTH_BOUNDARY < + ui_type = "slider"; + ui_min = 0.001; ui_max = 0.999; ui_step = 0.001; + ui_label = "AO Range"; + ui_tooltip = "The Z+ range/depth in which the effect is applied."; + ui_category = "Ambient Occlusion"; + hidden = false; +> = 0.6; + +uniform float DEPTH_FADE_START < + ui_type = "slider"; + ui_min = 0.1; ui_max = 1.0; ui_step = 0.01; + ui_label = "Z+ Fade Start (%)"; + ui_tooltip = "Z+ fraction where effect starts fading out (relative to AO Range)"; + ui_category = "Ambient Occlusion"; + hidden = true; +> = 0.75; + +uniform float AO_INTENSITY < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; + ui_label = "AO Strength"; + ui_tooltip = "Controls the intensity of the ambient occlusion effect."; + ui_category = "Ambient Occlusion"; +> = 1.0; + +//deprecated +// uniform int USER_GUIDE < +// ui_type = "radio"; +// ui_category = ""; +// ui_label = " "; +// ui_text = "RESOLUTION_SCALING:\n0: Renders AO at full-resolution.\n1: Renders AO at half-resolution."; +// >; + +/*--------------. +| :: IMPORTS :: | +'--------------*/ +namespace Kernel { + texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; + sampler2D sFlow { Texture = tFlow; MagFilter = POINT; MinFilter = POINT; }; + + texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; + sampler2D sConfidence { Texture = tConfidence; }; + + texture tNormals { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; MipLevels = 4; }; + sampler sNormals { Texture = tNormals; }; + + texture2D tDepth { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 4; }; + sampler2D sDepth { Texture = tDepth; }; +} + +namespace LumeniteRTAO { + +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ +texture tAOTrace { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = R16F; }; +sampler sAOTrace { Texture = tAOTrace; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; + +texture tAO1 { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sAO1 { Texture = tAO1; AddressU = CLAMP; AddressV = CLAMP; MagFilter = POINT; MinFilter = POINT; MipFilter = POINT; }; +sampler sAO1Linear { Texture = tAO1; AddressU = CLAMP; AddressV = CLAMP; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; }; + +texture tPrevAO { Width = BUFFER_WIDTH / 2; Height = BUFFER_HEIGHT / 2; Format = RG16F; }; +sampler sPrevAO { Texture = tPrevAO; AddressU = CLAMP; AddressV = CLAMP; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; }; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +void BuildOrthonormalBasis(float3 n, out float3 b1, out float3 b2) +{ + if (n.z < -0.9999999) { + b1 = float3(0.0, -1.0, 0.0); + b2 = float3(-1.0, 0.0, 0.0); + } else { + float a = rcp(1.0 + n.z); + float b = -n.x * n.y * a; + b1 = float3(mad(-n.x * n.x, a, 1.0), b, -n.x); + b2 = float3(b, mad(-n.y * n.y, a, 1.0), -n.y); + } +} + +float3 GenerateHemisphereDirection(float3 normal, float2 rand, float3 tangent, float3 bitangent) +{ + float phi = rand.x * 6.28318530718; //2.0*PI as constant + float sinPhi, cosPhi; + sincos(phi, sinPhi, cosPhi); + float cosTheta = sqrt(1.0 - rand.y); + float sinTheta = sqrt(rand.y); + float3 result = normal * cosTheta; + result = mad(bitangent, sinTheta * sinPhi, result); + result = mad(tangent, sinTheta * cosPhi, result); + return result; +} + +float CalculateDepthFade(float depth) +{ + float fadeStartDepth = DEPTH_BOUNDARY * DEPTH_FADE_START; + float fadeRange = DEPTH_BOUNDARY - fadeStartDepth; + return 1.0 - saturate((depth - fadeStartDepth) / fadeRange); +} + +float2 ATrousFilter(sampler SourceSampler, float2 uv, uint dilation) +{ + float4 gbuffer = tex2D(Kernel::sNormals, uv); + if (gbuffer.a == 0 || gbuffer.a >= DEPTH_BOUNDARY) return float2(1.0, 0.0); + float confidence = tex2Dlod(Kernel::sConfidence, float4(uv, 0, 0)).r; + dilation += uint(round((1.0 - confidence))); //scale filter radius with motion + float2 centerData = tex2Dlod(SourceSampler, float4(uv, 0, 0)).rg; + float variance = max(0.0, centerData.g - (centerData.r * centerData.r)); //Moment - AO^2 + variance = max(variance, 0.0001); + float2 sum = centerData; + float totalWeight = 1.0; + for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++) { + if (x == 0 && y == 0) continue; + float2 sampleUV = uv + float2(x, y) * dilation * (BUFFER_PIXEL_SIZE * 2.0); //don't forget the x2.0 to properly step half-res grid! + float2 sampleData = tex2Dlod(SourceSampler, float4(sampleUV, 0, 0)).rg; + float4 sampleGeo = tex2Dlod(Kernel::sNormals, float4(sampleUV, 0, 0)); + float depthWeight = exp(-abs(gbuffer.a - sampleGeo.a) / (gbuffer.a * 0.02 + 0.001)); + float normalWeight = pow(saturate(dot(gbuffer.rgb, sampleGeo.rgb)), 50.0); + float aoDiff = centerData.r - sampleData.r; + float aoWeight = exp(-(aoDiff * aoDiff) / (variance + 0.0001)); + float weight = depthWeight * normalWeight * aoWeight; + sum += sampleData * weight; + totalWeight += weight; + } + return sum / (totalWeight + EPSILON); +} + +/*--------------. +| :: SHADERS :: | +'--------------*/ +float PS_TraceAO(VSOUT input) : SV_Target +{ + //deprecated + // if (CHECKERBOARD_RENDERING) { + // #if RESOLUTION_SCALING + // if(CheckerboardSkip(uint2(input.vpos.xy), 2.0)) discard; + // #else + // if(CheckerboardSkip(uint2(input.vpos.xy), 1.0)) discard; + // #endif + // } + + float4 gbuffer = tex2D(Kernel::sNormals, input.uv); + float3 normal = gbuffer.rgb; + float depth = gbuffer.a; + if (depth == 0 || depth >= DEPTH_BOUNDARY) discard; + float3 startPos = UVToViewSpace(input.uv, depth, input); + float3 tangent, bitangent; + BuildOrthonormalBasis(normal, tangent, bitangent); + float2 noise = GetStratifiedNoise(input.vpos.xy); + float3 rayDir = GenerateHemisphereDirection(normal, noise, tangent, bitangent); + float invDepth = rcp(depth); + float totalRayLength = 0.02 * depth; + float initialStepScale = INITIAL_STEP_SCALE * rcp((float)AO_MAX_MARCH_STEPS); + float stepSize = totalRayLength * initialStepScale; + float3 rayPos = mad(rayDir, stepSize * 0.5, startPos); + rayPos += normal * depth * 0.0005; //push ray slightly OUTWARD along the normal; clears staircase artifacts + float occlusion = 0.0; + [loop] + for (int step = 0; step < AO_MAX_MARCH_STEPS; step++) { + float2 sampleUV = ViewSpaceToUV(rayPos, input); + float sceneDepth = GetDepth(sampleUV); + float depthDiff = rayPos.z - sceneDepth; + [branch] + if (depthDiff > 0.0 && depthDiff < rayPos.z) { + float3 scenePos = UVToViewSpace(sampleUV, sceneDepth, input); + float hitDistance = length(scenePos - startPos); + float normalizedDistance = hitDistance * invDepth; + occlusion = exp(-normalizedDistance * 15.0); + break; + } + stepSize *= STEP_GROWTH_FACTOR; + rayPos = mad(rayDir, stepSize, rayPos); + } + + float aoFactor = 1.0 - saturate(occlusion * AO_INTENSITY); + return aoFactor; +} + +float2 PS_TemporalFilter(VSOUT input) : SV_Target +{ + float depth = tex2D(Kernel::sDepth, input.uv).r; + //overwrite noise at boundary with clean White, prevents gaps + if (depth >= DEPTH_BOUNDARY) return float2(1.0, 1.0); //1.0 AO, 1.0 Moment + if (depth == 0) discard; + float ao = tex2D(sAOTrace, input.uv).r; + ao = lerp(1.0, ao, CalculateDepthFade(depth)); + float moment = ao * ao; + float2 flow = tex2D(Kernel::sFlow, input.uv).xy; + float confidence = tex2D(Kernel::sConfidence, input.uv).x; + confidence = saturate(confidence + log2(2.0 - confidence) * 0.6); //boost confidence + float2 rawHistory = tex2D(sPrevAO, input.uv + flow).rg; //history stores "1.0 - AO". 0.0 (Black Texture) -> Reads as 1.0 (White) + float prevAO = 1.0 - rawHistory.r; + float prevMoment = 1.0 - rawHistory.g; + float alpha = confidence * 0.98; + ao = lerp(ao, prevAO, alpha); + moment = lerp(moment, prevMoment, alpha); + //max(..., 0.001) to ensure we NEVER write exactly 0.0 again + //this tells the next frame "I contain data" + return float2(max(ao, 0.001), max(moment, 0.001)); +} + +float2 PS_StoreAO(VSOUT input) : SV_Target +{ + //must prevent history collision here + //if we store exactly 0.0 (means White), the next frame's blend pass thinks + //history is empty and resets it, causing shimmer + //so clamp to 0.0001 so the system knows "This is valid history data" + float2 data = tex2D(sAO1, input.uv).rg; + return float2(max(1.0 - data.r, 0.0001), max(1.0 - data.g, 0.0001)); //store inverted +} + +float4 PS_ToDisplay(VSOUT input) : SV_Target +{ + float depth = tex2D(Kernel::sDepth, input.uv).r; + float ao = ATrousFilter(sAO1Linear, input.uv, 2).r; //stable AO mask (fades to 1.0) + if (DEBUG_VIEW) { + #if BUFFER_COLOR_SPACE > 1 + return float4(ToOutputColorspace(ao.xxx, true), 1.0); + #else + return float4(ao.xxx, 1.0); + #endif + } + if (depth == 0 || depth >= DEPTH_BOUNDARY) discard; + float3 base = GetLinearColor(input.uv, true); + base *= ao; + return float4(ToOutputColorspace(base, true), 1.0); +} + + + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ +technique Lumenite_RTAO < + ui_label = "LUMENITE: RTAO"; + ui_tooltip = "Ray Traced Ambient Occlusion (Screen Space)."; +> +{ + pass { VertexShader = VS; PixelShader = PS_TraceAO; RenderTarget = tAOTrace; } + pass { VertexShader = VS; PixelShader = PS_TemporalFilter; RenderTarget = tAO1; } + pass { VertexShader = VS; PixelShader = PS_StoreAO; RenderTarget = tPrevAO; } + pass { VertexShader = VS; PixelShader = PS_ToDisplay; } +} + +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_SSSR.fx b/EXE/reshade-shaders/Shaders/lumenite_SSSR.fx new file mode 100644 index 00000000..f38035e0 --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_SSSR.fx @@ -0,0 +1,356 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + Filename : lumenite_SSSR.fx + Version : 2026.05.30 + Author : Afzaal (Kaidō) + Description: Stochastic Screen Space Reflections. + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#define FOV 70.0 +#define NEAR_PLANE 0.5 +#define RAY_LENGTH_SCALE 9.0 +#define RAY_ORIGIN_BIAS -0.0004 + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" +#include "./include/lumenite_Projections.fxh" +#include "./include/lumenite_Helpers.fxh" +#include "./include/lumenite_ColorManagement.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +uniform float DEPTH_BOUNDARY < + ui_type = "slider"; + ui_min = 0.001; ui_max = 0.999; ui_step = 0.001; + ui_label = "SSSR Range"; + ui_tooltip = "The Z+ range/depth in which the effect is applied."; + ui_category = ""; + hidden = false; +> = 0.85; + +uniform float DEPTH_FADE_START < + ui_type = "slider"; + ui_min = 0.1; ui_max = 1.0; ui_step = 0.01; + ui_label = "Z+ Fade Start (%)"; + ui_tooltip = "Z+ fraction where effect starts fading out (relative to Z+ boundary)"; + ui_category = ""; + hidden = true; +> = 0.75; + +uniform int MAX_STEPS < + ui_type = "drag"; + ui_min = 1; ui_max = 32; ui_step = 1; + ui_label = "Ray Resolution"; + ui_category = ""; + ui_tooltip = ""; +> = 32; + +uniform int BINARY_SEARCH_STEPS < + ui_type = "drag"; + ui_min = 1; ui_max = 8; ui_step = 1; + ui_label = "Hit Refinement"; + ui_category = ""; + ui_tooltip = ""; +> = 4; + +uniform float F0 < + ui_type = "drag"; + ui_min = 0.0; ui_max = 2.0; ui_step = 0.001; + ui_label = "Base Reflectivity (F0)"; + ui_category = ""; + ui_tooltip = ""; +> = 1.0; + +uniform float ROUGHNESS < + ui_type = "drag"; + ui_min = 0.0; ui_max = 0.3; ui_step = 0.001; + ui_label = "Roughness"; + ui_category = ""; + ui_tooltip = ""; +> = 0.1; + +uniform float BUMP_SCALE < + ui_type = "drag"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.001; + ui_label = "Bump Detail"; + ui_tooltip = "Scale of the extracted bump details. Lower = finer bumps."; +> = 0.5; + +uniform float TAIL_FEATHERING < + ui_type = "drag"; + ui_min = 0.0; ui_max = 5.0; ui_step = 0.001; + ui_label = "Tail Feathering"; + ui_category = ""; + ui_tooltip = ""; +> = 0.0; + + + +/*--------------. +| :: IMPORTS :: | +'--------------*/ +namespace Kernel { + texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; + sampler2D sFlow { Texture = tFlow; MagFilter = POINT; MinFilter = POINT; }; + + texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; + sampler2D sConfidence { Texture = tConfidence; }; + + texture tNormals { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; MipLevels = 4; }; + sampler sNormals { Texture = tNormals; }; + + texture2D tDepth { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 4; }; + sampler2D sDepth { Texture = tDepth; }; +} + +namespace LumeniteSSSR { + +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ +texture tSpec1 { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler sSpec1 { Texture = tSpec1; AddressU = CLAMP; AddressV = CLAMP; }; + +texture tSpec2 { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler sSpec2 { Texture = tSpec2; AddressU = CLAMP; AddressV = CLAMP; }; + +texture tPrevSpec { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler sPrevSpec { Texture = tPrevSpec; AddressU = CLAMP; AddressV = CLAMP; }; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +float CalculateDepthFade(float depth) +{ + float fadeStartDepth = DEPTH_BOUNDARY * DEPTH_FADE_START; + float fadeRange = DEPTH_BOUNDARY - fadeStartDepth; + return 1.0 - saturate((depth - fadeStartDepth) / fadeRange); +} + +float3 CalculateSmoothNormal(float2 uv, float4 gbuffer, int dilation, sampler SrcSampler) +{ + float3 normal = gbuffer.rgb; + float depth = gbuffer.a; + float3 normalSum = normal; + float weightSum = 1.0; + [unroll] for(int dy = -4; dy <= 4; dy++) for (int dx = -4; dx <= 4; dx++) { + float2 sampleUV = uv + float2(dx, dy) * ReShade::PixelSize * dilation; + float4 neighborData = tex2Dlod(SrcSampler, float4(sampleUV, 0, 0)); + float depthDiff = abs(neighborData.a - depth); + float normalDot = max(dot(normal, neighborData.rgb), 0.0); + float weight = exp(-depthDiff * 300.0) * pow(normalDot, 20.0); + normalSum += neighborData.rgb * weight; + weightSum += weight; + } + return normalize(normalSum / weightSum); +} + +float3 GetBackBuffer(float2 uv) +{ + return tex2Dlod(ReShade::BackBuffer, float4(uv,0,0)).rgb; +} + +float3 CalculateBumpyNormal(float2 uv, float3 geoNormal) +{ + float2 texelSize = BUFFER_PIXEL_SIZE * BUMP_SCALE; + float3 lumaWeights = float3(0.299, 0.587, 0.114); + //use gamma space intentionally + float lumaCenter = dot(GetBackBuffer(uv), lumaWeights); + float lumaRight = dot(GetBackBuffer(uv + float2(texelSize.x, 0.0)), lumaWeights); + float lumaBottom = dot(GetBackBuffer(uv + float2(0.0, texelSize.y)), lumaWeights); + //luma gradients + float dx = (lumaRight - lumaCenter) * 2.5; + float dy = (lumaBottom - lumaCenter) * 2.5; + //orthogonal tangent basis around macro geometry normal + float3 up = abs(geoNormal.z) < 0.999 ? float3(0.0, 0.0, 1.0) : float3(1.0, 0.0, 0.0); + float3 tangent = normalize(cross(up, geoNormal)); + float3 bitangent = cross(geoNormal, tangent); + //2D bump gradient to 3D tangent-space normal + float3 bumpVec = normalize(float3(-dx, -dy, 1.0)); + return normalize(tangent * bumpVec.x + bitangent * bumpVec.y + geoNormal * bumpVec.z); +} + +/*--------------. +| :: SHADERS :: | +'--------------*/ +float4 PS_TraceSpecular(VSOUT input) : SV_Target +{ + float4 gbuffer = tex2D(Kernel::sNormals, input.uv); + float3 normal = gbuffer.rgb; + float depth = gbuffer.a; + if (depth <= 0.0 || depth > DEPTH_BOUNDARY) return float4(0, 0, 0, 1); + + //process normals + normal = CalculateSmoothNormal(input.uv, gbuffer, 3, Kernel::sNormals); + if (BUMP_SCALE > 0.0) + normal = CalculateBumpyNormal(input.uv, normal); + + float3 StartPos = UVToViewSpace(input.uv, depth, input); + float3 viewDir = normalize(-StartPos); + float dynamicRayLengthNormalized = min(RAY_LENGTH_SCALE * depth, 1.0-depth); + float stepSize = dynamicRayLengthNormalized / float(MAX_STEPS); + float3 mirrorDir = reflect(-viewDir, normal); + if (dot(mirrorDir, mirrorDir) < 0.001) { //mirror reflection validation chck + return float4(0, 0, 0, 1); + } + float2 noise = GetStratifiedNoise(input.vpos.xy); + float3 jitterN = normalize(normal + float3((noise * 2.0 - 1.0) * ROUGHNESS * 0.2, 0.0)); + float3 rayDir = reflect(-viewDir, jitterN); + if (dot(rayDir, normal) < 0.0) rayDir = mirrorDir; //prevent jitter from pushing ray inside the geometry + float biasedOffset = RAY_ORIGIN_BIAS + (depth * RAY_ORIGIN_BIAS * 0.01); //intentional -ve origin bias + float3 biasedStartPos = StartPos - (normal * biasedOffset); //deliberately pushes the ray slightly into the floor, makes it immediately collide with the floor's depth, killing "joined reflections" + float t = stepSize * noise.x; + float3 spec = float3(0.0, 0.0, 0.0); + bool hitFound = false; + float distanceRatio = 0.0; + float2 finalUV = 0.0; + + for (int i = 0; i < MAX_STEPS; i++) + { + if (t >= dynamicRayLengthNormalized) + break; + + float3 currentPos = biasedStartPos + rayDir * t; + float2 hitUV = ViewSpaceToUV(currentPos, input); + + if (IsOOB(hitUV)) + break; + + float sceneDepth = tex2Dlod(Kernel::sDepth, float4(hitUV, 0, 0)).r; + if (sceneDepth > DEPTH_BOUNDARY) { + t += stepSize; + continue; + } + + float3 scenePos = UVToViewSpace(hitUV, sceneDepth, input); + float depthDiff = currentPos.z - scenePos.z; + + if (depthDiff > 0.0) //passed behind surface + { + float gateThreshold = dynamicRayLengthNormalized * 0.1; //initially, a broad thickness check + float extraThickness = (t > gateThreshold) ? 0.01 : 0.0; //if t is past the threshold, add extra thickness + float dynamicThickness = (currentPos.z * 0.05) + extraThickness; + + if (depthDiff < dynamicThickness) + { + //binary search refinement + float binarySearchT = t; + float binarySearchStep = stepSize; + float3 binarySearchCurrentPos = currentPos; + float2 binarySearchUV = hitUV; + float3 binarySearchScenePos = scenePos; + + for (int j = 0; j < BINARY_SEARCH_STEPS; j++) { + binarySearchStep *= 0.5; + binarySearchT += (binarySearchCurrentPos.z > binarySearchScenePos.z) ? -binarySearchStep : binarySearchStep; //move backwards if behind the surface, otherwise forwards + binarySearchCurrentPos = biasedStartPos + rayDir * binarySearchT; + binarySearchUV = ViewSpaceToUV(binarySearchCurrentPos, input); + float binarySearchSceneDepth = tex2Dlod(Kernel::sDepth, float4(binarySearchUV, 0, 0)).r; + binarySearchScenePos = UVToViewSpace(binarySearchUV, binarySearchSceneDepth, input); + } + + float finalDepthDiff = binarySearchCurrentPos.z - binarySearchScenePos.z; + + if (abs(finalDepthDiff) < (binarySearchCurrentPos.z * 0.01 + 0.01)) { //tighter thickness tolerance on the final refined hit to discard empty space behind thin grass + hitFound = true; + distanceRatio = binarySearchT / dynamicRayLengthNormalized; + finalUV = binarySearchUV; + break; + } + } + } + t += stepSize * noise.y; + } + + if (hitFound) { + float3 hitColor = GetLinearColor(finalUV, false); + float2 edgeFadeUV = abs(finalUV * 2.0 - 1.0); + float edgeFade = saturate(1.0 - max(edgeFadeUV.x, edgeFadeUV.y)); + edgeFade = smoothstep(0.0, 0.05, edgeFade); + float maxDistFade = pow(saturate(1.0 - distanceRatio), TAIL_FEATHERING + EPSILON); + spec = hitColor * maxDistFade * edgeFade; + } + + return float4(spec, 1.0); +} + +float4 PS_TemporalBlend(VSOUT input) : SV_Target +{ + float depth = tex2D(Kernel::sDepth, input.uv).r; + if (depth >= DEPTH_BOUNDARY) return float4(0, 0, 0, 0); + float3 spec = tex2D(sSpec1, input.uv).rgb; + float2 flow = tex2D(Kernel::sFlow, input.uv).xy; + float confidence = tex2D(Kernel::sConfidence, input.uv).x; + confidence = saturate(confidence + log2(2.0 - confidence) * 0.5); + float3 prevSpec = tex2D(sPrevSpec, input.uv + flow).rgb; + float historyMax = max(prevSpec.r, max(prevSpec.g, prevSpec.b)); + float blendWeight = (historyMax < 0.00001) ? 0.0 : (confidence * 0.98); + float3 blended = lerp(spec, prevSpec, blendWeight); + return float4(blended, 1.0); +} + +float4 PS_StoreHistory(VSOUT input) : SV_Target +{ + float depth = tex2D(Kernel::sDepth, input.uv).r; + if (depth >= DEPTH_BOUNDARY) return float4(0, 0, 0, 0); //if past boundary, store 0.0 to 'clear' history for next frame + return float4(max(tex2D(sSpec2, input.uv).rgb, 0.0001), 1.0); //clamp to 0.0001 so it knows 'valid hist data', prevents shimmer at depth boundary edges +} + +float4 PS_ToDisplay(VSOUT input) : SV_Target +{ + float3 base = GetLinearColor(input.uv, false); + float4 gbuffer = tex2D(Kernel::sNormals, input.uv); + float3 normal = gbuffer.rgb; + float depth = gbuffer.a; + float3 surfacePos = UVToViewSpace(input.uv, depth, input); + float depthFade = CalculateDepthFade(depth); + float3 viewDir = normalize(-surfacePos); + float NdotV = saturate(dot(normal, viewDir)); + float fresnel = F0 + (1.0 - F0) * pow(1.0 - NdotV, 5.0); //schlick's approximation + float3 spec = tex2D(sSpec2, input.uv).rgb; + spec *= depthFade; + spec *= fresnel; + float reflectionMask = saturate(length(spec) + fresnel * 0.5); + float3 conservationBase = base * (1.0 - reflectionMask * 0.7 * depthFade); + return float4(ToOutputColorspace(conservationBase + spec, false), 1.0); +} + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ +technique LUMENITE_SSSR < + ui_label = "LUMENITE: SSSR"; + ui_tooltip = "Stochastic Screen Space Reflections."; +> +{ + pass { VertexShader = VS; PixelShader = PS_TraceSpecular; RenderTarget = tSpec1; } + pass { VertexShader = VS; PixelShader = PS_TemporalBlend; RenderTarget = tSpec2; } + pass { VertexShader = VS; PixelShader = PS_ToDisplay; } + pass { VertexShader = VS; PixelShader = PS_StoreHistory; RenderTarget = tPrevSpec; } +} + +} diff --git a/EXE/reshade-shaders/Shaders/lumenite_TRAA.fx b/EXE/reshade-shaders/Shaders/lumenite_TRAA.fx new file mode 100644 index 00000000..3999c751 --- /dev/null +++ b/EXE/reshade-shaders/Shaders/lumenite_TRAA.fx @@ -0,0 +1,525 @@ +/* + ======================================================================== + Copyright (c) Afzaal. All rights reserved. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ======================================================================== + + GitHub : https://github.com/umar-afzaal/LumeniteFX + Discord : https://discord.gg/deXJrW2dx6 + + + Filename : lumenite_TRAA.fx + Version : 2026.07.28 + Author : Afzaal (Kaidō) + Description: Temporal Reprojection Anti-Aliasing + License : AGNYA License (https://github.com/nvb-uy/AGNYA-License) + + ======================================================================== +*/ + +/*------------------. +| :: DEFINITIONS :: | +'------------------*/ +#ifndef ENABLE_DLAA + #define ENABLE_DLAA 1 +#endif + +/*--------------. +| :: HEADERS :: | +'--------------*/ +#include "ReShade.fxh" +#include "./include/lumenite_ColorManagement.fxh" +#include "./include/lumenite_Helpers.fxh" + +/*---------------. +| :: UNIFORMS :: | +'---------------*/ +uniform int SHOW_STATUS < + ui_type = "radio"; + ui_label = " "; +#if ENABLE_DLAA + ui_text = "DLAA Prepass: Enabled."; +#else + ui_text = "DLAA Prepass: Disabled."; +#endif +>; + +#if ENABLE_DLAA + uniform bool DEBUG_EDGES < + ui_label = "Show Edge Mask"; + ui_tooltip = "Paints the detected edge mask over black background."; + > = false; + + uniform int EDGE_MODE < + ui_type = "combo"; + ui_label = "Edge Detection"; + ui_items = "Luma\0Geometric\0"; + ui_tooltip = "Luma: shading and texture edges as well; the classic DLAA mask.\n" + "Geometric: silhouettes only, ignores flat UI."; + > = 0; +#endif + +uniform float HISTORY_BLEND < + ui_type = "slider"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; + ui_label = "Temporal Blend"; + hidden = false; +> = 0.9; + +uniform float SHARP_STRENGTH < + ui_type = "drag"; + ui_min = 0; ui_max = 2.0; ui_step = 0.05; + ui_label = "Adaptive Sharpen"; + hidden = false; +> = 1.0; + +uniform float MAX_SHARP_DIFF < + ui_type = "drag"; + ui_min = 0.05; ui_max = 0.25; ui_step = 0.01; + ui_label = "Sharpen Guard"; + ui_tooltip = "Higher = more aggressive sharpening allowed.\nLower = tighter anti-ringing clamp."; + hidden = true; +> = 0.1; + +uniform float HFI_INTENSITY < + ui_type = "drag"; + ui_min = 0.0; ui_max = 0.1; ui_step = 0.001; + ui_label = "High-Frequency Injection"; + ui_tooltip = "Re-injects detail lost during Temporal blend."; +> = 0.01; + +/*--------------. +| :: IMPORTS :: | +'--------------*/ +namespace Kernel { + texture2D tFlow { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = RG16F; }; + sampler2D sFlow { Texture = tFlow; MagFilter = POINT; MinFilter = POINT; }; + + texture2D tConfidence { Width = BUFFER_WIDTH/8; Height = BUFFER_HEIGHT/8; Format = R16F; }; + sampler2D sConfidence { Texture = tConfidence; }; + + texture tNormals { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; MipLevels = 4; }; + sampler sNormals { Texture = tNormals; }; + + texture2D tDepth { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = R16F; MipLevels = 4; }; + sampler2D sDepth { Texture = tDepth; }; +} + +namespace LumeniteTRAA { +/*---------------------. +| :: RENDER TARGETS :: | +'---------------------*/ +#if ENABLE_DLAA +texture tDLAAPreFilter { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler sDLAAPreFilter { Texture = tDLAAPreFilter; MinFilter = LINEAR; MagFilter = LINEAR; MipFilter = LINEAR; }; + +texture tDLAAPrePass { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler sDLAAPrePass { Texture = tDLAAPrePass; MagFilter = POINT; MinFilter = POINT; }; +#endif + +texture tCurrHistory { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler sCurrHistory { Texture = tCurrHistory; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; }; + +texture tPrevHistory { Width = BUFFER_WIDTH; Height = BUFFER_HEIGHT; Format = RGBA16F; }; +sampler sPrevHistory { Texture = tPrevHistory; MagFilter = LINEAR; MinFilter = LINEAR; MipFilter = LINEAR; }; + +/*--------------. +| :: HELPERS :: | +'--------------*/ +//5-Tap 2D Catmull-Rom Filter (Brian Karis / Unreal Engine) +float3 SampleCatmullRom5Tap(sampler tex, float2 uv) { + float2 pos = uv * float2(BUFFER_WIDTH, BUFFER_HEIGHT); + float2 centerPos = floor(pos - 0.5) + 0.5; + float2 f = pos - centerPos; + + //1D Catmull-Rom weights + float2 f2 = f * f; + float2 f3 = f2 * f; + + float2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); + float2 w1 = 1.0 + f2 * (-2.5 + 1.5 * f); + float2 w2 = f * (0.5 + f * (2.0 - 1.5 * f)); + float2 w3 = f2 * (-0.5 + 0.5 * f); + + //group the inner positive lobes (w1, w2) for bilinear hardware + float2 w12 = w1 + w2; + float2 offset12 = w2 / (w12 + 0.00001); // Prevent div by zero + + //5-tap texture fetch in a cross pattern + float2 texCoord0 = (centerPos - float2(1.0, 0.0) + float2(0.0, offset12.y)) * BUFFER_PIXEL_SIZE; //left + float2 texCoord1 = (centerPos + float2(2.0, 0.0) + float2(0.0, offset12.y)) * BUFFER_PIXEL_SIZE; //right + float2 texCoord2 = (centerPos + float2(offset12.x, -1.0)) * BUFFER_PIXEL_SIZE; //top + float2 texCoord3 = (centerPos + float2(offset12.x, 2.0)) * BUFFER_PIXEL_SIZE; //bottom + float2 texCoord4 = (centerPos + offset12) * BUFFER_PIXEL_SIZE; //center + + //final 2D weights for the 5 taps + float weight0 = w0.x * w12.y; //left + float weight1 = w3.x * w12.y; //right + float weight2 = w12.x * w0.y; //top + float weight3 = w12.x * w3.y; //bottom + float weight4 = w12.x * w12.y; //center + + //normalize weights because we dropped the 4 corner taps (sum would be slightly < 1.0) + float weightSum = weight0 + weight1 + weight2 + weight3 + weight4; + weightSum = max(weightSum, 0.0001); + weight0 /= weightSum; + weight1 /= weightSum; + weight2 /= weightSum; + weight3 /= weightSum; + weight4 /= weightSum; + + //sample w. hw bilinear filtering (offsets take care of the interpolation) + float3 color0 = tex2Dlod(tex, float4(texCoord0, 0, 0)).rgb; + float3 color1 = tex2Dlod(tex, float4(texCoord1, 0, 0)).rgb; + float3 color2 = tex2Dlod(tex, float4(texCoord2, 0, 0)).rgb; + float3 color3 = tex2Dlod(tex, float4(texCoord3, 0, 0)).rgb; + float3 color4 = tex2Dlod(tex, float4(texCoord4, 0, 0)).rgb; + + float3 result = color0 * weight0 + color1 * weight1 + color2 * weight2 + color3 * weight3 + color4 * weight4; + //anti-ringing clamp + float3 minColor = min(min(min(color0, color1), min(color2, color3)), color4); + float3 maxColor = max(max(max(color0, color1), max(color2, color3)), color4); + return clamp(result, minColor, maxColor); +} + +//if ray misses the bounding box (tEnter > tExit), returning t = 1.0 is the safe fallback +float3 YCoCgLineBoxClip(float3 historyYCoCg, float3 meanYCoCg, float3 colorMin, float3 colorMax) { + float3 rayDir = meanYCoCg - historyYCoCg; + + rayDir = abs(rayDir) < 0.0001 ? float3(0.0001, 0.0001, 0.0001) : rayDir; //avoid div by zero + + //compute t for intersection with min and max bounds per-channel + float3 tMin = (colorMin - historyYCoCg) / rayDir; + float3 tMax = (colorMax - historyYCoCg) / rayDir; + + float3 t1 = min(tMin, tMax); + float3 t2 = max(tMin, tMax); + tMin = t1; + tMax = t2; + + //entry and exit points for ray-box intersection + float tEnter = max(max(tMin.x, tMin.y), tMin.z); + float tExit = min(min(tMax.x, tMax.y), tMax.z); + + //if ray misses the box; fallback to 1.0 (mean) to discard history + //else, clamp the entry point to [0, 1] to clip exactly at the box edge + float t = tEnter > tExit ? 1.0 : clamp(tEnter, 0.0, 1.0); + + return historyYCoCg + rayDir * t; +} + +/*--------------. +| :: SHADERS :: | +'--------------*/ +#if ENABLE_DLAA + +float4 PS_DLAAPreFilter(float4 vpos : SV_Position, float2 uv : TexCoord) : SV_Target { + float3 center = sqrt(max(GetLinearColor(uv, false), 0.0)); + float edge; + if (!EDGE_MODE) { + //luma edge in perceptual space; the extra sqrt fattens the mask + float2 px = float2(BUFFER_PIXEL_SIZE.x, 0.0); + float2 py = float2(0.0, BUFFER_PIXEL_SIZE.y); + float3 left = sqrt(max(GetLinearColor(uv - px, false), 0.0)); + float3 right = sqrt(max(GetLinearColor(uv + px, false), 0.0)); + float3 top = sqrt(max(GetLinearColor(uv - py, false), 0.0)); + float3 bottom = sqrt(max(GetLinearColor(uv + py, false), 0.0)); + float3 edges = 4.0 * abs((left + right + top + bottom) - 4.0 * center); + edge = GetLuminance(sqrt(max(edges, 0.0))); //recursive gamma compression: do another sqrt(), fattens the edge mask + } else { + float4 s0 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2(-1,-1), 0, 0)); + float4 s1 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2( 0,-1), 0, 0)); + float4 s2 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2( 1,-1), 0, 0)); + float4 s3 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2(-1, 0), 0, 0)); + float4 s4 = tex2Dlod(Kernel::sNormals, float4(uv, 0, 0)); + float4 s5 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2( 1, 0), 0, 0)); + float4 s6 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2(-1, 1), 0, 0)); + float4 s7 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2( 0, 1), 0, 0)); + float4 s8 = tex2Dlod(Kernel::sNormals, float4(uv + BUFFER_PIXEL_SIZE * float2( 1, 1), 0, 0)); + + //3x3 depth Sobel + float dC = s4.a; + float sxD = -s0.a + s2.a - 2.0 * s3.a + 2.0 * s5.a - s6.a + s8.a; + float syD = -s0.a - 2.0 * s1.a - s2.a + s6.a + 2.0 * s7.a + s8.a; + float depthEdge = saturate(sqrt(sxD * sxD + syD * syD) / (dC + 1e-5)); + + //3x3 normal Sobel + float3 sxN = -s0.xyz + s2.xyz - 2.0 * s3.xyz + 2.0 * s5.xyz - s6.xyz + s8.xyz; + float3 syN = -s0.xyz - 2.0 * s1.xyz - s2.xyz + s6.xyz + 2.0 * s7.xyz + s8.xyz; + float normalEdge = saturate(length(sxN) + length(syN)); + + edge = max(depthEdge, normalEdge); + } + return float4(center, edge); +} + +#define SAMPLE_G(uv, dx, dy) tex2Dlod(sDLAAPreFilter, float4((uv) + float2(dx, dy) * BUFFER_PIXEL_SIZE, 0, 0)) +float4 PS_DLAA(float4 vpos : SV_Position, float2 uv : TexCoord) : SV_Target { + float4 center = SAMPLE_G(uv, 0.0, 0.0); + float4 left01 = SAMPLE_G(uv, -1.5, 0.0); + float4 right01 = SAMPLE_G(uv, 1.5, 0.0); + float4 top01 = SAMPLE_G(uv, 0.0, -1.5); + float4 bottom01 = SAMPLE_G(uv, 0.0, 1.5); + + //flat-region early exit + float localEdges = max(center.a, max(max(left01.a, right01.a), max(top01.a, bottom01.a))); + if (localEdges < 0.05) return float4(center.xyz * center.xyz, 1.0); + + float4 wH = 2.0 * (left01 + right01); + float4 wV = 2.0 * (top01 + bottom01); + + float4 edgeH = abs(wH - 4.0 * center) / 4.0; + float4 edgeV = abs(wV - 4.0 * center) / 4.0; + + float4 blurredH = (wH + 2.0 * center) / 6.0; + float4 blurredV = (wV + 2.0 * center) / 6.0; + + float edgeHLum = GetLuminance(edgeH.xyz); + float edgeVLum = GetLuminance(edgeV.xyz); + float blurredHLum = GetLuminance(blurredH.xyz); + float blurredVLum = GetLuminance(blurredV.xyz); + + const float kLambda = 3.0; + const float kEpsilon = 0.1; + float edgeMaskH = saturate((kLambda * edgeHLum - kEpsilon) / (blurredVLum + 1e-5)); + float edgeMaskV = saturate((kLambda * edgeVLum - kEpsilon) / (blurredHLum + 1e-5)); + + float gate = (!EDGE_MODE) ? 1.0 : center.a; + edgeMaskH *= gate; + edgeMaskV *= gate; + + float4 clr = center; + clr = lerp(clr, blurredH, edgeMaskV); + clr = lerp(clr, blurredV, edgeMaskH * 0.5); + + //skip unnecessary work on long-edges + if (localEdges > 0.5) { + float4 h0 = right01; + float4 h1 = SAMPLE_G(uv, 3.5, 0.0); + float4 h2 = SAMPLE_G(uv, 5.5, 0.0); + float4 h3 = SAMPLE_G(uv, 7.5, 0.0); + float4 h4 = left01; + float4 h5 = SAMPLE_G(uv, -3.5, 0.0); + float4 h6 = SAMPLE_G(uv, -5.5, 0.0); + float4 h7 = SAMPLE_G(uv, -7.5, 0.0); + + float4 v0 = bottom01; + float4 v1 = SAMPLE_G(uv, 0.0, 3.5); + float4 v2 = SAMPLE_G(uv, 0.0, 5.5); + float4 v3 = SAMPLE_G(uv, 0.0, 7.5); + float4 v4 = top01; + float4 v5 = SAMPLE_G(uv, 0.0, -3.5); + float4 v6 = SAMPLE_G(uv, 0.0, -5.5); + float4 v7 = SAMPLE_G(uv, 0.0, -7.5); + + float longEdgeMaskH = (h0.a + h1.a + h2.a + h3.a + h4.a + h5.a + h6.a + h7.a) / 8.0; + float longEdgeMaskV = (v0.a + v1.a + v2.a + v3.a + v4.a + v5.a + v6.a + v7.a) / 8.0; + + longEdgeMaskH = saturate(longEdgeMaskH * 2.0 - 1.0); + longEdgeMaskV = saturate(longEdgeMaskV * 2.0 - 1.0); + + if (abs(longEdgeMaskH - longEdgeMaskV) > 0.2) { + float4 left = SAMPLE_G(uv, -1.0, 0.0); + float4 right = SAMPLE_G(uv, 1.0, 0.0); + float4 top = SAMPLE_G(uv, 0.0, -1.0); + float4 bottom = SAMPLE_G(uv, 0.0, 1.0); + + float4 longBlurredH = (h0 + h1 + h2 + h3 + h4 + h5 + h6 + h7) / 8.0; + float4 longBlurredV = (v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7) / 8.0; + + float lbHLum = GetLuminance(longBlurredH.xyz); + float lbVLum = GetLuminance(longBlurredV.xyz); + + float centerLum = GetLuminance(center.xyz); + float leftLum = GetLuminance(left.xyz); + float rightLum = GetLuminance(right.xyz); + float topLum = GetLuminance(top.xyz); + float bottomLum = GetLuminance(bottom.xyz); + + float4 clrV = center; + float4 clrH = center; + + float hx = saturate(0.0 + (lbHLum - topLum) / (centerLum - topLum + 1e-6)); + float hy = saturate(1.0 + (lbHLum - centerLum) / (centerLum - bottomLum + 1e-6)); + float vx = saturate(0.0 + (lbVLum - leftLum) / (centerLum - leftLum + 1e-6)); + float vy = saturate(1.0 + (lbVLum - centerLum) / (centerLum - rightLum + 1e-6)); + + float4 vhxy = float4(vx, vy, hx, hy); + vhxy.x = (vhxy.x == 0.0) ? 1.0 : vhxy.x; + vhxy.y = (vhxy.y == 0.0) ? 1.0 : vhxy.y; + vhxy.z = (vhxy.z == 0.0) ? 1.0 : vhxy.z; + vhxy.w = (vhxy.w == 0.0) ? 1.0 : vhxy.w; + + clrV = lerp(left, clrV, vhxy.x); + clrV = lerp(right, clrV, vhxy.y); + clrH = lerp(top, clrH, vhxy.z); + clrH = lerp(bottom, clrH, vhxy.w); + + clr = lerp(clr, clrV, longEdgeMaskV); + clr = lerp(clr, clrH, longEdgeMaskH); + } + } + + //highlight protection + float4 r0 = SAMPLE_G(uv, -1.5, -1.5); + float4 r1 = SAMPLE_G(uv, 1.5, -1.5); + float4 r2 = SAMPLE_G(uv, -1.5, 1.5); + float4 r3 = SAMPLE_G(uv, 1.5, 1.5); + float4 r = (4.0 * (r0 + r1 + r2 + r3) + center + top01 + bottom01 + left01 + right01) / 25.0; + + float mask = saturate(r.a * 3.0 - 2.0); + clr = lerp(clr, center, mask); + + return float4(clr.xyz * clr.xyz, 1.0); //store linear color here! +} + +#endif + +float4 PS_TRAA(float4 vpos : SV_Position, float2 texcoord : TexCoord) : SV_Target { + //3x3 neighborhood from DLAA prepass + static const float2 offsets[9] = { + float2(-1, -1), float2(0, -1), float2(1, -1), + float2(-1, 0) , float2(0, 0) , float2(1, 0), + float2(-1, 1) , float2(0, 1) , float2(1, 1) + }; + float3 samples[9]; + float3 samplesYCoCg[9]; + float3 meanYCoCg = float3(0, 0, 0); + + for (int i = 0; i < 9; i++) { + float2 samplePos = texcoord + BUFFER_PIXEL_SIZE * offsets[i]; + #if ENABLE_DLAA + samples[i] = tex2Dlod(sDLAAPrePass, float4(samplePos, 0, 0)).rgb; + #else + samples[i] = GetLinearColor(samplePos, false); + #endif + samplesYCoCg[i] = linearToYCoCg(samples[i]); + meanYCoCg += samplesYCoCg[i]; + } + meanYCoCg /= 9.0; + + //standard deviation per channel + float3 stddev = float3(0, 0, 0); + for (int i = 0; i < 9; i++) { + float3 diff = samplesYCoCg[i] - meanYCoCg; + stddev += diff * diff; + } + stddev = sqrt(stddev / 9.0); + + //variance-scaled bounding box in YCoCg + float3 colorMin = meanYCoCg - stddev * 1.25; + float3 colorMax = meanYCoCg + stddev * 1.25; + + float2 flow = tex2D(Kernel::sFlow, texcoord).xy; + float confidence = tex2D(Kernel::sConfidence, texcoord).x; + confidence = saturate(confidence + 0.11 * 4.0 * confidence * (1.0 - confidence)); + + float2 historyUV = texcoord + flow; + historyUV = clamp(historyUV, BUFFER_PIXEL_SIZE, 1.0 - BUFFER_PIXEL_SIZE); + float3 historyRGB = SampleCatmullRom5Tap(sPrevHistory, historyUV); + float3 historyYCoCg = linearToYCoCg(historyRGB); + + //clip history to current neighborhood bounds via line-box intersection + float3 clippedHistoryYCoCg = YCoCgLineBoxClip(historyYCoCg, meanYCoCg, colorMin, colorMax); + + //re-inject current pixel's detail into clipped history + float3 centerYCoCg = samplesYCoCg[4]; //blend against the center pixel (index 4 of the 3x3 grid) + float3 injectedHistory = clippedHistoryYCoCg + (centerYCoCg - meanYCoCg) * HFI_INTENSITY; + + //blend clipped history with current in YCoCg space + float blendVal = min(0.98, HISTORY_BLEND); + float3 blendedYCoCg = lerp(centerYCoCg, injectedHistory, confidence * blendVal); + float3 output = YCoCgToLinear(blendedYCoCg); + return float4(output, 1.0); +} + +float4 PS_ToDisplay(float4 vpos : SV_Position, float2 texcoord : TexCoord) : SV_Target { + #if ENABLE_DLAA + + if (DEBUG_EDGES) { + float edgeDbg = tex2D(sDLAAPreFilter, texcoord).a; + static const float3 edgeTint = float3(0.125, 0.698, 0.667) * float3(0.125, 0.698, 0.667); //target color squared so lands on the real hue + return float4(ToOutputColorspace(edgeTint * saturate(edgeDbg), false), 1.0); + } + + #endif + + float3 c = tex2D(sCurrHistory, texcoord).rgb; + float3 sharpened = c; + + if (SHARP_STRENGTH > 0) { + float2 off = BUFFER_PIXEL_SIZE * 0.5; + + float3 ne = tex2D(sCurrHistory, texcoord + float2( off.x, off.y)).rgb; + float3 sw = tex2D(sCurrHistory, texcoord + float2(-off.x, -off.y)).rgb; + float3 se = tex2D(sCurrHistory, texcoord + float2( off.x, -off.y)).rgb; + float3 nw = tex2D(sCurrHistory, texcoord + float2(-off.x, off.y)).rgb; + + //bounds for the neighborhood + float3 local_min = min(min(min(ne, nw), min(se, sw)), c); + float3 local_max = max(max(max(ne, nw), max(se, sw)), c); + + //high-pass + float3 diag_max = max(max(ne, nw), max(se, sw)); + float3 diag_min = min(min(ne, nw), min(se, sw)); + float3 diff_rgb = 2.0 * c + (ne + nw + se + sw) - 3.0 * (diag_max + diag_min); + + static const float3 luma_weight = float3(0.2126, 0.7152, 0.0722); + float luma_c = dot(c, luma_weight); + float luma_diff = dot(diff_rgb, luma_weight); + + //rational limit + float max_allowed = MAX_SHARP_DIFF * (luma_c + 0.1); + luma_diff = luma_diff / (rcp(SHARP_STRENGTH) + abs(luma_diff) / max(max_allowed, 0.001)); + + //lower epsilon (0.005) for more dark-area detail + float ratio = (luma_c + luma_diff) / max(luma_c, 0.005); + + //allow up to 3x brightness for extreme highlights + ratio = clamp(ratio, 0.3, 3.0); + sharpened = c * ratio; + + //anti-ringing + //instead of clamping strictly to min/max, we allow a 20% overshoot + //perceived "sharpness" while capping fireflies + float3 overshoot_min = local_min * 0.8; + float3 overshoot_max = local_max * 1.2; + + sharpened = clamp(sharpened, overshoot_min, overshoot_max); + } + + return float4(ToOutputColorspace(sharpened, false), 1.0); +} + +float4 PS_StoreHistory(float4 vpos : SV_Position, float2 texcoord : TexCoord) : SV_Target { + float3 taaResult = tex2D(sCurrHistory, texcoord).rgb; + return float4(taaResult, 1.0); +} + +/*----------------. +| :: TECHNIQUE :: | +'----------------*/ + +technique Lumenite_TRAA < + ui_label = "LUMENITE: TRAA"; + ui_tooltip = "Temporal Reprojection Anti-Aliasing."; +> +{ +#if ENABLE_DLAA + pass { VertexShader = PostProcessVS; PixelShader = PS_DLAAPreFilter; RenderTarget = tDLAAPreFilter; } + pass { VertexShader = PostProcessVS; PixelShader = PS_DLAA; RenderTarget = tDLAAPrePass; } //spatial filter +#endif + pass { VertexShader = PostProcessVS; PixelShader = PS_TRAA; RenderTarget = tCurrHistory; } //temporal filter + pass { VertexShader = PostProcessVS; PixelShader = PS_ToDisplay; } + pass { VertexShader = PostProcessVS; PixelShader = PS_StoreHistory; RenderTarget = tPrevHistory; } +} + +} diff --git a/EXE/reshade-shaders/Textures/lumenite_bluenoise256.png b/EXE/reshade-shaders/Textures/lumenite_bluenoise256.png new file mode 100644 index 00000000..bcbbcc38 Binary files /dev/null and b/EXE/reshade-shaders/Textures/lumenite_bluenoise256.png differ