Substance Painter Shader Guide part 1. – Adding to custom Sun light.

Summary.

No matter how well the game engine and substance painter’s tone mapping structure is matched, the art team puts the result in the game scene where sunlight is finally present.

Substance Painter takes irradiance (flux density) through important sampling from IBL Cube, which contains information about diffuse and specular reflections.
Unfortunately, no directional light (solar light) is implemented.
In fact, I cannot say that this is wrong.
This is because it is no problem to set the game engine’s sunlight to 0 during the calibration process, and perform an initial LookDev such as correcting the color difference with just an IBL light source and checking the result of applying tone mapping.

In recent years, some places use HDR Sky that was created procedurally, and in case of UE4, I also frequently used HDR Capture of all Light Sources after setting the ambient light and applying them as IBL Cubes in Subston Painter or Designer.
Actually, I have a project that worked in the same way as above when I worked for a Chinese game company in 2016.

//- Allegorithmic Metal/Rough PBR shader
//- Modifyied by JP.Lee
//- ====================================
//-
//- Import from libraries.
import lib-sss.glsl
import lib-pbr.glsl
import lib-emissive.glsl
import lib-pom.glsl
import lib-utils.glsl

//- Declare the iray mdl material to use with this shader.
//: metadata {
//:   "mdl":"mdl::alg::materials::skin_metallic_roughness::skin_metallic_roughness"
//: }

//- Channels needed for metal/rough workflow are bound here.
//: param auto channel_basecolor
uniform SamplerSparse basecolor_tex;
//: param auto channel_roughness
uniform SamplerSparse roughness_tex;
//: param auto channel_metallic
uniform SamplerSparse metallic_tex;
//: param auto channel_specularlevel
uniform SamplerSparse specularlevel_tex;

//-------- Lights ----------------------------------------------------//
// Directional Sun Settings
//: param custom { "default": false, "label": "Sun & Sky" }
uniform bool sun;

//: param custom { "default": 1.5, "label": "Sun Strenght", "min": 0.5, "max": 3.0 }
uniform float sun_strenght;

//: param custom { "default": [90.0, 90.0, 12.54], "label": "Sun Direction", "min": -180, "max": 180 }
uniform vec3 light_pos;

vec3 lightVector;

vec3 microfacets_brdf(
  vec3 Nn,
  vec3 Ln,
  vec3 Vn,
  vec3 Ks,
  float Roughness)
{
  vec3 Hn = normalize(Vn + Ln);
  float vdh = max( 0.0, dot(Vn, Hn) );
  float ndh = max( 0.0, dot(Nn, Hn) );
  float ndl = max( 0.0, dot(Nn, Ln) );
  float ndv = max( 0.0, dot(Nn, Vn) );
  return fresnel(vdh,Ks) *
    ( normal_distrib(ndh,Roughness) * visibility(ndl,ndv,Roughness) / 4.0 );
}

vec3 diffuse_brdf(
  vec3 Nn,
  vec3 Ln,
  vec3 Vn,
  vec3 Kd)
{
  return Kd * M_INV_PI;
}


vec3 pointLightContribution(
  vec3 fixedNormalWS,
  vec3 pointToLightDirWS,
  vec3 pointToCameraDirWS,
  vec3 diffColor,
  vec3 specColor,
  float roughness,
  vec3 LampColor)
{
  return  max(dot(fixedNormalWS,pointToLightDirWS), 0.0) * ( (
    diffuse_brdf(
      fixedNormalWS,
      pointToLightDirWS,
      pointToCameraDirWS,
      diffColor*(vec3(1.0,1.0,1.0)-specColor) * M_INV_PI)
    + microfacets_brdf(
      fixedNormalWS, 
      pointToLightDirWS, 
      pointToCameraDirWS, 
      specColor, 
      roughness)) * LampColor * M_PI);
}


