情報学入門(データ演習) / 第 15 章

コンピュータグラフィックス

計算した結果を、目に見える形にする

この章のねらい

この章の描画について。 元資料は Excel の図形(オートシェイプ)を使う GSolidLine という 手続きを用意して線を描いています。 この教材の処理系は図形オブジェクトを扱えないので、 同じ名前・同じ引数の GSolidLine x1, y1, x2, y2 を用意し、 右側の描画面に線を引くようにしました。 プログラムの書き方は元資料と同じです。 本物の Excel で動かすには GSolidLine の中身 (ActiveSheet.Shapes.AddLine を使うもの)が別途必要です。

1. セルを画素として使う

いちばん手軽な描画は、セルに色を塗ることです(第 1 章 §6)。 セルを小さな正方形=画素だと思えば、絵が描けます。

Sub Macro1()
    Dim x As Integer, y As Integer
    Dim cx As Integer, cy As Integer
    Dim r As Double, d As Double

    cx = 11: cy = 11        ' 中心
    r = 9                   ' 半径

    For y = 1 To 21
        For x = 1 To 21
            d = Sqr((x - cx) ^ 2 + (y - cy) ^ 2)
            If d <= r Then
                ' 中心からの距離で色を変える(濃淡をつける)
                Cells(y, x).Interior.Color = RGB(255 - d * 20, 190 - d * 10, 60)
            End If
        Next x
    Next y
End Sub

「円の中かどうか」を距離で判定しています。 第 3 章の 2 点間の距離の式がそのまま使えます。 If d <= r ThenIf Abs(d - r) < 0.6 Then に変えると 輪郭だけになります。試してみてください。
なお Sqr を使わずに (x-cx)^2 + (y-cy)^2 <= r^2 と 書くほうが速いのは、第 8 章の素因数分解で d * d <= n と書いたのと同じ理由です。

2. 線分を描く

GSolidLine(ByVal x As Double, ByVal y As Double, ByVal xx As Double, ByVal yy As Double)

点 (x, y) から点 (xx, yy) へ線分を引く(元資料 195・200 の 4 枚目)。

座標の向きに注意してください。 数学のグラフとは違い、y は下向きです(画面の座標系)。 元資料の図もそうなっています。

x y (0,0) (x, y) (xx, yy)
Sub Macro2()
    ' 壁(四角)
    GSolidLine 140, 180, 340, 180
    GSolidLine 340, 180, 340, 300
    GSolidLine 340, 300, 140, 300
    GSolidLine 140, 300, 140, 180

    ' 屋根(三角)
    GSolidLine 140, 180, 240, 100
    GSolidLine 240, 100, 340, 180

    ' ドア
    GColor 240, 131, 0
    GSolidLine 210, 300, 210, 240
    GSolidLine 210, 240, 270, 240
    GSolidLine 270, 240, 270, 300

    ' 窓
    GColor 0, 176, 80
    GSolidLine 165, 200, 205, 200
    GSolidLine 205, 200, 205, 230
    GSolidLine 205, 230, 165, 230
    GSolidLine 165, 230, 165, 200
End Sub

この教材で使える描画の命令(元資料の GSolidLine に加えて用意したもの)

命令働き
GSolidLine x1, y1, x2, y2線分を引く(元資料と同じ)
GColor r, g, bこれから引く線の色を決める(0〜255)
GWidth w線の太さ
GCircle cx, cy, r円(内部では 48 本の線分)
GClear描いたものを消す

3. 2 次元の回転(元資料 195・200 の 7 枚目〜)

2 次元座標平面上の点の座標 (x1, y1) を α ラジアン回転した座標を (x2, y2) とすると、

x2 = x1 * Cos(α) − y1 * Sin(α)
y2 = x1 * Sin(α) + y1 * Cos(α)

回転の中心は原点 (0, 0) です。 だから「画面の真ん中で回したい」ときは、 いったん原点に寄せて → 回して → 元の位置へ戻すという 3 段が必要です。 プログラムでは GSolidLine 240 + x, 180 + y, … のように 描くときに中心の分を足すやり方が簡単です。
また SinCos の引数はラジアンです (度ではありません)。360 度 = 2π ラジアンなので、 度から直すには 度 * pi / 180 とします。

