Search code examples
directx-11hlsllightingphongspecular

Why does my shader produce incorrect specular results? [DX11]


I am currently attempting to implement Phong shading & lighting on a textured cube using HLSL and DirectX 11. I believe my ambient and diffuse lighting calculations to be correct and visually they produce an expected result. However when I apply specular lighting I get strange results (See Links)

Diffuse, Ambient & Specular: https://i.gyazo.com/f7700d758e05227e27be91ab0cfdf64e.png

Specular Only: https://i.gyazo.com/27bbfa0efce5c60748f61f54365cc042.png

My .fx file:

//Texture Variables
Texture2D txDiffuse[2] : register(t0);
SamplerState anisoSampler : register(s0);

//--------------------------------------------------------------------------------------
// Constant Buffer Variables
//--------------------------------------------------------------------------------------
cbuffer ConstantBuffer : register(b0)
{
    matrix World;
    matrix View;
    matrix Projection;

    float4 DiffuseMtrl;
    float4 DiffuseLight;

    float3 LightPosition;

    float4 AmbientMaterial;
    float4 AmbientLight;

    float4 specularMaterial;
    float4 specularLight;
    float  specularPower;

    float3 eyePosW;
}
//--------------------------------------------------------------------------------------
struct VS_INPUT
{
    float4 Pos : POSITION;
    float3 Normal : NORMAL;
    float2 Tex : TEXCOORD0;
};

//--------------------------------------------------------------------------------------
struct VS_OUTPUT
{
    float4 Pos : SV_POSITION;
    float3 Norm : NORMAL;
    float3 PosW : POSITION; //Eye Vector
    float3 LPos : LIGHTPOS; //Position of light
    float2 Tex : TEXCOORD0;
};

//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------

VS_OUTPUT VS(VS_INPUT vIn)
{
    VS_OUTPUT output = (VS_OUTPUT)0;
    float4 worldPosition;

    output.Tex = vIn.Tex;
    vIn.Pos.w = 1.0f;
    output.Pos = mul(vIn.Pos, World);


    output.Pos = mul(output.Pos, View);
    output.Pos = mul(output.Pos, Projection);

    worldPosition = mul(vIn.Pos, World);

    output.LPos = normalize(worldPosition - LightPosition);
    output.PosW = normalize(eyePosW.xyz - worldPosition.xyz);
    float3 normalW = mul(float4(vIn.Normal, 0.0f), World).xyz;
    normalW = normalize(normalW);


    output.Norm = normalW;
    return output;
}


//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float4 PS(VS_OUTPUT input) : SV_Target
{
    float4 textureColor = txDiffuse[0].Sample(anisoSampler, input.Tex);
    float4 bumpNormal = txDiffuse[1].Sample(anisoSampler, input.Tex);
    float4 output;
    float lightIntensity;
    float specularAmount;

    input.Norm = normalize(input.Norm);
    bumpNormal = normalize(bumpNormal);

    //Invert LDir for calculations
    float3 LDir = -input.LPos;

    lightIntensity = saturate(dot((input.Norm + bumpNormal.rgb), LDir));

    float3 r = reflect(LDir, (input.Norm + bumpNormal.rgb));
    specularAmount = pow(max(dot(r, input.PosW), 0.0f), specularPower);


    // Compute Colour using Diffuse ambient and texture
    float diffuseAmount = max(dot(LDir, (input.Norm + bumpNormal.rgb)), 0.0f);

    float3 diffuse = (diffuseAmount * (DiffuseMtrl * DiffuseLight).rgb) * lightIntensity;
    float3 ambient = AmbientLight * AmbientMaterial;
    float3 specular = specularAmount * (specularMaterial * specularLight).rgb * lightIntensity;

    output.rgb = ((ambient + diffuse) * textureColor) + specular;
    output.a = textureColor.a;

    return output;
}

Please excuse any messy code or variable names, I've attempted to get this to work using various tutorials and textbooks each with their own naming conventions so mine is a little messed up at the moment.

EDIT: Using the answer and some other sources of information I reworked my shader and got it working. On of my issues was incorrect padding in the constant buffer. I also added tangent space calculations and correctly converted my bump normals into a -1 to +1 range

//--------------------------------------------------------------------------------------
// Constant Buffer Variables
//--------------------------------------------------------------------------------------
cbuffer ConstantBuffer
{
    matrix World;
    matrix View;
    matrix Projection;

}

struct PointLight
{
    float4 ambient;
    float4 diffuse;
    float4 specular;

