OpenGL ES——一個平平無奇的三角形

前言

隨著VR/AR技術(shù)的普及,人機(jī)交互的模式將產(chǎn)生新的變革。OpenGL ES作為移動端上的圖像渲染框架,將變得越來越重要。在此將學(xué)習(xí)OpenGL ES作為Q3的主要目標(biāo)。在10月1日前,希望能有階段性成果。

快速開始

判斷設(shè)備是否支持OpenGL ES

    fun checkSupported() : Boolean{
        var supportsEs2 = false;
        val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
        val configurationInfo = activityManager.getDeviceConfigurationInfo();
        supportsEs2 = configurationInfo.reqGlEsVersion >= 0x2000;

        val isEmulator = Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
                && (Build.FINGERPRINT.startsWith("generic")
                || Build.FINGERPRINT.startsWith("unknown")
                || Build.MODEL.contains("google_sdk")
                || Build.MODEL.contains("Emulator")
                || Build.MODEL.contains("Android SDK built for x86"));

        supportsEs2 = supportsEs2 || isEmulator

        return supportsEs2

    }

生命周期

    override fun onPause() {
        super.onPause()
        glSurfaceView.let { glSurfaceView.onPause() }
    }

    override fun onResume() {
        super.onResume()
        glSurfaceView.let { glSurfaceView.onResume() }
    }

用OpenGL渲染Activity


    lateinit var glSurfaceView: GLSurfaceView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (checkSupported()) {
            glSurfaceView = GLSurfaceView(this);
            glSurfaceView.let { glSurfaceView.setRenderer(GLRender2())
                setContentView(glSurfaceView); }
        } else {
            Toast.makeText(this, "當(dāng)前設(shè)備不支持OpenGL ES 2.0!", Toast.LENGTH_SHORT).show();
        }

    }

我們可以看到,OpenGL實際的渲染邏輯,全部封裝在了我自己創(chuàng)建的GLRender2中。

以上代碼,就是OpenGL渲染Activity最簡單的外部框架。

渲染邏輯


public class GLRender2 implements GLSurfaceView.Renderer {


    private float[] mTriangleArray = {
            0f, 1f, 0f,
            -1f, -1f, 0f,
            1f, -1f, 0f
    };
    //三角形各頂點顏色(三個頂點)
    private float[] mColor = new float[]{
            1, 1, 0, 1,
            0, 1, 1, 1,
            1, 0, 1, 1
    };
    private FloatBuffer mTriangleBuffer;
    private FloatBuffer mColorBuffer;


    public GLRender2() {
        Log.d("GLRender2" , "call GLRender init");
        //點相關(guān)
        //先初始化buffer,數(shù)組的長度*4,因為一個float占4個字節(jié)
        ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
        //以本機(jī)字節(jié)順序來修改此緩沖區(qū)的字節(jié)順序
        bb.order(ByteOrder.nativeOrder());
        mTriangleBuffer = bb.asFloatBuffer();
        //將給定float[]數(shù)據(jù)從當(dāng)前位置開始,依次寫入此緩沖區(qū)
        mTriangleBuffer.put(mTriangleArray);
        //設(shè)置此緩沖區(qū)的位置。如果標(biāo)記已定義并且大于新的位置,則要丟棄該標(biāo)記。
        mTriangleBuffer.position(0);


        //顏色相關(guān)
        ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
        bb2.order(ByteOrder.nativeOrder());
        mColorBuffer = bb2.asFloatBuffer();
        mColorBuffer.put(mColor);
        mColorBuffer.position(0);
    }

    @Override
    public void onDrawFrame(GL10 gl) {

        Log.d("GLRender2" , "call onDrawFrame");
        // 清除屏幕和深度緩存
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        // 重置當(dāng)前的模型觀察矩陣
        gl.glLoadIdentity();

        // 允許設(shè)置頂點
        //GL10.GL_VERTEX_ARRAY頂點數(shù)組
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // 允許設(shè)置顏色
        //GL10.GL_COLOR_ARRAY顏色數(shù)組
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

        //將三角形在z軸上移動
        gl.glTranslatef(0f, 0.0f, -2.0f);

        // 設(shè)置三角形
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
        // 設(shè)置三角形顏色
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
        // 繪制三角形
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);


        // 取消顏色設(shè)置
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
        // 取消頂點設(shè)置
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