Sub Macro3()
    Dim i As Integer
    Dim n As Integer
    Dim pi As Double, al As Double
    Dim x1 As Double, y1 As Double
    Dim x2 As Double, y2 As Double

    pi = Application.WorksheetFunction.Pi
    n = 12                       ' 何角形にするか

    x1 = 140: y1 = 0             ' 最初の点(原点から右へ 140)
    al = 2 * pi / n              ' 1 回に回す角度

    For i = 1 To n
        x2 = x1 * Cos(al) - y1 * Sin(al)
        y2 = x1 * Sin(al) + y1 * Cos(al)
        GSolidLine 240 + x1, 180 + y1, 240 + x2, 180 + y2
        x1 = x2                  ' 次はいまの点から
        y1 = y2
    Next i
End Sub

n を大きくすると円に近づきます(36 で見分けがつかなくなります)。 これは GCircle が内部でやっていることと同じです ── 曲線は、細かい線分の集まりで描くのがコンピュータグラフィックスの基本です。

Sub Macro4()
    Dim i As Integer, k As Integer
    Dim pi As Double, al As Double
    Dim x(4) As Double, y(4) As Double
    Dim nx As Double, ny As Double
    Dim x0(4) As Double, y0(4) As Double

    pi = Application.WorksheetFunction.Pi

    ' もとの図形(長方形)を原点まわりに置く
    x0(1) = -120: y0(1) = -40
    x0(2) =  120: y0(2) = -40
    x0(3) =  120: y0(3) =  40
    x0(4) = -120: y0(4) =  40

    ' 15 度ずつ、6 回描く
    For k = 0 To 5
        al = k * 15 * pi / 180
        GColor 30 + k * 40, 63, 200 - k * 30

        For i = 1 To 4
            x(i) = x0(i) * Cos(al) - y0(i) * Sin(al)
            y(i) = x0(i) * Sin(al) + y0(i) * Cos(al)
        Next i

        For i = 1 To 4
            nx = x(i Mod 4 + 1)
            ny = y(i Mod 4 + 1)
            GSolidLine 240 + x(i), 180 + y(i), 240 + nx, 180 + ny
        Next i
    Next k
End Sub

i Mod 4 + 1 で「次の頂点」を求めています。 i が 1,2,3 のときは 2,3,4 ですが、i が 4 のときは 1 になります (4 Mod 4 = 0、0 + 1 = 1)。 これで最後の頂点から最初の頂点へ戻る線が自動的に引けます。 Mod を「輪をつくる」ために使う ── よく出てくる書き方です。

4. 3 次元の回転 ── 回転体(元資料 197・205)

3 次元では、回す軸が 3 本あります。元資料はα・β・γ の 3 段に分けて、 順に回していく形で書いています。式は 2 次元のものを、それぞれの平面に当てはめたものです。

' ① x 軸まわりに α 回す(y と z が動く)
y2 = y1 * Cos(a) − z1 * Sin(a)
z2 = y1 * Sin(a) + z1 * Cos(a)

' ② y 軸まわりに β 回す(z と x が動く)
z3 = z2 * Cos(b) − x1 * Sin(b)
x3 = z2 * Sin(b) + x1 * Cos(b)

' ③ z 軸まわりに γ 回す(x と y が動く)
x4 = x3 * Cos(g) − y2 * Sin(g)
y4 = x3 * Sin(g) + y2 * Cos(g)

3 次元の点を画面に描くには、平らにしなければなりません。 いちばん簡単なのは z を捨てて (x, y) だけ使うことです (正投影といいます)。奥行きの感じは弱くなりますが、回転させれば立体だと分かります。
そして第 4 章の結合法則を思い出してください ── 3 つの回転をあらかじめ 1 つの行列にまとめておけば、 点ごとに 3 回計算しなくて済みます。これが 3D グラフィックスで行列を使う理由です。

