Learning Modern 3D Graphics Programming笔记

来源:互联网 发布:mac idea 代码提示 编辑:程序博客网 时间:2024/04/30 04:34

一.下载书中源码并编译


我们需要准备两样东西:
(1) 源代码(放在E:\opengl\Tutorial 0.3.8)
(2) premake4生成项目编译器

然后我们将下载的premake4.exe放到源代码的根目录下(E:\opengl\Tutorial 0.3.8) 和 使用库的目录下(E:\opengl\Tutorial 0.3.8\glsdk)

然后打开cmd. 进入到源代码的根目录下(此时包含premake4.exe),输入如图的指令:




此时项目会进行编译,生成相关的vs2010的工程.

再进入到库的目录下,输入如图的指令:




此时会生成vs2010的工程,我们打开工程,然后运行,编译相关的库,生成lib库文件.

到此,我们就可以开始opengl之旅了.


二.Chapter1. Hello, Triangle!


1.似乎有用

(1).在GLSL语言中,我们看见的以"gl_"开头的的变量此时它们都是内部定义的,我们定义的变量不能以此作为开头。

(2).顶点着色器中的gl_Position变量的定义是应该是:out vec4 gl_Position. 它的作用是:顶点剪裁空间中的坐标。所以我们在程序中指定顶点坐标的时候一般都是直接指定4个值作为一个顶点的坐标.(x,y,z,w)

(3).段着色器是被用于计算一个段的输出颜色。在段着色器执行完后,段的输出颜色被写入到输出的图像。

(4).对于顶点着色器,我们利用 layout(location = *) 标志来提供顶点着色器的输入和顶点属性下标之间的联系。

(5).那段着色器的输出和屏幕上的输入是怎么联系起来的呢?

答:opengl意识到,在许多的渲染过程中,只有一个理论上的地方可以用于段着色器的输出:那就是屏幕上当前被绘制的图像。因此,如果你只定义了一个输出值在段着色器变量中,这个输出值会自动的被写入到当前目的地的图像中。可能存在多个段着色器输出变量到不同目的地的图像,那有点复杂,不过有点类似于顶点着色器的属性下标。

(6).一个着色器程序代码字符串被编译为着色器对象,这很类似于一个着色器对件(像.obj)。一个或者多个着色器对象会链接成一个程序对象(像.exe)。一个程序对象在opengl包含了所有的着色器代码用于渲染。在教程中我们使用了顶点和段着色器,它们被链接在一起生成了一个单独的程序对象。

使用的相关函数有(省略了参数):

glCreateProgram();glCreateShader();glShaderSource();glCompileShader();glAttachShader();glLinkProgram();glUseProgram();glDetachShader();glDeleteShader();glDeleteProgram();

有两种方法保存所写的着色器的代码:

(a)直接在程序的源代码中,用字符串string保存着色器程序的代码

(b)将着色器程序的代码保存在文本文件中,然后用文件相关的函数读取其中的字符串出来保存到程序代码中(用string保存)。然后调用上述相关的gl*()函数来进行着色器的生成,链接等操作。


(7).只要程序对象正确的链接,我们的着色器对象就可以从程序中移除(调用glDetachShader()).程序的链接状态和功能不会受到影响被那些移除的着色器。所有所做的一切只是告诉opengl这些对象不再和程序有关系。然后我们可以调用(glDeleteShder())来将着色器程序对象删除掉。

(8).我们可以使用: layout(location = * )来显示的指明着色器属性变量的下标,然后再程序中利用。我们也可以在我们程序中通过glGetAttribLocation来得到属性的下标。


2.示例程序段


const float vertexPosition[]={....};const float vertexColor[]={....};


