///
/// Shiny SSRR - Screen Space Raytraced Reflections - (c) 2021 Kronnect
///
using UnityEngine;
using UnityEngine.Rendering;
namespace ShinySSRR {
public enum OutputMode {
Final,
OnlyReflections,
SideBySideComparison
}
public enum RaytracingPreset {
Fast = 10,
Medium = 20,
High = 30,
Superb = 35,
Ultra = 40
}
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
[ImageEffectAllowedInSceneView]
public class ShinySSRR : MonoBehaviour {
class SSRPass {
enum Pass {
CopyExact = 0,
SSRSurf = 1,
Resolve = 2,
BlurHoriz = 3,
BlurVert = 4,
Debug = 5,
Combine = 6,
CombineWithCompare = 7,
GBuffPass = 8,
Copy = 9
}
const string SHINY_CBUFNAME = "Shiny_SSRR";
const float GOLDEN_RATIO = 0.618033989f;
const int MIP_COUNT = 5;
Material sMat;
Texture noiseTex;
ShinySSRR settings;
readonly Plane[] frustumPlanes = new Plane[6];
CommandBuffer cmd;
int[] rtPyramid;
public void Setup(ShinySSRR settings, bool deferred) {
this.settings = settings;
if (cmd == null) {
cmd = new CommandBuffer {
name = SHINY_CBUFNAME
};
}
if (sMat == null) {
Shader shader = Shader.Find("Hidden/Kronnect/SSR");
sMat = new Material(shader);
}
if (noiseTex == null) {
noiseTex = Resources.Load("SSR/blueNoiseSSR64");
}
sMat.SetTexture(ShaderParams.NoiseTex, noiseTex);
// set global settings
sMat.SetVector(ShaderParams.SSRSettings2, new Vector4(settings.jitter, settings.contactHardening, settings.reflectionsMultiplier, settings.vignetteSize));
sMat.SetVector(ShaderParams.SSRSettings4, new Vector4(settings.separationPos, settings.reflectionsMinIntensity, settings.reflectionsMaxIntensity, settings.specularSoftenPower));
sMat.SetVector(ShaderParams.SSRBlurStrength, new Vector4(settings.blurStrength.x, settings.blurStrength.y, 0, 0));
sMat.SetVector(ShaderParams.SSRSettings5, new Vector4(settings.thicknessFine * settings.thickness, settings.smoothnessThreshold, 0, 0));
if (settings.specularControl) {
sMat.EnableKeyword(ShaderParams.SKW_DENOISE);
} else {
sMat.DisableKeyword(ShaderParams.SKW_DENOISE);
}
sMat.SetFloat(ShaderParams.MinimumBlur, settings.minimumBlur);
if (deferred) {
if (settings.jitter > 0) {
sMat.EnableKeyword(ShaderParams.SKW_JITTER);
} else {
sMat.DisableKeyword(ShaderParams.SKW_JITTER);
}
if (settings.refineThickness) {
sMat.EnableKeyword(ShaderParams.SKW_REFINE_THICKNESS);
} else {
sMat.DisableKeyword(ShaderParams.SKW_REFINE_THICKNESS);
}
sMat.SetVector(ShaderParams.SSRSettings, new Vector4(settings.thickness, settings.sampleCount, settings.binarySearchIterations, settings.maxRayLength));
sMat.SetVector(ShaderParams.MaterialData, new Vector4(0, settings.fresnel, settings.fuzzyness, settings.decay));
}
if (rtPyramid == null || rtPyramid.Length != MIP_COUNT) {
rtPyramid = new int[MIP_COUNT];
}
for (int k = 0; k < rtPyramid.Length; k++) {
rtPyramid[k] = Shader.PropertyToID("_BlurRTMip" + k);
}
}
public void Execute(RenderTexture source, RenderTexture destination, Camera cam) {
ExecuteInternal(source, destination, cam);
Graphics.Blit(source, destination, sMat, (int)Pass.CopyExact);
}
void ExecuteInternal(RenderTexture source, RenderTexture renderTexture, Camera cam) {
// ignore SceneView depending on setting
if (cam.cameraType == CameraType.SceneView) {
if (!settings.showInSceneView) return;
} else {
// ignore any camera other than GameView
if (cam.cameraType != CameraType.Game) return;
}
RenderTextureDescriptor sourceDesc = source.descriptor;
sourceDesc.colorFormat = settings.lowPrecision ? RenderTextureFormat.ARGB32 : RenderTextureFormat.ARGBHalf;
sourceDesc.width /= settings.downsampling;
sourceDesc.height /= settings.downsampling;
sourceDesc.msaaSamples = 1;
float goldenFactor = GOLDEN_RATIO;
if (settings.animatedJitter) {
goldenFactor *= (Time.frameCount % 480);
}
Shader.SetGlobalVector(ShaderParams.SSRSettings3, new Vector4(sourceDesc.width, sourceDesc.height, goldenFactor, settings.depthBias));
if (settings.isDeferredActive) {
// init command buffer
cmd.Clear();
// pass UNITY_MATRIX_V
sMat.SetMatrix(ShaderParams.WorldToViewDir, cam.worldToCameraMatrix);
// prepare ssr target
cmd.GetTemporaryRT(ShaderParams.RayCast, sourceDesc, FilterMode.Point);
// raytrace using gbuffers
cmd.Blit(source, ShaderParams.RayCast, sMat, (int)Pass.GBuffPass);
} else {
// early exit if no reflection objects
int count = Reflections.instances.Count;
if (count == 0) return;
bool firstSSR = true;
GeometryUtility.CalculateFrustumPlanes(cam, frustumPlanes);
for (int k = 0; k < count; k++) {
Reflections go = Reflections.instances[k];
if (go == null) continue;
int rendererCount = go.ssrRenderers.Count;
for (int j = 0; j < rendererCount; j++) {
Reflections.SSR_Renderer ssrRenderer = go.ssrRenderers[j];
Renderer goRenderer = ssrRenderer.renderer;
if (goRenderer == null || !goRenderer.isVisible) continue;
// if object is part of static batch, check collider bounds (if existing)
if (goRenderer.isPartOfStaticBatch) {
if (ssrRenderer.hasStaticBounds) {
// check artifically computed bounds
if (!GeometryUtility.TestPlanesAABB(frustumPlanes, ssrRenderer.staticBounds)) continue;
} else if (ssrRenderer.collider != null) {
// check if object is visible by current camera using collider bounds
if (!GeometryUtility.TestPlanesAABB(frustumPlanes, ssrRenderer.collider.bounds)) continue;
}
} else {
// check if object is visible by current camera using renderer bounds
if (!GeometryUtility.TestPlanesAABB(frustumPlanes, goRenderer.bounds)) continue;
}
if (!ssrRenderer.isInitialized) {
ssrRenderer.Init(sMat);
ssrRenderer.UpdateMaterialProperties(go, settings);
}
#if UNITY_EDITOR
else if (!Application.isPlaying) {
ssrRenderer.UpdateMaterialProperties(go, settings);
}
#endif
if (ssrRenderer.exclude) continue;
if (firstSSR) {
firstSSR = false;
// init command buffer
cmd.Clear();
// prepare ssr target
cmd.GetTemporaryRT(ShaderParams.RayCast, sourceDesc, FilterMode.Point);
cmd.SetRenderTarget(ShaderParams.RayCast);
cmd.ClearRenderTarget(true, true, new Color(0, 0, 0, 0));
}
for (int s = 0; s < ssrRenderer.ssrMaterials.Length; s++) {
if (go.subMeshMask <= 0 || ((1 << s) & go.subMeshMask) != 0) {
Material ssrMat = ssrRenderer.ssrMaterials[s];
cmd.DrawRenderer(goRenderer, ssrMat, s, (int)Pass.SSRSurf);
}
}
}
}
if (firstSSR) return;
}
// Resolve reflections
RenderTextureDescriptor copyDesc = sourceDesc;
copyDesc.depthBufferBits = 0;
cmd.GetTemporaryRT(ShaderParams.ReflectionsTex, copyDesc);
cmd.Blit(source, ShaderParams.ReflectionsTex, sMat, (int)Pass.Resolve);
RenderTargetIdentifier input = ShaderParams.ReflectionsTex;
// Pyramid blur
copyDesc.width /= settings.blurDownsampling;
copyDesc.height /= settings.blurDownsampling;
for (int k = 0; k < MIP_COUNT; k++) {
copyDesc.width = Mathf.Max(2, copyDesc.width / 2);
copyDesc.height = Mathf.Max(2, copyDesc.height / 2);
cmd.GetTemporaryRT(rtPyramid[k], copyDesc, FilterMode.Bilinear);
cmd.GetTemporaryRT(ShaderParams.BlurRT, copyDesc, FilterMode.Bilinear);
cmd.Blit(input, ShaderParams.BlurRT, sMat, (int)Pass.BlurHoriz);
cmd.Blit(ShaderParams.BlurRT, rtPyramid[k], sMat, (int)Pass.BlurVert);
cmd.ReleaseTemporaryRT(ShaderParams.BlurRT);
input = rtPyramid[k];
}
// Output
int finalPass;
if (settings.outputMode == OutputMode.Final) {
finalPass = (int)Pass.Combine;
} else if (settings.outputMode == OutputMode.SideBySideComparison) {
finalPass = (int)Pass.CombineWithCompare;
} else {
finalPass = (int)Pass.Debug;
}
cmd.Blit(ShaderParams.ReflectionsTex, source, sMat, finalPass);
if (settings.stopNaN) {
RenderTextureDescriptor nanDesc = source.descriptor;
nanDesc.depthBufferBits = 0;
nanDesc.msaaSamples = 1;
cmd.GetTemporaryRT(ShaderParams.NaNBuffer, nanDesc);
cmd.Blit(source, ShaderParams.NaNBuffer, sMat, (int)Pass.CopyExact);
cmd.Blit(ShaderParams.NaNBuffer, source, sMat, (int)Pass.CopyExact);
}
// Clean up
for (int k = 0; k < rtPyramid.Length; k++) {
cmd.ReleaseTemporaryRT(rtPyramid[k]);
}
cmd.ReleaseTemporaryRT(ShaderParams.RayCast);
Graphics.ExecuteCommandBuffer(cmd);
}
public void Cleanup() {
if (sMat != null) {
DestroyImmediate(sMat);
}
}
}
[Header("General Settings")]
[Tooltip("Show reflections in SceneView window")]
public bool showInSceneView = true;
[Tooltip("Downsampling multiplier applied to the final blurred reflections")]
[Range(1, 8)] public int downsampling = 1;
[Tooltip("Bias applied to depth checking. Increase if reflections desappear at the distance when downsampling is used")]
[Min(0)] public float depthBias = 0.01f;
[Tooltip("Show final result / debug view or compare view")]
public OutputMode outputMode = OutputMode.Final;
[Tooltip("Position of the dividing line")]
[Range(-0.01f, 1.01f)] public float separationPos = 0.5f;
[Tooltip("HDR reflections")]
public bool lowPrecision;
[Tooltip("Prevents out of range colors when composing reflections in the destination buffer. This operation performs a ping-pong copy of the frame buffer which can be expensive. Use only if required.")]
public bool stopNaN;
[Tooltip("Max number of samples used during the raymarch loop")]
[Range(4, 128)] public int sampleCount = 16;
[HideInInspector]
public float stepSize; // no longer used; kept for backward compatibility during upgrade
[Tooltip("Maximum reflection distance")]
public float maxRayLength;
[Tooltip("Assumed thickness of geometry in the depth buffer before binary search")]
public float thickness = 0.2f;
[Tooltip("Number of refinements steps when a reflection hit is found")]
[Range(0, 16)] public int binarySearchIterations = 6;
[Tooltip("Increase accuracy of reflection hit after binary search by discarding points further than a reduced thickness.")]
public bool refineThickness;
[Tooltip("Assumed thickness of geometry in the depth buffer after binary search")]
[Range(0.005f, 1f)]
public float thicknessFine = 0.05f;
[Tooltip("Jitter helps smoothing edges")]
[Range(0, 1f)] public float jitter = 0.3f;
[Tooltip("Animates jitter every frame")]
public bool animatedJitter = true;
[Header("Reflection Intensity")]
[Tooltip("Minimum smoothness to receive reflections")]
[Range(0,1)]
public float smoothnessThreshold;
[Tooltip("Reflection multiplier")]
[Range(0, 2)]
public float reflectionsMultiplier = 1f;
[Tooltip("Reflection min intensity")]
[Range(0, 1)]
public float reflectionsMinIntensity;
[Tooltip("Reflection max intensity")]
[Range(0, 1)]
public float reflectionsMaxIntensity = 1f;
[Range(0, 1)]
[Tooltip("Reduces reflection based on view angle")]
public float fresnel = 0.75f;
[Tooltip("Reflection decay with distance to reflective point")]
public float decay = 2f;
[Tooltip("Reduces intensity of specular reflections")]
public bool specularControl;
[Min(0), Tooltip("Power of the specular filter")]
public float specularSoftenPower = 15f;
[Tooltip("Controls the attenuation range of effect on screen borders")]
[Range(0.5f, 2f)]
public float vignetteSize = 1.1f;
[Header("Reflection Sharpness")]
[Min(0)]
[Tooltip("Ray dispersion with distance")]
public float fuzzyness;
[Tooltip("Makes sharpen reflections near objects")]
public float contactHardening;
[Range(0, 4f)]
[Tooltip("Produces sharper reflections based on distance")]
public float minimumBlur = 0.25f;
[Tooltip("Downsampling multiplier applied to the blur")]
[Range(1, 8)] public int blurDownsampling = 1;
[Tooltip("Custom directional blur strength")]
public Vector2 blurStrength = Vector2.one;
SSRPass renderPass;
Camera cam;
public bool isDeferredActive {
get {
if (cam != null) {
return cam.renderingPath != RenderingPath.Forward;
}
return false;
}
}
void OnDisable() {
if (renderPass != null) {
renderPass.Cleanup();
}
}
void OnEnable() {
if (maxRayLength == 0) {
maxRayLength = Mathf.Max(0.1f, stepSize * sampleCount);
}
if (renderPass == null) {
renderPass = new SSRPass();
}
cam = GetComponent();
cam.depthTextureMode |= DepthTextureMode.Depth;
}
void OnValidate() {
decay = Mathf.Max(1f, decay);
if (maxRayLength == 0) {
maxRayLength = stepSize * sampleCount;
}
maxRayLength = Mathf.Max(0.1f, maxRayLength);
fuzzyness = Mathf.Max(0, fuzzyness);
thickness = Mathf.Max(0.01f, thickness);
thicknessFine = Mathf.Max(0.01f, thicknessFine);
contactHardening = Mathf.Max(0, contactHardening);
reflectionsMaxIntensity = Mathf.Max(reflectionsMinIntensity, reflectionsMaxIntensity);
blurStrength.x = Mathf.Max(blurStrength.x, 0f);
blurStrength.y = Mathf.Max(blurStrength.y, 0f);
}
void OnRenderImage(RenderTexture source, RenderTexture destination) {
renderPass.Setup(this, isDeferredActive);
renderPass.Execute(source, destination, cam);
}
public void ApplyRaytracingPreset(RaytracingPreset preset) {
switch (preset) {
case RaytracingPreset.Fast:
sampleCount = 16;
maxRayLength = 6;
binarySearchIterations = 4;
downsampling = 3;
thickness = 0.5f;
refineThickness = false;
jitter = 0.3f;
break;
case RaytracingPreset.Medium:
sampleCount = 24;
maxRayLength = 12;
binarySearchIterations = 5;
downsampling = 2;
refineThickness = false;
break;
case RaytracingPreset.High:
sampleCount = 48;
maxRayLength = 24;
binarySearchIterations = 6;
downsampling = 1;
refineThickness = false;
thicknessFine = 0.05f;
break;
case RaytracingPreset.Superb:
sampleCount = 88;
maxRayLength = 48;
binarySearchIterations = 7;
downsampling = 1;
refineThickness = true;
thicknessFine = 0.02f;
break;
case RaytracingPreset.Ultra:
sampleCount = 128;
maxRayLength = 64;
binarySearchIterations = 8;
downsampling = 1;
refineThickness = true;
thicknessFine = 0.02f;
break;
}
}
}
}