Sub Macro5()
    Dim vx(8) As Double, vy(8) As Double, vz(8) As Double
    Dim px(8) As Double, py(8) As Double
    Dim i As Integer, j As Integer
    Dim a As Double, b As Double, t As Double

    ' 立方体の 8 頂点(原点が中心)
    vx(1) = -70: vy(1) = -70: vz(1) = -70
    vx(2) =  70: vy(2) = -70: vz(2) = -70
    vx(3) =  70: vy(3) =  70: vz(3) = -70
    vx(4) = -70: vy(4) =  70: vz(4) = -70
    vx(5) = -70: vy(5) = -70: vz(5) =  70
    vx(6) =  70: vy(6) = -70: vz(6) =  70
    vx(7) =  70: vy(7) =  70: vz(7) =  70
    vx(8) = -70: vy(8) =  70: vz(8) =  70

    a = 0.6                      ' x 軸まわり(ラジアン)
    b = 0.5                      ' y 軸まわり

    For i = 1 To 8
        ' ① x 軸まわりに a 回す
        t = vy(i) * Cos(a) - vz(i) * Sin(a)
        vz(i) = vy(i) * Sin(a) + vz(i) * Cos(a)
        vy(i) = t
        ' ② y 軸まわりに b 回す
        t = vx(i) * Cos(b) + vz(i) * Sin(b)
        vz(i) = -vx(i) * Sin(b) + vz(i) * Cos(b)
        vx(i) = t
        ' ③ z を捨てて画面へ(中心を 240,180 に)
        px(i) = 240 + vx(i)
        py(i) = 180 + vy(i)
    Next i

    ' 12 本の辺を引く
    For i = 1 To 4
        j = i Mod 4 + 1
        GColor 0, 63, 140
        GSolidLine px(i), py(i), px(j), py(j)             ' 手前の面
        GColor 130, 160, 200
        GSolidLine px(i + 4), py(i + 4), px(j + 4), py(j + 4)  ' 奥の面
        GColor 240, 131, 0
        GSolidLine px(i), py(i), px(i + 4), py(i + 4)     ' つなぐ辺
    Next i
End Sub

5. フラクタル ── 再帰で描く(元資料 197・205)

第 14 章の再帰が、ここで効いてきます。 「図形の一部が、全体と同じ形になっている」図形をフラクタルといい、 再帰でそのまま書けます。

5.1 コッホ曲線

作り方。1 本の線分を 3 等分し、 真ん中の 1 本を山形(正三角形の 2 辺)に置きかえます。 できた 4 本それぞれに、同じことを繰り返します。
段数を n とすると線分は 4n、 長さは (4/3)nになります ── 段数を無限にすると長さが無限になるのに、囲む面積は有限という不思議な図形です。

Sub Macro6()
    ' n = 0 なら 1 本の線。1 で 4 本、2 で 16 本、5 で 1024 本
    Call koch(30, 280, 450, 280, 4)
End Sub

Sub koch(ByVal x1 As Double, ByVal y1 As Double, _
         ByVal x2 As Double, ByVal y2 As Double, ByVal n As Integer)
    Dim ax As Double, ay As Double
    Dim bx As Double, by As Double
    Dim cx As Double, cy As Double
    Dim h As Double

    If n = 0 Then                     ' 終了条件(第 14 章)
        GSolidLine x1, y1, x2, y2
        Exit Sub
    End If

    ' 3 等分の点
    ax = x1 + (x2 - x1) / 3
    ay = y1 + (y2 - y1) / 3
    bx = x1 + (x2 - x1) * 2 / 3
    by = y1 + (y2 - y1) * 2 / 3

    ' 山の頂点(中点から垂直に √3/6 だけ上へ)
    h = Sqr(3) / 6
    cx = (x1 + x2) / 2 + (y2 - y1) * h
    cy = (y1 + y2) / 2 - (x2 - x1) * h

    Call koch(x1, y1, ax, ay, n - 1)   ' 4 つの小さい同じ問題
    Call koch(ax, ay, cx, cy, n - 1)
    Call koch(cx, cy, bx, by, n - 1)
    Call koch(bx, by, x2, y2, n - 1)
End Sub

5.2 ドラゴン曲線

作り方。1 本の線分を、 直角二等辺三角形の 2 辺に置きかえます。 ただし折る向きを左右交互にするところが要点で、 これだけで驚くほど複雑な形が現れます。 段数 n で線分は 2nです。

Sub Macro7()
    Call dragon(180, 200, 330, 200, 12, 1)
End Sub

Sub dragon(ByVal x1 As Double, ByVal y1 As Double, _
           ByVal x2 As Double, ByVal y2 As Double, _
           ByVal n As Integer, ByVal s As Integer)
    Dim mx As Double, my As Double

    If n = 0 Then
        GSolidLine x1, y1, x2, y2
        Exit Sub
    End If

    ' 中点を、線分に垂直な向きへ半分だけずらす(s で左右が決まる)
    mx = (x1 + x2) / 2 + s * (y2 - y1) / 2
    my = (y1 + y2) / 2 - s * (x2 - x1) / 2

    Call dragon(x1, y1, mx, my, n - 1, 1)      ' 前半は左へ折る
    Call dragon(mx, my, x2, y2, n - 1, -1)     ' 後半は右へ折る
