GGX image based lighting - mipmap artefacts

Started by
3 comments, last by L. Spiro 9 years, 2 months ago

Hi everyone,

i am currently implementing image based lighting according to the ue4 document

This is what i have got so far.

Final Image:
[attachment=26120:final.PNG]

Albedo:
[attachment=26105:albedo.PNG]

Roughness:
[attachment=26111:roughness.PNG]

Metallic:
[attachment=26110:metallic.PNG]

Specularcolor (Interpolated with the metallic value):
[attachment=26117:specular-color.PNG]

env brdf texture:
[attachment=26108:env-brdf.PNG]

ibl specular:
[attachment=26118:specular-IBL.PNG]

ibl sample color (without multiplying with env brdf):
[attachment=26119:specular-ibl-sample-color.PNG]

env brdf texture read with specularcolor:
[attachment=26109:env-brdf-read-from-texture.PNG]

diffuse ibl: (as of now i am using the last mip from the specular ibl cube)
[attachment=26107:diffuse-ibl.PNG]

Skybox miplevels
0
[attachment=26112:skybox-mip-0.PNG]

1
[attachment=26113:skybox-mip-1.PNG]

3
[attachment=26114:skybox-mip-3.PNG]

4
[attachment=26115:skybox-mip-4.PNG]

5
[attachment=26116:skybox-mip-5.PNG]

I am generating the specular skybox on the gpu. I take in the base cubemap and the current mip level i want to convole and save the results in another 6 2dtextures, from which i create a cubemap texture. The miplevels go down to 1x1 and starting from whatever the input texture size is.
Here is the shader code for that (taken from this post)


#version 130
uniform samplerCube cubeMap;
uniform vec2 pixelsize;

varying vec2 outUV;

uniform int currentMip;
uniform int maximumMips;
uniform float cubeSize;

/** Defines **/
#define M_PI 3.1415926535897932384626433832795
#define M_PI2 M_PI * 2
#define PI    3.14159265359
#define INVPI 0.31830988618
#define EPS   1e-5

float saturate (float x) {
	return clamp(x, 0.0, 1.0);
}

vec3 saturate (vec3 v) {
	return clamp(v, 0.0, 1.0);
}

float radicalInverse_VdC(uint bits) {
	 bits = (bits << 16u) | (bits >> 16u);
     bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
     bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
     bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
     bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
     return float(bits) * 2.3283064365386963e-10; // / 0x100000000
 }

 // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
 vec2 Hammersley(uint i, uint N) {
     return vec2(float(i)/float(N), radicalInverse_VdC(i));
 }

vec3 ImportanceSampleGGX( vec2 E, float Roughness, vec3 N ) {
	float m = Roughness * Roughness;

	float Phi = 2 * PI * E.x;
	float CosTheta = sqrt( (1 - E.y) / ( 1 + (m*m - 1) * E.y ) );
	float SinTheta = sqrt( 1 - CosTheta * CosTheta );

	vec3 H;
	H.x = SinTheta * cos( Phi );
	H.y = SinTheta * sin( Phi );
	H.z = CosTheta;

	vec3 UpVector = abs(N.z) < 0.999 ? vec3(0,0,1) : vec3(1,0,0);
	vec3 TangentX = normalize( cross( UpVector, N ) );
	vec3 TangentY = cross( N, TangentX );
	// tangent to world space
	return TangentX * H.x + TangentY * H.y + N * H.z;
}

 // Ignacio Castano via http://the-witness.net/news/2012/02/seamless-cube-map-filtering/
vec3 fix_cube_lookup_for_lod(vec3 v, float cube_size, float lod) {
	float M = max(max(abs(v.x), abs(v.y)), abs(v.z));
	float scale = 1 - exp2(lod) / cube_size;
	if (abs(v.x) != M) v.x *= scale;
	if (abs(v.y) != M) v.y *= scale;
	if (abs(v.z) != M) v.z *= scale;
	return v;
}
 
