User Tools

Site Tools


ibl_sample_shader_lys

This is an old revision of the document!


Image Based Lighting Sample Shader

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, though may still be a very useful general reference in other cases.

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

/*
 
% 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);
 
 
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); 
	}
}
ibl_sample_shader_lys.1493467927.txt.gz · Last modified: 2017/05/23 03:49 (external edit)