OpenGL ES初探(四) -- 用OpenGL畫正四面體,正方體,球

OpenGL ES初探(四) – 用OpenGL畫正四面體,正方體,球

目錄

準備工作

添加初始代碼

  1. 構建包com.yxf.variousshape3d

  2. 將上篇博客的源碼1中的MainActivityCommonUtils還有Point複製到com.yxf.variousshape3d包下;將res/raw/中的着色器代碼複製過來.

  3. com.yxf.variousshape3d下添加MyRender類如下

package com.yxf.variousshapes3d;

import android.content.Context;
import android.opengl.GLSurfaceView;


import com.yxf.variousshapes3d.programs.Program;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import static android.opengl.GLES20.GL_BLEND;
import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT;
import static android.opengl.GLES20.GL_ONE_MINUS_SRC_ALPHA;
import static android.opengl.GLES20.GL_SRC_ALPHA;
import static android.opengl.GLES20.glBlendFunc;
import static android.opengl.GLES20.glClear;
import static android.opengl.GLES20.glClearColor;
import static android.opengl.GLES20.glEnable;
import static android.opengl.GLES20.glViewport;

public class MyRenderer implements GLSurfaceView.Renderer {

    private Context context;

    private List<Program> programs = new CopyOnWriteArrayList<Program>();

    public MyRenderer(Context context) {
        this.context = context;
    }

    public void addProgram(Program program) {
        programs.add(program);
    }

    public void clearProgram() {
        programs.clear();
    }


    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        glClearColor(1f, 1f, 1f, 0f);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        for (Program program : programs) {
            program.onSurfaceCreated(context);
        }
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        glViewport(0, 0, width, height);
        for (Program program : programs) {
            program.onSurfaceChanged(width, height);
        }
    }


    @Override
    public void onDrawFrame(GL10 gl) {
        glClear(GL_COLOR_BUFFER_BIT);
        for (Program program : programs) {
            program.onDrawFrame();
        }
    }

}

  1. com.yxf.variousshapes3d包下創建包programs,然後在programs包下添加Program類如下
package com.yxf.variousshapes3d.programs;

import android.content.Context;


import com.yxf.variousshapes3d.CommonUtils;

import static android.opengl.GLES20.GL_BLEND;
import static android.opengl.GLES20.GL_ONE_MINUS_SRC_ALPHA;
import static android.opengl.GLES20.GL_SRC_ALPHA;
import static android.opengl.GLES20.glBlendFunc;
import static android.opengl.GLES20.glEnable;
import static android.opengl.GLES20.glUseProgram;

public abstract class Program {

    protected int program;

    private int vertexResourceId, fragmentResourceId;

    private ShaderCallback shaderCallback;

    public Program() {

    }

    public void onSurfaceCreated(Context context) {
        ShaderCallback callback = getShaderCallback();
        this.vertexResourceId = callback.getVertexResourceId();
        this.fragmentResourceId = callback.getFragmentResourceId();
        shaderCallback = callback;
        if (callback == null) {
            throw new RuntimeException("the shader callback of program can not is null , program : " + getClass().getName());
        }
        String vertexShaderSource = CommonUtils.readTextFromResource(context, vertexResourceId);
        String fragmentShaderSource = CommonUtils.readTextFromResource(context, fragmentResourceId);
        int vertexShader = CommonUtils.compileVertexShader(vertexShaderSource);
        int fragmentShader = CommonUtils.compileFragmentShader(fragmentShaderSource);
        program = CommonUtils.linkProgram(vertexShader, fragmentShader);
        shaderCallback.initialize(program);
    }

    public void onSurfaceChanged(int width, int height) {

    }


    protected void useProgram() {
        glUseProgram(program);
    }

    public void onDrawFrame() {
        useProgram();
        shaderCallback.prepareDraw(program);
    }

    public abstract ShaderCallback getShaderCallback();

    interface ShaderCallback {

        int getVertexResourceId();

        int getFragmentResourceId();

        void initialize(int program);

        void prepareDraw(int program);
    }
}

結構說明

這次結構和上次結構差異還是有點大的

這次我們基於OpenGL的program擴展出來Program類

然後以Program爲單位,在MyRenderer中draw出每個program

構建ShapeProgram

構思

