C言語で線を描画する

線描画の解説ページのアイキャッチ

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

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

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

線の描画は「座標を移動させながら連続的に点を描画する」ことで実現しますので、特に点の描画については上のページで理解しておいていただけると本ページの内容も理解しやすくなると思います。

横線を描画する

まずは「横線」の描画を行います。点の描画ができれば横線の描画も簡単です。

横線を描画する考え方

横線は「横方向に点を連続して描画したもの」と考えることができます。

なので、横方向に向かって座標を移動しながら各座標に点を描画してやることで、横線を描画することができます。

横方向に座標を移動させるためには、縦方向の座標 y を固定し、横方向の座標 x のみを増加させることになります。例えば y = y1 と固定して引いた横線は下の図のようになります。

横線を描画するイメージ

この座標 x のみを増加させる処理は、x に対するループ処理で簡単に実現することが可能です。

xの増加
for(x = 0; x < width; x++){
  /* (x, y1) 座標に対する処理 */
}

スポンサーリンク

横線を描画する関数

ここで紹介する横線を描画する関数は下記になります。

横線を描画する関数
void drawHorizontalLine(
  unsigned char *data, /* ビットマップデータ */
  unsigned int width, /* ビットマップの横幅 */
  unsigned int height, /* ビットマップの高さ */
  unsigned int y1, /* 横線のy座標 */
  unsigned char r, /* 赤の輝度値 */
  unsigned char g, /* 青の輝度値 */
  unsigned char b /* 緑の輝度値 */
){
  unsigned int x;
  unsigned char *p;

  for(x = 0; x < width; x++){
    p = data + y1 * width * 3 + x * 3;
    p[0] = r;
    p[1] = g;
    p[2] = b;
  }
}

x0 から width - 1 まで増加させるループの中で点を描画しているだけですね。

縦方向を示す y 座標を固定し、横方向を示す座標である x0 から width - 1 まで変化させながら点の描画を行なっているので、横方向に連続して点が描画され、結果的に線が描画されることになります。

例えば y = 100 の横線を描画するためには、上記の drawHorizontalLine 関数を下記のように呼び出せば良いです。第4引数で横線の y 座標を指定しています。色は赤色に指定しています。

drawHorizontalLineの呼び出し
/* y = 100 の横線を描画 */
drawHorizontalLine(
  bitmap.data,
  bitmap.width,
  bitmap.height,
  100, /* 横線のy座標 */
  0xFF, 0x00, 0x00 /* 赤色 */
);
main 関数について

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

C言語で図形を描画する

また PI の定義と math.h のインクルードは、後半で紹介する斜め線の描画時に必要になります

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

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

横線の描画結果

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

横線を描画した結果

縦線を描画する

続いては「縦線」です。横線の描画とほぼ同じやり方で縦線の描画もできてしまいます。

スポンサーリンク

縦線を描画する考え方

横線は横方向に点を描画していくのに対し、縦線は「縦方向に点を描画する」ことになります。横線との違いはこれだけです。

縦線を描画するイメージ

縦線を描画する関数

縦線を描画する関数は下記の通りになります。

縦線を描画する関数
void drawVerticallLine(
  unsigned char *data, /* ビットマップデータ */
  unsigned int width, /* ビットマップの横幅 */
  unsigned int height, /* ビットマップの高さ */
  unsigned int x1, /* 縦線のx座標 */
  unsigned char r, /* 赤の輝度値 */
  unsigned char g, /* 青の輝度値 */
  unsigned char b /* 緑の輝度値 */
){
  unsigned int y;
  unsigned char *p;

  for(y = 0; y < height; y++){
    p = data + y * width * 3 + x1 * 3;
    p[0] = r;
    p[1] = g;
    p[2] = b;
  }
}

例えば x = 100 の縦線を描画するためには、上記の drawVerticalLine 関数を下記のように呼び出せば良いです。第4引数で縦線の x 座標を指定しています。色は赤色に指定しています。

drawVerticalLineの呼び出し
  /* x = 100 の縦線を描画 */
  drawVerticalLine(
    bitmap.data,
    bitmap.width,
    bitmap.height,
    100, /* 縦線のx座標 */
    0xFF, 0x00, 0x00 /* 赤色 */
  );

縦線の描画結果

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

縦線を描画した結果

スポンサーリンク

斜め線を描画する(長さと角度指定)

ここでは「斜め線」の描画の仕方について説明します。斜め線については2つのやり方を紹介します。まずは「長さと角度」を指定して斜め線の描画を行う方法について紹介します。

斜め線を描画する考え方(長さと角度指定)

ここで紹介するのは、長さ length と角度 angle、さらには始点の座標 (x1, y1) を指定して斜め線を描画する方法です。

斜め線を描画するイメージ

MEMO

角度については反時計方向ではなく、時計方向が正方向になるので気をつけてください