void Initialize(){glGenBuffers(1,&vertexPositionObject);glBindBuffer(GL_ARRAY_BUFFER,vertexPositionObject);glBufferData(GL_ARRAY_BUFFER,sizeof(vertexPosition),vertexPosition,GL_STATIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER,0);glGenBuffers(1,&vertexColorObject);glBindBuffer(GL_ARRAY_BUFFER,vertexColorObject);glBufferData(GL_ARRAY_BUFFER,sizeof(vertexColor),vertexColor,GL_STATIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER,0);}


void display(){glClearColor(0.0f,0.0f,0.0f,0.0f);glClear(GL_COLOR_BUFFER_BIT);glUseProgram(theProgram);glBindBuffer(GL_ARRAY_BUFFER,vertexPositionObject);glEnableVetexAttribArray(0);glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,0,0);glBindBuffer(GL_ARRAY_BUFFER,vertexColorObject);glEnableVetexAttribArray(1);glVertexAttribPointer(1,4,GL_FLOAT,GL_FALSE,0,0);glDrawArrays(GL_TRIANGLES,0,3);}

在Initialize()中,我们分别创建了两个缓冲区对象,一个针对顶点颜色,一个针对顶点坐标。然后分别向该缓冲区对象中放入了相关数据。

在display()中,我们先激活了坐标缓冲区对象,然后传递了相关数据给着色器程序。然后我们再激活了颜色对象,再传递颜色数据给着色器程序。

此时我们两个顶点属性,对于我们每个属性,我们必须分别调用glEnableVertexAttribArray 和 glVertexAttribPointer 处理顶点属性。

我们不能像这样:

glBindBuffer(GL_ARRAY_BUFFER,vertexColorObject);glBindBuffer(GL_ARRAY_BUFFER,vertexPositionObject);glEnableVetexAttribArray(0);glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,0,0);glEnableVetexAttribArray(1);glVertexAttribPointer(1,4,GL_FLOAT,GL_FALSE,0,0);

这时我们先激活了颜色缓冲区对象,然后当激活坐标缓冲区对象的时候,此时就关闭了颜色缓冲区对象。后面的glVertexAttribPointer操作的数据都会是激活的坐标缓冲区对象中的,而不是颜色缓冲区对象里.


二.Chater2. Playing with Colors.


1.示例程序段

在一中的示例代码中我们将坐标数据和颜色数据分开来处理,其实也可以合并进行处理。代码如下:


const float vertexData[]={坐标数据块,颜色数据块};

void Initialize(){glGenBuffers(1,&vertexDataObject);glBindBuffer(GL_ARRAY_BUFFER,vertexDataObject);glBufferData(GL_ARRAY_BUFFER,sizeof(vertexData),vertexData,GL_STATIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER,0);}


void display(){glClearColor(0.0f,0.0f,0.0f,0.0f);glClear(GL_COLOR_BUFFER_BIT);glUseProgram(theProgram);glBindBuffer(GL_ARRAY_BUFFER,vertexDataObject);glEnableVetexAttribArray(0);glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,0,0);glEnableVetexAttribArray(1);glVertexAttribPointer(1,4,GL_FLOAT,GL_FALSE,0,(GLVoid*)坐标数据大小);glDrawArrays(GL_TRIANGLES,0,3);}


上面的代码,我们将坐标数据和颜色数据放到一个数组中,然后生成一个缓冲区对象来保存数据。与第一章的区别是:

glVertexAttribPointer(1,4,GL_FLOAT,GL_FALSE,0,(GLVoid*)坐标数据大小);

最后一个参数,指明了一个偏移值。作用是:知道颜色数据开始处,以便将数据传输给着色器程序中的对应index的属性变量。


2.顶点着色器与段着色器的交互



顶点着色器代码:

#version 330layout (location = 0) in vec4 position;layout (location = 1) in vec4 color;smooth out vec4 theColor;void main(){gl_Position = position;theColor = color;}


段着色器代码:

#version 330smooth in vec4 theColor;out vec4 outputColor;void main(){outputColor = theColor;}

