Skip to content

Commit

Permalink
Shadow volumes (#234)
Browse files Browse the repository at this point in the history
Initial support for shadow volumes, generated using compute shaders.
  • Loading branch information
mlavik1 authored Mar 23, 2024
1 parent 87d3a1b commit 25772d9
Show file tree
Hide file tree
Showing 8 changed files with 390 additions and 42 deletions.
7 changes: 7 additions & 0 deletions Assets/Editor/VolumeRenderedObjectCustomInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ public override void OnInspectorGUI()
);
// Convert back to linear scale, before setting updated value.
volrendObj.SetGradientLightingThreshold(new Vector2(gradLightThreshold.x * gradLightThreshold.x, gradLightThreshold.y * gradLightThreshold.y));

ShadowVolumeManager shadowVoumeManager = volrendObj.GetComponent<ShadowVolumeManager>();
bool enableShadowVolume = GUILayout.Toggle(shadowVoumeManager != null, "Enable shadow volume (expensive)");
if (enableShadowVolume && shadowVoumeManager == null)
shadowVoumeManager = volrendObj.gameObject.AddComponent<ShadowVolumeManager>();
else if (!enableShadowVolume && shadowVoumeManager != null)
GameObject.DestroyImmediate(shadowVoumeManager);
}
}

Expand Down
76 changes: 76 additions & 0 deletions Assets/Resources/ShadowVolume.compute
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#pragma kernel ShadowVolumeMain

#pragma multi_compile __ CUBIC_INTERPOLATION_ON
#pragma multi_compile __ CROSS_SECTION_ON

sampler3D _VolumeTexture;
sampler2D _TFTex;

float _MinVal;
float _MaxVal;

int3 _Dimension;
float3 _LightDirection;
float3 _TextureSize;
uint3 _DispatchOffsets;

RWTexture3D<float> _ShadowVolume;

#include "Assets/Shaders/Include/TricubicSampling.cginc"
#include "Assets/Shaders/Include/VolumeCutout.cginc"

float getDensity(float3 pos)
{
return interpolateTricubicFast(_VolumeTexture, float3(pos.x, pos.y, pos.z), _TextureSize).r;
}

// Gets the colour from a 1D Transfer Function (x = density)
float4 getTF1DColour(float density)
{
return tex2Dlod(_TFTex, float4(density, 0.0f, 0.0f, 0.0f));
}

float calculateShadow(float3 startPos, float3 lightDir)
{
float4 col = float4(0.0f, 0.0f, 0.0f, 0.0f);
int numSteps = 32;
float stepSize = 0.25f / numSteps;
for (int iStep = 1; iStep < numSteps; iStep++)
{
const float3 currPos = startPos + lightDir * stepSize * iStep;

if (currPos.x < 0.0f || currPos.y < 0.0f || currPos.z < 0.0f || currPos.x > 1.0f || currPos.y > 1.0f || currPos.z > 1.0f)
break;

// Perform slice culling (cross section plane)
if (IsCutout(currPos))
continue;

// Get the dansity/sample value of the current position
const float density = getDensity(currPos);

// Apply visibility window
if (density < _MinVal || density > _MaxVal) continue;

// Apply 1D transfer function
float4 src = getTF1DColour(density);
if (src.a == 0.0)
continue;
src.rgb *= src.a;
col = (1.0f - col.a) * src + col;
}
return col.a;
}

[numthreads(8, 8, 8)]
void ShadowVolumeMain(uint3 id : SV_DispatchThreadID)
{
id += _DispatchOffsets;
if (id.x < uint(_Dimension.x) && id.y < uint(_Dimension.y) & id.z < uint(_Dimension.z))
{
float3 rayOrigin = float3((float)id.x / uint(_Dimension.x), (float)id.y / uint(_Dimension.y), (float)id.z / uint(_Dimension.z));
float3 rayDir = _LightDirection;
float shadow = calculateShadow(rayOrigin, rayDir);
_ShadowVolume[id.xyz] = shadow;
}
}
7 changes: 7 additions & 0 deletions Assets/Resources/ShadowVolume.compute.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

235 changes: 235 additions & 0 deletions Assets/Scripts/Lighting/ShadowVolumeManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Experimental.GlobalIllumination;
using UnityEngine.Rendering;
using LightType = UnityEngine.LightType;

