離散関数曲線の描画


やあ子どもたち。元気でやっているか。プログラムから単値関数
y=f(x)

の曲線グラフを画面などの表示デバイスに出力する際にはみんなはどうしているかい。この場合「関数」とは理論式だったり、デジタル信号処理とかの実測データや、各種科学技術系の実験データだったりするものを想定してるよ。今日は表・グラフ計算ソフトなどを使わずに、プログラムで任意の関数の曲線グラフを描画する際に便利なコードの紹介をするぞ。
今回おじさんは「関数」とは、 単純にvector< double > の数値列として定義されているものとして、これの曲線グラフをいかに簡単に画面に描画するかということを考えたので見てくれ。
最低限与えたい情報は以下の通りだ。

  1. 数値列そのもの
  2. 描画対象とするx軸範囲
    (実際にはvector< double >のインデックス範囲)
  3. 描画対象とするy軸範囲
  4. 描画する画面内の部分矩形領域

これだけだね。2と3は自動で決めてくれる仕様があってもいいかな。入力情報としては実に簡潔にこれだけのことだ。この情報から、得たい出力とは、

  • 画面内に出力の曲線として描画すべき、画面座標で記述されたポリラインセットの情報

だ。そうだろ?この出力さえあれば、あとはポリラインを描画すればいいだけだからな。
さて上記の入出力仕様に答えた内容が、以下の実装になるよ。

void func_to_line_strip( const vector< double >& fnc,
                           const pair< size_t, size_t >& x_range,
                           const pair< double, double >& y_range,
                           const double left,
                           const double right,
                           const double bottom,
                           const double top,
                           vector< vector< pair< float, float > > >* line_strips,
                           float* w_ratio=0,
                           float* h_ratio=0
                           )
{
    // 離散定義関数 fnc の、指定条件でのグラフ形状を、
    // 描画用ポリライン line_strip に返します。
    // x_range, y_range 指定バージョン
    double w = x_range.second-x_range.first;
    double h = y_range.second-y_range.first;
    double wr = (right-left)/ (w-1);
    double hr = (top-bottom) / h;
    bool is_ini=!0;
    bool prev_is_in=0;
    float prev_x, prev_y;
    for( auto i=x_range.first; i<x_range.second; ++i )
    {
        float x = left + wr * (i-x_range.first);
        float y = bottom + hr * (fnc[i]-y_range.first);
        bool is_in = !( y<bottom || y>top );
        if( is_in ){
            if( is_ini ){
                line_strips->push_back( vector< pair<float, float> >() );
            }
            else if( !prev_is_in ){
                line_strips->push_back( vector< pair<float, float> >() );
                // prev点と現在点の間の線分と、境界線との交点を求めます。
                if( prev_y > top ){
                    double t = (top-prev_y) / (y-prev_y);
                    line_strips->back().push_back( make_pair( prev_x+t*(x-prev_x), top ) );
                }else if( prev_y < bottom ){
                    double t = (bottom-prev_y) / (y-prev_y);
                    line_strips->back().push_back( make_pair( prev_x+t*(x-prev_x), bottom ) );
                }
            }
            line_strips->back().push_back( make_pair( x, y ) );
        }else{
            if( prev_is_in ){
                if( y > top ){
                    double t = (top-prev_y) / (y-prev_y);
                    line_strips->back().push_back( make_pair( prev_x+t*(x-prev_x), top ) );
                }else if( y < bottom ){
                    double t = (bottom-prev_y) / (y-prev_y);
                    line_strips->back().push_back( make_pair( prev_x+t*(x-prev_x), bottom ) );
                }

            }else{
                if( ( y>top && prev_y<bottom ) || ( y<bottom && prev_y>top) ){
                    line_strips->push_back( vector< pair<float, float> >() );
                    double t = (top-prev_y) / (y-prev_y);
                    line_strips->back().push_back( make_pair( prev_x+t*(x-prev_x), top ) );
                    t = (bottom-prev_y) / (y-prev_y);
                    line_strips->back().push_back( make_pair( prev_x+t*(x-prev_x), bottom ) );
                }
            }
        }
        is_ini=0;
        prev_is_in = is_in;
        prev_x = x;
        prev_y = y;
    }// i
    if( w_ratio ) *w_ratio = wr;
    if( h_ratio ) *h_ratio = hr;    
    return;
}
void func_to_line_strip( const vector< double >& fnc,
                           const double left,
                           const double right,
                           const double bottom,
                           const double top,
                           vector< vector< pair< float, float > > >* line_strips,
                           float* w_ratio=0,
                           float* h_ratio=0,
                           pair< size_t, size_t >* px_range=0,
                           pair< double, double >* py_range=0
                           )
{
    // 離散定義関数 fnc の、指定条件でのグラフ形状を、
    // 描画用ポリライン line_strip に返します。
    // x_range, y_range 自動計算バージョン
    pair< size_t, size_t > x_range( 0, fnc.size() );
    auto pr = std::minmax_element( fnc.begin(), fnc.end() );
    pair< double, double > y_range( *pr.first, *pr.second );
    func_to_line_strip( fnc, x_range, y_range, left, right, bottom, top,
                          line_strips, w_ratio, h_ratio );
    if( px_range ) *px_range = x_range;
    if( py_range ) *py_range = y_range;
    return;
}

さてさて、引数の説明は要らないよな。冒頭に上げた入出力仕様そのものなので。const がついたものが入力で、ポインタで返してるものが出力だよ。一応言っておくとすれば、出力の line_strips は、関数を曲線として描画しなくてはならないポリラインのセットだよ。これは座標の連なりとしてのポリラインが複数入る入れ物だよ vector の中に、vector< pair< float, float > > が入っているわけだからな。。画面描画のための不動小数精度はfloat でいいやって思ってるので。
この関数の使い方は、以下のようになるよ。例えばだけどね。

 // 離散関数曲線描画のための、入力情報のトスと、出力情報の取得
    float wr;
    vector< vector< pair< float, float > > > line_strips;
    func_to_line_strip( fnc, x_range, y_range, left, right, bottom, top,
                          &line_strips, &wr );
 // …

 // 出力ポリライン情報の描画事例(昔のOpenGLならこうなる。)
    for( auto& istrip: line_strips )
    {
        glBegin( GL_LINE_STRIP );
        for( auto& ipos: istrip )
            glVertex3f( ipos.first, ipos.second, 0 );
        glEnd();
    }// ipos
 // ↑ポリラインを複数描画するわけだからこういう感じでfor文がネストする感じになるね。

というわけであとはOpenGL、GDI+、OpenCVといった、およそポリラインが描画出来る表示系プラットフォームであれば何だって関数の曲線グラフが簡単に描画できてしまうというわけさ。あえてOpenGLでポリラインを描画してみた結果が冒頭や下の画像だ。



最後に今回のコードの利点を幾つかあげておしまいにするぞ。

  1. 曲線グラフを正しく描画するための、描画範囲と描画領域に関する面倒な検討や計算が一切不要。
  2. 縦軸に関しては、範囲外から入ってきた場合、外に出ていく場合でも、それぞれ上限or下限のラインとの交点を求め、トリム処理が適切に行われます。(冒頭画像一番下の三角帽子関数の上限を突き抜けているところを参照)
  3. 一見一本に見える曲線なども、描画対象値域の設定によってはその境界により分断されてしまい、結果、表示上は複数のカーブとなってしまったりしますが、そういう場合にも適切に複数のポリラインが返されます。

(2,3をひっくるめて「描画矩形領域でのトリミングに対応」と言ってしまってもいいかも知れない。)
じゃっ、またな!