(1).我们可以观察到,在顶点着色器中和段着色器中都拥有变量theColor, 它们唯一的区别就是一个是in,另一个是out。分别作为输入和输出变量。其实这里就是顶点着色器将theColor的值输出到段着色器中,由段着色器中的theColor变量保存。这就是它们进行交互的方法。

(2).我们的顶点着色器只会运行3次,因为它处理3个输出的坐标和3个输出的颜色(根据传输的数据)。我们的段着色器不止运行3次。它为光栅化三角形所产生的每一个段运行一次。段所产生的数量是依靠于视图的处理和有多少屏幕的区域被三角形覆盖。假设我们三角形覆盖了屏幕的十分之一,窗口有500x500像素,那么段着色器就会运行500x500x(1/10)=25,000次。对于每一个被图形所覆盖的像素都会产生一个对应的段

(3).在(2)中的情况下,如果顶点着色器直接与段着色器进行交流,顶点着色器只输出了3个颜色值,那其余的24997个值从哪里来?

答:通过使用插入符“smooth”当定义顶点输出和段输入变量的时候,我们告诉OpenGL去做一些事情去指定这些值。而不是每一个段接收一个值来自一个顶点,每个段得到一个混合颜色值通过顶点着色器输出的三个颜色值,用于覆盖三角形的表面。如果你不提供一个插入关键字,"smooth"将会被默认使用。还有其余两个可以选用:“noperspective”和“flat”.

(4).每个段都拥有一个窗口坐标,还有包含一些其他的值。gl_FragCoord是GLSL中预定义的变量可以用作段着色器去根据当前的段去获取窗口坐标。


3.glDrawArrays的实现细节


代码实现:

void glDrawArrays(GLenum type, GLint start, GLint count){for(GLint element = start; element < start + count ; element++){VertexShader(positionAttribArray[element],colorAttribArray[element]);}}
positionAttribArray 和 colorAttribArray 存放数据的数组。

4.GLSL中的mix函数


原型:vec mix(vec initial, vec final, float alpha );

解释:展示一个线性的插值在initial 与 final之间,建立在alpha的基础上。

当alpha=0,mix函数返回initial;

当alpha=1,mix函数返回final;


alpha的值既可以是标量又可以是向量。alpha的值,或者它成员值的范围只能在[0,1]之间,其他值则这个函数是未定义的。


三.Chapter 3. OpenGL's Moving Trianle


1.glBufferData 和 glBufferSubData 的区别


(1) glBufferSubData 不会分配显存,而 glBufferData会分配显存。

(2) glBufferSubdata只会传输存在的数据到显存。

(3) 如果调用glBufferData 在已经分配过显存的缓冲区对象,那么会导致重新分配显存。原来的显存将会丢弃。

(4) 如果调用glBufferSubData所针对的缓冲区对象还没有使用glBufferData分配显存,则会产生一个错误。

(5) glBufferData 相当于 malloc 和 memcpy的结合体。 glBufferSubData 就像是memcpy。


2.示例代码


(1) 作用:不停跟新缓冲区对象中的数据,然后用于绘制图形。

(2) 代码:

void AdjustVertexData(float fXOffset, float fYOffset){vector<float> fNewData(ARRAY_COUNT(vertexPositions));memcpy(&fNewData[0],vertexPositions,sizeof(vertexPositions));for(int iVertex = 0; iVertex < ARRAY_COUNT(vertexPositions);iVertex +=4){fNewData[iVertex] +=fXOffset;fNewData[iVertex+1] +=fYOffset;}glBindBuffer(GL_ARRAY_BUFFER,positionBufferObject);glBufferSubData(GL_ARRAY_BUFFER,0,sizeof(vertexPosition),&fNewData[0]);glBindbuffer(GL_ARRAY_BUFFER,0);}

