Sign in to follow this  
alkisbkn

PBR (blurry reflections)

Recommended Posts

Hello,

 

so I am writing a PBR shader to experiment with the latest techniques, and I find the metalness, roughness and how they relate the the blurry reflections a bit confusing. I understand that the rougher the surface, the more blurry the cubemap will be. For my tests, I have a cubemap generated using CubemapGen, which I sample like this to get the blurred texel:

half current = roughness * 6;
half f0 = floor(current);
half f1 = ceil(current);
				
half f = saturate(current - f0) / (f1-f0);
				
half3 reflectDir = reflect(V, N_WS);
	
half4 reflection1 = texCUBElod (_IndirectLightTex, float4(reflectDir, f0));
half4 reflection2 = texCUBElod (_IndirectLightTex, float4(reflectDir, f1));
half4 reflection = lerp(reflection1, reflection2, f);

Where does metalness come in though? As I understand from the articles that I've been reading, the more metallic a surface, the more reflection it has (and less albedo). Is it as simple as this?

half3 totalReflection = metalness * reflection;

As a clarification, my roughness comes from the alpha channel of the main texture, and the metalness comes from the vertex color.

 

Thanks!

Share this post


Link to post
Share on other sites

As I understand from the articles that I've been reading, the more metallic a surface, the more reflection it has (and less albedo). Is it as simple as this?

Basically, yes. But there's more to it. There are other properties like the reflection color, shape, surface smoothness, etc.

PBR and BRDF is a huge topic, with many different models (Phong-model is a very basic BRDF).

 

What article are you reading? What are you trying to achieve?

I don't see any PBR in the code you posted. The shader does environment mapping with a very crude constant specular attenuation function. Unless the physics is baked into the env-map, this is not a real physical model.

Edited by N.I.B.

Share this post


Link to post
Share on other sites

Hi NIB, thanks for your reply.

 

So for our mobile project, we want to do a hybrid technique as follows:

- we have our albedo and we multiply it with a lightmap authored in maya, so the ship diffuse lighting is basically static

- then we add the specular component on top of that, taking *some* PBR ideas into the equation (like energy conservation, metalness and roughness), to mimic different surface types. We have plastic, metal, brushed metal, and so on.

 

The reason for doing this came up after multiple approaches to he problem (artists also wanted to try the PBR tech), seems to give the best result. Although of course this not exactly PBR, my initial post was misleading.

