Help with PBR implementation

Started by
10 comments, last by Gamewolf 7 years, 10 months ago

Hi!

So over the summer I decided to continue working on my 3D project that i started earlier this year, and one of the things I wanted to try was implementing pbr, since I wanted to refresh my shader programming as well as try something a bit fancier than what I have done before. For the information I have used the notes from when frostbite moved to pbr as well as some notes and code from one of the teachers at my school. I am definitly no expert at the subject which is why I'm now looking for some help with it :) I have tried inputing the same materials and models in another program (marmoset) to get some kind of reference of how it should look when it's done (though they might of course use more effects or different implementations than I do so I'm not expecting it to be exactly the same), pictures can be seen here.

So to start I used the information to implement the lighting calculations, the result can be seen here

After reading some more I realised that I needed to implement some sort of reflections, but I still think that there might be some slight problems with my calculations even before that, at least I feel like the grip on the gun is too dark, since it isn't metallisc, shouldn't it be more brownish even at this stage? Same things kinda go for the wood planks on the box.

For reflections my first thought was to utilize a cubemap and use that, so I have implemented a skybox and cubemap rendering. But at this point I am not really sure how to then use it in my shader. The frostbite notes do not seem to mention it (at least not that I found), so I used some info and code that I got from my teacher to experiment. The result can be seen here

It's not terrible (in my eyes) but it doesn't seem correct either. The reflection seems to be to dominant and things seem to be too "shiny", especially the wood. Also for some reason now it doesn't actually seem to mather where I place my light or how far it's range is (triangles seems to get light in someway, even those on the turned away from the source, as well as ones that should definitly be out of range). the white (or whatever colour I set the light to) glossy parts are affected by the light data but the rest seems to disregard it? either way it's obviously not correct to do it the way I have.

the shaders are as follows

Vertex Shader


cbuffer WorldMatrix : register(b0)
{
	float4x4 WorldMatrix;
}

cbuffer CameraMatrices : register(b1)
{
	float4x4 ViewMatrix;
	float4x4 ProjectionMatrix;
}

cbuffer CameraPosition : register(b2)
{
	float4 camPos;
}


struct VS_IN
{
	float3 Pos : POSITION;
	float2 Tex : TEXCOORD;
	float3 Normal : NORMAL;

	float3 Tangent : TANGENT;
	float3 Bitangent : BITANGENT;
};

struct VS_OUT
{
	float4 Pos : SV_POSITION;
	float4 WorldPos : WORLDPOS;
	float2 Tex : TEXCOORD;
	float3 Normal : NORMAL;

	float3 Tangent : TANGENT;
	float3 Bitangent : BITANGENT;
};
//-----------------------------------------------------------------------------------------
// VertexShader: VSScene
//-----------------------------------------------------------------------------------------
VS_OUT VS_main(VS_IN input)
{
	VS_OUT output = (VS_OUT)0;

	output.WorldPos = output.Pos = float4(input.Pos, 1);
	output.Pos = mul(mul(mul(output.Pos, WorldMatrix), ViewMatrix), ProjectionMatrix);
	output.WorldPos = mul(output.WorldPos, WorldMatrix);
	//output.Pos = mul(mul(output.Pos, ViewMatrix), ProjectionMatrix);

	output.Tex = input.Tex;

	output.Normal = normalize(mul(input.Normal, WorldMatrix));
	output.Tangent = normalize(mul(input.Tangent, WorldMatrix));
	output.Bitangent = normalize(mul(input.Bitangent, WorldMatrix));

	return output;
}

Pixel Shader


#define NROFLIGHTS 256

static const float PI = 3.14159265359f;

cbuffer LightData  : register(b0)
{
	float4 LightPos[NROFLIGHTS];
	float4 LightColour[NROFLIGHTS];
	float4 LightRangeType[NROFLIGHTS];
};

cbuffer CameraData  : register(b1)
{
	float4 CameraPos;
	float4 ViewVec;
};

struct VS_OUT
{
	float4 Pos : SV_POSITION;
	float4 WorldPos : WORLDPOS;
	float2 Tex : TEXCOORD;
	float3 Normal : NORMAL;

	float3 Tangent : TANGENT;
	float3 Bitangent : BITANGENT;
};

Texture2D diffuse : register(t0);
Texture2D roughness : register(t1);
Texture2D metallic : register(t2);
Texture2D normalMap : register(t3);
Texture2D displacementMap : register(t4);
TextureCube reflection : register(t5);

SamplerState standardSamp : register(s0);


float3 F_Schlick(float3 f0, float f90, float u)
{
	return f0 + (f90 - f0) * pow(1.0f - u, 5.0f);
}

float Fr_DisneyDiffuse(float NdotV, float NdotL, float LdotH, float linearRoughness)
{

	float energyBias = lerp(0, 0.5, linearRoughness);
	float energyFactor = lerp(1.0, 1.0 / 1.51, linearRoughness);
	float fd90 = energyBias + 2.0 * LdotH * LdotH * linearRoughness;
	float3 f0 = float3 (1.0f, 1.0f, 1.0f);

	float lightScatter = F_Schlick(f0, fd90, NdotL).r;
	float viewScatter = F_Schlick(f0, fd90, NdotV).r;

	return lightScatter * viewScatter * energyFactor;
}

float V_SmithGGXCorrelated(float NdotV, float NdotL, float alphaG) {

	// Original formulation of G_SmithGGX Correlated
	// lambda_v = (-1 + sqrt ( alphaG2 * (1 - NdotL2 ) / NdotL2 + 1)) * 0.5f;
	// lambda_l = (-1 + sqrt ( alphaG2 * (1 - NdotV2 ) / NdotV2 + 1)) * 0.5f;
	// G_SmithGGXCorrelated = 1 / (1 + lambda_v + lambda_l );
	// V_SmithGGXCorrelated = G_SmithGGXCorrelated / (4.0 f * NdotL * NdotV );

	float alphaG2 = alphaG * alphaG;
	//Caution: the "NdotL*" and "NdotV *" are explicitely inversed, this is not a mistake
	float Lambda_GGXV = NdotL * sqrt((-NdotV * alphaG2 + NdotV) * NdotV + alphaG2);
	float Lambda_GGXL = NdotV * sqrt((-NdotL * alphaG2 + NdotL) * NdotL + alphaG2);

	return 0.5f / (Lambda_GGXV + Lambda_GGXL);
}

float D_GGX(float NdotH, float m)
{
	// Divide by PI is apply later
	float m2 = m * m;
	float f = (NdotH * m2 - NdotH) * NdotH + 1;

	return m2 / (f * f);
}


float CalcFr(float3 f0, float f90, float LdotH, float NdotV, float NdotL, float roughness, float NdotH)
{
	float3 F = F_Schlick(f0, f90, LdotH);
	float Vis = V_SmithGGXCorrelated(NdotV, NdotL, roughness);
	float D = D_GGX(NdotH, roughness);
	float Fr = D * F * Vis / PI;

	return Fr;

}

float3 CalcBumpedNormal(VS_OUT indata)
{
	float3 Normal = normalize(indata.Normal);
	float3 Tangent = normalize(indata.Tangent);
	Tangent = normalize(Tangent - dot(Tangent, Normal) * Normal);
	float3 Bitangent = normalize(cross(Tangent, Normal));
	float3 BumpMapNormal = normalMap.Sample(standardSamp, indata.Tex).xyz;  //texture(gNormalMap, TexCoord0).xyz;
	//return BumpMapNormal;
	BumpMapNormal = normalize(2.0 * BumpMapNormal - float3(1.0, 1.0, 1.0));
	float3 NewNormal;
	float3x3 TBN = float3x3(Tangent, Bitangent, Normal);
	NewNormal = mul(BumpMapNormal,TBN);
	NewNormal = normalize(NewNormal);
	return NewNormal;
}

float3 FresnelSchlickWithRoughness(float3 SpecularColor, float3 E, float3 N, float Gloss)
{
	return SpecularColor + (max(Gloss, SpecularColor) - SpecularColor) * pow(1 - saturate(dot(E, N)), 5);
}
float3 SpecularEnvmap(float3 E, float3 N, float3 R, float3 SpecularColor, float Gloss)
{
	//float3 Envcolor = texCUBElod(EnvironmentTexture, float4(R, EnvMapMipmapScaleBias.x * Gloss + EnvMapMipmapScaleBias.y)).rgb;
	float3 Envcolor = reflection.Sample(standardSamp, R).rgb;
	return FresnelSchlickWithRoughness(SpecularColor, E, N, Gloss) * Envcolor.rgb; // * EnvMapScaleAndModulate; // EnvMapScaleAndModulate is used to decompress range
}

float4 PS_main(VS_OUT input) : SV_Target
{
	//float4 texFloat = diffuse.Sample(standardSamp, input.Tex);
	//return texFloat;

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

	float EPSILON = 0.00001f;
	float3 LightVec = LightPos[0].xyz - input.WorldPos.xyz;
	float dist = length(LightVec);
	float LightPower = max(1.0f - (dist / LightRangeType[0].x), 0.0f); 
	float3 calculatedNormal = CalcBumpedNormal(input);
	LightVec = normalize(LightVec);

	float3 V = normalize(CameraPos.xyz - input.WorldPos.xyz);
	float NdotV = abs(dot(calculatedNormal, V)) + EPSILON;
	float3 H = normalize(V + LightVec);
	float LdotH = saturate(dot(LightVec, H));
	float NdotH = saturate(dot(calculatedNormal, H));
	float NdotL = saturate(dot(calculatedNormal, LightVec));

	//LightPower = 0.0f;

	float3 baseColor = diffuse.Sample(standardSamp, input.Tex).rgb;
	float metalness = metallic.Sample(standardSamp, input.Tex).r;

	//roughness same for both diffuse and specular, as in Frostbite
	float linearRoughness = saturate(roughness.Sample(standardSamp, input.Tex).r + EPSILON);
	float roughness = pow(linearRoughness, 2);

	float3 diffuseColor = lerp(baseColor.rgb, 0.0f.rrr, metalness.r);
	//float3 diffuseColor = (1 - metalness.r) * baseColor;
	float3 f0 = lerp(0.03f.rrr, baseColor.rgb, metalness.r);
	float3 specularColor = lerp(f0, baseColor.rgb, metalness.r);
	//float3 specularColor_f0 = lerp(0.04f.rrr, baseColor.rgb, metalness.r);


	float3 incident = -V;
	float3 reflectionVector = reflect(incident, calculatedNormal);
	//float4 reflectionColor = reflection.Sample(samAnisotropic, reflectionVector);
	float3 reflectionColor = reflection.Sample(standardSamp, reflectionVector).rgb;


	float4 litColor = float4(reflectionColor, 1.0f);// *metalness;
	//return litColor;


	//Calculate fd (diffuse)

	float fd = Fr_DisneyDiffuse(NdotV, NdotL, LdotH, linearRoughness) / PI;

	float3 diffuse = fd.xxx * LightColour[0].xyz * LightPower * diffuseColor;

	//Calculate the fr (specular)

	float f90 = 0.16f * metalness * metalness;
	float fr = CalcFr(f0, f90, LdotH, NdotV, NdotL, roughness, NdotH);
	float3 se = SpecularEnvmap(CameraPos, calculatedNormal, reflectionVector, specularColor, 0.15f);
	//return float4(se, 1.0f);

	float3 specular = fr.xxx * LightColour[0].xyz * LightPower;// *specularColor;

	float4 finalColor = float4(saturate(diffuse), 1);
	finalColor.rgb += saturate(specular);
	//finalColor.rgb = float3(0.0f, 0.0f, 0.0f);

	float normDotCam = max(dot(lerp(input.Normal, calculatedNormal, max(dot(input.Normal, V), 0)), V), 0);
	//float normDotCam = max(dot(calculatedNormal, V), 0);
	//float normDotCam = max(dot(lerp(calculatedNormal, calculatedNormal, max(dot(calculatedNormal, V), 0)), V), 0);
	float3 schlickFresnel = saturate(f0 + (float3(1.0f, 1.0f, 1.0f) - f0)*pow(1 - normDotCam, 5));
	//float3 schlickFresnel = F_Schlick(f0, f90, normDotCam);
	finalColor.rgb = lerp(finalColor.rgb, litColor.rgb, schlickFresnel);
	//finalColor.rgb = lerp(finalColor.rgb, se, schlickFresnel);


	//return float4(diffuse + specular, 1.0f);
	return float4(finalColor.rgb, 1.0f);

	//return float4(diffuse + specular, 1);



}

All help is appreciated as I am currently pretty stuck with it. The light in the scene is a pointlight and currently calculations are only made using one light (to simplify).

Advertisement

Don't have the time to look through all your code. From the images though it looks like you have only applied specular. Where is the basic diffuse ( dot(normal, light) )?. In using a cube map you would get the diffuse term by mip mapping the cube map and using the normal of the surface to look up into the cube map at the lowest mip (1x1 for each face). The downsampling gives you the average of light in a hemisphere, which is what diffuse is. The sum of all light that can hit the surface and bounce back.

finalColor.rgb = lerp(finalColor.rgb, litColor.rgb, schlickFresnel);

Your last line looks fishy. Fresnel is the amount of specular, and specular should actually get added on top of the diffuse.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

Don't have the time to look through all your code. From the images though it looks like you have only applied specular. Where is the basic diffuse ( dot(normal, light) )?

Yes the diffuse is barely there, but it is applied and from testing it doesn't seem to be completly black either (but very litte actual colour), if I remember correctly from when I tested things the fd part of the diffuse calculations are what's making it so small, so maybe I goofed up somewhere in the disney diffuse calculations? But I can't find anything wrong when I look at them or compare to the frostbite notes.

I think that would be my NdotL?

float NdotL = saturate(dot(calculatedNormal, LightVec));

In using a cube map you would get the diffuse term by mip mapping the cube map and using the normal of the surface to look up into the cube map at the lowest mip (1x1 for each face). The downsampling gives you the average of light in a hemisphere, which is what diffuse is. The sum of all light that can hit the surface and bounce back.

I think this is what I am doing, except maybe the mip map part. I take the vector from the camera to the position of the object, reflect it using the normal, and use that vector to sample from the cube map.

float3 incident = -V;
float3 reflectionVector = reflect(incident, calculatedNormal);
float3 reflectionColor = reflection.Sample(standardSamp, reflectionVector).rgb;

finalColor.rgb = lerp(finalColor.rgb, litColor.rgb, schlickFresnel);

Your last line looks fishy. Fresnel is the amount of specular, and specular should actually get added on top of the diffuse.

Yeah as I said I was just experimenting and testing some different things I found here and there to see what kind of result I would get and if I could get lucky and find something that works, so I'm not suprised that you find it fishy :) :P I'm as mentioned not completly sure how to use the value from the cube map in my final calculations.

I have tried inputing the same materials and models in another program (marmoset) to get some kind of reference of how it should look when it's done (though they might of course use more effects or different implementations than I do so I'm not expecting it to be exactly the same), pictures can be seen here.

