在前面我们已经在NDK层搭建好了Opengl ES之EGL环境搭建,也介绍了一些着色器相关的理论知识:Opengl ES之着色器,那么这次我们就使用已经搭配的EGL绘制一个三角形吧。

在Opengl ES的世界中,无论多复杂的形状都是由点、线或三角形组成的。因此三角形的绘制在Opengl ES中相当重要,犹比武林高手的内功心法...

C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

坐标系

在Opengl ES中有很多坐标系,今天我们首先了解一些标准化的设备坐标。

标准化设备坐标(Normalized Device Coordinates, NDC),一旦你的顶点坐标已经在顶点着色器中处理过,它们就是标准化设备坐标了, 标准化设备坐标是一个x、y和z的值都在-1.0到1.0的之间,任何落在-1和1范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。

如下图,在在标准化设备坐标中,假设有一个正方形的屏幕,那么屏幕中心就是坐标原点,左上角就是坐标(-1,1),右下角则是坐标(1,-1)。

opengl绘制梯形(OpenglES之三角形绘制)(1)

上代码

这里需要说明亮点:

  1. 在后续的实战例子中,经常会复用到前面介绍的demo的代码,因此如果是复用之前的代码逻辑,为了节省篇幅,笔者就不重复贴了。
  2. 在demo中为了简洁,并没有开启子线程作为GL线程,很明显这是不对,实际开发中都应该开启子线程对Opengl进行操作。

首先为了后续方便使用,我们在Java层和C 分别创建一个BaseOpengl的基类:

BaseOpengl.java public class BaseOpengl { // 三角形 public static final int DRAW_TYPE_TRIANGLE = 0; public long glNativePtr; protected eglHelper eglHelper; protected int drawType; public BaseOpengl(int drawType) { this.drawType = drawType; this.eglHelper = new EGLHelper(); } public void surfaceCreated(Surface surface) { eglHelper.surfaceCreated(surface); } public void surfaceChanged(int width, int height) { eglHelper.surfaceChanged(width,height); } public void surfaceDestroyed() { eglHelper.surfaceDestroyed(); } public void release(){ if(glNativePtr != 0){ n_free(glNativePtr,drawType); glNativePtr = 0; } } public void onGlDraw(){ if(glNativePtr == 0){ glNativePtr = n_gl_nativeInit(eglHelper.nativePtr,drawType); } if(glNativePtr != 0){ n_onGlDraw(glNativePtr,drawType); } } // 绘制 private native void n_onGlDraw(long ptr,int drawType); protected native long n_gl_nativeInit(long eglPtr,int drawType); private native void n_free(long ptr,int drawType); }

下面是C 的BaseOpengl:

C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

BaseOpengl.h #ifndef NDK_OPENGLES_LEARN_BASEOPENGL_H #define NDK_OPENGLES_LEARN_BASEOPENGL_H #include "../eglhelper/EglHelper.h" #include "GLES3/gl3.h" #include <string> class BaseOpengl { public: EglHelper *eglHelper; GLint program{0}; public: BaseOpengl(); // 析构函数必须是虚函数 virtual ~BaseOpengl(); // 加载着色器并链接成程序 void initGlProgram(std::string ver,std::string fragment); // 绘制 virtual void onDraw() = 0; }; #endif //NDK_OPENGLES_LEARN_BASEOPENGL_H

注意基类的析构函数一定要是虚函数,为什么?如果不是虚函数的话则会导致无法完全析构,具体原因请大家面向搜索引擎编程。

BaseOpengl.cpp #include "BaseOpengl.h" #include "../utils/ShaderUtils.h" BaseOpengl::BaseOpengl() { } void BaseOpengl::initGlProgram(std::string ver, std::string fragment) { program = createProgram(ver.c_str(),fragment.c_str()); } BaseOpengl::~BaseOpengl(){ eglHelper = nullptr; if(program != 0){ glDeleteProgram(program); } }

然后使用BaseOpengl自定义一个SurfaceView,为MyGLSurfaceView:

public class MyGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback { public BaseOpengl baseOpengl; private OnDrawListener onDrawListener; public MyGLSurfaceView(Context context) { this(context,null); } public MyGLSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); getHolder().addCallback(this); } public void setBaseOpengl(BaseOpengl baseOpengl) { this.baseOpengl = baseOpengl; } public void setOnDrawListener(OnDrawListener onDrawListener) { this.onDrawListener = onDrawListener; } @Override public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { if(null != baseOpengl){ baseOpengl.surfaceCreated(surfaceHolder.getSurface()); } } @Override public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int w, int h) { if(null != baseOpengl){ baseOpengl.surfaceChanged(w,h); } if(null != onDrawListener){ onDrawListener.onDrawFrame(); } } @Override public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) { if(null != baseOpengl){ baseOpengl.surfaceDestroyed(); } } public interface OnDrawListener{ void onDrawFrame(); } }

有了以上基类,既然我们的目标是绘制一个三角形,那么我们在Java层和C 层再新建一个TriangleOpengl的类吧,他们都继承TriangleOpengl:

C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

TriangleOpengl.java public class TriangleOpengl extends BaseOpengl{ public TriangleOpengl() { super(BaseOpengl.DRAW_TYPE_TRIANGLE); } }

