Help With MetaBall shader

HI all,

following this tutorial and code here:

, i am attempting to make my own meta ball shader. However im trying to put a twist on this standard meta ball shader, which is the metaballs are only made up of 2 colors, either blue or red. the two meta balls need to blend colors with each other. So when a blue meta ball is next to a red meta ball, the middle part where they join should be a blended color between red and blue. Any ideas on how one can proceed?

The basic metaball shader looks like this:

Shader "Unlit/HeatmapShader"
{
  Properties
  {
    _MainTex("Texture", 2D) = "white" {}
    _Color0("Color 0",Color) = (0,0,0,1)
      _Color1("Color 1",Color) = (0,.9,.2,1)
      _Color2("Color 2",Color) = (.9,1,.3,1)
      _Color3("Color 3",Color) = (.9,.7,.1,1)
      _Color4("Color 4",Color) = (1,0,0,1)

      _Range0("Range 0",Range(0,1)) = 0.
      _Range1("Range 1",Range(0,1)) = 0.25
      _Range2("Range 2",Range(0,1)) = 0.5
      _Range3("Range 3",Range(0,1)) = 0.75
      _Range4("Range 4",Range(0,1)) = 1

      _Diameter("Diameter",Range(0,1)) = 1.0
      _Strength("Strength",Range(.1,4)) = 1.0
      _PulseSpeed("Pulse Speed",Range(0,5)) = 0
  }
    SubShader
    {
      Tags { "RenderType" = "Opaque" }
      LOD 100

      Pass
      {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_fog

        #include "UnityCG.cginc"

        struct appdata
        {
          float4 vertex : POSITION;
          float2 uv : TEXCOORD0;
        };

        struct v2f
        {
          float2 uv : TEXCOORD0;
          UNITY_FOG_COORDS(1)
          float4 vertex : SV_POSITION;
        };

        sampler2D _MainTex;
        float4 _MainTex_ST;

        float4 _Color0;
        float4 _Color1;
        float4 _Color2;
        float4 _Color3;
        float4 _Color4;


        float _Range0;
        float _Range1;
        float _Range2;
        float _Range3;
        float _Range4;
        float _Diameter;
        float _Strength;

        float _PulseSpeed;

        v2f vert(appdata v)
        {
          v2f o;
          o.vertex = UnityObjectToClipPos(v.vertex);
          o.uv = TRANSFORM_TEX(v.uv, _MainTex);
          UNITY_TRANSFER_FOG(o,o.vertex);
          return o;
        }
        //----

        float3 colors[5]; //colors for point ranges
        float pointranges[5];  //ranges of values used to determine color values
        float _Hits[3 * 32]; //passed in array of pointranges 3floats/point, x,y,intensity
        int _HitCount = 0;

        void initalize()
        {
          colors[0] = _Color0;
          colors[1] = _Color1;
          colors[2] = _Color2;
          colors[3] = _Color3;
          colors[4] = _Color4;
          pointranges[0] = _Range0;
          pointranges[1] = _Range1;
          pointranges[2] = _Range2;
          pointranges[3] = _Range3;
          pointranges[4] = _Range4;
        }

        float3 getHeatForPixel(float weight)
        {
          if (weight <= pointranges[0])
          {
            return colors[0];
          }
          if (weight >= pointranges[4])
          {
            return colors[4];
          }
          for (int i = 1; i < 5; i++)
          {
            if (weight < pointranges[i]) //if weight is between this point and the point before its range
            {
              float dist_from_lower_point = weight - pointranges[i - 1];
              float size_of_point_range = pointranges[i] - pointranges[i - 1];

              float ratio_over_lower_point = dist_from_lower_point / size_of_point_range;

              //now with ratio or percentage (0-1) into the point range, multiply color ranges to get color

              float3 color_range = colors[i] - colors[i - 1];

              float3 color_contribution = color_range * ratio_over_lower_point;

              float3 new_color = colors[i - 1] + color_contribution;
              return new_color;

            }
          }
          return colors[0];
        }

        //Note: if distance is > 1.0, zero contribution, 1.0 is 1/2 of the 2x2 uv size
        float distsq(float2 a, float2 b)
        {
          float area_of_effect_size = _Diameter;

          return  pow(max(0.0, 1.0 - distance(a, b) / area_of_effect_size), 2.0);
        }


        fixed4 frag(v2f i) : SV_Target
        {
          fixed4 col = tex2D(_MainTex, i.uv);

          initalize();
          float2 uv = i.uv;
          uv = uv * 4.0 - float2(2.0,2.0);  //our texture uv range is -2 to 2

          float totalWeight = 0.0;
          for (float i = 0.0; i < _HitCount; i++)
          {
            float2 work_pt = float2(_Hits[i * 3], _Hits[i * 3 + 1]);
            float pt_intensity = _Hits[i * 3 + 2];

            totalWeight += 0.5 * distsq(uv, work_pt) * pt_intensity * _Strength * (1 + sin(_Time.y * _PulseSpeed));
          }
          return col + float4(getHeatForPixel(totalWeight), .5);
        }


        ENDCG
      }
    }
}

