C言語で円と楕円を描画する

円と楕円描画の解説ページのアイキャッチ

このページではC言語で「円」や「楕円」を描画する方法について解説します。

MEMO

扇形の描画についても追記しました(2021/11/20)

円を描画するために必要な「ビットマップデータ」や「点の描画方法」については下のページで解説していますので、まだ読んでない方は事前に下のページを読むことをオススメします。

図形描画の解説ページのアイキャッチC言語で図形を描画する

円の描画も結局は線の描画同様に「点」の描画を円の形になるよう座標を制御してに実行するだけです。ですので、特に点の描画については上のページで理解しておいていただけると本ページの内容も理解しやすくなると思います。

円を描画する

まずは「円」を描画します。点の描画さえできれば、円の方程式を使えば簡単に描画可能です!

円を描画する考え方

「円内の座標に点を描画する」のが円の描画の考え方です。

つまり、円の原点からの距離が円の半径以内である座標に点を描画していきます。では具体的にその座標はどの座標になるのでしょうか?ここについて解説していきます。

ある座標 (x, y) が座標 (x1, y1) を中心とした半径 radius の円上に存在するかどうかは、下記の式で判断することが可能です。習ったことがある方なら懐かしさを感じるかもしれません。円の方程式というやつです。

(x - x1) * (x - x1) + (y - y1) * (y - y1) = radius * radius

要は、左辺では座標 (x, y) と円の中心座標 (x1, y1) との距離(の2乗)を計算しています。そして、その距離が半径 radius と一致する(つまり円の方程式が成り立つ)ということは、座標 (x, y) は中心座標 (x1, y1)・半径 radius円上に存在するということになります。

円の方程式の説明図1

また、その距離が半径よりも小さい場合は、その座標 (x, y) は円の内側に存在すると言えます。

円の方程式の説明図2

つまり、ビットマップデータ上の全座標 (x, y) に対して下の式が成立するかどうかを確認し、成立する場合にその座標に点を描画することを繰り返せば、内側に色を塗った円を描画することができます。

(x - x1) * (x - x1) + (y - y1) * (y - y1) <= radius * radius

スポンサーリンク

円を描画する関数

円を描画する関数は下記のようになります。

円を描画する関数

void drawCircle(
  unsigned char *data, /* ビットマップデータ */
  unsigned int width, /* ビットマップの横幅 */
  unsigned int height, /* ビットマップの高さ */
  unsigned int radius, /* 円の半径 */
  unsigned int x1, /* 始点のx座標 */
  unsigned int y1, /* 始点のy座標 */
  unsigned char r, /* 赤の輝度値 */
  unsigned char g, /* 青の輝度値 */
  unsigned char b /* 緑の輝度値 */
){
  unsigned int x, y;
  int dx, dy;
  unsigned char *p;
  
  for(y = 0; y < height; y++){
    for(x = 0; x < width; x++){
      /* 始点と(x, y)座標の距離を計算 */
      dx = (int)x - (int)x1;
      dy = (int)y - (int)y1;

      /* 円を描画 */
      if((dx * dx) + (dy * dy) <= radius * radius){
        p = data + y * width * 3 + x * 3;
        p[0] = r;
        p[1] = g;
        p[2] = b;
      }
    }
  }
}

例えば、中心座標を (500, 300) とした半径 100 の円を描画するためには下記のように drawCircle 関数を実行します。第4引数で半径を、第5引数と第6引数で中心座標を指定しています。

drawCircleの呼び出し

  drawCircle(
    bitmap.data,
    bitmap.width,
    bitmap.height,
    100,
    500, 300,
    0xFF, 0x00, 0x00
  );

main 関数について

上記の drawCircle 関数のように、このページに載せている関数は、下記ページで紹介した main 関数から呼び出すのがオススメです

C言語で図形を描画する

libpng をインストールすれば PNG 出力して画像のプレビューも簡単にできます