数学等で学んだ時とは逆方向だと思います

これは縦軸方向の正方向が下方向になっているためです。

斜め線は、「斜め線上の座標に点を描画する」ことで描画することができます。

なので、どの座標が「斜め線上に存在するのか」が分かれば斜め線の描画を実現することができます。

そこで、どの座標が「斜め線上に存在するのか」について考えてみましょう。これは三角関数を利用すれば簡単に求めることができます。

例えば、下図のように、始点座標を (x1, y1) 、x 軸と斜め線の成す角度を angle とした時の斜め線上に座標 (x, y) が存在する場合について考えてみましょう!

この座標 (x, y) と始点座標 (x1, y1) との距離は l とします。

斜め線を描画するときの各変数の関係

図を見ると分かるように、l を 0 から length – 1 まで増加させながら変化させ、それに対応した座標 (x, y) に点を描画していくことで斜め線を描画することが可能です。

で、この座標 (x, y) の xy は、距離 l と角度 angle を用いて下記の式で計算することができます(angleθ で表記してます)。

  • x = x1 + l * cos θ
  • y = y1 + l * sin θ

なので、l0 から length - 1 まで増加させるループの中で、その l に対応する (x, y) を上式で求め、その座標に点を描画することで斜め線が描画できます。

斜め線を描画する関数(長さと角度指定)

長さと角度を指定して斜め線を描画する関数は下記のようになります。

斜め線を描画する関数(長さと角度指定)
void drawLineAngleLength(
  unsigned char *data, /* ビットマップデータ */
  unsigned int width, /* ビットマップの横幅 */
  unsigned int height, /* ビットマップの高さ */
  unsigned int angle, /* 線の角度 */
  unsigned int length, /* 線の長さ */
  unsigned int x1, /* 線の始点のx座標 */
  unsigned int y1, /* 線の始点のy座標 */
  unsigned char r, /* 赤の輝度値 */
  unsigned char g, /* 青の輝度値 */
  unsigned char b /* 緑の輝度値 */
){
  unsigned int l;
  unsigned int x, y;
  unsigned char *p;
  double rad;

  rad = (double)angle / 180 * PI;
  
  /* 長さ分の線を描画 */
  for(l = 0; l < length; l++){
    /* x座標とy座標を計算 */
    x = x1 + l * cos(rad);
    y = y1 + l * sin(rad);

    /* ビットマップ外の点は描画しない */
    if((x >=0 && x < width) && (y >= 0 && y < height)){
      /* 座標に対するアドレスを取得 */
      p = data + y * width * 3 + x * 3;

      /* RGB値を格納 */
      p[0] = r;
      p[1] = g;
      p[2] = b;
    }
  }
}

注意点は cos 関数と sin 関数の引数に指定する値の単位がラジアンであることです。普通の角度(度数法による角度表記)ではないので注意です。

ラジアンでは 0° 〜 360° を 0 〜 2 π で表します(π は円周率)。

例えば 45° は 45 / 180  * π = 1/ 4 π 、180° は 180 / 180 * π = π といった感じで、ラジアンは角度を 180 で割った数字に π を掛けることで計算することが可能です。

上記ソースコードでは、角度からラジアンへの変換は下記で行なっています。

ラジアンへの変換
  rad = (double)angle / 180 * PI;

例えば始点を座標 (700, 300)・長さ 500 ・角度 210° の斜め線を描画をするためには下記のように drawLineAngleLength を呼び出しすれば良いです。第4引数で角度、第5引数で長さ、第6引数と第7引数で始点の座標を指定しています。指定している色は緑色になります。

drawLineAngleLengthの呼び出し
/* 始点を (700, 300) とした長さ 500 、角度 210° の線を描画 */
  drawLineAngleLength(
    bitmap.data,
    bitmap.width,
    bitmap.height,
    210,
    500,
    700, 300,
    0x00, 0xFF, 0x00
  );

スポンサーリンク

斜め線の描画結果(長さと角度指定)

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

長さと角度を指定したときの斜め線の描画結果

斜め線を描画する(2点指定)

次は2つの点を指定して「斜め線」を描画する方法を紹介します。

斜め線を描画する考え方(2点指定)

では、下の図のように始点 (x1, y1) と終点 (x2, y2) が指定された時に、この2点間に線を描画する方法について考えていきたいと思います。

点を二つ指定して線を描画するイメージ

結論的には、この2点間の線の描画は、斜め線を描画する(長さと角度指定)と同じ考え方で行うことができます。

これは、2つの点の座標から「2点間の距離」と「2つの点を結ぶ線が x 軸と成す角度(ラジアン)」を算出することが可能だからです。

2つの点から角度と長さを求めるイメージ

