【OpenGL】学习笔记#5

这一节主要讲光照计算和法线贴图。在此之前,先介绍一下顶点索引——它可以让我们重复利用“相似”的点,使用代码也很简单。

首先和之前一样填充一个顶点索引:

GLuint elementbuffer;
 glGenBuffers(1, &elementbuffer);
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer);
 glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

然后把DrawArrays换成这个:

// Index buffer
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer);

 // Draw the triangles !
 glDrawElements(
     GL_TRIANGLES,      // mode
     indices.size(),    // count
     GL_UNSIGNED_INT,   // type
     (void*)0           // element array buffer offset
 );

另外一提,GL_UNSIGNED_SHORT比GL_UNSIGNED_INT快喔。

好了,闲话不多说,进入正题。

我们都知道,我们的目标是让模型更加真实。所以,光照是必须的,不然就会像我们的红色三角形一样体现不出3D的效果。这时候就需要我们光照计算。但是呢,OpenGL并没有提供相关的接口或者API,因为光照千变万化,不同的光照计算会催生出不同的效果,所以需要我们自己实现。

中学课程中,我们都知道光线经过反射进入人眼,所以首要任务就是计算光线的反射。让我们从最简单的开始,计算一个点光源。它不停地向所有方向发射光线,由于我们要在摄像机坐标系里计算,所以设它的坐标为LightPosition_camera(命名时加上坐标系是很好的习惯),而摄像机在自己的坐标系里坐标是(0,0,0)。由于我们要一个顶点一个顶点渲染,所以我们设顶点坐标为VertexPosition_camera。同时,反射时要计算法线,法线由模型提供。这样,有了法线、坐标,就可以计算光照了。

首先,当一个光发生漫反射的时候(我们一般假设模型是漫反射,不然都是镜子了),入射角越小,光反射的就越集中;入射角越大,光反射的就越发散,当光通量相等时,很显然入射角小的光线比较亮(这段话仔细读读)。所以,只要求出射入我们摄像机的光线的入射角大小,即可算出光照强度。根据点积,a·b=|a|·|b|·cos(ab夹角),设ab夹角为Theta,则cos(Theta)=(a·b)/|a|·|b|。暂时忽略光线长度(*),此式子可以简化为cos(Theta)=dot(a,b) (dot(a,b)表示a,b的点积),又因为这个Theta实际上是入射角×2,但是我们只要求光照强度,方便起见,我们直接使用cos(Theta)作为光照强度。

但是有时候,如果光在三角形背后,那么此时a,b是负的,此时求得的点积没有意义,因此我们将它约定一个范围[1,0],用clamp函数来实现(clamp(dot(a,b),0,1))。

根据上文,很显然a,b是反射向量和入射向量。怎么计算呢?反射向量a直接使用向量相减,a = (0,0,0)-VertexPosition_camera,是尾为顶点头为摄像机的矢量;反射向量b使用向量相加,b = a+LightPosition_camera,为什么呢,因为此处我们把灯在摄像机坐标系里的位置看作一个大小和方向相等于从(0,0,0)到光源位置的矢量,根据向量相加法则,此向量等于前一个向量尾部(即顶点)到这一个向量头部(即灯),则顶点到灯的矢量就是入射矢量。

又因为物体反射的光线颜色取决于材料反射的光,所以光线颜色也受材料的颜色影响,这里的材料颜色就是纹素,设为MaterialDiffuseColor。

废了这么大劲,获得了光照强度,于是可以通过乘法建模:

color = MaterialDiffuseColor * LightColor * cosTheta;

太简单了,还记得我们之前标记的(*)吗,我们知道光通量与距离的平方成正比,正好|a|×|b|就是距离平方,因为向量长度相等(好好想想),多么完美,现在只需要把cosTheta除距离平方就可以了。

color = MaterialDiffuseColor * LightColor * cosTheta / (distance*distance);

为了控制灯的亮度,再加一个变量LightPower吧:

color = MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance);

关于光照计算的理论部分讲完了,来人呐,上代码!

In GLSL,Vertex Shader:

// Normal of the computed fragment, in camera space
 vec3 n = normalize( Normal_cameraspace );
 // Direction of the light (from the fragment to the light)
 vec3 l = normalize( LightDirection_cameraspace );

In GLSL,Fragment Shader:

// Output position of the vertex, in clip space : MVP * position
gl_Position =  MVP * vec4(vertexPosition_modelspace,1);

// Position of the vertex, in worldspace : M * position
Position_worldspace = (M * vec4(vertexPosition_modelspace,1)).xyz;

// Vector that goes from the vertex to the camera, in camera space.
// In camera space, the camera is at the origin (0,0,0).
vec3 vertexPosition_cameraspace = ( V * M * vec4(vertexPosition_modelspace,1)).xyz;
EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace;

