導入
>概要
openFrameworks 0.9.~からC++11
がサポートされました。
このC++11
の機能を用いることで乱数の生成に工夫が出来ます、今回はその方法を紹介します。
みなさん、こんな経験ありませんか?パラメータを初期化する際、ofRandom()
を使うと、のっぺり、すべて同じ確率で乱数が生成されてしまう、、ある箇所に集中させたいときどうすればいいんだろう?
そんな時、C++11
のstd::normal_distribution
を用いれば簡単に解決できます。
ここで解説するサンプルプロジェクトはgithubで公開しています。
https://github.com/Hiroki11x/openFrameworksSample/tree/master/oF_NormalDistributionSample
>正規分布に従う擬似乱数を生成
この画像もwikipediaから取ってきたものですが、正規分布とは大雑把に言うと、正規分布はある平均値に集中して集まるような確率分布です。(詳しくはwikipediaを)
この分布を使うことで、例えば中心に集中して円を以下のように配置したい場合、
このプログラムの実装方法について以下で説明します。
実装(2次元)
>ヘッダファイルの定義
- 正規分布に従う擬似乱数を生成するために
C++11
から追加されたnormal_distribution
を用いる必要があります。このクラスが定義されている<random>
をincludeする必要があります。 - 生成した擬似乱数を座標として保持するために
ofVec2f
型のvector
をメンバ変数として宣言しておきます。
#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_engine
mt19937
を選択
2. distribution
としてnormal_distribution
を選択
3. デフォルトのengine
を宣言
4. 釣鐘型の正規分布を平均値、標準偏差を与えて宣言
5. 擬似乱数生成器を作る
6. 生成器から生成した乱数をvector
にpush_back
ただし
用語 | 説明 |
---|---|
エンジン(engine ) |
乱数または擬似乱数を生成する |
分布(distribution ) |
生成した値を一定範囲の数学的分布へとマップする |
あとは、その座標をもとに描画を行っているだけである。
bind()
に関しては最後に補足を書いておきました。
normal_distribution
のリファレンスはこちらを参考にしてください。
#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次元に拡張しただけです。カメラの追加や視点の移動などを行っていますが、それ以外ほぼ変わりません。以下にソースコードと実行結果を載せておきます。
#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);
};
#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;
}
}
実行するとこのようになります、キー操作によってカメラの位置を変更できます。
補足(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>ヘッダからの引用です。