บทช่วยสอน OpenGL สำหรับ Android: การสร้าง Mandelbrot Set Generator

เผยแพร่แล้ว: 2022-03-11

OpenGL เป็น API ข้ามแพลตฟอร์มที่มีประสิทธิภาพที่ช่วยให้เข้าถึงฮาร์ดแวร์ของระบบอย่างใกล้ชิดในสภาพแวดล้อมการเขียนโปรแกรมที่หลากหลาย

แล้วทำไมคุณถึงต้องใช้มัน?

ให้การประมวลผลระดับต่ำมากสำหรับกราฟิกทั้ง 2D และ 3D โดยทั่วไป สิ่งนี้จะหลีกเลี่ยงปัญหาที่เรามีเนื่องจากภาษาโปรแกรมที่ตีความหรือระดับสูง ที่สำคัญกว่านั้น ยังให้การเข้าถึงระดับฮาร์ดแวร์สำหรับคุณสมบัติหลัก: GPU

GPU สามารถเพิ่มความเร็วของแอปพลิเคชั่นจำนวนมากได้อย่างมาก แต่มีบทบาทเฉพาะอย่างมากในคอมพิวเตอร์ อันที่จริงแกน GPU นั้นช้ากว่าคอร์ของ CPU หากเราต้องเรียกใช้โปรแกรมที่มีลักษณะเป็นอนุกรมโดยไม่มีกิจกรรมเกิดขึ้นพร้อมกัน คอร์ของ GPU จะช้ากว่าคอร์ของ CPU เกือบ ทุก ครั้ง ความแตกต่างที่สำคัญคือ GPU รองรับการประมวลผลแบบขนานขนาดใหญ่ เราสามารถสร้างโปรแกรมขนาดเล็กที่เรียกว่า shaders ที่จะทำงานอย่างมีประสิทธิภาพบนหลายร้อยคอร์ในคราวเดียว ซึ่งหมายความว่าเราสามารถทำงานที่ซ้ำซากจำเจและเรียกใช้พร้อมกันได้

ตัวสร้างชุด OpenGL และ Mandelbrot

ในบทความนี้ เราจะสร้างแอปพลิเคชัน Android อย่างง่ายที่ใช้ OpenGL เพื่อแสดงเนื้อหาบนหน้าจอ ก่อนที่เราจะเริ่ม เป็นสิ่งสำคัญที่คุณจะต้องคุ้นเคยกับความรู้ในการเขียนแอปพลิเคชัน Android และไวยากรณ์ของภาษาการเขียนโปรแกรมที่คล้ายกับ C ซอร์สโค้ดทั้งหมดของบทช่วยสอนนี้มีอยู่ใน GitHub

บทช่วยสอน OpenGL และ Android

เพื่อแสดงพลังของ OpenGL เราจะเขียนแอปพลิเคชันที่ค่อนข้างพื้นฐานสำหรับอุปกรณ์ Android ตอนนี้ OpenGL บน Android ถูกแจกจ่ายภายใต้ชุดย่อยที่เรียกว่า OpenGL สำหรับระบบสมองกลฝังตัว (OpenGL ES) โดยพื้นฐานแล้วเราสามารถคิดว่านี่เป็นเวอร์ชันที่แยกย่อยของ OpenGL แม้ว่าฟังก์ชันหลักที่จำเป็นจะยังคงมีอยู่

แทนที่จะเขียน "สวัสดีชาวโลก" พื้นฐาน เราจะเขียนแอปพลิเคชันง่ายๆ ที่หลอกลวง: ตัวสร้างชุด Mandelbrot ชุด Mandelbrot อยู่ในฟิลด์ของจำนวนเชิงซ้อน การวิเคราะห์ที่ซับซ้อนเป็นสาขาที่กว้างใหญ่อย่างสวยงาม ดังนั้นเราจะเน้นที่ผลลัพธ์ของภาพมากกว่าการคำนวณจริงที่อยู่เบื้องหลัง

ด้วย OpenGL การสร้างชุดเครื่องกำเนิดไฟฟ้า Mandelbrot ทำได้ง่ายกว่าที่คุณคิด!
ทวีต

รองรับเวอร์ชัน

รองรับเวอร์ชัน opengl

เมื่อเราสร้างแอปพลิเคชัน เราต้องการตรวจสอบให้แน่ใจว่ามีการแจกจ่ายให้กับผู้ที่มีการสนับสนุน OpenGL ที่เหมาะสมเท่านั้น เริ่มต้นด้วยการประกาศใช้ OpenGL 2.0 ในไฟล์รายการ ระหว่างการประกาศรายการและแอปพลิเคชัน:

 <uses-feature android:glEsVersion="0x00020000" android:required="true" />