爲了避免太多的重複工作,我們不需要每個立體圖形都構建一個Program類,我們可以先構建ShapeProgram類和BaseShape類,ShapeProgram負責構建場景和處理矩陣,BaseShape類負責繪製圖形,然後將BaseShape添加進ShapeProgram中達到解耦和簡化的目的.

構建BaseShape

我們在com.yxf.variousshapes3d包下添加shapes包,然後在shapes包下添加BaseShape類如下

package com.yxf.variousshapes3d.shapes;


import com.yxf.variousshapes3d.Point;

import java.nio.FloatBuffer;

import static android.opengl.GLES20.GL_FLOAT;
import static android.opengl.GLES20.glVertexAttribPointer;

public abstract class BaseShape {
    public static final int BYTES_PER_FLOAT = 4;

    public static final int POSITION_COMPONENT_COUNT = 3;
    public static final int COLOR_COMPONENT_COUNT = 4;
    public static final int STRIDE = (POSITION_COMPONENT_COUNT +
            COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
    protected int aPositionLocation;
    protected int aColorLocation;

    protected Point center;
    protected Object mLock = new Object();

    public BaseShape(Point center) {
        this.center = center;
    }

    public void setLocation(int aPositionLocation, int aColorLocation) {
        this.aPositionLocation = aPositionLocation;
        this.aColorLocation = aColorLocation;
    }


    public final void draw() {
        preDraw();
        synchronized (mLock) {
            FloatBuffer vertexData = getVertexData();
            vertexData.position(0);
            glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,
                    false, STRIDE, vertexData);
            vertexData.position(POSITION_COMPONENT_COUNT);
            glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GL_FLOAT,
                    false, STRIDE, vertexData);
            drawArrays();
        }
        afterDraw();
    }

    public void initialize() {
        synchronized (mLock) {
            initWithoutLock();
        }
    }

    public static FloatBuffer encodeVertices(float[] vertices) {
        FloatBuffer vertexData = ByteBuffer.allocateDirect(vertices.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        vertexData.put(vertices);
        return vertexData;
    }

    public abstract void initWithoutLock();

    protected abstract FloatBuffer getVertexData();

    protected abstract void drawArrays();

    protected abstract void preDraw();

    protected abstract void afterDraw();

}

由於OpenGL在Android中其實是有個渲染線程的,所以在其中數據處理的地方加了鎖.

構建ShapeProgram實例

我們再com.yxf.variousshapes3d.programs包下創建ShapeProgram類,繼承於Program類如下

package com.yxf.variousshapes3d.programs;



import com.yxf.variousshapes3d.R;
import com.yxf.variousshapes3d.shapes.BaseShape;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static android.opengl.GLES20.glEnableVertexAttribArray;
import static android.opengl.GLES20.glGetAttribLocation;
import static android.opengl.GLES20.glGetUniformLocation;
import static android.opengl.GLES20.glUniformMatrix4fv;
import static android.opengl.Matrix.orthoM;

public class ShapeProgram extends Program implements Program.ShaderCallback {

    private static final String A_POSITION = "a_Position";
    private int aPositionLocation;

    private static final String U_MATRIX = "u_Matrix";
    private final float[] projectionMatrix = new float[16];
    private int uMatrixLocation;

    private static final String A_COLOR = "a_Color";
    private int aColorLocation;

    private List<BaseShape> shapeList = new CopyOnWriteArrayList<BaseShape>();

    public ShapeProgram() {
        super();
    }

    @Override
    public void onSurfaceChanged(int width, int height) {
        super.onSurfaceChanged(width, height);
        final float aspectRatio = height / (float) width;
        orthoM(projectionMatrix, 0, -1, 1, -aspectRatio, aspectRatio, -1, 1);
    }

    public void addShape(BaseShape baseShape) {
        shapeList.add(baseShape);
        baseShape.initialize();
    }

    public void clearShapeList() {
        shapeList.clear();
    }

    private void initShapeList(float aspectRatio) {
        clearShapeList();
    }

    @Override
    public void onDrawFrame() {
        super.onDrawFrame();
        for (BaseShape baseShape : shapeList) {
            baseShape.setLocation(aPositionLocation, aColorLocation);
            baseShape.draw();
        }
    }

    @Override
    public ShaderCallback getShaderCallback() {
        return this;
    }

