法线贴图和平移破坏了我的光照效果

7
我遇到了一个法线贴图的问题。我使用ASSIMP库加载每个模型上的纹理和法线贴图。我使用ASSIMP库在每个对象上计算切向量,所以这些应该是正确的。这些对象在使用法线贴图时工作得很好,但是一旦我开始移动其中一个对象(因此影响了模型矩阵的平移),光照就会失败。如您在图像上看到的那样,地板(沿y轴向下平移)似乎失去了大部分漫反射光照,并且它的高光反射方向错误(应该在灯泡和玩家位置之间)。
可能与法线矩阵有关(虽然平移应该丢失),也可能与着色器中使用的错误矩阵有关。我已经没有更多的想法,希望您能对这个问题提供一些见解。
顶点着色器:
#version 330

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec3 tangent;
layout(location = 3) in vec3 color;
layout(location = 4) in vec2 texCoord;

// fragment pass through
out vec3 Position;
out vec3 Normal;
out vec3 Tangent;
out vec3 Color;
out vec2 TexCoord;

out vec3 TangentSurface2Light;
out vec3 TangentSurface2View;

uniform vec3 lightPos;
uniform vec3 playerPos;

// vertex transformation
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    mat3 normalMatrix = mat3(transpose(inverse(model))); 
    Position = vec3(model * vec4(position, 1.0)); 
    Normal = normalMatrix * normal;
    Tangent = tangent;
    Color = color;
    TexCoord = texCoord;

    gl_Position = projection * view * model * vec4(position, 1.0);

    // Calculate tangent matrix and calculate fragment bump mapping coord space.
    vec3 light = lightPos;
    vec3 n = normalize(normalMatrix * normal);
    vec3 t = normalize(normalMatrix * tangent);
    vec3 b = cross(n, t);
    // create matrix for tangent (from vertex to tangent-space)
    mat3 mat = mat3(t.x, b.x ,n.x, t.y, b.y ,n.y, t.z, b.z ,n.z);
    vec3 vector = normalize(light - Position);
    TangentSurface2Light = mat * vector;
    vector = normalize(playerPos - Position);
    TangentSurface2View = mat * vector;
}

片段着色器
    #version 330

in vec3 Position;
in vec3 Normal;
in vec3 Tangent;
in vec3 Color;
in vec2 TexCoord;

in vec3 TangentSurface2Light;
in vec3 TangentSurface2View;

out vec4 outColor;

uniform vec3 lightPos;
uniform vec3 playerPos;
uniform mat4 view;
uniform sampler2D texture0;
uniform sampler2D texture_normal; // normal

uniform float repeatFactor = 1;

void main()
{   
    vec4 texColor = texture(texture0, TexCoord * repeatFactor);
    vec4 matColor = vec4(Color, 1.0);
    vec3 light = vec3(vec4(lightPos, 1.0));
    float dist = length(light - Position);
    // float att = 1.0 / (1.0 + 0.01 * dist + 0.001 * dist * dist);
    float att = 1.0;
    // Ambient
    vec4 ambient = vec4(0.2);
    // Diffuse
    // vec3 surface2light = normalize(light - Position);
    vec3 surface2light = normalize(TangentSurface2Light);
    // vec3 norm = normalize(Normal); 
    vec3 norm = normalize(texture(texture_normal, TexCoord * repeatFactor).xyz * 2.0 - 1.0); 
    float contribution = max(dot(norm, surface2light), 0.0);
    vec4 diffuse = contribution * vec4(0.6);
    // Specular
    // vec3 surf2view = normalize(-Position); // Player is always at position 0
    vec3 surf2view = normalize(TangentSurface2View);
    vec3 reflection = reflect(-surface2light, norm); // reflection vector
    float specContribution = pow(max(dot(surf2view, reflection), 0.0), 32);
    vec4 specular = vec4(1.0) * specContribution;

    outColor = (ambient + (diffuse * att)+ (specular * pow(att, 3))) * texColor;
    // outColor = vec4(Color, 1.0) * texture(texture0, TexCoord);
}

编辑

编辑了着色器代码,计算全部在世界空间中进行,而不是在世界空间和相机空间之间来回切换(更易理解,且出错概率较小)。


3
我很难看出这与游戏有任何特定关系。这是在这里提问的完美场所。 - Christian Rau
1
他们不需要编程经验就能知道为什么映射法线可能会干扰着色器实现。这并不是关于告诉他可能存在的问题,而是关于说出他在着色器或代码其他部分做错了什么。否则我只会回答“似乎你的法线贴图不起作用,伙计”。 - Christian Rau
@jozxyqk:我刚刚发现当你移动相机时,surf2view的颜色应该会改变,而它们确实改变了。我还发现surface2light的颜色不会改变,因此与相机变换无关,这也是正确的。调试这些代码后,结果显示它们都是正确的。 - Joey Dewd
不要将所有其他向量从切线空间(它们本来就不在其中)转换,为什么不直接使用TBN矩阵将从法线贴图中采样的法线转换到视图空间呢?这是我见过的99.999%的切线空间法线贴图着色器的工作方式 :) 如果您使用“flat out mat3 TBN;”并将采样的法线乘以此矩阵以将其转换为视图空间,则可以轻松地将TBN矩阵传递到片段着色器中。 - Andon M. Coleman
@jozxyqk:是的,我的对象都是低多边形的,尤其是大地面(基本上只是一个有8个顶点的大立方体),所以光照在地面上会出现问题是有道理的。在片段着色器中使用TBN就可以解决这个问题。在讨论了celestis之后,我现在把所有的计算都放在世界空间中进行。由于我现在处于世界空间中,所以在玩家空间中使用vec3(0.0)作为玩家位置只有在相机空间中才有效,因此我不得不将其添加为一个统一变量(玩家位置在世界空间中)。 - Joey Dewd
显示剩余17条评论
1个回答

