OpenGL从入门到精通--你好三角形
三角形
github源码仓库
opengl环境准备
opengl编程从入门到精通-hello,window
OpenGL从入门到精通–你好三角形
OpenGL从入门到精通–着色器的使用
绘图中需要牢记下面这几个单词
- 顶点数组对象:
Vertex Array Object, VAO
- 顶点缓冲对象:
Vertex Buffer Object, VBO
- 索引缓冲区:
Element Buffer Object, EBO或Index Buffer Object, IBO
在
OpenGL
中,任何事物都在3D
空间中,而屏幕和窗口却是2D
像素数组,这导致OpenGL
的大部分工作都是关于把3D
坐标转变为适应你屏幕的2D
像素。3D
坐标转为2D
坐标的处理过程是由OpenGL
的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D
坐标转换为2D
坐标,第二部分是把2D
坐标转变为实际的有颜色的像素。这个教程里,我们会简单地讨论一下图形渲染管线,以及如何利用它创建一些漂亮的像素。
图形渲染管线接受一组
3D
坐标,然后把它们转变为你屏幕上的有色2D
像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU
上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
OpenGL
着色器是用OpenGL
着色器语言(OpenGL
Shading Language,GLSL
)写成的,在下一节中我们再花更多时间研究它。
顶点的输入
想绘制一个图形,肯定得给出图像的坐标点,这里我们直接给出的坐标点就是标准化之后的坐标点,所以在设置向量属性的时候,设置的也是GL_FALSE
不需要再进一步的进行标准化。
顶点信息
float vertices[] = {-0.5f, -0.5f, 0.0f, // left0.5f, -0.5f, 0.0f, // right0.0f, 0.5f, 0.0f // top
};
设置顶点属性也选择GL_FALSE
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
因为OpenGL
想要快速的绘制一张图形,一般都是走的GPU
因此这里绘制图形也是要用到GPU
关于GPU
着色器的介绍可以参考:
OpenGL-你好三角形
下面要做的就是把顶点信息放到缓存中去
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
/* 要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用 */
/* // ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..// 1. 绑定VAO */
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 把之前定义的顶点,复制到缓冲的内存中去
/*GL_STATIC_DRAW :数据不会或几乎不会改变。GL_DYNAMIC_DRAW:数据会被改变很多。GL_STREAM_DRAW :数据每次绘制时都会改变。 */
/*三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。 */
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);/*位置数据被储存为32位(4字节)浮点值。每个位置包含3个这样的值。在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。数据中第一个值在缓冲开始的位置 */
// 告诉GPU数据怎样取
/*第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。 */
// 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
/*现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。 */
glEnableVertexAttribArray(0);// note that this is allowed, the call to glVertexAttribPointer registered
//VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);// You can unbind the VAO afterwards so other VAO calls
//won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways
//so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);
着色器
着色器创建需要创建顶点着色器和片段着色器,然后使用链接程序把两个着色器链接,需要注意的是链接程序链接的着色器前一个程序的输出,必须要和后面一个程序的输入对应,否则就会报错。
- 创建着色器
// build and compile our shader program
// ------------------------------------
// vertex shader
// 创建一个着色器对象, 返回出着色器的ID
// 顶点着色器
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
因为着色器是运行在GPU
上的一个个小程序,因此需要动态编译,需要时将编译好的程序交给GPU
运行,使用的语言是GLSL
需要了解的可以看
着色器
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";
// 将着色器源码附加到着色器对象上,然后使用glCompileShader进行编译
// param1 着色器ID, param2 源码字符串数量
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
因为传入的是程序片段,我们需要知道程序是运行时出错还是根本就编译不过,可以使用glGetShaderiv
函数获取上述程序编译的状态
// check for shader compile errors
int success; // 获取程序编译的状态,0成功非0失败
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{// 程序编译失败的信息glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
用同样的方法处理片段着色器
// fragment shader
// 片段着色器
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
使用连接器链接两个着色器
// link shaders/*当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。*/int shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);// check for linking errorsglGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);if (!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;}
在Render
里面渲染三角形
// draw our first triangle
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
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
// 3. 绘制物体 GL_TRIANGLES 绘制三角形,从0起点开始绘制,绘制3个点
glDrawArrays(GL_TRIANGLES, 0, 3);
完整代码实现
//
// Created by andrew on 2021/1/17.
//
#include "glad/glad.h"
#include <GLFW/glfw3.h>#include <iostream>using namespace std;void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;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";int main()
{// 对glfw进行初始化glfwInit();// 打印出glfw的版本信息// int* major, int* minor, int* revint major, minor, rev;glfwGetVersion(&major, &minor, &rev);cout << "major = " << major << " minor = " << minor << " rev = " << rev << endl;glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// glfw window creation// glfw创建窗口GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr);if (window == nullptr){std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return -1;}// 为当前window设置上下文,每个线程只能设置一个,并且线程之间共用时,需要将当前线程设置为 non-currentglfwMakeContextCurrent(window);// 设置窗口大小的回调函数,当窗口大小改变时,会调用该函数调整串口的大小// 注册窗口大小改变回调函数glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glad: load all OpenGL function pointers// glad 会加载所有openGL函数指针,在调用任何opengl函数之前需要先初始化gladif (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){cout << "Failed to initialize GLAD" << std::endl;return -1;}// build and compile our shader program// ------------------------------------// vertex shader// 创建一个着色器对象, 返回出着色器的ID// 顶点着色器int vertexShader = glCreateShader(GL_VERTEX_SHADER);// 将着色器源码附加到着色器对象上,然后使用glCompileShader进行编译// param1 着色器ID, param2 源码字符串数量glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);glCompileShader(vertexShader);// check for shader compile errorsint success;char infoLog[512];glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}// fragment shader// 片段着色器int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);// check for shader compile errorsglGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;}// link shaders/*当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。*/int shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);// check for linking errorsglGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);if (!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;}glDeleteShader(vertexShader);glDeleteShader(fragmentShader);// set up vertex data (and buffer(s)) and configure vertex attributes// ------------------------------------------------------------------float vertices[] = {-0.5f, -0.5f, 0.0f, // left0.5f, -0.5f, 0.0f, // right0.0f, 0.5f, 0.0f // top};unsigned int VBO, VAO;glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s)./* 要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用 *//* // ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..// 1. 绑定VAO */glBindVertexArray(VAO);// 2. 把顶点数组复制到缓冲中供OpenGL使用glBindBuffer(GL_ARRAY_BUFFER, VBO);// 把之前定义的顶点,复制到缓冲的内存中去/*GL_STATIC_DRAW :数据不会或几乎不会改变。GL_DYNAMIC_DRAW:数据会被改变很多。GL_STREAM_DRAW :数据每次绘制时都会改变。 *//*三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。 */glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);/*位置数据被储存为32位(4字节)浮点值。每个位置包含3个这样的值。在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。数据中第一个值在缓冲开始的位置 */// 告诉GPU数据怎样取/*第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。 */// 设置顶点属性指针glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);/*现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。 */glEnableVertexAttribArray(0);// note that this is allowed, the call to glVertexAttribPointer //registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbindglBindBuffer(GL_ARRAY_BUFFER, 0);// You can unbind the VAO afterwards so other // VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other// VAOs requires a call to glBindVertexArray anyways // so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.glBindVertexArray(0);// uncomment this call to draw in wireframe polygons.//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);/*就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。*/// render loop// -----------while (!glfwWindowShouldClose(window)){// input// -----processInput(window);// render// ------// 北背景glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// draw our first triangle// 2. 当我们渲染一个物体时要使用着色器程序glUseProgram(shaderProgram);// 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 organizedglBindVertexArray(VAO);// 3. 绘制物体glDrawArrays(GL_TRIANGLES, 0, 3);// glBindVertexArray(0); // no need to unbind it every time// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)// -------------------------------------------------------------------------------glfwSwapBuffers(window);glfwPollEvents();}// 正确的释放之前分配的所有资源glfwTerminate();return 0;
}// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{// 用户按下 esc键,就设置退出串口为真if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);
}// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{// opengl渲染串口大小,每次调整窗口cout << "view port call back" << endl;//glViewport(0, 0, width, height);
}
如果没有出问题,你将得到下面这幅三角形绘制的图:
如果顺利得到这副图,那么恭喜你,你已经学完了opengl的入门,也学完了opengl的难点,能够掌握三角形的绘制以及原理,后面的学习将会简单很多
github源码仓库
opengl环境准备
opengl编程从入门到精通-hello,window
OpenGL从入门到精通--你好三角形相关推荐
- OpenGL从入门到精通--纹理
纹理 github源码仓库 opengl环境准备 opengl编程从入门到精通-hello,window OpenGL从入门到精通–你好三角形 OpenGL从入门到精通–着色器的使用 我们可以为每个顶 ...
- OpenGL从入门到精通--着色器的使用
着色器 github源码仓库 opengl环境准备 opengl编程从入门到精通-hello,window OpenGL从入门到精通–你好三角形 OpenGL从入门到精通–着色器的使用 着色器(Sha ...
- opengl从入门到精通
Hello opengl github源码仓库 opengl环境准备 opengl编程从入门到精通-hello,window OpenGL从入门到精通–你好三角形 OpenGL从入门到精通–着色器的使 ...
- 【OpenGL从入门到精通(七)】OpenGL中的数学
1.向量单位化 2.三维向量点乘/点积(结果为标量) 3.三维向量叉乘(叉积)结果为向量 3.坐标平移 因为在OpenGL中使用的都是齐次坐标,即x , y , z , w 如果使得点(0, 0, 0 ...
- 【OpenGL从入门到精通(六)】纹理对象与纹理坐标
1.在OpenGL想要显示一张图片,需要先绘制一个自定义的几何体. 2.把图片加载到纹理对象中= 3.当进行纹理贴图时候,使用纹理坐标来设置纹理对象. 2.
- 【OpenGL从入门到精通(三)】第一个点的理论
OpenGL状态机 一,OpenGL是一个状态机matrix中包括: model view (模型矩阵) worldpos(世界坐标,也称为顶点坐标)通过mv(模型矩阵)转到cameru摄像 ...
- 【OpenGL从入门到精通(二)】绘制一个点
1.想要绘制一个点,首先要在OpenGL初始化中先设置矩阵 2.然后在绘制场景中进行点的绘制.其中包括 当前颜色设置:点的位置,点的大小等等 #include <windows.h> #i ...
- 【OpenGL从入门到精通(一)】Windows搭建OpenGL的渲染环境,并初始化一个OPenGL窗口
注意:需要在Windows 窗口程序下,而不能是控制台程序,Windows平台的VS下已经包含了OpenGL相关的API,可以直接引用 #include <windows.h> #incl ...
- 【OpenGL从入门到精通】Shader专题
详解GPU的工作流程 1.shader通常称为着色器,作用是把CPU上的点渲染出来. 2.shader是并行的. 3.流程:数据data (顶点数据) ----->VS(输入:data的顶点数据 ...
最新文章
- 5G信令(就是用户身份信息)——手机开机后,先从USIM中读取之前运营商分配的临时身份信息GUTI/TMSI,发送携带该身份信息的信令给基站,请求接入运营商网络。...
- 合肥站 | 神策 2019 数据驱动大会「PPT 下载」新鲜出炉!
- gradle 配置java 项目maven 依赖
- SAP云平台,区块链,超级账本和智能合约
- java spring druid_Spring配置Druid连接池
- Yii 2 美化 url
- STM8单片机串口同时识别自定义协议和Modbus协议
- Linux 下安装 Redis
- 如何制作一款HTML5 RPG游戏引擎——第五篇,人物人物特效
- 新手学习selenium路线图(老司机亲手绘制)-学前篇
- python实例 输出你好
- ART加载OAT文件的过程分析
- Chrome下载文件,文件名出现乱码解决
- oracle重启rac2监听,RAC监听服务
- 用biobert标记基因和蛋白质
- Flannel host-gw 和 vxlan
- 带你打开C语言的大门
- MBR15200FAC-ASEMI塑封肖特基二极管MBR15200FAC
- matlab取色工具getpts
- 解读教育大数据的文化意蕴