//- Shader entry point.
void shade(V2F inputs)
{
  // Apply parallax occlusion mapping if possible
  vec3 viewTS = worldSpaceToTangentSpace(getEyeVec(inputs.position), inputs);
  applyParallaxOffset(inputs, viewTS);

  vec3 normal_vec = computeWSNormal(inputs.tex_coord, inputs.tangent, inputs.bitangent, inputs.normal);
  lightVector = light_pos.xyz - inputs.position.xyz;
  vec3 eye_vec = is_perspective ? normalize(camera_pos - inputs.position) :  -camera_dir;

  
  // Fetch material parameters, and conversion to the specular/roughness model
  float roughness = getRoughness(roughness_tex, inputs.sparse_coord);
  vec3 baseColor = getBaseColor(basecolor_tex, inputs.sparse_coord);
  float metallic = getMetallic(metallic_tex, inputs.sparse_coord);
  float specularLevel = getSpecularLevel(specularlevel_tex, inputs.sparse_coord);
  vec3 diffColor = generateDiffuseColor(baseColor, metallic);
  vec3 specColor = generateSpecularColor(specularLevel, baseColor, metallic);

  vec3 sunIrradiance = vec3(1.0);
  vec3 sun_vec = normalize(lightVector);
  vec3 sunContrib = pointLightContribution(normal_vec, sun_vec, eye_vec, diffColor, specColor, roughness, sunIrradiance * sun_strenght);

  if(sun)
  {
    diffColor +=sunContrib;
  }
  
  // Get detail (ambient occlusion) and global (shadow) occlusion factors
  float occlusion = getAO(inputs.sparse_coord) * getShadowFactor();
  float specOcclusion = specularOcclusionCorrection(occlusion, metallic, roughness);

  LocalVectors vectors = computeLocalFrame(inputs);

  // Feed parameters for a physically based BRDF integration
  emissiveColorOutput(pbrComputeEmissive(emissive_tex, inputs.sparse_coord));
  albedoOutput(diffColor);
  diffuseShadingOutput(occlusion * envIrradiance(vectors.normal));
  specularShadingOutput(specOcclusion * pbrComputeSpecular(vectors, specColor, roughness));
  sssCoefficientsOutput(getSSSCoefficients(inputs.sparse_coord));
}

First, let’s look at the entire shader.
You can see that the default Pbr metal rough.glsl file provided by Substance Painter has been modified.
The Substance Painter also provides very basic parts of the API document, so it is difficult to know each one while analyzing other shaders.

For example that …

//: param custom { "default": false, "label": "Sun & Sky" }
uniform bool sun;

If you have code like this, you can understand that //: is the part that connects to the substance painter interface.
Also, if a uniform variable is declared with a single line, it should be thought that it is a uniform variable that works in pairs with the interface connection directly above.

That is, the top is a property and the bottom is an external variable declaration.
Uniquely, you don’t need to define a variable name in a property, it will always work in pairs with the declared variable just below the property.

If you set the param custom variable initialization to false, this means that the pair of variables to be declared must be of type to bool.

//: param custom { "default": 1.5, "label": "Sun Strenght", "min": 0.5, "max": 3.0 }
uniform float sun_strenght;

For example, if the property variable is initialized as default: 1.5 as above, of course, we must know at once that we need to declare the external variable as a float type.

This image has an empty alt attribute; its file name is image-52.png
added sun lighting

Now we will talk about how we added sunlight.
I declare one bool data type because there are times when sunlight is used and sometimes not.
It was called sun.

//-------- Lights ----------------------------------------------------//
// Directional Sun Settings
//: param custom { "default": false, "label": "Sun & Sky" }
uniform bool sun;

The brightness of sunlight is required.

//: param custom { "default": 1.5, "label": "Sun Strenght", "min": 0.5, "max": 3.0 }
uniform float sun_strenght;

Add a float data type and name it sun_strength.
Please remember the description at the top of this post as the properties section is also important.

You need to add a calculation for Roughness.

vec3 microfacets_brdf(
  vec3 Nn,
  vec3 Ln,
  vec3 Vn,
  vec3 Ks,
  float Roughness)
{
  vec3 Hn = normalize(Vn + Ln);
  float vdh = max( 0.0, dot(Vn, Hn) );
  float ndh = max( 0.0, dot(Nn, Hn) );
  float ndl = max( 0.0, dot(Nn, Ln) );
  float ndv = max( 0.0, dot(Nn, Vn) );
  return fresnel(vdh,Ks) *
    ( normal_distrib(ndh,Roughness) * visibility(ndl,ndv,Roughness) / 4.0 );
}

The complicated explanation of the calculation formula is omitted.
If you search Google, there are too many resources, so it is recommended to browse.
Let’s first refer to the documentation provided by Allegorithmic.