vec3 UvAndIndexToBoxCoord(vec2 uv, int face){
	vec3 n = vec3(0, 0, 0);
	vec3 t = vec3(0, 0, 0);
	 
	// posx (red)	
	if (face == 0) {
		n = vec3(1, 0, 0);
		t = vec3(0, 1, 0);
	}
	// negx (cyan)
	else if (face == 1) {
		n = vec3(-1, 0, 0);
		t = vec3(0, 1, 0);
	}
	// posy (green)
	else if (face == 2) {
		n = vec3(0, -1, 0);
		t = vec3(0, 0, -1);
	}
	// negy (magenta)
	else if (face == 3) {
		n = vec3(0, 1, 0);
		t = vec3(0, 0, 1);
	}
	// posz (blue)
	else if (face == 4) {
		n = vec3(0, 0, -1);
		t = vec3(0, 1, 0);
	}
	// negz (yellow)
	else {
		n = vec3(0, 0, 1);
		t = vec3(0, 1, 0);
	}
	 
	vec3 x = cross(n, t);
	 
	uv = uv * 2 - 1;
	 
	n = n + t * uv.y + x * uv.x;
	n.y *= -1;
	n.z *= -1;
	return n;
}

void main(){
	vec2 texel = gl_FragCoord.xy * pixelsize;
	float Roughness = float(currentMip) / float(maximumMips - 1);

	for(int f = 0; f < 6; f++){
		vec3 V = UvAndIndexToBoxCoord(outUV, f);
		vec3 N = fix_cube_lookup_for_lod(V, cubeSize, currentMip);

		vec4 totalRadiance = vec4(0, 0, 0, 0);
		uint maxSamples = 1024u;
		for(uint i = 0u; i < maxSamples; i++){
			vec2 Xi = Hammersley(i, maxSamples);
			vec3 H = ImportanceSampleGGX(Xi, Roughness, N);
		    vec3 L = 2 * dot(N, H) * H - N; 
    		float nDotL = saturate(dot(L, N));

    		if (nDotL > 0){
		        vec4 pointRadiance = textureCube(cubeMap, L, 0);
		        totalRadiance.rgb += pointRadiance.rgb * nDotL;
		        totalRadiance.w += nDotL;
		    }
		}
		gl_FragData[f] = vec4(totalRadiance.rgb / totalRadiance.w, 1);
	}

}


To generate the ENV texture i simple render a fullscreen quad and save the results in a 2d texture. here is the code.
(taken from here and the ue4 document)


#version 130
uniform vec2 pixelsize;

varying vec2 outUV;

uniform int currentMip;
uniform int maximumMips;
uniform float cubeSize;

/** Defines **/
#define M_PI 3.1415926535897932384626433832795
#define M_PI2 M_PI * 2
#define PI    3.14159265359
#define INVPI 0.31830988618
#define EPS   1e-5

float saturate (float x) {
	return clamp(x, 0.0, 1.0);
}

vec3 saturate (vec3 v) {
	return clamp(v, 0.0, 1.0);
}

float radicalInverse_VdC(uint bits) {
	 bits = (bits << 16u) | (bits >> 16u);
     bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
     bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
     bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
     bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
     return float(bits) * 2.3283064365386963e-10; // / 0x100000000
 }

 // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
 vec2 Hammersley(uint i, uint N) {
     return vec2(float(i)/float(N), radicalInverse_VdC(i));
 }

vec3 ImportanceSampleGGX( vec2 E, float Roughness, vec3 N ) {
	float m = Roughness * Roughness;

	float Phi = 2 * PI * E.x;
	float CosTheta = sqrt( (1 - E.y) / ( 1 + (m*m - 1) * E.y ) );
	float SinTheta = sqrt( 1 - CosTheta * CosTheta );

	vec3 H;
	H.x = SinTheta * cos( Phi );
	H.y = SinTheta * sin( Phi );
	H.z = CosTheta;

	vec3 UpVector = abs(N.z) < 0.999 ? vec3(0,0,1) : vec3(1,0,0);
	vec3 TangentX = normalize( cross( UpVector, N ) );
	vec3 TangentY = cross( N, TangentX );
	// tangent to world space
	return TangentX * H.x + TangentY * H.y + N * H.z;
}

