کشیدن شکل‌ها

پس‌از تعریف شکل‌هایی که باید با OpenGL ترسیم شوند، احتمالاً می‌خواهید آن‌ها را ترسیم کنید. ترسیم اشکال با OpenGL ES 2.0 به کد بیشتری نسبت‌به آنچه تصور می‌کنید نیاز دارد، زیرا API کنترل زیادی بر خط لوله رندر گرافیک ارائه می‌دهد.

این درس توضیح می‌دهد که چگونه شکل‌هایی را که در درس قبلی تعریف کرده‌اید بااستفاده از OpenGL ES 2.0 API ترسیم کنید.

راه‌اندازی شکل‌ها

قبل‌از هرگونه طراحی، باید شکل‌هایی را که قصد دارید بکشید مقداردهی اولیه و بارگذاری کنید. مگر اینکه ساختار (مختصات اصلی) شکل‌هایی که در برنامه استفاده می‌کنید درطول اجرای برنامه تغییر کند، باید آن‌ها را در روش onSurfaceCreated() پردازنده خود مقداردهی اولیه کنید تا کارایی حافظه و پردازش بهبود یابد.

کاتلین

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

جاوا

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 برای رندر کردن رأس‌های شکل.
  • سایه‌زن قطعه - کد OpenGL ES برای رندر کردن وجه شکل با رنگ‌ها یا بافت‌ها.
  • برنامه - یک شیء OpenGL ES که حاوی سایه‌زن‌هایی است که می‌خواهید برای ترسیم یک یا چند شکل استفاده کنید.

برای رسم شکل به حداقل یک سایه‌زن رأس و برای رنگ‌آمیزی آن شکل به یک سایه‌زن قطعه نیاز دارید. این سایه‌زن‌ها باید کامپایل شوند و سپس به برنامه OpenGL ES اضافه شوند، که سپس برای ترسیم شکل استفاده می‌شود. در اینجا مثالی از نحوه تعریف سایه‌زن‌های پایه آورده شده است که می‌توانید از آن‌ها برای رسم شکل در کلاس Triangle استفاده کنید:

کاتلین

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;" +
            "}"

    ...
}

جاوا

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 کامپایل شود. برای کامپایل کردن این کد، یک روش کمکی در کلاس پردازنده خود ایجاد کنید:

کاتلین

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

جاوا

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 و زمان پردازش پرهزینه است، بنابراین باید از انجام این کار بیش‌از یک بار خودداری کنید. اگر محتوای سایه‌زن‌هایتان را در زمان اجرا نمی‌دانید، باید کدتان را طوری بسازید که فقط یک‌بار ایجاد شوند و سپس برای استفاده بعدی ذخیره شوند.

کاتلین

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

جاوا

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() را برای کشیدن شکل ایجاد کنید. این کد مقادیر موقعیت و رنگ را برای سایه‌زن رأس و سایه‌زن قطعه شکل تنظیم می‌کند و سپس تابع ترسیم را اجرا می‌کند.

کاتلین

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

جاوا

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() پردازنده خود فراخوانی کنید:

کاتلین

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

    mTriangle.draw()
}

جاوا

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

وقتی برنامه را اجرا می‌کنید، باید چیزی شبیه به این باشد:

شکل ۱. مثلث بدون تصویر یا نمای دوربین کشیده شده است.

این نمونه کد چند مشکل دارد. اول از همه، این کار دوستانتان را تحت تأثیر قرار نمی‌دهد. دوم اینکه مثلث کمی فشرده شده است و وقتی جهت صفحه دستگاه را تغییر می‌دهید، شکل آن تغییر می‌کند. دلیل کج بودن شکل این است که رئوس شیء برای تناسب‌های ناحیه صفحه‌نمایش که GLSurfaceView در آن نمایش داده می‌شود اصلاح نشده است. در درس بعدی می‌توانید این مشکل را بااستفاده از نمای دوربین و تصویرسازی برطرف کنید.

درنهایت، مثلث ثابت است که کمی خسته‌کننده است. در درس افزودن حرکت، این شکل را می‌چرخانید و از خط لوله گرافیک OpenGL ES استفاده جالب‌تری می‌کنید.