        //繪制結(jié)束
        gl.glFinish();

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.d("GLRender2" , "call onSurfaceChanged");
        float ratio = (float) width / height;
        // 設(shè)置OpenGL場景的大小,(0,0)表示窗口內(nèi)部視口的左下角,(w,h)指定了視口的大小
        gl.glViewport(0, 0, width, height);
        // 設(shè)置投影矩陣
        gl.glMatrixMode(GL10.GL_PROJECTION);
        // 重置投影矩陣
        gl.glLoadIdentity();
        // 設(shè)置視口的大小
        gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
        //以下兩句聲明,以后所有的變換都是針對模型(即我們繪制的圖形)
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.d("GLRender2" , "call onSurfaceCreated");
        // 設(shè)置白色為清屏
        gl.glClearColor(1, 1, 1, 1);

    }

}

以上代碼,渲染出一個變色的三角形:

詳細(xì)介紹

GLRender2

這個平平無奇的三角形,它的渲染邏輯究竟是什么樣的呢?
在此之前,我們需要先了解GLRender2是一個怎樣的類。

GLRender2實現(xiàn)了GLSurfaceView.Renderer接口。需要實現(xiàn)三個方法:


    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.d("GLRender" , "call onSurfaceCreated");
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.d("GLRender" , "call onSurfaceChanged");
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        Log.d("GLRender" , "call onDrawFrame");
    }

生命周期

這是GLSurfaceView生命周期的三個環(huán)節(jié)。

onSurfaceCreated

onSurfaceCreatedGLRender2被初始化后首先調(diào)用。通常用于初始化伴隨GLSurfaceView整個生命周期的數(shù)據(jù)和設(shè)置初始顏色。

onSurfaceChanged

onSurfaceChanged
當(dāng)GLSurfaceView大小改變時,對應(yīng)的Surface大小也會改變。值得注意的是,在Surface剛創(chuàng)建的時候,它的size其實是0,也就是說在畫第一次圖之前它也會被調(diào)用一次的。(而且對于很多時候,Surface的大小是不會改變的,那么此函數(shù)就只在創(chuàng)建之初被調(diào)用一次而已)

原型如下:

public abstract void onSurfaceChanged (GL10 gl, int width, int height)

另外值得注意的是,它告訴了我們這張紙有多高多寬。這點很重要。因為在onSurfaceCreated的時候我們是不知道紙的寬高的,所以有一些和長寬相關(guān)的初始化工作還得在此函數(shù)中來做。

onDrawFrame

以后會有兩種模式供你選擇:

  • RENDERMODE_CONTINUOUSLY
  • RENDERMODE_WHEN_DIRTY

第一種模式(RENDERMODE_CONTINUOUSLY):
連續(xù)不斷的刷,畫完一幅圖馬上又畫下一幅。這種模式很明顯是用來畫動畫的;

第二種模式(RENDERMODE_WHEN_DIRTY):
只有在需要重畫的時候才畫下一幅。這種模式就比較節(jié)約CPU和GPU一些,適合用來畫不經(jīng)常需要刷新的情況。多說一句,系統(tǒng)如何知道需要重畫了呢?當(dāng)然是你要告訴它……
調(diào)用GLSurfaceView的requestRender ()方法,使其重繪。

GLSurfaceView的setRenderMode(int renderMode)方法。可以供你設(shè)置你需要的刷新模式。

設(shè)置背景色

// 設(shè)置白色為清屏
gl.glClearColor(1, 1, 1, 1);

設(shè)置場景大小

 // 設(shè)置OpenGL場景的大小,(0,0)表示窗口內(nèi)部視口的左下角,(w,h)指定了視口的大小
gl.glViewport(0, 0, width, height);

設(shè)置投影矩陣

在渲染中,我們只繪制可見的東西。所以我們需要將真實物體轉(zhuǎn)化到可見區(qū)域,即謂之投影矩陣。

// 設(shè)置投影矩陣
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩陣
gl.glLoadIdentity();
// 設(shè)置視口的大小
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);

這三句將真實物體映射到坐標(biāo)系中。這個地方可能有點難以理解。

雖然在OpenGL中,我們畫的是3D物體,但手機(jī)屏幕畢竟是一個平面。我們在生活中,看見的也只是一個平面。那么,一個3D物體,我們看到的應(yīng)該是什么樣的,取決于我們的投影矩陣如何設(shè)置。

假設(shè),我們的三角形,三個點分別是:

    private float[] mTriangleArray = {
            0f, 1f, -2f,
            -1f, -1f, -2f,
            1f, -1f, -2f
    };

那么,這個三角形其實是在z軸為-2處的一個平面。我們用下圖的方式,進(jìn)行觀察。

下圖,近處的平面,距離視點為1,遠(yuǎn)處的為10。我們畫的三角平面,就在距離視點2的位置。在距離視點1處,我們的視口大小是 2ratio x 2。到距離2處,我們的視口大小一定為2ratio x 2。