void display(){float fXOffset = 0.0f , fYOffset = 0.0f;ComputePositionOffsets(fXOffset,fYOffset);AdjustVertexData(fXOffset,fYOffset);glClearColor(0.0f,0.0f,0.0f,0.0f);glClear(GL_COLOR_BUFFER_BIT);glUseProgram(theProgram);glBindBuffer(GL_ARRAY_BUFFER,positionBufferObject);glEnableVertexAttribArray(0);glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,0,0);glDrawArrays(GL_TRIANGLES,0,3);glDisableVertexAttribArray(0);glUseProgram(0);glutSwapBuffers();glutPostRedisplay();}

下面两句:

ComputePositionOffsets(fXOffset,fYOffset);AdjustVertexData(fXOffset,fYOffset);

计算每次顶点的偏移,然后将缓冲区对象的数据进行修改,主要是利用:glBufferSubData()这个函数。

并且这段代码,利用glutPostRedisplay()它会导致循环的调用display()这个函数。


(3) 当顶点数有几百万个的时候,上面的方法是给每一个顶点都加上一个偏移值,这样会很慢。我们可以利用顶点着色器中的变量,让它获取一个偏移值,然后作用到每个顶点,这样就可以不适用AdjustVertexData()函数,代码如下:

#version 330layout(location = 0) in vec4 position;uniform vec2 offset;void main(){vec4 totalOffset = vec4(offset.x,offset.y,0.0,0.0);gl_Position = position + totalOffset;}

解释:通过不停更新uniform变量的值,来改变偏移。怎么修改呢? 涉及两个函数:

glGetUniformLocation();glUniform*() (注:<span style="color:#cc0000;">设置uniform变量的值的前提是:顶点着色器程序被激活,怎么激活呢?调用:glUseProgram</span> )

uniform变量不会频繁的改变像in定义的变量一样,in变量会在每次顶点着色器执行的时候发生改变。uniform变量的改变时用户显示的设置了它的新值。

上面两个函数一个是获取uniform变量的位置,一个是设置uniform变量的值。我们不能像in变量那样显示的指定uniform的index值。


3.glBufferData 的最后一个参数

(1)解释

GL_STATIC_DRAW:告诉opengl你只会设置缓冲区中的数据一次。

GL_STREAM_DRAW:告诉opengl你会频繁的设置缓冲区中的数据,通常为每帧都一次。


(2)目的

  设置恰当的值,对于得到更好的缓冲区对象的读写性能至关重要。


4.glutPostRedisplay()

  通常display函数只会在窗口大小改变或者窗口被遮盖的时候调用,glutPostRedisplay()会导致FreeGLUT去重复的调用display,不是立即调用,而是以合理的速度。


5.两个着色器公用变量

  对于定义在顶点着色器和段着色器中,相同类型和变量名的uniforms变量,它们会合并成一个,glGetUnfiromLocation得到的位置使同一个。

  因为:GLSL编译模型,链接顶点和段着色器成为一个单独的对象。


6.总结

(1) 对于顶点数据的操作,我们都可以放到顶点着色器中进行。因为我们放在缓冲区中的顶点数据,会一个一个的传输到顶点着色器中的变量中。所以我们就可以在顶点着色器中进行相关的变换,而不用在源程序中进行处理。

(2) 当顶点着色器和段着色器拥有uniform 变量 time的时候我们怎么获取和设置?

答:GLSL编译模型会将顶点着色器和段着色器链接为单独的一个对象,所以相同名字和类型的uniform的变量将会连接起来,所以只会有一个uniform的位置对于time,修改会在两个着色器都会起作用.这两个unifrom相当于一个,像C++中的引用一样

注:如果你创建一个uniform有相同名字却不同类型的变量在两个不同着色器,那么Opengl会产生一个链接错误,然后失败生成程序。不过也可能突然链接成一个unifrom变量.



4.Objects at Rest

1.Face Culling

void init(){  glEnable(GL_CULL_FACE);  glCullFace(GL_BACK);  glFrontFace(GL_CW); } 
(1)glEnable 函数是一个多功能的工具。有很多标记用于 on/off opengl的状态。glEnable 用于开启状态,glDisable用于关闭状态。GL_CULL_FACE 标记,用去告诉opengl激活:face culling.