End Sub

プログラムは 10 行ほどしかありません。 それでこの複雑さが出るのが、再帰の力です。 「複雑な形」と「複雑なプログラム」は別のことだと分かります ── 自然界の形(海岸線、雪の結晶、木の枝分かれ、シダの葉)が 簡単な規則の繰り返しで説明できるのと同じです。
n を 1 から順に上げていくと、形が育っていくのが見えます。 14 にすると 16384 本になり、少し時間がかかります。

6. 並べ替え 3 種(元資料 195・197・205 の「ソート」)

第 12 章でバブルソートを書きました。元資料はさらに挿入法選択法を扱っています。 3 つとも同じ答えを出しますが、やり方が違います。

方法考え方たとえると
バブルソート 隣どうしを比べて、逆なら交換。何周もする 泡が浮き上がるように、大きい値が端へ寄る
挿入法
(挿入ソート)
左から 1 枚ずつ取り、すでに並んだ列の正しい位置に差し込む トランプを手札に並べていくときのやり方
選択法
(選択ソート)
残りの中からいちばん小さいものを選んで先頭と交換 背の順に並ぶとき、いちばん小さい人を先に決める
Dim hikaku As Long
Dim koukan As Long

Sub Macro8()
    Dim a(1 To 8) As Integer
    Dim i As Integer
    Dim n As Integer

    n = 8

    ' ① バブルソート
    Call yomu(a, n)
    hikaku = 0: koukan = 0
    Call bubble(a, n)
    Call kaku(a, n, 4, "バブル")

    ' ② 挿入法
    Call yomu(a, n)
    hikaku = 0: koukan = 0
    Call insertion(a, n)
    Call kaku(a, n, 7, "挿入法")

    ' ③ 選択法
    Call yomu(a, n)
    hikaku = 0: koukan = 0
    Call selection(a, n)
    Call kaku(a, n, 10, "選択法")
End Sub

Sub yomu(ByRef a() As Integer, ByVal n As Integer)
    Dim i As Integer
    For i = 1 To n
        a(i) = Cells(2, i).Value
    Next i
End Sub

Sub kaku(ByRef a() As Integer, ByVal n As Integer, ByVal gy As Integer, ByVal nm As String)
    Dim i As Integer
    Cells(gy, 1) = nm
    For i = 1 To n
        Cells(gy + 1, i) = a(i)
    Next i
    Cells(gy, 4) = "比較 " & hikaku & " 回"
    Cells(gy, 6) = "交換 " & koukan & " 回"
End Sub

Sub bubble(ByRef a() As Integer, ByVal n As Integer)
    Dim i As Integer, j As Integer, t As Integer
    For i = 1 To n - 1
        For j = 1 To n - i
            hikaku = hikaku + 1
            If a(j) > a(j + 1) Then
                t = a(j): a(j) = a(j + 1): a(j + 1) = t
                koukan = koukan + 1
            End If
        Next j
    Next i
End Sub

Sub insertion(ByRef a() As Integer, ByVal n As Integer)
    Dim i As Integer, j As Integer, x As Integer
    For i = 2 To n
        x = a(i)                       ' 手に取る 1 枚
        j = i - 1
        Do While j >= 1
            hikaku = hikaku + 1
            If a(j) <= x Then Exit Do  ' ここに差し込めばよい
            a(j + 1) = a(j)            ' 1 つ右へずらす
            koukan = koukan + 1
            j = j - 1
        Loop
        a(j + 1) = x                   ' 差し込む
    Next i
End Sub

Sub selection(ByRef a() As Integer, ByVal n As Integer)
    Dim i As Integer, j As Integer, mn As Integer, t As Integer
    For i = 1 To n - 1
        mn = i                         ' いちばん小さいものの位置
        For j = i + 1 To n
            hikaku = hikaku + 1
            If a(j) < a(mn) Then mn = j
        Next j
        If mn <> i Then
            t = a(i): a(i) = a(mn): a(mn) = t
            koukan = koukan + 1
        End If
    Next i
End Sub
方法比較回数交換(移動)回数得意な場面
バブルつねに 28 回
n(n−1)/2
逆順の度合いによる (学習用。実務では使わない)
挿入法並びぐあいによる
最良 n−1、最悪 n(n−1)/2
ずらす回数 ほぼ並んでいるデータに強い
選択法つねに 28 回最大 n−1 回だけ 交換の負担が大きいとき(重いデータ)