    @Override
    public int getVertexResourceId() {
        return R.raw.shape_vertex_shader;
    }

    @Override
    public int getFragmentResourceId() {
        return R.raw.shape_fragment_shader;
    }

    @Override
    public void initialize(int program) {
        aPositionLocation = glGetAttribLocation(program, A_POSITION);
        uMatrixLocation = glGetUniformLocation(program, U_MATRIX);
        aColorLocation = glGetAttribLocation(program, A_COLOR);

        glEnableVertexAttribArray(aPositionLocation);
        glEnableVertexAttribArray(aColorLocation);
    }

    @Override
    public void prepareDraw(int program) {
        glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
    }
}

以上的OpenGL的方法使用在之前博客中都有說明,故不再贅述.更多的是結構的變化,建議下載源碼分析,更容易理解.

值得注意的一點是,BaseShapeCOLOR_COMPONENT_COUNT常量設置成了4,這是我們引入了顏色的第四個分量a,而爲了開啓OpenGL透明度還在MyRenderer.onSurfaceCreated中添加了

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

兩個方法

創建正四面體

基礎類都創建完了我們開始創建實際的繪製類吧

先繪製正四面體,在com.yxf.variousshapes3d.shapes中添加Tetrahedron類繼承於BaseShape

package com.yxf.variousshapes3d.shapes;

import android.graphics.Color;

import com.yxf.variousshapes3d.Point;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;

import static android.opengl.GLES20.GL_TRIANGLES;
import static android.opengl.GLES20.GL_UNSIGNED_BYTE;
import static android.opengl.GLES20.glDrawElements;

public class Tetrahedron extends BaseShape {
    private float[] vertices;
    private FloatBuffer vertexData;
    private float radius;

    private ByteBuffer positionBuffer;

    private int coordinateCount;

    public Tetrahedron(Point center, float radius) {
        super(center);
        vertices = new float[(COLOR_COMPONENT_COUNT + POSITION_COMPONENT_COUNT) * 4];
        this.radius = radius;
        //繪製時所需頂點個數
        coordinateCount = 3 * 4;
    }

    @Override
    public void initWithoutLock() {
        //邊長
        float l = (float) (radius * 4 / Math.sqrt(6));
        //底部三角形高
        float bh = (float) (Math.sqrt(3) / 2 * l);
        //底部三角形中點距離底部三角形三個交點的距離
        float cl = bh * 2 / 3;
        //正四面體的高
        float th = (float) Math.sqrt(l * l - cl * cl);
        //底面距離四面體中心的距離,由(ch + radius) = th 和 radius * radius = cl * cl + ch * ch 可求得ch的值
        float ch = (float) (l / (2 * Math.sqrt(6)));
        int i = 0;
        int color;
        //底部三角形上頂點,0
        vertices[i++] = center.x;
        vertices[i++] = center.y + bh * 2 / 3;
        vertices[i++] = center.z - ch;
        color = Color.BLUE;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //底部三角形左頂點,1
        vertices[i++] = center.x - l / 2;
        vertices[i++] = center.y - bh / 3;
        vertices[i++] = center.z - ch;
        color = Color.GREEN;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //底部三角形右頂點,2
        vertices[i++] = center.x + l / 2;
        vertices[i++] = center.y - bh / 3;
        vertices[i++] = center.z - ch;
        color = Color.YELLOW;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //上定點,3
        vertices[i++] = center.x;
        vertices[i++] = center.y;
        vertices[i++] = center.z + radius;
        color = Color.RED;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        vertexData = encodeVertices(vertices);

        positionBuffer = ByteBuffer.allocateDirect(coordinateCount)
                .put(new byte[]{
                        0, 1, 2,
                        0, 1, 3,
                        2, 0, 3,
                        1, 2, 3,
                });
        positionBuffer.position(0);
    }

    @Override
    protected FloatBuffer getVertexData() {
        return vertexData;
    }

    @Override
    protected void drawArrays() {
        glDrawElements(GL_TRIANGLES, coordinateCount, GL_UNSIGNED_BYTE, positionBuffer);
    }

    @Override
    protected void preDraw() {

    }

    @Override
    protected void afterDraw() {

    }
}

代碼中對於頂點的計算已經寫的差不多了,若不能理解,請查閱高中數學或者百度

