// Copyright (c) 2025 Outmode // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // 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. #include "ReShade.fxh" texture2D screenTexture : COLOR; sampler2D screenSampler { Texture = screenTexture; AddressU = CLAMP; AddressV = CLAMP; }; texture2D depthTexture : DEPTH; sampler2D depthSampler { Texture = depthTexture; AddressU = CLAMP; AddressV = CLAMP; }; texture2D cursorTexture < source = "Cursor_512px.png"; > { Format = RGBA8; MipLevels = 7; Width = 512; Height = 512; }; sampler2D cursorSampler { Texture = cursorTexture; AddressU = CLAMP; AddressV = CLAMP; }; #define Monoscopic 0 #define Anaglyph 1 #define Side_By_Side 2 #define Top_Over_Bottom 3 #define Color_Plus_Depth 4 #define Free_View 5 #define Horizontal_Interlace 6 #define Vertical_Interlace 7 #define Checkerboard 8 #define Left_Side 0 #define Right_Side 1 #define Top_Side 2 #define Bottom_Side 3 #define Both_Sides 4 static const float zNear = 0.1; static const float zFar = 100.0; static const float stereoScale = 50000.0; static const float depthSamples[5] = { 0.125, 0.250, 0.375, 0.500, 0.625 }; static const int sampleCount = 5; static const float3x3 leftFilter = float3x3( float3(0.439, 0.0, 0.0), float3(0.447, 0.0, 0.0), float3(0.148, 0.0, 0.0)); static const float3x3 rightFilter = float3x3( float3(0.0, 0.095, -0.018), float3(0.0, 0.934, -0.028), float3(0.0, -0.005, 1.057)); static const float3 gammaMap = float3(1.6, 0.8, 1.0); static const float2 cursorOffset = float2(0.0015, 0.0125); static const float cursorSize = 512.0; static const float2 screenSize = float2(BUFFER_WIDTH, BUFFER_HEIGHT); static const float aspectRatio = (float)BUFFER_WIDTH / BUFFER_HEIGHT; static const float2 cursorDim = float2(cursorSize / screenSize.x, cursorSize / screenSize.y); static const float gutter = 0.001; static const float2 minUV = float2(gutter, 0.0); static const float2 maxUV = float2(1.0 - gutter, 1.0); uniform int stereoMode < ui_label = "Display Mode"; ui_tooltip = "3D Output Type"; ui_type = "combo"; ui_items = "Monoscopic\0Anaglyph\0Side-by-Side\0Top-Bottom\0Color+Depth\0Free-View\0Horizontal\0Vertical\0Checkerboard\0"; > = 0; uniform float stereoStrength < ui_label = "Stereo Strength"; ui_tooltip = "Eye Separation"; ui_type = "slider"; ui_min = 0.0; ui_max = 100.0; ui_step = 1.0; > = 50.0; uniform float stereoDepth < ui_label = "Depth Range"; ui_tooltip = "Z-Buffer Scaling"; ui_type = "slider"; ui_min = 0.0; ui_max = 100.0; ui_step = 1.0; > = 50.0; uniform float stereoOffset < ui_label = "Parallax Overlap"; ui_tooltip = "Zero Depth Plane"; ui_type = "slider"; ui_min = 0.0; ui_max = 100.0; ui_step = 1.0; > = 50.0; uniform bool swapLeftRight < #if __RESHADE__ >= 40500 ui_text = "Press \'+\' to toggle side-by-side mouse cursor."; #endif ui_label = "Swap Left/Right"; ui_tooltip = "Reverse Eyes"; ui_category = "Advanced Options"; ui_category_closed = true; > = false; uniform bool showDepth < ui_label = "Show Depth Buffer"; ui_tooltip = "Z-Buffer Debug"; ui_category = "Advanced Options"; > = false; uniform bool toggleCursor < source = "key"; keycode = 0xBB; mode = "toggle"; >; uniform float2 mousePosition < source = "mousepoint"; >; float getStereoStrength() { return stereoStrength / 100.0 * 0.25 + 0.25; } float getStereoDepth() { return stereoDepth / 100.0 * 8.0 - 0.25; } float getStereoOffset() { return (1.0 - stereoOffset / 100.0) / 100.0; } float getParallax(float depth) { return -getStereoDepth() / depth; } float4 getColor(sampler2D tex, float2 uv) { float4 color = tex2D(tex, uv).rgba; if (uv.x <= 0.0 || uv.x >= 1.0 || uv.y <= 0.0 || uv.y >= 1.0) color.rgb = float3(0, 0, 0); return color; } float3 correctColor(float3 original) { float3 corrected; corrected.r = pow(original.r, 1.0 / gammaMap.r); corrected.g = pow(original.g, 1.0 / gammaMap.g); corrected.b = pow(original.b, 1.0 / gammaMap.b); return corrected; } float getDepth(sampler2D tex, float2 uv, bool adjust = true) { #if RESHADE_DEPTH_INPUT_IS_UPSIDE_DOWN uv.y = 1.0 - uv.y; #endif float depth = tex2D(tex, uv).r; #if RESHADE_DEPTH_INPUT_IS_LOGARITHMIC depth = (exp(depth * log(1.01)) - 1.0) / 0.01; #endif #if RESHADE_DEPTH_INPUT_IS_REVERSED == 0 depth = 1.0 - depth; #endif if (adjust) depth = 2.0 / (-99.0 * depth + 101.0); depth = (2.0 * zNear * zFar) / (zFar + zNear - depth * (zFar - zNear)); depth /= zFar - zNear; return depth; } float2 getUV(float2 uv, int horz, int vert) { float2 result = uv; if (horz == Left_Side) { result.x = uv.x * 2.0; } else if (horz == Right_Side) { result.x = (uv.x - 0.5) * 2.0; } else if (vert == Top_Side) { result.y = uv.y * 2.0; } else if (vert == Bottom_Side) { result.y = (uv.y - 0.5) * 2.0; } return result; } float3 combineStereoViews(float3 leftColor, float3 rightColor, float4 pixelPosition, int horz, int vert) { float3 result = float3(1, 1, 1); int2 currentPixel = int2(pixelPosition.xy); if (swapLeftRight) { float3 tempColor = leftColor; leftColor = rightColor; rightColor = tempColor; } if (stereoMode == Anaglyph) { result = saturate(mul(leftColor, leftFilter)) + saturate(mul(rightColor,rightFilter)); result = correctColor(result); } else if (stereoMode == Side_By_Side) { if (horz == Left_Side) result = leftColor; else result = rightColor; } else if (stereoMode == Top_Over_Bottom) { if (vert == Top_Side) result = leftColor; else result = rightColor; } else if (stereoMode == Free_View) { if (horz == Left_Side) result = leftColor; else result = rightColor; } else if (stereoMode == Horizontal_Interlace) { if (currentPixel.y % 2 == 0) result = leftColor; else result = rightColor; } else if (stereoMode == Vertical_Interlace) { if (currentPixel.x % 2 == 0) result = leftColor; else result = rightColor; } else if (stereoMode == Checkerboard) { if (currentPixel.x % 2 == 0 && currentPixel.y % 2 == 0) result = leftColor; else if (currentPixel.x % 2 == 1 && currentPixel.y % 2 == 1) result = leftColor; else result = rightColor; } return result; } float2 clampEdge(float2 inUV) { const float edgeStretch = 0.333; if (inUV.x < minUV.x) inUV.x = (minUV.x - inUV.x) * edgeStretch; if (inUV.x > maxUV.x) inUV.x = maxUV.x + (maxUV.x - inUV.x) * edgeStretch; return clamp(inUV, minUV, maxUV); } float3 generateStereoImage(float2 uv, float4 pixelPosition, int horz, int vert) { const float centerDepth = getDepth(depthSampler, clampEdge(uv)); float minDepthLeft = centerDepth; float minDepthRight = centerDepth; float2 sampleUV = float2(0, 0); for (int i = 0; i < sampleCount; ++i) { sampleUV.x = (depthSamples[i] * getStereoStrength() / aspectRatio) / stereoScale + getStereoOffset(); minDepthLeft = min(minDepthLeft, getDepth(depthSampler, clampEdge(uv + sampleUV))); minDepthRight = min(minDepthRight, getDepth(depthSampler, clampEdge(uv - sampleUV))); } float parallaxLeft = (getStereoStrength() / aspectRatio * getParallax(minDepthLeft)) / stereoScale + getStereoOffset(); float parallaxRight = (getStereoStrength() / aspectRatio * getParallax(minDepthRight)) / stereoScale + getStereoOffset(); float3 colorLeft = getColor(screenSampler, clampEdge(uv + float2(parallaxLeft, 0.0))).rgb; float3 colorRight = getColor(screenSampler, clampEdge(uv - float2(parallaxRight, 0.0))).rgb; return combineStereoViews(colorLeft, colorRight, pixelPosition, horz, vert); } float2 iconUV(float2 uv, float2 cursorCoord) { return float2(((uv.x - cursorOffset.x) / cursorDim.x) * 2.0, (uv.y - cursorOffset.y) / cursorDim.y) - (cursorCoord / cursorDim) * float2(2, 1) + float2(0.5 + cursorOffset.x, 0.5 + cursorOffset.y); } [shader("pixel")] float4 StereoPS(float4 pixelPosition : SV_Position, float2 uv : TEXCOORD0) : SV_Target { float4 color = float4(0, 0, 0, 1); int horizontalSide = pixelPosition.x < screenSize.x * 0.5 ? Left_Side : Right_Side; int verticalSide = pixelPosition.y < screenSize.y * 0.5 ? Top_Side : Bottom_Side; float2 cursorCoord = float2(0, 0); if (stereoMode == Monoscopic) { color.rgb = getColor(screenSampler, uv).rgb; } else if (stereoMode == Side_By_Side) { color.rgb = generateStereoImage(getUV(uv, horizontalSide, Both_Sides), pixelPosition, horizontalSide, Both_Sides); cursorCoord = mousePosition / screenSize * float2(0.5, 1.0) + horizontalSide * float2(0.5, 0.0); } else if (stereoMode == Top_Over_Bottom) { color.rgb = generateStereoImage(getUV(uv, Both_Sides, verticalSide), pixelPosition, Both_Sides, verticalSide); cursorCoord = mousePosition / screenSize * float2(1.0, 0.5) + (verticalSide - 2) * float2(0.0, 0.5); } else if (stereoMode == Color_Plus_Depth) { float2 scaled = uv * float2(1.0, 1.0) - float2(0.0, 0.0); if (all(scaled > float2(0, 0)) && all(scaled < float2(1, 1))) { if (horizontalSide == Left_Side) { color.rgb = getColor(screenSampler, getUV(uv, horizontalSide, Both_Sides)).rgb; } else { float depth = 1.0 - getDepth(depthSampler, getUV(uv, horizontalSide, Both_Sides), false); color.rgb = depth.rrr; } } } else if (stereoMode == Free_View) { float2 freeUV = getUV(uv, horizontalSide, Both_Sides); freeUV.y = freeUV.y * 2.0 - 0.5; color.rgb = generateStereoImage(freeUV, pixelPosition, horizontalSide, Both_Sides); cursorCoord = mousePosition / screenSize * float2(0.5, 1.0) + horizontalSide * float2(0.5, 0.0); cursorCoord.y = cursorCoord.y * 0.5 + 0.25; } else { color.rgb = generateStereoImage(uv, pixelPosition, Both_Sides, Both_Sides); } if (toggleCursor && (stereoMode == Side_By_Side || stereoMode == Top_Over_Bottom || stereoMode == Free_View) && abs(uv.x - cursorCoord.x) < cursorDim.x && abs(uv.y - cursorCoord.y) < cursorDim.y) { float2 iconCoords = iconUV(uv, cursorCoord); if (stereoMode == Free_View) iconCoords.y = iconCoords.y * 2.0 - 0.5; float4 iconColor = tex2D(cursorSampler, iconCoords); if (iconColor.a > 0.0) iconColor.rgb /= iconColor.a; color.rgb = lerp(color.rgb, iconColor.rgb, iconColor.a); } if (showDepth) { if (horizontalSide == Left_Side) { color.rgb = getColor(screenSampler, uv).rgb; } else { float depth = 1.0 - getDepth(depthSampler, uv, false); color.rgb = depth.rrr; } return color; } return color; } technique Rendepth < ui_tooltip = "Stereoscopic 2D-to-3D Conversion"; >{ pass StereoPass { VertexShader = PostProcessVS; PixelShader = StereoPS; } }