ณ จุดนี้ รองรับ OpenGL 2.0 อย่างแพร่หลาย OpenGL 3.0 และ 3.1 กำลังเข้ากันได้ แต่การเขียนสำหรับทั้งสองจะทำให้ประมาณ 65% ของอุปกรณ์ ดังนั้นให้ตัดสินใจเฉพาะเมื่อคุณแน่ใจว่าคุณจะต้องใช้ฟังก์ชันเพิ่มเติม สามารถใช้งานได้โดยตั้งค่าเวอร์ชันเป็น '0x00030000' และ '0x000300001' ตามลำดับ

สถาปัตยกรรมประยุกต์

สถาปัตยกรรมแอปพลิเคชัน opengl

เมื่อสร้างแอปพลิเคชัน OpenGL นี้บน Android โดยทั่วไปแล้ว คุณจะมีคลาสหลักสามคลาสที่ใช้ในการวาดพื้นผิว: MainActivity ของคุณ ส่วนขยายของ GLSurfaceView และการใช้งาน GLSurfaceView.Renderer จากนั้นเราจะสร้าง Models ต่างๆ ที่จะห่อหุ้มภาพวาด

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(); } }

นี่จะเป็นคลาสที่คุณต้องการใส่ตัวแก้ไขระดับกิจกรรมอื่นๆ (เช่น เต็มหน้าจอแบบเต็มหน้าจอ)

ลึกลงไปอีกชั้นหนึ่ง เรามีส่วนขยายของ GLSurfaceView ซึ่งจะทำหน้าที่เป็นมุมมองหลักของเรา ในคลาสนี้ เราตั้งค่าเวอร์ชัน ตั้งค่า Renderer และควบคุมเหตุการณ์การสัมผัส ใน Constructor ของเรา เราต้องตั้งค่าเวอร์ชัน OpenGL ด้วย setEGLContextClientVersion(int version) และสร้างและตั้งค่า renderer ของเราด้วย:

 public FractalSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mRenderer = new FractalRenderer(); setRenderer(mRenderer); }

นอกจากนี้ เราสามารถตั้งค่าแอตทริบิวต์เช่นโหมดการเรนเดอร์ด้วย setRenderMode(int renderMode) เนื่องจากการสร้างชุด Mandelbrot อาจมีราคาแพงมาก เราจะใช้ RENDERMODE_WHEN_DIRTY ซึ่งจะแสดงเฉพาะฉากเมื่อเริ่มต้นและเมื่อมีการเรียกใช้ requestRender() อย่างชัดเจน ตัวเลือกเพิ่มเติมสำหรับการตั้งค่าสามารถพบได้ใน GLSurfaceView API

หลังจากที่เรามีตัวสร้างแล้ว เราอาจต้องการแทนที่อย่างน้อยหนึ่งวิธี: onTouchEvent(MotionEvent event) ซึ่งสามารถใช้สำหรับอินพุตของผู้ใช้แบบสัมผัสทั่วไป ฉันจะไม่ลงรายละเอียดมากเกินไปที่นี่ เนื่องจากนั่นไม่ใช่จุดสนใจหลักของบทเรียน

สุดท้าย เราลงไปที่ Renderer ซึ่งจะเป็นที่ที่งานจัดแสงส่วนใหญ่หรือบางทีอาจมีการเปลี่ยนแปลงในฉากเกิดขึ้น อันดับแรก เราจะต้องดูสักเล็กน้อยว่าเมทริกซ์ทำงานและทำงานอย่างไรในโลกของกราฟิก

บทเรียนอย่างรวดเร็วในพีชคณิตเชิงเส้น

OpenGL อาศัยการใช้เมทริกซ์เป็นอย่างมาก เมทริกซ์เป็นวิธีที่กะทัดรัดอย่างยิ่งในการแสดงลำดับการเปลี่ยนแปลงทั่วไปในพิกัด โดยปกติ สิ่งเหล่านี้จะทำให้เราสามารถหมุน ขยาย/หด และการสะท้อนได้ตามอำเภอใจ แต่ด้วยความละเอียดเพียงเล็กน้อย เราก็สามารถแปลได้เช่นกัน โดยพื้นฐานแล้ว ทั้งหมดนี้หมายความว่าคุณสามารถทำการเปลี่ยนแปลงตามสมควรที่คุณต้องการได้อย่างง่ายดาย รวมถึงการขยับกล้องหรือทำให้วัตถุโตขึ้น การคูณเมทริกซ์ด้วยเวกเตอร์แทนพิกัด เราสามารถสร้างระบบพิกัดใหม่ได้อย่างมีประสิทธิภาพ

