Search Unity

How can I create an arbitrary number of 3D volumes and access them from a shader?

Discussion in 'Shaders' started by hypnoslave, Jan 4, 2019.

  1. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    439
    Hi there! I have a weird effect that I'd like to play on any number of objects in my world, provided they fall inside of a number of volumes that I would be able to control/move.

    Is there a way of stashing a arbitrary number of colliders, perhaps, to some sort of 3D on/off map that a shader could use to simply lerp against?

    I have no idea how I might represent this map, or how a shader might read it.
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,342
    Yes and no.

    You'll want to look into signed distance fields. If your shapes are all boxes and spheres, those can be done analytically fairly inexpensively. For more complex mesh shapes you'd need to use a signed distance field stored in a 3d texture, or maybe a height map.

    http://iquilezles.org/www/articles/distfunctions/distfunctions.htm
     
  3. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    439
    very interesting.. Thanks (again) bgolus. You seem to be the shader savior around here.

    I'll dig into this and see if I can make heads or tails of it. Simple shapes are fine. I dare not tempt fate.
     
    Last edited: Jan 5, 2019
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,342
    If you stick to spheres you can very easily pass an array of float4 values with world position and radius^2 to iterate over. For boxes you'll need an array of 4x3 matrices to transform the pixel's world position into a local unit box position, or stick with world axis aligned boxes with min/max extents to test against.

    See this tutorial:
    https://www.alanzucconi.com/2016/01/27/arrays-shaders-heatmaps-in-unity3d/
    But be sure to read the 5.4+ follow up before getting too far in:
    https://www.alanzucconi.com/2016/10/24/arrays-shaders-unity-5-4/

    Note, you'll want to make the c# arrays as large as the number of volumes of a particular type as you think you'll ever need as you can't change the size after the first time you call Set*Array() for each shader property.
     
  5. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    439
    Oh man this is perfect! Yeah, Arrays in shaders is exactly what I need. I wasn't aware you could even have iterated loops in cg.

    Question: How optimized is this? How do GPUs handle iteration? having all of my world geometry iterate through a... let's say 30 length array to get the right value at that pixel... for ever pixel on the screen... I wonder (since its okay if this is somewhat low resolution) if it would be better to dump my spheres & cubes to a 3D texture on awake and have my shaders just do a lookup to that global texture. I think I could get the effect I needed while only loading one 256^3 into memory at any given time.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,342
    That's multiple questions ...

    First - How optimized is this?
    Not at all, but GPUs are fast, and as long as the tests are simple (like iterating over 100 spheres using a radiusSqr value to avoid doing a sqrt) then you're roughly doubling the overall complexity of a shader vs. lighting alone when using the Standard lighting model.

    How do GPUs handle iteration?
    Depends on the class of GPU. If you're trying to do this on an old OpenGL ES 2.0 mobile device (or D3D 9 desktop hardware, which isn't supported by Unity anymore), then the loop is unrolled. That is to say there's no such thing as a dynamic loop on those GPUs, so it'll iterate over a fixed number of elements as if they were written out in the shader.

    For example:
    Code (csharp):
    1. for (int i=0; i<100; i++)
    2.     color += colors[i];
    Turns into something like:
    Code (csharp):
    1. color += colors[0];
    2. color += colors[1];
    3. // ...
    4. color += colors[99];
    If you have the loop running from a uniform property, like this:
    Code (csharp):
    1. for (int i=0; i<_colorCount; i++)
    2.     color += colors[i];
    Well, GLES 2.0, and even GL 3.3 will technically just croak on this. It can only use fixed iteration counts. This is totally valid for OpenGL 4.0 and for Direct3D 11 though. For older OpenGL specs the loop is still unrolled, but using some magical guessed fixed iteration count which I have no idea how it's determined, and the code becomes something like this:
    Code (csharp):
    1. fixed4 tempColor0 = color + color[0];
    2. color = 0 < _colorCount ? tempColor0 : color;
    3. fixed4 tempColor1 = color + color[1];
    4. color = 1 < _colorCount ? tempColor1 : color;
    5. // ...
    6. fixed4 tempColor99 = color + color[99];
    7. color = 99 < _colorCount ? tempColor99 : color;
    You get the idea.

    Now I mentioned OpenGL 4.0 and Direct3D 11. For those GPUs are totally capable of using dynamic loops like this and it "just works". There's some overhead to having a dynamic loop vs an unrolled fixed iteration loop of the same size, but it's not too bad.

    A 3D texture
    The loop iteration can get expensive, so a 3D texture would absolutely be cheaper to sample from in a lot of cases, especially with a lot of shapes. You might need to try both and see which one ends up being faster as generating the 3D texture to begin with won't necessarily be that fast either. But, as you said, a low enough resolution 3D texture may result in a big win overall. Really, the shader code for doing this on every on screen pixel vs into a 3d texture will be pretty similar if you wanted to use a compute shader to fill in the 3d texture. Otherwise you could do it from c# if it's not going to be updated often.


    Unity doesn't use Cg anymore. Everything inside the cginc files and CGPROGRAM blocks is straight HLSL.
     
    Marco-Sperling likes this.
  7. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    439
    I am now 1% more enlightened.

    Thank you sir :). This is incredibly useful.
     
  8. Przemyslaw_Zaworski

    Przemyslaw_Zaworski

    Joined:
    Jun 9, 2017
    Posts:
    328
    Example.

    Source:
    https://github.com/przemyslawzaworski/Unity3D-CG-programming/tree/master/VolumeRenderTexture

    CS script:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3.  
    4. public class VolumeRenderTexture : MonoBehaviour
    5. {
    6.     public ComputeShader VolumeShader;
    7.     public Material VolumeMaterial;
    8.     RenderTexture VRTA, VRTB;
    9.     float cx, cy, cz;
    10.     bool swap = true;
    11.    
    12.     void Start()
    13.     {
    14.         RenderTextureDescriptor RTD = new RenderTextureDescriptor(256, 256, RenderTextureFormat.ARGB32);
    15.         RTD.dimension = TextureDimension.Tex3D;
    16.         RTD.volumeDepth = 256;
    17.         VRTA = new RenderTexture(RTD);
    18.         VRTA.enableRandomWrite = true;
    19.         VRTA.Create();
    20.         VRTB = new RenderTexture(RTD);
    21.         VRTB.enableRandomWrite = true;
    22.         VRTB.Create();
    23.         cx = cy = cz = 0.5f;
    24.     }
    25.  
    26.     void Update()
    27.     {
    28.         if (Input.GetKey(KeyCode.W)) cy+=0.001f;
    29.         if (Input.GetKey(KeyCode.S)) cy-=0.001f;
    30.         if (Input.GetKey(KeyCode.A)) cx+=0.001f;
    31.         if (Input.GetKey(KeyCode.D)) cx-=0.001f;
    32.         if (Input.GetKey(KeyCode.Q)) cz+=0.001f;
    33.         if (Input.GetKey(KeyCode.E)) cz-=0.001f;
    34.         VolumeShader.SetFloats("Center",new float[3] {cx, cy, cz});
    35.         if (swap)
    36.         {
    37.             VolumeShader.SetTexture(0, "VolumeReader", VRTA);
    38.             VolumeShader.SetTexture(0, "VolumeWriter", VRTB);
    39.             VolumeShader.Dispatch(0, 256 / 8, 256 / 8, 256 / 8);
    40.         }
    41.         else
    42.         {
    43.             VolumeShader.SetTexture(0, "VolumeReader", VRTB);
    44.             VolumeShader.SetTexture(0, "VolumeWriter", VRTA);
    45.             VolumeShader.Dispatch(0, 256 / 8, 256 / 8, 256 / 8);
    46.         }
    47.         swap = !swap;
    48.         VolumeMaterial.SetTexture("_Volume",VRTB);
    49.     }
    50. }
    Compute shader to generate data:
    Code (CSharp):
    1. #pragma kernel CSMain
    2.  
    3. Texture3D<float4> VolumeReader;
    4. RWTexture3D<float4> VolumeWriter;
    5. SamplerState _PointClamp;
    6. float3 Center;
    7.  
    8. float circle(float3 p, float3 c, float r)
    9. {
    10.     return step(length(p-c)-r,0.0);
    11. }
    12.  
    13. [numthreads(8,8,8)]
    14. void CSMain (uint3 id : SV_DispatchThreadID)
    15. {
    16.     float3 uv = float3((float)id.x/256.0,(float)id.y/256.0,(float)id.z/256.0);
    17.     float k = circle(uv,Center,0.03) + VolumeReader.SampleLevel(_PointClamp,uv,0);
    18.     VolumeWriter[id] = float4(k,k,k,k);
    19. }
    Shader to render 3D texture
    Code (CSharp):
    1. Shader "Volume Render Texture"
    2. {
    3.     SubShader
    4.     {
    5.         Cull Back
    6.         Pass
    7.         {
    8.             CGPROGRAM
    9.             #pragma vertex VSMain
    10.             #pragma fragment PSMain
    11.             #pragma target 5.0
    12.  
    13.             sampler3D _Volume;
    14.  
    15.             void VSMain(inout float4 vertex:POSITION, out float3 world:WORLD)
    16.             {
    17.                 world = mul(unity_ObjectToWorld, vertex).xyz;
    18.                 vertex = UnityObjectToClipPos(vertex);
    19.             }
    20.  
    21.             float4 PSMain(float4 vertex:POSITION, float3 world:WORLD) : SV_Target
    22.             {
    23.                 float3 ro = mul(unity_WorldToObject, float4(world, 1)).xyz;
    24.                 float3 rd = normalize(mul((float3x3) unity_WorldToObject, normalize(world - _WorldSpaceCameraPos)));
    25.                 float3 tbot = (1.0 / rd) * (-0.5 - ro);
    26.                 float3 ttop = (1.0 / rd) * (0.5 - ro);
    27.                 float3 tmin = min(ttop, tbot);
    28.                 float3 tmax = max(ttop, tbot);
    29.                 float2 a = max(tmin.xx, tmin.yz);
    30.                 float tnear = max(0.0,max(a.x, a.y));
    31.                 float2 b = min(tmax.xx, tmax.yz);
    32.                 float tfar = min(b.x, b.y);
    33.                 float3 d = normalize((ro + rd * tfar) - ro) * (abs(tfar - tnear) / 128.0);
    34.                 float4 t = float4(0, 0, 0, 0);
    35.                 [unroll]
    36.                 for (int i = 0; i < 128; i++)
    37.                 {
    38.                     float v = tex3D(_Volume, ro+0.5).r;
    39.                     float4 s = float4(v, v, v, v);
    40.                     s.a *= 0.5;
    41.                     s.rgb *= s.a;
    42.                     t = (1.0 - t.a) * s + t;
    43.                     ro += d;
    44.                     if (t.a > 0.99) break;
    45.                 }
    46.                 return saturate(t);
    47.             }
    48.  
    49.             ENDCG
    50.         }
    51.     }
    52. }
    Example configuration:
    upload_2019-1-11_10-37-17.png

    I use voxel sphere as brush to "sculpt" virtual geometry inside 3D render texture. 256x256x256 and fps is still high.
    Key configuration inside CS script.
     
  9. hypnoslave

    hypnoslave

    Joined:
    Sep 8, 2009
    Posts:
    439
    Oh damn! that's fantastic!

    Thanks very much Przemyslaw, I can learn a lot from this.