上面關鍵點在於使用了一個新的方法glDrawElements

關於這個方法可結合源碼參見Android OpenGL ES 部分函數說明 – gldrawelements

爲了讓應用顯示正四面體我們還得更新下MainActivity.onCreate的代碼如下

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //2,創建GLSurfaceView對象
        glSurfaceView = new GLSurfaceView(this);
        //3,設置OpenGL ES版本爲2.0
        glSurfaceView.setEGLContextClientVersion(2);
        //4,設置渲染器
        MyRenderer renderer = new MyRenderer(this);
        glSurfaceView.setRenderer(renderer);

        //添加Program
        ShapeProgram program = new ShapeProgram();
        program.addShape(new Tetrahedron(new Point(0f, 0.8f, 0f), 0.4f));
        renderer.addProgram(program);

        //5,設置GLSurfaceView爲主窗口
        setContentView(glSurfaceView);
    }

運行程序,會得到如下效果

Tetrahedron

創建正方體

shapes包下添加Cube類如下

package com.yxf.variousshapes3d.shapes;

import android.graphics.Color;

import com.yxf.variousshapes3d.Point;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;

import static android.opengl.GLES20.GL_TRIANGLES;
import static android.opengl.GLES20.GL_UNSIGNED_BYTE;
import static android.opengl.GLES20.glDrawElements;

public class Cube extends BaseShape {

    private float[] vertices;
    private FloatBuffer vertexData;
    private float size;
    private int coordinateCount;
    private ByteBuffer positionBuffer;


    public Cube(Point center, float size) {
        super(center);
        vertices = new float[(COLOR_COMPONENT_COUNT + POSITION_COMPONENT_COUNT) * 8];
        this.size = size;
        //繪製時所需頂點個數
        coordinateCount = 3 * 2 * 6;
    }

    @Override
    public void initWithoutLock() {
        float len = size / 2;
        int color;
        int i = 0;
        //頂面正方形左上角,0
        vertices[i++] = center.x - len;
        vertices[i++] = center.y + len;
        vertices[i++] = center.z + len;
        color = Color.BLUE;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //頂面正方形左下角,1
        vertices[i++] = center.x - len;
        vertices[i++] = center.y - len;
        vertices[i++] = center.z + len;
        color = Color.RED;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //頂面正方形右下角,2
        vertices[i++] = center.x + len;
        vertices[i++] = center.y - len;
        vertices[i++] = center.z + len;
        color = Color.YELLOW;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //頂面正方形右上角,3
        vertices[i++] = center.x + len;
        vertices[i++] = center.y + len;
        vertices[i++] = center.z + len;
        color = Color.GREEN;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //底面正方形左上角,4
        vertices[i++] = center.x - len;
        vertices[i++] = center.y + len;
        vertices[i++] = center.z - len;
        color = Color.GRAY;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //底面正方形左下角,5
        vertices[i++] = center.x - len;
        vertices[i++] = center.y - len;
        vertices[i++] = center.z - len;
        color = Color.CYAN;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //底面正方形右下角,6
        vertices[i++] = center.x + len;
        vertices[i++] = center.y - len;
        vertices[i++] = center.z - len;
        color = Color.GREEN;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        //底面正方形右上角,7
        vertices[i++] = center.x + len;
        vertices[i++] = center.y + len;
        vertices[i++] = center.z - len;
        color = Color.CYAN;
        vertices[i++] = Color.red(color) / 255f;
        vertices[i++] = Color.green(color) / 255f;
        vertices[i++] = Color.blue(color) / 255f;
        vertices[i++] = Color.alpha(color) / 255f;
        vertexData = encodeVertices(vertices);

        positionBuffer = ByteBuffer.allocateDirect(coordinateCount)
                .put(new byte[]{
                        //頂正方形
                        0, 1, 2,
                        0, 2, 3,
                        //底正方形
                        7, 6, 5,
                        7, 5, 4,
                        //前正方形
                        1, 5, 6,
                        1, 6, 2,
                        //後正方形
                        3, 7, 4,
                        3, 4, 0,
                        //左正方形
                        0, 4, 5,
                        0, 5, 1,
                        //右正方形
                        2, 6, 7,
                        2, 7, 3,
                });
        positionBuffer.position(0);
    }