(2)Face culling是什么?

  对于一个长方体,我们不可能同时看到超过3面,所以为什么要门要花费时间去绘制其余3面的呢?face culling就是告诉opengl不要绘制我们看不到的面。

(3)glFrontFace()定义了顺时针还是逆时针是正面。GL_CW(cw:ClockWise)顺时针是正面,GL_CCW(ccw:Counter-ColockWise) 逆时针是正面.

(4)glCullFace()定义了哪一面将被culled(根据glFrontFace的结果). 相应值有:GL_BACK, GL_FRONT, GL_FRONT_AND_BACK(它将culls 所有东西,没有三角形可以被绘画出来).



Primitives generated by previous stages are collected and then clipped to the view volume. Each vertex has a clip-space position (thegl_Position​ output of the lastVertex Processing stage).

回想起x,y,z都同时除以w是opengl所定义的转换剪切坐标到NDC坐标的一部分

这个地方的注释将mvp矩阵解释为是将顶点变换到NDC的矩阵。看到这儿我困惑了一下,查了查以前写的博客,我是这样认为的:经过投影矩阵,顶点被变换到投影空间,即clip space,然后再经过透视除法,顶点被变换到NDC。

这个地方其实应该只是一处笔误(或者说作者意思nds是clip space而不是ndc),因为书后面讲图元装配的章节是说的很清楚的,无论如何,我还是把clip space和NDC再理一下,正好结合一下gles 2.0。

1) 顶点通过vertex shader,乘以mvp矩阵,被变换到clip space, 变换后的值写入gl_Position。
clip space中,顶点是4d齐次坐标(x,y,z,w), 其中x,y,z的范围是[-w,w]。这儿会有个错误的理解,以为x,y,z的范围是[-1,1],确实clip space是一个[-1,1]的立方体。但是,并非所有的顶点都是在clip space中啊,所以才需要剪裁。而且经过透视除法变换到NDC的x,y,z范围才是[-1,1],由于透视除法是除以w,反过来就可以知道clip space中x,y,z的范围是[-w,w]。另外w=-Ze, 即eye空间z坐标的负值。

2)vs之后是图元装备阶段,图元装备包含3个步骤:clipping, 透视除法,viewport变换。

首先会进行的是剪裁(clipping)。clip会根据图元的类型进行处理,对于三角形如果图元部分处于clip space外面则三角形会被切割并生成新顶点形成一个三角扇。对于线如果部分剪裁就会被截断,也会生成新顶点。对于point sprite有点点特殊,点的位置只是用来检测是否在near,far plane之间,而quard会被用来检测是否在clip space之外,并且只要point sprite不被丢弃就直接保留原样(不会做部分剪裁),只是依靠scissor测试来做2d剪裁。
(书上这儿说clip volume的范围是[-w,w],我觉得不对,clip volume就是clip space,他的范围就是[-1,1],如果他是[-w,w],点也是[-w,w]还裁个啥?)

clipping之后就是透视除法了,通过clip的点(以及新生成的点)经过透视除法从clip齐次坐标变成了3d的NDC坐标。透视除法其实就是x/w,y/w,z/w。由于w=-Ze,因此这个除法就对x,y坐标产生了透视效果(近大远小),因此称为透视除法。显然NDC坐标(x,y,z)的范围是[-1,1],他是3d坐标,没有w。

再之后就是viewport变换,将3d的NDC坐标变换为3d的窗口坐标。x,y进过线性映射,变换到窗口坐标系x,y,而z会映射到depth range,经过z test后写入z buffer。viewport是由glViewport(x,y,w,h)定义的。depth range由glDepthRange(n,f)定义。


NDC: normal device coord


剪裁空间和NDC空间的不同只是除以了一个w,这时通过硬件来执行该除法。正射投影倾向于将W设置为1.0.