The basic 2D “heatmap” metaball is a simplification of what this really is, which is SDF blending. Right now you’re accumulating the total weight at each pixel, but you aren’t keeping track of which points contributed and their individual weight contribution. You need to know that to figure out what color it should be, normalize by the total contribution (divide each contribution by the total weight), and then add the color values from each point multiplied by that normalized contribution.

Hi Bgolus,

thanks for the reply, since this, my situation has gotten a bit different. Instead of a metaball shader, I have something slightly different. It would be great if you can shed some light for me. The general structure still holds, when a user clicks, it generates a circular metaballish graphic on the shader. However now, instead of having each color blend between multiple colors based on clicks and their contribution, I simply want to overlay color with alpha values on top.

I have kind of a weird setup right now, I have a camera looking at a plane with the shader and rendering to a render texture. I then pass in the render texture to the shader as _MainTex. In the fragment shader, I compare the new color value with the existing color of the shader via sampling _MainTex. If the colors are the same, I don’t do any overlaying, and if the colors are different, I add then using some blend operation like lighten, darken, color dodge etc. However, now my problem is, the color I sample from the _MainTex is always different than my calculated color, even when there’s no new clicks.

Here is my shader code:

Shader "Unlit/blendedShader"
{
    Properties
    {
      _MainTex("Texture", 2D) = "white" {}
      _PixelSizeX("Pixel Size X", Range(1, 250)) = 1
      _PixelSizeY("Pixel Size Y", Range(1, 250)) = 1

      _Color0("Color 0",Color) = (0,0,0,1)
      _Color1("Color 1",Color) = (0,.9,.2,1)
      _Color2("Color 2",Color) = (.9,1,.3,1)
      _Color3("Color 3",Color) = (.9,.7,.1,1)
      _Color4("Color 4",Color) = (1,0,0,1)
      _Color5("Color 5",Color) = (0,0,0,1)
      _Color6("Color 6",Color) = (0,.9,.2,1)
      _Color7("Color 7",Color) = (.9,1,.3,1)
      _Color8("Color 8",Color) = (.9,.7,.1,1)
      _Color9("Color 9",Color) = (1,0,0,1)


      _Range0("Range 0",Range(0,1)) = 0.
      _Range1("Range 1",Range(0,1)) = 0.25
      _Range2("Range 2",Range(0,1)) = 0.5
      _Range3("Range 3",Range(0,1)) = 0.75
      _Range4("Range 4",Range(0,1)) = 1

      _Diameter("Diameter",Range(0,5)) = 1.0
      _Effect("Effect",Range(0,1)) = 0.0001
      _Effect2("Effect2",Range(0,1000)) = 1.0
      _Strength("Strength",Range(.1,100)) = 1.0
      _PulseSpeed("Pulse Speed",Range(0,5)) = 0
    }
        SubShader
      {
        Tags { "RenderType" = "Opaque" }
        LOD 100

        Pass
        {
          CGPROGRAM
          #pragma vertex vert
          #pragma fragment frag
          #pragma multi_compile_fog

          #include "UnityCG.cginc"

          struct appdata
          {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
          };

          struct v2f
          {
            float2 uv : TEXCOORD0;
            UNITY_FOG_COORDS(1)
            float4 vertex : SV_POSITION;
          };

          sampler2D _MainTex;
          float4 _MainTex_ST;

          float _PixelSizeX;
          float _PixelSizeY;

          float4 _Color0;
          float4 _Color1;
          float4 _Color2;
          float4 _Color3;
          float4 _Color4;

          float4 _Color5;
          float4 _Color6;
          float4 _Color7;
          float4 _Color8;
          float4 _Color9;


          float _Range0;
          float _Range1;
          float _Range2;
          float _Range3;
          float _Range4;
          float _Diameter;
          float _Strength;
          float _Effect;
          float _Effect2;

          float _PulseSpeed;

          v2f vert(appdata v)
          {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.uv, _MainTex);
            UNITY_TRANSFER_FOG(o,o.vertex);
            return o;
          }
          //----

          float4 colors[10]; //colors for point ranges
          float pointranges[5];  //ranges of values used to determine color values
          float4 _Hits[2000]; //passed in array of pointranges 3floats/point, x,y,intensity
          int _HitCount = 0;

          void initalize()
          {
            colors[0] = _Color0;
            colors[1] = _Color1;
            colors[2] = _Color2;
            colors[3] = _Color3;
            colors[4] = _Color4;
            colors[5] = _Color5;
            colors[6] = _Color6;
            colors[7] = _Color7;
            colors[8] = _Color8;
            colors[9] = _Color9;
            pointranges[0] = _Range0;
            pointranges[1] = _Range1;
            pointranges[2] = _Range2;
            pointranges[3] = _Range3;
            pointranges[4] = _Range4;
          }

          float4 getHeatForPixel(float weight, float playerIdx)
          {
              if (playerIdx == 0.0) {
                  //return colors[3];
                  if (weight <= pointranges[0])
                  {
                      return float4(0, 0, 0, 0);
                  }
                  if (weight >= pointranges[4])
                  {
                      return colors[4];
                  }
                  for (int i = 1; i < 5; i++)
                  {
                      if (weight < pointranges[i]) //if weight is between this point and the point before its range
                      {
                          float dist_from_lower_point = weight - pointranges[i - 1];
                          float size_of_point_range = pointranges[i] - pointranges[i - 1];

                          float ratio_over_lower_point = dist_from_lower_point / size_of_point_range;

                          //now with ratio or percentage (0-1) into the point range, multiply color ranges to get color

                          float4 color_range = colors[i] - colors[i - 1];

                          float4 color_contribution = color_range * ratio_over_lower_point;

                          float4 new_color = colors[i - 1] + color_contribution;
                          return new_color;

                      }
                  }
              }
              else {
                  //return colors[5];
                  if (weight <= pointranges[0])
                  {
                      return float4(0, 0, 0, 0);
                  }
                  if (weight >= pointranges[4])
                  {
                      return colors[9];
                  }
                  for (int i = 1; i < 5; i++)
                  {
                      if (weight < pointranges[i]) //if weight is between this point and the point before its range
                      {
                          float dist_from_lower_point = weight - pointranges[i - 1];
                          float size_of_point_range = pointranges[i] - pointranges[i - 1];

                          float ratio_over_lower_point = dist_from_lower_point / size_of_point_range;

                          //now with ratio or percentage (0-1) into the point range, multiply color ranges to get color

                          float4 color_range = colors[i + 5] - colors[i - 1 + 5];

                          float4 color_contribution = color_range * ratio_over_lower_point;

                          float4 new_color = colors[i - 1 + 5] + color_contribution;
                          return new_color;

                      }
                  }
              }

              return colors[0];
          }

          //Note: if distance is > 1.0, zero contribution, 1.0 is 1/2 of the 2x2 uv size
          float distsq(float2 a, float2 b)
          {
              float area_of_effect_size = _Diameter;

              return  pow(max(0.0, area_of_effect_size - distance(a, b)), 2.0);
          }

          half remap(half x, half t1, half t2, half s1, half s2)
          {
              return (x - t1) / (t2 - t1) * (s2 - s1) + s1;
          }

          fixed4 frag(v2f i) : SV_Target
          {
            float4 col = tex2D(_MainTex, i.uv);

            initalize();
            float area_of_effect_size = _Diameter;

            float newx = floor(i.uv.x * _PixelSizeX) / _PixelSizeX;
            float newy = floor(i.uv.y * _PixelSizeY) / _PixelSizeY;
            float2 uv = float2(newx, newy);
            //uv = uv * 4.0 - float2(2.0,2.0);  //our texture uv range is -2 to 2

            float hitCount = _HitCount - 1;
            float2 work_pt = float2(_Hits[hitCount].x, _Hits[hitCount].y);
            float pt_intensity = _Hits[hitCount].z;
            if (pt_intensity == 0.0)
                return float4(0, 0, 0, 0);
            float d = distsq(uv, work_pt);
            float dd = remap(d, 0, pow(_Diameter, 2), 0, 1);
            float4 colNew = getHeatForPixel(dd, _Hits[_HitCount - 1].w) * pt_intensity;

            //WTF???
            if (abs(distance(colNew, col)) < _Effect) {
                return col;
            }
            else {
                return col + colNew;
            }

            //return colNew;
           
           

            //if (dd > pointranges[3]) {
            //    // alpha is high, so just switch to new colors
            //    colNew = colNew;
            //}
            //else {
            //    // alpha is low, so overlay on top of old color using algorithm
            //    colNew = (col + colNew);
            //}

            //return colNew;
           
            //no memory
            //return colNew;

           
           
            // SCreen
            /*colNew = float4(1.0, 1.0, 1.0, 1.0) - colNew;
            float4 colOG = float4(1.0, 1.0, 1.0, 1.0) - col;
            return  float4(1.0, 1.0, 1.0, 1.0) - colNew * colOG;*/

            // Color Dodge
            //return min(colNew, col);

          }

          ENDCG
        }
      }
}

just ran into another thread that you replied to and I was thinking if using the render texture isn’t gonna work for some reason (can’t check new and old fragment color), then maybe I can use a compute buffer that stores every pixel color:
https://discussions.unity.com/t/784320 , this should work as another way right?

actually, I figured out the problem, it was with my render texture’s format. Before it was at R8G8B8A8_UNORM, and when I switched it to R32G32B32A32_FLOAT, it seems to be better. However now, I am getting the direct opposite of the problem before, the sampled color seems to be always black. When I do col + colNew, I seem to only get colNew.

I also changed the type of _MainTex from sampler2D to sampler2D_float to make sure I’m reading a 32 bit precision float from the R32G32B32A32_FLOAT render texture. But it seems like now reading from this texture is completely broken as I only get float4(0, 0, 0, 0) back.