Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

WebGL Won't allow my shader to use a switch for ray marching shader

Discussion in 'Shaders' started by sicblivn, May 20, 2021.

  1. sicblivn

    sicblivn

    Joined:
    Jan 7, 2016
    Posts:
    3
    I've been trying to make a shader that will allow ray marching to happen on devices without heavy graphics cards and have been stuck on a problem for about 2 weeks.

    The shader i have right now is super simple and works and gives good fps on different platforms. It is even working really fast in WebGL (which has been a nightmare to figure out) but the last piece of the puzzle is allowing a user to pick different ray marching shapes.

    I can't for the life of me figure out why, a simple switch at line 94 won't work in WebGL.

    I've tried rewriting it as if/else statements, ternary operators, and even trying to decide on the correct signed distance via linear logical operators and even comparison based math to avoid branching with conditionals. WebGL just ignores it all and only prints (ERROR: too many uniforms) in the web console.

    It's like it always knows what i'm up to (◕︵◕)

    I'm new to shader programming so don't really know the best way to debug this and was wondering if anyone in the community knew a way out.

    Code (CSharp):
    1. // Adapted from shader at https://github.com/ratchet3789/Metaball-Shader-Unity
    2.  
    3. Shader "Custom/Raymarch"
    4. {
    5.     Properties
    6.     {
    7.         _MainTex ("Texture", 2D) = "white" { }
    8.     }
    9.     SubShader
    10.     {
    11.         // No culling or depth
    12.         Cull Off ZWrite Off ZTest Always
    13.  
    14.         Pass
    15.         {
    16.             CGPROGRAM
    17.            
    18.             #pragma vertex vert
    19.             #pragma fragment frag
    20.            
    21.             #include "UnityCG.cginc"
    22.             #include "SignedDistanceFunctions.cginc"
    23.  
    24.             sampler2D _MainTex;
    25.             float4x4 _FrustrumCornersES;
    26.             float4 _TexelSize;
    27.             float4x4 _CameraInvViewMatrix;
    28.             float _Scale;
    29.             float _Size;
    30.             float4 _LightDir;
    31.             sampler2D _CameraDepthTexture;
    32.             float _SphereScale;
    33.             float _SmoothUnion;
    34.  
    35.             //Shape Arrays
    36.             int _ShapeCount;
    37.             float4 shapePosition[256];
    38.             float4x4 shapeToLocal[256];
    39.             float4 shapeExtent[256];
    40.             float shapeType[256];
    41.             float4 shapeColor[256];
    42.            
    43.             struct appdata
    44.             {
    45.                 float4 vertex: POSITION;
    46.                 float2 uv: TEXCOORD0;
    47.             };
    48.            
    49.             struct v2f
    50.             {
    51.                 float2 uv: TEXCOORD0;
    52.                 float4 vertex: SV_POSITION;
    53.                 float3 ray: TEXCOORD1;
    54.             };
    55.  
    56.  
    57.             // How many times each ray is marched
    58.             // Higher values give higher resolution
    59.             // (and potentially longer draw distances)
    60.             // but lower performance
    61.             static const int maxSteps = 60;
    62.  
    63.             //How close does a ray have to get to be consider a hit
    64.             //Higher values give a sharper definition of shape
    65.             // but lower performance
    66.             static const float epsilon = 0.03;
    67.  
    68.             // The maximum distance we want a ray to be from
    69.             // the nearest surface before giving up
    70.             //Higher values give a longer draw distance but
    71.             // lower performance
    72.             static const float maxDist = 10;
    73.  
    74.             // Utility function to find out
    75.             // when two values are equal
    76.             // Return 1 if equal, 0 if not.
    77.             // Tried to use this inplace of switch
    78.             // but WebGL knew what i was up to :(
    79.             float when_eq(float x, float y) {
    80.                  return 1.0 - abs(sign(x - y));
    81.             }
    82.            
    83.             //Get the specific shape of a raymarch object
    84.             float4 GetShape(float3 p, int index)
    85.             {
    86.                 float3 position = mul(shapeToLocal[index], float4((p),1.0));
    87.  
    88.                 float3 col = float3(shapeColor[index].x,
    89.                                              shapeColor[index].y,
    90.                                              shapeColor[index].z);
    91.  
    92.                 float dst = 0;
    93.  
    94.                 switch (shapeType[index])
    95.                 {
    96.                     case 0:
    97.                         dst = sdSphere(shapePosition[index] - p,
    98.                                          length(float3(shapeExtent[index].x,
    99.                                          shapeExtent[index].y,
    100.                                          shapeExtent[index].z)));
    101.                         break;
    102.                     case 1:
    103.                         dst = sdBox(position,
    104.                                     float3(shapeExtent[index].x,
    105.                                     shapeExtent[index].y,
    106.                                     shapeExtent[index].z));
    107.                         break;
    108.                     case 2:
    109.                         dst = sdTorus(position,
    110.                                     length(shapeExtent[index].x),
    111.                                     length(shapeExtent[index].z));
    112.                         break;
    113.                     case 3:
    114.                         dst = sdCone(position,
    115.                                     float2(shapeExtent[index].x,
    116.                                     shapeExtent[index].z),
    117.                                     shapeExtent[index].y);
    118.                         break;
    119.                     case 4:
    120.                         dst = sdCylinder(position,
    121.                                         length(float2(shapeExtent[index].x,
    122.                                         shapeExtent[index].z)),
    123.                                         shapeExtent[index].y);
    124.                         break;
    125.                 }
    126.  
    127.                 return float4(col, dst);
    128.             }
    129.  
    130.             //Map out Signed distances          
    131.             float4 map(float3 p)
    132.             {
    133.                 float4 dist = GetShape(p,0);
    134.  
    135.                 // [unroll(4)] Testing unroll
    136.                 for (int i = 0; i < _ShapeCount; i++){
    137.                     dist = opSmoothUnion(dist,
    138.                                         GetShape(p,i),
    139.                                         _SmoothUnion);
    140.                 }
    141.                
    142.                 return dist;
    143.             }
    144.  
    145.             //Get normals given a point          
    146.             float3 calcNormal(float3 p)
    147.             {
    148.                 float x = map(float3(p.x + epsilon, p.y, p.z)).w
    149.                         - map(float3(p.x - epsilon, p.y, p.z)).w;
    150.                 float y = map(float3(p.x, p.y + epsilon, p.z)).w
    151.                         - map(float3(p.x, p.y - epsilon, p.z)).w;
    152.                 float z = map(float3(p.x, p.y, p.z + epsilon)).w
    153.                         - map(float3(p.x, p.y, p.z - epsilon)).w;
    154.  
    155.                 return normalize(float3(x,y,z));
    156.             }
    157.  
    158.             //Actual Raymarching function, returns a color for
    159.             //different points where our rays hit objects.
    160.             fixed4 raymarch(float3 rayOrigin, float3 rayDirection, float s)
    161.             {
    162.                 fixed4 col = fixed4(0, 0, 0, 0);
    163.                
    164.                 const int timeStep = 100;
    165.                 float travelled = 0;
    166.                 for (int i = 0; i < timeStep; i ++)
    167.                 {
    168.                     float3 position = rayOrigin + rayDirection * travelled;
    169.                     float4 surf = map(position);
    170.                    
    171.                     if (travelled > maxDist || travelled >= s)
    172.                     // if (travelled >= s)
    173.                     {
    174.                         col = fixed4(0, 0, 0, 0);
    175.                         break;
    176.                     }
    177.                    
    178.                     if(surf.w < 0.03)
    179.                     {
    180.                         float3 n = calcNormal(position);
    181.                         col = fixed4(surf.rgb * dot(-_LightDir.xyz, n).rrr, 1);
    182.                         break;
    183.                     }
    184.                    
    185.                     travelled += surf.w;
    186.                 }
    187.                 return col;
    188.             }
    189.  
    190.             //Vertex          
    191.             v2f vert(appdata v)
    192.             {
    193.                 v2f o;
    194.                
    195.                 half index = v.vertex.z;
    196.                 v.vertex.z = 0;
    197.                 o.vertex = UnityObjectToClipPos(v.vertex);
    198.                 o.uv = v.uv.xy;
    199.                
    200.                 // #if UNITY_UV_STARTS_AT_TOP
    201.                 //     if(_TexelSize.y < 0)
    202.                 //         o.uv.y = 1 - o.uv.y;
    203.                 // #endif
    204.                
    205.                 o.ray = _FrustrumCornersES[(int)index].xyz;
    206.                 //Normalize on the Z axis - viewspace position
    207.                 o.ray /= abs(o.ray.z);
    208.                
    209.                 o.ray = mul(_CameraInvViewMatrix, o.ray);
    210.                
    211.                 return o;
    212.             }
    213.            
    214.             //Fragment
    215.             fixed4 frag(v2f i): SV_Target
    216.             {
    217.                 float3 rayDir = normalize(i.ray.xyz);
    218.                 float3 rayOrigin = _WorldSpaceCameraPos;
    219.                
    220.                 float2 duv = i.uv;
    221.                 if (_TexelSize.y < 0)
    222.                     duv.y = 1 - duv.y;
    223.                
    224.                 float depth = LinearEyeDepth(tex2D(_CameraDepthTexture, duv).r);
    225.                 depth *= length(i.ray.xyz);
    226.                
    227.                 fixed3 col = tex2D(_MainTex, i.uv);
    228.                 fixed4 add = raymarch(rayOrigin, rayDir, depth);
    229.                
    230.                 return (fixed4(col * (1.0 - add.w) + add.xyz * add.w, 1.0) + 0.2);
    231.             }
    232.             ENDCG
    233.            
    234.         }
    235.     }
    236. }
    237.  
     

    Attached Files:

  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    You're guessing at what the problem is and ignoring the error, which is telling you exactly what the problem is.
    You have too many uniforms. A uniform is an input into the shader. WebGL is usually limited to between 256 and 1024 total float4 uniforms per shader stage and you have a bunch of arrays with 256 elements each, which is 256 uniforms each. That
    float4x4 shapeToLocal[256];
    actually counts as 1024 uniforms by itself!

    Change all those arrays to only have 8 elements and it should work. If you're using WebGL 1.0, you can use data packed into textures to send more data. If you're using WebGL 2.0, you can use compute buffers / structured buffers.
     
  3. sicblivn

    sicblivn

    Joined:
    Jan 7, 2016
    Posts:
    3
    Oh okay, thank you for your reply!

    And it definitely works with a lower size for the arrays and i think the screenshot that is attached to the original post is from a working early test.

    But for sure I did not think about the uniforms being literally the amount of data that i'm sending! thank you so much for clarifying the error and i can now see how to move forward as you've suggested

    I think i also should have mentioned from the start that i started with a low value for the arrays (about 4), and the reason i want to have a high number is to allow shapes to be added dynamically.

    I tried Structured buffers but they don't seem to work on my 2016 mac, only my pc and i can't figure out why yet (cause i think geom shaders also arent supported on the previous macbooks so it might be a hardware thing) but have been trying to find a solution that worked on both older pcs and macs which is why i turned to arrays to ensure some backwards compatibility.

    Then i also saw your reply to another post from about using textures to send more data and have been studying the KvantSpray repository by Keijiro and so far the main problem i'm trying to understand is how to traverse the texture cause it seems you always need to sample it with a uv and not just say the index of the ray marching object.

    Also i'm trying to figure out how to store the float4x4 matrix data into a texture. In that post you mentioned using 4 rgba pixels so would i essentially write my own method to get a value in the matrix and map it to the appropriate value in a pixel and back again?

    I'm hoping i'll understand how to do it soon but wanted to ask if what i'm thinking of next concerning your suggestion about storing data in textures is right. Would i literally just be putting data into the different texture coordinates from (0.0, 0.0) to (1.0, 1.0) and then define my own uv's in the shader that correspond to where i know my data is located as if i'm traversing a 2d array?

    But regardless i also don't get why textures can send 4 million float4 values but arrays can't. Shouldn' that mean there is a max number of textures you can send, say 2 cause won't you be asking webgl for a lot of memory allocation either way?

    Thank you again !
     
    Last edited: May 20, 2021
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Presumably you're using Safari then, which is still limited to WebGL 1.0 on desktop. You can enable WebGL 2.0 on iOS Safari, but I don't know if you can on desktop Safari. Apple also has a long history of intentionally holding back Khronos standards (OpenGL, GLES, WebGL) support on the desktop, intentionally limiting support to 2010 era OpenGL 4.1, which doesn't support the feature set of OpenGLES 3.0, which WebGL 2.0 is based on.

    Yes, iOS devices have better and more modern OpenGL support than MacOS.

    There's a preview version of Safari that supports WebGL 2.0, and I think it can be enabled in the latest Chrome on MacOS, as they now support emulation of WebGL 2.0 via Apple's Metal API.

    On Windows, Chrome supports WebGL 2.0 out of the box via Direct3D based emulation, so actually supports something like 32768 uniforms (since Direct3D doesn't really have a limit on total uniforms you have to worry about, only max size on individual arrays).

    Note that WebGL 1.0 doesn't support dynamic flow control. The short version of what that means is this: Lets say you have your shader setup to support a max of 256 SDF shapes of 5 different types. The compiled shader will always be calculating 256 spheres, 256 boxes, 256 tori, 256 cones, and 256 cylinders. Always. For every pixel. That
    [unroll(4)]
    you have commented out doesn't do anything when compiling for WebGL 1.0 because WebGL 1.0 shaders are always unrolled. The
    _ShapeCount
    value still does something, but it doesn't stop the shader from doing all of the possible work all of the time.

    The work around for that is to use
    #pragma multi_compile
    variants with some set of hard coded limits. Though you'll probably never be able to do 256 SDFs with 5 different shapes as it'll generate shaders that are so long they'll fail to compile either due to instruction count limits or the computer timing out the compiler because it thinks it crashed (assuming it didn't actually crash, which it might).

    Correct. Keijiro's Kvant Spray generates a mesh that has the appropriate UV for each "index" baked into it, so there's no fancy work needed in the shader. Just sample the data textures with the vertex's pre setup UV and done.

    For what you're doing you'd have to convert an index into a UV, which isn't hard. The idea is use a 1 texel high by however many shapes as you want wide and treat it as arbitrary Vector4 data in a point filtered RGBAHalf texture. For storing a matrix you have to store the 4 rows in separate texels, so you can either keep it 1 texel high or do 4 (or 3 as you don't need the fourth row) texels high. Or pack all of your data into a single 6 texel high texture (I'd pack the shape type into the .w / alpha of the position).

    After that going from index to UV is as straight forward as:
    Code (csharp):
    1. float2 dataUV = float2(_MyDataTexture_TexelSize.x * (index + 0.5), 0.5); // for a 1 texel high texture
    2. float4 data = tex2Dlod(_MyDataTexture, float4(dataUV, 0.0, 0.0));
    The
    0.5
    s are because you want to sample from the middle of each texel. For a single texel high texture the
    *_TexelSize.y
    is 1.0 so we didn't need to multiply the 0.5 by anything. If you pack into a texture that's multiple texels high, then do something like:
    Code (csharp):
    1. _MyDataTexture_TexelSize.xy * (float2(index, dataRow) + 0.5)
    The performance killer is sampling a texture so many times, but it's the only option for complicated stuff in WebGL 1.0.
     
  5. sicblivn

    sicblivn

    Joined:
    Jan 7, 2016
    Posts:
    3
    bgolus you are a life saver! I got it working like you said and it has helped a lot with ensuring backward compatibility in general for the long term. I've got a screenshot below of just the positions of spheres working by passing the data to a texture

    That explains so much! At this point i've basically given up on safari and have been doing tests on other browsers after making some adjustments from your last post.

    I've seen a tonne of your posts and i've always wanted to ask how you know all this stuff about shaders.

    Regardless thank you so much for your time today and especially for always helping out the community with so much in-depth information and answers!
     

    Attached Files:

  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Mainly just because I’ve been writing shaders for almost 15 years. Was lucky to at times work with people with even more experience than that whom I could ask questions of and were happy the indulge my curiosity. These days it’s mostly just me poking at stuff on my own or reading stuff others write. And answering questions here, which often helps me hone my knowledge and understanding further.
     
    sicblivn likes this.