4
你正在对矩阵进行奇怪的操作。在VS中,你通过反向视图-世界变换来转换法线(即模型空间)。这没有任何意义。在世界空间中进行计算可能更容易。我有一些有效的示例代码,但它使用了稍微不同的命名。
顶点着色器:
void main_vs(in A2V input, out V2P output) 
{
    output.position = mul(input.position, _worldViewProjection);
    output.normal = input.normal;
    output.binormal = input.binormal;
    output.tangent = input.tangent;
    output.positionWorld = mul(input.position, _world);
    output.tex = input.tex;
}

在这里,我们将位置转换为投影(屏幕)空间,TBN保留在模型空间中,稍后将使用它们。同时,我们获取世界空间位置进行照明评估。

像素着色器:

void main_ps(in V2P input, out float4 output : SV_Target)
{
    float3x3 tbn = float3x3(input.tangent, -input.binormal, input.normal);

    //extract & decode normal:
    float3 texNormal = _normalTexture.Sample(_normalSampler, input.tex).xyz * 2 - 1;

    //now transform TBN-space texNormal to world space:
    float3 normal = mul(texNormal, tbn);
    normal = normalize(mul(normal, _world));

    float3 lightDirection = -_lightPosition.xyz;//directional
    float3 viewDirection = normalize(input.positionWorld - _camera);
    float3 reflectedLight = reflect(lightDirection, normal);

    float diffuseIntensity = dot(normal, lightDirection);
    float specularIntensity = max(0, dot(reflectedLight, viewDirection)*1.3);

    output = ((_ambient + diffuseIntensity * _diffuse) * _texture.Sample(_sampler, input.tex) 
        + pow(specularIntensity, 7) * float4(1,1,1,1)) * _lightColor;
}

这里我使用定向光,你应该做类似的事情。
float3 lightDirection = normalize(input.positionWorld - _lightPosition.xyz);//omni

这里我们首先有一个在TBN空间中的法线纹理。然后我们应用TBN矩阵将其转换为模型空间。接着应用世界矩阵将其转换为世界空间,此时我们已经有了光源位置、视点等信息。

上面省略了一些其他的着色器代码(DX11),但很容易翻译。

cbuffer ViewTranforms
{
    row_major matrix _worldViewProjection;
    row_major matrix _world;
    float3 _camera;
};

cbuffer BumpData
{
    float4 _ambient;
    float4 _diffuse;
};

cbuffer Textures
{
    texture2D _texture;
    SamplerState _sampler;

    texture2D _normalTexture;
    SamplerState _normalSampler;
};

cbuffer Light
{
    float4 _lightPosition;
    float4 _lightColor;
};

//------------------------------------

struct A2V
{
    float4 position : POSITION;
    float3 normal : NORMAL;
    float3 binormal : BINORMAL;
    float3 tangent : TANGENT;
    float2 tex : TEXCOORD;
};

struct V2P
{
    float4 position : SV_POSITION;
    float3 normal : NORMAL;
    float3 binormal : BINORMAL;
    float3 tangent : TANGENT;
    float3 positionWorld : NORMAL1;
    float2 tex : TEXCOORD;
};

此外,我在这里使用了预先计算的副法线:您应该保留计算代码(通过normal和tangent的叉积)。 希望这可以帮助你。

感谢您发布一些着色器代码,这可以帮助我找出法线贴图计算中的差异。然而,在您的顶点着色器中,我没有看到任何对normal向量进行normalMatrix计算的地方。据我所知,您应该使用一个normal矩阵(模型(视图)矩阵的转置逆矩阵,不包括平移分量),或者我漏掉了什么?目前,我正在使用模型和视图组件来生成法线矩阵,但我仍然不确定它是否应该只是模型或者是模型视图矩阵(当不进行平移时,模型视图矩阵提供良好的结果,而只使用模型则不行)。 - Joey Dewd
我的评论太长了,所以我会分开写。你明白为什么需要逆M(V)矩阵吗?我试图解释的是,你可以在不同的空间(世界、模型、TBN、视图甚至屏幕)中执行计算。为此,你需要转换数据,使得所有组件(眼睛、光线方向、顶点位置等)都在同一个空间中,否则它会返回垃圾值。 - Celestis
我理解这个逻辑。如果我有时让人感到困惑,我很抱歉,因为我还没有习惯将所有坐标空间可视化。如果我没弄错的话:我想在相机空间中进行计算,所以光和位置向量应该没问题(光的坐标已经在世界空间中了)。所以唯一需要改变的就是法线矩阵,将其转换为 mat3 normalMatrix = mat3(transpose(inverse(model)));。我是对的吗?然而,镜面反射现在似乎对相机移动有奇怪的反应。 - Joey Dewd
我知道什么是相机空间 :) 我的意思是,光源坐标被提供为世界空间坐标,然后通过视图矩阵转换到相机空间。我将尝试在模型空间而不是相机/眼睛空间中进行所有计算,并查看会发生什么 :) - Joey Dewd
我现在正在使用世界空间计算所有内容,并将摄像机位置(playerPos)作为世界空间中的uniform加入以帮助我的计算。我应该仅在vector = normalize(playerPos - Position); TangentSurface2View = mat * vector;处使用playerPos。然而,它仍然不能正常工作(相同的照明错误),我将编辑着色器代码到新代码,也许它能揭示一些关于为什么出现照明错误的见解。 - Joey Dewd
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接