file:///C:/Program%20Files/Allegorithmic/Substance%20Painter/resources/shader-doc/lib-pbr.html

Distribution Function (Cook_torrance) provided by Allegorithmic
If you are familiar with graphics, you may have seen some terms like BSDF, BRDF, BTDF. I’m not sure what it is, but you can see that the DF header is the same. The initials for “Distribution Function” in the title are “DF”. “Distribution Function” can be expressed as “distribution function” in Korean.
We can see the term BRDF in many 3D authoring tools.

You can’t use this function directly.

Since you need to use Vec3 type data, you need to add and apply the modified function yourself.

Add it by referring to Substance Designer Shader Lib.

vec3 microfacets_brdf(
	vec3 Nn,
	vec3 Ln,
	vec3 Vn,
	vec3 Ks,
	float Roughness)
{
	vec3 Hn = normalize(Vn + Ln);
	float vdh = max( 0.0, dot(Vn, Hn) );
	float ndh = max( 0.0, dot(Nn, Hn) );
	float ndl = max( 0.0, dot(Nn, Ln) );
	float ndv = max( 0.0, dot(Nn, Vn) );
	return fresnel(vdh,Ks) *
		( normal_distrib(ndh,Roughness) * visibility(ndl,ndv,Roughness) / 4.0 );
}

Substance Designer rendering provides Point Light, see Code in this section.

vec3 pointLightContribution(
	vec3 fixedNormalWS,
	vec3 pointToLightDirWS,
	vec3 pointToCameraDirWS,
	vec3 diffColor,
	vec3 specColor,
	float roughness,
	vec3 LampColor,
	float LampIntensity,
	float LampDist)
{
	// Note that the lamp intensity is using ˝computer games units" i.e. it needs
	// to be multiplied by M_PI.
	// Cf https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/

	return  max(dot(fixedNormalWS,pointToLightDirWS), 0.0) * ( (
		diffuse_brdf(
			fixedNormalWS,
			pointToLightDirWS,
			pointToCameraDirWS,
			diffColor*(vec3(1.0,1.0,1.0)-specColor))
		+ microfacets_brdf(
			fixedNormalWS,
			pointToLightDirWS,
			pointToCameraDirWS,
			specColor,
			roughness) ) *LampColor*(lampAttenuation(LampDist)*LampIntensity*M_PI) );
}

The code above implements Point Light. We only need to implement directional light.

vec3 pointLightContribution(
  vec3 fixedNormalWS,
  vec3 pointToLightDirWS,
  vec3 pointToCameraDirWS,
  vec3 diffColor,
  vec3 specColor,
  float roughness,
  vec3 LampColor)
{
  return  max(dot(fixedNormalWS,pointToLightDirWS), 0.0) * ( (
    diffuse_brdf(
      fixedNormalWS,
      pointToLightDirWS,
      pointToCameraDirWS,
      diffColor*(vec3(1.0,1.0,1.0)-specColor) * M_INV_PI)
    + microfacets_brdf(
      fixedNormalWS, 
      pointToLightDirWS, 
      pointToCameraDirWS, 
      specColor, 
      roughness)) * LampColor * M_PI);
}

Implemented a part to obtain the direction of sunlight.

vec3 normal_vec = computeWSNormal(inputs.tex_coord, inputs.tangent, inputs.bitangent, inputs.normal);
  lightVector = light_pos.xyz - inputs.position.xyz;
  vec3 eye_vec = is_perspective ? normalize(camera_pos - inputs.position) :  -camera_dir;
vec3 sunIrradiance = vec3(1.0);
  vec3 sun_vec = normalize(lightVector);
  vec3 sunContrib = pointLightContribution(normal_vec, sun_vec, eye_vec, diffColor, specColor, roughness, sunIrradiance * sun_strenght);
if(sun)
  {
    diffColor +=sunContrib;
  }

The sun variable declared as a bool type and the if statement complete the Toggle function.

Related post

https://leegoonz.blog/2020/01/13/white-color-temperature-calibrations-of-game-engines/

https://leegoonz.blog/2020/04/07/substance-painter-shader-guide-part-2-adding-to-custom-structure/

Categories: tutorials

Tagged as: ,

2 replies

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s