2025.11.24

Github:

https://github.com/sakkom/portfolio-kuhu/tree/main/app/kuhu/sdf/day3Metaball2d

2D SDFでメタボール風


uniform vec2 uResolution;
uniform float uTime;
in vec2 vUv;

float sdCircle(vec2 p, float radius) {
  return length(p) - radius;
}

float sdSquare(vec2 p, float size) {
  vec2 d = abs(p) - size;
  return max(d.x, d.y);
}

//https://iquilezles.org/articles/distfunctions/
float opSmoothUnion(float d1, float d2, float k) {
  k *= 4.0;
  float h = max(k - abs(d1 - d2), 0.0);
  return min(d1, d2) - h * h * 0.25 / k;
}

//PI/4の時に最小最大値の高さ♾️なる
vec2 lemniscate(float t, float scale) {
  float x = cos(t);
  float y = sin(t) * cos(t) * 1.0;
  return vec2(x, y) * scale;
}

mat2 rotate2D(float angle) {
  float c = cos(angle), s = sin(angle);
  return mat2(c, -s, s, c);
}

float hash(float n) {
  return fract(sin(n) * 43758.5453123);
}

void main() {
  vec2 uv = vUv;
  uv.x *= uResolution.x / uResolution.y;

  float dist = 100.0;
  for (int i = 1; i <= 10; i++) {
    float fi = float(i);
    float angle = hash(fi) * 6.28;
    //[1.0, 2.0]の範囲
    // float speed = 1.0 + fi * 0.1;
    float speed = 1.0 + hash(fi);
    //[0.3, 0.5]
    float scale = 0.3 + fi * 0.02;
    float radius = 0.2;

    vec2 offset = lemniscate(uTime * speed, scale);
    offset *= rotate2D(angle);
    float ballDist = sdCircle(uv + offset, radius);

    dist = opSmoothUnion(dist, ballDist, 0.1);
  }

  vec3 color;
  if (dist < 0.0) {
    float r = 0.2;
    color = vec3(1.0 - (abs(dist) / r));
  }
  else {
    color = vec3(1.0, 1.0, 1.0);
  }
  gl_FragColor = vec4(color, 1.0);
}

    /*重なった領域は反転で+空間で優先され、その他はマイナス空間で比べる*/
    float opSubtraction(float d1, float d2) {
      return max(-d1, d2);
    }

    float opIntersection(float d1, float d2) {
      return max(d1, d2);
    }
    

2dのsdfとして円と正方形それらの基本的な演算としてunion, subtraction,intersectionについて学習をしました。 ここではopSmoothUnion()を借用してメタボール風のビジュアルをつくってみました。

工夫した点

    vec2 offset = lemniscate(uTime * speed, scale);
    offset *= rotate2D(angle);
    //uv+move座標での0の場所はどこにいるのかを確認
    float ballDist = sdCircle(uv + offset, radius);
    dist = opSmoothUnion(dist, ballDist, 0.1);
    

円のsdfの場合0の地点で最小値を返します。なのでオフセット座標とは逆の場所に0が出現し円が描画されます。 座標を動かして円を描くのですが、座標はレムニスケートの軌道です。時間を入力として極座標から直交座標変換で取得します。 それらは同一な座標をとるので各円に応じて回転をすることで重ならないようにしました。 unionを複数回かける際にAIでのリファクタリングでfor文がでてきました。 ここでの注意点としてフラグメントシェーダの基本に振り返り各ピクセルごとに変数が保持されることで理解を進めました。 各ピクセルは順次処理されていきsdfの返す距離の負空間を維持することで色付けが決定されます。 色付けに関しては負空間の最小値を最大値としてネガポジで色を決定しています。