C TriangleOpengl类,TriangleOpengl.h:

#ifndef NDK_OPENGLES_LEARN_TRIANGLEOPENGL_H #define NDK_OPENGLES_LEARN_TRIANGLEOPENGL_H #include "BaseOpengl.h" class TriangleOpengl: public BaseOpengl{ public: TriangleOpengl(); virtual ~TriangleOpengl(); virtual void onDraw(); private: GLint positionHandle{-1}; GLint colorHandle{-1}; }; #endif //NDK_OPENGLES_LEARN_TRIANGLEOPENGL_H

TriangleOpengl.cpp:

#include "TriangleOpengl.h" #include "../utils/Log.h" // 定点着色器 static const char *ver = "#version 300 es\n" "in vec4 aColor;\n" "in vec4 aPosition;\n" "out vec4 vColor;\n" "void main() {\n" " vColor = aColor;\n" " gl_Position = aPosition;\n" "}"; // 片元着色器 static const char *fragment = "#version 300 es\n" "precision mediump float;\n" "in vec4 vColor;\n" "out vec4 fragColor;\n" "void main() {\n" " fragColor = vColor;\n" "}"; // 三角形三个顶点 const static GLfloat VERTICES[] = { 0.0f,0.5f, -0.5f,-0.5f, 0.5f,-0.5f }; // rgba const static GLfloat COLOR_ICES[] = { 0.0f,0.0f,1.0f,1.0f }; TriangleOpengl::TriangleOpengl():BaseOpengl() { initGlProgram(ver,fragment); positionHandle = glGetAttribLocation(program,"aPosition"); colorHandle = glGetAttribLocation(program,"aColor"); LOGD("program:%d",program); LOGD("positionHandle:%d",positionHandle); LOGD("colorHandle:%d",colorHandle); } TriangleOpengl::~TriangleOpengl() noexcept { } void TriangleOpengl::onDraw() { LOGD("TriangleOpengl onDraw"); glClearColor(0.0f, 1.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(program); /** * size 几个数字表示一个点,显示是两个数字表示一个点 * normalized 是否需要归一化,不用,这里已经归一化了 * stride 步长,连续顶点之间的间隔,如果顶点直接是连续的,也可填0 */ glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,VERTICES); // 启用顶点数据 glEnableVertexAttribArray(positionHandle); // 这个不需要glEnableVertexAttribArray glVertexAttrib4fv(colorHandle, COLOR_ICES); glDrawArrays(GL_TRIANGLES,0,3); glUseProgram(0); // 禁用顶点 glDisableVertexAttribArray(positionHandle); if(nullptr != eglHelper){ eglHelper->swapBuffers(); } LOGD("TriangleOpengl onDraw--end"); }

在前面的章节中我们介绍了着色器的创建、编译、链接等,但是缺少了具体使用方式,这里我们补充说明一下。

着色器的使用只要搞懂如何传递数据给着色器中变量。首先我们需要获取到着色器程序中的变量,然后赋值。

我们看上面的TriangleOpengl.cpp的构造函数:

TriangleOpengl::TriangleOpengl():BaseOpengl() { initGlProgram(ver,fragment); // 获取aPosition变量 positionHandle = glGetAttribLocation(program,"aPosition"); // 获取aColor colorHandle = glGetAttribLocation(program,"aColor"); LOGD("program:%d",program); LOGD("positionHandle:%d",positionHandle); LOGD("colorHandle:%d",colorHandle); }

由上,我们通过函数glGetAttribLocation获取了变量aPosition和aColor的句柄,这里我们定义的aPosition和aColor是向量变量,如果我们定义的是uniform统一变量的话,则需要使用函数glGetUniformLocation获取统一变量句柄。 有了这些变量句柄,我们就可以通过这些变量句柄传递函数给着色器程序了,具体可参考TriangleOpengl.cpp的onDraw函数。

此外如果变量是一个统一变量(uniform)的话,则通过一系列的 glUniform...函数传递参数。

这里说明一下函数glVertexAttribPointer的stride参数,一般情况下不会用到,传递0即可,但是如果需要提高性能,例如将顶点坐标和纹理/颜色坐标等放在同一个数组中传递,则需要使用到这个stride参数了,目前顶点坐标数组和其他数组是分离的,暂时可以不管。

C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

在Activity中调用一下测试结果:

public class DrawTriangleActivity extends AppCompatActivity { private TriangleOpengl mTriangleOpengl; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_draw_triangle); MyGLSurfaceView glSurfaceView = findViewById(R.id.my_gl_surface_view); mTriangleOpengl = new TriangleOpengl(); glSurfaceView.setBaseOpengl(mTriangleOpengl); glSurfaceView.setOnDrawListener(new MyGLSurfaceView.OnDrawListener() { @Override public void onDrawFrame() { mTriangleOpengl.onGlDraw(); } }); } @Override protected void onDestroy() { if(null != mTriangleOpengl){ mTriangleOpengl.release(); } super.onDestroy(); } }

如果运行起来,看到一个蓝色的三角形,则说明三角形绘制成功啦!

opengl绘制梯形(OpenglES之三角形绘制)(2)

如果你对音视频开发感兴趣,或者对本文的一些阐述有自己的看法,可以在下方的留言框给我留言,一起探讨。

,