Просмотр исходного кода

Refactored lighting shader code so it is easier to follow

BearishSun 8 лет назад
Родитель
Сommit
0c5f53ce74
1 измененных файлов с 149 добавлено и 71 удалено
  1. 149 71
      Data/Raw/Engine/Includes/LightingCommon.bslinc

+ 149 - 71
Data/Raw/Engine/Includes/LightingCommon.bslinc

@@ -75,38 +75,79 @@ Technique
 			{
 				float output = saturate((dot(-toLight, direction) - angles.y) * angles.z);
 				return output * output;
+			}
+
+			// Window function to ensure the light contribution fades out to 0 at attenuation radius
+			float getRadialAttenuation(float distance2, LightData lightData)
+			{
+				float radialAttenuation = distance2 * lightData.attRadiusSqrdInv;
+				radialAttenuation *= radialAttenuation;
+				radialAttenuation = saturate(1.0f - radialAttenuation);
+				radialAttenuation *= radialAttenuation;
+				
+				return radialAttenuation;
 			}			
-			
-			float3 getDirLightContibution(SurfaceData surfaceData, LightData lightData)
+						
+			// Calculates illuminance from a non-area point light
+			float illuminancePointLight(float distance2, float NoL, LightData lightData)
 			{
-				return lightData.color * lightData.luminance;
+				return (lightData.luminance * NoL) / max(distance2, 0.01f*0.01f);
 			}
 			
-			float3 getPointLightContribution(float3 toLight, SurfaceData surfaceData, LightData lightData)
+			// Calculates illuminance scale for a sphere or a disc area light, while also handling the case when
+			// parts of the area light are below the horizon.
+			// Input NoL must be unclamped.
+			// Sphere solid angle = arcsin(r / d)
+			// Right disc solid angle = atan(r / d)
+			//   - To compensate for oriented discs, multiply by dot(diskNormal, -L)
+			float illuminanceScaleSphereDiskAreaLight(float unclampedNoL, float sinSolidAngleSqrd)
 			{
-				float3 N = surfaceData.worldNormal.xyz;
-				
-				float distanceSqrd = dot(toLight, toLight);
-				float distanceAttenuation = 1.0f / max(distanceSqrd, 0.01f*0.01f);
+				// Handles parts of the area light below the surface horizon
+				// See https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf for reference
+				float sinSolidAngle = sqrt(sinSolidAngleSqrd);
+				if(unclampedNoL < sinSolidAngle)
+				{
+					// Hermite spline approximation (see reference for exact formula)
+					unclampedNoL = max(unclampedNoL, -sinSolidAngle);
+					return ((sinSolidAngle + unclampedNoL) * (sinSolidAngle + unclampedNoL)) / (4 * sinSolidAngle);
+				}
+				else
+					return PI * sinSolidAngleSqrd * saturate(unclampedNoL);
+			}
+
+			// Calculates illuminance from a sphere area light.
+			float illuminanceSphereAreaLight(float unclampedNoL, float distToLight2, LightData lightData)
+			{
+				float radius2 = lightData.srcRadius * lightData.srcRadius;
 				
-				// Window function to ensure the light contribution fades out to 0 at attenuation radius
-				float radiusAttenuation = distanceSqrd * lightData.attRadiusSqrdInv;
-				radiusAttenuation *= radiusAttenuation;
-				radiusAttenuation = saturate(1.0f - radiusAttenuation);
-				radiusAttenuation *= radiusAttenuation;
+				// Squared sine of the sphere solid angle
+				float sinSolidAngle2 = radius2 / distToLight2;
 
-				float attenuation = distanceAttenuation * radiusAttenuation;
-				return lightData.color * lightData.luminance * attenuation;
+				// Prevent divide by zero
+				sinSolidAngle2 = min(sinSolidAngle2, 0.9999f);
+				
+				return lightData.luminance * illuminanceScaleSphereDiskAreaLight(unclampedNoL, sinSolidAngle2);	
 			}
 			
-			float3 getSpotLightContribution(float3 toLight, SurfaceData surfaceData, LightData lightData)
+			// Calculates illuminance from a disc area light.
+			float illuminanceDiscAreaLight(float unclampedNoL, float distToLight2, float3 L, LightData lightData)
 			{
-				float3 pointLightContribution = getPointLightContribution(toLight, surfaceData, lightData);
-				float spotFalloff = getSpotAttenuation(toLight, lightData.direction, lightData.spotAngles);
+				// Solid angle for right disk = atan (r / d)
+				//  atan (r / d) = asin((r / d)/sqrt((r / d)^2+1))
+				//  sinAngle = (r / d)/sqrt((r / d)^2 + 1)
+				//  sinAngle^2 = (r / d)^2 / (r / d)^2 + 1
+				//             = r^2 / (d^2 + r^2)
+			
+				float radius2 = lightData.srcRadius * lightData.srcRadius;
 				
-				return pointLightContribution * spotFalloff;
+				// max() to prevent light penetrating object
+				float sinSolidAngle2 = saturate(radius2 / (radius2 + max(radius2, distToLight2)));
+				
+				// Multiply by extra term to somewhat handle the case of the oriented disc (formula above only works
+				// for right discs).
+				return lightData.luminance * illuminanceScaleSphereDiskAreaLight(unclampedNoL, sinSolidAngle2 * saturate(dot(lightData.direction, -L)));	
 			}
