/*****************************************************************************/
/*
  Project   - MudBun
  Publisher - Long Bunny Labs
              http://LongBunnyLabs.com
  Author    - Ming-Lun "Allen" Chou
              http://AllenChou.net
*/
/******************************************************************************/

#ifndef MUDBUN_BRUSH_FUNCS
#define MUDBUN_BRUSH_FUNCS

#include "BrushDefs.cginc"

#include "AabbTreeFuncs.cginc"
#include "BrushMaskFuncs.cginc"
#include "Math/CatmullRom.cginc"
#include "Math/Codec.cginc"
#include "Math/MathConst.cginc"
#include "Math/Quaternion.cginc"
#include "Math/Vector.cginc"
#include "Noise/RandomNoise.cginc"
#include "SDF/SDF.cginc"
#include "VoxelDefs.cginc"

SdfBrushMaterial init_brush_material()
{
  SdfBrushMaterial mat;
  mat.color = float4(0.0f, 0.0f, 0.0f, 1.0f);
  mat.emissionHash = float4(0.0f, 0.0f, 0.0f, 0.0f);
  mat.metallicSmoothnessSizeTightness = float4(0.0f, 0.0f, 1.0f, 0.0f);
  mat.textureWeight = float4(0.0f, 0.0f, 0.0f, 0.0f);
  mat.iBrush = -1;
  mat.padding0 = mat.padding1 = mat.padding2 = 0;
  return mat;
}

SdfBrushMaterial lerp(SdfBrushMaterial a, SdfBrushMaterial b, float t)
{
  SdfBrushMaterial o = a;

  o.color = lerp(a.color, b.color, t);
  o.emissionHash.rgb = lerp(a.emissionHash.rgb, b.emissionHash.rgb, t);
  o.emissionHash.a = (t < 0.5f) ? a.emissionHash.a : b.emissionHash.a;
  o.iBrush = (t < 0.5f) ? a.iBrush : b.iBrush;
  o.metallicSmoothnessSizeTightness.xyz = lerp(a.metallicSmoothnessSizeTightness.xyz, b.metallicSmoothnessSizeTightness.xyz, t);
  o.textureWeight = lerp(a.textureWeight, b.textureWeight, t);

  return o;
}

SdfBrushMaterialCompressed lerp(SdfBrushMaterialCompressed a, SdfBrushMaterialCompressed b, float t)
{
  return pack_material(lerp(unpack_material(a), unpack_material(b), t));
}

float sdf_boundary(float3 pRel, SdfBrush b, int shape, out float fadeDist)
{
  float3 h = abs(0.5f * b.size);

  fadeDist = 0.0f;

  float res = kInfinity;
  switch (shape)
  {
    case kSdfNoiseBoundaryBox:
    {
      res = sdf_box(pRel, h);
      fadeDist = max_comp(h);
      break;
    }

    case kSdfNoiseBoundarySphere:
    {
      res = sdf_ellipsoid(pRel, b.radius * b.size);
      fadeDist = b.radius * max_comp(b.size);
      break;
    }

    case kSdfNoiseBoundaryCylinder:
    {
      float2 elongation = max(0.0f, b.size.xz - 1.0f);
      pRel.xz -= clamp(pRel.xz, -elongation, elongation);
      res = sdf_cylinder(pRel, h.y, b.radius);
      fadeDist = max(b.radius, h.y);
      break;
    }

    case kSdfNoiseBoundaryTorus:
    {
      float3 hTorus = float3(h.x + 0.5f * b.radius, h.y, h.z + 0.5f * b.radius);
      res = sdf_torus(pRel, hTorus.x - hTorus.z, hTorus.z - b.radius, b.radius);
      fadeDist = max(max(h.x, h.z), b.radius);
      break;
    }

    case kSdfNoiseBoundarySolidAngle:
    {
      res = sdf_solid_angle(pRel, float2(b.data3.x, b.data3.y), b.radius);
      res = sdf_int(res, sdf_box(pRel, b.radius * b.size));
      fadeDist = b.radius;
      break;
    }
  }

  return res;
}

#include "../Customization/CustomBrush.cginc"