Marmoset ship their shader source code :wink:

float3 diffuse = fd.xxx * LightColour[0].xyz * LightPower * diffuseColor;

fd.xxx ? you mean xyz?

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

I have tried inputing the same materials and models in another program (marmoset) to get some kind of reference of how it should look when it's done (though they might of course use more effects or different implementations than I do so I'm not expecting it to be exactly the same), pictures can be seen here.

Marmoset ship their shader source code :wink:

Found it, thats kinda cool. Might get some ideas from it, but it seems very split up so might be hard to find the relevant parts :P still worth a look at some point tho :) thanks for the tip.

float3 diffuse = fd.xxx * LightColour[0].xyz * LightPower * diffuseColor;

fd.xxx ? you mean xyz?

I think that's correct? fd is a single float, so if my understanding of things are right then that simply treats it as a float3 with the same value at each position? it worked without .xxx as well, as simply fd, but I thought it looked a bit nicer with fd.xxx (at the time at least, maybe it's just confusing).

Yea that is fine. I'd be curious to see what the output of NdotL is (I assume it is fine since your specular highlights seem to work). And then what fd.xxx output is. If you put a light source directly above a flat plane, that is textured half white, you should get a plane output that is half white + some specular depending on the angle.

From looking at that Disney function, it looks like you are using NdotL in some equation based on fresnel. Diffuse light doesn't change based on view, so I think that is where you should start looking.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