namespace UnityVolumeRendering
{
[ExecuteInEditMode]
[RequireComponent(typeof(VolumeRenderedObject))]
public class ShadowVolumeManager : MonoBehaviour
{
private const int NUM_DISPATCH_CHUNKS = 5;
private const int dispatchCount = NUM_DISPATCH_CHUNKS * NUM_DISPATCH_CHUNKS * NUM_DISPATCH_CHUNKS;

private VolumeRenderedObject volumeRenderedObject = null;
private RenderTexture shadowVolumeTexture = null;
private Vector3 lightDirection;
private bool initialised = false;
private ComputeShader shadowVolumeShader;
private int handleMain;
private int currentDispatchIndex = 0;
private float cooldown = 1.0f;
private double lastUpdateTimeEditor = 0.0f;
private bool isDirty = true;

private void Awake()
{
if (!SystemInfo.supportsComputeShaders)
{
Debug.LogError("Shadow volumes not supported on this platform (SystemInfo.supportsComputeShaders == false)");
DestroyImmediate(this);
}
}

private void Start()
{
if (!initialised)
Initialise();
}

private void OnValidate()
{
if (!initialised)
Initialise();
}

private void Update()
{
HandleUpdate();
}

private void OnEnable()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.update += OnEditorUpdate;
#endif
if (volumeRenderedObject != null)
{
volumeRenderedObject.meshRenderer.sharedMaterial.EnableKeyword("SHADOWS_ON");
}
}

private void OnDisable()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.update -= OnEditorUpdate;
#endif
currentDispatchIndex = 0;
if (volumeRenderedObject != null)
{
volumeRenderedObject.meshRenderer.sharedMaterial.DisableKeyword("SHADOWS_ON");
}
}

private void OnEditorUpdate()
{
#if UNITY_EDITOR
if (!UnityEditor.EditorApplication.isPlaying)
{
if (isDirty || (UnityEditor.EditorApplication.timeSinceStartup - lastUpdateTimeEditor > 0.02f))
{
HandleUpdate();
UnityEditor.EditorUtility.SetDirty(UnityEditor.SceneView.lastActiveSceneView);
}
}
#endif
}

private void Initialise()
{
Debug.Log("Initialising shadow volume buffers");
volumeRenderedObject = GetComponent<VolumeRenderedObject>();
Debug.Assert(volumeRenderedObject != null);

Vector3Int shadowVolumeDimensions = new Vector3Int(512, 512, 512);

shadowVolumeTexture = new RenderTexture(shadowVolumeDimensions.x, shadowVolumeDimensions.y, 0, RenderTextureFormat.RHalf, RenderTextureReadWrite.Linear);
shadowVolumeTexture.dimension = TextureDimension.Tex3D;
shadowVolumeTexture.volumeDepth = shadowVolumeDimensions.z;
shadowVolumeTexture.enableRandomWrite = true;
shadowVolumeTexture.wrapMode = TextureWrapMode.Clamp;
shadowVolumeTexture.Create();

volumeRenderedObject.meshRenderer.sharedMaterial.SetTexture("_ShadowVolume", shadowVolumeTexture);
volumeRenderedObject.meshRenderer.sharedMaterial.SetVector("_ShadowVolumeTextureSize", new Vector3(shadowVolumeDimensions.x, shadowVolumeDimensions.y, shadowVolumeDimensions.z));

shadowVolumeShader = Resources.Load("ShadowVolume") as ComputeShader;
handleMain = shadowVolumeShader.FindKernel("ShadowVolumeMain");
if (handleMain < 0)
{
Debug.LogError("Shadow volume compute shader initialization failed.");
}
initialised = true;
}

private void HandleUpdate()
{
#if UNITY_EDITOR
lastUpdateTimeEditor = UnityEditor.EditorApplication.timeSinceStartup;
#endif
// Dirty hack for broken data texture
// TODO: Investigate issue with calling VolumeDataset.GetDataTexture from first update in editor after leaving play mode
if (cooldown > 0.0f)
{
cooldown -= Time.deltaTime;
return;
}

if (volumeRenderedObject.GetRenderMode() != RenderMode.DirectVolumeRendering)
{
return;
}

lightDirection = -GetLightDirection(volumeRenderedObject);

if (currentDispatchIndex == 0)
{
ConfigureCompute();
}
if (currentDispatchIndex < dispatchCount)
{
DispatchComputeChunk();
currentDispatchIndex++;
}
if (currentDispatchIndex == dispatchCount)
{
currentDispatchIndex = 0;
}
isDirty = false;
}

