رسم الأشكال

بعد تحديد الأشكال التي سيتم رسمها باستخدام 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 مكلف من حيث دورات وحدة المعالجة المركزية (CPU) ومدة المعالجة، لذا عليك تجنُّب إجراء ذلك أكثر من مرة. إذا كنت لا تعرف محتوى أدوات التظليل في وقت التشغيل، يجب إنشاء التعليمات البرمجية بحيث يتم إنشاؤها مرة واحدة فقط ثم تخزينها مؤقتًا لاستخدامها لاحقًا.

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 بشكل أكثر تشويقًا.