Yea that is fine. I'd be curious to see what the output of NdotL is (I assume it is fine since your specular highlights seem to work). And then what fd.xxx output is. If you put a light source directly above a flat plane, that is textured half white, you should get a plane output that is half white + some specular depending on the angle.

I guess I could make the shader just output the fd as colour and show that if it would help? Also, if i render a white plane as you said, what kind of metallic and roughness values should it have?

From looking at that Disney function, it looks like you are using NdotL in some equation based on fresnel. Diffuse light doesn't change based on view, so I think that is where you should start looking.

Can't say if it's correct or not, but after double checking with the source in the frostbite notes they seem to be doing the same thing. I won't lie and say that I understand everything that is done 100% so I am in no position to say if they are doing something wonky or just thinking in a different way :P

With a roughness of 1 and metallic of zero, it would be basically a white table tennis ball material. So it should come out pretty white.

So they are using that equation to make the energy of the light sum to 1. So whatever that disney diffuse outputs, if you put a direction light right above the center of a plane, it must return exactly the color of the texture or whatever surface color you have. So the reason everything you have is black is maybe due to some attenuation factor where you have a point light really far away and it isn't contributing to the surface??? Otherwise I dont know, but obviously a surface that directly faces a light source needs to be lit so start from there.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

With a roughness of 1 and metallic of zero, it would be basically a white table tennis ball material. So it should come out pretty white.

