十二星座怎么画(十二星座怎么画简单又可爱)
头像:手绘古风十二星座,融合的美美哒
素材网络,侵删
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
通过阅读本文,你将获得以下收获:
1.什么是着色器
2.顶点着色器代码的基本外观以及着色器语言GLSL介绍
3.OpenGL的坐标系统
4.如何使用着色器
上篇回顾一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)主要讲了Android平台上开发OpenGL es应用项目的搭建以及EGL配置相关,万丈高楼平地起,上一篇博文已经帮我们打好了地基,那我们的渲染程序,就可以如雨后春笋一般破土而出了。
当前我们已经知道图形渲染管线的第一步是顶点着色器(不清楚的读者可以看下一看就懂的OpenGL ES教程——图形渲染管线的那些事和一看就懂的OpenGL ES教程——再谈OpenGL工作机制 )
,那么我们此时的问题就是:
1.顶点着色器的真面目是什么样的?
2.作为客户端的C++程序如何将数据传给着色器?
记住,阅读本文的时候,一定要时刻记得每一个步骤都是在图形渲染管线中进行,一定要明确数据的从哪里来到哪里去,这也是我前面文章所强调的大局观。
着色器(Shader)Shader官方文档的定义是:
A Shader is a user-defined program designed to run on some stage of a graphics processor. Shaders provide the code for certain programmable stages of the rendering pipeline. They can also be used in a slightly more limited form for general, on-GPU computation.
着色器就是一个运行在图形渲染管线中某个阶段的一段开发者写的程序,通过着色器,我们可以自定义渲染的效果。由于图形渲染管线是运行在硬件中(一般是GPU),所以着色器一般就是运行在GPU中,这和大家以前写的运行在CPU的代码就必然有所不同。
C++音视频学习资料免费获取方法:关注音视频开发T哥,+「链接」即可免费获取2023年最新C++音视频开发进阶独家免费学习大礼包!
关于着色器在图形渲染管线中的阶段,官方文档描述如下:
The rendering pipeline defines certain sections to be programmable. Each of these sections, or stages, represents a particular type of programmable processing. Each stage has a set of inputs and outputs, which are passed from prior stages and on to subsequent stages (whether programmable or not).
Shaders are written in the OpenGL Shading Language. The OpenGL rendering pipeline defines the following shader stages, with their enumerator name:
Vertex Shaders: GL_VERTEX_SHADER
Tessellation Control and Evaluation Shaders: GL_TESS_CONTROL_SHADER and GL_TESS_EVALUATION_SHADER. (requires GL 4.0 or ARB_tessellation_shader)
Geometry Shaders: GL_GEOMETRY_SHADER
Fragment Shaders: GL_FRAGMENT_SHADER
Compute Shaders: GL_COMPUTE_SHADER. (requires GL 4.3 or ARB_compute_shader)
主要就2个重点:
1.上游阶段的着色器可以传递数据给下游阶段的着色器。
2.目前图形渲染管线已有的着色器有6种,分别是……(上文列出来的那几种),目前我们只需要关注Vertex Shaders(顶点着色器)和Fragment Shaders(片段着色器)即可。
如果我们控制了着色器,就控制了图形渲染管线的半壁江山,从而就一定程度控制了整个绘制的效果。于是接下来,Vertex Shaders(顶点着色器)和Fragment Shaders(片段着色器)两大主角将粉墨登场。
很多博文讲着色器Shader可能就直接将 Shader官方文档 叙述或者罗列下重点,这样子我觉得可能效果还不如直接看官方文档来的有效果,接下来,我想用自己的方式讲解这部分内容。
顶点着色器我们再用类似轻松入门OpenGL ES——图形渲染管线的那些事想想要画一个三角形的步骤,
第一步,当然就是确定三个顶点的位置,所以我们要将三个顶点位置传给图形渲染管线,那么图形渲染管线的第一个阶段便义无反顾地承担起了这个任务,于是第一个阶段也就是处理顶点的阶段——顶点着色器。
简单来说,顶点着色器的核心功能就是完成将3维坐标中的点,通过变换和投影,转换为2维的屏幕上。
作为渲染管线的开端,顶点着色器不止承担着接收顶点的任务,作为一段拥有具体逻辑的应用程序,它还承受着处理顶点位置以便完成一些效果等任务(比如位置变换、调整形状,或者三维变换),另外还起着传输各种从客户端程序传入的各种数据(比如颜色、变换矩阵、时间参数等)并将数据传递给后面阶段的任务。
先看下它长什么模样:
//指定GLSL版本 #version 300 es //输入的顶点坐标,会在客户端程序将数据传入到该字段 in vec4 aPosition; void main() { //直接把传入的坐标值作为输出传入渲染管线下一个阶段。gl_Position是OpenGL内置的变量 gl_Position = aPosition; }
上面是一个最简单的顶点着色器的代码,它只做一件事:接收客户端程序传来的顶点数据,然后传给图形渲染管线的下一个阶段。
相当于上面程序啥都没做,只是无脑的数据传送机。那它有什么价值呢?当然有,勇敢作为排头兵,接收外头丢过来的“烫手山芋”就是它最大的贡献。 (当然你可别认为顶点着色器就这么没用,毕竟这只是一个最简单的顶点着色器程序,真正“成熟”的顶点着色器程序技能可是很爆表的。
因为在顶点着色器中,你已经拿到了对每个顶点的实质控制权,可以尽情发挥想象力去操纵每一个顶点)
GLSL接下来就是具体讲解着色器代码的时刻了。首先要知道的是,着色器用的语言是一种特殊的语言,叫做GLSL,首先看下GLSL官方定义:
The OpenGL Shading Language is a C-style language, so it covers most of the features you would expect with such a language. Control structures (for-loops, if-else statements, etc) exist in GLSL, including the switch statement.
GLSL是一种C语言风格的语言,它包含大部分编程语言的特性。
所以它是和C语言类似的,只要你能看懂C语言,就能看懂它。只要你写过C语言程序,那基本也能写GLSL程序。
上面的实例代码为GLSL3.30所写,那么这里就以现在最主流的GLSL3.30为例,逐行级别讲解着色器。
我不太喜欢文档式地罗列语法点,所以关于着色器还是遵循边用边学原则去讲。
第一行:
#version 300 es 是来告诉编译器GLSL版本的,这样编译器才知道如何编译这段代码。这里300指的就是GLSL3.30,es指的就是OpenGL es。
OpenGL es和GLSL对应的版本关系如下图所示:
第二行:
in vec4 aPosition;
这是定义一个变量aPosition,但是和我们熟悉的C语言又有所不同。
迎面而来的是in关键字,表示这个变量是接收外面传来数值的,当然有in就会有out,表示变量是传给下一个阶段。(可能有的童鞋已经接触过GLSL2.0版本的代码了,熟悉的是arrtibute、varying等关键字,但这些在3.0已经被in、out取代)
接下来看到vec4 aPosition,vec4表示变量类型,aPosition表示变量名。这个格式大家都很熟悉,不过vec4这种类型可能有点陌生。其实就是表示4维向量。
4维向量可以当做4个数的组合,GLSL特定这里指的是浮点数,用法也非常灵活:
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0}vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5}vec3 temp = vec3(myVec3); //构造器也可以接收向量,表示 temp = myVec3vec2 myVec2 = vec2(myVec3); // myVec2 = {myVec3.x, myVec3.y} 如果构造器传入的向量维度更大,则进行截取。myVec4 = vec4(myVec2, temp, 0.0); // myVec4 = {myVec2.x, myVec2.y , temp, 0.0 }
以上我相信加上注释大家都能看懂,这里就不做赘述。
C++音视频学习资料免费获取方法:关注音视频开发T哥,+「链接」即可免费获取2023年最新C++音视频开发进阶独家免费学习大礼包!
当客户端需要传数据给该变量的时候,客户端就会绑定aPosition这个名字,指定数据传给着色器aPosition变量。
但是这样客户端C++程序和着色器内部的名字强行绑定,以我们多年的编程经验来看,并非好的做法,万一某天哪个小可爱不小心改了一下着色器的变量名,突然整个程序就挂了。
于是乎layout便应运而生,关于这个就有点意思了,layout是GLSL的几个修饰符中的一个,一般用来指定变量布局的,可以说是着色器和客户端程序或者其他阶段着色器的通信接口,详细的依旧可以看官方文档Layout Qualifier (GLSL)
layout (location = 0) in vec4 aPosition;
以下是layout的通用语法格式:
layout(qualifier1, qualifier2 = value, ...) variable definition
其中qualifier表示具体的修饰符,最常见的就是代码中的“location”,可以理解为变量的位置。
何为变量的位置?你可以想象每一个着色器程序是一层楼,每个需要对接外部的变量住在一个个房间中,假如你现在是客户端的C++程序,你想给着色器里面某个变量传参,然后你又不知道变量名字,如果此时你知道变量所在的房间编号,那你照样还是可以将数据传给变量,就像一个蹑手蹑脚的间谍,偷偷打开对应location的房门,偷偷将一份机密文件交给里面的人(location对应变量)。所以layout加location的作用已经很明显了,你不需要房间里面住着谁(不用知道变量名称),只需要知道房间号(location),也能按成变量数值的传递。
至于客户端C++程序怎么传数据给location对应变量,先别急,后面小节还会讲。
第三行:
void main() {
这个各位太熟悉了,自从第一次写了hello world程序之后,对main函数应该烂熟于心了吧。
既然GLSL是类似C的语言,那么当然也是以main函数作为入口函数的。
main函数究竟做了什么:
gl_Position = aPosition;
直接把传入的坐标值作为输出传入渲染管线下一个阶段。gl_Position是OpenGL内置的vec4变量,表示当前顶点最终的值。
所以说到底,顶点着色器最重要的目标就是将从客户端程序传入的数据经过逻辑处理之后,再赋值给gl_Position。这里gl_Position表示一个顶点的最终值。
最后有个问题:顶点着色器会执行多少次呢?
我们想下,顶点着色器是用来处理顶点的,也就是每个顶点都要经过它的处理,那么也就是传入多少顶点就执行多少次。
坐标系统那么传入的vec4类型aPosition具体是什么样的呢?我们知道在三维空间中表达一个点,就是用一个3维的坐标x、y、z。
那么为什么是vec4(4维)的呢?这里vec4分别是x、y、z、w,其中w是一个新的概念,叫做齐次坐标,那么齐次坐标在这里是什么作用呢?
齐次坐标因为本文并不是用来讲解数学的,所以这里只是简单解释下其次坐标的作用,假如2维坐标系中有一个点(x,y),如果要对该点进行缩放和旋转,我们是可以通过线性变换,即乘上一个矩阵的方式来计算:
如果要对该点进行平移,则是要能够以下方式表示:
那如果对该点即做缩放和旋转,又做平移,则要用如下方式表示:
这就不是线性变换了,那么如果我们坚持一定要用线性变换来表示呢?不用担心,数学家已经帮我们实现了,不过要再增加一个维度w就可以了:
这方面要详细了解数学推导的话,可以看GAMES101-现代计算机图形学入门-闫令琪
OpenGL中的坐标:虽然OpenGL的坐标是一个3维空间坐标,但是本系列教程针对的是2维空间,即我们可以直接忽略z坐标,即z始终为0,而OpenGL用的是标准化设备坐标,即x、y坐标的范围会限定在-1到1以内,超出的就会被裁剪(在轻松入门OpenGL ES——图形渲染管线的那些事一文的“图元装配”一章,已经提到了裁剪这个步骤),这种坐标处理方式也叫作归一化则OpenGL的坐标会变为如下:
这种坐标下,假如有个手机屏幕是720*1080,则x方向的1个单位长度表示720个像素,而y方向的1个单位长度表示1080个像素。
为什么要这样处理呢?因为不同的屏幕分辨率相差甚多,如果要用绝对的像素点作为坐标点的话,那么针对不同的屏幕可能就需要传一套坐标点给顶点着色器,而通过标准化设备坐标的归一化,坐标点的都是用比例标记,这样显示的屏幕中也是按照比例显示,也就是OpenGL帮我们做好了屏幕适配工作。
编译、链接着色器前面说了这么多,那么到底要怎么给图形渲染管线“喂”数据呢?在“喂”数据之前,还有一个手续需要办理一下,那就是编译、链接着色器程序。
再看下上一篇文章(轻松入门OpenGL ES——这或许是你遇过最难画的三角形(一))中的绘制三角形的代码:
extern "C"JNIEXPORT void JNICALLJava_com_example_openglstudydemo_YuvPlayer_drawTriangle(JNIEnv *env, jobject thiz, jobject surface) { /** 此处开始EGL的配置 **/ ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface); EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (display == EGL_NO_DISPLAY) { LOGD("egl display failed"); return; } if (EGL_TRUE != eglInitialize(display, 0, 0)) { LOGD("eglInitialize failed"); return; } EGLConfig eglConfig; EGLint configNum; EGLint configSpec[] = { EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE }; if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) { LOGD("eglChooseConfig failed"); return; } EGLSurface winSurface = eglCreateWindowSurface(display, eglConfig, nwin, 0); if (winSurface == EGL_NO_SURFACE) { LOGD("eglCreateWindowSurface failed"); return; } const EGLint ctxAttr[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; EGLContext context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr); if (context == EGL_NO_CONTEXT) { LOGD("eglCreateContext failed"); return; } if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) { LOGD("eglMakeCurrent failed"); return; } /** 此处结束EGL的配置 **/ /** 此处开始加载着色器程序 **/ GLint vsh = initShader(vertexSimpleShape, GL_VERTEX_SHADER); GLint fsh = initShader(fragSimpleShape, GL_FRAGMENT_SHADER); GLint program = glCreateProgram(); if (program == 0) { LOGD("glCreateProgram failed"); return; } glAttachShader(program, vsh); glAttachShader(program, fsh); glLinkProgram(program); GLint status = 0; glGetProgramiv(program, GL_LINK_STATUS, &status); if (status == 0) { LOGD("glLinkProgram failed"); return; } LOGD("glLinkProgram success"); glUseProgram(program); /** 此处加载着色器程序结束 **/ /** 此处开始将数据传入图形渲染管线 **/ static float Ver[] = { 0.8f, -0.8f, 0.0f, -0.8f, -0.8f, 0.0f, 0.0f, 0.8f, 0.0f, }; GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPosition")); glEnableVertexAttribArray(apos); glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, Ver); /** 此处结束将数据传入图形渲染管线 **/ /** 此处开始将图像渲染到屏幕 **/ glClearColor(1.0f, 1.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 3); eglSwapBuffers(display, winSurface); /** 此处结束图像渲染 **/}
可以看到,在上篇文章讲的配置EGL环境之后,就进入到加载着色器程序阶段,而此阶段最主要的操作,就是编译、链接着色器程序。着色器程序是在我们整个程序运行时编译,因为我们必须在程序运行时将着色器的逻辑安插到图形渲染管线对应的阶段里面,就像一条工厂流水线有那么2、3个工序是空的,我们在启动流水线之前,必须将空的工序一个个填补上去。
关于加载着色器程序的基本流程如下图所示:
创建着色器对象着色器对象加载着色器代码编译着色器对象创建着色器程序关联着色器对象和着色器程序链接着色器程序使用着色器程序
这里有2个概念,一个是着色器对象,表示一段具体的着色器代码的抽象,另一个是着色器程序,表示整个图形渲染管线的着色器程序集合。
接着是令大家兴奋的代码演示,代码就是根据上面的流程图一步步走的。首先将编译链接着色器程序的代码抽出来,逐行分析,首先是initShader方法:
GLint initShader(const char *source, GLint type) { //创建着色器对象,type表示着色器类型,比如顶点着色器为GL_VERTEX_SHADER,片段着色器为GL_FRAGMENT_SHADER。返回值为一个类似引用的数字。 GLint sh = glCreateShader(type); if (sh == 0) { //返回值sh为0则表示创建着色器失败 LOGD("glCreateShader %d failed", type); return 0; } //着色器对象加载着色器对象代码source glShaderSource(sh, 1,//shader数量 &source, 0);//代码长度,传0则读到字符串结尾 //编译着色器对象 glCompileShader(sh); //以下为打印出编译异常信息 GLint status; glGetShaderiv(sh, GL_COMPILE_STATUS, &status); if (status == 0) { LOGD("glCompileShader %d failed", type); LOGD("source %s", source); auto *infoLog = new GLchar[512]; GLsizei length; glGetShaderInfoLog(sh, 512, &length, infoLog); LOGD("ERROR::SHADER::VERTEX::COMPILATION_FAILED %s", infoLog); return 0; } LOGD("glCompileShader %d success", type); return sh;}
1.首先用glCreateShader方法根据传入的type参数创建对应的着色器对象,type的可选范围为GL_COMPUTE_SHADER, GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, or GL_FRAGMENT_SHADER。顶点着色器为 GL_VERTEX_SHADER,片段着色器为 GL_FRAGMENT_SHADER。
返回值sh为一个具有引用作用的整数,这也是OpenGL最常见的一个种引用对象的方式,再之后的教程中还会非常常见。拿到这个引用整数,就犹如将绑着该着色器对象的绳子牢牢抓住,以后只要操控着色器对象,就靠这根绳子。
2.此时的着色器对象还是空的,只是一个壳,所以我们需要通过glShaderSource方法将着色器代码注入进去。
3.代码注入进去了,就要类似C程序一样需要一个编译过程,用的是glCompileShader方法。
4.当然有编译就有可能有编译错误,由于着色器程序是爱GPU中编译运行的,所以目前并不像我们平时写在CPU运行的程序那样有详细的编译错误信息以及可以断点调试这些高端操作,但是至少还是可以通过glGetShaderiv方法看到一些报错信息的。
//分别创建和编译顶点着色器、片段着色器对象 GLint vsh = initShader(vertexSimpleShape, GL_VERTEX_SHADER); GLint fsh = initShader(fragSimpleShape, GL_FRAGMENT_SHADER); //创建着色器程序对象 GLint program = glCreateProgram(); if (program == 0) { LOGD("glCreateProgram failed"); return; } //将上面创建的着色器对象关联到着色器程序对象上 glAttachShader(program, vsh); glAttachShader(program, fsh); //链接着色器程序 glLinkProgram(program); //打印出链接异常信息 GLint status = 0; glGetProgramiv(program, GL_LINK_STATUS, &status); if (status == 0) { LOGD("glLinkProgram failed"); return; } LOGD("glLinkProgram success"); //使用着色器程序 glUseProgram(program);
1.通过glCreateProgram方法创建一个着色器程序对象。
2.此时着色器程序对象还是空空如也,所以需要glAttachShader方法,将前面创建的着色器对象关联给它,它才是一个有实质内容的着色器程序对象。
3.此时着色器程序还是没有真正和我们的OpenGL程序关联上,所以需要通过glLinkProgram方法链接着色器程序,类似我们C语言程序链接一个动态链接库一样,去关联上。
4.当然,链接可能会发生异常,所以通过glGetProgramiv方法打印异常信息。
5.最后再用glUseProgram,名正言顺宣告,该着色器程序将被当前OpenGL程序使用。
接下来就和 一看就懂的OpenGL ES教程——再谈OpenGL工作机制 中所讲的状态机相关了,此时OpenGL es程序处于已经处于激活着色器程序的状态,后面的操作都可以针对着色器进行操作。
传递数据给着色器前面说过顶点着色器作为图形渲染管线的开端,会接收包括顶点数据在内的多种渲染管线需要的数据,这里我们就先从只传顶点数据讲起。
OpenGL客户端程序传数据到OpenGL(服务端)内部的操作也是让无数初学者数夜辗转反侧的一个点,毕竟OpenGL是面向过程的,而且为了灵活性,就必须牺牲一些易用性,所以不好意思,肯定不像Java老司机梦想的那种传数据方式:
Point point1 = new Point(x,y,z);...boonlean isSuccess = input(point1,point2,point3);
OpenGL传输数据是这样的:
//三角形坐标static float Ver[] = { 0.8f, -0.8f, 0.0f, -0.8f, -0.8f, 0.0f, 0.0f, 0.8f, 0.0f, };//指定接收三角形坐标的变量名,program为上面的编译链接好的着色器程序 GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPosition")); //真正传输数据的地方 glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, Ver); glEnableVertexAttribArray(apos);
如果你是第一次看到,可能会出现以下表情:
不过不用担心,经过我一番“感性”的解释,你一定能彻底搞懂。
首先,前面已经客户端程序要传入OpenGL的数据,即使是专门传给顶点的数据,除了顶点坐标数据,还有其他数据,比如变换矩阵、颜色等,所以它不拘留于具体格式,干脆打平用一个数组传,你们要什么数据都往这个数组里面塞进去,到时候开发者根据需要自己取。比如三角形的三个顶点坐标,那就让三个点坐标的x、y、z依次排成一队:
static float Ver[] = { 0.8f, -0.8f, 0.0f, -0.8f, -0.8f, 0.0f, 0.0f, 0.8f, 0.0f, };
这个数组再OpenGL有个专有名词,叫做“顶点属性数组”。(注意,不是“顶点数组”,中间还有“属性”二字,即该数组包含除了顶点坐标以外,还有其他顶点相关的属性)
回头看下顶点着色器:
//指定GLSL版本 #version 300 es //输入的顶点坐标,会在客户端程序将数据传入到该字段 in vec4 aPosition; void main() { //直接把传入的坐标值作为输出传入渲染管线下一个阶段。gl_Position是OpenGL内置的变量 gl_Position = aPosition; }
我们的目标是要把顶点坐标传到变量“aPosition”中,所以先指定接收的变量名:
GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPosition"));
接下来是重点,就是告诉OpenGL如何解析传入的顶点属性数组数据:
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, Ver);
官网对该API的解释是:
glVertexAttribPointer specify the location and data format of the array of generic vertex attributes at index index to use when rendering. size specifies the number of components per attribute and must be 1, 2, 3, 4, or GL_BGRA. type specifies the data type of each component, and stride specifies the byte stride from one attribute to the next, allowing vertices and attributes to be packed into a single array or stored in separate arrays.
就是说glVertexAttribPointer方法指定了顶点属性数组要传到着色器哪个变量中(这里的变量指的是顶点着色器中被in修饰的变量),另外就是指定传入的顶点属性数组的格式以及着色器如何去取顶点属性数组数据。
函数定义为:
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * pointer``);
index:表示着色器中要接收数据的变量的引用。即着色器中的layout。
size:表示每一个顶点属性需要用多少个数组元素表示。比如一个3维坐标是xyz表示,那么size就是3,即3个数可以表示一个点坐标。
type:每一个数组元素的格式是什么,比如GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE等。
normalized:是否需要归一化,即是否需要将数据范围映射到-1到1的区间内。
stride:步长,一个重要概念,表示前一个顶点属性的起始位置到下一个顶点属性的起始位置在数组中有多少字节。如果传0,则说明顶点属性数据是紧密挨着的。
这么一说恐怕懵逼二字写在每个人脸上,下面用一张图来说明(图来源于(learnopengl-cn.github.io/01%20Gettin…)
以数组起始位置为地址0来看,第一个顶点属性VERTEX1的开端是地址0,因为数据类型为Float,每个Float大小为4个字节,而一个Vertex需要3个元素,则第二个顶点属性VERTEX2开端是地址12,那么此时stride就是12。
这种情况属于元素之间紧密挨着的,所以stride传0,OpenGL也能自己处理。但是如果顶点属性不止顶点坐标数据,还有颜色属性,如下图:
每个顶点属性Vertex由一个坐标点和一个RGB颜色数据组成,这样子就必须告诉OpenGL,从一个坐标点数据到下一个坐标点数据有多“远”的stride,这个它才知道怎么取对应的数据。
所以这行代码
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, Ver);复制代码
可以解释为: 将Ver数组的每个元素用Float类型来看待,每3个元素为一个属性,每间隔12个字节(stride传0告诉OpenGL顶点属性都是紧密挨着)属性依次传给着色器中apos引用的变量。
所以,你看OpenGL挺傻的,我们需要苦口婆心地详细告诉它怎么解析数据它才知道如何解析,但是这样的好处是我们自由度高,可以非常灵活地传递数据,后面就会展示需要传递颜色数据的情况。
那么结合实例,我们传入的数组此时就应该这样看待:
{ 0.8f, -0.8f, 0.0f, //第一个顶点 -0.8f, -0.8f, 0.0f, //第二个顶点 0.0f, 0.8f, 0.0f, //第三个顶点}
然后OpenGL就会一个一个顶点坐标传入顶点着色器执行,顶点着色器执行完之后,数据就会输出给下一个阶段——图元装配(千万别以为是直接到片段着色器啊,不然前面的文章就白写了= =)
最后的:
glEnableVertexAttribArray(apos);
这就是一个开关方法,就是打开着色器中apos这个变量,想象下你还是那个间谍,你需要偷偷打开对应变量的房门,才能偷偷将一份机密文件交给里面的人。
总结不知不觉又是写了几千字,OpenGL的东西细讲真的很多,要入门还真不容易。不过我们已经翻过了图形渲染管线以及着色器入门2座大山了,后面的路会相对稍微平坦一些了,下一篇文章一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三)将继续讲下一个阶段——片段着色器,敬请期待~~
参考Shader官方文档 Core Language (GLSL) 你好,三角形
作者:半岛铁盒里的猫 链接:/d/file/gt/2023-05/yjjwngzswdk 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在开发的路上你不是一个人,欢迎加入C++音视频开发交流群「链接」大家庭讨论交流!
十二星座专属手绘古风妆容,双鱼座可爱清纯,双子座娇艳绝美!
看过古装剧的都知道里面的古装造型都很漂亮惊艳,吸引观众的眼球,那么手绘古风妆容都是什么样的呢?一起看看十二星座专属手绘古风妆容,双鱼座可爱清纯,双子座娇艳绝美!
双子座专属古风妆容
双子座特别的善变,很话唠,对任何人都有话说,也特别的聪明。双子座专属古风妆容蓝色的眼眸,清澈美丽,眉心花钿是红色和绿色的装点,细长的柳叶眉。
双鱼座专属古风妆容
双鱼座特别的浪漫多情,特别爱幻想,沉浸自己的世界里,虽然双鱼温柔安静,但他们每一条的脑回路也各有特色。双鱼座专属古风妆容可爱清纯,大大的眼睛,红润的嘴唇。
白羊座专属古风妆容
白羊座特别的天真,特别倔强,比较好面子,直到老了还自带一股孩子气,敢爱敢恨。白羊座专属古风妆容温婉贤淑,头发盘起,头戴蓝色花朵头饰。
射手座专属古风妆容
射手座特别的幽默潇洒,喜欢到处玩,骨子里就是一个大侠,行走天涯。射手座专属古风妆容简单素雅,黑色的头发随意盘起,眼睛深邃不见底。
水瓶座专属古风妆容
水瓶座特别的理智,很会来事,脑洞是真的特别的大,想法千奇百怪。水瓶座专属古风妆容是细长的眉毛,大大的眼睛,长长的睫毛,眼角有颗美人痣,粉色的眼妆。
巨蟹座专属古风妆容
巨蟹座特别的温柔体贴,很爱家也很会持家,如果遇到巨蟹座千万不要错过,一定好好对待。巨蟹座专属古风妆容是素妆,干净漂亮,简单大方,温柔如水。
天蝎座专属古风妆容
天蝎座特别的神秘性感,也有些小傲娇,一般人都觉得他们很高冷,让人又爱又恨。天蝎座专属古风妆容细小的眉毛。长长的睫毛,眼神很有诱惑力。
摩羯座专属古风妆容
摩羯座特别低调,特别能忍,特别认真,特别是摩羯座的男生,看起来很普通,在人群中都是面无表情的。摩羯座专属古风妆容像佟丽娅演的赵飞燕手绘,美丽迷人。
狮子座专属古风妆容
狮子座特别的霸气傲娇,特别的阳光自信,自信的人最迷人。狮子座专属古风妆容是上挑的眉毛,火形状的眉心花钿,看起来有些霸气中带着温柔。
金牛座专属古风妆容
金牛座有些任性,特别忠诚,为朋友两肋插刀也不是事,很喜欢吃喝,是一个大吃货。金牛座专属古风妆容有些八字形的眉毛,大大的眼睛,漂亮的眉心花钿图案。
天秤座专属古风妆容
天秤座特别爱纠结,在意自己的形象,当然也特别的有魅力,他们总是寻找内心与周遭的稳定与平衡。天秤座专属古风妆容五官端正,面饰和头饰都是蝴蝶的造型,搭配起来非常好看。
座专属古风妆容
座特别的细心,爱干净整洁,喜欢挑剔、吐槽,透着冰清玉洁的气质,优秀的追求着事物的极致完美。座专属古风妆容是精致的妆容,头上戴着金属花朵头饰,看起来有些忧伤。
喜欢的小可爱们可以关注我哦!
免责声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请发送邮件举报,一经查实,本站将立刻删除。