คลาส Matrix ที่จัดเตรียมโดย OpenGL ให้วิธีการคำนวณเมทริกซ์สำเร็จรูปจำนวนหนึ่งที่เราจำเป็นต้องใช้ แต่การทำความเข้าใจวิธีทำงานของเมทริกซ์นั้นเป็นแนวคิดที่ชาญฉลาดแม้ว่าจะทำงานกับการแปลงแบบธรรมดาก็ตาม

อันดับแรก เราสามารถอธิบายได้ว่าทำไมเราจะใช้เวกเตอร์และเมทริกซ์สี่มิติเพื่อจัดการกับพิกัด ที่จริงแล้วสิ่งนี้กลับไปสู่แนวคิดในการปรับการใช้พิกัดของเราให้ละเอียดเพื่อให้สามารถแปลได้ ในขณะที่การแปลในพื้นที่ 3 มิตินั้นเป็นไปไม่ได้โดยใช้เพียงสามมิติ การเพิ่มมิติที่สี่ก็ช่วยให้มีความสามารถ

เพื่อแสดงสิ่งนี้ เราสามารถใช้มาตราส่วน/เมทริกซ์การแปลทั่วไปขั้นพื้นฐานได้:

มาตราส่วน opengl และเมทริกซ์การแปล

หมายเหตุสำคัญ เมทริกซ์ OpenGL เป็นแบบคอลัมน์ ดังนั้นเมทริกซ์นี้จึงเขียนเป็น {a, 0, 0, 0, 0, b, 0, 0, 0, 0, c, 0, v_a, v_b, v_c, 1} ซึ่งตั้งฉากกับวิธีการอ่านตามปกติ สิ่งนี้สามารถหาเหตุผลเข้าข้างตนเองได้โดยการทำให้แน่ใจว่าเวกเตอร์ซึ่งปรากฏในการคูณเป็นคอลัมน์นั้นมีรูปแบบเดียวกับเมทริกซ์

กลับไปที่รหัส

ด้วยความรู้เกี่ยวกับเมทริกซ์นี้ เราสามารถกลับไปออกแบบ Renderer ของเราได้ โดยปกติ เราจะสร้างเมทริกซ์ในคลาสนี้ซึ่งเกิดขึ้นจากผลคูณของเมทริกซ์สามตัว ได้แก่ โมเดล มุมมอง และการฉายภาพ สิ่งนี้จะเรียกว่า MVPMatrix อย่างเหมาะสม คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับข้อมูลเฉพาะได้ที่นี่ เนื่องจากเราจะใช้ชุดการแปลงพื้นฐานมากขึ้น ชุด Mandelbrot เป็นโมเดล 2 มิติแบบเต็มหน้าจอ และไม่จำเป็นต้องใช้แนวคิดเรื่องกล้องจริงๆ

ขั้นแรก มาตั้งค่าคลาสกันก่อน เราจำเป็นต้องใช้วิธีการที่จำเป็นสำหรับอินเทอร์เฟซ Renderer: 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 เพื่อช่วยในการดีบักและการใช้ shaders

ทั้งหมดนี้ เราส่งต่อสายการบังคับบัญชาต่อไปเพื่อสรุปส่วนต่างๆ ของโปรแกรม ในที่สุด เราก็มาถึงจุดที่เราสามารถเขียนสิ่งที่โปรแกรมของเรา ทำ จริงๆ ได้ แทนที่จะเป็นวิธีที่เราจะทำการเปลี่ยนแปลงในทางทฤษฎี เมื่อทำเช่นนี้ เราจำเป็นต้องสร้างคลาสโมเดลที่มีข้อมูลที่จำเป็นต้องแสดงสำหรับวัตถุที่กำหนดในฉาก ในฉาก 3 มิติที่ซับซ้อน นี่อาจเป็นสัตว์หรือกาต้มน้ำ แต่เรากำลังจะทำแฟร็กทัลเป็นตัวอย่าง 2 มิติที่ง่ายกว่ามาก

ในคลาส Model เราเขียนทั้งคลาส—ไม่มีซูเปอร์คลาสที่ต้องใช้ เราต้องการเพียงตัวสร้างและวิธีการวาดบางประเภทที่ใช้พารามิเตอร์ใด ๆ

สิ่งนี้กล่าวว่ายังคงมีตัวแปรจำนวนหนึ่งที่เราจะต้องมีซึ่งเป็นต้นแบบ มาดูตัวสร้างที่แน่นอนที่ใช้ในคลาส 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 ) เพื่อให้มีสามเหลี่ยมสองรูป

