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]