TKYM's profile※ 画面は開発中のものですPhotosBlogListsMore Tools Help

Blog


    June 29

    C# でベジエ曲線(2)

    無事、ベジエ曲線とその並行曲線がかけたのが前回まで。

    曲線の式が分かることで、列車を滑らかにカーブさせることができそうだが、
    まだ曲線の上を一定速度で動かすことができない。
    前回のベジエ曲線の式は、パラメータ t で表されていて、
    t を一定に増加させても 等速度で動いてくれないように見える。

    だから、 適当な距離 l において、パラメータ t はいくつなのか調べないといけない。
    また、曲線全体の距離も知りたい。

    そこで、高校の数学教科書の最後のほうのページに、「曲線の長さ」という項目があったことを思い出し、開いてみた。

    (b~a の定積分)∫√[ { f’(t) }^2  +  { g’(t) }^2 ] dt

    次数の高い多項式がルートの中に入ったものを積分するのは非常に難しそう。
    つい最近、大学で積分の数値計算を習ったことから、それで計算することにする。

    とはいっても、積分は基本的に、地道に面積を計上していくただのループ。
    なんとかならないものかという気持ちが残る。

    --------------

    積分のアルゴリズムには、台形公式とシンプソンの公式と、ほかに各点の重みや積分区間を不等間隔にする何とかという公式があるらしい。

    あるグラフを区間ごとに区切って面積を計算する時、その区間のグラフが曲線でないこととして計算すれば、台形公式になる。 それでは誤差が大きくなってしまうので、 区間上の3点を通る2次曲線の積分として考えれば、少しは誤差は小さくなる。これがシンプソンの公式である。

    他にもいい方法があると本には書いてあるが、難しくなると面倒だし、ピクセル単位であっていればいいのでシンプソン法で計算してみることにした。

    ------------

    とりあえず、 曲線の パラメータ t での長さ l が何ピクセルなのかを知りたい。

    計算のやり方は教科書のまる写しでなんとかする。
    グラフを、 (t0, l0), (t1, l1), (t2, l2),,,,,(tN, lN) と区切って、
    (h/3)(l0 + 4l1 + l2) + (h/3)(l2 + 4l3 + l4) + … と計算していく。

    これを簡単にするとこんな感じ。
    (h/3) * {
    l0 + lN
    + l偶数番 * 2
    + l奇数番 * 4
    }

    ちなみに、区間は偶数個に区切らなければならないらしい。

            //パラメータ t を 開始点からの距離 l に変換する h は積分刻み幅
            //シンプソンの公式を用いた
            static public float t2l(float t, PointF[] pt, float h)
            {
                float tcnt, prev =0.0f, tmplen, tmpadd, total = 0.0f;
                int i;
                for (tcnt = 0.0f, i = 0; tcnt <= t; i++, tcnt += h) {
                    tmplen = length(bezier.tangental_func(pt, tcnt));
     
                    if (tcnt + h > t) 
                    { //最後
                        if (i % 2 == 1)
                        { //奇数個で終わる
                            tmpadd = (prev + tmplen * 4 + length(bezier.tangental_func(pt, tcnt + h))) / 2;
                        }
                        else
                        { //偶数個で終わる
                            tmpadd = tmplen;
                        }
                    }
                    else if (tcnt < h)
                    { //最初:そのまま
                        tmpadd = tmplen;
                    }
                    else if (i % 2 != 0)
                    { //奇数
                        tmpadd = tmplen * 4;
                    }
                    else
                    { //偶数
                        tmpadd = tmplen * 2;
                    }
                    total += tmpadd;
                    prev = tmplen;
                }
                return h/3.0f * total;
            }

    積分刻み幅を任意に定められることにしたかったので、項数が奇数になってしまった場合、区間を超えて計算し、超えた区間を2で割るようにしてみた。

    そして、その逆変換

            //開始点からの距離 l を パラメータ t に変換する h は積分刻み幅
            //シンプソンの公式
            static public float l2t(float l, PointF[] pt, float h)
            {
                float tcnt, prev =0.0f, tmplen, tmpadd, total = 0.0f, lcnt, endlen = 0.0f;
                int i;
                for (tcnt = 0.0f, i = 0; tcnt <= 1.0f && endlen < l; i++, tcnt += h) {
                    tmplen = length(bezier.tangental_func(pt, tcnt));
     
                    //ここで終了した場合の長さ
                    if (i % 2 == 1)
                    { //奇数個で終わる
                        endlen = (total + (prev + tmplen * 4 + length(bezier.tangental_func(pt, tcnt + h))) / 2)
                            * (h / 3.0f);
                    }
                    else
                    { //偶数個で終わる
                        endlen = (total + tmplen)
                             * (h / 3.0f);
                    }
    
                    //加算処理
                    if (tcnt < h)
                    { //最初
                        tmpadd = tmplen;
                    }
                    else if (i % 2 != 0)
                    { //奇数
                        tmpadd = tmplen * 4;
                    }
                    else
                    { //偶数
                        tmpadd = tmplen * 2;
                    }
    
                    total += tmpadd;
                    prev = tmplen;
                }
                return tcnt - h/2.0f;
            }

    距離から t を求める場合、ループするたびにその地点での距離を知りたいので、そのつど終了した場合の距離を計算し、終了判断しなければならない。 あと、戻り値は必ず 本来の値以上になることから、 刻み幅の半分を引いてみた。

    image

    t = 0.5 の位置を距離に変換し、戻してみた。
    大体あっているが、実用的なのか微妙なところ。

     

    それより、コードがなんか汚い。 言い訳としては、ご丁寧に中かっこをエディタが改行してくれるということ。
    IntelliSense も C++ に比べるとおせっかいに働いてくれる。

    C# でベジエ曲線

    C# を始めた。
    Visual studio にはいくつかの言語が入っていて、 VB とかもできるのだが 手は伸びていなかった。
    C を使っていると 効率が悪いということにいい加減気づくべきではないかと思い、 C# を始めてみようと思ったわけです。

    #include の順番とか、 定義の順番だとか、ヘッダファイルとか、悩まされることが少ない。
    メモリの確保とかも考えるべきことがない。

    C++はC++で深い。 Windows の深いところまで勉強するには C++ が必須。
    でも、普通のソフトを作るのに手間をかけすぎていては困る。器用に使い分けてみたい。

    -----------------

    C# で作りたいものは、ちょっとしたゲームのようなもの。
    鉄道系のシミュレーションが好きなので、それの簡易版っぽいものを考えている。
    それで、そのレールをベジエ曲線にしてその上をガタゴト走らせようと考えている。

    ただ、単純にベジエ曲線を 便利なライブラリにおそらく入っているであろう DrawBezier などという関数でビーッと引くだけではすまないであろう。その線に沿って、列車が動かなければならない。

    となると、数式でその座標を表さなければならない。
    なので、数式とにらめっこする必要が生じてくる。

    ----------------

    調べてみると、
    http://ja.wikipedia.org/wiki/%E3%83%99%E3%82%B8%E3%82%A7%E6%9B%B2%E7%B7%9A
    こんな感じの数式であることがわかった。

    ∑ Bi Jni(t)

    Jni(t) = (n i) t^i (1-t)^(n-i)

    (n i) という部分をベクトルかと思ってずっと考えていたらこれは nCi (組み合わせ)ということが判明。
    http://ja.wikipedia.org/wiki/%E4%BA%8C%E9%A0%85%E5%AE%9A%E7%90%86
    (n i) というのは二項係数というらしい。最初からそう書いてあればいいのに。

    とりあえず線の式はわかった。
    パラメータ t での座標を返す関数を書いてみた。制御点の数は可変にしておいた。

            static public PointF curve(PointF[] pt, float t) {
                PointF total = new PointF(0.0f, 0.0f);
                int n = pt.Length - 1;
                int i;
    
                for (i = 0; i < pt.Length; i++) {
                    total.X += pt[i].X * mCn(n, i) * (float)Math.Pow((double)t, i) * (float)Math.Pow(1.0 - t, (double)n - i);
                    total.Y += pt[i].Y * mCn(n, i) * (float)Math.Pow((double)t, i) * (float)Math.Pow(1.0 - t, (double)n - i);
                }
    
                return total;
            }

    でも1本だけでは足りない。平行にもう1本ほしい。レールだから。

    曲線の接線に対して直角に、一定距離進んだところをつないで線にすれば平行になりそう。
    接線を求めるには式を微分すればよさそう。

    Bi と(n i) を定数として、 { (t^i) * (1-t)^(n-i)} ’  の部分を考える。
    単純に考えると
    (t^i)’ * (1-t)^(n-i)  +  t^i * {(1-t)^(n-i)}’ 積の微分
    it^(i-1) * (1-t)^(n-i)  +  t^i * (n-i)(1-t)^(n-i-1)(-1) 合成関数

    よって ∑ Bi (n i) { it^(i-1) * (1-t)^(n-i)  +  t^i * (n-i)(1-t)^(n-i-1)(-1) }

    という感じになりそうなのだが、このままコードにすると失敗した。
    0乗のときは1なので微分するとそのまま消えないといけないから。

            //ベジエ曲線の、接線を表す関数
            public static SizeF tangental_func(PointF[] pt, float t)
            {
                PointF total = new PointF(0.0f, 0.0f);
                int i;
                int n = pt.Length - 1;
    
                for (i = 0; i < pt.Length; i++)
                {
                    float d1 = (i == 0) ? 0.0f : (float)i * powFI(t, i - 1);
                    float d2 = (n - i == 0) ? 0.0f : (float)(n - i) * powFI(1.0f - t, n - i - 1) * (-1);
    
                    total.X += pt[i].X * mCn(n, i) *
                    (
                        d1 * powFI(1.0f - t, n - i) + powFI(t, i) * d2
                    );
    
    
                    total.Y += (pt[i].Y * mCn(n, i) *
                    (
                        d1 * powFI(1.0f - t, n - i) + powFI(t, i) * d2
                    ));
                }
                return new SizeF(total.X, total.Y);
            }

    * powFI は float 型をキャストせずに べき乗できるようにした単純な関数。
    * d1 と d2 がそれぞれ t^i と (1-t)^(n-i) を微分したものをいれる変数。

    この戻り値をそのまま描画したらうまく接線が描画された。
    しかし、場所によって長さが変わってしまった。 曲線の パラメータ t の 1あたりの長さになってしまっているかららしい。

    よって、このx, y に同じ値 l をかけて 長さを1にする必要がある。
    そうしないと並行曲線が描けない。

    (lx)^2 + (ly)^2 = 1^2
    l^2 = 1 / (x^2 + y^2)
    l = 1 / √(x^2 + y^2)

    となり、(x / √(x^2 + y^2) , y / √(x^2 + y^2) ) が長さ1ピクセル分のベクトルになる。
    英語のwikipedia に Parallel curve という項目があって、これを見ると何となく正しいような気がする。
    http://en.wikipedia.org/wiki/Parallel_curve

    この値に必要な長さをかけて使えばよい。

    ----------

    接線を直角に曲げて、一定距離の地点を線でつないで行けば平行な線が引けそうである。
    接線のベクトルが (x, y) なら、 その直角は ( y, –x )。

    で、レール(道路)の幅だけ掛け算する。
    これで平行線のグラフもわかった。

    これらを使って適当に線を引く

    image

    はい、完成。

    ところで、 bezier 曲線は、 ベジエと発音するのがどちらかというと原語に近いとのこと。
    少し早くしゃべるだけで区別がつかなくなるような気もするが。

    June 17

    Visual studio コマンドウィンドウ

    先生は 「統合開発環境を使えば簡単ですが、 何かと応用が利かなくなったりするのでコマンドも覚えましょう。」 といいます。
     
    今日、 Visual Studio の画面上に 「コマンドウィンドウ」 を発見したので、適当に dir と打ち込んで見たところ、
     
    "コマンド "dir" は有効ではありません。"
    との反応。
     
    調べてみたところ、こんなコマンドを入力するらしいです。
     
    Visual Studio コマンドの定義済みのエイリアス
     
    ショートカットキーで十分そうなコマンドが多いですが、
    使いそうなものをメモ。
     
    • ? x : xの内容を表示する
    • gotoln : 指定行に移動(ステータスバーの行数をダブルクリックしてもOK)
    • lcase : エディタの選択範囲を小文字にする
    • ucase : エディタの選択範囲を大文字にする
    • nav : ブラウザタブを開く
    • shell x : シェルで x を実行
    • wordwrap : 右端で折り返す(折り返さない)

    エイリアスの作成方法
    http://msdn.microsoft.com/ja-jp/library/xasxzd71(VS.80).aspx

    June 04

    simutrans200806

    Simutrans 、フリーのシミュレーションゲーム。

    町にバスを走らせ、町外れに駅を作り、町同士をつないでネットワークを広げていく。
    最初はコストのかからない交通機関を多く使用してネットワークの拡大に資金を注ぎ、
    拡大した後は儲けすぎて楽になるので、想像力を働かせて遊ぶ。

    車両自体、小さくて識別しにくいが、アドオンが多く公開されている。
    東武の車両を一式入れてみた。
    地名は変えられるが、デフォルトではランダムに割り当てられる。

    下図 : 中央の駅で通過待ちをしている。
    信号を駆使して、追い越させるようにする。ちょっとしたパズルっぽい。
    simscr08


    常磐線の中距離電車は10両編成が多い。
    反面、緑色の快速は15両編成が多い。
    現状、明らかに乗車率が違う。

    短距離の電車と同じ停車駅にとまる上に、中距離を運転するのだから、同じ定員なら乗車率が高くなることは想像できる。しかし、なぜ短距離電車の編成が長く、中距離が短いのか。

    ・・・グリーン車に乗ってもらいたいからか。