float sdf_brush(float res, inout float3 p, SdfBrush b)
{
  float preMirrorX = p.x;

  bool doMirrorX = ((b.flags & kSdfBrushFlagsMirrorX) != 0);
  if (doMirrorX)
    p.x = abs(p.x);

  bool flipX = ((b.flags & kSdfBrushFlagsFlipX) != 0);
  if (flipX)
    p.x = -p.x;

  // extent
  float3 h = abs(0.5f * b.size);

  // relative to transform
  float3 pRel = quat_rot(quat_inv(b.rotation), p - b.position);

  switch (b.type)
  {
    case kSdfBox:
    {
      float pivotShift = b.data0.x;
      pRel.y += pivotShift * h.y;
      res = sdf_box(pRel, h, b.radius);
      break;
    }

    case kSdfSphere:
    {
      float pivotShift = b.data0.x;
      pRel.y += pivotShift * h.y;
      res = sdf_ellipsoid(pRel, b.radius * b.size);
      break;
    }

#ifndef MUDBUN_FAST_ITERATION
    case kSdfCylinder:
    {
      float pivotShift = b.data0.z;
      pRel.y += pivotShift * h.y;

      float2 elongation = max(0.0f, b.size.xz - 1.0f);
      pRel.xz -= clamp(pRel.xz, -elongation, elongation);

      res = sdf_capped_cone(pRel, h.y, b.radius, max(0.0f, b.radius + b.data0.y), b.data0.x);
      break;
    }

    case kSdfTorus:
    {
      float elongation = b.data0.x;
      pRel.y -= clamp(pRel.y, -elongation, elongation);
      float3 hTorus = float3(h.x + 0.5f * b.radius, h.y, h.z + 0.5f * b.radius);
      float r = abs(0.25f * b.size.y);
      res = sdf_torus(pRel, hTorus.x - hTorus.z, hTorus.z - r, r);
      break;
    }

    case kSdfSolidAngle:
    {
      res = sdf_solid_angle(pRel, float2(b.data0.x, b.data0.y), b.radius, b.data0.z);
      break;
    }

  #ifndef MUDBUN_DISABLE_SDF_NOISE_VOLUME
    case kSdfNoiseVolume:
    case kSdfNoiseModifier:
    {
      float thresholdFadeDist = kEpsilon;
      int boundaryShape = int(b.data2.z);
      float noiseRes = sdf_boundary(pRel, b, boundaryShape, thresholdFadeDist);
      float thresholdFadeT = sqrt(saturate(length(pRel) / thresholdFadeDist));

      // because noise functions are not real SDFs
      float distScale = 1.0f;

      float3 aSample[2];
      float3 aPeriod[2];
      float aWeight[2];
      int numSamples = 0;

      bool lockPosition = ((b.flags & kSdfBrushFlagsLockNoisePosition) != 0);
      float3 size = b.data0.xyz;
      float3 offset = b.data1.xyz;
      if ((b.flags & kSdfBrushFlagsSphericalNoiseCoordinates) == 0)
      {
        // uniform
        aSample[0] = lockPosition ? pRel : p;
        aPeriod[0] = kCartesianNoisePeriod;
        aWeight[0] = 1.0f;
        numSamples = 1;
      }
      else
      {
        // radial
        aSample[0] = cartesian_to_spherical(pRel.xzy + kEpsilon) * float3(1.0f, kSphericalNoisePeriod / kTwoPi, 1.0f);
        aSample[1] = cartesian_to_spherical(-pRel.xyz + kEpsilon) * float3(1.0f, kSphericalNoisePeriod / kTwoPi, 1.0f);
        aPeriod[0] = float3(kCartesianNoisePeriod, kSphericalNoisePeriod, kCartesianNoisePeriod);
        aPeriod[1] = float3(kCartesianNoisePeriod, kSphericalNoisePeriod, kCartesianNoisePeriod);
        aWeight[0] = min(aSample[0].z, kPi - aSample[0].z) / kHalfPi;
        aWeight[1] = 1.0f - aWeight[0];
        numSamples = 2;

        if (!lockPosition)
        {
          aSample[0] += b.position;
          aSample[1] += b.position;
        }

        distScale = 0.25f; //clamp(s.x, 1.0f, 1.0f);
      }
      float threshold = b.data0.w;
      float thresholdFade = b.data2.y;
      threshold += (1.0f - threshold) * thresholdFade * thresholdFadeT;
      int numOctaves = int(b.data1.w);
      float octaveOffsetFactor = b.data2.x;
      float boundaryBlend = b.data2.w;
      boundaryBlend = max(0.1f * min(min(h.x, h.y), h.z), boundaryBlend);
      int noiseType = int(b.data3.z);
      float sRes = 0.0f;
      [loop] for (int i = 0; i < numSamples; ++i)
      {
        float s = sdf_noise(noiseType, aSample[i], -h, h, offset, size, threshold, numOctaves, octaveOffsetFactor, aPeriod[i]);
        sRes += aWeight[i] * s * distScale;
      }
      noiseRes = sdf_int_cubic(noiseRes, sRes, boundaryBlend);

      switch (b.type)
      {
        case kSdfNoiseVolume:
          res = noiseRes;
          break;
        case kSdfNoiseModifier:
          res -= noiseRes * b.blend;
          break;
      }
      break;
    }
  #endif // MUDBUN_DISABLE_SDF_NOISE_VOLUME

  #ifndef MUDBUN_DISABLE_SDF_SIMPLE_CURVE
    case kSdfCurveSimple:
    {
      float3 pA = b.data0.xyz;
      float3 pB = b.data1.xyz;
      float3 pC = b.data2.xyz;

      float3 pRelRaw = pRel;
      float elongation = b.data3.x;
      pRel.z -= clamp(pRel.z, -elongation, elongation);

      float controlPointR = b.data3.y;
      float smoothStepBlend = b.data3.z;
      float r = 0.0f;

      const bool colinear = b.data3.w > 0.0f;
      float2 curRes = 
        b.data3.w > 0.0f // colinear?
          ? sdf_segment(pRel, pA, pB) 
          : sdf_bezier(pRel, pA, pC, pB);

      if (controlPointR < 0.0f)
      {
        float t = curRes.y;
        r = b.data0.w + (b.data1.w - b.data0.w) * lerp(t, smoothstep(0.0f, 1.0f, t), smoothStepBlend);
      }
      else
      {
        if (curRes.y < 0.5f)
        {
          float t = 2.0f * curRes.y;
          r = b.data0.w + (controlPointR - b.data0.w) * lerp(t, smoothstep(0.0f, 1.0f, t), smoothStepBlend);
        }
        else
        {
          float t = 2.0f * (curRes.y - 0.5f);
          r = controlPointR + (b.data1.w - controlPointR) * lerp(t, smoothstep(0.0f, 1.0f, t), smoothStepBlend);
        }
      }
      res = curRes.x - r;

      bool useNoise = (b.data2.w > 0.0f);
      if (useNoise)
      {
        float curveLen = 0.0f;
        int precision = 16;
        float dt = 1.0f / precision;
        float t = dt;
        float3 prevPos = pA;
        [loop] for (int i = 1; i < precision; ++i, t += dt)
        {
          float3 currPos = bezier_quad(pA, pC, pB, t);
          curveLen += length(currPos - prevPos);
          prevPos = currPos;
        }
        if (curRes.y < 0.0001f)
          curRes.y = min(0.0f, -dot(normalize(pA - pC), pRel - pA) / curveLen);
        else if (curRes.y > 0.9999f)
          curRes.y = max(1.0f, 1.0f + dot(normalize(pB - pC), pRel - pB) / curveLen);

        float3 up = normalize(kUnitY + 1e-3f * mbn_rand(pA));
        float3 front = normalize(slerp(pA - pC, pC - pB, curRes.y));
        float3 left = normalize(cross(up, front));
        up = cross(front, left);
        float3 closest = bezier_quad(pA, pC, pB, curRes.y);
        float3 pDelta = pRelRaw - closest;
        float3 s = float3(curRes.y * curveLen, dot(pDelta, up), dot(pDelta, left));

        // advance to additional noise data
        b = aBrush[b.index + 1];

        float thresholdFade = b.data3.x;
        float thresholdCoreBias = b.data3.y;

        // twist
        float twistA = b.data2.y;
        float twistB = b.data2.z;
        float twistT = lerp(twistA, twistB, curRes.y);
        float twistCos = cos(twistT);
        float twistSin = sin(twistT);
        s.yz = mul(float2x2(twistCos, twistSin, -twistSin, twistCos), s.yz);

        float3 offset = b.data1.xyz;
        float3 size = b.data0.xyz;
        float threshold = b.data0.w;
        float rDelta = length(pDelta);
        float coreBiasT = 1.0f - saturate(rDelta / max(kEpsilon, r));
        threshold = saturate(threshold + sign(thresholdCoreBias) * abs(thresholdCoreBias) * coreBiasT);
        threshold += (1.0f - threshold) * thresholdFade * saturate(curRes.y);
        int numOctaves = int(b.data1.w);
        float octaveOffsetFactor = b.data2.x;

        float twistSdfMult = 1.0f / (1.0f + saturate(abs(twistA - twistB))); // hack: evlauate more surrounding voxels when twisted to avoid holes
        float n = twistSdfMult * sdf_noise(kSdfNoiseTypeCachedPerlin, s, -kInfinity, kInfinity, offset, size, threshold, numOctaves, octaveOffsetFactor, kCartesianNoisePeriod);
        res = sdf_int_cubic(res, n, 0.5f * r);
      }

      break;
    }
  #endif // MUDBUN_DISABLE_SDF_SIMPLE_CURVE

  #ifndef MUDBUN_DISABLE_SDF_FULL_CURVE
    case kSdfCurveFull:
    {
      int numPoints = int(b.data0.x);
      if (numPoints > 1)
      {
        res = kInfinity;

        int precision = int(b.data0.y);
        float dt = 1.0f / precision;

        bool useNoise = false;//(b.data0.z > 0.0f);

        int iA = b.index + (useNoise ? 2 : 1);
        float globalLen = 0.0f;
        int iClosest = -1;
        float tClosest = 0.0f;
        float segResClosest = 0.0f;
        float rClosest = 0.0f;
        float3 pClosest = 0.0f;
        float closestLen = 0.0f;
        [loop] for (int i = 1, n = numPoints - 2; i < n; ++i, ++iA)
        {
          float3 pA = aBrush[iA + 0].data0.xyz;
          float3 pB = aBrush[iA + 1].data0.xyz;
          float3 pC = aBrush[iA + 2].data0.xyz;
          float3 pD = aBrush[iA + 3].data0.xyz;
          float3 prevPos = pB;
          float r = aBrush[iA + 1].data0.w;
          float dr = (aBrush[iA + 2].data0.w - r) * dt;
          float localLen = 0.0f;
          for (float t = dt; t < 1.0001f; t += dt)
          {
            float3 currPos = catmull_rom(pA, pB, pC, pD, min(1.0f, t));
            float segLen = length(currPos - prevPos);
            float d = sdf_round_cone(p, prevPos, currPos, r, r + dr);
            if (d < res)
            {
              float2 segRes = sdf_segment(p, prevPos, currPos);
              res = d;
              iClosest = i;
              tClosest = t;
              rClosest = r + dr * segRes.y;
              pClosest = lerp(prevPos, currPos, segRes.y);
              closestLen = globalLen + localLen + segLen * segRes.y;
              segResClosest = segRes.y;
            }
            prevPos = currPos;
            r += dr;
            localLen += segLen;
          }
          globalLen += localLen;
        }
        
        /*
        if (iClosest > 0 
            && globalLen > kEpsilon 
            && useNoise)
        {
          int iA = b.index + (useNoise ? 2 : 1) + (iClosest - 1); // reset
          float3 p0 = (iClosest > 1) ? aBrush[iA - 1].data0.xyz : aBrush[iA + 0].data0.xyz;
          float3 pA = aBrush[iA + 0].data0.xyz;
          float3 pB = aBrush[iA + 1].data0.xyz;
          float3 pC = aBrush[iA + 2].data0.xyz;
          float3 pD = aBrush[iA + 3].data0.xyz;
          float3 pE = (iClosest < numPoints - 3) ? aBrush[iA + 4].data0.xyz : aBrush[iA + 3].data0.xyz;
          float3 segPosB = catmull_rom(pA, pB, pC, pD, tClosest - dt);
          float3 segPosC = catmull_rom(pA, pB, pC, pD, tClosest);
          float3 front = normalize(segPosC - segPosB);
          float3 dir0B = pB - p0;
          float3 dirAC = pC - pA;
          float3 dirBD = pD - pB;
          float3 dirCE = pE - pC;
          //float3 front = normalize(catmull_rom(dir0B, dirAC, dirBD, dirCE, tClosest + (-1.0f + segResClosest) * dt));
          float3 up = normalize(kUnitY + 1e-3f * mbn_rand(aBrush[iA].data0.xyz));
          float3 left = normalize(cross(up, front));
          up = cross(front, left);
          float3 s = float3(closestLen, dot(p - pClosest, up), dot(p - pClosest, left));
          float3 offset = aBrush[b.index + 1].data1.xyz;
          float3 size = aBrush[b.index + 1].data0.xyz;
          float threshold = aBrush[b.index + 1].data0.w;
          int numOctaves = int(aBrush[b.index + 1].data1.w);
          float octaveOffsetFactor = aBrush[b.index + 1].data2.x;
          // TODO: noise type (if we ever re-instate noise along full curves...)
          float n = sdf_noise(kSdfNoiseTypeCachedPerlin, s, -kInfinity, kInfinity, offset, size, threshold, numOctaves, octaveOffsetFactor, kCartesianNoisePeriod);
          res = sdf_int_cubic(res, n, 0.5f * rClosest);
        }
        */
      }
      break;
    }
  #endif // MUDBUN_DISABLE_SDF_FULL_CURVE

    case kSdfParticleSystem:
    {
      res = kInfinity;
      int numParticles = int(b.data2.x);
      for (int i = 0; i < numParticles; ++i)
      {
        float3 pos = aBrush[b.index + i].data0.xyz;
        float r = aBrush[b.index + i].data0.w;
        float selfBlend = aBrush[b.index + i].data1.x;
        res = sdf_uni_cubic(res, sdf_sphere(p - pos, r), selfBlend);
      }
      break;
    }

  #ifndef MUDBUN_DISABLE_SDF_DISTORTION_BRUSHES
    case kSdfFishEye:
    {
      float r = length(pRel);
      if (r > b.radius)
        break;

      float t = r / b.radius;
      float strength = b.data0.x;
      float fade = 1.0f - pow(abs(t), strength);
      p -= (b.radius * fade) * quat_rot(b.rotation, normalize_safe(pRel, kUnitY));
      break;
    }

    case kSdfPinch:
    {
      float depth = b.data0.x;
      float r = length(pRel.xz);
      if (sdf_cylinder(pRel + float3(0.0f, 0.5f * depth, 0.0f), 0.5f * depth, b.radius) > 0.0f)
        break;
      
      float amount = b.data0.y;
      float strength = b.data0.z;
      float g = -pRel.y / depth;
      float t = r / max(kEpsilon, b.radius);
      float pinchRatio = pow(abs(1.0f - t), strength);
      g = pow(abs(g), 0.5f);
      pRel.y = -g * depth; // remap
      float fade = (depth + pRel.y) / depth;
      p += (amount * pinchRatio * fade) * quat_rot(b.rotation, float3(0.0f, pRel.y, 0.0f));
      break;
    }

    case kSdfTwist:
    {
      if (sdf_cylinder(pRel, h.y, b.radius, 0.0f) > 0.0f)
        break;
    
      float angle = b.data0.x;
      float strength = b.data0.y;
      float r = length(pRel.xz);
      float t = r / b.radius;
      float a = angle * (1.0f - pow(abs(t), strength));
      float s = sin(a);
      float c = cos(a);
      pRel.xz = mul(float2x2(c, -s, s, c), pRel.xz);
      p = quat_rot(b.rotation, pRel) + b.position;
      break;
    }

    case kSdfQuantize:
    {
      float cellSize = b.data0.x;
      float fade = b.data0.z;
      float d = sdf_box(pRel, h, fade * cellSize);
      if (d > 0.0f)
        break;
      
      float strength = b.data0.y;
      float3 r = p / cellSize;
      float3 f = floor(r);
      float3 t = r - f;
      float3 q = (f + smoothstep(0.0f, 1.0f, max(1.0f, strength) * (t - 0.5f) + 0.5f)) * cellSize;
      p = lerp(p, q, saturate(strength) * saturate(-d / max(kEpsilon, fade * cellSize)));
      break;
    }
  #endif // MUDBUN_DISABLE_SDF_DISTORTION_BRUSHES

  #ifndef MUDBUN_DISABLE_SDF_MODIFIER_BRUSHES
    case kSdfOnion:
    {
      float d = sdf_box(pRel, h, b.blend);
      if (d > 0.0f)
        break;
      
      float thickness = b.data0.x;
      res = abs(res) - thickness;
      break;
    }
  #endif // MUDBUN_DISABLE_SDF_MODIFIER_BRUSHES
#endif // MUDBUN_FAST_ITERATION

    default:
    {
      res = sdf_custom_brush(res, p, pRel, b);
      break;
    }
  }

  if (flipX || doMirrorX)
    p.x = preMirrorX;

  return res;
}