private void ConfigureCompute()
{
VolumeDataset dataset = volumeRenderedObject.dataset;

Texture3D dataTexture = dataset.GetDataTexture();

if (volumeRenderedObject.GetCubicInterpolationEnabled())
shadowVolumeShader.EnableKeyword("CUBIC_INTERPOLATION_ON");
else
shadowVolumeShader.DisableKeyword("CUBIC_INTERPOLATION_ON");

shadowVolumeShader.SetVector("_TextureSize", new Vector3(dataset.dimX, dataset.dimY, dataset.dimZ));
shadowVolumeShader.SetInts("_Dimension", new int[] { shadowVolumeTexture.width, shadowVolumeTexture.height, shadowVolumeTexture.volumeDepth });
shadowVolumeShader.SetTexture(handleMain, "_VolumeTexture", dataTexture);
shadowVolumeShader.SetTexture(handleMain, "_TFTex", volumeRenderedObject.transferFunction.GetTexture());
shadowVolumeShader.SetTexture(handleMain, "_ShadowVolume", shadowVolumeTexture);
shadowVolumeShader.SetVector("_LightDirection", lightDirection);

Material volRendMaterial = volumeRenderedObject.meshRenderer.sharedMaterial;
shadowVolumeShader.SetFloat("_MinVal", volRendMaterial.GetFloat("_MinVal"));
shadowVolumeShader.SetFloat("_MaxVal", volRendMaterial.GetFloat("_MaxVal"));

if (volRendMaterial.IsKeywordEnabled("CROSS_SECTION_ON"))
{
shadowVolumeShader.EnableKeyword("CROSS_SECTION_ON");
shadowVolumeShader.SetMatrixArray("_CrossSectionMatrices", volRendMaterial.GetMatrixArray("_CrossSectionMatrices"));
shadowVolumeShader.SetFloats("_CrossSectionTypes", volRendMaterial.GetFloatArray("_CrossSectionTypes"));
shadowVolumeShader.SetInt("_NumCrossSections", 1);
}
else
{
shadowVolumeShader.DisableKeyword("CROSS_SECTION_ON");
}
if (volumeRenderedObject != null)
{
volumeRenderedObject.meshRenderer.sharedMaterial.EnableKeyword("SHADOWS_ON");
}
}

private void DispatchComputeChunk()
{
int threadGroupsX = (shadowVolumeTexture.width / NUM_DISPATCH_CHUNKS + 7) / 8;
int threadGroupsY = (shadowVolumeTexture.height / NUM_DISPATCH_CHUNKS + 7) / 8;
int threadGroupsZ = (shadowVolumeTexture.volumeDepth / NUM_DISPATCH_CHUNKS + 7) / 8;
int dispatchChunkWidth = shadowVolumeTexture.width / NUM_DISPATCH_CHUNKS;
int dispatchChunkHeight = shadowVolumeTexture.height / NUM_DISPATCH_CHUNKS;
int dispatchChunkDepth = shadowVolumeTexture.volumeDepth / NUM_DISPATCH_CHUNKS;

int ix = currentDispatchIndex % NUM_DISPATCH_CHUNKS;
int iy = (currentDispatchIndex / NUM_DISPATCH_CHUNKS) % NUM_DISPATCH_CHUNKS;
int iz = currentDispatchIndex / (NUM_DISPATCH_CHUNKS * NUM_DISPATCH_CHUNKS);
shadowVolumeShader.SetInts("_DispatchOffsets", new int[] { dispatchChunkWidth * ix, dispatchChunkHeight * iy, dispatchChunkDepth * iz });
shadowVolumeShader.Dispatch(handleMain, threadGroupsX, threadGroupsY, threadGroupsZ);
}

private Vector3 GetLightDirection(VolumeRenderedObject targetObject)
{
Transform targetTransform = targetObject.volumeContainerObject.transform;
if (targetObject.GetLightSource() == LightSource.SceneMainLight)
{
Light[] lights = GameObject.FindObjectsOfType(typeof(Light)) as Light[];
Light directionalLight = lights.FirstOrDefault(l => l.type == LightType.Directional);
if ( directionalLight != null)
{
return targetTransform.InverseTransformDirection(directionalLight.transform.forward);
}

if (lights.Length > 0)
{
return targetTransform.InverseTransformDirection(lights[0].transform.forward); // TODO
}
}
#if UNITY_EDITOR
if (!Application.isPlaying)
{
return targetTransform.InverseTransformDirection(UnityEditor.SceneView.lastActiveSceneView.camera.transform.forward);
}
#endif
return targetTransform.InverseTransformDirection(Camera.main.transform.forward);
}
}
}
3 changes: 3 additions & 0 deletions Assets/Scripts/Lighting/ShadowVolumeManager.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 25772d9

Please sign in to comment.