このページで紹介する描画結果も上記の main 関数で出力した output.png をプレビューしたものになります

円の描画結果

上記のように drawCircle を呼び出した際に出力される output.png は下のようになります。

円の描画結果

楕円を描画する

次は「楕円」を描画します。こちらも楕円の方程式を用いれば描画できます。

スポンサーリンク

楕円を描画する考え方

楕円の描画の考え方も、基本的には円の描画と同じです。要は、楕円の内側に存在する座標に点を描画します。

下図のような、座標 (0, 0) を中心とし、x 軸上の楕円の線までの距離を rxy 軸上の楕円の線までの距離を ry とした楕円について考えてみましょう(この距離 rxry は便宜上 x 軸上の半径と y 軸上の半径と呼ばせていただきます)。

原点を中心とした時の楕円の説明図

この時、下記式を満たす座標 (x, y) は、楕円上に存在することになります(楕円の方程式)。

(x * x) / (rx * rx) + (y * y) / (ry * ry) = 1

さらに、楕円の中心を下の図のように (x1, y1) に移動させてみましょう。

原点を座標(x1,y1)を中心とした時の楕円の説明図

この時、下記式を満たす座標 (x, y) は、中心を座標 (x1, y1) とした楕円上に存在することになります。

(x - x1) * (x - x1) / (rx * rx) + (y - y1) * (y - y1) / (ry * ry) = 1

さらに、下記式を満たす座標 (x, y)) は中心を座標 (x1, y1) とした楕円内に存在することになります。

(x - x1) * (x - x1) / (rx * rx) + (y - y1) * (y - y1) / (ry * ry) <= 1

つまり、中心を座標 (x1, y1)、x 軸方向の半径を rxy 軸方向の半径を ry とした楕円の描画(内側は塗りつぶし)は、ビットマップデータ上の全座標 (x, y) に対して上の式が成立するかどうかを確認し、成立する場合にその座標に点を描画していくことで描画できることになります。

楕円を描画する関数

楕円を描画する関数は下記のようになります。

楕円を描画する関数

void drawEllipse(
  unsigned char *data, /* ビットマップデータ */
  unsigned int width, /* ビットマップの横幅 */
  unsigned int height, /* ビットマップの高さ */
  unsigned int rx, /* x軸状の楕円の半径 */
  unsigned int ry, /* y軸状の楕円の半径 */
  unsigned int x1, /* 始点のx座標 */
  unsigned int y1, /* 始点のy座標 */
  unsigned char r, /* 赤の輝度値 */
  unsigned char g, /* 青の輝度値 */
  unsigned char b /* 緑の輝度値 */
){
  unsigned int x, y;
  int dx, dy;
  unsigned char *p;
  
  for(y = 0; y < height; y++){
    for(x = 0; x < width; x++){
      /* 始点と(x, y)座標の距離を計算 */
      dx = (int)x - (int)x1;
      dy = (int)y - (int)y1;

      /* 楕円を描画 */
      if((double)(dx * dx) / (double)(rx * rx) + (double)(dy * dy) / (double)(ry * ry) <= 1){
        p = data + y * width * 3 + x * 3;
        p[0] = r;
        p[1] = g;
        p[2] = b;
      }
    }
  }
}

例えば、中心座標を (500, 250) 、x 軸上の半径を 200y 軸上の半径を 100 の楕円を描画するためには下記のように drawEllipse 関数を実行します。第4引数と第5引数で x 軸上の半径と y 軸上の半径を、第5引数と第6引数で楕円の中心座標を指定しています。

drawEllipseの呼び出し

  drawEllipse(
    bitmap.data,
    bitmap.width,
    bitmap.height,
    200, 100,
    500, 250,
    0xFF, 0x00, 0x00
  );

楕円の描画結果

上記のように drawEllipse 関数を呼び出した時に出力される output.png は下のようになります。

楕円の描画結果

スポンサーリンク

扇形を描画する

