openGl学习系列第一篇

278 阅读9分钟

一:openGl 基础知识

opengl是什么 ?openGl能干什么

OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近350个不同的函数调用组成,用来从简单的图形位元绘制复杂的三维景象。而另一种程式介面系统是仅用于Microsoft Windows上的Direct3D。OpenGL常用于CAD、虚拟实境、科学视觉化程式和电子游戏开发。

OpenGL的高效实现(利用图形加速硬件)存在于Windows,部分UNIX平台和Mac OS。这些实现一般由显示装置厂商提供,而且非常依赖于该厂商提供的硬体。开放原始码函式库Mesa是一个纯基于软体的图形API,它的代码兼容于OpenGL。但是,由于许可证的原因,它只声称是一个“非常相似”的API。

OpenGL规范由1992年成立的OpenGL架构评审委员会(ARB)维护。ARB由一些对建立一个统一的、普遍可用的API特别感兴趣的公司组成。根据OpenGL官方网站,2002年6月的ARB投票成员包括3Dlabs、Apple Computer、ATI Technologies、Dell Computer、Evans & Sutherland(英语:Evans & Sutherland)、Hewlett-Packard、IBM、Intel、Matrox、NVIDIA、SGI和Sun Microsystems,Microsoft曾是创立成员之一,但已于2003年3月退出。

----摘自维基百科

是什么?

​ 简单来说 opengl是一种规范,这种规范用在大部分你能看到的电子设备中,如笔记本电脑,显示器,移动端设备等

做什么?

​ 能用来绘制你所能见到的电子设备上需要显示的所有东西,如加载一张图,进行动画的显示等

怎么做?

​ openGl因为是跨平台的,因此你在Window上,android上均能使用,只要下载一些基本组件库,本系列后续会进行使用openGl 在Android 平台上实现代码和Mac上使用C++实现opengl的一些常见入门用法,高级用法学习和使用庆参考教程网站:

opengl学习网站

二:openGl环境搭建

2.1:Mac上我使用的Clion IDE,同时需要你安装glew 和glfw3 ,具体详细搭建过程件请自行百度,提供一篇参考

Mac Clion OpenGL开发环境配置

2.2:Android 上openGl 环境搭建比较简单,搭建方法就是在 AndroidManifest.xml中配置上即可使用,如下:

  <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

2.3:Window上搭建opengl环境,我在Window上使用的是Clion +(glad+glfw)来实现调用使用opengl的,具体详细过程参考如下:

Window下使用CLion搭建opengl 开发环境

三:你好 三角形

1:window +clion + (glad+ glfw) + C++ 使用 版本

3.1:先上代码,讲解在注释,Clion中实现绘制三角形的代码如下:

#include <iostream>
#include "glew.h"
#include "glfw3.h"

using namespace std;

void GlClearError();

bool GlLogCall(const char* fun,const char* file,int  line);

#define ASSERT(x) if(!(x) ) cout<<"file:"<<__FILE__<<",LINE:"<<__LINE__<<",FUNC:"<<__FUNCTION__ <<endl;