// http://graphicrants.blogspot.com.au/2013/08/specular-brdf-reference.html
float GGX(float NdotV, float a){
	float k = a / 2;
	return NdotV / (NdotV * (1.0f - k) + k);
}

// http://graphicrants.blogspot.com.au/2013/08/specular-brdf-reference.html
float G_Smith(float a, float nDotV, float nDotL){
	return GGX(nDotL, a * a) * GGX(nDotV, a * a);
}

vec2 IntegrateBRDF(float Roughness, float NoV ){
	vec3 V;
	V.x = sqrt( 1.0f - NoV * NoV ); // sin
	V.y = 0;
	V.z = NoV;
	// cos
	float A = 0;
	float B = 0;

	uint NumSamples = 1024u;
	for( uint i = 0u; i < NumSamples; i++){
		vec2 Xi = Hammersley(i, NumSamples);
		vec3 H = ImportanceSampleGGX(Xi, Roughness, vec3(0, 0, 1));
		vec3 L = 2 * dot( V, H ) * H - V;
		float NoL = saturate(L.z);
		float NoH = saturate(H.z);
		float VoH = saturate(dot(V, H));
		if( NoL > 0 ){
			float G = G_Smith( Roughness, NoV, NoL );
			float G_Vis = G * VoH / (NoH * NoV);
			float Fc = pow( 1 - VoH, 5 );
			A += (1 - Fc) * G_Vis;
			B += Fc * G_Vis;
		}
	}
	return vec2( A, B ) / NumSamples;
}

void main(){
	vec2 texel = gl_FragCoord.xy * pixelsize;
	float nDotV = outUV.x;
	float Roughness = outUV.y;
 
	vec2 integral = IntegrateBRDF(Roughness, nDotV);
	gl_FragData[0] = vec4(integral.r, integral.g, 0, 1);
}


this is how i put everything together to render the "ibl specular"-texture:


#version 130

uniform vec2 pixelsize;
uniform sampler2D positionTex;		// Vertexpositions in ViewSpace
uniform sampler2D normalTex; 		// Normals in ViewSpace
uniform sampler2D roughnessTex; 	// Roughness
uniform sampler2D specularTex;		// Specularcolor & Specularintensity (Unused)

// Inverse Viewmatrix, to transform Positions & Normal back into Worldspace
uniform mat4 inverseViewMatrix;

uniform sampler2D envBRDFTexture;
uniform samplerCube skyboxCube;
uniform int maxMipCount;

uniform vec3 cameraPosition;

/** Defines **/
#define M_PI 3.1415926535897932384626433832795
#define M_PI2 M_PI * 2
#define PI    3.14159265359
#define INVPI 0.31830988618
#define EPS   1e-5

float saturate (float x) {
	return clamp(x, 0.0, 1.0);
}

vec3 saturate (vec3 v) {
	return clamp(v, 0.0, 1.0);
}

// taken from ue4
float ComputeCubemapMipFromRoughness( float Roughness, float MipCount ){
	// Level starting from 1x1 mip
	float Level = 3 - 1.15 * log2( Roughness );
	return MipCount - 1 - Level;
}

// taken from ue4
vec3 EnvBRDF( vec3 SpecularColor, float Roughness, float NoV ){
	vec2 AB =  texture2D(envBRDFTexture, vec2(NoV, Roughness), 0).rg;
	vec3 GF = SpecularColor * AB.x + saturate( 50.0 * SpecularColor.g ) * AB.y;
	//vec3 GF = SpecularColor * AB.x + AB.y;
	return GF;
}