    @Override
    protected FloatBuffer getVertexData() {
        return vertexData;
    }

    @Override
    protected void drawArrays() {
        glDrawElements(GL_TRIANGLES, coordinateCount, GL_UNSIGNED_BYTE, positionBuffer);
    }

    @Override
    protected void preDraw() {

    }

    @Override
    protected void afterDraw() {

    }
}

繪製過程和繪製正四面體幾乎一致便不再解釋

MainActivity,onCreate()中添加如下代碼

program.addShape(new Cube(new Point(0f, 0f, 0f), 0.6f));

運行程序會獲得如下效果
cube

然而繪製的是正方體,看起來卻是正方形,根本沒有立體感可言,如何處理這個問題,我們先放一放,等繪製完球體再討論

創建球形

shapes包下添加Ball類如下

package com.yxf.variousshapes3d.shapes;

import android.graphics.Color;

import com.yxf.variousshapes3d.Point;

import java.nio.FloatBuffer;

import static android.opengl.GLES20.GL_TRIANGLE_STRIP;
import static android.opengl.GLES20.glDrawArrays;

public class Ball extends BaseShape {
    private float radius;
    private int circleCount;
    private float angleStep;

    private float[] vertices;
    private FloatBuffer vertexData;

    private final int bottomColor = Color.BLUE;
    private final int topColor = Color.GREEN;
    private int pointCount = 0;

    public Ball(Point center, float radius, int circleCount) {
        super(center);
        this.radius = radius;
        this.circleCount = circleCount;

        angleStep = (float) (Math.PI * 2 / circleCount);


    }

    @Override
    public void initWithoutLock() {
        int br = Color.red(bottomColor);
        int bg = Color.green(bottomColor);
        int bb = Color.blue(bottomColor);
        int ba = Color.alpha(bottomColor);

        int tr = Color.red(topColor);
        int tg = Color.green(topColor);
        int tb = Color.blue(topColor);
        int ta = Color.alpha(topColor);

        int zCount = circleCount / 2 + 1;
        int cCount = circleCount + 1;
        int len = POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT;
        pointCount = zCount * cCount * 2;
        vertices = new float[len * pointCount];
        int i = 0;
        for (int k = 0; k < zCount; k++) {
            float zAngle = (float) (-Math.PI / 2 + k * angleStep);
            float r = (float) (radius * Math.cos(zAngle));
            for (int j = 0; j < cCount; j++) {
                int index;
                if (k == 0) {
                    index = i / len;
                    vertices[i++] = center.x;
                    vertices[i++] = center.y;
                    vertices[i++] = center.z - radius;
                    vertices[i++] = br;
                    vertices[i++] = bg;
                    vertices[i++] = bb;
                    vertices[i++] = ba;
                    i += len;
                } else {
                    index = i / len;
                    vertices[i++] = center.x + (float) (r * Math.cos(j * angleStep));
                    vertices[i++] = center.y + (float) (r * Math.sin(j * angleStep));
                    vertices[i++] = center.z + (float) (radius * Math.sin(zAngle));
                    vertices[i++] = getMixtureColor(br, tr, index, pointCount);
                    vertices[i++] = getMixtureColor(bg, tg, index, pointCount);
                    vertices[i++] = getMixtureColor(bb, tb, index, pointCount);
                    vertices[i++] = getMixtureColor(ba, ta, index, pointCount);

                    int strip = len * 2 * cCount;
                    int t = i - len;
                    vertices[i++ - strip] = vertices[t++];
                    vertices[i++ - strip] = vertices[t++];
                    vertices[i++ - strip] = vertices[t++];
                    vertices[i++ - strip] = vertices[t++];
                    vertices[i++ - strip] = vertices[t++];
                    vertices[i++ - strip] = vertices[t++];
                    vertices[i++ - strip] = vertices[t++];
                }
            }
        }
        vertexData = encodeVertices(vertices);
    }

    private float getMixtureColor(int firstColor, int secondColor, int index, int count) {
        return (firstColor + (secondColor - firstColor) * index / count) / 255f;
    }

    @Override
    protected FloatBuffer getVertexData() {
        return vertexData;
    }

    @Override
    protected void drawArrays() {
        glDrawArrays(GL_TRIANGLE_STRIP, 0, pointCount);
    }