//检测错误的宏定义
# define GLCall(x) GlClearError(); \
x;                            \
ASSERT(GlLogCall(#x,__FILE__,__LINE__))

void GlClearError()
{
    while (glGetError() != GL_NO_ERROR);
}

bool GlLogCall(const char *fun, const char *file, int line) {
    while (GLenum error = glGetError()){
        cout<<"OpenGl Error:" <<error<<endl;
        cout<<"file:"<<file<<",LINE:"<<line<<",FUNC:"<<fun <<endl;
        return false;
    }
    return true;
}


//顶点着色器脚本
const char *vertexShaderSource =
        "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"
        "void main()\n"
        "{\n"
        "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
        "}\0";
//片元着色器脚本
const char *fragmentShaderSource =
        "#version 330 core\n"
        "out vec4 FragColor;\n"
        "void main()\n"
        "{\n"
        "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
        "}\n\0";

void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

void key_callback(GLFWwindow   * window, int key, int scancode, int action, int mode)
{
    //如果按下ESC,把windowShouldClose设置为True,外面的循环会关闭应用
    if(key==GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
    std::cout<<"ESC"<<mode;
}
const int GLSL_TYPE_VERTEX = 0;
const int GLSL_TYPE_FRAGMENT = 1;

static unsigned  int VAO = 0;
static unsigned  int VBO = 0;
static unsigned  int EBO = 0;

bool createAndCompileShader(const char* glslStr,int glslType,int &shaderId )
{
    unsigned  int shaderType = -1;
    if(glslType == GLSL_TYPE_VERTEX ){
        shaderType =GL_VERTEX_SHADER;
    } else if(glslType == GLSL_TYPE_FRAGMENT){
        shaderType = GL_FRAGMENT_SHADER;
    } else{
        cout << "please input right glslType\n" <<endl;
        return false;
    }

    GLCall(shaderId = glCreateShader(shaderType));
    GLCall( glShaderSource(shaderId, 1, &glslStr, NULL));
    GLCall( glCompileShader(shaderId));
    int success;
    char infoLog[512];
    GLCall( glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success));
    cout << "createAndCompileShader success is   " << success<< std::endl;
    if (!success)
    {
        GLCall( glGetShaderInfoLog(shaderId, 512, NULL, infoLog));
        if(glslType == GLSL_TYPE_VERTEX){
            cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
            return false;
        } else{
            cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
            return false;
        }
    }
    cout << "success::SHADER::VERTEX::COMPILATION " <<  std::endl;
    return true;
}

int linkProgram(int  &vertexId,int &fragmentId )
{

    //shaderProgram为opengl运行时 GPU上跑的程序Id, 此程序id 会被用来查找顶点和片元的着色器中定义的变量
    unsigned int shaderProgram;
    GLCall( shaderProgram = glCreateProgram());
    GLCall( glAttachShader(shaderProgram, vertexId));
    GLCall( glAttachShader(shaderProgram, fragmentId));
    GLCall( glLinkProgram(shaderProgram));

    // check for linking errors
    int success = -1;
    string infoLog = "";
    GLCall( glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success));
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, const_cast<char *>(infoLog.c_str()));
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    std::cout << "linkProgram success result" << shaderProgram << std::endl;
    glDeleteShader(vertexId);
    glDeleteShader(fragmentId);
    return shaderProgram;
}

void initVertexConfiguration(float * vertexArray,int vertexArrayLength, int * index,int indexLength){
    //创建顶点缓存数组 VAO 顶点数组VBO 和元素缓存数组(元素缓存)EBO
    GLCall( glGenVertexArrays(1, &VAO));
    GLCall( glGenBuffers(1, &VBO));
    GLCall( glGenBuffers(1, &EBO));

    //绑定VAO VBO 和EBO
    GLCall( glBindVertexArray(VAO));
    std::cout << "initVertexConfiguration   sizeof(vertexArray) : " <<  sizeof(vertexArray) << std::endl;
    GLCall(glBindBuffer(GL_ARRAY_BUFFER, VBO));
//    GLCall(glBufferData(GL_ARRAY_BUFFER, sizeof(vertexArray) * sizeof(float), (void *)vertexArray, GL_STATIC_DRAW));
//glBufferData 函数第二个参数 指的是 顶点坐标的长度
    GLCall(glBufferData(GL_ARRAY_BUFFER, vertexArrayLength, (void *)vertexArray, GL_STATIC_DRAW));

    std::cout << "initVertexConfiguration   sizeof(index : " <<  sizeof(index) << std::endl;
    GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO));
    GLCall(glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexLength, index, GL_STATIC_DRAW));

    //关键步骤,指定传递出去的数据之间的排列方式和数据大小 数据类型等
    GLCall(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0));
    GLCall(glEnableVertexAttribArray(0));

    //解绑
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

