Android için OpenGL Eğitimi: Bir Mandelbrot Kümesi Oluşturucu Oluşturma
Yayınlanan: 2022-03-11OpenGL, çeşitli programlama ortamlarında sistem donanımına çok yakın erişime izin veren güçlü bir platformlar arası API'dir.
Peki, neden kullanmalısınız?
Hem 2D hem de 3D grafikler için çok düşük seviyeli bir işleme sağlar. Genel olarak, bu, yorumlanmış veya yüksek seviyeli programlama dilleri nedeniyle sahip olduğumuz herhangi bir tıkanıklığı önleyecektir. Daha da önemlisi, aynı zamanda önemli bir özelliğe donanım düzeyinde erişim sağlar: GPU.
GPU birçok uygulamayı önemli ölçüde hızlandırabilir, ancak bir bilgisayarda çok özel bir rolü vardır. GPU çekirdekleri aslında CPU çekirdeklerinden daha yavaştır. Eşzamanlı etkinliği olmayan özellikle seri olan bir program çalıştıracak olsaydık, GPU çekirdeğinde neredeyse her zaman CPU çekirdeğinden daha yavaş olacaktır. Temel fark, GPU'nun büyük paralel işlemeyi desteklemesidir. Aynı anda yüzlerce çekirdekte etkin bir şekilde çalışacak, gölgelendirici adı verilen küçük programlar oluşturabiliriz. Bu, aksi takdirde inanılmaz derecede tekrarlayan görevleri alıp aynı anda çalıştırabileceğimiz anlamına gelir.
Bu yazıda, içeriğini ekranda oluşturmak için OpenGL kullanan basit bir Android uygulaması oluşturacağız. Başlamadan önce, Android uygulamaları yazma bilgisine ve bazı C benzeri programlama dillerinin sözdizimine aşina olmanız önemlidir. Bu öğreticinin tüm kaynak kodu GitHub'da mevcuttur.
OpenGL Eğitimi ve Android
OpenGL'nin gücünü göstermek için bir Android cihaz için nispeten basit bir uygulama yazacağız. Şimdi, Android'de OpenGL, Gömülü Sistemler için OpenGL (OpenGL ES) adlı bir alt küme altında dağıtılır. Temelde bunu OpenGL'nin sadeleştirilmiş bir versiyonu olarak düşünebiliriz, ancak gerekli olan temel işlevsellik hala mevcut olacaktır.
Basit bir “Merhaba Dünya” yazmak yerine, aldatıcı derecede basit bir uygulama yazacağız: bir Mandelbrot set üreteci. Mandelbrot kümesi, karmaşık sayılar alanına dayanmaktadır. Karmaşık analiz oldukça geniş bir alandır, bu nedenle arkasındaki gerçek matematikten çok görsel sonuca odaklanacağız.
Sürüm Desteği
Uygulamayı yaparken, sadece uygun OpenGL desteğine sahip olanlara dağıtıldığından emin olmak istiyoruz. Bildirim bildirimi ve uygulama arasındaki bildirim dosyasında OpenGL 2.0 kullanımını bildirerek başlayın:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
Bu noktada, OpenGL 2.0 desteği her yerde mevcuttur. OpenGL 3.0 ve 3.1 uyumluluk kazanıyor, ancak her ikisi için yazmak cihazların yaklaşık %65'ini dışarıda bırakacak, bu nedenle yalnızca ek işlevselliğe ihtiyacınız olacağından eminseniz karar verin. Sürümü sırasıyla '0x000300000' ve '0x000300001' olarak ayarlayarak uygulanabilirler.
Uygulama Mimarisi
Android'de bu OpenGL uygulamasını yaparken, genellikle yüzeyi çizmek için kullanılan üç ana sınıfınız olacaktır: MainActivity
, GLSurfaceView
uzantısı ve bir GLSurfaceView.Renderer
uygulaması. Oradan, çizimleri içine alacak çeşitli Modeller oluşturacağız.
Bu örnekte MainActivity
olarak adlandırılan FractalGenerator
, esasen GLSurfaceView
somutlaştıracak ve tüm genel değişiklikleri hattan aşağı yönlendirecektir. Esasen ortak kod kodunuz olacak bir örnek:
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(); } }
Bu aynı zamanda diğer aktivite düzeyi değiştiricilerini (örneğin, sürükleyici tam ekran) koymak isteyeceğiniz sınıf olacaktır.
Bir sınıf daha derinde, birincil görünümümüz olarak işlev görecek bir GLSurfaceView
uzantısına sahibiz. Bu sınıfta sürümü ayarlıyoruz, bir Renderer kuruyoruz ve dokunma olaylarını kontrol ediyoruz. Yapıcımızda, yalnızca OpenGL sürümünü setEGLContextClientVersion(int version)
ile ayarlamamız ve ayrıca oluşturucumuzu oluşturup ayarlamamız gerekiyor:
public FractalSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mRenderer = new FractalRenderer(); setRenderer(mRenderer); }
Ayrıca setRenderMode(int renderMode)
ile render modu gibi öznitelikler ayarlayabiliriz. Bir Mandelbrot kümesi oluşturmak çok maliyetli olabileceğinden, sahneyi yalnızca başlatma sırasında ve requestRender()
açık çağrılar yapıldığında oluşturacak olan RENDERMODE_WHEN_DIRTY
kullanacağız. Ayarlar için daha fazla seçenek GLSurfaceView
API'sinde bulunabilir.
Oluşturucuya sahip olduktan sonra, muhtemelen en az bir başka yöntemi geçersiz kılmak isteyeceğiz: genel dokunmaya dayalı kullanıcı girişi için kullanılabilen onTouchEvent(MotionEvent event)
. Dersin ana konusu bu olmadığı için burada çok fazla ayrıntıya girmeyeceğim.
Son olarak, aydınlatma veya belki de sahnedeki değişikliklerin çoğunun gerçekleştiği Renderer'ımıza iniyoruz. İlk olarak, matrislerin grafik dünyasında nasıl çalıştığına ve çalıştığına biraz bakmamız gerekecek.
Lineer Cebirde Hızlı Ders
OpenGL, büyük ölçüde matrislerin kullanımına dayanır. Matrisler, koordinatlardaki genelleştirilmiş değişikliklerin dizilerini temsil etmenin harika bir kompakt yoludur. Normalde, keyfi döndürmeler, genişlemeler/daralmalar ve yansımalar yapmamıza izin verirler, ancak biraz incelikle ötelemeler de yapabiliriz. Temel olarak, tüm bunlar, bir kamerayı hareket ettirmek veya bir nesneyi büyütmek de dahil olmak üzere, istediğiniz herhangi bir makul değişikliği kolayca gerçekleştirebileceğiniz anlamına gelir. Matrislerimizi koordinatımızı temsil eden bir vektörle çarparak yeni koordinat sistemini etkin bir şekilde üretebiliriz.
OpenGL tarafından sağlanan Matrix sınıfı, ihtiyaç duyacağımız bir dizi hazır matris hesaplama yöntemi sağlar, ancak bunların nasıl çalıştığını anlamak, basit dönüşümlerle çalışırken bile akıllıca bir fikirdir.
İlk olarak, koordinatlarla başa çıkmak için neden dört boyutlu vektörleri ve matrisleri kullanacağımızı gözden geçirebiliriz. Bu aslında, çeviri yapabilmek için koordinat kullanımımızı incelikli hale getirme fikrine geri dönüyor: 3B uzayda bir çeviri sadece üç boyut kullanarak imkansızken, dördüncü bir boyut eklemek bu yeteneği mümkün kılıyor.
Bunu göstermek için çok basit bir genel ölçek/çeviri matrisi kullanabiliriz:
Önemli bir not olarak, OpenGL matrisleri sütun bazındadır, dolayısıyla bu matris {a, 0, 0, 0, 0, b, 0, 0, 0, 0, c, 0, v_a, v_b, v_c, 1}
, genellikle nasıl okunacağına diktir. Bu, çarpma işleminde sütun olarak görünen vektörlerin matrislerle aynı formata sahip olması sağlanarak rasyonelleştirilebilir.
Kod'a Geri Dön
Bu matris bilgisiyle donanmış olarak, Renderer'ımızı tasarlamaya geri dönebiliriz. Genellikle, bu sınıfta üç matrisin çarpımından oluşan bir matris oluşturacağız: Model, Görünüm ve Projeksiyon. Buna uygun bir şekilde MVPMatrix adı verilir. Daha temel bir dönüşüm seti kullanacağımız için burada ayrıntılar hakkında daha fazla bilgi edinebilirsiniz—Mandelbrot seti 2 boyutlu, tam ekran bir modeldir ve gerçekten bir kamera fikri gerektirmez.
Öncelikle sınıfımızı oluşturalım. Renderer arayüzü için gerekli yöntemleri uygulamamız gerekecek: onSurfaceCreated(GL10 gl, EGLConfig config)
, onSurfaceChanged(GL10 gl, int width, int height)
ve onDrawFrame(GL10 gl)
. Tam sınıf şöyle bir şeye benzeyecek:
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 }
Ayrıca sağlanan kodda kullanılan iki yardımcı yöntem vardır: hata ayıklamaya ve gölgelendiricilerin kullanımına yardımcı olmak için checkGLError
ve loadShaders
.
Tüm bunlarda, programın farklı bölümlerini kapsamak için komuta zincirini satırdan aşağıya aktarmaya devam ediyoruz. Sonunda, programda teorik değişiklikleri nasıl yapabileceğimizi değil , gerçekte ne yaptığını yazabileceğimiz noktaya geldik. Bunu yaparken, sahnedeki herhangi bir nesne için görüntülenmesi gereken bilgileri içeren bir model sınıfı yapmamız gerekiyor. Karmaşık 3B sahnelerde, bu bir hayvan veya bir çaydanlık olabilir, ancak çok daha basit bir 2B örnek olarak bir fraktal yapacağız.
Model sınıflarında, sınıfın tamamını yazarız; kullanılması gereken üst sınıflar yoktur. Yalnızca bir kurucuya ve herhangi bir parametreyi içine alan bir çeşit çizim yöntemine ihtiyacımız var.
Bununla birlikte, esasen standart olan, sahip olmamız gereken bir dizi değişken var. Fraktal sınıfında kullanılan tam yapıcıya bir göz atalım:
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); }
Oldukça ağız dolusu, değil mi? Neyse ki, bu programın bir parçası, hiçbir şekilde değiştirmeniz gerekmeyecek, modelin adını kaydedin. Sınıf değişkenlerini uygun şekilde değiştirmeniz şartıyla, bunun temel şekiller için iyi çalışması gerekir.