    @Override
    protected void preDraw() {

    }

    @Override
    protected void afterDraw() {

    }
}

這次的繪製我們沒有再使用glDrawElements(),而是使用回了glDrawArrays(),並且用了GL_TRIANGLE_STRIP屬性,這個也在之前博客中講過,故而不再贅述,參見Android OpenGL ES 部分方法說明 – glDrawArrays

繪製過程則是採用從球底部到頂部一圈一圈繪製的方式繪製的,由於底部和頂部的小圈也用了和中間一樣那麼多的頂點來繪製,故而感覺性能並沒有達到最佳.在這裏只做簡單的示例,有興趣的同學可以自己嘗試考慮讓繪製頂點數隨着接近上下兩端而減小,以獲得更好的性能.

然後在MainActivity,onCreate()中添加如下代碼

program.addShape(new Ball(new Point(0f, -0.8f, 0f), 0.6f));

運行程序會獲得如下效果
ball

這個球立體感也不強,那麼接下來我們正式來解決這個問題吧

引入透視投影

之前我們使用的是正交投影,正交投影的屬性是無論距離遠近,大小都一樣,而正交投影也大多數時間是用於二維的

接下來我們將建立透視投影,透視投影是近大遠小的,完全相同的兩個物體,不同的距離,兩物體所有相同點的連線都將交於一點.

首先在CommonUtils類中添加如下方法

    public static void perspectiveM(float[] m, float yFovInDegrees, float aspect, float n, float f) {
        final float angleInRadians = (float) (yFovInDegrees * Math.PI / 180.0);

        final float a = (float) (1.0 / Math.tan(angleInRadians / 2.0));

        int i = 0;

        m[i++] = a / aspect;
        m[i++] = 0f;
        m[i++] = 0f;
        m[i++] = 0f;

        m[i++] = 0f;
        m[i++] = a;
        m[i++] = 0f;
        m[i++] = 0f;

        m[i++] = 0f;
        m[i++] = 0f;
        m[i++] = -((f + n) / (f - n));
        m[i++] = -1f;

        m[i++] = 0f;
        m[i++] = 0f;
        m[i++] = -(2f * f * n) / (f - n);
        m[i++] = 0f;
    }

此方法用於創建一個透視投影矩陣

方法參數說明如下

image

ShapeProgram頭部增加兩個全局變量widthheight

更新ShapeProgram.onSurfaceChanged()如下

    @Override
    public void onSurfaceChanged(int width, int height) {
        super.onSurfaceChanged(width, height);
        this.width = width;
        this.height = height;
        initProjectionMatrix();
    }

ShapeProgram頭部加入modeMatrix的定義

private final float[] modeMatrix = new float[16];
    private void initProjectionMatrix() {
        synchronized (mMatrixLock) {
            //構建透視投影矩陣
            CommonUtils.perspectiveM(projectionMatrix, 45, (float) width
                    / (float) height, 1f, 10f);
            //重置模型矩陣
            setIdentityM(modeMatrix, 0);
            //設置相機位置
            setLookAtM(modeMatrix, 0, 0f, 1.2f, 2.2f, 0f, 0f, 0f, 0f, 1f, 0f);
            //將模型矩陣和透視投影矩陣相乘獲得新的投影矩陣
            final float[] temp = new float[16];
            multiplyMM(temp, 0, projectionMatrix, 0, modeMatrix, 0);
            System.arraycopy(temp, 0, projectionMatrix, 0, temp.length);
        }
    }

上訴方法中引入了一個新的矩陣處理方法setLookAtm,
這個方法是用來設置觀察者的位置和視覺方向的,可以將其認爲是攝像機,其方法參數說明如下
setLookAtm

運行程序會獲得如下效果

3d

增加觸摸事件

現在我們有3D效果了,但是卻還沒有全貌

我們再加入觸摸事件,讓我們可以通過滑動來旋轉圖形以觀察全貌

ShapeProgram類頭部添加rotateX,rotateY,rotateZ來記錄旋轉值.

加入

private Object mMatrixLock = new Object();

來控制矩陣的數據和旋轉角同步

然後在ShapeProgram中添加如下方法

    public void rotate(float x, float y, float z) {
        synchronized (mMatrixLock) {
            rotateX += x;
            rotateY += y;
            rotateZ += z;
        }
    }