3 つの違いは「何を節約するか」です。 選択法は交換の回数がとても少ない(最大 n−1 回)ので、 1 件のデータが大きくて動かすのが重いときに有利です。 挿入法はすでにほぼ並んでいるときにほとんど比較せずに終わります。
上のモデルで、B2〜I2 にすでに小さい順に並んだ数を入れて実行してみてください ── 挿入法の比較回数が 7 回まで減ります(他の 2 つは 28 回のまま)。 「速さ」はデータの性質によって変わるのです。

7. まとめ

  1. セルを画素として塗れば絵になる。「円の中か」は距離で判定
  2. 描画の座標は x が右、y が下(数学のグラフと上下が逆)
  3. 2 次元の回転は x2 = x1·Cos − y1·Siny2 = x1·Sin + y1·Cos中心は原点なので、描くときに位置を足す
  4. 曲線は細かい線分の集まりで描く。角数を増やせば円になる
  5. 3 次元は3 つの軸まわりの回転を重ねて、最後に z を捨てて描く
  6. フラクタルは再帰でそのまま書ける。コッホは 4n 本、ドラゴンは 2n
  7. 並べ替えはバブル・挿入・選択。同じ答えでも何を節約するかが違う

この授業の終わりに。 第 1 章でセルに 1 つ値を入れるところから始めて、 変数・条件・繰り返し・部品・配列・再帰と積み上げ、 いま10 行のプログラムでドラゴン曲線が描けるところまで来ました。
どの回も使った道具は同じ 3 つの構造(順次・分岐・繰り返し)と、 それを部品にまとめる仕組みだけです。 プログラミングで覚えることは、実はそれほど多くありません ── 難しいのは、やりたいことを機械が実行できる手順に分解することです。 それは練習で身につきます。

章末問題

付録:元資料からの修正一覧

もとにしたのは「情報学入門(VBA)190・195・197・200・205」の 5 本(計 110 枚)です。

元資料この教材種別
同じ内容が5 本に分かれている (195 と 200 がほぼ同内容、197 と 205 がほぼ同内容。表題は「〜2」「〜3」「〜5」「〜6」で、 「〜4」が欠番) 1 つの章にまとめた。新しい版(200・205)を採った重複
もくじの表題が「ユーザフォーム」なのに、 中身は回転体・コッホ曲線・ドラゴン曲線(197・205 の 2 枚目) もくじの見出しと中身が対応していない。内容に合わせて節を立てた不整合
GSolidLine中身が示されていない (呼び出し方だけがある) この教材では処理系に組み込み、同じ呼び出し方で描けるようにした。 本物の Excel では Shapes.AddLine を使う実装が別途必要なことを明記不足
座標系の向き(y が下)が図でしか示されていない 本文で明示。回転の中心が原点であることと、位置合わせの必要性も追記不足
SinCos の引数がラジアンである ことへの注意がない 度から直す式(度 * pi / 180)を追記不足
3 次元の回転を 3 段並べているが、画面に描くには z を捨てるという段が明示されていない 正投影として明示し、第 4 章の結合法則(3 つの回転を 1 つの行列にまとめられる)と結びつけた不足
コッホ曲線・ドラゴン曲線の線分の本数や性質に触れていない 4n 本・2n 本、 コッホは「長さが無限で面積は有限」であることを追記。第 14 章の再帰と明示的につないだ不足
3 つの並べ替えを並べているが、使い分けが書かれていない 比較回数・交換回数を実際に数えるモデルを作り、 選択法は交換が少ない/挿入法はほぼ並んだデータに強いという違いを示した不足
バブルソートが 195・197・200・205 の4 本すべてに登場 第 12 章と本章に整理重複

元資料で正しかったこと。 2 次元の回転の式、3 次元の回転を 3 軸に分けて重ねる考え方、 GSolidLine(ByVal x As Double, ByVal y As Double, ByVal xx As Double, ByVal yy As Double) という引数の設計、コッホ曲線・ドラゴン曲線・回転体という題材の選び方 ── いずれも適切です。 とくに「線分を引く手続きを 1 つ用意して、あとはその組み合わせで描く」という構成は、 グラフィックスの考え方そのもので、再帰(第 14 章)の応用としても的確な締めくくりです。