最後は「扇形」を描画していきたいと思います。扇形は 円を描画する で紹介した円の描画の考え方を応用することで描画することができます。

扇形を描画する考え方

円の描画は円内に存在する座標に対して点を描画をすることで実現できました。

それに対して扇形の描画は、円内かつ、円の中心から伸びる2つの直線の間に存在する座標に対して点を描画することで実現できます。

扇形を描画する際の考え方

なので、扇形を描画するためには、座標 (x, y) がこの2つの直線の間に存在するのかどうかを判断しながら処理を行なっていく必要があります。

ちょっと難しそうではありますが、これは角度を用いれば簡単に判断できます。

まず、座標 (x, y) から円の中心を結ぶ直線と x 軸とのなす角の角度を angle、さらに2つの直線が x 軸とのなす角の角度をそれぞれ startend とします(なす角は全て “正方向になす角” です)。

座標が2つの直線の間に存在するかどうかを判断する様子

y 軸の正方向が下方向のため、角度の正方向が時計回りになっている点に注意が必要です(数学で扱う座標とは縦軸の向きが反対)。

このように角度で表せば、この3つの角度の大小関係の比較により、座標 (x, y) がこの2つの直線の間に存在するかどうかが判断できます。

すなわち、下記を満たす場合、座標 (x, y) は2つの直線の間に存在すると判断できます。

start <= angle <= end

さらに、座標 (x, y) から円の中心を結ぶ直線と x 軸とのなす角の角度に関しては、下記ページで紹介している atan2 関数を利用することで簡単に求めることができます。

【C言語】atan関数とatan2関数について解説(傾きor座標から角度を求める関数)

なので、扇形は下記のような手順で描画することができることになります。

まず引数で2つの直線の角度 startend を受け取り、さらに atan2 関数を利用して座標 (x, y) に対する角度 angle を求めます。

座標が2つの直線の間に存在するかどうかを判断する様子

続いて、3つの角度の大小関係を比較し、座標 (x, y) が2つの直線に存在するのかどうかを判断します。もし存在しないのであればその座標に点を描画する必要はありません。

存在する場合、さらに円内にその座標 (x, y) が存在するかどうかを判断し、存在する場合のみ、座標 (x, y) に点を描画します。

これらを全ての座標に対して実行すれば、扇形を描画することができます。

ただ、扇形を描画する際には何点かの注意点があります。

まず atan2 関数の返却値の角度の単位はラジアンです。ですので、引数で受け取る角度の単位が “度” の場合、単位を一方に統一してから比較を行う必要があります。この単位を統一するためには、下記ページで解説している「ラジアン ⇔ 度」の単位変換が必要になります。

度とラジアンの単位変換の方法の解説ページアイキャッチ度(度数法)⇔ラジアン(弧度法)の変換

また、atan2 関数の返却値は π です。引数で受け取る角度が正の値のみである場合などは 0 で扱えるよう、返却値が負の値の場合は +2π した方が両方の角度の比較がしやすいと思います。

さらに、前述の通り下記の判断式で角度の大小関係を比較するのですが、引数で受け取る start よりも end の方が小さい場合、そのまま下記式で判断すると上手く判定できません。必ず不成立することになります。

start <= angle <= end

このような場合は end に対して +360  or +2π をして角度の大小関係が start <= end を満たすようにしてから判断を行う必要があります。で、これを行うと end の角度が 360 or 以上になるので、atan2 関数の返却値もこの辺りも考慮しつつ判断を行う必要があることになります(atan2 関数の返却値に  を足してから比較するなど)。

扇形を描画する関数

扇形を描画する関数は下記のようになります。atan2 を利用しているので math.h のインクルードが必要です。

ここまでの説明で用いてきた angle が、下記関数で使用している変数 prad に相当します(単位はラジアン)。

扇形を描画する関数