So they are using that equation to make the energy of the light sum to 1. So whatever that disney diffuse outputs, if you put a direction light right above the center of a plane, it must return exactly the color of the texture or whatever surface color you have. So the reason everything you have is black is maybe due to some attenuation factor where you have a point light really far away and it isn't contributing to the surface??? Otherwise I dont know, but obviously a surface that directly faces a light source needs to be lit so start from there.

So i did as you said and took a cube (it was easier than a plane since I have a cube obj to just load in :P ) and gave it a completly white texture for both base colour and roughness, and then a completly black texture for metallic.

First i used the point light (the sphere above the cube is a model bound to the light, it's colour and size got nothing to do with the light itself, it's just there so I know where the light is ^^) that I have used before and put the cube directly under it. The point light has a range of 100, the distance between the light and the cube should be approx 2, and the colour of the light is white (1, 1, 1). Here is the result

It doesn't look right even from the top in my opinion, and it seems like it gets more light as i look at it from a flat perspective (when the angle between my vector and the normal approaches 90 degrees) which is weird,also the whole thing seems to run black as I look from underneath it?

I then implemented a directional light by changing lightVec and light power to


	LightVec = float3(0.0f, 1.0f, 0.0f); // top down hardcoded directional light
	LightPower = 1.0f;

I have never used a directional light before but I think this should be a working directional light coming from top to bottom? The result can be seen here

Now this confuses me even more, the cube seems to behave like in the last case basically, but the sphere that represented the light seems to work somewhat ok, getting light on top of itself, but nothing on the bottom. I be very very confused :P

This topic is closed to new replies.

Advertisement