ในการเพิ่มทั้งสองสิ่งนี้ลงในโปรแกรม ก่อนอื่นคุณต้องแปลงเป็นบัฟเฟอร์ไบต์ดิบเพื่อเชื่อมต่อเนื้อหาของอาร์เรย์โดยตรงด้วยอินเทอร์เฟซ OpenGL Java เก็บอาร์เรย์เป็นอ็อบเจ็กต์ที่มีข้อมูลเพิ่มเติมซึ่งเข้ากันไม่ได้โดยตรงกับอาร์เรย์ C ที่ใช้ตัวชี้ซึ่งการใช้งานของ OpenGL ใช้ เพื่อแก้ไขปัญหานี้ จะใช้ ByteBuffers ซึ่งจัดเก็บการเข้าถึงหน่วยความจำดิบของอาร์เรย์

หลังจากที่เราใส่ข้อมูลสำหรับจุดยอดและลำดับการวาดแล้ว เราต้องสร้างเฉดสีของเรา

Shaders

เมื่อสร้างแบบจำลอง ต้องทำ Shader สองอัน: Vertex Shader และ Fragment (Pixel) Shader เฉดสีทั้งหมดเขียนด้วย GL Shading Language (GLSL) ซึ่งเป็นภาษา C ที่มีการเพิ่มฟังก์ชันในตัว ตัวปรับแต่งตัวแปร ค่าพื้นฐาน และอินพุต/เอาต์พุตเริ่มต้น บน Android สิ่งเหล่านี้จะถูกส่งผ่านเป็น Strings สุดท้ายผ่าน loadShader(int type, String shaderCode) ซึ่งเป็นหนึ่งในสองวิธีการของทรัพยากรใน Renderer อันดับแรก มาดูประเภทต่าง ๆ ของรอบคัดเลือก:

  • const : ตัวแปรสุดท้ายใดๆ สามารถประกาศเป็นค่าคงที่ได้ จึงสามารถเก็บค่าของมันไว้เพื่อให้เข้าถึงได้ง่าย ตัวเลขอย่างเช่น π สามารถประกาศเป็นค่าคงที่ได้ หากมีการใช้ตัวเลขเหล่านี้บ่อยๆ ตลอดทั้ง shader มีความเป็นไปได้ที่คอมไพเลอร์จะประกาศค่าที่ไม่ได้รับการแก้ไขโดยอัตโนมัติเป็นค่าคงที่ ขึ้นอยู่กับการใช้งาน
  • uniform : ตัวแปรยูนิฟอร์มคือตัวแปรที่ประกาศเป็นค่าคงที่สำหรับการเรนเดอร์เดี่ยวใดๆ พวกมันถูกใช้เป็นอาร์กิวเมนต์คงที่สำหรับเฉดสีของคุณ
  • varying : หากตัวแปรถูกประกาศเป็นค่าแปรผันและตั้งค่าไว้ในเชดเดอร์จุดยอด ตัวแปรนั้นจะถูกอินเทอร์โพเลตแบบเส้นตรงในแฟรกเมนต์เชดเดอร์ สิ่งนี้มีประโยชน์สำหรับการสร้างการไล่ระดับสีทุกประเภทและโดยนัยสำหรับการเปลี่ยนแปลงความลึก
  • attribute : คุณลักษณะสามารถคิดได้ว่าเป็นอาร์กิวเมนต์ที่ไม่คงที่สำหรับ shader หมายถึงชุดของอินพุตที่มีจุดยอดเฉพาะ และจะปรากฏใน Vertex Shaders เท่านั้น

นอกจากนี้ เราควรหารือเกี่ยวกับ primitives อีกสองประเภทที่เพิ่มเข้ามา:

  • vec2 , vec3 , vec4 : เวกเตอร์จุดลอยตัวของมิติที่กำหนด
  • mat2 , mat3 , mat4 : เมทริกซ์จุดลอยตัวของมิติที่กำหนด

เวกเตอร์สามารถเข้าถึงได้โดยส่วนประกอบ x , y , z , และ w หรือ r , g , b และ a พวกเขายังสามารถสร้างเวกเตอร์ขนาดใดก็ได้ที่มีดัชนีหลายตัว: สำหรับ vec3 a , a.xxyz จะส่งกลับ vec4 ด้วยค่าที่สอดคล้องกันของ a

เมทริกซ์และเวกเตอร์สามารถจัดทำดัชนีเป็นอาร์เรย์ได้ และเมทริกซ์จะส่งกลับเวกเตอร์ที่มีองค์ประกอบเพียงองค์ประกอบเดียว ซึ่งหมายความว่าสำหรับ 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) ซึ่งไม่ใช่สิ่งที่เราคาดหวังจากพฤติกรรมของวัตถุ โดยที่บรรทัดที่สองจะให้ b ตัวชี้ไปที่ a