ですので、与えられた2つの点から「2点間の距離」と「2つの点を結ぶ線が x 軸と成す角度(ラジアン)」を求めてやれば、結局は斜め線を描画する(長さと角度指定)と同様にして線を描画することが可能です。

  • x = x1 + l * cos θ
  • y = y1 + l * sin θ

具体的には、距離とラジアンの計算は下記により行うことが可能です。

dx = x2 - x1
dy = y2 - y1
距離 = sqrt(dx * dx + dy * dy)
ラジアン = atan2(dy, dx)

dxdy は終点から始点の xy の値を引いたものになります(負の値の場合もあり得る)。

この dxdy を用いれば、2点間の距離をピタゴラスの定理によって計算できます(sqrt 関数は指定された数の平方根を求める関数で、ピタゴラスの定理で距離を求めるために使用しています)。

ラジアンに関しては atan2 関数により算出することできます。

atan2 は引数で指定された “y 方向の距離” と “x 方向の距離” からラジアンを計算して返却してくれる関数です。tan 関数の逆関数的な関数です。

atan2 関数ではなく atan 関数でもラジアンを取得することはできるのですが、atan 関数の場合、返却値は 0 から π の間の値のみです。

下の図のように終点よりも始点の座標の方が大きい場合は方向が逆になってしまい(つまり角度が180度分おかしい)、うまく線が描画できません。

要は atan 関数では始点と終点の位置関係を考慮してくれません(atan 関数の引数は1つなので位置関係を考慮することができない)。

角度産出がおかしく線がうまく描けない様子

一方で、atan2 関数は始点と終点の位置関係を考慮して(つまり引数で指定する2つの距離の正負を考慮して)ラジアンを求めてくれます。そして、0 から 2 * π の間の値を返却してくれます(つまり360度全方向)。

今回も始点と終点の位置関係を考慮して角度を求める必要があるため、atan2 を用いてラジアンを算出します。

スポンサーリンク

斜め線を描画する関数(2点指定)

始点と終点を指定して線を描画する関数は下記のようになります。

斜め線を描画する関数(2点指定)
void drawLineTwoPixels(
  unsigned char *data, /* ビットマップデータ */
  unsigned int width, /* ビットマップの横幅 */
  unsigned int height, /* ビットマップの高さ */
  unsigned int xs, /* 線の始点のx座標 */
  unsigned int ys, /* 線の始点のy座標 */
  unsigned int xe, /* 線の終点のx座標 */
  unsigned int ye, /* 線の終点のy座標 */
  unsigned char r, /* 赤の輝度値 */
  unsigned char g, /* 青の輝度値 */
  unsigned char b /* 緑の輝度値 */
){
  unsigned int x, y;
  unsigned char *p;
  int dx, dy;
  double rad;
  unsigned int length;
  unsigned int l;
  int sigx, sigy;

  /* 始点と終点のx座標とy座標の差を計算 */
  dx = xe - xs;
  dy = ye - ys;

  /* 線の長さを計算 */
  length = sqrt(dx * dx + dy * dy);

  /* 横軸との成す角を計算 */
  rad = atan2(dy, dx);

  /* 長さ分の線を描画 */
  for(l = 0; l < length; l++){
    /* x座標とy座標を計算 */
    x = xs + l * cos(rad);
    y = ys + l * sin(rad);

    /* ビットマップ外の点は描画しない */
    if((x >=0 && x < width) && (y >= 0 && y < height)){
      /* 座標に対するアドレスを取得 */
      p = data + y * width * 3 + x * 3;

      /* RGB値を格納 */
      p[0] = r;
      p[1] = g;
      p[2] = b;
    }
  }
}

例えば始点を座標 (700, 300) 、終点を座標 (100, 100) とした線を描画をするためには下記のように drawLineTwoPixels を呼び出しすれば良いです。第4引数と第5引数で始点、第6引数と第7引数で終点の座標を指定しています。指定している色は緑色になります。

drawLineTwoPixelesの呼び出し
  drawLineTwoPixels(
    bitmap.data,
    bitmap.width,
    bitmap.height,
    700, 300,
    100, 100,
    0x00, 0xFF, 0x00
  );

斜め線の描画結果(2点指定)

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

点を2つ指定して描画した線の結果

まとめ

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

線が描画できると、これを利用していろいろな図形を描画することができます。

一例として多角形を描画する方法を下のページで解説していますので、興味のある方は是非読んでみてください。

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

2 COMMENTS

daeu

やまさん

コメントありがとうございます!

まさに書きかけの状態で記事を公開してしまっていました…。
解説も不十分でしたし、ソースコードも途中で終わっていましたし、結果画像もアップ忘れていました…。
大変申し訳ございません。

先程記事を完成させて公開し直しました。ご指摘大変ありがとうございました。

で、よく考えたら atan 関数ではなく atan2 関数で簡単にラジアンを算出できることに気づいたので、atan2 を使用した方法での解説に変更しています。この点はご容赦ください。

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です