视景体是指成像景物所在空间的集合。它是一个空间集合体。投影变换的目的就是定义一个视景体,使得视景体外多余的部分裁剪掉,最终图像只是视景体内的有关部分。



五.Objects in Depth


1.glDrawArrays(GLEnum mode, GLint first, GLsizei count)

(1) 参数

mode: 指定绘制图形的类型,如:GL_TRIANGLES,GL_LINE_LOOP。

first: 指定在绑定在GL_ARRAY_BUFFER 点上缓冲区对象中的数据储存的起始位置(以缓冲区数据中每个元素大小为单位划分)。

count: 以first作为开头,后续有count个缓冲区数据元素。(first为起始位置,first+count-1为结束位置,此时位置使指缓冲区对象中数据的起始于结束)


(2) 作用

操纵绑定点为GL_ARRAY_BUFFER的缓冲区对象中的数据,用于绘制图形。


2.glDrawElement(GLenum mode, GLsizei count, GLenum type, const GLvoide *indices)

(1)参数

mode: 指定绘制图形的类型,如:GL_TRIANGLES,GL_LINE_LOOP。

count: 使用绑定在GL_ELEMENT_ARRAY_BUFFER点上的缓冲区对象的数据,总共count的元素。

type: 指明了GL_ELEMENT_ARRAY_BUFFER点上缓冲区对象中数据的类型。如:GL_UNSIGNED_SHORT, GL_UNSIGNED_BYTE, GL_UNSIGNED_INT

indices: 呈现为一个偏移,以字节为单位。指明了GL_ELEMENT_ARRAY_BUFFER点缓冲区中以indices为起始位置。


(2) 作用

使用绑定点为GL_ARRAY_BUFFER中的缓冲区中的顶点数据,使用绑定点为GL_ELEMENT_ARRAY_BUFFER中缓冲区中的下标数据,下标数据指明的是GL_ARRAY_BUFFER中每个顶点的下标位置。函数利用指明的下标,来绘制图形。


3.深度值

(1) 重叠的三角形正确的绘制应该是较近的三角形遮挡住较远的三角形。但光栅化按照用户给予的顺序来绘制每个三角形,如果有三角形重叠,后绘制的三角形将遮挡住先绘制的三角形。我们可以处理这种情况使用:先绘制最远处的三角形,然后绘制较近的三角形。但有几百万个三角形的时候,这个办法就无法实行。况且,如果我们要绘制三个相互重叠的三角形,这种情况就无法处理。


(2) 我们可以利用窗口坐标的 Z 值,来达到正确的绘制。段着色器输出颜色到图像的缓冲区,相应的也伴随着深度值被储存到 Depth-Buffer(也叫做:Z Buffer)。深度值只是一个单一的浮点值。


(3) 如果深度测试通过,段会输出颜色和深度值到正确的缓冲区中。如果失败则不会输出。为了激活深度测试,我们调用:glEnable(GL_DEPTH_TEST); glDepthFunc()设置相关的深度测试。随着段的深度值成为段输出的一部分,你可能会想象要在段着色器中计算,你确实可以,但是段的深度值就是窗口坐标下的Z坐标。如果你不在段着色器中显示写入深度值,这个值会被使用为默认值。


(4) glViewport()函数转换NDC坐标(范围:[-1,1])到窗口坐标,但是glViewport只是定义了转换X和Y坐标到NDC坐标。窗口坐标下的Z范围是[0,1] , NDC下的Z范围是[-1,1]并且是由glDepthRange函数定义。这个函数需要两个参数,zNear和zFar.这些值是在窗口坐标下,它简单的定义了一个线性的映射从NDC坐标到窗口坐标。如果zNear是0.5和

zFar是1.0, 则NDC的-1映射到0.5, 1映射到1.0.


(5) glDepthMask( GL_TRUE ) 它会导致渲染去将段中的深度值输入到深度缓冲区。

0 0
原创粉丝点击