float sdf_distortion_modifier_bounds_query(float3 p, SdfBrush b)
{
  float res = kInfinity;

  float3 pRel = quat_rot(quat_inv(b.rotation), p - b.position);
  float3 h = 0.5f * b.size;

  switch (b.type)
  {
#ifndef MUDBUN_FAST_ITERATION
    case kSdfPinch:
    {
      float depth = b.data0.x;
      res = sdf_cylinder(pRel + float3(0.0f, 0.5f * depth, 0.0f), 0.5f * depth, b.radius);
      break;
    }

    case kSdfTwist:
    {
      float angle = b.data0.x;
      res = sdf_cylinder(pRel, h.y, b.radius);
      break;
    }

    case kSdfQuantize:
    {
      float cellSize = b.data0.x;
      float fade = b.data0.z;
      res = sdf_box(pRel, h, fade * cellSize);
      break;
    }

    case kSdfFishEye:
    {
      res = sdf_sphere(pRel, b.radius);
      break;
    }

    case kSdfOnion:
    {
      float thickness = b.data0.x;
      res = sdf_box(pRel, h + thickness, b.blend);
      break;
    }
#endif // MUDBUN_FAST_ITERATION

    default:
    {
      res = sdf_custom_distortion_modifier_bounds_query(p, pRel, b);
      break;
    }
  }

  return res;
}

