OpenGL.Shader:8-学习光照-法线矩阵
上篇文章记录了光照的基本知识——法向量,以及简单的光照计算原理。从效果图可以看到,光源跟随摄像头位置变化,正方体各个面呈现出不同的阴暗效果。那如果光源位置不变,模型发生改变呢?根据以前所学的知识,模型位置发生改变,需要乘以mvp矩阵,那么法线向量是不是也乘以mvp矩阵就可以了呢?
错误示范,直接用mvp充当法线矩阵
#version 320 es
uniform mat4 _mvp;
uniform mat3 _normalMatrix;
uniform vec3 _lightDir;
uniform vec3 _lightColor;
uniform vec3 _lightDiffuse;
in vec3 _position;
in vec3 _normal;
in vec2 _uv;
out vec2 _outUV;
out vec4 _outComposeColor;
void main()
{
_outUV = _uv;
vec3 normal = normalize(_normalMatrix * _normal); //输入法线*法线矩阵 再归一化
float lightStrength = max(dot(normal, -_lightDir), 0.0);
_outComposeColor = vec4(_lightColor * lightStrength + _lightDiffuse, 1);
gl_Position = _mvp * vec4(_position,1.0);
}
#version 320 es
precision mediump float;
in vec4 _outComposeColor;
in vec2 _outUV;
uniform sampler2D _texture;
out vec4 _fragColor;
void main()
{
vec4 color = texture(_texture,_outUV);
_fragColor = color * _outComposeColor;
}
首先我们贴上使用的着色器程序组,引入uniform mat3 _normalMatrix; 用于改变法线向量的法线矩阵,normalize(_normalMatrix * _normal);得出变化后的法向量,再进行光照计算。
void render(Camera3D& camera)
{
sprogram.begin();
static float angle = 0;
angle += 0.3f;
CELL::matrix4 matRot;
matRot.rotateYXZ(angle, 0.0f, 0.0f);
// 这里的模型矩阵只进行简单进行旋转操作。
CELL::matrix4 model = mModelMatrix * matRot; //mModelMatrix只是一个简单的单位矩阵
CELL::matrix4 vp = camera.getProject() * camera.getView();
CELL::matrix4 mvp = (vp * model);
glUniformMatrix4fv(sprogram._mvp, 1, GL_FALSE, mvp.data());
// 设置法线矩阵 为 mvp
glUniformMatrix3fv(sprogram._normalMatrix, 1, GL_FALSE, mat4_to_mat3(mvp).data());
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, mCubeSurfaceTexId);
glUniform1i(sprogram._texture, 0);
glUniform3f(sprogram._lightDiffuse, 0.1f, 0.1f, 0.1f); // 漫反射 环境光
glUniform3f(sprogram._lightColor, 1.0f, 1.0f, 1.0f); // 定向光源的颜色
glUniform3f(sprogram._lightDir, // 定向光源的方向
static_cast<GLfloat>(camera._dir.x),
static_cast<GLfloat>(camera._dir.y),
static_cast<GLfloat>(camera._dir.z));
glVertexAttribPointer(static_cast<GLuint>(sprogram._position), 3, GL_FLOAT, GL_FALSE,
sizeof(CubeIlluminate::V3N3T2), &_data[0].x);
glVertexAttribPointer(static_cast<GLuint>(sprogram._normal), 3, GL_FLOAT, GL_FALSE,
sizeof(CubeIlluminate::V3N3T2), &_data[0].nx);
glVertexAttribPointer(static_cast<GLuint>(sprogram._uv), 2, GL_FLOAT, GL_FALSE,
sizeof(CubeIlluminate::V3N3T2), &_data[0].u);
glDrawArrays(GL_TRIANGLES, 0, 36);
sprogram.end();
}
//-------------------------------
tmat3x3<T> mat4_to_mat3(const tmat4x4<T> & m)
{
return tmat3x3<T>(
tvec3<T>(m[0][0],m[0][1],m[0][2])
,tvec3<T>(m[1][0],m[1][1],m[1][2])
,tvec3<T>(m[2][0],m[2][1],m[2][2]));
}
再贴上渲染参数的设置,简单的沿着Y轴进行旋转。有一点需要注意的是,法线向量是vec3的类型,只代表方向,没有位置信息。mvp是4x4的matrix,所以两者不能直接相乘,自定义mat4_to_mat3方法去掉w分量。
从效果图中看到,自动旋转后光照计算失常了,手动滑动镜头,使光照与模型到达指定角度后好像又正常了。为什么会这样呢?其中隐含的数学道理我们来简单分析一波:
在OpenGL ES 2.0中,将一个顶点转换至眼睛坐标系中,通过:
vertexEyeSpace = view_Matrix * model_Matrix * vertex;
那为什么我们不能像法线向量一样做同样的工作呢?首先法线向量是3个floats的向量,而modelView矩阵是4X4的矩阵。这可以通过以下代码来实现:
normalEyeSpace = vec3(view_Matrix * model_Matrix * vec4(normal, 0.0));
上面存在一个潜在的问题:
顶点是(x,y,z)表示缺省的向量(x,y,z,1);而法线向量是一个方向性向量,没有固定的点。因为法线向量可由法线上两个固定顶点(x1,y1,z1,1), (x2,y2,z2,1)相减得到。因此,从这点就可以看到顶点与法线向量不同,也就造成了不现的变换。法线向量只能保证方向性的一致,不能保证位置性的一致。
在上图我们看到一个三角形, 有一个法线向量和平面切线向量. 接下来的图显示当一个观察矩阵缩放的时候所显示的情景。如果我们还是调用上面的代码的话:
如上图, 观察矩阵影响到所有的顶点以及法线. 很明显这个结果是错误的. 法线并不垂直于平面切线。所以现在我们得知并不能将观察矩阵应用于所有的法线。所以我们应当使用怎样的矩阵来变换法线向量呢?看看下方表格的解决方法。
由上边方框里边的推导我们可知,如果我们想要对u向量做一个对应矩阵为A的变换,如果我们要保证在变换后法线向量任然要垂直于对应的切线向量的话,那么我们就需要对垂向量做一个逆转矩阵变换就好了,这样子我们就不用担心上图中所对应的情况发生了,而且做了逆转矩阵变换后就是我们期待的正确结果。
所以最终结论是 变换法线的矩阵 = 模型矩阵的逆矩阵的转置
void render(Camera3D& camera)
{
sprogram.begin();
static float angle = 0;
angle += 0.1f;
CELL::matrix4 matRot;
matRot.rotateYXZ(angle, 0.0f, 0.0f);
// 这里的模型矩阵只进行简单进行旋转操作。
CELL::matrix4 model = mModelMatrix * matRot;
// 法线矩阵 = 模型矩阵的逆矩阵的转置
CELL::matrix3 matNormal= mat4_to_mat3(model)._inverse()._transpose();
glUniformMatrix3fv(sprogram._normalMatrix, 1, GL_FALSE, matNormal.data());
CELL::matrix4 vp = camera.getProject() * camera.getView();
CELL::matrix4 mvp = (vp * model);
glUniformMatrix4fv(sprogram._mvp, 1, GL_FALSE, mvp.data());
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, mCubeSurfaceTexId);
glUniform1i(sprogram._texture, 0);
... ...
}
更新效果如下,可以看到无论是 模型旋转 还是 手动滑动摄像机改变光源位置,最终的光照效果都是正确的。
Demo工程链接 https://github.com/MrZhaozhirong/NativeCppApp ->LightRenderer.cpp CubeIlluminate.hpp CubeIlluminateProgram.hpp