ในชุด Mandelbrot โค้ดส่วนใหญ่จะอยู่ใน Fragment Shader ซึ่งเป็น Shader ที่ทำงานบนทุกพิกเซล ในนาม Shader จะทำงานบนทุกจุดยอด รวมถึงแอตทริบิวต์ที่จะอยู่บนพื้นฐานต่อจุดยอด เช่น การเปลี่ยนสีหรือความลึก มาดูจุดสุดยอดที่ง่ายมากสำหรับเศษส่วนกัน:

 private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}";

ในที่นี้ gl_Position เป็นตัวแปรเอาต์พุตที่กำหนดโดย OpenGL เพื่อบันทึกพิกัดของจุดยอด ในกรณีนี้ เราส่งผ่านตำแหน่งสำหรับแต่ละจุดยอดที่เราตั้งไว้ gl_Position ในแอปพลิเคชันส่วนใหญ่ เราจะคูณ vPosition ด้วย MVPMatrix ซึ่งจะเปลี่ยนจุดยอดของเรา แต่เราต้องการให้เศษส่วนแสดงเต็มหน้าจอเสมอ การแปลงทั้งหมดจะดำเนินการด้วยระบบพิกัดในพื้นที่

Fragment Shader จะเป็นที่ที่งานส่วนใหญ่ทำเพื่อสร้างฉาก เราจะตั้งค่า 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 ซึ่งรับอาร์กิวเมนต์เวกเตอร์และส่งคืนสเกลาร์

ตอนนี้เราได้ตั้งค่า shaders สำหรับการใช้งานแล้ว เรามีขั้นตอนสุดท้ายคือการนำฟังก์ชันการ 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"); }

ฟังก์ชันจะส่งผ่านอาร์กิวเมนต์ทั้งหมดไปยัง shaders รวมถึงเมทริกซ์การแปลง uniform และตำแหน่ง attribute

หลังจากประกอบทุกส่วนของโปรแกรมแล้ว เราก็สามารถรันโปรแกรมได้ในที่สุด หากมีการจัดการการรองรับการสัมผัสที่เหมาะสม ฉากที่ชวนให้หลงใหลจะถูกทาสี:

กวดวิชา opengl

กวดวิชา opengl mandelbrot

กวดวิชาเครื่องกำเนิดไฟฟ้า mandelbrot

ความแม่นยำของจุดลอยตัว

หากเราซูมเข้าไปอีกเล็กน้อย เราจะเริ่มสังเกตเห็นการพังทลายของภาพ:

สิ่งนี้ไม่มีส่วนเกี่ยวข้องกับคณิตศาสตร์ของฉากหลังและทุกอย่างที่เกี่ยวข้องกับวิธีจัดเก็บและประมวลผลตัวเลขใน OpenGL แม้ว่าจะมีการรองรับ double precision ล่าสุด แต่ OpenGL 2.0 ไม่รองรับอะไรมากไปกว่า float s เรากำหนดให้พวกมันเป็นโฟลตที่มีความแม่นยำสูงที่สุดที่มีให้ precision highp float ในเชดเดอร์ของเรา แต่ถึงอย่างนั้นก็ยังดีไม่พอ

เพื่อแก้ไขปัญหานี้ วิธีเดียวที่จะเลียนแบบ double s โดยใช้ two float s วิธีนี้จริง ๆ แล้วมาในลำดับความสำคัญของความแม่นยำที่แท้จริงของวิธีที่ใช้โดยกำเนิด แม้ว่าจะมีค่าใช้จ่ายในการเร่งความเร็วค่อนข้างมาก นี่จะเป็นแบบฝึกหัดสำหรับผู้อ่านหากต้องการให้มีความแม่นยำในระดับที่สูงขึ้น

บทสรุป

ด้วยคลาสการสนับสนุนไม่กี่คลาส OpenGL สามารถรักษาการเรนเดอร์ฉากที่ซับซ้อนตามเวลาจริงได้อย่างรวดเร็ว การสร้างเลย์เอาต์ที่ประกอบด้วย GLSurfaceView การตั้งค่า Renderer และสร้างแบบจำลองด้วยเฉดสีทั้งหมดนั้นมีผลสูงสุดในการสร้างภาพโครงสร้างทางคณิตศาสตร์ที่สวยงาม ฉันหวังว่าคุณจะพบความสนใจในการพัฒนาแอปพลิเคชัน OpenGL ES มากพอ!