    float3 pos;
    float range;

    float3 att;
    float pad;

};

cbuffer CbPerFrame
{
    PointLight light;
    float3 eyePosW;
    float pad;
    float4 SpecularMaterial;
    float SpecularPower;
    float3 pad2;
};

Texture2D ObjTexture[2];
SamplerState ObjSamplerState;

//--------------------------------------------------------------------------------------
struct VS_OUTPUT
{
    float4 Pos : SV_POSITION;
    float4 worldPos : POSITION;
    float2 TexCoord : TEXCOORD;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
    float3 biTangent : BITANGENT;
};

void CalcTanBiTan(float3 norm, out float3 tan, out float3 biTan)
{

    float3 c1 = cross(norm, float3(0.0f, 0.0f, 1.0f));
    float3 c2 = cross(norm, float3(0.0f, 1.0f, 0.0f));

    if (length(c1) > length(c2))
    {
        tan = c1;
    }
    else
    {
        tan = c2;
    }
    tan = normalize(tan);

    biTan = cross(norm, tan);
    biTan = normalize(biTan);
}

//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------

VS_OUTPUT VS(float4 inPos : POSITION, float2 inTexCoord : TEXCOORD, float3 normal : NORMAL)
{
    VS_OUTPUT output = (VS_OUTPUT)0;

    output.Pos = mul(inPos, World);
    output.worldPos = mul(inPos, World);
    output.Pos = mul(output.Pos, View);
    output.Pos = mul(output.Pos, Projection);
    output.normal = mul(normal, World);
    output.TexCoord = inTexCoord;

    float3 tangent, biTangent;
    CalcTanBiTan(normal, tangent, biTangent);

    output.tangent = mul(tangent, (float3x3)World);
    output.tangent = normalize(output.tangent);
    output.biTangent = mul(biTangent, (float3x3)World);
    output.biTangent = normalize(output.biTangent);

    return output;
}


//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float4 PS(VS_OUTPUT input) : SV_Target
{
    input.normal = normalize(input.normal);
    float4 diffuse = ObjTexture[0].Sample(ObjSamplerState, input.TexCoord);
    float4 bumpMap = ObjTexture[1].Sample(ObjSamplerState, input.TexCoord);

    bumpMap = (bumpMap * 2.0f) - 1.0f;

    float3 bumpNormal = (bumpMap.x * input.tangent) + (bumpMap.y * input.biTangent) + (bumpMap.z * input.normal);
    bumpNormal = normalize(bumpNormal);

    float3 finalColor = float3(0.0f, 0.0f, 0.0f);

    //Create vector between light and pixel
    float3 lightToPixelVec = light.pos - input.worldPos;

    //find distance between light pos and pixel pos
    float d = length(lightToPixelVec);

    float3 finalAmbient = diffuse * light.ambient;
    if (d > light.range)
        return float4(finalAmbient, diffuse.a);

    //Turn lightToPixelVec into a unit vector describing pixel direction from the light position
    lightToPixelVec /= d;

    float howMuchLight = dot(lightToPixelVec, bumpNormal);

    float3 toEye = normalize(eyePosW - input.worldPos);
    float3 spec;
    [flatten]
    if (howMuchLight > 0.0f)
    {
        float3 v = reflect(-lightToPixelVec, bumpNormal);
        float specFactor = pow(max(dot(v, toEye), 0.0f), SpecularPower);
        spec = specFactor * SpecularMaterial * light.specular;
        finalColor += howMuchLight * diffuse * light.diffuse;

        finalColor /= light.att[0] + (light.att[1] * d) + light.att[2] * (d*d);
    }

    finalColor = saturate(finalColor + finalAmbient + spec);

    return float4(finalColor, diffuse.a);
}

Solution

  • First:

     float4 bumpNormal = txDiffuse[1].Sample(anisoSampler, input.Tex);
     bumpNormal = normalize(bumpNormal);
    

    the data store in a texture is usually a RGBA format with 8 bit for each channel. unless you are reading from a float texture. when texture store each channel with 8 bits. you need mapping the value form 0~1 to -1~1 in shader.

    second:

     input.Norm = normalize(input.Norm);
     bumpNormal = normalize(bumpNormal);
     (input.Norm + bumpNormal.rgb)
    

    add is incorrect, you should replace the normal with bump normal.

    third:

    i'm not sure in what space your normal stored, usually normal will be store in tangent space, that means you should also pass tangent or binormal from vertex shader.

    if you only want test the specular, you could use the original normal.