Bunun parçalarını tartışmak için bazı değişken bildirimlerine bakalım:
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
tüm koordinatlarını belirtiriz. Ekrandaki tüm koordinatların, sol altta (-1,-1)
ve sağ üstte (1,1)
ile bir ızgara olarak temsil edildiğini unutmayın.
drawOrder
, kareyi oluşturacak üçgenlere dayalı koordinatların sırasını belirtiriz. Özellikle tutarlılık ve hız için OpenGL, tüm yüzeyleri temsil etmek için üçgenler kullanır. Bir kare yapmak için, iki üçgen vermek üzere bir köşegeni (bu durumda 0
ila 2
) kesin.
Bunların her ikisini de programa eklemek için, dizi içeriğini doğrudan OpenGL arayüzü ile arayüzlemek için onları bir ham bayt arabelleğine dönüştürmeniz gerekir. Java, dizileri, OpenGL uygulamasının kullandığı işaretçi tabanlı C dizileriyle doğrudan uyumlu olmayan ek bilgiler içeren nesneler olarak depolar. Bunu düzeltmek için, dizinin ham belleğine erişimi depolayan ByteBuffers
kullanılır.
Köşeler için verileri ve çizim sırasını girdikten sonra gölgelendiricilerimizi oluşturmalıyız.
gölgelendiriciler
Model oluştururken iki shader oluşturulmalıdır: Vertex Shader ve Fragment (Pixel) Shader. Tüm gölgelendiriciler, bir dizi yerleşik işlevin, değişken değiştiricilerin, temel öğelerin ve varsayılan giriş/çıkışın eklenmesiyle C tabanlı bir dil olan GL Shading Language (GLSL) ile yazılmıştır. Android'de bunlar, Renderer'daki iki kaynak yönteminden biri olan loadShader(int type, String shaderCode)
aracılığıyla son Dizeler olarak geçirilecektir. Önce farklı niteleyici türlerini gözden geçirelim:
-
const
: Herhangi bir son değişken sabit olarak bildirilebilir, böylece değeri kolay erişim için saklanabilir. π gibi sayılar, gölgelendirici boyunca sıklıkla kullanılıyorlarsa sabit olarak bildirilebilir. Uygulamaya bağlı olarak, derleyicinin değiştirilmemiş değerleri otomatik olarak sabitler olarak bildirmesi olasıdır. -
uniform
: Tekdüze değişkenler, herhangi bir tek işleme için sabit olarak bildirilenlerdir. Temelde gölgelendiricileriniz için statik argümanlar olarak kullanılırlar. -
varying
: Bir değişken değişken olarak bildirilirse ve bir tepe gölgelendiricisinde ayarlanırsa, parça gölgelendiricide doğrusal olarak enterpolasyon yapılır. Bu, renkte herhangi bir tür degrade oluşturmak için kullanışlıdır ve derinlik değişiklikleri için örtüktür. -
attribute
: Nitelikler, bir gölgelendiricinin statik olmayan argümanları olarak düşünülebilir. Köşeye özel olan ve yalnızca Köşe Gölgelendiricilerinde görünecek olan girdi kümesini belirtirler.
Ek olarak, eklenen iki diğer ilkel türünü tartışmalıyız:
-
vec2
,vec3
,vec4
: Verilen boyutun kayan nokta vektörleri. -
mat2
,mat3
,mat4
: Verilen boyutta kayan nokta matrisleri.
Vektörlere x
, y
, z
ve w
veya r
, g
, b
ve a
bileşenleri ile erişilebilir. Ayrıca, birden çok indeksi olan herhangi bir boyut vektörü oluşturabilirler: vec3 a
için vec4
, karşılık gelen a değerlerine sahip a
a.xxyz
döndürür.
Matrisler ve vektörler diziler olarak da indekslenebilir ve matrisler sadece tek bileşenli bir vektör döndürür. Bu, mat2 matrix
için matrix[0].a
.a'nın geçerli olduğu ve matrix[0][0]
döndüreceği anlamına gelir. Bunlarla çalışırken, nesnelerin değil, ilkel olarak hareket ettiklerini unutmayın. Örneğin, aşağıdaki kodu göz önünde bulundurun:
vec2 a = vec2(1.0,1.0); vec2 b = a; bx=2.0;
Bu, a=vec2(1.0,1.0)
ve b=vec2(2.0,1.0)
; bu, nesne davranışından beklendiği gibi değildir, burada ikinci satır b
a'ya a
işaretçi verir.
Mandelbrot Setinde, kodun çoğu, her pikselde çalışan gölgelendirici olan parça gölgelendiricisinde olacaktır. Nominal olarak, köşe gölgelendiricileri, renk veya derinlikteki değişiklikler gibi her bir köşe temelinde olacak nitelikler de dahil olmak üzere her köşe üzerinde çalışır. Bir fraktal için son derece basit köşe gölgelendiricisine bir göz atalım:
private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}";
Burada gl_Position
, bir tepe noktasının koordinatlarını kaydetmek için OpenGL tarafından tanımlanan bir çıktı değişkenidir. Bu durumda, gl_Position
ayarladığımız her köşe için bir pozisyona geçiyoruz. Çoğu uygulamada, köşelerimizi dönüştürerek vPosition'ı bir vPosition
ile MVPMatrix
, ancak fraktalın her zaman tam ekran olmasını istiyoruz. Tüm dönüşümler yerel bir koordinat sistemi ile yapılacaktır.
Fragment Shader, seti oluşturmak için işin çoğunun yapıldığı yer olacak. fragmentShaderCode
aşağıdakine ayarlayacağız:
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; }
Kodun çoğu, yalnızca kümenin nasıl çalıştığına ilişkin matematik ve algoritmadır. Çeşitli yerleşik işlevlerin kullanımına dikkat edin: tümü vektörler veya skalerler üzerinde çalışan ve vektörler veya skalerler döndüren fract
, abs
, mix
, sin
ve clamp
. Ek olarak, vektör argümanlarını alan ve bir skaler döndüren dot
kullanılır.
Artık gölgelendiricilerimizi kullanıma hazır hale getirdiğimize göre, modelimizde draw
işlevini uygulamak olan son bir adımımız var:
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"); }
İşlev, uniform
dönüştürme matrisi ve attribute
konumu da dahil olmak üzere tüm bağımsız değişkenleri gölgelendiricilere iletir.
Programın tüm parçalarını bir araya getirdikten sonra nihayet çalıştırabiliriz. Doğru dokunma desteği sağlanırsa, kesinlikle büyüleyici sahneler boyanacaktır:
Kayan Nokta Doğruluğu
Biraz daha yakınlaştırırsak, görüntüde bir bozulma olduğunu fark etmeye başlarız:
Bunun arkasındaki kümenin matematiğiyle ve sayıların OpenGL'de saklanma ve işlenme şekliyle kesinlikle ilgisi yoktur. double
duyarlık için daha yeni destek yapılmış olsa da, OpenGL 2.0 doğal olarak float
s'den fazlasını desteklemez. Bunları, gölgelendiricimizde precision highp float
yüksek şamandıralı mevcut en yüksek hassasiyetli şamandıralar olarak özellikle belirledik, ancak bu bile yeterince iyi değil.
Bu sorunu aşmak için tek yol, double
s'yi iki float
s kullanarak öykünmek olacaktır. Hızlandırmanın oldukça ciddi bir maliyeti olsa da, bu yöntem aslında yerel olarak uygulanan bir yöntemin gerçek kesinliğinin bir büyüklük sırası içinde gelir. Daha yüksek bir doğruluk düzeyine sahip olmak istenirse, bu okuyucuya bir alıştırma olarak bırakılacaktır.
Çözüm
Birkaç destek sınıfıyla OpenGL, karmaşık sahnelerin gerçek zamanlı görüntülenmesini hızla sürdürebilir. GLSurfaceView
oluşan bir düzen oluşturmak, Renderer
ayarlamak ve gölgelendiricilerle bir model oluşturmak, hepsi güzel bir matematiksel yapının görselleştirilmesiyle sonuçlandı. Bir OpenGL ES uygulaması geliştirmeye ilgi duyacağınızı umuyorum!