LoginSignup
20
19

More than 5 years have passed since last update.

openFrameworks C++11 で乱数を工夫する

Last updated at Posted at 2016-05-11

導入

>概要

openFrameworks 0.9.~からC++11がサポートされました。
このC++11の機能を用いることで乱数の生成に工夫が出来ます、今回はその方法を紹介します。
みなさん、こんな経験ありませんか?パラメータを初期化する際、ofRandom()を使うと、のっぺり、すべて同じ確率で乱数が生成されてしまう、、ある箇所に集中させたいときどうすればいいんだろう?
そんな時、C++11std::normal_distributionを用いれば簡単に解決できます。
ここで解説するサンプルプロジェクトはgithubで公開しています。
https://github.com/Hiroki11x/openFrameworksSample/tree/master/oF_NormalDistributionSample

>正規分布に従う擬似乱数を生成

この画像もwikipediaから取ってきたものですが、正規分布とは大雑把に言うと、正規分布はある平均値に集中して集まるような確率分布です。(詳しくはwikipediaを)
imgres.png

この分布を使うことで、例えば中心に集中して円を以下のように配置したい場合、

スクリーンショット 2016-05-12 3.58.56.png

このプログラムの実装方法について以下で説明します。

実装(2次元)

>ヘッダファイルの定義

  1. 正規分布に従う擬似乱数を生成するためにC++11から追加されたnormal_distributionを用いる必要があります。このクラスが定義されている<random>をincludeする必要があります。
  2. 生成した擬似乱数を座標として保持するためにofVec2f型のvectorをメンバ変数として宣言しておきます。
ofApp.h
#pragma once

#include "ofMain.h"
#include <random> // <- ここ

class ofApp : public ofBaseApp{
private:
    vector<ofVec2f> position_vector_2d; // <- ここ
public:
    void setup();
    void update();
    void draw();
    void keyPressed(int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void mouseEntered(int x, int y);
    void mouseExited(int x, int y);
    void windowResized(int w, int h);
    void dragEvent(ofDragInfo dragInfo);
    void gotMessage(ofMessage msg);
};

>実装ファイルについて

まず正規分布の擬似乱数生成に関して以下のStepで行っている
1. engineとしてdefault_random_enginemt19937を選択
2. distributionとしてnormal_distributionを選択
3. デフォルトのengineを宣言
4. 釣鐘型の正規分布を平均値、標準偏差を与えて宣言
5. 擬似乱数生成器を作る
6. 生成器から生成した乱数をvectorpush_back

ただし

用語 説明
エンジン(engine) 乱数または擬似乱数を生成する
分布(distribution) 生成した値を一定範囲の数学的分布へとマップする

あとは、その座標をもとに描画を行っているだけである。
bind()に関しては最後に補足を書いておきました。
normal_distributionのリファレンスはこちらを参考にしてください。

ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){

    //Normal Distribution
    using my_engine = mt19937; // <-1
    using my_distribution = normal_distribution<>; // <-2
    my_engine mEngine {}; // <-3
    my_distribution mDistribution {0.0,10.0};// <-4 (平均0.0、標準偏差10.0で分布させる)

    auto generate_random = bind(mDistribution,mEngine);// <-5
    for(int i = 0 ; i<10000 ; i++){ //<-以下6
        float  x = round(generate_random());
        float y = round(generate_random());
        position_vector_2d.push_back(ofVec2f(x,y));
    }

    ofBackground(0);
}

//--------------------------------------------------------------
void ofApp::update(){
}

//--------------------------------------------------------------
void ofApp::draw(){
    ofSetColor(0,200,255,20);
    ofPushMatrix();
    ofTranslate(ofGetWidth()/2,ofGetHeight()/2);
    for(auto v :position_vector_2d){
        ofDrawCircle(v.x*10 ,v.y*10 ,5);
    }
    ofPopMatrix();
}

実装(3次元)

2次元のものを3次元に拡張しただけです。カメラの追加や視点の移動などを行っていますが、それ以外ほぼ変わりません。以下にソースコードと実行結果を載せておきます。

ofApp.h
#pragma once

#include "ofMain.h"
#include <random>

class ofApp : public ofBaseApp{
private:
    vector<ofVec3f> position_vector_3d;
    ofEasyCam cam;
    float cam_dist;

public:
    void setup();
    void update();
    void draw();

    void keyPressed(int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void mouseEntered(int x, int y);
    void mouseExited(int x, int y);
    void windowResized(int w, int h);
    void dragEvent(ofDragInfo dragInfo);
    void gotMessage(ofMessage msg);

};
ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){

    //Normal Distribution
    using my_engine = mt19937;
    using my_distribution = normal_distribution<>;
    my_engine mEngine {};
    my_distribution mDistribution {0.0,10.0};// 平均0.0、標準偏差10.0で分布させる

    auto generate_random = bind(mDistribution,mEngine);
    for(int i = 0 ; i<10000 ; i++){
        float x = round(generate_random());
        float y = round(generate_random());
        float z = round(generate_random());
        position_vector_3d.push_back(ofVec3f(x,y,z));
    }

    ofBackground(0);
    cam_dist = 600;
}