並修改ShapeProgram.onPreDraw()方法如下

    @Override
    public void prepareDraw(int program) {
        synchronized (mMatrixLock) {
            initProjectionMatrix();
            setIdentityM(modeMatrix, 0);
            rotateM(modeMatrix, 0, rotateX, 1f, 0f, 0f);
            rotateM(modeMatrix, 0, rotateY, 0f, 1f, 0f);
            rotateM(modeMatrix, 0, rotateZ, 0f, 0f, 1f);
            float[] temp = new float[16];
            multiplyMM(temp, 0, projectionMatrix, 0, modeMatrix, 0);
            System.arraycopy(temp, 0, projectionMatrix, 0, temp.length);
            Log.d("VariousShapes", "x ; " + rotateX + " , y : " + rotateY);
            glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
        }
    }

這樣我們的ShapeProgram就具備了旋轉功能

然後修改MainActivity,在onCreate最後添加如下代碼

        glSurfaceView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        lastX = (int) event.getX();
                        lastY = (int) event.getY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int x = (int) event.getX();
                        int y = (int) event.getY();
                        int dx = x - lastX;
                        int dy = y - lastY;
                        lastX = x;
                        lastY = y;
                        program.rotate(dy / 10f,dx / 10f, 0f);
                        break;
                }
                return true;
            }
        });

運行程序,便可以通過滑動控制旋轉圖形了
rotate

但是會發現有個缺點,我們顏色明明不是透明的,但是卻可以看到對面背部的顏色,這是非常影響視覺效果的,那麼該如何處理呢?

背面裁剪

通過背面裁剪的方式我們可以將背對相機的顏色影藏掉

MyRender.onCreate()中添加如下代碼

glEnable(GL_CULL_FACE);

運行程序獲得效果如圖
cull_face

卷繞

然而細心一點又會發現,上面正四面體旋轉後消失了

這是爲什麼呢?

原來在我們啓用背面裁剪時,OpenGL也是會根據是否順時針繪製還是逆時針繪製來判斷繪製哪面.

默認繪製點連線程逆時針的那一面.

這說明我們上面代碼中的正四面體是存在順時針的點的.

查看代碼確實,在positionBuffer中,定義了一組0,1,2的點是順時針的.

修改positionBuffer的值如下

        positionBuffer = ByteBuffer.allocateDirect(coordinateCount)
                .put(new byte[]{
                        0, 2, 1,
                        0, 1, 3,
                        2, 0, 3,
                        1, 2, 3,
                });

將0,1,2改成0,2,1後再運行下應該就沒有上面的問題了.

示例源碼

OpenGL-VariousShapes3D

參考

OpenGL ES應用開發實踐指南 Android卷

相關鏈接

OpenGL ES 中 GLSL Shader常見屬性說明

Android OpenGL ES 部分方法說明

OpenGL ES初探(一) – 用OpenGL畫一個三角形(1)

OpenGL ES初探(三) – 用OpenGL畫正方形,圓,正多邊形,圓環,正五角星


附錄

最初的源碼

附1

MainActivity.java

package com.yxf.variousshapes;

import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    //1,定義GLSurfaceView對象,這個View提供了OpenGL ES的顯示窗口
    private GLSurfaceView glSurfaceView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //2,創建GLSurfaceView對象
        glSurfaceView = new GLSurfaceView(this);
        //3,設置OpenGL ES版本爲2.0
        glSurfaceView.setEGLContextClientVersion(2);
        //4,設置渲染器
        glSurfaceView.setRenderer(new MyRenderer(this));
        //5,設置GLSurfaceView爲主窗口
        setContentView(glSurfaceView);
    }

    @Override
    protected void onPause() {
        super.onPause();
        //6.1,當Activity暫停時暫停glSurfaceView
        glSurfaceView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        //6.2,當Activity恢復時恢復glSurfaceView
        glSurfaceView.onResume();
    }
}

附2

CommonUtils.java

package com.yxf.variousshapes;

