How to Implement PBR shader - LWJGL?

1.9k views Asked by At

I've been following Thinmatrix tutorials to practice GLSL and Vector math, and now I'm trying to implement a PBR shader, that I borrowed from here(learnOpenGL). My first go made sure the models would not render. My old lambert shader works fine, but implementing the shader has been tricky. Otherwise I'm gonna have to port the c++ code block by block to java, which while I want to try over a period of time, I would like to implement a PBR Shader in my existing engine. I believe I've initiated the attributes:

@Override
    protected void bindAttributes() {
        super.bindAttribute(0, "aPos");
        super.bindAttribute(1, "aTexCoords");
        super.bindAttribute(2, "aNormal");
    }

but unsure how I should add the uniforms, at this point I'm taking a stab at it in the dark. I've noticed that the shader has a mat4 uniform view, mat4 uniform camPos and mat4 uniform model which I don't have binded in LWJGL (excuse my poor terminology). Currently my uniforms are:

@Override
protected void getAllUniformLocations() {
    location_transformationMatrix = super.getUniformLocation("transformationMatrix");
    location_projectionMatrix = super.getUniformLocation("projection");
    location_viewMatrix = super.getUniformLocation("view");
    location_shineDamper = super.getUniformLocation("shineDamper");
    location_reflectivity = super.getUniformLocation("reflectivity");
    location_useFakeLighting = super.getUniformLocation("useFakeLighting");
    location_skyColour = super.getUniformLocation("skyColour");
    location_numberOfRows = super.getUniformLocation("numberOfRows");
    location_offset = super.getUniformLocation("offset");
    location_lightPosition = new int[MAX_LIGHTS];
    location_lightColour = new int[MAX_LIGHTS];
    location_attenuation = new int [MAX_LIGHTS];
    for(int i=0;i<MAX_LIGHTS;i++) {
        location_lightPosition[i] = super.getUniformLocation("lightPositions[" + i + "]");
        location_lightColour[i] = super.getUniformLocation("lightColors[" + i + "]");
}

Vertex shader from learnOpengl:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;
layout (location = 2) in vec3 aNormal;

out vec2 TexCoords;
out vec3 WorldPos;
out vec3 Normal;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
    TexCoords = aTexCoords;
    WorldPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(model) * aNormal;   

    gl_Position =  projection * view * vec4(WorldPos, 1.0);
}

Fragment shader from learnopengl:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

// material parameters
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

uniform vec3 camPos;

const float PI = 3.14159265359;
// ----------------------------------------------------------------------------
// Easy trick to get tangent-normals to world-space to keep PBR code simplified.
// Don't worry if you don't get what's going on; you generally want to do normal 
// mapping the usual way for performance anways; I do plan make a note of this 
// technique somewhere later in the normal mapping tutorial.
vec3 getNormalFromMap()
{
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(WorldPos);
    vec3 Q2  = dFdy(WorldPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1*st2.t - Q2*st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}
// ----------------------------------------------------------------------------
float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a = roughness*roughness;
    float a2 = a*a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;

    float nom   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}
// ----------------------------------------------------------------------------
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// ----------------------------------------------------------------------------
void main()
{       
    vec3 albedo     = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    float metallic  = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao        = texture(aoMap, TexCoords).r;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(camPos - WorldPos);

    // calculate reflectance at normal incidence; if dia-electric (like plastic) use F0 
    // of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)    
    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);

    // reflectance equation
    vec3 Lo = vec3(0.0);
    for(int i = 0; i < 4; ++i) 
    {
        // calculate per-light radiance
        vec3 L = normalize(lightPositions[i] - WorldPos);
        vec3 H = normalize(V + L);
        float distance = length(lightPositions[i] - WorldPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lightColors[i] * attenuation;

        // Cook-Torrance BRDF
        float NDF = DistributionGGX(N, H, roughness);   
        float G   = GeometrySmith(N, V, L, roughness);      
        vec3 F    = fresnelSchlick(max(dot(H, V), 0.0), F0);

        vec3 nominator    = NDF * G * F; 
        float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; // 0.001 to prevent divide by zero.
        vec3 specular = nominator / denominator;

        // kS is equal to Fresnel
        vec3 kS = F;
        // for energy conservation, the diffuse and specular light can't
        // be above 1.0 (unless the surface emits light); to preserve this
        // relationship the diffuse component (kD) should equal 1.0 - kS.
        vec3 kD = vec3(1.0) - kS;
        // multiply kD by the inverse metalness such that only non-metals 
        // have diffuse lighting, or a linear blend if partly metal (pure metals
        // have no diffuse light).
        kD *= 1.0 - metallic;     

        // scale light by NdotL
        float NdotL = max(dot(N, L), 0.0);        

        // add to outgoing radiance Lo
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;  // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again
    }   

    // ambient lighting (note that the next IBL tutorial will replace 
    // this ambient lighting with environment lighting).
    vec3 ambient = vec3(0.03) * albedo * ao;

    vec3 color = ambient + Lo;

    // HDR tonemapping
    color = color / (color + vec3(1.0));
    // gamma correct
    color = pow(color, vec3(1.0/2.2)); 

    FragColor = vec4(color, 1.0);
}

I noticed that their are uniforms I'm not binding in lwjgl, some I'm unsure how to initiate or why it needs to be done (uniform mat4 model). I thought I could leave the uniforms from the thinmatrix tutorial and assumed that it wouldn't process. Please correct me if I'm wrong.

Any insight to the process behind making this will be much appreciated. I've been mostly watching talks and shadertoy based videos dealing with PBR.

heres my code based on thinmatrix tutorials: here

UPDATE: This is close, but now I'm trying out the textures and the albedo appears very dark and the others are indistinguishable. I also don't know what I'm supposed to do with the camPos uniform, or if its even neccesary.

enter image description here

lastest source code: here

update:

So the albedo seems to work but the ao, roughness and metallic seem to not want to bind. And its very dark.

In the renderer class that is supposed to help bind the textures.

 //Activate texture albedo texture 0
  GL13.glActiveTexture(GL13.GL_TEXTURE0);
  GL11.glBindTexture(GL11.GL_TEXTURE_2D, model.getAlbedo().getID());

  //Activate texture roughness texture 1
  GL13.glActiveTexture(GL13.GL_TEXTURE1);
  GL11.glBindTexture(GL11.GL_TEXTURE_2D, model.getRoughness().getID());

  //Activate texture metallic texture 2
  GL13.glActiveTexture(GL13.GL_TEXTURE2);
  GL11.glBindTexture(GL11.GL_TEXTURE_2D, model.getMetallic().getID());

  //Activate texture ao texture 0
  GL13.glActiveTexture(GL13.GL_TEXTURE3);
  GL11.glBindTexture(GL11.GL_TEXTURE_2D, model.getAo().getID());

enter image description here

UPDATE:

enter image description here

At the moment I've got the textures working. I feel like I can figure out the rest now. My only question would be: whats up with this huge texture seam? Is it because the texture maps im using are not seamless? I'm assuming it would go away if I make the normal, ao, rough and metallic seamless. The albedo seems seamless already, or perhaps not. Im being a bit nitpicky but just curious if that would help make it look better.

enter image description here

0

There are 0 answers