void drawSector(
  unsigned char *data, /* ビットマップデータ */
  unsigned int width, /* ビットマップの横幅 */
  unsigned int height, /* ビットマップの高さ */
  unsigned int radius, /* 円の半径 */
  unsigned int x1, /* 始点のx座標 */
  unsigned int y1, /* 始点のy座標 */
  unsigned int start, /* 扇形の開始角度(単位は度) */
  unsigned int end, /* 扇形の終了角度(単位は度) */
  unsigned char r, /* 赤の輝度値 */
  unsigned char g, /* 青の輝度値 */
  unsigned char b /* 緑の輝度値 */
){
  unsigned int x, y;
  int dx, dy;
  unsigned char *p;
  double srad, erad, prad;
  
  /* 引数の角度を0〜359の値に丸め、さらにラジアンに変換 */
  srad = PI / 180 * (start % 360);
  erad = PI / 180 * (end % 360);

  /* 必ずsrad<=eradが成立するようにする */
  if (srad > erad) {
    erad += 2 * PI;
  }

  for(y = 0; y < height; y++){
    for(x = 0; x < width; x++){

      /* 円の中心と(x, y)座標の差を計算 */
      dx = (int)x - (int)x1;
      dy = (int)y - (int)y1;

      /* 円の中心と(x,y)座標を結ぶ直線の角度を取得 */
      prad = atan2(dy, dx);

      /* 0〜2πの値に丸める */
      if (prad < 0) {
        prad += 2 * PI;
      }

      /* 角度sradと角度pradの直線の間に存在するかどうかを判断 */
      if (prad < srad || prad > erad) {
        /* srad<=prad<=eradを満たさなかった場合 */
        if (erad >= 2 * PI) {
          /* eradが2π以上の場合はpradも2π足した状態でも判断する */
          if (prad + 2 * PI < srad || prad + 2 * PI > erad) {
            /* 2つの直線の間に存在しないので次の座標の描画に移る */
            continue;
          }
        } else {
          /* 2つの直線の間に存在しないので次の座標の描画に移る */
          continue;
        }
      }

      /* 座標(x,y)が半径radiusの円内に存在するかどうかを判断 */
      if((dx * dx) + (dy * dy) <= radius * radius){
        /* 座標(x,y)は円内に存在する&2つの直線の間に存在するので点を描画 */
        p = data + y * width * 3 + x * 3;
        p[0] = r;
        p[1] = g;
        p[2] = b;
      }
    }
  }
}

例えば、中心座標を (500, 300)、半径 100、扇形の開始角度 180 度、終了角度 45 度の扇形を描画するためには下記のように drawSector 関数を実行します。第4引数で半径を、第5引数と第6引数で中心座標を、第7引数と第8引数で開始角度と終了角度をそれぞれ指定しています。

drawSectorの呼び出し

  drawSector(
    bitmap.data,
    bitmap.width,
    bitmap.height,
    100,
    500, 300,
    180, 45,
    0xFF, 0x00, 0x00
  );

スポンサーリンク

扇形の描画結果

上記のように drawSector 関数を呼び出した時に出力される output.png は下のようになります。

扇形の描画結果

角度の正方向が数学等で扱う座標と逆で気持ち悪いという場合は、drawSector 関数の atan2 関数の戻り値に - を付加してやれば良いです。

角度の正方向を逆にする

prad = -atan2(dy, dx);

この場合、上記の引数で drawSector 関数を実行した時に出力される output.png は下のようになります。

角度の正方向を逆方向にした場合の扇形の描画結果

まとめ

このページでは円と楕円を描画する方法の解説と円を描画する関数、楕円を描画する関数の紹介を行いました。

昔ならった無理やり覚えた円の方程式や楕円の方程式も、プログラミングで実際に円や楕円を描画してみると、楽しみながら方程式を理解できたのではないかと思います。

図形を描画したりするためには数学的要素が必要だったりしますが、プログラミングで実際に図形を描画するとすぐに結果が確認できて楽しいですし、数学についての理解も進んでオススメですよ!

同じカテゴリのページ一覧を表示