import android.content.Context;
import android.util.Log;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import static android.opengl.GLES20.GL_COMPILE_STATUS;
import static android.opengl.GLES20.GL_FRAGMENT_SHADER;
import static android.opengl.GLES20.GL_LINK_STATUS;
import static android.opengl.GLES20.GL_VERTEX_SHADER;
import static android.opengl.GLES20.glAttachShader;
import static android.opengl.GLES20.glCompileShader;
import static android.opengl.GLES20.glCreateProgram;
import static android.opengl.GLES20.glCreateShader;
import static android.opengl.GLES20.glGetProgramInfoLog;
import static android.opengl.GLES20.glGetProgramiv;
import static android.opengl.GLES20.glGetShaderInfoLog;
import static android.opengl.GLES20.glGetShaderiv;
import static android.opengl.GLES20.glLinkProgram;
import static android.opengl.GLES20.glShaderSource;

//1,靜態導入導入OpenGL ES 2.0常用方法

public class CommonUtils {

    private static final String TAG = "CommonUtils";

    /**
     * 用於讀取GLSL Shader文件內容
     *
     * @param context
     * @param resId
     * @return
     */
    public static String readTextFromResource(Context context, int resId) {
        StringBuilder builder = new StringBuilder();
        InputStream inputStream = null;
        InputStreamReader inputStreamReader = null;
        BufferedReader reader = null;
        try {
            inputStream = context.getResources().openRawResource(resId);
            inputStreamReader = new InputStreamReader(inputStream);
            reader = new BufferedReader(inputStreamReader);

            String nextLine;
            while ((nextLine = reader.readLine()) != null) {
                builder.append(nextLine);
                builder.append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (inputStreamReader != null) {
                    inputStreamReader.close();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return builder.toString();
    }

    /**
     * 編譯着色器
     *
     * @param type
     * @param source
     * @return
     */
    public static int compileShader(int type, String source) {
        final int shaderId = glCreateShader(type);
        if (shaderId == 0) {
            //2,如果着色器創建失敗則會返回0
            Log.w(TAG, "Could not create new shader");
            return 0;
        }
        //3,將Shader源文件加載進ID爲shaderId的shader中
        glShaderSource(shaderId, source);
        //4,編譯這個shader
        glCompileShader(shaderId);
        final int[] status = new int[1];
        //5,獲取編譯狀態儲存於status[0]
        glGetShaderiv(shaderId, GL_COMPILE_STATUS, status, 0);

        Log.v(TAG, "compile source : \n" + source + "\n" +
                "info log : " + glGetShaderInfoLog(shaderId));
        if (status[0] == 0) {
            //6,檢查狀態是否正常,0爲不正常
            Log.w(TAG, "Compilation of shader failed.");
            return 0;
        }
        return shaderId;
    }

    /**
     * 編譯頂點着色器
     *
     * @param source
     * @return
     */
    public static int compileVertexShader(String source) {
        return compileShader(GL_VERTEX_SHADER, source);
    }

    /**
     * 編譯片段着色器
     *
     * @param source
     * @return
     */
    public static int compileFragmentShader(String source) {
        return compileShader(GL_FRAGMENT_SHADER, source);
    }

    /**
     * 創建OpenGL對象,並添加着色器,返回OpenGL對象Id
     * @param vertexShaderId
     * @param fragmentShaderId
     * @return
     */
    public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
        //7,創建OpenGL對象
        final int programId = glCreateProgram();
        if (programId == 0) {
            Log.w(TAG, "Create OpenGL program failed");
            return 0;
        }

        //8,在program上附上着色器
        glAttachShader(programId, vertexShaderId);
        glAttachShader(programId, fragmentShaderId);
        //9,鏈接程序
        glLinkProgram(programId);
        final int[] status = new int[1];
        glGetProgramiv(programId, GL_LINK_STATUS, status, 0);
        Log.v(TAG, "Results of linking program : \n" + glGetProgramInfoLog(programId));
        if (status[0] == 0) {
            Log.w(TAG, "Link program failed");
            return 0;
        }
        return programId;
    }
}

附3

Point.java


package com.yxf.variousshapes;

public class Point {
    public float x;
    public float y;
    public float z;

    public Point(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

附4

shape_vertex_shader.glsl

uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec4 a_Color;

varying vec4 v_Color;
void main() {
    v_Color = a_Color;
    gl_Position = u_Matrix * a_Position;
}

shape_fragment_shader.glsl

precision mediump float;

varying vec4 v_Color;

void main(){
    gl_FragColor = v_Color;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章