
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:


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


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


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


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
