Pular para o conteúdo

Em Direção ao PBR (Windows, Linux, Mac e Raspbian)

PBR image

Eu comecei a implementar um shader PBR (Physically Based Rendering) e pretendo executá-lo no meu Raspberry Pi 3 Model B+.

YouTube

No vídeo abaixo está uma apresentação desse conteúdo (em inglês):

Conceitos Tecnológicos

Para a minha surpresa, existem vários materiais bons sobre PBR na internet.

Usei duas referências principais para começar minha implementação:

Em resumo PBR é um tipo de modelo de iluminação proposto por by Cook-Torrance.

Ele usa propriedades de objetos reais ou propriedades de luzes reais para alimentar o modelo de iluminação.

A Implementação

Comecei a codificar um tipo de implementação conhecida como Forwarding Lighting.

Ela é baseada em realizar uma soma de cada componente de luz em um passo de renderização, seguindo a equação:

Result = ambientLight + LightComponent1 + LightComponent2 + ... + LightComponentN

Essa forma se adequa bem para o modelo de iluminação de Phong.

Quando trabalhamos com PBR, precisamos considerar o valor gamma que está presente em imagens e na saída do monitor, conhecido também como espaço gamma.

O modelo de iluminação de Phong utilizado para gerar imagens é empírico.

PBR em outra mão é um modelo baseado na real interação da luz com a superfície. E ele foi criado para trabalhar com as equações no espaço linear.

Para implementar corretamente as equações PBR, precisamos usar a soma da seguinte forma:

Result = (ambientLight + LightComponent1 + LightComponent2 + ... + LightComponentN)gamma

Correção Gamma: Problema e Solução

Para realizar a soma considerando o valor gamma, podemos usar a extensão OpenGL chamada FRAMEBUFFER_SRGB.

A placa de vídeo ou chip gráfico realizam a soma considerando o valor gamma.

Problema: O Raspberry Pi 3 Model B+ não realiza esse tipo de soma...

A solução encontrada foi calcular todas as contribuições de luz em um passo de shader único.

Para tratar essa restrições foi criado o FrankenShader.

FrankenShader

Esse shader é criado e compilado dinamicamente de acordo com o material e configuração de luz corrente da cena.

O algoritmo monta parte da equação usando como base o conjunto de parâmetros (material do objeto e configuração de luz).

Podemos ver um exemplo de um shader criado para trabalhar com luz ambiente usando uma cor, normalmap e luz do sol, no código abaixo:

