**[Will's Journal](../index.html)** (#) **2024/06/25: Environment/PBR Mapping Results** This journal entry is a continuation of the previous entry, where I have now finally implemented environment mapping which influences PBR shading. An environment map is an approximation of the incoming irradiance of the scene from extreme distances, which is typically used for distant backgrounds such as skies and landscapes. These maps can be taken a step further and be used to roughly approximate the integral in the rendering equation. Incorporating the environment map into the shading calculations makes objects appear more as if they were a part of the scene. The results are striking. ![No Environment](images/vulkan/SponzaEnvironment3.png)![Meadow Environment](images/vulkan/SponzaEnvironment2.png)![Wasteland Environment](images/vulkan/SponzaEnvironment.png) Quite simply, without any form of global/local illumination outside of the direct specular/diffuse component, objects simply do not have enough incoming light to be at all convincing. The first image is an example of this. Additionally, with the metallic/roughness properties of the surfaces, their shades can be even more complex. ![0 roughness 1 metallic](images/vulkan/smoothMetallicBall.png)![Smooth Metallic Array](images/vulkan/smoothMetallicBalls.png) I am extremely satisfied with the results of environment and pbr mapping. My renderer is in a good stage with regards to shading, but still has a long way to go before I'd like to begin with features common to games, such as physics and audio. There are several features that can be implemented as the next immediate step. Below are the said features ranked by priority. 0. Other Basic Features - Some features from the GLTF structure are not implemented as of yet. This includes Normal, Emission, and AO Mapping. These features shouldn't take very long to incorporate into the current pipeline. 1. Ambient Occlusion - More specifically, Ground-Truth Ambient Occlusion and/or Horizon-Based Ambient Occlusion. While the bright spots are adequately lit for the moment, the shading can still look flat without any form of AO. 2. Deferred Rendering - However, giving AO implementations a quick initial review, it seems that GTAO/HBAO/SSAO benefit from a deferred workflow. I did not intend for my renderer to support deferred rendering so early, but I think it might be the right move to do that now. 3. Shadows - While shadows are a broad subject, I am specifically looking to at least implement point lights and cascaded shadow maps. Spotlights would be nice, but are a subset of point lights anyway. 4. Reflection Probes - Environment/PBR mapping is great, but fail to properly capture reflections indoors. With extremely bright environment maps, indoor areas tend to look excessively bright. It is my hope that reflection probes will alleviate some of this. 5. Post-Processing Effects - Certain effects that are simply standard for a renderer. This includes Bloom, Depth of Field, Motion Blur, and Lens Flare. 6. Global Illumination - Global illumination is a important but highly complex subject, and I may end up putting this off until physics and audio have been implemented. 7. Area Lights - Area lights are a complex but important extension of Global Illumination and is so important that they warrant their own spot on this feature list. (###) **PBR Forward Shader** Finally, I'd like to end this entry with a code snippet of the pbr fragment shader as it currently exists in this forward renderer. There is an almost 100% likelihood that the following shader will be entirely modified soon to support a deferred workflow. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #version 460 #extension GL_EXT_buffer_reference : require #extension GL_GOOGLE_include_directive : require #include "indirect_input_structures.glsl" // All calculations are done in world space layout (location = 0) in vec3 inPosition; layout (location = 1) in vec3 inNormal; layout (location = 2) in vec3 inColor; layout (location = 3) in vec2 inUV; layout (location = 4) flat in uint inMaterialIndex; layout (location = 0) out vec4 outFragColor; vec3 lambert(vec3 kD, vec3 albedo) { return kD * albedo / 3.14159265359; }; float D_GGX(vec3 N, vec3 H, float roughness){ // Brian Karis says GGX NDF has a longer "tail" that appears more natural float a = roughness * roughness; float a2 = a * a; // disney reparameterization float NdotH = max(dot(N, H), 0.0f); float NdotH2 = NdotH * NdotH; float num = a2; float premult = NdotH2 * (a2 - 1.0f) + 1.0f; float denom = 3.14159265359 * premult * premult; return num / denom; }; float G_SCHLICKGGX(float NdotX, float k){ float num = NdotX; float denom = NdotX * (1.0f - k) + k; return num / denom; }; float G_SCHLICKGGX_SMITH(vec3 N, vec3 V, vec3 L, float roughness){ // height correlated smith method // "Disney reduced hotness" - Brian Karis float r = (roughness + 1); float k = (r * r) / 8.0f; float NDotV = max(dot(N, V), 0.0f); float NDotL = max(dot(N, L), 0.0f); float ggx2 = G_SCHLICKGGX(NDotV, k); float ggx1 = G_SCHLICKGGX(NDotL, k); return ggx1 * ggx2; }; float unreal_fresnel_power(vec3 V, vec3 H){ float VDotH = dot(V, H); return (-5.55473 * VDotH - 6.98316) * VDotH; }; vec3 F_SCHLICK(vec3 V, vec3 H, vec3 F0){ float VdotH = max(dot(V, H), 0.0f); // classic //return F0 + (1.0f - F0) * pow(1.0f - VdotH, 5.0f); // unreal optimized return F0 + (1 - F0) * pow(2, unreal_fresnel_power(V, H)); } void main() { Material m = buffer_addresses.materialBufferDeviceAddress.materials[inMaterialIndex]; uint colorSamplerIndex = uint(m.texture_sampler_indices.x); uint colorImageIndex = uint(m.texture_image_indices.x); vec4 _col = texture(sampler2D(textures[colorImageIndex], samplers[colorSamplerIndex]), inUV); uint metalSamplerIndex = uint(m.texture_sampler_indices.y); uint metalImageIndex = uint(m.texture_image_indices.y); vec4 _metal_rough_sample = texture(sampler2D(textures[metalImageIndex], samplers[metalSamplerIndex]), inUV); _col *= m.color_factor; if (_col.w < m.alpha_cutoff.w) { discard; } vec3 albedo = inColor * _col.xyz; float metallic = _metal_rough_sample.b * m.metal_rough_factors.x; float roughness = _metal_rough_sample.g * m.metal_rough_factors.y; vec3 light_color = sceneData.sunlightColor.xyz * sceneData.sunlightColor.w; vec3 N = normalize(inNormal); vec3 V = normalize(sceneData.cameraPos.xyz - inPosition); vec3 L = normalize(sceneData.sunlightDirection.xyz); // for point lights, light.pos - inPosition vec3 H = normalize(V + L); // SPECULAR float NDF = D_GGX(N, H, roughness); float G = G_SCHLICKGGX_SMITH(N, V, L, roughness); vec3 F0 = mix(vec3(0.04), albedo, metallic); vec3 F = F_SCHLICK(V, H, F0); vec3 numerator = NDF * G * F; float denominator = 4.0f * max(dot(N, V), 0.0f) * max(dot(N, L), 0.0f); vec3 specular = numerator / max(denominator, 0.001f); vec3 kS = F; vec3 kD = vec3(1.0f) - kS; kD *= 1.0f - metallic; // DIFFUSE float nDotL = max(dot(N, L), 0.0f); vec3 diffuse = lambert(kD, albedo); // REFLECTIONS vec3 irradiance = DiffuseIrradiance(N); vec3 reflection_diffuse = irradiance * albedo; vec3 reflection_specular = SpecularReflection(V, N, roughness, F); vec3 ambient = (kD * reflection_diffuse + reflection_specular); vec3 final_color = (diffuse + specular) * light_color * nDotL; //final_color += ambient; vec3 corrected_ambient = ambient / (ambient + vec3(1.0f)); // Reinhard corrected_ambient = pow(corrected_ambient, vec3(1.0f / 2.2f)); // gamma correction final_color += corrected_ambient; outFragColor = vec4(final_color, _col.w); vec3 corrected_final_color = final_color / (final_color + vec3(1.0f)); // Reinhard corrected_final_color = pow(corrected_final_color, vec3(1.0f / 2.2f)); // gamma correction } ; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The indirect_input_structures.glsl is poorly named, but is a simple header which includes everything necessary for shading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ struct Vertex { vec3 position; //float uv_x; float pad; vec3 normal; //float uv_y; float pad2; vec4 color; vec2 uv; uint material_index; // only use X, can pack with other values in future float pad3; }; struct Material { vec4 color_factor; vec4 metal_rough_factors; vec4 texture_image_indices; vec4 texture_sampler_indices; vec4 alpha_cutoff; // only use X, can pack with other values in future }; struct Model { mat4 model; uint vertex_offset; uint index_count; uint mesh_index; float pad; }; layout(buffer_reference, std430) readonly buffer VertexBuffer { Vertex vertices[]; }; layout(buffer_reference, std430) readonly buffer ModelData { Model models[]; }; layout(buffer_reference, std430) readonly buffer MaterialData { Material materials[]; }; layout(set = 0, binding = 0) uniform addresses { VertexBuffer vertexbufferDeviceAddress; MaterialData materialBufferDeviceAddress; ModelData modelBufferDeviceAddress; } buffer_addresses; layout(set = 1, binding = 0) uniform sampler samplers[32]; layout(set = 1, binding = 1) uniform texture2D textures[255]; layout(set = 2, binding = 0) uniform GlobalUniform { mat4 view; mat4 proj; mat4 viewproj; vec4 ambientColor; vec4 sunlightDirection; //w for sun power vec4 sunlightColor; vec4 cameraPos; } sceneData; const uint DIFF_IRRA_MIP_LEVEL = 5; const bool FLIP_ENVIRONMENT_MAP_Y = true; const float MAX_REFLECTION_LOD = 4.0; layout(set = 3, binding = 0) uniform samplerCube environmentDiffuseAndSpecular; layout(set = 3, binding = 1) uniform sampler2D lut; vec3 DiffuseIrradiance(vec3 N) { vec3 ENV_N = N; if (FLIP_ENVIRONMENT_MAP_Y) { ENV_N.y = -ENV_N.y; } return textureLod(environmentDiffuseAndSpecular, ENV_N, DIFF_IRRA_MIP_LEVEL).rgb; } vec3 SpecularReflection(vec3 V, vec3 N, float roughness, vec3 F) { vec3 R = reflect(-V, N); if (FLIP_ENVIRONMENT_MAP_Y) { R.y = -R.y; } // dont need to skip mip 5 because never goes above 4 vec3 prefilteredColor = textureLod(environmentDiffuseAndSpecular, R, roughness * MAX_REFLECTION_LOD).rgb; vec2 envBRDF = texture(lut, vec2(max(dot(N, V), 0.0f), roughness)).rg; return prefilteredColor * (F * envBRDF.x + envBRDF.y); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~