// Vector that goes from the vertex to the light, in camera space. M is ommited because it's identity.
vec3 LightPosition_cameraspace = ( V * vec4(LightPosition_worldspace,1)).xyz;
LightDirection_cameraspace = LightPosition_cameraspace + EyeDirection_cameraspace;

// Normal of the the vertex, in camera space
Normal_cameraspace = ( V * M * vec4(vertexNormal_modelspace,0)).xyz; // Only correct if ModelMatrix does not scale the model ! Use its inverse transpose if not.

相信通过了我的说明,忘记这些之后的我一定能看懂,加油,奥利给!

终于说完了光照计算,下面开始了解FA♂线法线贴图。

什么是法线贴图呢(原谅我词汇贫乏)?

如果你玩过20世纪末21世纪初的某些3D游戏,例如古墓丽影1,你会发现墙像是一层模糊的墙纸。。。为什么说是墙纸?因为墙缝黑色的地方也一样像平面一样反光:这就是问题了。怎么解决呢?第一个办法是使用更精细的模型,使用更多三角形表示这些墙缝,虽然很直观,但是这样性能消耗很大,不适合大多数情况用;第二个办法是使用深度法,大意是给纹理设置一个深度,根据深度确定法线和反射等等,但是这样也有诸多弊端,此处不表。第三种方法就是使用法线贴图,是目前欺骗视觉应用最广泛的技术之一。它的原理就是给每一个纹素指定一个向量,这个向量就是法线。于是在片元着色器里我们就可以根据传入的法线进行反射的计算。我们常说的法线贴图就是把原有的RGB值替换为XYZ值,由于法线的outside方向大部分情况下是Z轴,导致法向量都偏向于Z轴,导致整体法线贴图大多呈蓝色。读取就是直接读取图片,只不过读取得到的数据传入着色器后不被我们当做纹理处理。

然而,还是存在问题。既然存在向量,就存在坐标系的问题。所以我们需要创建一个法线空间,用一个矩阵来进行转换。关于法线空间,请看此图:

显然法线空间有三个轴,up轴显然是法线,问题在于求两个切线。我们把它们分别叫做T(切线tangent)和B(副切线Bitangent),为了方便起见我们把它与UV坐标系的坐标轴重合,红色的就是U,代表T;绿色的是V,代表B。而三角形是我们要渲染的三角形,它有三个顶点。我们取其中两个,P1,P2。设E1为模型空间的向量,E2也是模型空间的向量。我们要求的就是T和B在模型空间的表达。显然,E2是△U2所在线段向量和△V2所在线段向量的合向量,注意此处△U2和△V2仅仅是一个一维标量,△U2=1-U2;△V2=V3。U2,V3都是坐标的一部分。所以,要使它们成为模型空间的向量,需要把它们乘上T和B,这样关系式就出来了:

E2 = △U2T + △V2B

同理列出E1等式:

E1 = △U1T + △V1B

根据这两个方程,求解出未知矢量T和B,就可以建立坐标系了!

矢量不好算,让我们把它们分解为XYZ,显然可以得到从N(法线空间,以后不提)到M(模型空间,以后不提)的转换矩阵:

Great!如果想从M到N,只需要对它进行一次转置即可:

invTBN = transpose(TBN)

代码:

float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
        glm::vec3 tangent = (deltaPos1 * deltaUV2.y   - deltaPos2 * deltaUV1.y)*r;
        glm::vec3 bitangent = (deltaPos2 * deltaUV1.x   - deltaPos1 * deltaUV2.x)*r;

deltaUV1.x对应△x,deltaUVn.x/y同理,deltaPos1对应E1,deltaPos2同理,此式子用于求解二元一次方程,求得T和B。(以上运算都处于模型坐标系)

向上的向量已知,通过这三个量,就可以建立矩阵了,让我们看看着色器:

In GLSL,Vertex Shader:

vec3 vertexTangent_cameraspace = MV3x3 * vertexTangent_modelspace;
    vec3 vertexBitangent_cameraspace = MV3x3 * vertexBitangent_modelspace;
    vec3 vertexNormal_cameraspace = MV3x3 * vertexNormal_modelspace;
    
    mat3 TBN = transpose(mat3(
        vertexTangent_cameraspace,
        vertexBitangent_cameraspace,
        vertexNormal_cameraspace    
    )); // You can use dot products instead of building this matrix and transposing it. See References for details.

    LightDirection_tangentspace = TBN * LightDirection_cameraspace;
    EyeDirection_tangentspace =  TBN * EyeDirection_cameraspace;

首先把三个基乘上M,V(模型Model,摄像机View)矩阵以转换到摄像机坐标系,TBN求转置矩阵,最后把入射反射光线乘上这个矩阵,就可以转换成以此法线空间的入射反射向量了。

Fragment Shader基本没变。

有点难喔

原文地址:https://www.cnblogs.com/dudujerry/p/13547520.html