שרטוט צורות

אחרי שמגדירים צורות לשרטוט באמצעות OpenGL, סביר להניח שתרצו לשרטט אותן. שרטוט צורות ל-OpenGL ES 2.0 נדרש קצת יותר קוד ממה שציפיתם, כי ה-API מספק שליטה רבה בצינור עיבוד הגרפיקה.

בשיעור הזה מוסבר איך לשרטט את הצורות שהגדרתם בשיעור הקודם באמצעות OpenGL API של ES 2.0

אתחול צורות

לפני שמבצעים שרטוט, צריך לאתחל ולטעון את הצורות שמתכננים לשרטט. אלא אם מבנה (הקואורדינטות המקוריות) של הצורות שבהן אתם משתמשים בתוכנית, ששיניתם במהלך הקורס צריך לאתחל אותם אמצעי תשלום אחד (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 לעיבוד הפנים של צורה בצבעים או של טקסטורות.
  • Program (תוכנית) – אובייקט OpenGL ES שמכיל את תוכנות ההצללה שבהן רוצים להשתמש לשרטוט צורה אחת או יותר.

יש צורך בכלי הצללה (shader) אחד של קודקוד כדי לשרטט צורה וכלי הצללה (shader) אחד של מקטעים כדי לצבוע את הצורה הזו. צריך להדר את תוכנות ההצללה האלה ולהוסיף אותן לתוכנת 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 Shading Language (GLSL) שצריך להדר לפני שמשתמשים בו בסביבת OpenGL ES. כדי להדר את הקוד הזה, צריך ליצור method לכלי עזר במחלקה של כלי הרינדור:

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 ואז לקשר את התוכנה. מבצעים את הפעולה הזו באמצעות ה-constructor של האובייקט שציירתם, פעם אחת.

הערה: הידור של תוכנות הצללה (shader) של 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() לשרטוט הצורה. הקוד הזה קובע את המיקום לצבע של תוכנת ההצללה (shader) של הקודקוד של הצורה, ולביצוע של השרטוט. מותאמת אישית.

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 ולהשתמש בו בצורה מעניינת יותר.