-			
+		
 			// With microfacet BRDF the BRDF lobe is not centered around the reflected (mirror) direction.
 			// Because of NoL and shadow-masking terms the lobe gets shifted toward the normal as roughness
 			// increases. This is called the "off-specular peak". We approximate it using this function.
@@ -118,7 +159,7 @@ Technique
 			
 				float r2 = roughness * roughness;
 				return normalize(lerp(N, R, (1 - r2) * (sqrt(1 - r2) + r2)));
-			}
+			}		
 			
 			float3 getSurfaceShading(float3 V, float3 L, float specLobeEnergy, SurfaceData surfaceData)
 			{
@@ -168,19 +209,22 @@ Technique
 				float alpha = 0.0f;
 				if(surfaceData.worldNormal.w > 0.0f)
 				{
+					// Handle directional lights
 					for(uint i = 0; i < lightOffsets.x; ++i)
 					{
-						float3 L = -gLights[i].direction;
+						LightData lightData = gLights[i];
+					
+						float3 L = -lightData.direction;
 						float NoL = saturate(dot(N, L));
 						float specEnergy = 1.0f;
 						
 						// Distant disk area light. Calculate its contribution analytically by
 						// finding the most important (least error) point on the area light and
 						// use it as a form of importance sampling.
-						if(gLights[i].srcRadius > 0)
+						if(lightData.srcRadius > 0)
 						{
-							float diskRadius = sin(gLights[i].srcRadius);
-							float distanceToDisk = cos(gLights[i].srcRadius);
+							float diskRadius = sin(lightData.srcRadius);
+							float distanceToDisk = cos(lightData.srcRadius);
 							
 							// Closest point to disk (approximation for distant disks)
 							float DoR = dot(L, R);
@@ -190,56 +234,34 @@ Technique
 						
 						// TODO - Energy conservation term?
 
-						float3 lightContrib = getDirLightContibution(surfaceData, gLights[i]);
-						lightContribution += lightContrib * getSurfaceShading(V, L, specEnergy, surfaceData) * NoL;
+						float3 surfaceShading = getSurfaceShading(V, L, specEnergy, surfaceData);
+						float illuminance = lightData.luminance * NoL;
+						lightContribution += lightData.color * illuminance * surfaceShading;
 					}
 					
+					// Handle radial lights
                     for (uint i = lightOffsets.y; i < lightOffsets.z; ++i)
                     {
                         uint lightIdx = gLightIndices[i];
+						LightData lightData = gLights[lightIdx];
                         
-						float3 toLight = gLights[lightIdx].position - worldPos;
+						float3 toLight = lightData.position - worldPos;
 						float distToLightSqrd = dot(toLight, toLight);
 						float invDistToLight = rsqrt(distToLightSqrd);
 						
 						float3 L = toLight * invDistToLight;
-						float NoL = saturate(dot(N, L));
-						float specEnergy = 1.0f;
-						
-						// TODO - Need to return only attenuation (split into three method: dist, spot and radius attenuation)
-						float3 lightContrib = getPointLightContribution(toLight, surfaceData, gLights[lightIdx]);
+						float NoL = dot(N, L);
 						
+						float specEnergy = 1.0f;
+						float illuminance = 0.0f;
+
 						// Sphere area light. Calculate its contribution analytically by
 						// finding the most important (least error) point on the area light and
 						// use it as a form of importance sampling.
-						if(gLights[i].srcRadius > 0)
+						if(lightData.srcRadius > 0)
 						{
-							// Handle parts of the area light below the surface horizon
-							float radius2 = gLights[i].srcRadius * gLights[i].srcRadius;
-							float sinAlphaSqr = saturate(radius2 / distToLightSqrd);
-							float sinAlpha = sqrt(sinAlphaSqr);
-							
-							NoL = dot(N, L);
-							if(NoL < sinAlpha)
-							{
-							#if REFERENCE_QUALITY
-								float cosBeta = NoL;
-								float sinBeta = sqrt(1 - cosBeta * cosBeta);
-								float tanBeta = sinBeta / cosBeta;
-							
-								float x = sqrt(1 / sinAlphaSqr - 1);
-								float y = -x / tanBeta;
-								float z = sinBeta * sqrt(1 - y*y);
-
-								DistanceAttenuation = sinAlphaSqr * (NoL * acos(y) - x * z) + atan(z / x);
-								DistanceAttenuation /= PI * radius2;
-								NoL = 1;
-							#else
-								// Hermite spline approximation
-								NoL = max(NoL, -sinAlpha);
-								NoL = ((sinAlpha + NoL) * (sinAlpha + NoL)) / (4 * sinAlpha);
-							#endif
-							}		
+							// Calculate illuminance depending on source size, distance and angle
+							illuminance = illuminanceSphereAreaLight(NoL, distToLightSqrd, lightData);	
 
 							// Energy conservation:
 							//    We are widening the specular distribution by the sphere's subtended angle, 
@@ -247,7 +269,8 @@ Technique
 							//    for the sphere solid angle, since the energy difference is highly dependent on
 							//    specular distribution. By accounting for this energy difference we ensure glossy
 							//    reflections have sharp edges, instead of being too blurry.
-							float sphereAngle = saturate(gLights[i].srcRadius * invDistToLight);
+							//    See http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf for reference
+							float sphereAngle = saturate(lightData.srcRadius * invDistToLight);
 							
 							specEnergy = roughness2 / saturate(roughness2 + 0.5f * sphereAngle);
 							specEnergy *= specEnergy;							
@@ -256,28 +279,83 @@ Technique
 							float3 closestPointOnRay = dot(toLight, R) * R;
 							float3 centerToRay = closestPointOnRay - toLight;
 							float invDistToRay = rsqrt(dot(centerToRay, centerToRay));
-							float3 closestPointOnSphere = toLight + centerToRay * saturate(gLights[i].srcRadius * invDistToRay);
+							float3 closestPointOnSphere = toLight + centerToRay * saturate(lightData.srcRadius * invDistToRay);
 							
 							toLight = closestPointOnSphere;
 							L = normalize(toLight);
 						}
-
-						lightContribution += lightContrib * getSurfaceShading(V, L, specEnergy, surfaceData) * NoL;
+						else
+						{
+							NoL = saturate(NoL);
+							illuminance = illuminancePointLight(distToLightSqrd, NoL, lightData);
+						}
+						
+						float attenuation = getRadialAttenuation(distToLightSqrd, lightData);
+						float3 surfaceShading = getSurfaceShading(V, L, specEnergy, surfaceData);
+							
+						lightContribution += lightData.color * illuminance * attenuation * surfaceShading;
                     }
 
+					// Handle spot lights
 					for(uint i = lightOffsets.z; i < lightOffsets.w; ++i)
                     {
                         uint lightIdx = gLightIndices[i];
+						LightData lightData = gLights[lightIdx];
 						
-						float3 toLight = gLights[lightIdx].position - worldPos;
-                        
-						// TODO - Handle spot area light
+						float3 toLight = lightData.position - worldPos;
+						float distToLightSqrd = dot(toLight, toLight);
+						float invDistToLight = rsqrt(distToLightSqrd);
 						
-						float3 lightContrib = getSpotLightContribution(toLight, surfaceData, gLights[lightIdx]);
+						float3 L = toLight * invDistToLight;
+						float NoL = dot(N, L);
 						
-						float3 L = normalize(toLight);
-						float NoL = saturate(dot(N, L));
-						lightContribution += lightContrib * NoL;
+						float specEnergy = 1.0f;
+						float illuminance = 0.0f;
+						
+						float spotAttenuation = getSpotAttenuation(toLight, lightData.direction, lightData.spotAngles);
+						
+						// Disc area light. Calculate its contribution analytically by
+						// finding the most important (least error) point on the area light and
+						// use it as a form of importance sampling.
+						if(lightData.srcRadius > 0)
+						{
+							// Calculate illuminance depending on source size, distance and angle
+							NoL = illuminanceDiscAreaLight(NoL, distToLightSqrd, L, lightData);	
+						
+							// TODO - Using sphere energy conservation
+						
+							// Energy conservation:
+							//    We are widening the specular distribution by the sphere's subtended angle, 
+							//    so we need to handle the increase in energy. It is not enough just to account
+							//    for the sphere solid angle, since the energy difference is highly dependent on
+							//    specular distribution. By accounting for this energy difference we ensure glossy
+							//    reflections have sharp edges, instead of being too blurry.
+							//    See http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf for reference
+							float sphereAngle = saturate(lightData.srcRadius * invDistToLight);
+							
+							specEnergy = roughness2 / saturate(roughness2 + 0.5f * sphereAngle);
+							specEnergy *= specEnergy;							
+						
+							// Find closest point on sphere to ray
+							float3 closestPointOnRay = dot(toLight, R) * R;
+							float3 centerToRay = closestPointOnRay - toLight;
+							float invDistToRay = rsqrt(dot(centerToRay, centerToRay));
+							float3 closestPointOnSphere = toLight + centerToRay * saturate(lightData.srcRadius * invDistToRay);
+							
+							toLight = closestPointOnSphere;
+							L = normalize(toLight);
+						}
+						else
+						{
+							NoL = saturate(NoL);
+							illuminance = illuminancePointLight(distToLightSqrd, NoL, lightData);
+						}
+						
+						float radialAttenuation = getRadialAttenuation(distToLightSqrd, lightData);
+						float attenuation = spotAttenuation * radialAttenuation;
+						float3 surfaceShading = getSurfaceShading(V, L, specEnergy, surfaceData);
+							
+						lightContribution += lightData.color * illuminance * attenuation * surfaceShading;
                     }
 					
 					// Ambient term for in-editor visualization, not used in actual lighting