所以此時,我們渲染我們的三角形,它的高一定為畫布高度的1/2。

如果我們將近平面,視點距離改為0.5f。同樣的三角形,我們渲染出來高度一定為畫布高度的1/4。

如果我們將三角形改為:

    private float[] mTriangleArray = {
            0f, 1f, -1f,
            -1f, -1f, -2f,
            1f, -1f, -2f
    };

視點距離改為1.0f 。三角形的高度將變?yōu)楫嫴几叨鹊?/4。

而遠(yuǎn)平面的視點距離,則決定了我們可以看到多遠(yuǎn)的元素。比如我們還是三角形為:

    private float[] mTriangleArray = {
            0f, 1f, -1f,
            -1f, -1f, -2f,
            1f, -1f, -2f
    };

將遠(yuǎn)平面視點距離改為1.5f,此時我們將只能看到上個例子中三角形的上半部分。

其中變化讀者可以畫立體圖,慢慢感受一下。

回歸模型

完成了對投影的操作后,我們將操作模式設(shè)置到模型操作。

        //以下兩句聲明,以后所有的變換都是針對模型(即我們繪制的圖形)
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

完成了種種矩陣的設(shè)置后,我們可以開始進(jìn)行繪制了。

圖形和色彩數(shù)據(jù)

OpenGL并不是對堆里面的數(shù)據(jù)進(jìn)行操作,而是在直接內(nèi)存中(Direct Memory),即操作的數(shù)據(jù)需要保存到NIO里面的Buffer對象中。而我們上面聲明的float[]對象保存在堆中,因此,需要我們將float[]對象轉(zhuǎn)為java.nio.Buffer對象。

    private float[] mTriangleArray = {
            0f, 1f, 1f,
            -1f, -1f, 0f,
            1f, -1f, 0f
    };
    //三角形各頂點顏色(三個頂點)
    private float[] mColor = new float[]{
            1, 1, 0, 1,
            0, 1, 1, 1,
            1, 0, 1, 1
    };
    private FloatBuffer mTriangleBuffer;
    private FloatBuffer mColorBuffer;


    public GLRender2() {
        Log.d("GLRender2" , "call GLRender init");
        //點相關(guān)
        //先初始化buffer,數(shù)組的長度*4,因為一個float占4個字節(jié)
        ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
        //以本機(jī)字節(jié)順序來修改此緩沖區(qū)的字節(jié)順序
        bb.order(ByteOrder.nativeOrder());
        mTriangleBuffer = bb.asFloatBuffer();
        //將給定float[]數(shù)據(jù)從當(dāng)前位置開始,依次寫入此緩沖區(qū)
        mTriangleBuffer.put(mTriangleArray);
        //設(shè)置此緩沖區(qū)的位置。如果標(biāo)記已定義并且大于新的位置,則要丟棄該標(biāo)記。
        mTriangleBuffer.position(0);


        //顏色相關(guān)
        ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
        bb2.order(ByteOrder.nativeOrder());
        mColorBuffer = bb2.asFloatBuffer();
        mColorBuffer.put(mColor);
        mColorBuffer.position(0);
    }

繪制

我們在onDrawFrame的生命周期中進(jìn)行繪制。

    @Override
    public void onDrawFrame(GL10 gl) {

        Log.d("GLRender2" , "call onDrawFrame");
        // 清除屏幕和深度緩存
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        // 重置當(dāng)前的模型觀察矩陣
        gl.glLoadIdentity();

        // 允許設(shè)置頂點
        //GL10.GL_VERTEX_ARRAY頂點數(shù)組
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // 允許設(shè)置顏色
        //GL10.GL_COLOR_ARRAY顏色數(shù)組
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

        //將畫筆在z軸上移動
        gl.glTranslatef(0f, 0.0f, -2.0f);

        // 設(shè)置三角形
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
        // 設(shè)置三角形顏色
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
        // 繪制三角形
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

        // 取消顏色設(shè)置
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
        // 取消頂點設(shè)置
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

        //繪制結(jié)束
        gl.glFinish();

    }

繪制的過程比較模式化,不再贅述。大致包含了:

  • 清除緩存
  • 啟動頂點數(shù)組模式
  • 啟動顏色數(shù)組模式
  • 移動畫筆
  • 設(shè)置圖形
  • 設(shè)置顏色
  • 關(guān)閉頂點數(shù)組模式
  • 關(guān)閉顏色數(shù)組模式
  • 繪制結(jié)束

至此,便完成了一個平平無奇的三角形的繪制過程。

如有問題,歡迎指正。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容