diff --git a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs index ca85cab6..d4c27d41 100644 --- a/Assets/Editor/VolumeRenderedObjectCustomInspector.cs +++ b/Assets/Editor/VolumeRenderedObjectCustomInspector.cs @@ -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(); + bool enableShadowVolume = GUILayout.Toggle(shadowVoumeManager != null, "Enable shadow volume (expensive)"); + if (enableShadowVolume && shadowVoumeManager == null) + shadowVoumeManager = volrendObj.gameObject.AddComponent(); + else if (!enableShadowVolume && shadowVoumeManager != null) + GameObject.DestroyImmediate(shadowVoumeManager); } } diff --git a/Assets/Resources/ShadowVolume.compute b/Assets/Resources/ShadowVolume.compute new file mode 100644 index 00000000..fc2381c5 --- /dev/null +++ b/Assets/Resources/ShadowVolume.compute @@ -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 _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; + } +} diff --git a/Assets/Resources/ShadowVolume.compute.meta b/Assets/Resources/ShadowVolume.compute.meta new file mode 100644 index 00000000..395976e5 --- /dev/null +++ b/Assets/Resources/ShadowVolume.compute.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6b94dacf27ec26946ade2872f5a8146f +ComputeShaderImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lighting/ShadowVolumeManager.cs b/Assets/Scripts/Lighting/ShadowVolumeManager.cs new file mode 100644 index 00000000..92ce98b7 --- /dev/null +++ b/Assets/Scripts/Lighting/ShadowVolumeManager.cs @@ -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(); + 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); + } + } +} diff --git a/Assets/Scripts/Lighting/ShadowVolumeManager.cs.meta b/Assets/Scripts/Lighting/ShadowVolumeManager.cs.meta new file mode 100644 index 00000000..621cc587 --- /dev/null +++ b/Assets/Scripts/Lighting/ShadowVolumeManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 952eca46d42a45a0974a9a9515bd2c1c +timeCreated: 1710481509 \ No newline at end of file diff --git a/Assets/Shaders/DirectVolumeRenderingShader.shader b/Assets/Shaders/DirectVolumeRenderingShader.shader index 269faed5..5c39c24c 100644 --- a/Assets/Shaders/DirectVolumeRenderingShader.shader +++ b/Assets/Shaders/DirectVolumeRenderingShader.shader @@ -6,13 +6,15 @@ _GradientTex("Gradient Texture (Generated)", 3D) = "" {} _NoiseTex("Noise Texture (Generated)", 2D) = "white" {} _TFTex("Transfer Function Texture (Generated)", 2D) = "" {} + _ShadowVolume("Shadow volume Texture (Generated)", 3D) = "" {} _MinVal("Min val", Range(0.0, 1.0)) = 0.0 _MaxVal("Max val", Range(0.0, 1.0)) = 1.0 _MinGradient("Gradient visibility threshold", Range(0.0, 1.0)) = 0.0 _LightingGradientThresholdStart("Gradient threshold for lighting (end)", Range(0.0, 1.0)) = 0.0 _LightingGradientThresholdEnd("Gradient threshold for lighting (start)", Range(0.0, 1.0)) = 0.0 + [HideInInspector] _ShadowVolumeTextureSize("Shadow volume dimensions", Vector) = (1, 1, 1) [HideInInspector] _TextureSize("Dataset dimensions", Vector) = (1, 1, 1) - } +} SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } @@ -29,6 +31,7 @@ #pragma multi_compile __ TF2D_ON #pragma multi_compile __ CROSS_SECTION_ON #pragma multi_compile __ LIGHTING_ON + #pragma multi_compile __ SHADOWS_ON #pragma multi_compile DEPTHWRITE_ON DEPTHWRITE_OFF #pragma multi_compile __ RAY_TERMINATE_ON #pragma multi_compile __ USE_MAIN_LIGHT @@ -37,7 +40,7 @@ #pragma fragment frag #include "UnityCG.cginc" - #include "TricubicSampling.cginc" + #include "Include/TricubicSampling.cginc" #define AMBIENT_LIGHTING_FACTOR 0.5 #define JITTER_FACTOR 5.0 @@ -71,25 +74,24 @@ sampler3D _GradientTex; sampler2D _NoiseTex; sampler2D _TFTex; + sampler3D _ShadowVolume; float _MinVal; float _MaxVal; float3 _TextureSize; + float3 _ShadowVolumeTextureSize; float _MinGradient; float _LightingGradientThresholdStart; float _LightingGradientThresholdEnd; #if CROSS_SECTION_ON -#define CROSS_SECTION_TYPE_PLANE 1 -#define CROSS_SECTION_TYPE_BOX_INCL 2 -#define CROSS_SECTION_TYPE_BOX_EXCL 3 -#define CROSS_SECTION_TYPE_SPHERE_INCL 4 -#define CROSS_SECTION_TYPE_SPHERE_EXCL 5 - - float4x4 _CrossSectionMatrices[8]; - float _CrossSectionTypes[8]; - int _NumCrossSections; +#include "Include/VolumeCutout.cginc" +#else + bool IsCutout(float3 currPos) + { + return false; + } #endif struct RayInfo @@ -232,6 +234,15 @@ return diffuse + specular; } + float calculateShadow(float3 pos, float3 lightDir) + { +#if CUBIC_INTERPOLATION_ON + return interpolateTricubicFast(_ShadowVolume, float3(pos.x, pos.y, pos.z), _ShadowVolumeTextureSize); +#else + return tex3Dlod(_ShadowVolume, float4(pos.x, pos.y, pos.z, 0.0f)); +#endif + } + // Converts local position to depth value float localToDepth(float3 localPos) { @@ -244,37 +255,6 @@ #endif } - bool IsCutout(float3 currPos) - { -#if CROSS_SECTION_ON - // Move the reference in the middle of the mesh, like the pivot - float4 pivotPos = float4(currPos - float3(0.5f, 0.5f, 0.5f), 1.0f); - - bool clipped = false; - for (int i = 0; i < _NumCrossSections && !clipped; ++i) - { - const int type = (int)_CrossSectionTypes[i]; - const float4x4 mat = _CrossSectionMatrices[i]; - - // Convert from model space to plane's vector space - float3 planeSpacePos = mul(mat, pivotPos); - if (type == CROSS_SECTION_TYPE_PLANE) - clipped = planeSpacePos.z > 0.0f; - else if (type == CROSS_SECTION_TYPE_BOX_INCL) - clipped = !(planeSpacePos.x >= -0.5f && planeSpacePos.x <= 0.5f && planeSpacePos.y >= -0.5f && planeSpacePos.y <= 0.5f && planeSpacePos.z >= -0.5f && planeSpacePos.z <= 0.5f); - else if (type == CROSS_SECTION_TYPE_BOX_EXCL) - clipped = planeSpacePos.x >= -0.5f && planeSpacePos.x <= 0.5f && planeSpacePos.y >= -0.5f && planeSpacePos.y <= 0.5f && planeSpacePos.z >= -0.5f && planeSpacePos.z <= 0.5f; - else if (type == CROSS_SECTION_TYPE_SPHERE_INCL) - clipped = length(planeSpacePos) > 0.5; - else if (type == CROSS_SECTION_TYPE_SPHERE_EXCL) - clipped = length(planeSpacePos) < 0.5; - } - return clipped; -#else - return false; -#endif - } - frag_in vert_main (vert_in v) { frag_in o; @@ -346,6 +326,10 @@ float factor = smoothstep(_LightingGradientThresholdStart, _LightingGradientThresholdEnd, gradMag); float3 shaded = calculateLighting(src.rgb, gradient / gradMag, getLightDirection(-ray.direction), -ray.direction, 0.3f); src.rgb = lerp(src.rgb, shaded, factor); +#if defined(SHADOWS_ON) + float shadow = calculateShadow(currPos, getLightDirection(-ray.direction)); + src.rgb *= (1.0f - shadow); +#endif #endif src.rgb *= src.a; diff --git a/Assets/Shaders/TricubicSampling.cginc b/Assets/Shaders/Include/TricubicSampling.cginc similarity index 100% rename from Assets/Shaders/TricubicSampling.cginc rename to Assets/Shaders/Include/TricubicSampling.cginc diff --git a/Assets/Shaders/Include/VolumeCutout.cginc b/Assets/Shaders/Include/VolumeCutout.cginc new file mode 100644 index 00000000..a82e013e --- /dev/null +++ b/Assets/Shaders/Include/VolumeCutout.cginc @@ -0,0 +1,36 @@ +#define CROSS_SECTION_TYPE_PLANE 1 +#define CROSS_SECTION_TYPE_BOX_INCL 2 +#define CROSS_SECTION_TYPE_BOX_EXCL 3 +#define CROSS_SECTION_TYPE_SPHERE_INCL 4 +#define CROSS_SECTION_TYPE_SPHERE_EXCL 5 + +float4x4 _CrossSectionMatrices[8]; +float _CrossSectionTypes[8]; +int _NumCrossSections; + +bool IsCutout(float3 currPos) +{ + // Move the reference in the middle of the mesh, like the pivot + float4 pivotPos = float4(currPos - float3(0.5f, 0.5f, 0.5f), 1.0f); + + bool clipped = false; + for (int i = 0; i < _NumCrossSections && !clipped; ++i) + { + const int type = (int)_CrossSectionTypes[i]; + const float4x4 mat = _CrossSectionMatrices[i]; + + // Convert from model space to plane's vector space + float3 planeSpacePos = mul(mat, pivotPos).xyz; + if (type == CROSS_SECTION_TYPE_PLANE) + clipped = planeSpacePos.z > 0.0f; + else if (type == CROSS_SECTION_TYPE_BOX_INCL) + clipped = !(planeSpacePos.x >= -0.5f && planeSpacePos.x <= 0.5f && planeSpacePos.y >= -0.5f && planeSpacePos.y <= 0.5f && planeSpacePos.z >= -0.5f && planeSpacePos.z <= 0.5f); + else if (type == CROSS_SECTION_TYPE_BOX_EXCL) + clipped = planeSpacePos.x >= -0.5f && planeSpacePos.x <= 0.5f && planeSpacePos.y >= -0.5f && planeSpacePos.y <= 0.5f && planeSpacePos.z >= -0.5f && planeSpacePos.z <= 0.5f; + else if (type == CROSS_SECTION_TYPE_SPHERE_INCL) + clipped = length(planeSpacePos) > 0.5; + else if (type == CROSS_SECTION_TYPE_SPHERE_EXCL) + clipped = length(planeSpacePos) < 0.5; + } + return clipped; +}