void main(){
	vec2 texel = gl_FragCoord.xy * pixelsize;

	vec3 worldSpaceNormal = normalize(mat3(inverseViewMatrix) * texture2D(normalTex, texel).rgb);
	vec3 worldSpacePosition = vec3(inverseViewMatrix * texture2D(positionTex, texel));

	vec3 incidentVector = normalize(worldSpacePosition - cameraPosition);
    vec3 reflectedVector = normalize(reflect(incidentVector, worldSpaceNormal));

	float Roughness = texture2D(roughnessTex, texel).r;
	vec3 specularColor = texture2D(specularTex, texel).rgb;

    // a hack to skip the skybox
	if(worldSpaceNormal.x == 0 && worldSpaceNormal.y == 0 && worldSpaceNormal.z == 0){
	   	gl_FragData[0] = vec4(0); 	// Diffuse IBL
	   	gl_FragData[1] = vec4(0); 	// Specular IBL
	}else{
	   	// use the last mip as diffuselight
	   	gl_FragData[0] = vec4(textureCube(skyboxCube, worldSpaceNormal, float(maxMipCount)).rgb, 1);

		vec3 R = reflectedVector;
		float AbsoluteSpecularMip = ComputeCubemapMipFromRoughness(Roughness, float(maxMipCount ));
		vec3 SampleColor = textureCube(skyboxCube, R, AbsoluteSpecularMip).rgb;
		float NoV = saturate(dot(worldSpaceNormal, -incidentVector));
		vec3 result = SampleColor * EnvBRDF(specularColor, Roughness, NoV);

		//http://marmosetco.tumblr.com/post/81245981087
		float horizonOcclusion = 1.3;
		float horizon = saturate( 1 + horizonOcclusion * dot(R, worldSpaceNormal));
		horizon *= horizon;
		
		gl_FragData[1] = vec4(horizon * result, 1);
	}
}


the final image gets generated like this:


#version 120
uniform sampler2D texture;	// albedo
uniform sampler2D lightpassDiffuse;	// lightpass
uniform sampler2D lightpassSpecular;
uniform sampler2D ssaotex;
uniform sampler2D iblDiffuse;
uniform sampler2D iblSpecular;

uniform vec2 pixelsize;
uniform vec3 ambientLight;

varying vec2 outUV;

void main(){
  vec2 texel = gl_FragCoord.xy * pixelsize;

  vec3 albedo = texture2D(texture, texel).rgb;
  vec3 diffuseLight = texture2D(lightpassDiffuse, texel).rgb;
  vec4 specularTexture = texture2D(lightpassSpecular, texel);
  vec3 specularLight = specularTexture.rgb;

  float ssao = texture2D(ssaotex, texel).r;
  vec3 iblDiffuseLight = texture2D(iblDiffuse, texel).rgb;
  vec3 iblSpecularLight = texture2D(iblSpecular, texel).rgb;
  vec3 finalColor =  ssao * (albedo * (iblDiffuseLight + diffuseLight) + specularLight + iblSpecularLight);

   gl_FragData[0] = vec4(finalColor, 1);
}

my question is:
1. what am i doing wrong? because the final image looks like crap
2. am i creating the cubemap correctly?
3. why are there white spots on the specular- / diffuse-ibl texture? (they do not only appear on normalmapped materials)
4. also there is a huge gap between the transition of roughness = 0, to roughness = 0.03 for example. why is that? (see ibl sample color-texture) am i retrieving the miplevel from the roughness correctly?

ps:
1. i have not done any conversion to linearspace or gammspace. i want to fix this before i move on
2. as of now the input image for the cubemap is not HDR.

edit:

i now use hdr input textures.

here are more screenshots to illustrate 3.)

diffuse ibl

[attachment=26127:ibl-diffuse-normalmapped.PNG]

specular ibl

[attachment=26128:ibl-specular-normalmapped.PNG]

diffuse ibl

[attachment=26126:diffuse-ibl-error.PNG]

specular ibl

[attachment=26129:specular-ibl-error.PNG]