//--------------------------------------------------------------
void ofApp::update(){
    cam.setPosition(0, 0, cam_dist);
}

//--------------------------------------------------------------
void ofApp::draw(){
    ofSetColor(0,200,255,20);
    ofPushMatrix();
    ofTranslate(ofGetWidth()/2,ofGetHeight()/2);

    cam.begin();

    for(auto v :position_vector_3d){
        ofDrawBox(v.x*10 ,v.y*10 ,v.z*10 ,5);
    }

    cam.end();

    ofPopMatrix();
    ofDrawBitmapStringHighlight("cam_dist: "+ofToString(cam_dist), 20,20);
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){
    if(key == OF_KEY_UP){
        cam_dist += 10;
    }else if(key ==OF_KEY_DOWN){
        cam_dist-= 10;
    }
}

実行するとこのようになります、キー操作によってカメラの位置を変更できます。

gif animation

補足(bind()について)

標準ライブラリのbind()は例えば今回の場合、以下のようになっていました。

auto generate_random = bind(mDistribution,mEngine);

この時、第二引数のmEngineを受け取り、第一引数のmDistributionを実行する関数オブジェクトを作成します。
つまりgenerate_random()の呼び出し自体は、mDistribution(mEngine)の呼び出しと等価です。

訂正版

yumetodoさんからご指摘をいただきましたpush_back重そうなので、std::reserve(),std::generate()を使うのがいいのではないかという点、std::bind()を最適化できないコンパイラ(VS)もあるということで、三次元の実装に関して修正を加えたいと思います。ヘッダファイルは変わらず、実装ファイル(ofApp.cpp::setup())について以下に載せておきます。

1. std::generate()を用いる場合

//--------------------------------------------------------------
void ofApp::setup(){
//Normal Distribution /std::generate:32868msec
    using my_engine = mt19937;
    using my_distribution = normal_distribution<>;
    my_engine mEngine {};
    my_distribution mDistribution {0.0,10.0};// 平均0.0、標準偏差10.0で分布させる

    const auto t1 = std::chrono::high_resolution_clock::now();
    const auto size = position_vector_3d.size();
    position_vector_3d.resize(size + 100000);
    std::generate(position_vector_3d.begin()+ size, position_vector_3d.end(),[&mDistribution, &mEngine](){ return ofVec3f(mDistribution(mEngine),mDistribution(mEngine),mDistribution(mEngine));});
    const auto t2 = std::chrono::high_resolution_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "msec" << std::endl;
    ofBackground(0);
    cam_dist = 600;
}

2. std::reserve()を用いる場合

//--------------------------------------------------------------
void ofApp::setup(){
//Normal Distribution /std::reserve: 30977msec /without reserve -> 32646msec
    using my_engine = mt19937;
    using my_distribution = normal_distribution<>;
    my_engine mEngine {};
    my_distribution mDistribution {0.0,10.0};// 平均0.0、標準偏差10.0で分布させる

    const auto t1 = std::chrono::high_resolution_clock::now();
    position_vector_3d.reserve(100000);
    for(int i = 0 ; i<100000 ; i++){//338663microsec/344389microsec
        float  x = round(mDistribution(mEngine));
        float y = round(mDistribution(mEngine));
        float z = round(mDistribution(mEngine));
        position_vector_3d.push_back(ofVec3f(x,y,z));
    }

    const auto t2 = std::chrono::high_resolution_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count() << "msec" << std::endl;
    ofBackground(0);
    cam_dist = 600;
}

また、エンジン型に関してdefault_random_engineより、mt19937を採用した方がいいみたいです。

3. エンジン型について

擬似乱数エンジンは、基本的には32ビット版メルセンヌ・ツイスターの実装であるmt19937、もしくはその64ビット版の実装であるmt19937_64のどちらかを使用することを推奨する。
非専門用途のためのdefault_random_engineというエンジン型も定義されているが、この型は環境によって乱雑度が低く、周期も短い擬似乱数エンジンの別名として定義される場合がある。
mt19937は、状態の大きさがsizeof(std::uint_fast32_t) * (624 + 1)だけあり、少々サイズが大きいが、それを受け入れられる状況であれば、デフォルトでmt19937を採用しよう。このエンジンでは、乱雑度や周期の長さが問題になることは少ない。
cpprefjp <random>ヘッダからの引用です。

20
19
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
19