The reflection will also use a blurry reflection model on higher end devices (based on the surface's roughness).

 

The main article that I've been studying is the Remember Me tech article about their PBR tech, and also Lagarde's articles on his blog regarding Fresnel and reflectance.

Share this post


Link to post
Share on other sites

You can also check http://blog.selfshadow.com/publications/s2012-shading-course/gotanda/s2012_pbs_beyond_blinn_slides_v3.pdf but if you know Lagarde maybe you already know Gotanda as well.

 

You might want to modulate the energy that you read from your lightmap. Empirically : no specular = maximum diffuse power. high specular = high energy concentration in the reflection therefore no more energy for the diffuse part. You need somekind of balance so that the albedo is always inferior to 1.

Also you cannot state "the more reflection the less albedo". albedo is the integral of the radiance over the hemisphere, and to be physically correct, it cannot be greater than the irradiance. So one would expect rather "the more reflection the same the albedo". This would mean exactly what I said, the diffuse part must get darker in order for the albedo to not vary. If you had the same diffuse component, then "the more reflection the more the albedo", because the specular zone would augment the interal and make the total emnitted power (the albedo), higher.

I hope that's clear, it might be easier to hear than to read.

Share this post


Link to post
Share on other sites

It makes sense in a way, yes. I am just trying to get my head around this concept, I've been reading the article you sent me.

 

How I do my energy conservation:

half diffuseScale = 1 - specular;
diffuse *= diffuseScale;

// calculate final color
half4 color = diffuse + specular + reflection*metalness;

The specular is calculated in a similar fashion as in Remember Me article. I will carry on on Monday, perhaps do some more tests during the weekend.

Share this post


Link to post
Share on other sites

half-precision (or even fixed) floats are generally faster than 32bit floats, especially on mobile hardware. I'm just trying to squeeze out every performance that the powervr chips have to offer :)

Share this post


Link to post
Share on other sites

Right, so I restructured my shader to something which seems to be correct.

Basically I have 2 material modes, metallic and non metallic (as UE4 does), and if I want anything in between, I interpolate. The shader code as it stands now looks like this:


half3 reflection = texCUBEbias (_ReflectionTex, float4(i.reflectDir, (1-roughness)*6));
				
half specPower = exp2(10 * roughness + Log2Of1OnLn2_Plus1);
				
half3 light = DecodeLightmap(lightmap);
half3 fresnel = schlick(NdotV, metalness);

half3 specular =  (LN2DIV8 * specPower + 0.25) * SphericalGaussianApprox(HdotN, specPower) * fresnel;
				
half3 ec = saturate(1 - (specular + reflection));
				
half3 plastic = albedo.rgb * light * ec + specular;
half3 metallic = (specular*albedo + reflection * albedo.rgb) * Luminance(light);

return half4(lerp(plastic, metallic, metalness), 1);

I am also using Lagarde's spherical gaussian approximation for the specular component which saves me quite a bit with negligible quality loss. Thumbs up for that!

 

So basically if it's metal, I ignore the albedo and simply have specular + reflection, multiplied by the lightmap.

If it's plastic, I use the albedo times lightmap, with added specular.

 

The specular colour depends on the metalness. For metals, it's coloured (using the albedo), for non-metals it's constant white.

The result looks like this (all the spheres have an albedo of "gold", but metalness and roughness varies diagonally, specular on the plastic balls is there, but it's hidden by the geometry at times)

 

2j2ddp2.png

 

The reflection cubemap is not prefiltered properly, so that's my next step!

 

Total fragment shader cost is about 27 instructions, with room for more optimisation.

Share this post


Link to post
Share on other sites

Here the metallic part :

SURFACE_DATA SurfaceData;
float4 DiffuseData = DiffuseMap.Sample( LinearSampler, Input.TexCoord );
float4 FinalBaseColor = BaseColor * DiffuseData;
SurfaceData.DiffuseColor = FinalBaseColor.rgb - (FinalBaseColor.rgb * Metallic);
SurfaceData.SpecularColor = lerp( 0.08f * Specular, FinalBaseColor.rgb, Metallic );

Here the compute spec factor function used in lighting :

float3 ComputeSpecFactor( in SURFACE_DATA SurfaceData, in float3 LightDirection, in float NdotL )
{
  // Variables used to compute the lighting factor.
  float3 ViewDirection = normalize( -SurfaceData.Position );
  float3 HalfDirection = normalize( LightDirection + ViewDirection );
  
  // Compute the lighting factors.
  float NdotH = max( 0.0f, dot( SurfaceData.Normal, HalfDirection ) );
  float NdotV = max( 0.0f, dot( SurfaceData.Normal, ViewDirection ) );
  float VdotH = max( 0.0f, dot( ViewDirection, HalfDirection ) );
  
  // Generalized microfacet specular.
  float D = D_GGX( Roughness, NdotH );
  float G = G_Schlick( Roughness, NdotV, NdotL );
  float3 F = F_Schlick( SurfaceData.SpecularColor, VdotH );
  
  // Return the specular factor.
  return D * G * F;
}

About the reflection, did you tried the importance sampling like in the paper of Brian Karis ?

One thing is when using metallic to 1.0f you have black diffuse color and only specular is visible.

Here the filter of envmap from unreal paper :

float3 PrefilterEnvMap( float Roughness, float3 R )
{
  float3 N = R;
  float3 V = R;
  float3 PrefilteredColor = 0;
  const uint NumSamples = 1024;
  for( uint i = 0; i < NumSamples; i++ )
  {
    float2 Xi = Hammersley( i, NumSamples );
    float3 H = ImportanceSampleGGX( Xi, Roughness, N );
    float3 L = 2 * dot( V, H ) * H - V;
    float NoL = saturate( dot( N, L ) );
    if( NoL > 0 )
    {
      PrefilteredColor += EnvMap.SampleLevel( EnvMapSampler , L, 0 ).rgb * NoL;
      TotalWeight += NoL;
    }
  }
  return PrefilteredColor / TotalWeight;
}

Here how to combine all that to have the specular value (from unreal paper) :

float3 ApproximateSpecularIBL( float3 SpecularColor , float Roughness, float3 N, float3 V )
{
  float NoV = saturate( dot( N, V ) );
  float3 R = 2 * dot( V, N ) * N - V;
  float3 PrefilteredColor = PrefilterEnvMap( Roughness, R );
  float2 EnvBRDF = IntegrateBRDF( Roughness, NoV );
  return PrefilteredColor * ( SpecularColor * EnvBRDF.x + EnvBRDF.y );
}
Edited by Alundra

Share this post


Link to post
Share on other sites

Oh wow, thanks for this! I see I've done a couple of mistakes regarding the diffuse component.

 

I prefilter my cubemaps using Lagarde's version of AmdCubemapGen, gives me a nice result (not visible in that screenshot), so I don't do any runtime prefiltering.

Share this post


Link to post
Share on other sites

half current = roughness * 6;
half f0 = floor(current);
half f1 = ceil(current);
half f = saturate(current - f0) / (f1-f0);
//...
half4 reflection1 = texCUBElod (_IndirectLightTex, float4(reflectDir, f0));
half4 reflection2 = texCUBElod (_IndirectLightTex, float4(reflectDir, f1));
half4 reflection = lerp(reflection1, reflection2, f);

Won't this break when current is an integer? What is wrong with enabling mipmap interpolation and using:
half current = roughness * 6;
half4 reflection = texCUBElod (_IndirectLightTex, float4(reflectDir, current));
On mobile this might even be faster.

Share this post


Link to post
Share on other sites
I prefilter my cubemaps using Lagarde's version of AmdCubemapGen, gives me a nice result (not visible in that screenshot), so I don't do any runtime prefiltering.

Is it possible to have more info about your work on this part ? Do you convert the roughness to specular power to filter the cubemap ?

Why do you add the reflection on the diffuse and the specular and not only add the reflection at the end ?

(BaseColor * LightColor + Specular) + (ReflectionFiltered * Metallic)
Edited by Alundra

Share this post


Link to post
Share on other sites

I use the modified AmdCubemapGen from here, which converts gloss to specular power.

The reflection and specular are added like this at the moment:

return half4 ( ( albedo.rgb - albedo.rgb*metalness) * light.rgb + specular + reflection * F0, 1);
Edited by alkisbkn

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this