OpenGL法线贴图
OpenGL Normal Mapping
我正在尝试使用我创建的简单立方体实现法线贴图。我遵循了本教程 https://learnopengl.com/Advanced-Lighting/Normal-Mapping 但我无法真正理解在绘制 3d 对象时应该如何进行法线映射,因为本教程使用的是 2d 对象。
特别是,我的立方体似乎几乎正确地点亮了,但我认为有些东西没有按应有的方式工作。我正在使用将输出绿色矢量法线和红色矢量切线的几何着色器来帮助我。这里是我post我作品的三张截图。
直射
侧光
在这里,我实际上尝试以不同的方式计算我的法线和切线。 (大错特错)
在第一张图片中,我一次计算一个面的立方体法线和切线。这似乎适用于脸部,但如果我旋转我的立方体,我认为邻接面上的照明是错误的。正如您在第二张图片中看到的,它并没有完全消失。
在第三张图片中,我尝试对每个顶点的所有法线和切线求和,因为我认为应该这样做,但结果似乎很不对劲,因为光照太少。
最后,我的问题是我应该如何计算法线和切线。
我应该考虑按面计算还是对所有相关面的每个顶点求和矢量,否则?
编辑 --
我将法线和切线传递给顶点着色器并设置我的 TBN 矩阵。但是正如您在第一张图片中看到的那样,面对面地绘制我的立方体,似乎与我直接看的那张脸(光线充足)相邻的那些脸没有正确照亮,我不知道为什么。我以为我没有正确计算 'per face' 法线和切线。我认为计算一些通常对对象进行计数的法线和切线可能是正确的方法。
如果计算第二个图像中可见的法线和切线(绿色法线,红色切线)以设置 TBN 矩阵是正确的,为什么右脸似乎没有很好地照亮?
编辑 2 --
顶点着色器:
void main(){
texture_coordinates = textcoord;
fragment_position = vec3(model * vec4(position,1.0));
mat3 normalMatrix = transpose(inverse(mat3(model)));
vec3 T = normalize(normalMatrix * tangent);
vec3 N = normalize(normalMatrix * normal);
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T);
mat3 TBN = transpose(mat3(T,B,N));
view_position = TBN * viewPos; // camera position
light_position = TBN * lightPos; // light position
fragment_position = TBN * fragment_position;
gl_Position = projection * view * model * vec4(position,1.0);
}
在 VS 中,我设置了我的 TBN 矩阵,并将所有光、片段和视图向量转换为切线 space;这样做我将不必在片段着色器中进行任何其他计算。
片段着色器:
void main() {
vec3 Normal = texture(TextSamplerNormals,texture_coordinates).rgb; // extract normal
Normal = normalize(Normal * 2.0 - 1.0); // correct range
material_color = texture2D(TextSampler,texture_coordinates.st); // diffuse map
vec3 I_amb = AmbientLight.color * AmbientLight.intensity;
vec3 lightDir = normalize(light_position - fragment_position);
vec3 I_dif = vec3(0,0,0);
float DiffusiveFactor = max(dot(lightDir,Normal),0.0);
vec3 I_spe = vec3(0,0,0);
float SpecularFactor = 0.0;
if (DiffusiveFactor>0.0) {
I_dif = DiffusiveLight.color * DiffusiveLight.intensity * DiffusiveFactor;
vec3 vertex_to_eye = normalize(view_position - fragment_position);
vec3 light_reflect = reflect(-lightDir,Normal);
light_reflect = normalize(light_reflect);
SpecularFactor = pow(max(dot(vertex_to_eye,light_reflect),0.0),SpecularLight.power);
if (SpecularFactor>0.0) {
I_spe = DiffusiveLight.color * SpecularLight.intensity * SpecularFactor;
}
}
color = vec4(material_color.rgb * (I_amb + I_dif + I_spe),material_color.a);
}
处理不连续性与连续性
你想错了。
根据用例,您的法线贴图可能是连续的或不连续的。例如在你的立方体中,想象一下如果每个面都有不同的表面类型,那么法线会根据你当前所在的面而不同。
您使用的法线由纹理本身决定,而不是由片段中的任何混合决定。
实际算法是
- 加载正常的 rgb 值
- 转换为 -1 到 1 范围
- 按模型矩阵旋转
- 在阴影计算中使用新值
如果您想要连续的法线,则需要确保您使用的纹理 space 中的图表符合纹理坐标的限制。
从数学上讲,这意味着如果 U 和 V 是映射到 Shape 的法线场 N 的 R^2 区域,那么如果映射的函数是 f,则应该是:
如果 lim S(x_1, x_2) = lim S(y_1, y_2) 其中 {x1,x2} \subset U 和 {y_1, y_2} \subset V 然后 lim f(x_1, x_2) = lim f(y_1, y_2).
用简单的英语来说,如果图表中的坐标映射到形状接近的位置,那么它们映射到的法线也应该接近法线 space。
TL;DR 不要插入片段。这是应该在烘焙时由法线贴图本身完成的事情,而不是由您在渲染时完成。
处理切线space
你有2个选项。选项 n1,将切线 T 和法线 N 传递给着色器。在这种情况下,双法线 B 是 T X N,基础 {T, N, B} 为您提供真实的 space,其中需要表达法线。
假设在切线上space,x在边上,y在前,z在上。您转换后的法线变为 (xB, yT, zN)。
如果不通过切线,则必须先创建一个与法线正交的随机向量,然后将其用作切线。
(注意N是模型法线,其中(x,y,z)是法线贴图法线)
我正在尝试使用我创建的简单立方体实现法线贴图。我遵循了本教程 https://learnopengl.com/Advanced-Lighting/Normal-Mapping 但我无法真正理解在绘制 3d 对象时应该如何进行法线映射,因为本教程使用的是 2d 对象。
特别是,我的立方体似乎几乎正确地点亮了,但我认为有些东西没有按应有的方式工作。我正在使用将输出绿色矢量法线和红色矢量切线的几何着色器来帮助我。这里是我post我作品的三张截图。
直射
侧光
在这里,我实际上尝试以不同的方式计算我的法线和切线。 (大错特错)
在第一张图片中,我一次计算一个面的立方体法线和切线。这似乎适用于脸部,但如果我旋转我的立方体,我认为邻接面上的照明是错误的。正如您在第二张图片中看到的,它并没有完全消失。
在第三张图片中,我尝试对每个顶点的所有法线和切线求和,因为我认为应该这样做,但结果似乎很不对劲,因为光照太少。
最后,我的问题是我应该如何计算法线和切线。 我应该考虑按面计算还是对所有相关面的每个顶点求和矢量,否则?
编辑 --
我将法线和切线传递给顶点着色器并设置我的 TBN 矩阵。但是正如您在第一张图片中看到的那样,面对面地绘制我的立方体,似乎与我直接看的那张脸(光线充足)相邻的那些脸没有正确照亮,我不知道为什么。我以为我没有正确计算 'per face' 法线和切线。我认为计算一些通常对对象进行计数的法线和切线可能是正确的方法。 如果计算第二个图像中可见的法线和切线(绿色法线,红色切线)以设置 TBN 矩阵是正确的,为什么右脸似乎没有很好地照亮?
编辑 2 --
顶点着色器:
void main(){
texture_coordinates = textcoord;
fragment_position = vec3(model * vec4(position,1.0));
mat3 normalMatrix = transpose(inverse(mat3(model)));
vec3 T = normalize(normalMatrix * tangent);
vec3 N = normalize(normalMatrix * normal);
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T);
mat3 TBN = transpose(mat3(T,B,N));
view_position = TBN * viewPos; // camera position
light_position = TBN * lightPos; // light position
fragment_position = TBN * fragment_position;
gl_Position = projection * view * model * vec4(position,1.0);
}
在 VS 中,我设置了我的 TBN 矩阵,并将所有光、片段和视图向量转换为切线 space;这样做我将不必在片段着色器中进行任何其他计算。
片段着色器:
void main() {
vec3 Normal = texture(TextSamplerNormals,texture_coordinates).rgb; // extract normal
Normal = normalize(Normal * 2.0 - 1.0); // correct range
material_color = texture2D(TextSampler,texture_coordinates.st); // diffuse map
vec3 I_amb = AmbientLight.color * AmbientLight.intensity;
vec3 lightDir = normalize(light_position - fragment_position);
vec3 I_dif = vec3(0,0,0);
float DiffusiveFactor = max(dot(lightDir,Normal),0.0);
vec3 I_spe = vec3(0,0,0);
float SpecularFactor = 0.0;
if (DiffusiveFactor>0.0) {
I_dif = DiffusiveLight.color * DiffusiveLight.intensity * DiffusiveFactor;
vec3 vertex_to_eye = normalize(view_position - fragment_position);
vec3 light_reflect = reflect(-lightDir,Normal);
light_reflect = normalize(light_reflect);
SpecularFactor = pow(max(dot(vertex_to_eye,light_reflect),0.0),SpecularLight.power);
if (SpecularFactor>0.0) {
I_spe = DiffusiveLight.color * SpecularLight.intensity * SpecularFactor;
}
}
color = vec4(material_color.rgb * (I_amb + I_dif + I_spe),material_color.a);
}
处理不连续性与连续性
你想错了。
根据用例,您的法线贴图可能是连续的或不连续的。例如在你的立方体中,想象一下如果每个面都有不同的表面类型,那么法线会根据你当前所在的面而不同。
您使用的法线由纹理本身决定,而不是由片段中的任何混合决定。
实际算法是
- 加载正常的 rgb 值
- 转换为 -1 到 1 范围
- 按模型矩阵旋转
- 在阴影计算中使用新值
如果您想要连续的法线,则需要确保您使用的纹理 space 中的图表符合纹理坐标的限制。
从数学上讲,这意味着如果 U 和 V 是映射到 Shape 的法线场 N 的 R^2 区域,那么如果映射的函数是 f,则应该是:
如果 lim S(x_1, x_2) = lim S(y_1, y_2) 其中 {x1,x2} \subset U 和 {y_1, y_2} \subset V 然后 lim f(x_1, x_2) = lim f(y_1, y_2).
用简单的英语来说,如果图表中的坐标映射到形状接近的位置,那么它们映射到的法线也应该接近法线 space。
TL;DR 不要插入片段。这是应该在烘焙时由法线贴图本身完成的事情,而不是由您在渲染时完成。
处理切线space
你有2个选项。选项 n1,将切线 T 和法线 N 传递给着色器。在这种情况下,双法线 B 是 T X N,基础 {T, N, B} 为您提供真实的 space,其中需要表达法线。
假设在切线上space,x在边上,y在前,z在上。您转换后的法线变为 (xB, yT, zN)。
如果不通过切线,则必须先创建一个与法线正交的随机向量,然后将其用作切线。
(注意N是模型法线,其中(x,y,z)是法线贴图法线)