int main() {
    std::cout << "Hello, World!" << std::endl;

    if(!glfwInit())
        return -1;

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);
    glfwWindowHint(GLFW_OPENGL_PROFILE ,GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);//

    //创建窗口以及上下文,窗口大小为960 * 540,这个值可以随便更改,电脑会根据你配置的值生成对应大小的窗口显示器
    GLFWwindow* window = glfwCreateWindow(960, 540, "hello opengl", NULL, NULL);
    if(!window)
    {
        //创建失败会返回NULL
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent (window);

    // Set the required callback functions
    glfwSetKeyCallback(window, key_callback);

    // Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions
    glewExperimental = GL_TRUE;

    int result = glewInit();
    cout << "result : "<<result<<endl;
    cout << "gl version "<<glGetString(GL_VERSION)<<endl;

    /*****************标准opengl 代码 在不同平台上 使用opengl 均需要以下代码*********************/
    //设置opengl窗口显示的原点和宽高,opengl的显示视图窗口和 我们上面创建的 window窗口一样大小
    glViewport(0,0,960,540);

    //设置刷新频率
    glfwSwapInterval(1);

    //第一步,创建glsl脚本,即顶点着色器和片元着色器 并编译
    int vertexId  = -1;
    int fragmentId  = -1;
    cout << "first step "<<endl;
    createAndCompileShader(vertexShaderSource,GLSL_TYPE_VERTEX,vertexId);
    createAndCompileShader(fragmentShaderSource,GLSL_TYPE_FRAGMENT,fragmentId);
    cout << "first step end,vertexId"<<vertexId<<endl;
    cout << "first step end,fragmentId"<<fragmentId<<endl;

    cout << "Second step "<<endl;
    //第二步:生成使用顶点和片元着色器的opengl 程序 ID
    unsigned  int programId = linkProgram(vertexId,fragmentId);
    cout << "Second step result  programId is  "<< programId <<endl;

    //顶点坐标
    float vertices[] = {
            0.5f,  0.5f, 0.0f,  // top right    //0
            0.5f, -0.5f, 0.0f,  // bottom right //1
            -0.5f, -0.5f, 0.0f,  // bottom left //2
            -0.5f,  0.5f, 0.0f   // top left    //3
    };
    //顶点坐标对应的索引坐标
     int indices[] = {
             // note that we start from 0!
            0, 1, 3,  // first Triangle
            1, 2, 3   // second Triangle
    };
    //第三步:初始化顶坐标相关环境
    initVertexConfiguration(vertices,sizeof(vertices),indices,sizeof(indices));

    while (!glfwWindowShouldClose(window))
    {
        processInput(window);
        //初始化背景颜色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw our first triangle
        GLCall(glUseProgram(programId));

        GLCall(glBindVertexArray(VAO)); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized

        GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0));
        // glBindVertexArray(0); // no need to unbind it every time

        // -------------------------------------------------------------------------------
        GLCall(glfwSwapBuffers(window));
        GLCall(glfwPollEvents());
    }

    //删除
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteProgram(programId);
    /*****************标准opengl 代码 在不同平台上 使用opengl 均需要以上代码*********************/

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;

}
    

CmakeList配置脚本如下:

cmake_minimum_required(VERSION 3.20)
project(OpenGlLearn)

set(CMAKE_CXX_STANDARD 14)

# 添加头文件
set(GLEW_H /usr/local/Cellar/glew/2.2.0_1/include/GL)
set(GLFW_H /usr/local/Cellar/glfw/3.3.4/include/GLFW)
include_directories(${GLEW_H} ${GLFW_H})

set(GLM /Users/didi/code/Clion/openGlTest/glm/)
include_directories(${GLM} ${GLM})

set(GLM_GTC /Users/didi/code/Clion/openGlTest/glm/gtc)
include_directories(${GLM_GTC} ${GLM_GTC})

