رسم الأشكال

بعد تحديد الأشكال المراد رسمها باستخدام OpenGL، ربما ترغب في رسمها. أشكال الرسم مع OpenGL ES 2.0 يتطلب رمزًا أكبر قليلاً مما قد تتخيل، لأن واجهة برمجة التطبيقات توفر قدرًا كبيرًا من التحكم في مسار عرض الرسومات.

يشرح هذا الدرس كيفية رسم الأشكال التي حددتها في الدرس السابق باستخدام OpenGL ES 2.0 API.

تهيئة الأشكال

قبل أن تقوم بأي رسم، يجب تهيئة وتحميل الأشكال التي تخطط لرسمها. ما لم يتغير هيكل (الإحداثيات الأصلية) للأشكال التي تستخدمها في برنامجك خلال الدورة التنفيذ، فينبغي عليك إعدادها طريقة واحدة (onSurfaceCreated()) من عارض لكفاءة الذاكرة والمعالجة.

Kotlin

class MyGLRenderer : GLSurfaceView.Renderer {
    ...
    private lateinit var mTriangle: Triangle
    private lateinit var mSquare: Square

    override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
        ...
        // initialize a triangle
        mTriangle = Triangle()
        // initialize a square
        mSquare = Square()
    }
    ...
}

Java

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...
    private Triangle mTriangle;
    private Square   mSquare;

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        ...
        // initialize a triangle
        mTriangle = new Triangle();
        // initialize a square
        mSquare = new Square();
    }
    ...
}

رسم شكل

يتطلب رسم شكل محدد باستخدام OpenGL ES 2.0 قدرًا كبيرًا من التعليمات البرمجية، لأنك أن يقدم الكثير من التفاصيل لمسار عرض الرسومات. وعلى وجه التحديد، يجب عليك تحديد التالي:

  • Vertex Shader - رمز رسومات OpenGL ES لعرض رؤوس الشكل.
  • Fragment Shader - رمز OpenGL ES لعرض وجه شكل باستخدام الألوان أو وزخارفها.
  • برنامج - كائن OpenGL ES يحتوي على أدوات التظليل التي تريد استخدامها للرسم شكل واحد أو أكثر.

تحتاج إلى مظلل رأس واحد على الأقل لرسم شكل وتظليل جزء واحد لتلوين هذا الشكل. ويجب تجميع أدوات التظليل هذه ثم إضافتها إلى برنامج OpenGL ES، الذي يُستخدم بعد ذلك لرسم الشكل. فيما يلي مثال على كيفية تحديد أدوات التظليل الأساسية التي يمكنك استخدامها لرسم شكل في صف واحد (Triangle):

Kotlin

class Triangle {

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

    private val fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}"

    ...
}

Java

public class Triangle {

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

    private final String fragmentShaderCode =
        "precision mediump float;" +
        "uniform vec4 vColor;" +
        "void main() {" +
        "  gl_FragColor = vColor;" +
        "}";

    ...
}

تحتوي أدوات التظليل على رمز لغة تظليل OpenGL (GLSL) الذي يجب تجميعه قبل استخدامه في بيئة OpenGL ES. لتجميع هذا الرمز، أنشئ طريقة أداة في فئة العارض:

Kotlin

fun loadShader(type: Int, shaderCode: String): Int {

    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    return GLES20.glCreateShader(type).also { shader ->

        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
    }
}

Java

public static int loadShader(int type, String shaderCode){

    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    int shader = GLES20.glCreateShader(type);

    // add the source code to the shader and compile it
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);

    return shader;
}

لرسم الشكل، يجب تجميع رمز أداة التظليل وإضافتها إلى برنامج OpenGL ES. ثم نربط البرنامج. افعل ذلك في الدالة الإنشائية للكائن المرسوم، بحيث يتم ذلك فقط مرة واحدة.

ملاحظة: إنّ تجميع برامج تظليل OpenGL ES وربط البرامج أمر مُكلف. فيما يتعلق بدورات وحدة المعالجة المركزية ووقت المعالجة، لذا يجب أن تتجنب إجراء ذلك أكثر من مرة. في حال إجراء ذلك محتوى أدوات التظليل في وقت التشغيل، فيجب إنشاء التعليمات البرمجية بحيث يتم إنشاؤها مرة واحدة ثم تخزينها مؤقتًا لاستخدامها لاحقًا.

