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