edit2:

after capping the miplevels with


glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, maximumMipLevel - 1);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LOD, maximumMipLevel - 1);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, maximumMipLevel - 1);

the artefacts disappear from the cubemap. the diffuse looks nice now, but i obviously can't access the mips for the specular. any idea why this artefacts occur and how to fix that? if i dont cap the miplevels and move closer to an object, the diffuse ibl starts gets all weired.

[attachment=26130:artefact1.PNG]

[attachment=26131:artefact2.PNG]

Advertisement

You posted a bunch of random stuff and basically said "nothing I have works". Which is why I can't spend an hour decoding what is wrong. These issue have nothing to do with image based lighting really, just regular graphics/shaders.

Mip-Mapping: Have you tried manually sending mip map level , 0,1,2,3,4,5,6, etc instead of computing them as is just to make sure your mip-maps look ok? If so then a simple mathmateical linear scale factor of roughness in range 0 to 1, multiplied by the number of mip levels should work. If your skybox is your reflection map then the jump from level 3 to 4 obviously looks wrong.

White spots:? Debug your shaders. I'm sure you can figure out where they are coming from.

Specular: Your specular textures have a strange edge effect (though I didn't see a specific comment on this.) I assume that is the reflection amount calculated using a fresnel type shader? That specular output should be "reflectance" from what I can tell.

Throwing 10 images and all your shaders and saying "why does everything look bad", is not the way to solve a problem. If this is supposed to be the standard left to right roughness/reflection and you are expecting shiny on the left and rough on the right, then it shouldn't be hard to pick apart these steps one by one and solve each problem 1 by 1.

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

i dont think i just posted "random stuff". i tried to provide as much information as i could and asked some questions, which i hoped someone could answer given the code and screenshots i provided.

"Have you tried manually sending mip map level" see my edit2.

"Your specular textures have a strange edge effect" thats why i posted all my shader code....

From edit2, it appears you forced the hardware to select the same mip level. I meant in the shader selectively choosing 0,1,2,3,4,5,6,7 and displaying those on your sphere to see if that portion looks correct.

thats why i posted all my shader code....

This is why this is a problem. When you have to post all of your code instead of maybe a few lines that calculate something, you have approached problem solving in programming a bad way. If you can't narrow the problem down then you should take the time to do so. If you want to fix your specular problem, Then separate or comment all other code out and solve that problem. If you can't figure that out, then post that code. For me personally, I don't know about others, but I don't have more than a few minutes to look at people's problems here and respond. I can't read all of your code and decipher all of your images. And that may be why nobody else has responded as well. You need a smaller more precise problem that you can arrive at. Code is iterative and shaders are no different. It builds up over time, so I'm confused how you wrote that many shader lines and then said it doesn't work, instead of putting in the roughness alone and testing, specular and testing.

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

i tried to provide as much information as i could and asked some questions, which i hoped someone could answer given the code and screenshots i provided.

People didn’t reply mainly because of this. You overwhelmed everyone.
The point in providing information with your question isn’t just to blindly maximize the amount of information you dump on us, it is to minimize the amount of directly relevant information.

Not only did I personally not feel the energy (or find the time) to go through that haystack looking for a needle, I didn’t even reply to point out some basic/obvious things because I didn’t want to feel pressured to reply to the rest after having publicly shown that I have read the topic, etc.

Here are the few things for which I have energy:


Skybox miplevels

Look at the transitions between each level. From 0 to 1 and 1 to 2 there is almost no change. From 2 to 3 looks mostly correct, and then suddenly from 3 to 4 it gets crazy-blurred.

It is very safe to say your mipmaps are wrong. How did you generate them?

That isn’t your main bug, but something you will have to fix eventually.


"Have you tried manually sending mip map level" see my edit2.

Feel free to take advantage of the built-in quoting system, which further helps those of us who are used to the site in helping you.

And as for your reply, textureCubeLod().

L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

This topic is closed to new replies.

Advertisement