User Tools

Site Tools


Image Based Lighting Sample Shader

Throughout the following page we are providing a free IBL sample shader and the associated source files.

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.

The IBL sample shader running in FX Composer 2.51.

Downloading and running the shader in FX Composer

  1. Download the shader and asset files from HERE.
  2. Download FX Composer from HERE & install.
  3. Extract the .zip file to a location of your choosing.
  4. Double click the freeLysIblSample.fxcproj file found in the extracted folder.
  5. Ensure that the renderer is set to Direct3D10 by selecting the option found within in the dropdown menu located at the far right of the top top row of buttons.
  6. Select the IblLysBurley material in the Materials panel.
  7. Assign the cubemap and textures within the Properties panel by clicking each texture slot and their associated tool buttons> clicking the Image icon> clicking the + icon located at the top left of the popup and then finally selecting the textures you want to load. Repeat for all the textures as required.
  8. Enjoy!

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.
% 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"
// Despite the difference in distribution of MIPs the lit specular response resulting from
// the “roughness texture” will be identical to existing PBR based game engines and tools.
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(,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(,0), g_mWorldToObjTransposed).xyz );
    // used by rasterizer
    res.position = mul(float4(, 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;  // inverted roughness texture map (1-x) and no gamma correction
    float perceptualRoughness = 1.0 - smoothness;
    //float perceptualRoughness = roughness_tex.Sample(samLinear, st).x; // For those using roughness texture maps use this line instead to initialize perceptualRoughness.
    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(,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);
    float l = BurleyToMipSimple(perceptualRoughness, numMips);
    // 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
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 );
ibl_sample_shader_lys.txt · Last modified: 2017/05/23 03:49 (external edit)