Reitano 727 Report post Posted January 13, 2014 (edited) Hi, Over the weekend I've read the presentation on physically-based shading in the Unreal 4 engine (http://www.unrealengine.com/files/downloads/2013SiggraphPresentationsNotes.pdf). I have a question on the integration of environment maps. As described in the paper, this is accomplished by splitting the integration in two parts: the average of the environment lighting (a mip mapped cubemap) and a pre-convolved BRDF, parametrized by the dot product (normal.view) and the material roughness. For the BRDF, we calculate many random directions around the normal based on the roughness, then calculate the corresponding reflected vector and use it to evaluate the BRDF. My question is: should we weight each sample by the dot product between the reflected vector and the normal ? That makes sense to me as it's part of the lighting equation, but it gives very dark results at glancing angles and for low roughness values because in that case, the majority of reflected vectors are almost perpendicular to the normal. The sample code in the paper does not consider this factor which is a little surprising. Thanks, Stefano Edited January 13, 2014 by Reitano 2 Share this post Link to post Share on other sites
Reitano 727 Report post Posted January 14, 2014 I read that presentation a second time and found the answer to my question. The guys at Epic pre-convolve cubemaps offline and weight each sample by the aforementioned dot product. As my cubemaps are dynamically rendered and mip-mapped automatically by the hardware, I do not have any control on their convolution and I will have to move the dot product to the pre-integrated BRDF instead. If anybody's interested I could post here the C++ and shader code of my implementation. 4 Share this post Link to post Share on other sites
Migi0027 4628 Report post Posted January 14, 2014 Reitano: I would be deeply interested in it! 2 Share this post Link to post Share on other sites
Hodgman 51234 Report post Posted January 15, 2014 Always good to compare different implementations ;) Btw, you could always generate the mip-maps manually. Implementing a Gaussian rather than bilinear filter is a small bit of work, but shouldn't have much of an impact on performance. 1 Share this post Link to post Share on other sites
Reitano 727 Report post Posted January 15, 2014 I had no time yesterday to prepare the code but hopefully tonight I'll manage. On a related note, what representation do you use for the environment lighting and what filter do you use for the pre-convolution ? At the moment I'm still stuck with DirectX 9 and rough materials exhibit lighting seams due to the discontinuos sampling of adjacent cubemap faces. Apparently that is fixed in Dx10/Dx11 which implies that graphic cards have an internal swtich to control the sampling. I wonder if any of the DirectX 9 hacks could enable it. Another option is to use dual paraboloid maps which are free from this issue. Or the trick described http://the-witness.net/news/2012/02/seamless-cube-map-filtering/ might help Hodgman: manual subsampling of a cubemap with shaders sounds appealing (HW filters for auto mipmaps are really poor) but in DirectX 9 you can't use a cubemap both as sampler and render target in the same pass. Is there a workaround perhaps ? 0 Share this post Link to post Share on other sites
Hodgman 51234 Report post Posted January 15, 2014 (edited) I'm using sparse cube-light-probes at the moment, and still experimenting with filters. None of this is final in my project, so I'm still trying to figure out a good solution at this stage, and don't have a solid implementation yet. I'm also thinking of abandoning actual cubes and instead using a 2D texture containing an atlas/array of unwrapped cubes in my D3D9 version. Or the same thing with an atlas of dual-parabolas, or dual-spheres, etc... e.g. | +X1 | -X1 | +Y1 | -Y1 | +Z1 | -Z1 | +-----+-----+-----+-----+-----+-----+ | +X2 | -X2 | +Y2 | -Y2 | +Z2 | -Z2 | ... Yeah for D3D9 cube sampling, I would use the method at the bottom of your link. It seems to be the most recent hack to deal with that issue There's a bunch of workarounds, but I'm not sure which is best... You could do all the mip-generation in a large 2D texture, and then copy regions of it into all the mip-levels when complete... You could ping-pong between two cubes and then merge the odd-levels from the 2nd into the 1st when complete... You could generate all the mips from the base level (instetad of from the previous level); render the initial cube-map to a non-mipped texture, which is then used to generate the entire mipped texture... You could ignore the restriction and see if it works on the user's driver (it may, even though it's not allowed)... Edited January 15, 2014 by Hodgman 0 Share this post Link to post Share on other sites
Reitano 727 Report post Posted January 17, 2014 As promised here's some code. I'm still not sure about some details (in particular those related to importance sampling) and it'd be great if other people could spot errors and add more variants to the code, like different distribution functions. void GenerateFGLookupTexture(Graphics::Texture* texture, RenderingDevice& device) { Graphics::TextureDesc textureDesc; device.GetDesc(texture, textureDesc); assert(textureDesc.format == Graphics::Format::G16R16); assert(textureDesc.levels == 1); const float deltaNdotV = 1.f / static_cast<float>(textureDesc.width); const float deltaRoughness = 1.f / static_cast<float>(textureDesc.height); // Lock texture const Graphics::MappedData mappedData = device.Map(texture, 0, Graphics::MapType::Write, 0); uint8* const RESTRICT dataPtr = reinterpret_cast<uint8*>(mappedData.data); const uint numSamples = 512; float roughness = 0.f; for (uint v = 0; v < textureDesc.height; v++) { float NdotV = 0.f; uint32* dst = reinterpret_cast<uint32*>(dataPtr + v * mappedData.rowPitch); for (uint u = 0; u < textureDesc.width; u++, ++dst) { float a = 0; float b = 0; CalculateFGCoeff(NdotV, roughness, numSamples, a, b); assert(a <= 1.f); assert(b <= 1.f); *dst = (PACKINTOSHORT_0TO1(b) << 16) | PACKINTOSHORT_0TO1(a); NdotV += deltaNdotV; } roughness += deltaRoughness; } device.Unmap(texture, 0); } void PlaneHammersley(float& x, float& y, int k, int n) { float u = 0; float p = 0.5f; // FIXME Optimize by removing conditional for (int kk = k; kk; p *= 0.5f, kk /= 2) { if (kk & 1) { u += p; } } x = u; y = (k + 0.5f) / n; } vec3 ImportanceSampleGGX(float x, float y, float a4) { const float PI = 3.1415926535897932384626433832795028841971693993751f; // Convert uniform random variables x, y to a sample direction const float phi = 2 * PI * x; const float cosTheta = std::sqrt( (1 - y) / ( 1 + (a4 - 1) * y) ); const float sinTheta = std::sqrt(1 - cosTheta * cosTheta); // Convert direction to cartesian coordinates const vec3 H(sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta); //D = a2 / (PI * std::pow(cosTheta * (a2 - 1) + 1, 2)); return H; } // Reference: GPU Gems 3 - GPU Based Importance Sampling //s2012_pbs_physics_math_slides.pdf vec3 ImportanceSampleBlinn(float x, float y, float specularPower) { const float PI = 3.1415926535897932384626433832795028841971693993751f; // Convert uniform random variables x, y to a sample direction const float phi = 2 * PI * x; const float cosTheta = std::pow(y, 1.f / (specularPower + 1)); const float sinTheta = std::sqrt(1 - cosTheta * cosTheta); // Convert direction to cartesian coordinates const vec3 H(sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta); //D = (specularPower + 2) / (2 * PI) * std::pow(cosTheta, specularPower); return H; } float G_Schlick(float k, float NdotV, float NdotL) { return (NdotV * NdotL) / ( (NdotV * (1 - k) + k) * (NdotL * (1 - k) + k) ); } void CalculateFGCoeff(float NoV, float roughness, uint numSamples, float& a, float& b) { // Work in a coordinate system where normal = vec3(0,0,1) #define GGX 0 #define BLINN 1 #define G_SCHLICK 0 // Build view vector const vec3 V(std::sqrt(1.0f - NoV * NoV), 0, NoV); #if BLINN const float blinnSpecularPower = std::pow(2.f, 13.f * roughness); #elif GGX const float GXX_a4 = std::pow(roughness, 4); #endif #if G_SCHLICK const float G_k = std::pow(roughness + 1, 2.f) / 8.f; #endif a = 0; b = 0; for (uint i = 0; i < numSamples; ++i) { float x, y; PlaneHammersley(x, y, i, numSamples); // Microfacet specular model: // f = D*G*F / (4*NoL*NoV) // V = G / (NoL*NoV) // Importance-based sampling: // f / pdf // Calculate random half vector based on roughness #if BLINN const vec3 H = ImportanceSampleBlinn(x, y, blinnSpecularPower); // D and pdfH cancel each other so just set to 1 const float D = 1; const float pdfH = D; #elif GXX const vec3 H = ImportanceSampleGGX(x, y, GXX_a4); // D and pdfH cancel each other so just set to 1 const float D = 1; const float pdfH = D; #endif // Calculate light direction const vec3 L = 2 * dot( V, H ) * H - V; const float NoL = saturate( L.z ); const float VoH = saturate( dot( V, H ) ); const float NoH = saturate( H.z ); const float LoH = saturate( dot( L, H ) ); // = VoH // Convert pdf(H) to pdf(L) // Reference: Torrance and Sparrow // http://graphics.stanford.edu/~boulos/papers/brdftog.pdf const float pdfL = pdfH / (4 * LoH); if (NoL > 0) { #if G_SCHLICK // FIXME NoV cancel out const float G = G_Schlick(G_k, NoV, NoL); const float V = G / (NoL * NoV); #elif const float V = 1; #endif const float G_Vis = D * V / 4 / pdfL; const float Fc = std::pow(1 - VoH, 5.f); // FIXME : NoL ? Part of the lighting eq but gives dark reflections at grazing angles. Need a better BRDF probably a += (1 - Fc) * G_Vis * NoL; b += Fc * G_Vis * NoL; } } a /= numSamples; b /= numSamples; } } 1 Share this post Link to post Share on other sites