set(GLM_DETAIL /Users/didi/code/Clion/openGlTest/glm/detail)
include_directories(${GLM_DETAIL} ${GLM_DETAIL})

set(GLM_EXT /Users/didi/code/Clion/openGlTest/glm/ext)
include_directories(${GLM_EXT} ${GLM_EXT})

set(GLM_GTX /Users/didi/code/Clion/openGlTest/glm/gtx)
include_directories(${GLM_GTX} ${GLM_GTX})

set(GLM_SIMD /Users/didi/code/Clion/openGlTest/glm/simd)
include_directories(${GLM_SIMD} ${GLM_SIMD})


# 添加目标链接
set(GLEW_LINK /usr/local/Cellar/glew/2.2.0_1/lib/libGLEW.2.2.dylib)
set(GLFW_LINK /usr/local/Cellar/glfw/3.3.4/lib/libglfw.3.dylib)
#link_libraries(${OPENGL} ${GLEW_LINK} ${GLFW_LINK})
link_libraries("-framework OpenGL" ${GLEW_LINK} ${GLFW_LINK})

# 执行编译命令
set(SOURCE_FILES
        src/main.cpp)
add_executable(openGlTest ${SOURCE_FILES})


if (APPLE)
    target_link_libraries(openGlTest "-framework OpenGL")
    target_link_libraries(openGlTest "-framework GLUT")
endif()

执行之后电脑上显示的窗口如下:

image-20220630232014295.png

具体代码细节讲解:参考注释

代码传送门

2:Android studio +java + openglES 实现 版本

android 实现使用opengl 绘制三角形 其实主要是3步:

  • 创建一个activity,layout文件中设置一个glsurview组件即可,并且制定opengl的版本(不指定版本无法执行后面的代码),为glsurview指定一个实现 GLSurfaceView.Renderer 的render的实例
  • 实例化要绘制界面的的render实例,并重写 onSurfaceCreated,onSurfaceChanged,onDrawFrame三个方法
  • 指定定点着色器和 片元着色器,指定定点坐标,在onDrawFrame中开始绘制
public class SimpleRenderActivity extends Activity {
     GLSurfaceView glSurfaceView;
      @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_render);
        initView();
    } 
    void initView(){

        glSurfaceView = findViewById(R.id.surfaceview_opengles);
        // glSurfaceView.setElevation(2.0f);
        //设置onengl的版本
        glSurfaceView.setEGLContextClientVersion(2);

        //设置渲染器 自实现  背景颜色改变
        //glSurfaceView.setRenderer(new BackGroundRender());

        //设置渲染器 自实现  画三角形
        glSurfaceView.setRenderer(new TriagenRender());

        //设置渲染模式
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }  
    }
public class TriagenRender implements GLSurfaceView.Renderer {

    FloatBuffer floatBuffer ;
    int progrem;
    private int vPosition;
    int colorHandle;
    //设置颜色,依次为红绿蓝和透明通道
    float color[] = { 1.0f, 0f, 0f, 1.0f };

    //三角形定点坐标
    float titagenrender  [] = {
        0.5f, 0.5f, 0f,
        -0.5f,-0.5f,0f,
        0.5f,-0.5f,0f
    };

    //定点着色器
    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;" +
                    "}";

    private String TAG  = "TriagenRender";
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.i(TAG,"onSurfaceCreated 1");
        //1: 申请CPU内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(titagenrender.length  *4);
        byteBuffer.order(ByteOrder.nativeOrder());
        floatBuffer = byteBuffer.asFloatBuffer();

        Log.i(TAG,"onSurfaceCreated 2");
        // 2:将所要做的三角形的坐标输入floatbytebuffer
        floatBuffer.put(titagenrender);
        floatBuffer.position(0);

        Log.i(TAG,"onSurfaceCreated 3");
        //3:创建定点着色器和片元着色器
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