float dist_blend_weight(float distA, float distB, float strength)
{
  float m = 1.0f / max(kEpsilon, distA);
  float n = 1.0f / max(kEpsilon, distB);
  m = pow(m, strength);
  n = pow(n, strength);
  return saturate(n / (m + n));
}

float sdf_brush_apply(float res, float groupRes, SdfBrushMaterial groupMat, inout float3 p, SdfBrush b, inout SdfBrushMaterial oMat, bool outputMat = true, float halfNodeDiag = -1.0f)
{
  float d = sdf_brush(res, p, b);

  if (b.type == kSdfEndGroup)
    d = groupRes;

  bool isGroupBrush = false;
  switch (b.type)
  {
  case kSdfBeginGroup:
  case kSdfEndGroup:
    isGroupBrush = true;
    break;
  }

  float tMat = 0.0f;
  float blend = b.blend;
  switch (b.op)
  {
    /*
    case kSdfUnionQuad:
      // compute tMat before res is updated!
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      tMat = dist_blend_weight(res, d, 1.5f);
      if (enable2dMode && d < 0.25 * blend)
        tMat = max(tMat, min(1.0f, 1.0f - d / max(kEpsilon, 0.25 * blend)));
      res = sdf_uni_quad(res, d, blend);
      break;

    case kSdfSubtractQuad:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d += surfaceShift;
      res = sdf_sub_quad(res, d, blend);
      if (enable2dMode)
        tMat = 1.0f - saturate(d / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(2.0f * (d - 1.5f * voxelSize) / max(kEpsilon, blend));
      break;

    case kSdfIntersectQuad:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      res = sdf_int_quad(res, d, blend);
      if (enable2dMode)
        tMat = saturate((-d + voxelSize) / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(-2.0f * (d + 1.0f * voxelSize) / max(kEpsilon, blend));
      break;
    */

    case kSdfUnionCubic:
      // compute tMat before res is updated!
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      tMat = dist_blend_weight(res, d, 1.5f);
      if (enable2dMode && d < 0.25 * blend)
        tMat = max(tMat, min(1.0f, 1.0f - d / max(kEpsilon, 0.25 * blend)));
      res = sdf_uni_cubic(res, d, blend);
      break;

    case kSdfSubtractCubic:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d += surfaceShift;
      res = sdf_sub_cubic(res, d, blend);
      if (enable2dMode)
        tMat = 1.0f - saturate(d / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(2.0f * (d - 1.5f * voxelSize) / max(kEpsilon, blend));
      break;

    case kSdfIntersectCubic:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      res = sdf_int_cubic(res, d, blend);
      if (enable2dMode)
        tMat = saturate((-d + voxelSize) / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(-2.0f * (d + 1.0f * voxelSize) / max(kEpsilon, blend));
      break;

    /*
    case kSdfUnionRound:
      // compute tMat before res is updated!
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      tMat = dist_blend_weight(res, d, 1.5f);
      if (enable2dMode && d < 0.25 * blend)
        tMat = max(tMat, min(1.0f, 1.0f - d / max(kEpsilon, 0.25 * blend)));
      res = sdf_uni_round(res, d, blend);
      break;

    case kSdfSubtractRound:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d += surfaceShift;
      res = sdf_sub_round(res, d, blend);
      if (enable2dMode)
        tMat = 1.0f - saturate(d / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(2.0f * (d - 1.5f * voxelSize) / max(kEpsilon, blend));
      break;

    case kSdfIntersectRound:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      res = sdf_int_round(res, d, blend);
      if (enable2dMode)
        tMat = saturate((-d + voxelSize) / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(-2.0f * (d + 1.0f * voxelSize) / max(kEpsilon, blend));
      break;
    */

    case kSdfUnionChamfer:
      // compute tMat before res is updated!
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      tMat = dist_blend_weight(res, d, 1.5f);
      if (enable2dMode && d < 0.25 * blend)
        tMat = max(tMat, min(1.0f, 1.0f - d / max(kEpsilon, 0.25 * blend)));
      res = sdf_uni_chamfer(res, d, blend);
      break;

    case kSdfSubtractChamfer:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d += surfaceShift;
      res = sdf_sub_chamfer(res, d, blend);
      if (enable2dMode)
        tMat = 1.0f - saturate(d / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(2.0f * (d - 1.5f * voxelSize) / max(kEpsilon, blend));
      break;

    case kSdfIntersectChamfer:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      res = sdf_int_chamfer(res, d, blend);
      if (enable2dMode)
        tMat = saturate((-d + voxelSize) / max(kEpsilon, blend));
      else
        tMat = 1.0f - saturate(-2.0f * (d + 1.0f * voxelSize) / max(kEpsilon, blend));
      break;

    case kSdfPipe:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      res = sdf_pipe(res, d, blend);
      tMat = saturate((-d + voxelSize) / max(kEpsilon, blend));
      break;

    case kSdfEngrave:
      res = sdf_engrave(res, d, blend);
      tMat = 1.0f - saturate((abs(d) - voxelSize) / max(kEpsilon, blend));
      break;

    case kSdfCullInside:
      if (halfNodeDiag < 0.0f)
        break;
      if (!isGroupBrush 
          || b.type == kSdfEndGroup)
      {
        if (d < -halfNodeDiag)
          res = kCull;
      }
      break;

    case kSdfCullOutside:
      if (halfNodeDiag < 0.0f)
        break;
      if (!isGroupBrush 
          || b.type == kSdfEndGroup)
      {
        if (d > halfNodeDiag)
          res = kCull;
      }
      break;

    case kSdfDistort:
      res = sdf_uni(res, d);
      break;

    case kSdfModify:
      res = d;
      break;

    /*
    case kSdfDye:
      if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
        d -= surfaceShift;
      if (enable2dMode)
        tMat = 1.0f - saturate((d - voxelSize) / max(kEpsilon, 0.25f * blend));
      else
        tMat = 1.0f - saturate(max(0.0f, d) / max(kEpsilon, blend));
      break;
    */

    default:
      if (is_sdf_dye(b.op))
      {
        if (!isGroupBrush) // don't perform surface shift on group brushes, or it might crash the GPU!
          d -= surfaceShift;
        if (enable2dMode)
          tMat = 1.0f - saturate((d - voxelSize) / max(kEpsilon, 0.25f * blend));
        else
          tMat = 1.0f - saturate(max(0.0f, d) / max(kEpsilon, blend));
      }
      break;
  }

  if (b.materialIndex >= 0)
  {
    float blendTightness = aBrushMaterial[b.materialIndex].metallicSmoothnessSizeTightness.w;
    if (blendTightness > 0.0f)
    {
      // remap to between [-1.0, 1.0]
      // take 1.0 - x
      // curve with tightness
      // take 1.0 - x
      // remap back to [0.0, 1.0]
      tMat -= 0.5f;
      tMat = 0.5f + 0.5f * sign(tMat) * (1.0f - pow(abs(1.0f - abs(2.0f * tMat)), pow(1.0f + blendTightness, 5.0f)));
    }

    SdfBrushMaterial iMat = aBrushMaterial[b.materialIndex];
    if (b.type == kSdfEndGroup)
      iMat = groupMat;

    if ((b.flags & kSdfBrushFlagsContributeMaterial) == 0)
    {
      iMat = oMat;
    }

    iMat.emissionHash.a = b.hash;
    iMat.iBrush = b.index;

    // dye blend modes
    // https://docs.unity3d.com/Packages/com.unity.shadergraph@14.0/manual/Blend-Node.html
    if (is_sdf_dye(b.op))
    {
      float3 base = oMat.color.rgb;
      float3 blend = iMat.color.rgb;
      float3 opacity = iMat.color.a;
      switch (b.op)
      {
        case kSdfDye:
          // overwrite; do nothing
          break;
        /*
        case kSdfDyeBlendBurn:
          iMat.color.rgb = lerp(base, 1.0f - (1.0f - blend) / max(1e-6f, base), opacity);
          break;
        case kSdfDyeBlendDarken:
          iMat.color.rgb = lerp(base, min(blend, base), opacity);
          break;
        case kSdfDyeBlendDifference:
          iMat.color.rgb = lerp(base, abs(blend - base), opacity);
          break;
        case kSdfDyeBlendDodge:
          iMat.color.rgb = lerp(base, base / max(1e-6f, 1.0f - blend), opacity);
          break;
        case kSdfDyeBlendDivide:
          iMat.color.rgb = lerp(base, base / max(1e-6f, blend), opacity);
          break;
        case kSdfDyeBlendExclusion:
          iMat.color.rgb = lerp(base, blend + base - 2.0f * blend * base, opacity);
          break;
        case kSdfDyeBlendHardLight:
          {
            float3 result1 = 1.0f - 2.0f * (1.0f - base) * (1.0f - blend);
            float3 result2 = 2.0f * base * blend;
            float3 zeroOrOne = step(blend, 0.5f);
            blend = result2 * zeroOrOne + (1.0f - zeroOrOne) * result1;
            iMat.color.rgb = lerp(base, blend, opacity);
          }
          break;
        case kSdfDyeBlendHardMix:
          iMat.color.rgb = lerp(base, step(1.0f - base, blend), opacity);
          break;
        case kSdfDyeBlendLighten:
          iMat.color.rgb = lerp(base, max(blend, base), opacity);
          break;
        case kSdfDyeBlendLightBurn:
          iMat.color.rgb = lerp(base, base + blend - 1.0f, opacity);
          break;
        case kSdfDyeBlendLinearDodge:
          iMat.color.rgb = lerp(base, base + blend, opacity);
          break;
        case kSdfDyeBlendLinearLight:
          iMat.color.rgb = lerp(base, blend < 0.5f ? max(base + 2.0f * blend - 1.0f, 0.0f) : min(base + 2.0f * (blend - 0.5f), 1.0f), opacity);
          break;
        */
        case kSdfDyeBlendMultiply:
          iMat.color.rgb = lerp(base, base * blend, opacity);
          break;
        /*
        case kSdfDyeBlendNegation:
          iMat.color.rgb = lerp(base, 1.0f - abs(1.0f - blend - base), opacity);
          break;
        */
        case kSdfDyeBlendOverlay:
          {
            float3 result1 = 1.0f - 2.0f * (1.0f - base) * (1.0f - blend);
            float3 result2 = 2.0f * base * blend;
            float3 zeroOrOne = step(base, 0.5f);
            blend = result2 * zeroOrOne + (1.0f - zeroOrOne) * result1;
            iMat.color.rgb = lerp(base, blend, opacity);
          }
          break;
        /*
        case kSdfDyeBlendPinLight:
          {
            float3 check = step(0.5f, blend);
            float3 result1 = check * max(2.0f * (base - 0.5f), blend);
            blend = result1 + (1.0f - check) * min(2.0f * base, blend);
            iMat.color.rgb = lerp(base, blend, opacity);
          }
          break;
        */
        case kSdfDyeBlendScreen:
          iMat.color.rgb = lerp(base, 1.0f - (1.0f - blend) * (1.0f - base), opacity);
          break;
        /*
        case kSdfDyeBlendSoftLight:
          {
            float3 result1 = 2.0f * base * blend + base * base * (1.0f - 2.0f * blend);
            float3 result2 = sqrt(base) * (2.0f * blend - 1.0f) + 2.0f * base * (1.0f - blend);
            float3 zeroOrOne = step(0.5f, blend);
            blend = result2 * zeroOrOne + (1.0f - zeroOrOne) * result1;
            iMat.color.rgb = lerp(base, blend, opacity);
          }
          break;
        case kSdfDyeBlendSubtract:
          iMat.color.rgb = lerp(base, base - blend, opacity);
          break;
        case kSdfDyeBlendVividLight:
          {
            float3 result1 = 1.0f - (1.0f - blend) / (2.0f * base);
            float3 result2 = blend / (2.0f * (1.0f - base));
            float3 zeroOrOne = step(0.5f, base);
            blend = result2 * zeroOrOne + (1.0f - zeroOrOne) * result1;
            iMat.color.rgb = lerp(base, blend, opacity);
          }
          break;
        */
        case kSdfDyeBlendModePaint:
          iMat.color.rgb = lerp(base, 2 * base * blend, opacity);
          break;
      }

      iMat.color.rgb = saturate(iMat.color.rgb);

      if (b.op != kSdfDye)
      {
        // non-overwrite blend modes don't affect alpha
        iMat.color.a = oMat.color.a;
      }
    } // end: dye blend modes

    // selection highlight
    if (b.hash < 0.0f)
    {
      iMat.color.rgb = saturate(iMat.color.rgb + 0.1f);
      iMat.emissionHash.rgb = saturate(iMat.emissionHash.rgb + 0.1f);
    }

    oMat = lerp(oMat, iMat, tMat);
  }

  return res;
}

float sdf_all_brushes(float3 p, int iBrushMask, out SdfBrushMaterial mat, bool outputMat = true)
{
  mat = init_brush_material();
  float res = kInfinity;
  for (int iBrush = 0; iBrush < numBrushes; ++iBrush)
    res = sdf_brush_apply(res, res, mat, p, aBrush[iBrush], mat);

  res -= surfaceShift;

  return res;
}

float sdf_masked_brushes(float3 p, int iBrushMask, out SdfBrushMaterial mat, bool outputMat = true, float halfNodeDiag = -1.0f)
{
  int iStack = -1;
  float3 pStack[kMaxBrushGroupDepth];
  float resStack[kMaxBrushGroupDepth];
  SdfBrushMaterial matStack[kMaxBrushGroupDepth];

  float res = kInfinity;
  mat = init_brush_material();
  float groupRes = kInfinity;
  SdfBrushMaterial groupMat = init_brush_material();
  FOR_EACH_BRUSH(iBrushMask, 
    switch (aBrush[iBrush].type)
    {
      case kSdfBeginGroup:
        {
          iStack = min(kMaxBrushGroupDepth - 1, iStack + 1);
          pStack[iStack] = p;
          resStack[iStack] = res;
          matStack[iStack] = mat;
          res = kInfinity;
          mat = init_brush_material();

          bool doMirrorX = ((aBrush[iBrush].flags & kSdfBrushFlagsMirrorX) != 0);
          if (doMirrorX)
            p.x = abs(p.x);

          bool flipX = ((aBrush[iBrush].flags & kSdfBrushFlagsFlipX) != 0);
          if (flipX)
            p.x = -p.x;
        }
        break;
      case kSdfEndGroup:
        {
          groupRes = res;
          groupMat = mat;
          p = pStack[iStack];
          res = resStack[iStack];
          mat = matStack[iStack];
        }
        break;
    }
    res = sdf_brush_apply(res, groupRes, groupMat, p, aBrush[iBrush], mat, outputMat, halfNodeDiag);
    if (res == kCull)
      break; // early-out if we have decided to cull the node
    switch (aBrush[iBrush].type)
    {
      case kSdfEndGroup:
        iStack = max(-1, iStack - 1);
        break;
    }
  );

  return res;
}

#endif


