Knaldtech Start
Knaldtech Start
This is an old revision of the document!
Throughout the following page we are providing a free IBL sample shader, the associated source files for the shader and a general user guide in that you may test the various aspects of IBL with a minimal setup and iteration time.
The shader specifically covers cube maps that have been generated within Lys using the Burley option for image based lighting but may also serve as a useful reference in general.
Below is the full shader. You can download the sample shader and assets HERE
/* % Copyright 2017 Knald Technologies, LLC % See LICENSE.txt for licensing and redistribution terms. % Free IBL sample using cube map exported from Knaldtech's tool Lys. https://www.knaldtech.com/lys/ % The cube map was made with offset set to 3 and exported as GGX with Burley roughness drop. */ #define FLT_EPSILON 1.192092896e-07f // smallest such that 1.0+FLT_EPSILON != 1.0 // assume the default value of 3 which assigns maximum roughness to mip level 8x8 const int nMipOffset = 3; // global matrices float4x4 g_mObjToViewProj : WorldViewProjection; float4x4 g_mObjToWorld : World; float4x4 g_mWorldToObjTransposed : WorldInverseTranspose; float4x4 g_mViewToWorld : ViewInverse; float4x4 g_mObjToView : WorldView; float4x4 g_mViewToObj : WorldViewInverse; float4x4 g_mViewToObjTransposed : WorldViewInverseTranspose; // sampler SamplerState samLinear { Filter = MIN_MAG_MIP_LINEAR; AddressU = Wrap; AddressV = Wrap; }; // textures TextureCube lysBurleyCube < string UIName = "IBL. cube"; string ResourceType = "cube"; >; Texture2D albedo_tex < string UIName = "albedo Texture"; string ResourceType = "2D"; >; Texture2D smoothness_tex < string UIName = "smoothness Texture"; string ResourceType = "2D"; >; Texture2D metalness_tex < string UIName = "metalness Texture"; string ResourceType = "2D"; >; Texture2D normal_tex < string UIName = "normal Texture"; string ResourceType = "2D"; >; Texture2D ao_tex < string UIName = "ao Texture"; string ResourceType = "2D"; >; // vertex shader input struct vertInput { float4 position : POSITION; float3 normal : NORMAL; float3 tang : TANGENT; float3 bino : BINORMAL; float2 texcoord : TEXCOORD0; }; // vertex shader output (fed to pixel shader) struct vertexOutput { float4 position : POSITION; float3 pos : TEXCOORD0; float3 normal : TEXCOORD1; float2 stcoord : TEXCOORD2; float3 tang : TEXCOORD3; float3 bino : TEXCOORD4; }; float SpecularPowerFromPerceptualRoughness(float fPerceptualRoughness); float PerceptualRoughnessFromSpecularPower(float fSpecPower); float3 EvalBRDF(TextureCube lysBurleyCube, float3 vN, float3 vN_unit, float3 to_cam, float perceptualRoughness, float metalness, float3 albedo, float ao); float3 GetSpecularDominantDir(float3 vN, float3 vR, float fRealRoughness); float RoughnessFromPerceptualRoughness(float fPerceptualRoughness); float gain(float value, float g); float GetReductionInMicrofacets(float perceptualRoughness); float EmpiricalSpecularAO(float ao, float perceptualRoughness); float ApproximateSpecularSelfOcclusion(float3 vR, float3 vertNormalNormalized); // Note that our implementation of BurleyToMip() below differs from the more typical // form as cube maps convolved in Lys are based on RdotL and not NdotH. You can find // a more detailed description in "Pre-convolved Cube Maps vs Path Tracers" float BurleyToMip(float fPerceptualRoughness, int nMips, float NdotR) { float fSpecPower = SpecularPowerFromPerceptualRoughness(fPerceptualRoughness); fSpecPower /= (4*max(NdotR, FLT_EPSILON)); // see section "Pre-convolved Cube Maps vs Path Tracers" float fScale = PerceptualRoughnessFromSpecularPower(fSpecPower); return fScale*(nMips-1-nMipOffset); } float BurleyToMipSimple(float fPerceptualRoughness, int nMips) { float fScale = fPerceptualRoughness*(1.7 - 0.7*fPerceptualRoughness); // approximate remap from LdotR based distribution to NdotH return fScale*(nMips-1-nMipOffset); } int GetNumMips(TextureCube cubeTex) { int iWidth=0, iHeight=0, numMips=0; cubeTex.GetDimensions(0, iWidth, iHeight, numMips); return numMips; } // sRGB not built into fx composer. Must do by hand. // don't do this in your own engine. float3 GammaToLinear( float3 color) { return pow(color,2.2); } float3 LinearToGamma( float3 linearColor) { return pow(linearColor,1.0/2.2); } // vertex shader vertexOutput main_VS(vertInput IN) { vertexOutput res; res.stcoord = float2(IN.texcoord.x, 1.0-IN.texcoord.y); // transform attributes to world space res.pos = mul(float4(IN.position.xyz,1), g_mObjToWorld).xyz; res.tang = normalize( mul(float4(IN.tang, 0), g_mObjToWorld ).xyz ); res.bino = -normalize( mul(float4(IN.bino, 0), g_mObjToWorld ).xyz ); // bitangent negated in fxcomposer // normals are transformed using inverse transposed so this gives us the normal in world space. res.normal = normalize( mul(float4(IN.normal.xyz,0), g_mWorldToObjTransposed).xyz ); // used by rasterizer res.position = mul(float4(IN.position.xyz, 1.0), g_mObjToViewProj); return res; } // pixel shader float4 main_FP(vertexOutput IN) : COLOR { // gather inputs float3 vN = IN.normal; float3 vT = IN.tang; float3 vB = IN.bino; float3 vN_unit = normalize(vN); float3 pos = IN.pos; float2 st = IN.stcoord.xy; // material properties from texture float smoothness = smoothness_tex.Sample(samLinear, st).x; // not gamma corrected float perceptualRoughness = 1.0 - smoothness; float metalness = metalness_tex.Sample(samLinear, st).x; // not gamma corrected float3 texNormal = 2*normal_tex.Sample(samLinear, st).xyz - 1.0; // not gamma corrected float3 albedo = GammaToLinear( albedo_tex.Sample(samLinear, st).xyz ); float ao = ao_tex.Sample(samLinear, st).x; // not gamma corrected // get camera position and direction in world space float3 eyePos = float3(g_mViewToWorld[3].x,g_mViewToWorld[3].y,g_mViewToWorld[3].z); float3 to_cam = normalize(eyePos - pos); // to view vector // normal mapping //vN = normalize(vT*texNormal.x + vB*texNormal.y + vN*texNormal.z); // tangent space normal map vN = normalize( mul(float4(texNormal.xyz,0), g_mWorldToObjTransposed).xyz ); // object space normal map //vN = vN_unit; // normal mapping disabled (use interpolated vertex normal) // evaluate ibl based brdf float3 outRadiance = EvalBRDF(lysBurleyCube, vN, vN_unit, to_cam, perceptualRoughness, metalness, albedo, ao); // sRGB not built into fx composer. Must do by hand. // don't do this in your own engine. return float4(LinearToGamma(outRadiance), 1.0); } float3 EvalBRDF(TextureCube lysBurleyCube, float3 vN, float3 org_normal, float3 to_cam, float perceptualRoughness, float metalness, float3 albedo, float ao) { int numMips = GetNumMips(lysBurleyCube); const int nrBrdfMips = numMips-nMipOffset; float VdotN = clamp(dot(to_cam, vN), 0.0, 1.0f); // same as NdotR const float3 vRorg = 2*vN*VdotN-to_cam; float3 vR = GetSpecularDominantDir(vN, vRorg, RoughnessFromPerceptualRoughness(perceptualRoughness)); float RdotNsat = saturate(dot(vN, vR)); #if 1 float l = BurleyToMip(perceptualRoughness, numMips, RdotNsat); #else float l = BurleyToMipSimple(perceptualRoughness, numMips); #endif // fxcomposer uses a right hand coordinate frame (unlike d3d which uses left) // and has Y axis up. We've exported accordingly in Lys. For conventional // d3d11 just set Y axis as up in Lys before export. float3 specRad = lysBurleyCube.SampleLevel(samLinear, vR, l).xyz; float3 diffRad = lysBurleyCube.SampleLevel(samLinear, vN, (float) (nrBrdfMips-1)).xyz; float3 spccol = lerp( (float3) 0.04, albedo, metalness); float3 dfcol = lerp( (float3) 0.0, albedo, 1-metalness); // fresnel float fT = 1.0-RdotNsat; float fT5 = fT*fT; fT5 = fT5*fT5*fT; spccol = lerp(spccol, (float3) 1.0, fT5); // take reduction in brightness into account. float fFade = GetReductionInMicrofacets(perceptualRoughness); fFade *= EmpiricalSpecularAO(ao, perceptualRoughness); fFade *= ApproximateSpecularSelfOcclusion(vR, org_normal); // final result return ao*dfcol*diffRad + fFade*spccol*specRad; } float GetReductionInMicrofacets(float perceptualRoughness) { // this is not needed if you separately precompute an integrated FG term such as proposed // by epic. Alternatively this simple analytical approximation retains the energy // loss associated with Integral GGX(NdotH)*NdotH * (NdotL>0) dH which // for GGX equals 1/(roughness^2+1) when integrated over the half sphere. // without the NdotL>0 indicator term the integral equals one. float roughness = RoughnessFromPerceptualRoughness(perceptualRoughness); return 1.0 / (roughness*roughness+1.0); } float EmpiricalSpecularAO(float ao, float perceptualRoughness) { // basically a ramp curve allowing ao on very diffuse specular // and gradually less so as the reflection hardens. float fSmooth = 1-perceptualRoughness; float fSpecAo = gain(ao,0.5+max(0.0,fSmooth*0.4)); return min(1.0,fSpecAo + lerp(0.0, 0.5, fSmooth*fSmooth*fSmooth*fSmooth)); } // marmoset horizon occlusion http://marmosetco.tumblr.com/post/81245981087 float ApproximateSpecularSelfOcclusion(float3 vR, float3 vertNormalNormalized) { const float fFadeParam = 1.3; float rimmask = clamp( 1 + fFadeParam * dot(vR, vertNormalNormalized), 0.0, 1.0); rimmask *= rimmask; return rimmask; } float RoughnessFromPerceptualRoughness(float fPerceptualRoughness) { return fPerceptualRoughness*fPerceptualRoughness; } float PerceptualRoughnessFromRoughness(float fRoughness) { return sqrt(max(0.0,fRoughness)); } float SpecularPowerFromPerceptualRoughness(float fPerceptualRoughness) { float fRoughness = RoughnessFromPerceptualRoughness(fPerceptualRoughness); return (2.0/max(FLT_EPSILON, fRoughness*fRoughness))-2.0; } float PerceptualRoughnessFromSpecularPower(float fSpecPower) { float fRoughness = sqrt(2.0/(fSpecPower + 2.0)); return PerceptualRoughnessFromRoughness(fRoughness); } // frostbite presentation (moving frostbite to pbr) float3 GetSpecularDominantDir(float3 vN, float3 vR, float fRealRoughness) { float fInvRealRough = saturate(1 - fRealRoughness); float lerpFactor = fInvRealRough * (sqrt(fInvRealRough)+fRealRoughness); return lerp(vN, vR, lerpFactor); } float bias(float value, float b) { return (b > 0.0) ? pow(value, log(b) / log(0.5)) : 0.0; } // contrast function. float gain(float value, float g) { return 0.5 * ((value < 0.5) ? bias(2.0*value, 1.0-g) : (2.0 - bias(2.0-2.0*value, 1.0-g))); } // set depth, cull and blend states DepthStencilState EnableDepth { DepthEnable = TRUE; DepthWriteMask = ALL; DepthFunc = LESS_EQUAL; }; BlendState NoBlending { AlphaToCoverageEnable = FALSE; BlendEnable[0] = FALSE; }; RasterizerState RasterizerSettings { CullMode = FRONT; }; technique10 Render { pass p0 { SetVertexShader( CompileShader( vs_4_0, main_VS() ) ); SetPixelShader( CompileShader( ps_4_0, main_FP() ) ); SetBlendState( NoBlending, float4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xFFFFFFFF ); SetDepthStencilState( EnableDepth, 0 ); SetRasterizerState(RasterizerSettings); } }