Android用OpenGLチュートリアル:マンデルブロ集合ジェネレーターの構築
公開: 2022-03-11OpenGLは強力なクロスプラットフォームAPIであり、さまざまなプログラミング環境でシステムのハードウェアに非常に密接にアクセスできます。
それで、なぜあなたはそれを使うべきですか?
2Dと3Dの両方のグラフィックスに非常に低レベルの処理を提供します。 一般に、これにより、インタプリタまたは高級プログラミング言語が原因で発生する問題を回避できます。 ただし、さらに重要なのは、主要な機能であるGPUへのハードウェアレベルのアクセスも提供することです。
GPUは多くのアプリケーションを大幅に高速化できますが、コンピューターでは非常に特殊な役割を果たします。 GPUコアは実際にはCPUコアよりも低速です。 特にシリアルで同時アクティビティのないプログラムを実行する場合、GPUコアではCPUコアよりもほとんどの場合低速になります。 主な違いは、GPUが大規模な並列処理をサポートしていることです。 一度に数百のコアで効果的に実行されるシェーダーと呼ばれる小さなプログラムを作成できます。 これは、他の方法では信じられないほど反復的なタスクを実行し、それらを同時に実行できることを意味します。
この記事では、OpenGLを使用してコンテンツを画面にレンダリングする単純なAndroidアプリケーションを構築します。 始める前に、Androidアプリケーションの作成とCのようなプログラミング言語の構文に関する知識をすでに理解していることが重要です。 このチュートリアルのソースコード全体は、GitHubで入手できます。
OpenGLチュートリアルとAndroid
OpenGLの能力を実証するために、Androidデバイス用の比較的基本的なアプリケーションを作成します。 現在、Android上のOpenGLは、OpenGL for Embedded Systems(OpenGL ES)と呼ばれるサブセットの下で配布されています。 基本的に、これはOpenGLの簡略版と考えることができますが、必要なコア機能は引き続き利用できます。
基本的な「HelloWorld」を作成する代わりに、マンデルブロ集合ジェネレーターという一見単純なアプリケーションを作成します。 マンデルブロ集合は、複素数の分野に基づいています。 複雑な分析は美しく広大な分野であるため、その背後にある実際の数学よりも視覚的な結果に焦点を当てます。
バージョンサポート
アプリケーションを作成するときは、適切なOpenGLサポートを備えたユーザーにのみ配布されるようにする必要があります。 マニフェスト宣言とアプリケーションの間で、マニフェストファイルでOpenGL2.0の使用を宣言することから始めます。
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
この時点で、OpenGL2.0のサポートはどこにでもあります。 OpenGL 3.0と3.1は互換性が増していますが、どちらかを書くとデバイスの約65%が除外されるため、追加機能が必要であることが確実な場合にのみ決定してください。 これらは、バージョンをそれぞれ「0x000300000」および「0x000300001」に設定することで実装できます。
アプリケーションアーキテクチャ
このOpenGLアプリケーションをAndroidで作成する場合、通常、サーフェスの描画に使用される3つの主要なクラスがありますMainActivity
、GLSurfaceViewの拡張、およびGLSurfaceView
の実装GLSurfaceView.Renderer
。 そこから、図面をカプセル化するさまざまなモデルを作成します。
この例ではMainActivity
と呼ばれるFractalGenerator
は、基本的にGLSurfaceView
をインスタンス化し、グローバルな変更をすべてルーティングします。 基本的にボイラープレートコードになる例を次に示します。
public class FractalGenerator extends Activity { private GLSurfaceView mGLView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Create and set GLSurfaceView mGLView = new FractalSurfaceView(this); setContentView(mGLView); } //[...] @Override protected void onPause() { super.onPause(); mGLView.onPause(); } @Override protected void onResume() { super.onResume(); mGLView.onResume(); } }
これは、他のアクティビティレベルモディファイア(没入型フルスクリーンなど)を配置するクラスにもなります。
1つ深いクラスでは、 GLSurfaceView
の拡張機能があり、これがプライマリビューとして機能します。 このクラスでは、バージョンを設定し、レンダラーを設定し、タッチイベントを制御します。 コンストラクターでは、 setEGLContextClientVersion(int version)
を使用してOpenGLバージョンを設定し、レンダラーを作成して設定するだけで済みます。
public FractalSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mRenderer = new FractalRenderer(); setRenderer(mRenderer); }
さらに、 setRenderMode(int renderMode)
を使用してレンダリングモードなどの属性を設定できます。 マンデルブロ集合の生成には非常にコストがかかる可能性があるため、 RENDERMODE_WHEN_DIRTY
を使用します。これは、初期化時とrequestRender()
への明示的な呼び出し時にのみシーンをレンダリングします。 設定のその他のオプションは、 GLSurfaceView
にあります。
コンストラクターを作成したら、少なくとも1つの他のメソッドonTouchEvent(MotionEvent event)
をオーバーライドする必要があります。これは、一般的なタッチベースのユーザー入力に使用できます。 レッスンの主な焦点ではないので、ここではあまり詳しく説明しません。
最後に、レンダラーに行きます。レンダラーでは、照明やシーンの変更のほとんどの作業が行われます。 まず、グラフィックスの世界でマトリックスがどのように機能し、動作するかを少し調べる必要があります。
線形代数のクイックレッスン
OpenGLは、マトリックスの使用に大きく依存しています。 行列は、座標の一般化された変更のシーケンスを表すための非常にコンパクトな方法です。 通常、これらを使用すると、任意の回転、拡張/収縮、および反射を行うことができますが、少し精巧に行うことで、平行移動も行うことができます。 基本的に、これはすべて、カメラの移動やオブジェクトの拡大など、必要な合理的な変更を簡単に実行できることを意味します。 行列に座標を表すベクトルを乗算することで、新しい座標系を効果的に生成できます。
OpenGLが提供するMatrixクラスは、必要な行列を計算するための既成の方法をいくつか提供しますが、単純な変換を使用する場合でも、それらがどのように機能するかを理解することは賢明なアイデアです。
まず、座標を処理するために4次元のベクトルと行列を使用する理由を説明します。 これは実際には、変換を実行できるように座標の使用を微調整するという考えに戻ります。3D空間での変換は、3次元だけでは不可能ですが、4次元を追加すると機能が有効になります。
これを説明するために、非常に基本的な一般的なスケール/変換行列を使用できます。
重要な注意として、OpenGLマトリックスは列単位であるため、このマトリックスは{a, 0, 0, 0, 0, b, 0, 0, 0, 0, c, 0, v_a, v_b, v_c, 1}
、これは通常の読み取り方法に垂直です。 これは、乗算で列として表示されるベクトルが行列と同じ形式になるようにすることで合理化できます。
コードに戻る
このマトリックスの知識があれば、レンダラーの設計に戻ることができます。 通常、このクラスには、Model、View、およびProjectionの3つの行列の積から形成される行列を作成します。 これは、適切には、MVPMatrixと呼ばれます。 より基本的な変換セットを使用するため、ここで詳細を学ぶことができます。マンデルブロ集合は2次元のフルスクリーンモデルであり、実際にはカメラのアイデアを必要としません。
まず、クラスを設定しましょう。 レンダラーインターフェイスに必要なメソッドonSurfaceCreated(GL10 gl, EGLConfig config)
、 onSurfaceChanged(GL10 gl, int width, int height)
、およびonDrawFrame(GL10 gl)
を実装する必要があります。 フルクラスは次のようになります。
public class FractalRenderer implements GLSurfaceView.Renderer { //Provide a tag for logging errors private static final String TAG = "FractalRenderer"; //Create all models private Fractal mFractal; //Transformation matrices private final float[] mMVPMatrix = new float[16]; //Any other private variables needed for transformations @Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { // Set the background frame color GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //Instantiate all models mFractal = new Fractal(); } @Override public void onDrawFrame(GL10 unused) { //Clear the frame of any color information or depth information GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); //Create a basic scale/translate matrix float[] mMVPMatrix = new float[]{ -1.0f/mZoom, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f/(mZoom*mRatio), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, -mX, -mY, 0.0f, 1.0f}; //Pass the draw command down the line to all models, giving access to the transformation matrix mFractal.draw(mMVPMatrix); } @Override public void onSurfaceChanged(GL10 unused, int width, int height) { //Create the viewport as being fullscreen GLES20.glViewport(0, 0, width, height); //Change any projection matrices to reflect changes in screen orientation } //Other public access methods for transformations }
提供されているコードでは、デバッグとシェーダーの使用を支援するために、 checkGLError
とloadShaders
の2つのユーティリティメソッドも使用されています。
このすべてにおいて、プログラムのさまざまな部分をカプセル化するために、コマンドのチェーンを次の行に渡し続けます。 理論的な変更を加える方法ではなく、プログラムが実際に行うことを記述できるようになりました。 これを行うときは、シーン内の特定のオブジェクトに対して表示する必要のある情報を含むモデルクラスを作成する必要があります。 複雑な3Dシーンでは、これは動物ややかんの場合がありますが、はるかに単純な2Dの例としてフラクタルを実行します。
モデルクラスでは、クラス全体を記述します。使用する必要のあるスーパークラスはありません。 必要なのは、コンストラクターと、のパラメーターを受け取るある種の描画メソッドだけです。
とはいえ、本質的に定型的なものである必要がある変数はまだたくさんあります。 Fractalクラスで使用される正確なコンストラクターを見てみましょう。
public Fractal() { // initialize vertex byte buffer for shape coordinates ByteBuffer bb = ByteBuffer.allocateDirect( // (# of coordinate values * 4 bytes per float) squareCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(squareCoords); vertexBuffer.position(0); // initialize byte buffer for the draw list ByteBuffer dlb = ByteBuffer.allocateDirect( // (# of coordinate values * 2 bytes per short) drawOrder.length * 2); dlb.order(ByteOrder.nativeOrder()); drawListBuffer = dlb.asShortBuffer(); drawListBuffer.put(drawOrder); drawListBuffer.position(0); // Prepare shaders int vertexShader = FractalRenderer.loadShader( GLES20.GL_VERTEX_SHADER, vertexShaderCode); int fragmentShader = FractalRenderer.loadShader( GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); // create empty OpenGL Program mProgram = GLES20.glCreateProgram(); // add the vertex shader to program GLES20.glAttachShader(mProgram, vertexShader); // add the fragment shader to program GLES20.glAttachShader(mProgram, fragmentShader); // create OpenGL program executables GLES20.glLinkProgram(mProgram); }
かなり一口ですね。 幸いなことに、これはプログラムの一部であり、モデルの名前を保存して、まったく変更する必要はありません。 クラス変数を適切に変更すれば、これは基本的な形状でうまく機能するはずです。

これの一部を議論するために、いくつかの変数宣言を見てみましょう:
static float squareCoords[] = { -1.0f, 1.0f, 0.0f, // top left -1.0f, -1.0f, 0.0f, // bottom left 1.0f, -1.0f, 0.0f, // bottom right 1.0f, 1.0f, 0.0f }; // top right private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
squareCoords
では、正方形のすべての座標を指定します。 画面上のすべての座標は、左下に(-1,-1)
、右上に(1,1)
のグリッドとして表されていることに注意してください。
drawOrder
では、正方形を構成する三角形に基づいて座標の順序を指定します。 特に一貫性と速度のために、OpenGLはすべてのサーフェスを表すために三角形を使用します。 正方形を作成するには、対角線(この場合は0
から2
)を切り取って、2つの三角形を作成します。
これらの両方をプログラムに追加するには、最初にそれらを生のバイトバッファーに変換して、配列の内容をOpenGLインターフェイスと直接インターフェイスさせる必要があります。 Javaは、OpenGLの実装が使用するポインタベースのC配列と直接互換性のない追加情報を含むオブジェクトとして配列を格納します。 これを修正するために、配列のrawメモリへのアクセスを格納するByteBuffers
が使用されます。
頂点と描画順序のデータを入力したら、シェーダーを作成する必要があります。
シェーダー
モデルを作成するときは、Vertex ShaderとFragment(Pixel)Shaderの2つのシェーダーを作成する必要があります。 すべてのシェーダーはGLシェーディング言語(GLSL)で記述されています。これは、Cベースの言語であり、多数の組み込み関数、変数修飾子、プリミティブ、およびデフォルトの入出力が追加されています。 Androidでは、これらは、レンダラーの2つのリソースメソッドの1つであるloadShader(int type, String shaderCode)
を介して最終的な文字列として渡されます。 まず、さまざまなタイプの修飾子について説明します。
-
const
:任意のfinal変数を定数として宣言できるため、簡単にアクセスできるようにその値を格納できます。 πのような数値は、シェーダー全体で頻繁に使用される場合、定数として宣言できます。 実装によっては、コンパイラが変更されていない値を定数として自動的に宣言する可能性があります。 -
uniform
:均一変数は、単一のレンダリングに対して定数として宣言されている変数です。 これらは基本的に、シェーダーへの静的引数として使用されます。 -
varying
:変数が可変として宣言され、頂点シェーダーに設定されている場合、フラグメントシェーダーで線形補間されます。 これは、あらゆる種類の色のグラデーションを作成するのに役立ち、深さの変更に対して暗黙的に行われます。 -
attribute
:属性は、シェーダーへの非静的引数と考えることができます。 これらは、頂点固有であり、頂点シェーダーにのみ表示される入力のセットを示します。
さらに、追加された他の2つのタイプのプリミティブについて説明する必要があります。
-
vec2
、vec3
、vec4
:指定された次元の浮動小数点ベクトル。 -
mat2
、mat3
、mat4
:指定された次元の浮動小数点行列。
ベクトルには、そのコンポーネントx
、 y
、 z
、およびw
またはr
、 g
、 b
、およびa
からアクセスできます。 また、複数のインデックスを持つ任意のサイズのベクトルを生成できますvec3 a
場合、 a.xxyz
は対応するaの値をa
vec4
を返します。
行列とベクトルは配列としてインデックス付けすることもでき、行列は1つのコンポーネントのみを含むベクトルを返します。 これは、 mat2 matrix
の場合、 matrix[0].a
が有効であり、 matrix[0][0]
を返すことを意味します。 これらを操作するときは、オブジェクトではなくプリミティブとして機能することを忘れないでください。 たとえば、次のコードについて考えてみます。
vec2 a = vec2(1.0,1.0); vec2 b = a; bx=2.0;
これによりa=vec2(1.0,1.0)
およびb=vec2(2.0,1.0)
が残ります。これは、オブジェクトの動作から期待されるものではなく、2行目でb
にaへa
ポインターが与えられます。
マンデルブロ集合では、コードの大部分はフラグメントシェーダーにあります。これは、すべてのピクセルで実行されるシェーダーです。 名目上、頂点シェーダーは、色や深さの変更など、頂点ごとに設定される属性を含め、すべての頂点で機能します。 フラクタル用の非常に単純な頂点シェーダーを見てみましょう。
private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}";
この場合、 gl_Position
は、頂点の座標を記録するためにOpenGLによって定義された出力変数です。 この場合、 gl_Position
を設定した各頂点の位置を渡します。 ほとんどのアプリケーションでは、 MVPMatrix
にvPosition
を掛けて頂点を変換しますが、フラクタルは常にフルスクリーンである必要があります。 すべての変換は、ローカル座標系で実行されます。
フラグメントシェーダーは、セットを生成するためにほとんどの作業が行われる場所になります。 fragmentShaderCode
を次のように設定します。
precision highp float; uniform mat4 uMVPMatrix; void main() { //Scale point by input transformation matrix vec2 p = (uMVPMatrix * vec4(gl_PointCoord,0,1)).xy; vec2 c = p; //Set default color to HSV value for black vec3 color=vec3(0.0,0.0,0.0); //Max number of iterations will arbitrarily be defined as 100. Finer detail with more computation will be found for larger values. for(int i=0;i<100;i++){ //Perform complex number arithmetic p= vec2(px*px-py*py,2.0*px*py)+c; if (dot(p,p)>4.0){ //The point, c, is not part of the set, so smoothly color it. colorRegulator increases linearly by 1 for every extra step it takes to break free. float colorRegulator = float(i-1)-log(((log(dot(p,p)))/log(2.0)))/log(2.0); //This is a coloring algorithm I found to be appealing. Written in HSV, many functions will work. color = vec3(0.95 + .012*colorRegulator , 1.0, .2+.4*(1.0+sin(.3*colorRegulator))); break; } } //Change color from HSV to RGB. Algorithm from https://gist.github.com/patriciogonzalezvivo/114c1653de9e3da6e1e3 vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 m = abs(fract(color.xxx + K.xyz) * 6.0 - K.www); gl_FragColor.rgb = color.z * mix(K.xxx, clamp(m - K.xxx, 0.0, 1.0), color.y); gl_FragColor.a=1.0; }
コードの多くは、セットがどのように機能するかについての数学とアルゴリズムにすぎません。 fract
、 abs
、 mix
、 sin
、 clamp
などのいくつかの組み込み関数の使用に注意してください。これらはすべてベクトルまたはスカラーで動作し、ベクトルまたはスカラーを返します。 さらに、ベクトル引数を取り、スカラーを返すdot
が使用されます。
シェーダーを使用できるように設定したので、最後のステップとして、モデルにdraw
関数を実装します。
public void draw(float[] mvpMatrix) { // Add program to OpenGL environment GLES20.glUseProgram(mProgram); // get handle to vertex shader's vPosition member mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); //Pass uniform transformation matrix to shader GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0); //Add attribute array of vertices GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer( mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer); // Draw the square GLES20.glDrawElements( GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer); // Disable vertex array GLES20.glDisableVertexAttribArray(mPositionHandle); FractalRenderer.checkGlError("Test"); }
この関数は、 uniform
変換行列やattribute
の位置など、すべての引数をシェーダーに渡します。
プログラムのすべての部分を組み立てた後、最終的に実行することができます。 適切なタッチサポートが処理されると、絶対に魅惑的なシーンがペイントされます。
浮動小数点の精度
もう少しズームインすると、画像の内訳に気づき始めます。
これは、その背後にあるセットの計算とはまったく関係がなく、OpenGLでの数値の格納方法と処理方法とはすべて関係があります。 最近ではdouble
がサポートされていますが、OpenGL2.0はfloat
以外のものをネイティブにサポートしていません。 シェーダーの高精度precision highp float
で利用可能な最高精度のフロートとして特別に指定しましたが、それでも十分ではありません。
この問題を回避するための唯一の方法は、2つのfloat
を使用してdouble
をエミュレートすることです。 この方法は、実際には、ネイティブに実装された方法の実際の精度の1桁以内に収まりますが、速度にはかなり厳しいコストがかかります。 より高いレベルの精度が必要な場合、これは読者の練習問題として残されます。
結論
いくつかのサポートクラスを使用すると、OpenGLは複雑なシーンのリアルタイムレンダリングをすばやく維持できます。 GLSurfaceView
で構成されるレイアウトの作成、そのRenderer
の設定、およびシェーダーを使用したモデルの作成はすべて、美しい数学的構造の視覚化につながります。 OpenGLESアプリケーションの開発に多くの関心を持っていただければ幸いです。