Kotlin

class Triangle {
    ...

    private var mProgram: Int

    init {
        ...

        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram().also {

            // add the vertex shader to program
            GLES20.glAttachShader(it, vertexShader)

            // add the fragment shader to program
            GLES20.glAttachShader(it, fragmentShader)

            // creates OpenGL ES program executables
            GLES20.glLinkProgram(it)
        }
    }
}

Java

public class Triangle() {
    ...

    private final int mProgram;

    public Triangle() {
        ...

        int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                                        vertexShaderCode);
        int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
                                        fragmentShaderCode);

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram();

        // add the vertex shader to program
        GLES20.glAttachShader(mProgram, vertexShader);

        // add the fragment shader to program
        GLES20.glAttachShader(mProgram, fragmentShader);

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(mProgram);
    }
}

في هذه المرحلة، تكون جاهزًا لإضافة المكالمات الفعلية التي ترسم شكلك. رسم الأشكال باستخدام يتطلب OpenGL ES منك تحديد عدة معلَمات لإخبار مسار العرض بما تريده الرسم وكيفية رسمه. نظرًا لأن خيارات الرسم يمكن أن تختلف حسب الشكل، فمن الجيد أن يكون لديك على فئات الأشكال على منطق الرسم الخاص بها.

أنشئ طريقة draw() لرسم الشكل. تحدد هذه التعليمة البرمجية موضع قيم اللون إلى مظلل رأس الشكل وتظليل الأجزاء، ثم يتم تنفيذ الرسم الأخرى.

Kotlin

private var positionHandle: Int = 0
private var mColorHandle: Int = 0

private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex

fun draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram)

    // get handle to vertex shader's vPosition member
    positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {

        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(it)

        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(
                it,
                COORDS_PER_VERTEX,
                GLES20.GL_FLOAT,
                false,
                vertexStride,
                vertexBuffer
        )

        // get handle to fragment shader's vColor member
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->

            // Set color for drawing the triangle
            GLES20.glUniform4fv(colorHandle, 1, color, 0)
        }

        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(it)
    }
}

Java

private int positionHandle;
private int colorHandle;

private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

public void draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram);

    // get handle to vertex shader's vPosition member
    positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

    // Enable a handle to the triangle vertices
    GLES20.glEnableVertexAttribArray(positionHandle);

    // Prepare the triangle coordinate data
    GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
                                 GLES20.GL_FLOAT, false,
                                 vertexStride, vertexBuffer);

    // get handle to fragment shader's vColor member
    colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

    // Set color for drawing the triangle
    GLES20.glUniform4fv(colorHandle, 1, color, 0);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(positionHandle);
}

بمجرد وضع كل هذه التعليمات البرمجية في مكانها، فإن رسم هذا الكائن يتطلب فقط استدعاء طريقة draw() من داخل طريقة onDrawFrame() في العارض:

Kotlin

override fun onDrawFrame(unused: GL10) {
    ...

    mTriangle.draw()
}

Java

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

عند تشغيل التطبيق، يُفترض أن يظهر على النحو التالي:

الشكل 1. مثلث مرسوم بدون عرض أو كاميرا.

هناك بعض المشاكل في مثال الرمز هذا. بادئ ذي بدء، لن يثير إعجاب أصدقاؤنا. ثانيًا، يتم ضغط المثلث قليلاً ويتغير شكله عندما تغير الشاشة اتجاه الجهاز. يرجع سبب انحراف الشكل إلى حقيقة أن للرؤوس، والتي تشمل نسب مساحة الشاشة، يتم عرض GLSurfaceView. ويمكنك حلّ هذه المشكلة باستخدام جهاز عرض وكاميرا. التي ستظهر في الدرس التالي.

أخيرًا، يكون المثلث ثابتًا، وهو ممل بعض الشيء. في جلسة المعمل، درس إضافة حركة، ستصبح هذا الشكل استخدام مسار رسومات OpenGL ES بطريقة مثيرة للاهتمام بشكل أكبر.