diff --git a/Effects/PBREffect.cs b/Effects/PBREffect.cs new file mode 100644 index 0000000..be79d33 --- /dev/null +++ b/Effects/PBREffect.cs @@ -0,0 +1,160 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Smuggler +{ + public struct PBRLight + { + public Vector3 direction; + public Vector3 radiance; + + public PBRLight(Vector3 direction, Vector3 radiance) + { + this.direction = direction; + this.radiance = radiance; + } + } + + public class PBRLightCollection + { + readonly EffectParameter directionParam; + readonly EffectParameter radianceParam; + + private readonly Vector3[] directions = new Vector3[PBREffect.NUM_LIGHTS]; + private readonly Vector3[] radiances = new Vector3[PBREffect.NUM_LIGHTS]; + + public PBRLightCollection(EffectParameter directionParam, EffectParameter radianceParam) + { + this.directionParam = directionParam; + this.radianceParam = radianceParam; + } + + public PBRLightCollection(PBRLightCollection other) + { + for (int i = 0; i < PBREffect.NUM_LIGHTS; i++) + { + directions[i] = other.directions[i]; + radiances[i] = other.radiances[i]; + } + } + + public PBRLight this[int index] + { + get + { + return new PBRLight(directions[index], radiances[index]); + } + + set + { + directions[index] = value.direction; + radiances[index] = value.radiance; + directionParam.SetValue(directions); + radianceParam.SetValue(radiances); + } + } + } + + public class PBREffect : Effect + { + public static readonly int NUM_LIGHTS = 3; + + readonly EffectParameter viewProjectionParam; + readonly EffectParameter sceneRotationParam; + readonly EffectParameter eyePositionParam; + readonly EffectParameter specularTextureLevelsParam; + + readonly EffectParameter albedoTextureParam; + + Matrix world = Matrix.Identity; + Matrix view = Matrix.Identity; + Matrix projection = Matrix.Identity; + Vector3 eyePosition = Vector3.Zero; + int specularTextureLevels = 0; + + public Matrix World + { + get { return world; } + set + { + world = value; + sceneRotationParam.SetValue(world); + } + } + + public Matrix View + { + get { return view; } + set + { + view = value; + viewProjectionParam.SetValue(projection * view); + eyePositionParam.SetValue(Matrix.Invert(view).Backward); + } + } + + public Matrix Projection + { + get { return projection; } + set + { + projection = value; + viewProjectionParam.SetValue(projection * view); + } + } + + public PBRLightCollection Lights { get; } + + public int SpecularTextureLevels + { + get { return specularTextureLevels; } + set + { + specularTextureLevels = value; + specularTextureLevelsParam.SetValue(specularTextureLevels); + } + } + + public Texture2D AlbedoTexture + { + get { return albedoTextureParam.GetValueTexture2D(); } + set { albedoTextureParam.SetValue(value); } + } + + public PBREffect(GraphicsDevice graphicsDevice, byte[] effectCode) : base(graphicsDevice, effectCode) + { + viewProjectionParam = Parameters["viewProjectionMatrix"]; + sceneRotationParam = Parameters["sceneRotationMatrix"]; + + eyePositionParam = Parameters["eyePosition"]; + specularTextureLevelsParam = Parameters["specularTextureLevels"]; + + Lights = new PBRLightCollection(Parameters["directions"], Parameters["radiance"]); + } + + protected PBREffect(PBREffect cloneSource) : base(cloneSource) + { + viewProjectionParam = Parameters["viewProjectionMatrix"]; + sceneRotationParam = Parameters["sceneRotationMatrix"]; + + eyePositionParam = Parameters["eyePosition"]; + specularTextureLevelsParam = Parameters["specularTextureLevels"]; + + World = cloneSource.World; + View = cloneSource.View; + Projection = cloneSource.Projection; + Lights = new PBRLightCollection(cloneSource.Lights); + } + + public override Effect Clone() + { + return new PBREffect(this); + } + + // FIXME: do param applications here for performance + protected override void OnApply() + { + base.OnApply(); + } + } +} diff --git a/Effects/pbr.fx b/Effects/pbr.fx new file mode 100644 index 0000000..71b2b02 --- /dev/null +++ b/Effects/pbr.fx @@ -0,0 +1,217 @@ +// Physically Based Rendering +// Copyright (c) 2017-2018 MichaƂ Siejak + +// Physically Based shading model: Lambetrtian diffuse BRDF + Cook-Torrance microfacet specular BRDF + IBL for ambient. + +// This implementation is based on "Real Shading in Unreal Engine 4" SIGGRAPH 2013 course notes by Epic Games. +// See: http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf + +static const float PI = 3.141592; +static const float Epsilon = 0.00001; + +static const uint NumLights = 3; + +// Constant normal incidence Fresnel factor for all dielectrics. +static const float3 Fdielectric = 0.04; + +// UNIFORM CONSTANTS +float4x4 viewProjectionMatrix; +float4x4 sceneRotationMatrix; + +float3 direction[NumLights]; +float3 radiance[NumLights]; + +float3 eyePosition; +int specularTextureLevels; + +struct VertexShaderInput +{ + float3 position : POSITION; + float3 normal : NORMAL; + float3 tangent : TANGENT; + float3 binormal : BINORMAL; + float2 texcoord : TEXCOORD; +}; + +struct PixelShaderInput +{ + float4 pixelPosition : SV_POSITION; + float3 position : POSITION1; + float2 texcoord : TEXCOORD0; + float3 T : TEXCOORD1; + float3 B : TEXCOORD2; + float3 N : TEXCOORD3; +}; + +sampler albedoTexture : register(s0); +sampler normalTexture : register(s1); +sampler metalnessTexture : register(s2); +sampler roughnessTexture : register(s3); +sampler specularTexture : register(s4); +sampler irradianceTexture : register(s5); +sampler specularBRDF_LUT : register(s6); + +// GGX/Towbridge-Reitz normal distribution function. +// Uses Disney's reparametrization of alpha = roughness^2. +float ndfGGX(float cosLh, float roughness) +{ + float alpha = roughness * roughness; + float alphaSq = alpha * alpha; + + float denom = (cosLh * cosLh) * (alphaSq - 1.0) + 1.0; + return alphaSq / (PI * denom * denom); +} + +// Single term for separable Schlick-GGX below. +float gaSchlickG1(float cosTheta, float k) +{ + return cosTheta / (cosTheta * (1.0 - k) + k); +} + +// Schlick-GGX approximation of geometric attenuation function using Smith's method. +float gaSchlickGGX(float cosLi, float cosLo, float roughness) +{ + float r = roughness + 1.0; + float k = (r * r) / 8.0; // Epic suggests using this roughness remapping for analytic lights. + return gaSchlickG1(cosLi, k) * gaSchlickG1(cosLo, k); +} + +// Shlick's approximation of the Fresnel factor. +float3 fresnelSchlick(float3 F0, float cosTheta) +{ + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +// Vertex shader +PixelShaderInput main_vs(VertexShaderInput vin) +{ + PixelShaderInput vout; + vout.position = mul(sceneRotationMatrix, float4(vin.position, 1.0)).xyz; + vout.texcoord = float2(vin.texcoord.x, 1.0-vin.texcoord.y); + + // Pass tangent space basis vectors (for normal mapping). + float3 worldTangent = mul(vin.tangent, (float3x3) sceneRotationMatrix); + vout.T = normalize(worldTangent); + + float3 worldBinormal = mul(vin.binormal, (float3x3) sceneRotationMatrix); + vout.B = normalize(worldBinormal); + + float3 worldNormal = mul(vin.normal, (float3x3) sceneRotationMatrix); + vout.N = normalize(worldNormal); + + float4x4 mvpMatrix = mul(viewProjectionMatrix, sceneRotationMatrix); + vout.pixelPosition = mul(mvpMatrix, float4(vin.position, 1.0)); + return vout; +} + +// Pixel shader +float4 main_ps(PixelShaderInput pin) : SV_Target0 +{ + // Sample input textures to get shading model params. + float3 albedo = tex2D(albedoTexture, pin.texcoord).rgb; + float metalness = tex2D(metalnessTexture, pin.texcoord).r; + float roughness = tex2D(roughnessTexture, pin.texcoord).r; + + // Outgoing light direction (vector from world-space fragment position to the "eye"). + float3 Lo = normalize(eyePosition - pin.position); + + // tranpose to transform tangent space => world + float3x3 TBN = transpose(float3x3(normalize(pin.T), normalize(pin.B), normalize(pin.N))); + + // Get current fragment's normal and transform to world space. + float3 tangentNormal = normalize(2.0 * tex2D(normalTexture, pin.texcoord).rgb - 1.0); + + // world normal + float3 N = normalize(mul(TBN, tangentNormal)); + + // Angle between surface normal and outgoing light direction. + float cosLo = max(0.0, dot(N, Lo)); + + // Specular reflection vector. + float3 Lr = 2.0 * cosLo * N - Lo; + + // Fresnel reflectance at normal incidence (for metals use albedo color). + float3 F0 = lerp(Fdielectric, albedo, metalness); + + // Direct lighting calculation for analytical lights. + float3 directLighting = 0.0; + for(uint i=0; i