使用 OpenGL/GLSL 实现法线贴图
Implementing Normal Mapping using OpenGL/GLSL
我正在学习 GLSL 并尝试实现一些光照和贴图技巧。我正在使用 ShaderDesigner 工具。编码法线贴图后,我发现我的模型光照看起来不真实。这是我的代码和一些图片。如果可以告诉我我的问题是什么。
顶点着色器
#define MAX_LIGHTS 1
struct LightProps
{
vec3 direction[MAX_LIGHTS];
};
attribute vec3 tangent;
attribute vec3 bitangent;
varying LightProps lights;
void main()
{
vec3 N = normalize(gl_NormalMatrix*gl_Normal);
vec3 T = normalize(gl_NormalMatrix*tangent);
vec3 B = normalize(gl_NormalMatrix*bitangent);
mat3 TBNMatrix = mat3(T,B,N);
vec4 vertex = gl_ModelViewMatrix*gl_Vertex;
for(int i = 0; i < MAX_LIGHTS; i++)
{
vec4 lightPos = gl_LightSource[i].position;
lights.direction[i] = vec3(lightPos.w > 0 ? lightPos-vertex : lightPos);
lights.direction[i] *= TBNMatrix;
}
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
}
片段着色器
#define MAX_LIGHTS 1
struct LightProps
{
vec3 direction[MAX_LIGHTS];
};
uniform sampler2D textureUnit;
uniform sampler2D normalTextureUnit;
uniform vec4 TexColor;
varying LightProps lights;
void main()
{
vec3 N = normalize(texture2D(normalTextureUnit,gl_TexCoord[0].st).rgb*2.0-1.0);
vec4 color = vec4(0,0,0,0);
for(int i = 0; i < MAX_LIGHTS; i++)
{
vec3 L = lights.direction[i];
float dist = length(L);
L = normalize(L);
float NdotL = max(dot(N,L),0.0);
if(NdotL > 0)
{
float att = 1.0;
if(gl_LightSource[i].position.w > 0)
{
att = 1.0/ (gl_LightSource[i].constantAttenuation +
gl_LightSource[i].linearAttenuation * dist +
gl_LightSource[i].quadraticAttenuation * dist * dist);
}
vec4 ambient = gl_FrontLightProduct[i].ambient;
vec4 diffuse = clamp(att*NdotL*gl_FrontLightProduct[i].diffuse,0,1);
color += att*(ambient+diffuse);
}
}
vec4 textureColor = texture2D(textureUnit, vec2(gl_TexCoord[0]));
gl_FragColor = TexColor*textureColor + gl_FrontLightModelProduct.sceneColor + color;
}
我将 TexColor 设置为 (0.3,0.3,0.3,1.0)
并截图:
截图
当我向左旋转相机和灯光时,有一点灯光,
但是当我向右旋转时,平面完全 illuminated.I 认为有问题,因为没有法线贴图平面从两边看起来都一样。这是正常的纹理。提前致谢。
法线贴图:
该法线贴图位于切线-space,但您将其视为对象-space。
除了法线之外,每个顶点还需要一个双切线 and/or 切线向量,以便形成执行进出切线变换的基础-space。该矩阵通常简称为 TBN
.
这里有两个选择:
将所有光照方向向量转换为切线-space
- 对前向着色有用,可以在顶点着色器中完成
将法线贴图从切线-space 转换回视图-space
- 延迟着色需要,必须在片段着色器中完成
这两个选项都需要构建 TBN 矩阵,如果您的切线-space 基是正交的(可以配置像 Assimp 这样的建模软件来为您执行此操作),您可以转置做任何一个的 TBN 矩阵。
您正在实施前向着色,因此解决方案 #1 是您应该采用的方法。
下面是解决方案 #1 的必要步骤的粗略概述。通常你会在顶点着色器中计算光照方向向量以获得更好的性能。
用于将光照向量转换为切线的顶点着色器-space:
attribute vec3 tangent;
attribute vec3 bitangent;
varying vec3 N;
varying vec3 V;
varying vec3 E;
varying vec3 T;
varying vec3 B;
void main()
{
N = normalize(gl_NormalMatrix*gl_Normal);
V = vec3(gl_ModelViewMatrix*gl_Vertex);
E = normalize(-V);
T = normalize(gl_NormalMatrix*tangent);
B = normalize(gl_NormalMatrix*bitangent);
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
}
将光照向量转换为切线的片段着色器-space:
varying vec3 N;
varying vec3 V;
varying vec3 E;
varying vec3 B;
varying vec3 T;
uniform sampler2D textureUnit;
uniform sampler2D normalTextureUnit;
uniform vec4 TexColor;
#define MAX_LIGHTS 1
void main()
{
// Construct Tangent Space Basis
mat3 TBN = mat3 (T, B, N);
vec3 normal = normalize (texture2D(normalTextureUnit,gl_TexCoord[0].st).xyz*2.0 - 1.0);
vec4 color = vec4(0,0,0,0);
for(int i = 0; i < MAX_LIGHTS; i++)
{
vec4 lightPos = gl_LightSource[i].position;
vec3 L = lightPos.w > 0 ? lightPos.xyz - V : lightPos;
L *= TBN; // Transform into tangent-space
float dist = length(L);
L = normalize(L);
float NdotL = max(dot(L,N),0.0);
if(NdotL > 0)
{
float att = 1.0;
if(lightPos.w > 0)
{
att = 1.0/ (gl_LightSource[i].constantAttenuation +
gl_LightSource[i].linearAttenuation * dist +
gl_LightSource[i].quadraticAttenuation * dist * dist);
}
vec4 diffuse = clamp(att*NdotL*gl_FrontLightProduct[i].diffuse,0,1);
color += att*gl_FrontLightProduct[i].ambient + diffuse;
}
}
vec4 textureColor = texture2D(textureUnit, vec2(gl_TexCoord[0]));
gl_FragColor = TexColor*textureColor + gl_FrontLightModelProduct.sceneColor + color;
}
有一个教程 here 应该可以填补空白并解释如何计算切线和副切线。
我正在学习 GLSL 并尝试实现一些光照和贴图技巧。我正在使用 ShaderDesigner 工具。编码法线贴图后,我发现我的模型光照看起来不真实。这是我的代码和一些图片。如果可以告诉我我的问题是什么。
顶点着色器
#define MAX_LIGHTS 1
struct LightProps
{
vec3 direction[MAX_LIGHTS];
};
attribute vec3 tangent;
attribute vec3 bitangent;
varying LightProps lights;
void main()
{
vec3 N = normalize(gl_NormalMatrix*gl_Normal);
vec3 T = normalize(gl_NormalMatrix*tangent);
vec3 B = normalize(gl_NormalMatrix*bitangent);
mat3 TBNMatrix = mat3(T,B,N);
vec4 vertex = gl_ModelViewMatrix*gl_Vertex;
for(int i = 0; i < MAX_LIGHTS; i++)
{
vec4 lightPos = gl_LightSource[i].position;
lights.direction[i] = vec3(lightPos.w > 0 ? lightPos-vertex : lightPos);
lights.direction[i] *= TBNMatrix;
}
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
}
片段着色器
#define MAX_LIGHTS 1
struct LightProps
{
vec3 direction[MAX_LIGHTS];
};
uniform sampler2D textureUnit;
uniform sampler2D normalTextureUnit;
uniform vec4 TexColor;
varying LightProps lights;
void main()
{
vec3 N = normalize(texture2D(normalTextureUnit,gl_TexCoord[0].st).rgb*2.0-1.0);
vec4 color = vec4(0,0,0,0);
for(int i = 0; i < MAX_LIGHTS; i++)
{
vec3 L = lights.direction[i];
float dist = length(L);
L = normalize(L);
float NdotL = max(dot(N,L),0.0);
if(NdotL > 0)
{
float att = 1.0;
if(gl_LightSource[i].position.w > 0)
{
att = 1.0/ (gl_LightSource[i].constantAttenuation +
gl_LightSource[i].linearAttenuation * dist +
gl_LightSource[i].quadraticAttenuation * dist * dist);
}
vec4 ambient = gl_FrontLightProduct[i].ambient;
vec4 diffuse = clamp(att*NdotL*gl_FrontLightProduct[i].diffuse,0,1);
color += att*(ambient+diffuse);
}
}
vec4 textureColor = texture2D(textureUnit, vec2(gl_TexCoord[0]));
gl_FragColor = TexColor*textureColor + gl_FrontLightModelProduct.sceneColor + color;
}
我将 TexColor 设置为 (0.3,0.3,0.3,1.0)
并截图:
截图
当我向左旋转相机和灯光时,有一点灯光, 但是当我向右旋转时,平面完全 illuminated.I 认为有问题,因为没有法线贴图平面从两边看起来都一样。这是正常的纹理。提前致谢。
法线贴图:
该法线贴图位于切线-space,但您将其视为对象-space。
除了法线之外,每个顶点还需要一个双切线 and/or 切线向量,以便形成执行进出切线变换的基础-space。该矩阵通常简称为 TBN
.
这里有两个选择:
将所有光照方向向量转换为切线-space
- 对前向着色有用,可以在顶点着色器中完成
将法线贴图从切线-space 转换回视图-space
- 延迟着色需要,必须在片段着色器中完成
这两个选项都需要构建 TBN 矩阵,如果您的切线-space 基是正交的(可以配置像 Assimp 这样的建模软件来为您执行此操作),您可以转置做任何一个的 TBN 矩阵。
您正在实施前向着色,因此解决方案 #1 是您应该采用的方法。
下面是解决方案 #1 的必要步骤的粗略概述。通常你会在顶点着色器中计算光照方向向量以获得更好的性能。
用于将光照向量转换为切线的顶点着色器-space:
attribute vec3 tangent;
attribute vec3 bitangent;
varying vec3 N;
varying vec3 V;
varying vec3 E;
varying vec3 T;
varying vec3 B;
void main()
{
N = normalize(gl_NormalMatrix*gl_Normal);
V = vec3(gl_ModelViewMatrix*gl_Vertex);
E = normalize(-V);
T = normalize(gl_NormalMatrix*tangent);
B = normalize(gl_NormalMatrix*bitangent);
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
}
将光照向量转换为切线的片段着色器-space:
varying vec3 N;
varying vec3 V;
varying vec3 E;
varying vec3 B;
varying vec3 T;
uniform sampler2D textureUnit;
uniform sampler2D normalTextureUnit;
uniform vec4 TexColor;
#define MAX_LIGHTS 1
void main()
{
// Construct Tangent Space Basis
mat3 TBN = mat3 (T, B, N);
vec3 normal = normalize (texture2D(normalTextureUnit,gl_TexCoord[0].st).xyz*2.0 - 1.0);
vec4 color = vec4(0,0,0,0);
for(int i = 0; i < MAX_LIGHTS; i++)
{
vec4 lightPos = gl_LightSource[i].position;
vec3 L = lightPos.w > 0 ? lightPos.xyz - V : lightPos;
L *= TBN; // Transform into tangent-space
float dist = length(L);
L = normalize(L);
float NdotL = max(dot(L,N),0.0);
if(NdotL > 0)
{
float att = 1.0;
if(lightPos.w > 0)
{
att = 1.0/ (gl_LightSource[i].constantAttenuation +
gl_LightSource[i].linearAttenuation * dist +
gl_LightSource[i].quadraticAttenuation * dist * dist);
}
vec4 diffuse = clamp(att*NdotL*gl_FrontLightProduct[i].diffuse,0,1);
color += att*gl_FrontLightProduct[i].ambient + diffuse;
}
}
vec4 textureColor = texture2D(textureUnit, vec2(gl_TexCoord[0]));
gl_FragColor = TexColor*textureColor + gl_FrontLightModelProduct.sceneColor + color;
}
有一个教程 here 应该可以填补空白并解释如何计算切线和副切线。