OrangeSignal CSV 2.0 開始
しばらくエンハンスしていなかった OrangeSignal CSV ですが、お使いいただいている方々からの要望に応えるべく、SourceForge.JP から GitHub へ移行してプロジェクトを再度開始しました。
※SourceForge.JP じゃなくなったので、パッケージ名も com.orangesignal に変わります
https://github.com/orangesignal/orangesignal-csv
正式リリースはちょっと先になりますが、フィードバックを受けて今回追加する予定機能は
な感じです。
実は、一番目の「POJO と CSV を行単位でやり取りできる各種 Reader および Writer の追加」の設計と実装・テストはほぼ終わっています...というかこれの為に再設計したところがあり、パッケージ構成が一部変更になっています。
再設計・再実装しなければならない程密結合だったのは
http://d.hatena.ne.jp/tatsu-no-toshigo/20130310
↑この方のエントリでも指摘されています。
そして、二番目のは設計中です。
Maven Central は JLHA をどうするかという課題があるので、まだ予定段階ですが、皆さんにとってより便利になればと思いますのでちょっと頑張って検討してまいります。
Mac OS X + Jenkins での文字化け対応メモ
Jenkins + Genymotion で Android なテストをしたメモ
Jenkins での自動テストで、Android エミュレータの代わりに Genymotion を使用して自動テストをする場合のメモ。CI 環境については前エントリをご覧下さい。
Genymotion で仮想端末を作った後にやること
- [設定]-[セキュリティ]-[提供元不明のアプリ]を オン にする
- [設定]-[セキュリティ]-[アプリを確認する]を オフ にする
Jenkins ジョブ設定
- ビルド環境の "Run an Android emulator during build" を オフ にしてエミュレータを使わないようにする。
- ビルドの前処理として adb 経由で Genymotion へ接続する以下のようなシェルを作って置き、呼び出す。(adb root しないとテスト結果を pull できないので注意)
#!/bin/sh # genymotion_connect.sh adb start-server adb connect 192.168.56.101 adb root
- ビルドの後処理として adb 経由で Genymotion から切断する以下のようなシェルを作って置き、呼び出す。
#!/bin/sh # genymotion_disconnect.sh adb disconnect 192.168.56.101
Jenkins ジョブを開始する前にやっておくこと
- Oracle VM VirtualBox 起動
- Genymotion 起動
- Genymotion の仮想端末を起動 (Play) しておくこと
※起動しっぱなしにする必要があるのでログアウト方法に注意する
Jenkins + Android + SonarQube (旧 Sonar) で一歩進んだ自動テストな CI 環境を構築する おまけ
おまけというか蛇足です。
Sonatype Nexus の導入
Maven リポジトリ管理ソフトの Sonatype Nexus の導入手順です。Maven で CI するなら connection refused を出さないためにも導入しておくのが現実的かと思います。類似プロダクトとして Artifactory などいくつかあります。各プロダクトの比較については、Maven Repository Manager Feature Matrix をご覧下さい。
- Tomcat を停止させます。
$ ${HOME}/apache-tomcat-7.0.42/bin/shutdown.sh
$ cd $home
$ mv nexus-2.6.1-02.war apache-tomcat-7.0.42/webapps/nexus.war
- Tomcat を開始します。
$ ${HOME}/apache-tomcat-7.0.42/bin/startup.sh
- 必要があれば http://localhost:8080/nexus へアクセスして Nexus へログイン (デフォルト管理者アカウント:admin / admin123) してミラーリングの設定や他のリポジトリのプロキシ設定などを行います。※どこかに良記事があると思いますのでググってみて下さい。
- Nexus 公式の導入手順通りに、Maven の処理全てが Nexus を経由するように設定を行います。また、Jenkins から Nexus のリポジトリへ deploy きるように Nexus の開発者アカウントの指定もしておきます。
$ cd $home
$ vi .m2/settings.xml
<?xml version="1.0" encoding="UTF-8"?> <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <servers> <server> <id>releases</id> <username>deployment</username> <password>deployment123</password> </server> <server> <id>snapshots</id> <username>deployment</username> <password>deployment123</password> </server> </servers> <mirrors> <mirror> <id>nexus</id> <mirrorOf>*</mirrorOf> <url>http://localhost:8080/nexus/content/groups/public</url> </mirror> </mirrors> <profiles> <profile> <id>nexus</id> <activation> <activeByDefault>true</activeByDefault> </activation> <repositories> <repository> <id>central</id> <url>http://central</url> <releases><enabled>true</enabled></releases> <snapshots><enabled>true</enabled></snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>central</id> <url>http://central</url> <releases><enabled>true</enabled></releases> <snapshots><enabled>true</enabled></snapshots> </pluginRepository> </pluginRepositories> </profile> </profiles> </settings>
PSI-Probe の導入
個人的には、Tomcat 標準の manager Web アプリや host-manager Web アプリなどより使いやすいと思っている Tomcat 管理用ソフトの PSI Probe の導入手順です。
- PSI Probe の ダウンロードページ から執筆時点での最新である 2.3.3 (probe-2.3.3.zip) をダウンロードします。
- ZIP を解凍して probe.war を Tomcat の webapps へデプロイします。
- http://localhost:8080/probe へアクセスして使用できます。ログインアカウントは manager アプリと同じです。
Android NDK
入手先:http://developer.android.com/tools/sdk/ndk/index.html
JNI な Android アプリをビルドする場合に必要となる Android NDK の導入手順です。
- 32 bit 用と 64 bit 用のファイルをすべてダウンロードし、同じ場所に展開します。
$ cd $home
$
$ wget http://dl.google.com/android/ndk/android-ndk-r9-linux-x86.tar.bz2
$ bzip2 -dc android-ndk-r9-linux-x86.tar.bz2 | tar xvf -
$ rm -f android-ndk-r9-linux-x86.tar.bz2
$
$ wget http://dl.google.com/android/ndk/android-ndk-r9-linux-x86-legacy-toolchains.tar.bz2
$ bzip2 -dc android-ndk-r9-linux-x86-legacy-toolchains.tar.bz2 | tar xvf -
$ rm -f android-ndk-r9-linux-x86-legacy-toolchains.tar.bz2
$
$ wget http://dl.google.com/android/ndk/android-ndk-r9-linux-x86_64.tar.bz2
$ bzip2 -dc android-ndk-r9-linux-x86_64.tar.bz2 | tar xvf -
$ rm -f android-ndk-r9-linux-x86_64.tar.bz2
$
$ wget http://dl.google.com/android/ndk/android-ndk-r9-linux-x86_64-legacy-toolchains.tar.bz2
$ bzip2 -dc android-ndk-r9-linux-x86_64-legacy-toolchains.tar.bz2 | tar xvf -
$ rm -f android-ndk-r9-linux-x86_64-legacy-toolchains.tar.bz2
- "保存" ボタンを押下して設定を完了します。
※ジョブ側の設定で使用する Maven のゴールとオプションで "android:ndk-build" を設定することで、 JNI ビルドを行うことが可能です。詳しくはプラグインドキュメントの Native Development や android:ndk-build をご覧下さい。
その他
- Ant ではなく Maven マターな方が Jenkins での管理で幸せになれると思います。Gradle は知らんです。
- EMMA はバイナリに計測用のコードを埋め込むので、リリース APK の生成を同じジョブでしない方がよいと思います。また APK を生成するだけであればエミュレータの設定は不要なのでやはり別ジョブが良いのではないかと思います。
- EMMA ではなく uiautomator + スクショ保存については、stephanenicolas/Quality-Tools-for-Android の uiautomator profile を参考にすると幸せになれると思います。開発V字モデルで適用するフェーズが EMMA とは異なると思うので、これも別ジョブで良いと思います。
- モックを多様したテストはアンチパターンの一つですが、Android でのテストではその方がテストしやすい場合がまれにあります。特にライブラリプロジェクトでは、EMMA を使うべきか、Robolectric + cobertura を使うべきか慎重に検討した方が良いと思います。
- Android NDK を使用して Jenkins でのビルドは問題ないですが、Sonar は Android NDK に対応していませんのでご注意下さい。iOS の場合は Xcode や Jenkins Xcode Integration Plugin を導入済であれば Sonar へ octo-technology/sonar-objective-c の Objecttive-C プラグインを入れればいけるはずです。
おわり
Jenkins + Android + SonarQube (旧 Sonar) で一歩進んだ自動テストな CI 環境を構築する パート2
前回の続きです。今回は、Android 用プロジェクトを Jenkins のジョブ登録してビルド実行・Sonar で確認まで行いたいと思います。
テスト可能な Android の Maven プロジェクト一式を用意します。
stephanenicolas/Quality-Tools-for-Android や SonarSource/sonar-examples の android-maven-emma などいくつかあります。Android のテストを実行可能な Maven 形式のプロジェクトは、親プロジェクト+アプリ用プロジェクト+テスト用プロジェクトの構成になります。各プロジェクトの pom.xml 設定は Quality-Tools-for-Android よりも android-maven-emma の方が簡潔なので、こちらをダウンロードし各 pom.xml について以下の様に修正します。
- android-maven-plugin のバージョンを 3.6.1 (3.6.0 以上) へ修正します。
- dependency の書き方が mosabua/maven-android-sdk-deployer と異なっているので、groupId を com.google.android ⇒ android、version を 2.3.3 ⇒ 2.3.3_r2 へ変更します。また、android-test は不要となるので android-test の dependency ブロックを削除します。
Jenkins ジョブ作成
- http://localhost:8080/jenkins へアクセスして "新規ジョブ作成" をクリックします。
- ジョブ名に何か入力し、"Maven2/3プロジェクトのビルド" を選択の上、"OK" ボタン押下します。
- "ソースコード管理" に Subversion や Git を指定したいところですが、今回の方法だとローカルにしかソースがないので、Jenkins ジョブのワークスペースへ直接ソース一式を放り込みました。
※ここで pom.xml を修正してください。
$ cd $home
$ unzip sonar-examples-master.zip
$ # android-maven-emma を workspace という名前に変更して移動します
$ mv sonar-examples-master/projects/languages/android/android-maven-emma .jenkins/jobs/${ジョブ名}/workspace
- "ビルド" を設定します。
- "ビルド環境" を設定します。
- "ビルド後の処理" から "Sonar" を選択し設定をします。-Dsonar.profile で Sonar 側で使用する品質プロファイルを指定しています。Sonar 側で "Android" 品質プロファイルをデフォルトに設定している場合、ここでの指定は不要です。また、pom.xml のプロパティで
タグによる指定も可能です。
- これでジョブの設定は終わりです。"保存" ボタンを押してジョブ設定を保存します。
- これでジョブの作成ができました。早速 "ビルドの実行" をクリックしてジョブを実行してみましょう。
ジョブが成功したら http://localhost:8080/sonar/ へアクセスして、テストのカバレッジ (網羅率) やソースの品質指標など見てみて下さい。Issues の Blocker や Critical は深刻な不具合や重大なルール違反の件数を表しています。各指標はクリックして深堀ことが可能です。また左側のメニューや画面上部のプルダウンから様々な切り口や時間軸で品質の分析が可能です。
※Sonar の画面キャプチャなどは Twitter アカウントの方へより具体的なのを張ってあるので、詳しく見たい方は @orangesignal へフォロー申請下さい。
かなり駆け足かつ色々端折りながらの説明となりましたがいかがだったでしょうか?
Jenkins + Android + SonarQube (旧 Sonar) で一歩進んだ自動テストな CI 環境を構築する パート1
Jenkins だけでなく Sonar も使用した場合のまとまった記事がないとなげきの声が散見されるので僕自身の備忘録も込めてセットアップ方法を簡単にまとめてみます。(例によって書き殴りとなるでしょう)
Maven+Jenkins+Sonar の環境構築手順については、若干古いバージョンの例ですが、Maven,Jenkins,Sonarの導入手順にまとまっているので、このエントリでは Android アプリ開発向けのナレッジを加味してその辺を含めてちょっと記載していこうと思います。
っとその前に Sonar て何?って方は コード探知機「Sonar」でプロジェクトの深海を探れ! を見てみて下さい。正直な話、テスト結果を含むコード品質については Jenkins だけでは役不足感が否めないので Sonar の導入を強くお勧めします。尚 Sonar の公式サイトではどのような情報が把握できるのか実際に デモ を見ることができるので参考にしてみて下さい。
これからインストールする主なソフトウェアは以下の通りです。
それでは早速解説して参ります。下記コマンド記載は思い出しつつ記載しているので間違っているところなどあるかもしれません。また${HOME}は適宜読替えて下さいませ。恐らく Mac でも Windows でも環境構築は可能ですがコマンド例は Linux となっていますのでプラットフォームが異なる場合は適宜読替えて下さい。
尚、tar 形式や zip 形式のミドルウェアを導入している理由は、バージョンアップや切り戻しの容易性のためです。また各ミドルウェアは基本的に $home 直下に配置するようにしています。これも管理の容易性のためです。(インストールの容易性よりもメンテの容易性の方が重要なので)
JDK
入手先:http://www.oracle.com/technetwork/java/javase/downloads/index.html
SonarQube (旧 Sonar) のシステム要件などから Oracle の JDK を使用します。このエントリでは紹介しませんが、Maven のリポジトリ管理に Sonatype Nexus を使いたいとかあるかもしれないので、最新の JDK7 の使用をおすすめします。Jenkins で Android プロジェクトのビルドには JDK6 を使いたい!! というこだわりがあれば JDK7 と JDK6 の両方を入れて下さい。尚、JRE ではなく JDK を使用するのは Android SDK などいくつかのプロダクトが JDK を要求しているのと、ケースバイケースで JRE / JDK を使い分ける説明が混乱を招く恐れがあることからです。
$ tar zxvf jdk-7u25-linux-x64.tar.gz
$ mv jdk1.7.0_25 ${HOME}
$ rm -f jdk-7u25-linux-x64.tar.gz
※注:JAVA_HOME は Tomcat の setenv.sh や Jenkins で設定するので .bashrc や .bash_profile には書かずに進めます。
Tomcat
入手先:http://tomcat.apache.org/download-70.cgi
システム要件から Jetty か Tomcat を使用することになるかと思います。このエントリでは Tomcat を導入します。執筆時点の最新は 7.0.42 です。
$ cd $home
$ wget http://www.us.apache.org/dist/tomcat/tomcat-7/v7.0.42/bin/apache-tomcat-7.0.42.tar.gz
$ tar zxvf apache-tomcat-7.0.42.tar.gz
$ rm -f apache-tomcat-7.0.42.tar.gz
Tomcat 起動時の環境設定用ファイル setenv.sh (Windows の場合は setenv.bat) を作成して、JAVA_HOME の設定と CATALINA_OPTS で server モードの指定をします。
vi ${HOME}/apache-tomcat-7.0.42/bin/setenv.sh #!/bin/sh # JDK7を指定して下さい。 export JAVA_HOME=${HOME}/jdk1.7.0_25 CATALINA_OPTS="-server" CATALINA_OPTS="${CATALINA_OPTS} -Djava.awt.headless=true" CATALINA_OPTS="${CATALINA_OPTS} -Dorg.apache.jasper.runtime.BodyContentImpl.LIMIT_BUFFER=true" # 以下は例なので環境に合わせて適宜変更して下さい # Sonar を入れるので PermSize は 256m 以上取って下さい。 CATALINA_OPTS="${CATALINA_OPTS} -XX:PermSize=256m" CATALINA_OPTS="${CATALINA_OPTS} -XX:MaxPermSize=256m" # Sonar のシステム要件。512m 以上 CATALINA_OPTS="${CATALINA_OPTS} -Xms512m" CATALINA_OPTS="${CATALINA_OPTS} -Xmx768m" CATALINA_OPTS="${CATALINA_OPTS} -XX:NewRatio=2" CATALINA_OPTS="${CATALINA_OPTS} -XX:+UseConcMarkSweepGC" CATALINA_OPTS="${CATALINA_OPTS} -XX:+CMSParallelRemarkEnabled" CATALINA_OPTS="${CATALINA_OPTS} -XX:+UseParNewGC" CATALINA_OPTS="${CATALINA_OPTS} -XX:MaxTenuringThreshold=32" CATALINA_OPTS="${CATALINA_OPTS} -XX:SurvivorRatio=8" CATALINA_OPTS="${CATALINA_OPTS} -XX:TargetSurvivorRatio=90" CATALINA_OPTS="${CATALINA_OPTS} -XX:+DisableExplicitGC" export CATALINA_OPTS
Jenkins を導入した際に Tomcat へ URIEncoding="UTF-8" の指定をしろと警告がでるので、先に HTTP コネクタ (8080) に同設定を追加します。また Apache 連携しないので AJP コネクタ (8009) をコメントアウトします。
vi ${HOME}/apache-tomcat-7.0.42/conf/server.xml :中略 <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> :中略 <!-- <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> --> :中略
ここで1度 Tomcat が起動できることを確認しておきます。
$ cd ${HOME}/apache-tomcat-7.0.42/bin
$ ./startup.sh
$ # tail コマンドでログを見て起動を確認 (Ctrl + C で抜けます)
$ tail -f ../logs/catalina.out
起動確認できたら Tomcat を停止します。
$ cd ${HOME}/apache-tomcat-7.0.42/bin
$ ./shutdown.sh
$ # ps コマンドで停止を確認
$ ps aux|grep java
Jenkins
入手先:http://jenkins-ci.org/
jenkins.war ファイルをダウンロードして Tomcat の webapps へコピーまたは移動します。
$ cd $home
$ wget http://mirrors.jenkins-ci.org/war/latest/jenkins.war
$ mv jenkins.war ${HOME}/apache-tomcat-7.0.42/webapps
Ant
入手先:http://ant.apache.org/bindownload.cgi
Jenkins で Ant が使えるように『一応』 Ant もインストールしておきます。※本連載では使いませんのでインストールは任意でかまいません。
$ cd $home
$ wget http://www.us.apache.org/dist//ant/binaries/apache-ant-1.9.2-bin.tar.gz
$ tar zxvf apache-ant-1.9.2-bin.tar.gz
$ rm -f apache-ant-1.9.2-bin.tar.gz
Maven
入手先:http://maven.apache.org/download.cgi
本構成の要である Maven をインストールします。執筆時点の最新は 3.1.0 です。
Jenkins は 1.526 から Sonar は 3.7 から Maven 3.1.0 に対応しましたが、Android のビルドに使う Maven のプラグインである android-maven-plugin (執筆時点の最新は 3.6.1) は Maven 3.1.0 に対応していないので Maven 3.0 系の最新である 3.0.5 を使います。
$ cd $home
$ wget http://www.us.apache.org/dist/maven/maven-3/3.0.5/binaries/apache-maven-3.0.5-bin.tar.gz
$ tar zxvf apache-maven-3.0.5-bin.tar.gz
$ rm -f apache-maven-3.0.5-bin.tar.gz
Android SDK
入手先:http://developer.android.com/sdk/index.html
SDK Tools Only の Android SDKをインストールして update sdk を実行します。※ライセンスを問い合わせられるので "y" を入力して進めて下さい。
$ cd $home
$ wget http://dl.google.com/android/android-sdk_r22.0.5-linux.tgz
$ tar zxvf android-sdk_r22.0.5-linux.tgz
$ rm -f android-sdk_r22.0.5-linux.tgz
$
$ cd android-sdk-linux/tools
$ ./android update sdk -u
SonarQube (旧 Sonar)
入手先:http://www.sonarqube.org/
執筆時点の最新である 3.7 を導入します。
$ cd $home
$ wget http://dist.sonar.codehaus.org/sonar-3.7.zip
$ unzip sonar-3.7.zip
次に Sonar の設定ファイルを編集します。Sonar も Tomcat 経由で使うので公式の手順通り以下の様に変更します。
※他にも接続するデータベースの設定変更などが可能です。デフォルトでは H2 という Sonar に組込み済のデータベースを使用する設定となっています。評価向けであれば、まずは H2 で問題ないです。
vi ${HOME}/sonar-3.7/conf/sonar.properties
:中略
sonar.web.host: localhost
sonar.web.port: 8080
sonar.web.context: /sonar
:中略
Sonar の設定変更が完了したら war ファイルをビルドします。
$ cd ${HOME}/sonar-3.7/war
$ ./build-war.sh
war ファイルが生成されたら Tomcat の webapps へコピーまたは移動します。
※Sonar の設定を変更したり JDBC ドライバを変更した場合は、再度 war をビルドして Tomcat へ配布して下さい。
$ mv ${HOME}/sonar-3.7/war/sonar.war ${HOME}/apache-tomcat-7.0.42/webapps
ここまでで土台となるミドルウェアのインストールは完了です。次からは各ミドルウェアのセットアップをして参ります。まずは Tomcat を起動して Jenkins や Sonar の管理画面へアクセスできるようにします。(Tomcat の起動方法は先の手順を参照して下さい)
尚、Sonar の初回起動時はデータベースのテーブル作成などが行われるため、Tomcat の catalina.out にそれらのログが出力されます。
Jenkins へプラグインをインストールする
- 利用可能なプラグイン画面を表示します。
- 一覧から "Android Emulator Plugin"、"Jenkins Sonar Plugin" にチェックを入れ、"再起動せずにインストール"ボタンを押下します。(※依存関係のある他のプラグインも一緒にダウンロードされます。)
- すべてのプラグインがダウンロードされたら Tomcat を再起動して下さい。
※必要があれば Subversion プラグインや Git プラグインなども入れて下さい。
Jenkins を設定する
- システムの設定画面を表示します。
- Android エミュレータを同時に使用できないので、"同時ビルド数"を "1" にします。
- "グローバル プロパティ"セクションで "環境変数" にチェックを入れ、以下の様に環境変数を設定します。
キー | ANDROID_HOME |
値 | (※Android SDKを配置した場所。例: /home/orangesignal/android-sdk-linux) |
Android SDK root | ${ANDROID_HOME} |
- JDKを設定します。※JDK6を使いたい場合はJDK6のパスや名前を設定して下さい。
名前 | jdk1.7.0_25 |
JAVA_HOME | (※JDKを配置した場所。例: /home/orangesignal/jdk1.7.0_25) |
自動インストール | オフ |
- Antを設定します。
名前 | apache-ant-1.9.2 |
ANT_HOME | (※Antを配置した場所。例: /home/orangesignal/apache-ant-1.9.2) |
自動インストール | オフ |
- Mavenを設定します。
名前 | apache-maven-3.0.5 |
MAVEN_HOME | (※Mavenを配置した場所。例 /home/orangesignal/apache-maven-3.0.5) |
自動インストール | オフ |
- "Sonar"セクションの"高度な設定"ボタンを押下してSonarプラグイン設定をすべて表示します。
- このセクションではSonraのsonar.propertiesの内容と整合するように値を入力します。
名前 | sonar (何でも良い) |
Server URL | http://localhost:8080/sonar |
Database URL | sonar.propertiesのsonar.jdbc.urlの値と同じ |
Database login | sonar.propertiesのsonar.jdbc.usernameの値と同じ |
Database password | sonar.propertiesのsonar.jdbc.passwordの値と同じ |
Database driver | H2の場合はブランクでOK、それ以外の場合は右の?を押下してヘルプ参照のこと |
Version of sonar-maven-plugin | 2.1 ※恐らくブランクでも問題ないが執筆時点での最新プラグインを使って欲しいので指定しています |
Sonar へプラグインをインストールする
- アップデートセンターの Available Plugins 画面を表示します。(※ログイン要求されたら admin/admin でログインして下さい)
Android 用の品質プロファイルを作成する
- プロファイル一覧にて上記で作成したプロファイル名のリンクをクリックします。
- プロファイルの継承 (inheritance) タブを押下します。
※必要があればコーディング規則タブで、標準では無効になっている PMD の Android 向けルールや、CheckStyle の Comment pattern matcher ルールなどを有効にして下さい。
※この Sonar 環境を Android 以外の Java プロジェクトで使用する予定がなければ、プロファイル画面で潔く"デフォルトに設定"をすることで pom.xml や Jenkins ジョブ側でプロファイルを指定する手間が減ります。
Android SDK を Maven のリポジトリへ登録する
GitHub の maven-android-sdk-deployer をダウンロードして、How to Use の手順通り Android SDK 一式を Maven のローカルリポジトリへ登録します。
$ cd $home
$ unzip maven-android-sdk-deployer-master.zip
$ cd maven-android-sdk-deployer-master
$ export ANDROID_HOME=${HOME}/android-sdk-linux
$ ${HOME}/apache-maven-3.0.5/bin/mvn install
$ rm -r maven-android-sdk-deployer-master
$ rm -f maven-android-sdk-deployer-master.zip
ミドルのセットアップはこれで準備完了です。Android 用プロジェクト& Jenkins ジョブ設定は次回へ
Android で OpenGL ES 2.0 事始め FBO 編 〜スーパーサンプル〜
前回のエントリから半年近く空いてしまいましたが2013年初のエントリとして「Android で OpenGL ES 2.0 事始め」の続編として、今回は Android の OpenGL ES 2.0 ネタではなぜか極端に紹介が少ないフレームバッファオブジェクト (FBO) を使ってオフスクリーンレンダリングをしてみたいと思います。
FBO 自体の解説についてはググってねということにしますが、OpenGL 1.x/OpenGL ES 1.x 系のサイトも結構引っかかるかと思うので、ちょっと区別できるようにフォローしておくと、
- FBO は OpenGL ES 1.x では拡張サポート。
- OpenGL ES 2.0 では標準で FBO 使用可能。
- iOS で OpenGL ES 2.0 を使用する場合は、FBO の使用が必須。
となっています。Android で OpenGL ES 2.0 を使用する場合は、FBO の使用は必須ではないですが、デフォルトのフレームバッファへ直接描画を続けているとチラつきが発生してしまいますし、どの GPU ガイドにも FBO 使えと書いてあるので、実践的なアプリでは事実上必須となるはずです。
あとテクスチャサイズについてもフォローしておくと、
となっています。
さて、今回のソースは、
- FBO 管理クラス
- Shader 管理クラス
- FBO を使用する Renderer
- OpenGL ES 2.0 ユーティリティ
でポイントのみの差分ソースです。(※抜粋している為コンパイル通らないとかあれば前回、前々回など参照してみて下さい)
それではご覧下さい。
FBO 管理クラス
毎回書くのが面倒なので管理クラスにしてみました。後々この FBO のテクスチャへ描画して使うことになります。ちなみにテクスチャへの描画が終わったからと言ってフレームバッファとレンダーバッファを削除すると紐付いていたテクスチャを入力として使用した場合に正しく描画できない GPU があるので、このクラスではフレームバッファ、レンダーバッファ、テクスチャの全部を保持するか全部破棄するかにしています。また、FBO テクスチャのフォーマットは GL_RGBA しか許容しない GPU がありますが今のところ特に困らないので GL_RGBA 固定としています。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import android.annotation.TargetApi; import android.opengl.GLES20; import android.os.Build; /** * オフスクリーン描画用の OpenGL ES 2.0 のフレームバッファオブジェクト管理クラスを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.FROYO) public class GLES20FramebufferObject { /** * 幅を保持します。 */ private int mWidth; /** * 高さを保持します。 */ private int mHeight; /** * フレームバッファ識別子を保持します。 */ private int mFramebufferName; /** * レンダーバッファ識別子を保持します。 */ private int mRenderbufferName; /** * テクスチャ識別子を保持します。 */ private int mTexName; ////////////////////////////////////////////////////////////////////////// // セッター / ゲッター /** * 幅を返します。 * * @return 幅 */ public int getWidth() { return mWidth; } /** * 高さを返します。 * * @return 高さ */ public int getHeight() { return mHeight; } /** * テクスチャ識別子を返します。 * * @return テクスチャ識別子 */ public int getTexName() { return mTexName; } ////////////////////////////////////////////////////////////////////////// // パブリック メソッド /** * 指定された幅と高さでフレームバッファオブジェクト (FBO) を構成します。<p> * 既にフレームバッファオブジェクト (FBO) が構成されている場合は、 * 現在のフレームバッファオブジェクト (FBO) を削除して新しいフレームバッファオブジェクト (FBO) を構成します。 * * @param width 幅 * @param height 高さ * @throws RuntimeException フレームバッファの構成に失敗した場合。 */ public void setup(final int width, final int height) { // 現在のフレームバッファオブジェクトを削除します。 release(); try { mWidth = width; mHeight = height; final int[] args = new int[1]; // フレームバッファ識別子を生成します。 GLES20.glGenFramebuffers(args.length, args, 0); mFramebufferName = args[0]; // フレームバッファ識別子に対応したフレームバッファオブジェクトを生成します。 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferName); // レンダーバッファ識別子を生成します。 GLES20.glGenRenderbuffers(args.length, args, 0); mRenderbufferName = args[0]; // レンダーバッファ識別子に対応したレンダーバッファオブジェクトを生成します。 GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mRenderbufferName); // レンダーバッファの幅と高さを指定します。 GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height); // フレームバッファのアタッチメントとしてレンダーバッファをアタッチします。 GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, mRenderbufferName); // Offscreen position framebuffer texture target GLES20.glGenTextures(args.length, args, 0); mTexName = args[0]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexName); GLES20Utils.setupSampler(GLES20.GL_TEXTURE_2D, GLES20.GL_LINEAR, GLES20.GL_NEAREST); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); // フレームバッファのアタッチメントとして 2D テクスチャをアタッチします。 GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mTexName, 0); // フレームバッファが完全かどうかチェックします。 final int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { throw new RuntimeException("Failed to initialize framebuffer object " + status); } } catch (final RuntimeException e) { release(); throw e; } } /** * クリーンアップを行います。 */ public void release() { GLES20.glDeleteTextures(1, new int[]{ mTexName }, 0); GLES20.glDeleteRenderbuffers(1, new int[]{ mRenderbufferName }, 0); GLES20.glDeleteFramebuffers(1, new int[]{ mFramebufferName }, 0); mTexName = 0; mRenderbufferName = 0; mFramebufferName = 0; } /** * このフレームバッファオブジェクトをバインドして有効にします。 */ public void enable() { GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferName); } }
Shader 管理クラス
シェーダープログラムのところは最近突っ込み受けましたし、こちらも glsl 変わるたびに毎回書くのは面倒なのでクラスにしてみました。さりげなく Vertex Buffer Object (VBO) とかのベストプラクティスも使ってますが、所詮は 2D イメージ描画用なので気持ちの問題かもしれません。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import java.util.HashMap; import android.annotation.TargetApi; import android.opengl.GLES20; import android.os.Build; /** * OpenGL ES 2.0 向けのシェーダーオブジェクト管理クラスを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.GINGERBREAD) public class GLES20Shader { /** * デフォルトのポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコードです。 */ protected static final String DEFAULT_VERTEX_SHADER = "attribute vec4 aPosition;\n" + "attribute vec4 aTextureCoord;\n" + "varying highp vec2 vTextureCoord;\n" + "void main() {\n" + "gl_Position = aPosition;\n" + "vTextureCoord = aTextureCoord.xy;\n" + "}\n"; /** * デフォルトの色描画用のピクセル/フラグメントシェーダのソースコードです。 */ protected static final String DEFAULT_FRAGMENT_SHADER = "precision mediump float;\n" + // 演算精度を指定します。 "varying highp vec2 vTextureCoord;\n" + "uniform lowp sampler2D sTexture;\n" + "void main() {\n" + "gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + "}\n"; /** * 頂点データとテクスチャ座標 (UV マッピング) の構造体配列形式データです。 */ private static final float[] VERTICES_DATA = new float[] { // X, Y, Z, U, V -1f, 1f, 0f, 0f, 1f, // 左上 1f, 1f, 0f, 1f, 1f, // 右上 -1f, -1f, 0f, 0f, 0f, // 左下 1f, -1f, 0f, 1f, 0f // 右下 }; private static final int FLOAT_SIZE_BYTES = 4; protected static final int VERTICES_DATA_POS_SIZE = 3; protected static final int VERTICES_DATA_UV_SIZE = 2; protected static final int VERTICES_DATA_STRIDE_BYTES = (VERTICES_DATA_POS_SIZE + VERTICES_DATA_UV_SIZE) * FLOAT_SIZE_BYTES; protected static final int VERTICES_DATA_POS_OFFSET = 0 * FLOAT_SIZE_BYTES; protected static final int VERTICES_DATA_UV_OFFSET = VERTICES_DATA_POS_OFFSET + VERTICES_DATA_POS_SIZE * FLOAT_SIZE_BYTES; ////////////////////////////////////////////////////////////////////////// /** * 頂点シェーダーのソースコードを保持します。 */ private final String mVertexShaderSource; /** * フラグメントシェーダーのソースコードを保持します。 */ private final String mFragmentShaderSource; /** * プログラム識別子を保持します。 */ private int mProgram; /** * 頂点シェーダーの識別子を保持します。 */ private int mVertexShader; /** * フラグメントシェーダーの識別子を保持します。 */ private int mFragmentShader; /** * 頂点バッファオブジェクト名を保持します。 */ private int mVertexBufferName; /** * 変数名とハンドル識別子のマッピングを保持します。 */ private final HashMap<String, Integer> mHandleMap = new HashMap<String, Integer>(); ////////////////////////////////////////////////////////////////////////// // コンストラクタ /** * デフォルトコンストラクタです。 */ public GLES20Shader() { this(DEFAULT_VERTEX_SHADER, DEFAULT_FRAGMENT_SHADER); } /** * シェーダーのソースコードを指定してこのクラスのインスタンスを構築するコンストラクタです。 * * @param vertexShaderSource ポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコード * @param fragmentShaderSource 色描画用のピクセル/フラグメントシェーダのソースコード */ public GLES20Shader(final String vertexShaderSource, final String fragmentShaderSource) { mVertexShaderSource = vertexShaderSource; mFragmentShaderSource = fragmentShaderSource; } ////////////////////////////////////////////////////////////////////////// /** * 指定された GLSL ソースコードをコンパイルしてプログラムオブジェクトを構成します。 */ public void setup() { release(); mVertexShader = GLES20Utils.loadShader(GLES20.GL_VERTEX_SHADER, mVertexShaderSource); mFragmentShader = GLES20Utils.loadShader(GLES20.GL_FRAGMENT_SHADER, mFragmentShaderSource); mProgram = GLES20Utils.createProgram(mVertexShader, mFragmentShader); mVertexBufferName = GLES20Utils.createBuffer(VERTICES_DATA); } /** * フレームサイズを指定します。 * * @param width フレームの幅 * @param height フレームの高さ */ public void setFrameSize(final int width, final int height) { } /** * このシェーダーオブジェクトの構成を破棄します。 */ public void release() { GLES20.glDeleteProgram(mProgram); GLES20.glDeleteShader(mVertexShader); GLES20.glDeleteShader(mFragmentShader); GLES20.glDeleteBuffers(1, new int[]{ mVertexBufferName }, 0); mProgram = 0; mVertexShader = 0; mFragmentShader = 0; mVertexBufferName = 0; mHandleMap.clear(); } /** * 指定されたテクスチャ識別子を入力データとして描画します。 * * @param texName テクスチャ識別子 */ public void draw(final int texName) { useProgram(); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVertexBufferName); GLES20.glEnableVertexAttribArray(getHandle("aPosition")); GLES20.glVertexAttribPointer(getHandle("aPosition"), VERTICES_DATA_POS_SIZE, GLES20.GL_FLOAT, false, VERTICES_DATA_STRIDE_BYTES, VERTICES_DATA_POS_OFFSET); GLES20.glEnableVertexAttribArray(getHandle("aTextureCoord")); GLES20.glVertexAttribPointer(getHandle("aTextureCoord"), VERTICES_DATA_UV_SIZE, GLES20.GL_FLOAT, false, VERTICES_DATA_STRIDE_BYTES, VERTICES_DATA_UV_OFFSET); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texName); GLES20.glUniform1i(getHandle("sTexture"), 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisableVertexAttribArray(getHandle("aPosition")); GLES20.glDisableVertexAttribArray(getHandle("aTextureCoord")); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } ////////////////////////////////////////////////////////////////////////// /** * プログラムを有効にします。 */ protected final void useProgram() { GLES20.glUseProgram(mProgram); } /** * 頂点バッファオブジェクトの識別子を返します。 * * @return 頂点バッファオブジェクトの識別子。または {@code 0} */ protected final int getVertexBufferName() { return mVertexBufferName; } /** * 指定された変数のハンドルを返します。 * * @param name 変数 * @return 変数のハンドル */ protected final int getHandle(final String name) { final Integer value = mHandleMap.get(name); if (value != null) { return value.intValue(); } int location = GLES20.glGetAttribLocation(mProgram, name); if (location == -1) { location = GLES20.glGetUniformLocation(mProgram, name); } if (location == -1) { throw new IllegalStateException("Could not get attrib or uniform location for " + name); } mHandleMap.put(name, Integer.valueOf(location)); return location; } }
FBO を使用する Renderer
今回一番肝となるクラスです。やってることは大したことなく FBO とシステムデフォルトのフレームバッファを切り替えつつ描画するのみです。今回のサンプルではこのクラスのサブクラスをつけませんので、目視で確認したい場合お手数ですがサブクラスを作り前回のサンプルなどを流用して何か描画してみて下さい。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.annotation.TargetApi; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.os.Build; /** * OpenGL ES 2.0 のフレームバッファオブジェクトによる * オフスクリーンレンダリングをサポートした {@link GLSurfaceView.Renderer} の基底クラスを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.FROYO) public abstract class GLES20FramebufferObjectRenderer implements GLSurfaceView.Renderer { /** * オフスクリーン描画用のフレームバッファオブジェクトを保持します。 */ private GLES20FramebufferObject mFramebufferObject; /** * オンスクリーン描画用の GLSL シェーダーオブジェクトを保持します。 */ private GLES20Shader mShader; ////////////////////////////////////////////////////////////////////////// // オーバーライドメソッド /** * 実装は、オフスクリーン描画用のフレームバッファオブジェクトとオンスクリーン描画用のシェーダーオブジェクトを初期化した後に、{@link #onSurfaceCreated(EGLConfig)} を呼び出します。 */ @Override public final void onSurfaceCreated(final GL10 gl, final EGLConfig config) { mFramebufferObject = new GLES20FramebufferObject(); mShader = new GLES20Shader(); mShader.setup(); onSurfaceCreated(config); } /** * 実装は、オフスクリーン描画用のフレームバッファオブジェクトを構成または再構成した後に、{@link #onSurfaceChanged(int, int)} を呼び出します。 */ @Override public final void onSurfaceChanged(final GL10 gl, final int width, final int height) { mFramebufferObject.setup(width, height); mShader.setFrameSize(width, height); onSurfaceChanged(width, height); } /** * 実装はオフスクリーン描画用のフレームバッファオブジェクトを有効にした後に、{@link #onDrawFrame(GLES20FramebufferObject)} を呼び出します。 * その後、フレームバッファオブジェクトの内容をウィンドウシステムが提供するデフォルトのフレームバッファへ描画します。 */ @Override public final void onDrawFrame(final GL10 gl) { //////////////////////////////////////////////////////////// // オフスクリーンレンダリング // FBO へ切り替えます。 mFramebufferObject.enable(); GLES20.glViewport(0, 0, mFramebufferObject.getWidth(), mFramebufferObject.getHeight()); // オフスクリーン描画を行います。 onDrawFrame(mFramebufferObject); //////////////////////////////////////////////////////////// // オンスクリーンレンダリング // ウィンドウシステムが提供するフレームバッファへ切り替えます。 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glViewport(0, 0, mFramebufferObject.getWidth(), mFramebufferObject.getHeight()); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); mShader.draw(mFramebufferObject.getTexName()); } ////////////////////////////////////////////////////////////////////////// /** * Called when the surface is created or recreated. * <p> * Called when the rendering thread * starts and whenever the EGL context is lost. The context will typically * be lost when the Android device awakes after going to sleep. * <p> * Since this method is called at the beginning of rendering, as well as * every time the EGL context is lost, this method is a convenient place to put * code to create resources that need to be created when the rendering * starts, and that need to be recreated when the EGL context is lost. * Textures are an example of a resource that you might want to create * here. * <p> * Note that when the EGL context is lost, all OpenGL resources associated * with that context will be automatically deleted. You do not need to call * the corresponding "glDelete" methods such as glDeleteTextures to * manually delete these lost resources. * <p> * * @param config the EGLConfig of the created surface. Can be used * to create matching pbuffers. */ public abstract void onSurfaceCreated(EGLConfig config); /** * Called when the surface changed size. * <p> * Called after the surface is created and whenever * the OpenGL ES surface size changes. * <p> * Typically you will set your viewport here. If your camera * is fixed then you could also set your projection matrix here: * <pre class="prettyprint"> * void onSurfaceChanged(int width, int height) { * // for a fixed camera, set the projection too * float ratio = (float) width / height; * Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7); * } * </pre> * @param width * @param height */ public abstract void onSurfaceChanged(int width, int height); /** * Called to draw the current frame.<p> * This method is responsible for drawing the current frame.<p> * The implementation of this method typically looks like this: * <pre> * void onDrawFrame(GLES20FramebufferObject fbo) { * GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); * //... other gl calls to render the scene ... * } * </pre> * @param fbo オフスクリーン描画用のフレームバッファオブジェクト */ public abstract void onDrawFrame(GLES20FramebufferObject fbo); }
OpenGL ES 2.0 ユーティリティ
前回からあまり変更ないですが、setupSampler や createBuffer メソッドなどを追加しています。
/* * Copyright (c) 2012-2013 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.opengl.GLES20; import android.opengl.GLException; import android.os.Build; import android.util.Log; import com.orangesignal.android.camera.preview.gl.BuildConfig; /** * OpenGL ES 2.0 に関するユーティリティを提供します。 * * @author 杉澤 浩二 */ @TargetApi(Build.VERSION_CODES.FROYO) public final class GLES20Utils { : 中略 : /** * サンプラーを構成します。 * * @param target * @param mag GL_TEXTURE_MAG_FILTER * @param min GL_TEXTURE_MIN_FILTER */ public static void setupSampler(final int target, final int mag, final int min) { // テクスチャを拡大/縮小する方法を設定します。 GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, mag); // 拡大するときピクセルの中心付近の線形で補完 GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, min); // 縮小するときピクセルの中心に最も近いテクスチャ要素で補完 // テクスチャの繰り返し方法を設定します。 GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); } private static final int FLOAT_SIZE_BYTES = 4; /** * 指定されたプリミティブ型配列のバッファーデータから {@link FloatBuffer} を作成して返します。 * * @param array バッファーデータ * @return 作成された {@link FloatBuffer} */ public static FloatBuffer toFloatBuffer(final float[] data) { final FloatBuffer buffer = ByteBuffer .allocateDirect(data.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); buffer.put(data).position(0); return buffer; } /** * 指定されたデータでバッファオブジェクトを新規に作成します。 * * @param data データ * @return バッファオブジェクト名 */ @TargetApi(Build.VERSION_CODES.FROYO) public static int createBuffer(final float[] data) { return createBuffer(toFloatBuffer(data)); } /** * 指定されたデータでバッファオブジェクトを新規に作成します。 * * @param data データ * @return バッファオブジェクト名 */ public static int createBuffer(final FloatBuffer data) { final int[] buffers = new int[1]; GLES20.glGenBuffers(buffers.length, buffers, 0); updateBufferData(buffers[0], data); return buffers[0]; } /** * 指定されたバッファオブジェクト名を指定されたデータで更新します。 * * @param bufferName バッファオブジェクト名 * @param data 更新するデータ */ public static void updateBufferData(final int bufferName, final float[] data) { updateBufferData(bufferName, toFloatBuffer(data)); } /** * 指定されたバッファオブジェクト名を指定されたデータで更新します。 * * @param bufferName バッファオブジェクト名 * @param data 更新するデータ */ public static void updateBufferData(final int bufferName, final FloatBuffer data) { GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferName); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, data.capacity() * FLOAT_SIZE_BYTES, data, GLES20.GL_STATIC_DRAW); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); } }
以上が今回のサンプルです。
これで画面がチラつかない描画ができるようになりました。
蛇足
FBO を何枚か使い描画する場合は入力と出力の FBO (正確にはテクスチャ) を同じ物にすると正しく描画できない GPU があるので必ず別々の物を使う必要があります。