// Compiling FrankenShader
// --------------------------
//   normalMap: 1
//   ambientLightColor: 1
//   ambientLightSkybox: 0
//   sunLight0: 1
//   sunLight1: 0
//   sunLight2: 0
//   sunLight3: 0
// 
// VERTEX SHADER
// 
attribute vec4 aPosition;
attribute vec3 aUV0;
uniform mat4 uMVP;
varying vec2 uv;
attribute vec3 aNormal;
attribute vec3 aTangent;
varying mat3 worldTBN;
uniform vec3 uCameraPosWorld;
varying vec3 viewWorld;
uniform mat4 uLocalToWorld;
uniform mat4 uLocalToWorld_it;
void main(){
  uv = aUV0.xy;
  vec3 N = normalize( uLocalToWorld_it * vec4( aNormal, 0.0 ) ).xyz;
  vec3 T = normalize( uLocalToWorld * vec4( aTangent, 0.0 ) ).xyz;
  T = normalize(T - dot(T, N) * N);
  vec3 B = cross(T, N);
  worldTBN = mat3(T,B,N);
  viewWorld = (uCameraPosWorld - (uLocalToWorld * aPosition).xyz);
  gl_Position = uMVP * aPosition;
}
// 
// FRAGMENT SHADER
// 
varying vec2 uv;
uniform vec4 uMaterialAlbedoColor;
uniform float uMaterialRoughness;
uniform float uMaterialMetallic;
uniform sampler2D uTextureAlbedo;
varying vec3 viewWorld;
varying mat3 worldTBN;
uniform sampler2D uTextureNormal;
uniform vec3 uAmbientColor;
uniform vec3 uLightRadiance0;
uniform vec3 uLightDir0;
vec3 fresnelSchlick(float cosTheta, vec3 F0){
  return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
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 num   = a2;
  float denom = (NdotH2 * (a2 - 1.0) + 1.0);
  denom = 3.14159265358 * denom * denom;
  return num / denom;
}
float GeometrySchlickGGX(float NdotV, float roughness){
  float r = (roughness + 1.0);
  float k = (r*r) / 8.0;
  float num   = NdotV;
  float denom = NdotV * (1.0 - k) + k;
  return num / 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 computePBR(vec3 albedo, vec3 radiance, vec3 N, vec3 V, vec3 L, vec3 H){
  vec3 F0 = vec3(0.04);
  F0      = mix(F0, albedo, uMaterialMetallic);
  vec3 F  = fresnelSchlick(max(dot(H, V), 0.0), F0);
  float G = GeometrySmith(N, V, L, uMaterialRoughness);
  float NDF = DistributionGGX(N, H, uMaterialRoughness);
  vec3 numerator    = NDF * G * F;
  float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
  vec3 specular     = numerator / max(denominator, 0.001);
  vec3 kS = F;
  vec3 kD = vec3(1.0) - kS;
  kD *= 1.0 - uMaterialMetallic;
  float NdotL = max(dot(N, L), 0.0);
  return (kD * albedo * 3.18309877326e-01 + specular) * radiance * NdotL;
}
vec3 readNormalMap(){
  vec3 normal = texture2D(uTextureNormal, uv).xyz;
  normal = normal * 2.0 - 1.0;
  normal = worldTBN * normal;
  return normalize(normal);
}
void main(){
  vec4 texel = texture2D(uTextureAlbedo, uv) * uMaterialAlbedoColor;
  vec3 V = normalize(viewWorld);
  vec3 L,H,radiance;
  vec3 N = readNormalMap();
  vec3 albedo = texel.rgb;
  texel.rgb = vec3(0);
  texel.rgb += albedo * pow(uAmbientColor, vec3(2.2));
  L = -uLightDir0;
  H = normalize(V + L);
  radiance = uLightRadiance0;
  texel.rgb += computePBR(albedo,radiance,N,V,L,H);
  gl_FragColor = texel;
}

Se você quiser ver o algoritmo que gerou esse shader, você pode ver ele aqui.

Resultados

Local de onde eu peguei os assets:

Para gerar os modelos com poucos polígonos e gerar o normalmap, eu utilizei o Blender.

É possível ver que nas imagens abaixo o modelo 3D se mistura bem com o fundo.

As imagens abaixo mostram o modelo 3D somente com a luz ambiente baseada em uma cor:

PBR Virgin Mary Ambient Light Color

PBR Jesus Ambient Light Color

As imagens abaixo mostram a luz ambiente baseada no skybox junto do normalmap:

PBR Virgin Mary Ambient Light Skybox

PBR Jesus Ambient Light Skybox

As imagens abaixo mostram a área iluminada quando ligamos a luz do sol:

PBR Virgin Mary Ambient And Sun Light

PBR Jesus Ambient And Sun Light

Por último, as imagens abaixo mostram a luz do sol sem o componente da luz ambiente:

PBR Virgin Mary Sun Light

PBR Jesus Sun Light

Conclusão

Para implementar esse projeto eu acabei fazendo uma grande atualização no framework.

Atualizações:

  • A ferramenta de importar/exportar modelos baseado no assimp está funcionando para mesh estática
  • O grafo de cena 3D (ou árvore 3D) foi modificada para trabalhar com configuração de câmera 2D
  • Existe um gerenciador de UI (interface de usuário) rudimentar
  • A parte interna da mini-gl-engine agora consegue tratar a extensão FRAMEBUFFER_SRGB do OpenGL
  • O componente Mesh e component Material estão sincronizados para trabalhar com a configuração PBR
  • O componente EngineShader e componente Mesh estão sincronizados para trabalhar com qualquer atributo de vértice conhecido
  • Os lados do CubeMap podem ser usados como render target em um framebuffer object dinâmico
  • O pipeline de renderização do projeto está configurado para trabalhar com a luz em vários passos de renderização ou trabalhar com a luz em um passo único de renderização

O código fonte do projeto está aqui.

Obrigado por ler esse post.

Um grande abraço,

Alessandro Ribeiro

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *