vec2 SampleHeightmap(sampler2D cSampler, vec2 cSize, vec2 cPos, float rThreshold)
{
  vec2 cSizeInv = 1.0 / cSize;
  cPos = cPos * cSize - 0.5;
  vec2 uv = (floor(cPos) + 0.5) * cSizeInv;
  vec2 t = fract(cPos);

  vec4 cVals = vec4(
    texture2D(cSampler, uv).r,
    texture2D(cSampler, uv + vec2(cSizeInv.x, 0.0)).r,
    texture2D(cSampler, uv + vec2(0.0, cSizeInv.y)).r,
    texture2D(cSampler, uv + cSizeInv).r);

  bvec4 cValid = greaterThan(cVals, vec4(rThreshold));

//  if (all(cValid)) return vec2(mix(mix(cVals.x, cVals.y, t.x), mix(cVals.z, cVals.w, t.x), t.y), 1.0);

  vec4 bc1 = t.x < 1.0 - t.y ?
             vec4(1.0 - t.x - t.y, t.x, t.y, 0.0) :
             vec4(0.0, 1.0 - t.y, 1.0 - t.x, t.x + t.y - 1.0),
       bc2 = t.x > t.y ?
             vec4(1.0 - t.x, t.x - t.y, 0.0, t.y) :
             vec4(1.0 - t.y, 0.0, t.y - t.x, t.x);

  if (all(cValid) || cValid == greaterThan(bc1, vec4(0.0))) return vec2(dot(cVals, bc1), 1.0);
  if (cValid == greaterThan(bc2, vec4(0.0))) return vec2(dot(cVals, bc2), 1.0);
  return vec2(rThreshold, 0.0);

  vec2 cTemp = mix(mix(
      vec2(cVals.x, float(cValid.x)),
      vec2(cVals.y, float(cValid.y)), t.x), mix(
      vec2(cVals.z, float(cValid.z)),
      vec2(cVals.w, float(cValid.w)), t.x), t.y);
  return vec2(cTemp.x / cTemp.y, 0.0);
}

vec2 SampleHeightmapNoValue(sampler2D cSampler, vec2 cSize, vec2 cPos, float rNoValue)
{
  vec2 cSizeInv = 1.0 / cSize;
  cPos = cPos * cSize - 0.5;
  vec2 uv = (floor(cPos) + 0.5) * cSizeInv;
  vec2 t = fract(cPos);

  vec4 cVals = vec4(
    texture2D(cSampler, uv).r,
    texture2D(cSampler, uv + vec2(cSizeInv.x, 0.0)).r,
    texture2D(cSampler, uv + vec2(0.0, cSizeInv.y)).r,
    texture2D(cSampler, uv + cSizeInv).r);

  bvec4 cValid = notEqual(cVals, vec4(rNoValue));

  vec4 bc1 = t.x < 1.0 - t.y ?
             vec4(1.0 - t.x - t.y, t.x, t.y, 0.0) :
             vec4(0.0, 1.0 - t.y, 1.0 - t.x, t.x + t.y - 1.0),
       bc2 = t.x > t.y ?
             vec4(1.0 - t.x, t.x - t.y, 0.0, t.y) :
             vec4(1.0 - t.y, 0.0, t.y - t.x, t.x);

  if (all(cValid) || cValid == greaterThan(bc1, vec4(0.0))) return vec2(dot(cVals, bc1), 1.0);
  if (cValid == greaterThan(bc2, vec4(0.0))) return vec2(dot(cVals, bc2), 1.0);
  return vec2(rNoValue, 0.0);
}

vec2 SampleHorizonGradient(sampler2D cSampler, vec2 cSize, vec2 cPos, float rThreshold)
{
  vec2 cSizeInv = 1.0 / cSize;

  float val = texture2D(cSampler, cPos).r;
  if (val <= rThreshold) return vec2(0.0);
  vec4 vals = vec4(
    texture2D(cSampler, cPos - vec2(cSizeInv.x, 0.0)).r,
    texture2D(cSampler, cPos + vec2(cSizeInv.x, 0.0)).r,
    texture2D(cSampler, cPos - vec2(0.0, cSizeInv.y)).r,
    texture2D(cSampler, cPos + vec2(0.0, cSizeInv.y)).r);

  bvec4 valid = greaterThan(vals, vec4(rThreshold));
  if (!any(valid)) return vec2(0.0);

  return
    vec2(vals.x - val, vals.z - val) * vec2(valid.xz) +
    vec2(val - vals.y, val - vals.w) * vec2(valid.yw);
}

vec3 GetHorizonNormal(sampler2D cSampler, vec2 cSize, vec2 cPos, vec2 cValueScaleOffset)
{
  vec2 cSizeInv = 1.0 / cSize;
  vec2 uv0 = floor(cPos * cSize - 0.5) * cSizeInv + 0.5 * cSizeInv;
  vec2 grad0 = SampleHorizonGradient(cSampler, cSize, uv0, -cValueScaleOffset.y / cValueScaleOffset.x);
  vec2 grad1 = SampleHorizonGradient(cSampler, cSize, uv0 + vec2(cSizeInv.x, 0.0), -cValueScaleOffset.y / cValueScaleOffset.x);
  vec2 grad2 = SampleHorizonGradient(cSampler, cSize, uv0 + vec2(0.0, cSizeInv.y), -cValueScaleOffset.y / cValueScaleOffset.x);
  vec2 grad3 = SampleHorizonGradient(cSampler, cSize, uv0 + cSizeInv, -cValueScaleOffset.y / cValueScaleOffset.x);
  vec2 t = fract(cPos * cSize - 0.5);
  vec2 grad = mix(mix(grad0, grad1, t.x), mix(grad2, grad3, t.x), t.y);
  grad *= cValueScaleOffset.x;
  return normalize(vec3(grad * cSize, 2.0));
}
