#ifndef BADDOG_AREA_LIGHTING_INCLUDED #define BADDOG_AREA_LIGHTING_INCLUDED #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/AreaLighting.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GlobalSamplers.hlsl" #include "Packages/com.baddog.rendering.arealight/Shaders/Include/PreIntegratedFGD.hlsl" #include "Packages/com.baddog.rendering.arealight/Shaders/Include/LTCAreaLight.hlsl" //----------------------------------------------------------------------------- // Constants //----------------------------------------------------------------------------- #define GPULIGHTTYPE_RECTANGLE (6) #define GPULIGHTTYPE_TUBE (5) //----------------------------------------------------------------------------- // PreLightData Structure (Area Light Related) //----------------------------------------------------------------------------- // Precomputed lighting data for area light evaluation // Reference: Based on HDRP's PreLightData structure, simplified for area lights only struct BGPreLightData { float NdotV; // Dot product between normal and view direction (could be negative due to normal mapping) // Pre-integrated FGD (Fresnel, Geometric, Diffuse) terms float3 specularFGD; // Pre-integrated specular FGD term float diffuseFGD; // Pre-integrated diffuse FGD term float reflectivity; // Pre-integrated reflectivity term (for multiscattering) float energyCompensation; // Energy compensation for multiscattering (1.0 / specularReflectivity - 1.0) // Area light LTC (Linearly Transformed Cosines) transforms float3x3 orthoBasisViewNormal; // Right-handed view-dependent orthogonal basis around the normal // Warning: These matrices are transposed! They are designed to transform row vectors via mul(V, M) float3x3 ltcTransformDiffuse; // Inverse LTC transformation matrix for diffuse BRDF (Lambertian or Disney Diffuse) float3x3 ltcTransformSpecular; // Inverse LTC transformation matrix for specular BRDF (GGX) }; //----------------------------------------------------------------------------- // Area Light Data Structure //----------------------------------------------------------------------------- // Area light data structure for URP // This structure contains the necessary information for evaluating area lights (rectangular and line/tube lights) // IMPORTANT: Must match C# BGAreaLightRenderingData layout exactly // Uses float4 instead of float3+padding to match C# Vector4 layout (avoids alignment issues) struct BGAreaLightData { float4 positionWS; // float4: xyz = position, w = 1 when this entry is an area light (matches C# Vector4 flag) float4 right; // float4: xyz = right vector, w = unused float4 up; // float4: xyz = up vector, w = unused float4 forward; // float4: xyz = forward vector, w = unused float4 size; // float4: xy = size (length, height), zw = unused float4 colorIntensity; // float4: rgb = color, w = intensity float4 rangeParams; // float4: x = range, y = rangeAttenuationScale, z = rangeAttenuationBias, w = isRectLight float4 renderingLayerMask; // float4: x = uint mask bits (reinterpreted from C#), yzw = unused }; //----------------------------------------------------------------------------- // Area Light Data Buffer //----------------------------------------------------------------------------- // Structured buffer containing area light data for additional lights (per-object order) // Entries without BGAreaLight component will have intensity = 0 StructuredBuffer _AreaLightDataBuffer; // Count of entries in the area light data buffer (matches additionalLightsCount) int _AreaLightDataBufferCount; //----------------------------------------------------------------------------- // Area Light Shadow Data //----------------------------------------------------------------------------- #include "Packages/com.baddog.rendering.arealight/Shaders/Include/BGAreaLightShadows.hlsl" #if UNITY_VERSION < 60000000 // This function assumes that inputs are well-behaved, e.i. // that the line does not pass through the origin and // that the light is (at least partially) above the surface. float I_diffuse_line(float3 C, float3 A, float hl) { // Solve C.z + h * A.z = 0. float h = -C.z * rcp(A.z); // May be Inf, but never NaN // Clip the line segment against the z-plane if necessary. float h2 = (A.z >= 0) ? max( hl, h) : min( hl, h); // P2 = C + h2 * A float h1 = (A.z >= 0) ? max(-hl, h) : min(-hl, h); // P1 = C + h1 * A // Normalize the tangent. float as = dot(A, A); // |A|^2 float ar = rsqrt(as); // 1/|A| float a = as * ar; // |A| float3 T = A * ar; // A/|A| // Orthogonal 2D coordinates: // P(n, t) = n * N + t * T. float tc = dot(T, C); // C = n * N + tc * T float3 P0 = C - tc * T; // P(n, 0) = n * N float ns = dot(P0, P0); // |P0|^2 float nr = rsqrt(ns); // 1/|P0| float n = ns * nr; // |P0| float Nz = P0.z * nr; // N.z = P0.z/|P0| // P(n, t) - C = P0 + t * T - P0 - tc * T // = (t - tc) * T = h * A = (h * a) * T. float t2 = tc + h2 * a; // P2.t float t1 = tc + h1 * a; // P1.t float s2 = ns + t2 * t2; // |P2|^2 float s1 = ns + t1 * t1; // |P1|^2 float mr = rsqrt(s1 * s2); // 1/(|P1|*|P2|) float r2 = s1 * (mr * mr); // 1/|P2|^2 float r1 = s2 * (mr * mr); // 1/|P1|^2 // I = (i1 + i2 + i3) / Pi. // i1 = N.z * (P2.t / |P2|^2 - P1.t / |P1|^2). // i2 = -T.z * (P2.n / |P2|^2 - P1.n / |P1|^2). // i3 = N.z * ArcCos[Dot[P1, P2] / (|P1| * |P2|)] / |P0|. float i12 = (Nz * t2 - (T.z * n)) * r2 - (Nz * t1 - (T.z * n)) * r1; // Guard against numerical errors. float dt = min(1, (ns + t1 * t2) * mr); float i3 = acos(dt) * (Nz * nr); // angle * cos(θ) / r^2 // Guard against numerical errors. return INV_PI * max(0, i12 + i3); } // A hack to smoothly limit the influence of the light to the interior of a pillow. // A "pillow" (for the lack of a better name) is formed by sweeping a ball across a rectangle. // This function behaves like CapsuleAttenuation() for a narrow rectangle. // This function behaves like SmoothWindowedDistanceAttenuation() for a small rectangle. // Convention: the surface point is located at the origin of the coordinate system. real PillowWindowing(real3 center, real3 xAxis, real3 yAxis, real halfLength, real halfHeight, real rangeAttenuationScale, real rangeAttenuationBias) { // Conceptually, the idea is very simple: after taking the symmetry // of the pillow into account, it is clear that the problem can be // reduced to finding the closest sphere inside the pillow. // We begin our search at the center of the pillow, and then translate // this point along and across the plane of symmetry until we either // a) find the closest point on the plane, or b) hit an edge of the rectangle. // The problem is simplified by working in the coordinate system of the pillow. real x = dot(center, xAxis); // -x, strictly speaking real dx = max(0, abs(x) - halfLength); real y = dot(center, yAxis); // -y, strictly speaking real dy = max(0, abs(y) - halfHeight); real r2 = dot(center, center); // r^2 real z2 = max(0, r2 - x * x - y * y); // z^2 real d2 = z2 + dx * dx + dy * dy; // Squared distance to the center of the closest sphere return SmoothDistanceWindowing(d2, rangeAttenuationScale, rangeAttenuationBias); } #endif //----------------------------------------------------------------------------- // Area Light Shadow Sampling (moved to BGAreaLightShadows.hlsl) //----------------------------------------------------------------------------- // Get area light data for a specific additional light index (per-object order) BGAreaLightData GetAreaLightData(uint lightIndex) { return _AreaLightDataBuffer[lightIndex]; } // Legacy helper for visible light indexing (kept for compatibility) BGAreaLightData GetAreaLightDataFromRealIndex(uint realLightIndex) { return _AreaLightDataBuffer[realLightIndex]; } //----------------------------------------------------------------------------- // Area Light Evaluation //----------------------------------------------------------------------------- // Evaluates area light using LTC approximation // isRectLight: true for rectangular light, false for line/tube light // center: Light center position in local coordinate system (shaded point at origin) // right: Right vector of the light in local coordinate system // up: Up vector of the light in local coordinate system // halfLength: Half-length of the light along the right axis // halfHeight: Half-height of the light along the up axis (0 for line lights) // invM: Inverse LTC transformation matrix (transposed) // perceptualRoughness: Material perceptual roughness [0, 1] // Returns: float4 where rgb = color (1,1,1), a = irradiance float4 EvaluateLTC_Area(bool isRectLight, float3 center, float3 right, float3 up, float halfLength, float halfHeight, float3x3 invM, float perceptualRoughness) { float3 ortho = cross(center, right); float orthoSq = dot(ortho, ortho); // Check whether the light is in a vertical orientation bool quit = (orthoSq == 0); // Check whether the light is entirely below the surface // We must test twice, since a linear transformation // may bring the light above the surface (a side-effect) quit = quit || (center.z + halfLength * abs(right.z) + halfHeight * abs(up.z) <= 0); float4 ltcValue = float4(1, 1, 1, 0); if (!quit) { // Perform sparse matrix multiplication float3 C = mul(invM, center); float3 A = mul(invM, right); float3 B = mul(invM, up); // Check whether the light is entirely below the surface after transformation if (C.z + halfLength * abs(A.z) + halfHeight * abs(B.z) > 0) { if (isRectLight) { // Transform the rectangular light vertices float4x3 lightVerts; lightVerts[0] = C - halfLength * A - halfHeight * B; // LL lightVerts[1] = lightVerts[0] + (2.0 * halfHeight) * B; // UL lightVerts[2] = lightVerts[1] + (2.0 * halfLength) * A; // UR lightVerts[3] = lightVerts[2] - (2.0 * halfHeight) * B; // LR // Compute polygon irradiance in the transformed configuration #if UNITY_VERSION >= 60000000 float3 formFactor; ltcValue.a = PolygonIrradiance(lightVerts, formFactor); #else ltcValue.a = PolygonIrradiance(lightVerts); #endif } else { // Line light evaluation #if UNITY_VERSION >= 60000000 float w = ComputeLineWidthFactor(invM, ortho, orthoSq); #else float w = ComputeLineWidthFactor(invM, ortho); #endif ltcValue.a = I_diffuse_line(C, A, halfLength) * w; } } } return ltcValue; } //----------------------------------------------------------------------------- // PreLightData Initialization //----------------------------------------------------------------------------- // Initialize PreLightData for area light evaluation BGPreLightData GetAreaLightPreLightData( float3 V, float3 N, float perceptualRoughness, float3 fresnel0, uint bsdfModelDiffuse, uint bsdfModelSpecular) { BGPreLightData preLightData; // Initialize all fields to zero preLightData.NdotV = 0.0; preLightData.specularFGD = float3(0, 0, 0); preLightData.diffuseFGD = 0.0; preLightData.reflectivity = 0.0; preLightData.energyCompensation = 0.0; preLightData.orthoBasisViewNormal = (float3x3)0; preLightData.ltcTransformDiffuse = (float3x3)0; preLightData.ltcTransformSpecular = (float3x3)0; // Compute NdotV preLightData.NdotV = dot(N, V); float clampedNdotV = ClampNdotV(preLightData.NdotV); // Compute pre-integrated FGD (Fresnel, Geometric, Diffuse) terms float specularReflectivity; #ifdef USE_DIFFUSE_LAMBERT_BRDF // Use Lambertian diffuse FGD (diffuseFGD = 1.0) GetPreIntegratedFGDGGXAndLambert( clampedNdotV, perceptualRoughness, fresnel0, preLightData.specularFGD, preLightData.diffuseFGD, preLightData.reflectivity ); specularReflectivity = preLightData.reflectivity; #else // Use Disney Diffuse FGD GetPreIntegratedFGDGGXAndDisneyDiffuse( clampedNdotV, perceptualRoughness, fresnel0, 1, preLightData.specularFGD, preLightData.diffuseFGD, preLightData.reflectivity ); specularReflectivity = preLightData.reflectivity; #endif // Compute energy compensation for multiscattering preLightData.energyCompensation = (specularReflectivity > 0.0) ? (1.0 / specularReflectivity - 1.0) : 0.0; // Build orthogonal basis around normal (view-dependent) preLightData.orthoBasisViewNormal = GetOrthoBasisViewNormal(V, N, preLightData.NdotV); // Sample LTC matrices for diffuse // HDRP style: Use compile-time macro to choose between Lambertian and other BRDF models #ifdef USE_DIFFUSE_LAMBERT_BRDF // Use identity matrix for Lambertian (no texture sampling) preLightData.ltcTransformDiffuse = k_identity3x3; #else // Sample LTC matrix for the specified BRDF model (e.g., Disney Diffuse) preLightData.ltcTransformDiffuse = SampleLtcMatrix(perceptualRoughness, clampedNdotV, bsdfModelDiffuse); #endif // Sample LTC matrix for specular preLightData.ltcTransformSpecular = SampleLtcMatrix(perceptualRoughness, clampedNdotV, bsdfModelSpecular); return preLightData; } // Area light lighting function compatible with URP's LightingPhysicallyBased style // This function can be used in URP's additional lights loop half3 LightingPhysicallyBasedAreaLight( BGAreaLightData areaLightData, BGPreLightData preLightData, float3 positionWS, half3 normalWS, half3 viewDirectionWS, half3 brdfDiffuse, half3 brdfSpecular, bool specularHighlightsOff) { // Step 1: Calculate light-to-surface vector and half dimensions float3 unL = areaLightData.positionWS.xyz - positionWS; float halfLength = areaLightData.size.x * 0.5; float halfHeight = areaLightData.size.y * 0.5; bool isRectLight = areaLightData.rangeParams.w > 0.5; // Step 2: Check if light is front-facing (for rectangular lights only) // Light forward points in the direction the light is facing (light emission direction) // If dot(forward, unL) < 0, the point is behind the light (not illuminated) if (isRectLight && dot(areaLightData.forward.xyz, unL) >= 0) return half3(0, 0, 0); // Step 3: Calculate intensity attenuation (matching HDRP implementation) // HDRP uses PillowWindowing for all area lights (rect and line) // For line lights, halfHeight = 0, so PillowWindowing degrades to CapsuleWindowing behavior float intensity = PillowWindowing(unL, areaLightData.right.xyz, areaLightData.up.xyz, halfLength, halfHeight, areaLightData.rangeParams.y, // rangeAttenuationScale areaLightData.rangeParams.z); // rangeAttenuationBias // Early exit if light is not visible if (intensity <= 0) return half3(0, 0, 0); // Step 4: Transform light vectors into local coordinate system (using pre-computed basis) float3 center = mul(preLightData.orthoBasisViewNormal, unL); float3 right = mul(preLightData.orthoBasisViewNormal, areaLightData.right.xyz); float3 up = mul(preLightData.orthoBasisViewNormal, areaLightData.up.xyz); // Step 5: Evaluate diffuse part (using pre-computed LTC matrix) float4 ltcDiffuse = EvaluateLTC_Area(isRectLight, center, right, up, halfLength, halfHeight, transpose(preLightData.ltcTransformDiffuse), 1.0); // Roughness = 1 for diffuse // Step 6: Evaluate specular part (using pre-computed LTC matrix) half3 lighting = half3(0, 0, 0); // Diffuse contribution (FGD from preLightData) lighting += brdfDiffuse * ltcDiffuse.a * preLightData.diffuseFGD; // Specular contribution (FGD from preLightData) half3 specularLighting = half3(0, 0, 0); #ifndef _SPECULARHIGHLIGHTS_OFF [branch] if (!specularHighlightsOff) { float4 ltcSpecular = EvaluateLTC_Area(isRectLight, center, right, up, halfLength, halfHeight, transpose(preLightData.ltcTransformSpecular), 1.0); // Roughness is handled by LTC matrix specularLighting = brdfSpecular * preLightData.specularFGD * ltcSpecular.a; // Apply energy compensation for multiscattering (matching HDRP) specularLighting *= (1.0 + preLightData.energyCompensation); } #endif // Combine diffuse and specular lighting += specularLighting; // Step 7: Apply intensity and light color lighting *= intensity * areaLightData.colorIntensity.rgb * areaLightData.colorIntensity.w; return lighting; } half3 LightingPhysicallyBasedAreaLight( BGAreaLightData areaLightData, BGPreLightData preLightData, float3 positionWS, half3 normalWS, half3 viewDirectionWS, half3 brdfDiffuse, half3 brdfSpecular) { return LightingPhysicallyBasedAreaLight(areaLightData, preLightData, positionWS, normalWS, viewDirectionWS, brdfDiffuse, brdfSpecular, false); } half3 LightingPhysicallyBasedAreaLightWithShadow( uint areaLightIndex, BGAreaLightData areaLightData, BGPreLightData preLightData, float3 positionWS, half3 normalWS, half3 viewDirectionWS, half3 brdfDiffuse, half3 brdfSpecular, bool specularHighlightsOff) { // Calculate base lighting (without shadow) half3 lighting = LightingPhysicallyBasedAreaLight( areaLightData, preLightData, positionWS, normalWS, viewDirectionWS, brdfDiffuse, brdfSpecular, specularHighlightsOff); // Apply shadow attenuation half shadowAttenuation = SampleAreaLightShadow(areaLightIndex, positionWS); lighting *= shadowAttenuation; return lighting; } half3 LightingPhysicallyBasedAreaLightWithShadow( uint areaLightIndex, BGAreaLightData areaLightData, BGPreLightData preLightData, float3 positionWS, half3 normalWS, half3 viewDirectionWS, half3 brdfDiffuse, half3 brdfSpecular) { return LightingPhysicallyBasedAreaLightWithShadow( areaLightIndex, areaLightData, preLightData, positionWS, normalWS, viewDirectionWS, brdfDiffuse, brdfSpecular, false); } half3 EvaluateAreaLight( float3 positionWS, half3 normalWS, half3 viewDirectionWS, half3 brdfDiffuse, half3 brdfSpecular, float perceptualRoughness, float3 fresnel0, bool specularHighlightsOff) { half3 totalLighting = half3(0, 0, 0); if (_AreaLightDataBufferCount <= 0) { return totalLighting; } uint bsdfModelDiffuse = LTCLIGHTINGMODEL_DISNEY_DIFFUSE; uint bsdfModelSpecular = LTCLIGHTINGMODEL_GGX; BGPreLightData preLightData = GetAreaLightPreLightData( viewDirectionWS, normalWS, perceptualRoughness, fresnel0, bsdfModelDiffuse, bsdfModelSpecular ); #ifdef _LIGHT_LAYERS uint meshRenderingLayers = GetMeshRenderingLayer(); #endif for (uint i = 0; i < (uint)_AreaLightDataBufferCount; ++i) { BGAreaLightData areaLightData = GetAreaLightData(i); #ifdef _LIGHT_LAYERS uint lightLayerMask = asuint(areaLightData.renderingLayerMask.x); if (!IsMatchingLightLayer(lightLayerMask, meshRenderingLayers)) { continue; } #endif half3 lightContribution = LightingPhysicallyBasedAreaLightWithShadow( i, // areaLightIndex areaLightData, preLightData, positionWS, normalWS, viewDirectionWS, brdfDiffuse, brdfSpecular, specularHighlightsOff); totalLighting += lightContribution; } return totalLighting; } half3 EvaluateAreaLight( float3 positionWS, half3 normalWS, half3 viewDirectionWS, half3 brdfDiffuse, half3 brdfSpecular, float perceptualRoughness, float3 fresnel0) { return EvaluateAreaLight( positionWS, normalWS, viewDirectionWS, brdfDiffuse, brdfSpecular, perceptualRoughness, fresnel0, false); } half3 EvaluateAreaLight(InputData inputData, SurfaceData surfaceData) { BRDFData brdfData; InitializeBRDFData(surfaceData, brdfData); float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surfaceData.smoothness); float3 fresnel0 = brdfData.specular; // Check if specular highlights are disabled bool specularHighlightsOff = false; #if defined(_SPECULARHIGHLIGHTS_OFF) specularHighlightsOff = true; #endif return EvaluateAreaLight( inputData.positionWS, inputData.normalWS, inputData.viewDirectionWS, brdfData.diffuse, brdfData.specular, perceptualRoughness, fresnel0, specularHighlightsOff); } #endif // BADDOG_AREA_LIGHTING_INCLUDED