        Log.i(TAG,"onSurfaceCreated 4");
        // 4:创建承载着色器的opengl 方法,并将点点着色器和片元着色器加入
        progrem = GLES20.glCreateProgram();
        GLES20.glAttachShader(progrem,vertexShader);
        GLES20.glAttachShader(progrem,fragmentShader);

        Log.i(TAG,"onSurfaceCreated 5");
        //5:连接程序
        GLES20.glLinkProgram(progrem);

    }
    //创建着色器 (本方法主要为定点着色器和 片元着色器)
    private int loadShader(int type,String shaderCode){
        //根据类型创建着色器
        int shade = GLES20.glCreateShader(type);
        //加载资源
        GLES20.glShaderSource(shade,shaderCode);
        GLES20.glCompileShader(shade);
        return shade;
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

    }

    @Override
    public void onDrawFrame(GL10 gl) {
        Log.i(TAG,"onDrawFrame 1");
        //1:获取程序中的句柄并启用
        GLES20.glUseProgram(progrem);
        vPosition = GLES20.glGetAttribLocation(progrem, "vPosition");
        GLES20.glEnableVertexAttribArray(vPosition);
//        GLES20.glEnable(vPosition);

        Log.i(TAG,"onDrawFrame 2");
        // 2:准备三角形的定点坐标数据
        GLES20.glVertexAttribPointer(vPosition,3,GLES20.GL_FLOAT, false,3*4,floatBuffer);

        Log.i(TAG,"onDrawFrame 3");
        //3:准备着色器片元
        colorHandle = GLES20.glGetUniformLocation(progrem, "vColor");
        GLES20.glUniform4fv(colorHandle,1,color,0);

        Log.i(TAG,"onDrawFrame 4");
        // 4:绘制三角形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,3);
        GLES20.glDisableVertexAttribArray(vPosition);
    }
}

绘制结果展示:

image-20220711224059439.png

代码传送门

四:2种opengl实现方式对比

经过上述代码C++实现版本和android实现版本,opengl的核心不变,主要变化的是其在不同平台上已经不同语言的一些区别,但是基本流程是一样的,主要概括如下:

  • 初始化opengl环境,指定opengl版本,如
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);
    glfwWindowHint(GLFW_OPENGL_PROFILE ,GLFW_OPENGL_CORE_PROFILE);
  glSurfaceView.setEGLContextClientVersion(2);
        //设置渲染器 自实现  画三角形
        glSurfaceView.setRenderer(new TriagenRender());
       //设置渲染模式
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
  • 指定顶点和片元着色器,并且编译,得到gpu opengl运行ID,根据得到的这个ID 获取在定点和片元着色器中定义的各种变量,如顶点坐标和颜色坐标 纹理坐标等,常见的就是这三个坐标
  • 指定运行时的一些参数,关键参数就是告诉opengl 各种定点之间坐标的stride,最后启动绘制。在这个步骤上,C++ 在PC上实现和 Android上实现的代码API 都基本一致
  //关键步骤,指定传递出去的数据之间的排列方式和数据大小 数据类型等
    GLCall(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0));
    GLCall(glEnableVertexAttribArray(0));
    //最后一步启动绘制 C++绘制的是EBO,如绘制VAO 则api 是glDrawArrays
  GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0));
 // 2:准备三角形的定点坐标数据
GLES20.glVertexAttribPointer(vPosition,3,GLES20.GL_FLOAT, false,3*4,floatBuffer);
//启动绘制 java启动绘制的是VAO 
GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,3);

五:结语

经过上面代码细节的对比 ,我们基本了解了什么是opengl,以及opengl在PC上和android上进行入门绘制的基本流程。那么opengl的核心元素就是glsl语言 ,定点着色器 片元着色器 ,VAO EBO等基本元素在不同平台上的绘制都是一样的,那么这些核心元素是什么?如何理解?在何种场合使用?一系列问题的答案我们下回再见。