diff --git a/com.microsoft.mrtk.graphicstools.unity/CHANGELOG.md b/com.microsoft.mrtk.graphicstools.unity/CHANGELOG.md index 0cbe4b47..d3d2d601 100644 --- a/com.microsoft.mrtk.graphicstools.unity/CHANGELOG.md +++ b/com.microsoft.mrtk.graphicstools.unity/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.8.4] - 2025-04-03 + +### Changed + +- Added more properties to the experimental AreaLight component. + ## [0.8.3] - 2025-03-26 ### Changed diff --git a/com.microsoft.mrtk.graphicstools.unity/Editor/Experimental/AreaLight/AreaLightInspector.cs b/com.microsoft.mrtk.graphicstools.unity/Editor/Experimental/AreaLight/AreaLightInspector.cs index 51f30afb..31a4a94a 100644 --- a/com.microsoft.mrtk.graphicstools.unity/Editor/Experimental/AreaLight/AreaLightInspector.cs +++ b/com.microsoft.mrtk.graphicstools.unity/Editor/Experimental/AreaLight/AreaLightInspector.cs @@ -41,7 +41,8 @@ private void OnSceneGUI() // Draw the area light's normal only if it will not overlap with the current tool. if (!((Tools.current == Tool.Move || Tools.current == Tool.Scale) && Tools.pivotRotation == PivotRotation.Local)) { - Handles.DrawLine(light.transform.position, light.transform.position + light.transform.forward); + var normal = light.transform.forward * ((light.Facing == AreaLight.ForwardFacing.PositiveZ) ? 1.0f : -1.0f); + Handles.DrawLine(light.transform.position, light.transform.position + normal); } Handles.color = new Color(255.0f / 255.0f, 165.0f / 255.0f, 0.0f / 255.0f); // Orange. diff --git a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLight.cs b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLight.cs index 85e6d0e0..6f86168f 100644 --- a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLight.cs +++ b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLight.cs @@ -21,20 +21,23 @@ public partial class AreaLight : BaseLight, IComparable private static readonly float[,] offsets = new float[4, 2] { { 1, 1 }, { 1, -1 }, { -1, -1 }, { -1, 1 } }; private const int lutResolution = 64; private const int lutMatrixDim = 3; + private static readonly Matrix4x4 rotation180Up = Matrix4x4.Rotate(Quaternion.AngleAxis(180.0f, Vector3.up)); private static Texture2D transformInvTextureSpecular; private static Texture2D transformInvTextureDiffuse; private static Texture2D ampDiffAmpSpecFresnel; + private static int lastAreaLightUpdate = -1; private static List activeAreaLights = new(maxAreaLights); private static List activeAreaLightsSorted = new(maxAreaLights); private static Vector4[] areaLightData = new Vector4[areaLightDataSize * areaLightCount]; private static Matrix4x4[] areaLightVerts = new Matrix4x4[areaLightCount]; private static Texture[] areaLightCookies = new Texture[areaLightCount]; - private static int _AreaLightDataID; - private static int _AreaLightVertsID; - private static int[] _AreaLightCookiesIDs = new int[areaLightCount]; - private static int lastAreaLightUpdate = -1; + private static int areaLightDataID; + private static int areaLightVertsID; + private static int[] areaLightCookiesIDs = new int[areaLightCount]; + private static int facingID; + private static int uvStartsAtTopID; private static CullingGroup cullingGroup; private static BoundingSphere[] boundingSpheres = new BoundingSphere[maxAreaLights]; @@ -89,6 +92,25 @@ public Vector2 Size set => size = value; } + public enum ForwardFacing + { + PositiveZ, + NegativeZ, + } + + [Tooltip("The forward direction of the light.")] + [SerializeField] + private ForwardFacing facing = ForwardFacing.PositiveZ; + + /// + /// The forward direction of the light. + /// + public ForwardFacing Facing + { + get => facing; + set => facing = value; + } + [Tooltip("Optional texture to use instead of a solid color.")] [SerializeField] private Texture cookie; @@ -102,6 +124,19 @@ public Texture Cookie set => cookie = value; } + [Tooltip("Should the texture UV coordinate convention for this cookie have Y starting at the top of the image.")] + [SerializeField] + private bool cookieUVStartsAtTop = true; + + /// + /// Should the texture UV coordinate convention for this cookie have Y starting at the top of the image. + /// + public bool CookieUVStartsAtTop + { + get => cookieUVStartsAtTop; + set => cookieUVStartsAtTop = value; + } + [Tooltip("Should the area light have a visualization?")] [SerializeField] private bool drawLightSource = true; @@ -215,14 +250,17 @@ public Camera CullingGroupCamera /// protected override void Initialize() { - _AreaLightDataID = Shader.PropertyToID("_AreaLightData"); - _AreaLightVertsID = Shader.PropertyToID("_AreaLightVerts"); + areaLightDataID = Shader.PropertyToID("_AreaLightData"); + areaLightVertsID = Shader.PropertyToID("_AreaLightVerts"); - for (int i = 0; i < _AreaLightCookiesIDs.Length; ++i) + for (int i = 0; i < areaLightCookiesIDs.Length; ++i) { - _AreaLightCookiesIDs[i] = Shader.PropertyToID($"_AreaLightCookie{i}"); + areaLightCookiesIDs[i] = Shader.PropertyToID($"_AreaLightCookie{i}"); } + facingID = Shader.PropertyToID("_facing"); + uvStartsAtTopID = Shader.PropertyToID("_uvStartsAtTop"); + CreateLUTs(); UpdateLightSourceVisual(); @@ -362,18 +400,23 @@ protected override void UpdateLights(bool forceUpdate = false) if (light) { - areaLightData[dataIndex] = light.Color; + var color = light.Color; + areaLightData[dataIndex] = new Vector4(color.r, + color.g, + color.b, + light.cookieUVStartsAtTop ? 1.0f : 0.0f); + + var lightVerts = new Matrix4x4(); + var localToWorld = light.transform.localToWorldMatrix; - // A little bit of bias to prevent the light from lighting itself. - const float z = 0.01f; + if (light.facing == ForwardFacing.NegativeZ) + { + localToWorld *= rotation180Up; + } - Matrix4x4 lightVerts = new Matrix4x4(); for (int v = 0; v < 4; ++v) { - Vector3 vertex = new Vector3(light.size.x * offsets[v, 0], - light.size.y * offsets[v, 1], - z) * 0.5f; - lightVerts.SetRow(v, light.transform.TransformPoint(vertex)); + lightVerts.SetRow(v, TransformVertex(v, light.size, localToWorld)); } areaLightVerts[i] = lightVerts; @@ -397,13 +440,13 @@ protected override void UpdateLights(bool forceUpdate = false) } } - Shader.SetGlobalVectorArray(_AreaLightDataID, areaLightData); - Shader.SetGlobalMatrixArray(_AreaLightVertsID, areaLightVerts); + Shader.SetGlobalVectorArray(areaLightDataID, areaLightData); + Shader.SetGlobalMatrixArray(areaLightVertsID, areaLightVerts); // There is no SetGlobalTextureArray so pass in 1 by 1. for (int i = 0; i < areaLightCookies.Length; ++i) { - Shader.SetGlobalTexture(_AreaLightCookiesIDs[i], areaLightCookies[i]); + Shader.SetGlobalTexture(areaLightCookiesIDs[i], areaLightCookies[i]); } lastAreaLightUpdate = Time.frameCount; @@ -467,6 +510,18 @@ private void OnDrawGizmos() } #endif + private static Vector3 TransformVertex(int index, Vector2 size, Matrix4x4 localToWorld) + { + // A little bit of bias to prevent the light from lighting itself. + const float z = 0.01f; + + var vertex = new Vector3(size.x * offsets[index, 0], + size.y * offsets[index, 1], + z) * 0.5f; + + return localToWorld.MultiplyPoint(vertex); + } + private static void CreateLUTs() { if (transformInvTextureDiffuse == null) @@ -506,6 +561,8 @@ private void UpdateLightSourceVisual() lightSourceVisual.sharedMaterial.color = Color; lightSourceVisual.sharedMaterial.mainTexture = drawLightSourceCookie ? drawLightSourceCookie : cookie; + lightSourceVisual.sharedMaterial.SetFloat(facingID, (float)facing); + lightSourceVisual.sharedMaterial.SetFloat(uvStartsAtTopID, cookieUVStartsAtTop ? 0.0f : 1.0f); lightSourceVisual.transform.localScale = new Vector3(size.x, size.y, 1.0f); } else diff --git a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLightCookieFilter.cs b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLightCookieFilter.cs index c1532e48..063de233 100644 --- a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLightCookieFilter.cs +++ b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/AreaLightCookieFilter.cs @@ -59,7 +59,7 @@ public Material CookieFilterMaterial [Tooltip("How many blur passes to perform during Dual blurring.")] [SerializeField] - [Range(2, 7)] + [Range(0, 7)] private int blurPasses = 3; /// @@ -68,7 +68,7 @@ public Material CookieFilterMaterial public int BlurPasses { get => blurPasses; - set => blurPasses = Mathf.Clamp(value, 2, 7); + set => blurPasses = Mathf.Clamp(value, 0, 7); } /// diff --git a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLight.hlsl b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLight.hlsl index 6a954f05..d7baef2f 100644 --- a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLight.hlsl +++ b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLight.hlsl @@ -10,15 +10,15 @@ #define AREA_LIGHT_COUNT 2 #define AREA_LIGHT_DATA_SIZE 1 -#define AREA_LIGHT_ENABLE_DIFFUSE 0 +//#define AREA_LIGHT_ENABLE_DIFFUSE /// /// Global properties. /// -#if AREA_LIGHT_ENABLE_DIFFUSE +#if defined(AREA_LIGHT_ENABLE_DIFFUSE) sampler2D _TransformInvDiffuse; -#endif +#endif // AREA_LIGHT_ENABLE_DIFFUSE sampler2D _TransformInvSpecular; sampler2D _AmpDiffAmpSpecFresnel; @@ -33,19 +33,81 @@ float4x4 _AreaLightVerts[AREA_LIGHT_COUNT]; /// Lighting methods. /// -float IntegrateEdge(in float3 v1, in float3 v2) +bool RayPlaneIntersect(float3 direction, float3 origin, float4 plane, out float t) { - float cosTheta = dot(v1, v2); - float theta = acos(cosTheta); - float cross = (v1.x * v2.y - v1.y * v2.x); - return cross * ((theta > 0.001) ? theta / sin(theta) : 1.0); + t = -dot(plane, float4(origin, 1.0)) / dot(plane.xyz, direction); + return t > 0.0; } - -float PolygonRadiance(in float4x3 L) + +half4 SampleAreaLightCookie(in int lightIndex, in float2 uv) +{ +#if defined(_AREA_LIGHTS_ACTIVE) + [forcecase] + switch (lightIndex) + { + case 0: + return tex2D(_AreaLightCookie0, uv); + case 1: + return tex2D(_AreaLightCookie1, uv); + default: + return half4(1, 1, 1, 1); + } +#else + return tex2D(_AreaLightCookie0, uv); +#endif // _AREA_LIGHTS_ACTIVE +} + +half3 SampleDiffuseFilteredTexture(in int lightIndex, in float4x3 L, float3 direction) +{ + float3 p1 = L[0]; + float3 p2 = L[1]; + float3 p3 = L[2]; + float3 p4 = L[3]; + + // Area light plane basis. + float3 V1 = p2 - p1; + float3 V2 = p4 - p1; + float3 planeOrtho = cross(V1, V2); + float planeAreaSquared = dot(planeOrtho, planeOrtho); + + float4 plane = float4(planeOrtho, -dot(planeOrtho, p1)); + float planeDist; + RayPlaneIntersect(direction, 0, plane, planeDist); + + float3 P = planeDist * direction - p1; + + // Find tex coords of P. + float dot_V1_V2 = dot(V1, V2); + float inv_dot_V1_V1 = 1.0 / dot(V1, V1); + float3 V2_ = V2 - V1 * dot_V1_V2 * inv_dot_V1_V1; + float2 uv; + uv.x = dot(V2_, P) / dot(V2_, V2_); + uv.y = abs(_AreaLightData[lightIndex].a - (dot(V1, P) * inv_dot_V1_V1 - dot_V1_V2 * inv_dot_V1_V1 * uv.x)); + + return SampleAreaLightCookie(lightIndex, uv).rgb; +} + +float3 IntegrateEdge(in float3 v1, in float3 v2) +{ + float x = dot(v1, v2); + float y = abs(x); + + float a = 0.8543985 + (0.4965155 + 0.0145206 * y) * y; + float b = 3.4175940 + (4.1616724 + y) * y; + float v = a / b; + + float theta_sintheta = (x > 0.0) ? v : 0.5 * rsqrt(max(1.0 - x * x, 1e-7)) - v; + + return cross(v1, v2) * theta_sintheta; +} + +float4 PolygonRadiance(in int lightIndex, in float4x3 L) { // Baum's equation // Expects non-normalized vertex positions + float4x3 unclippedL = L; + // Detect clipping config. uint config = 0; if (L[0].z > 0) { config += 1; } @@ -180,9 +242,9 @@ float PolygonRadiance(in float4x3 L) L4 = normalize(L4); } } - + // Integrate. - float sum = 0; + float3 sum = 0; sum += IntegrateEdge(L[0], L[1]); sum += IntegrateEdge(L[1], L[2]); sum += IntegrateEdge(L[2], L[3]); @@ -196,13 +258,16 @@ float PolygonRadiance(in float4x3 L) { sum += IntegrateEdge(L4, L[0]); } - - sum *= 0.15915; // 1/2pi - - return max(0, sum); + + float3 direction = normalize(sum); + return float4(max(0, sum.z * 0.15915) * SampleDiffuseFilteredTexture(lightIndex, unclippedL, direction), direction.z); } - -float TransformedPolygonRadiance(in float4x3 L, in float2 uv, in sampler2D transformInv, in float amplitude) + +half4 TransformedPolygonRadiance(in int lightIndex, + in float4x3 L, + in float2 uv, + in sampler2D transformInv, + in float amplitude) { // Get the inverse LTC matrix M. float3x3 Minv = 0; @@ -213,59 +278,7 @@ float TransformedPolygonRadiance(in float4x3 L, in float2 uv, in sampler2D trans float4x3 LTransformed = mul(L, Minv); // Polygon radiance in transformed configuration - specular. - return PolygonRadiance(LTransformed) * amplitude; -} - -half4 SampleAreaLightCookie(in int lightIndex, in float2 uv) -{ -#if defined(_AREA_LIGHTS_ACTIVE) - [forcecase] - switch (lightIndex) - { - case 0: - return tex2D(_AreaLightCookie0, uv); - case 1: - return tex2D(_AreaLightCookie1, uv); - default: - return half4(1, 1, 1, 1); - } -#else - return tex2D(_AreaLightCookie0, uv); -#endif // _AREA_LIGHTS_ACTIVE -} - -half3 SampleDiffuseFilteredTexture(in int lightIndex, in float4x3 L) -{ - float3 p1_ = L[0]; - float3 p2_ = L[1]; - float3 p3_ = L[2]; - float3 p4_ = L[3]; - - // Area light plane basis. - float3 V1 = p2_ - p1_; - float3 V2 = p4_ - p1_; - float3 planeOrtho = (cross(V1, V2)); - float planeAreaSquared = dot(planeOrtho, planeOrtho); - float planeDistxPlaneArea = dot(planeOrtho, p1_); - - // Orthonormal projection of (0,0,0) in area light space. - float3 P = planeDistxPlaneArea * planeOrtho / planeAreaSquared - p1_; - - // Find tex coords of P. - float dot_V1_V2 = dot(V1, V2); - float inv_dot_V1_V1 = 1.0 / dot(V1, V1); - float3 V2_ = V2 - V1 * dot_V1_V2 * inv_dot_V1_V1; - float2 Puv; - Puv.x = dot(V2_, P) / dot(V2_, V2_); - Puv.y = 1 - (dot(V1, P) * inv_dot_V1_V1 - dot_V1_V2 * inv_dot_V1_V1 * Puv.x); - float2 uv = float2(0.125, 0.125) + 0.75 * Puv; - - // TODO - [Cameron-Micka] calculate mip level based on distance to area light if the texture has pre-filtered mip levels. - //float d = abs(planeDistxPlaneArea) / pow(planeAreaSquared, 0.75); - //float w = log(1024.0 * d) / log(6.0); // TODO get texture size. - //return tex2Dlod(texLightFiltered, float4(uv.x, uv.y, 0, w)).rgb; - - return SampleAreaLightCookie(lightIndex, uv).rgb; + return PolygonRadiance(lightIndex, LTransformed) * float4(amplitude.xxx, 1); } void CalculateAreaLight(in float3 worldPosition, @@ -292,9 +305,6 @@ void CalculateAreaLight(in float3 worldPosition, float4x3 L; L = (float4x3)_AreaLightVerts[lightIndex] - float4x3(worldPosition, worldPosition, worldPosition, worldPosition); L = mul(L, transpose(basis)); - - // TODO - [Cameron-Micka] disable if no texture? - half3 textureColor = SampleDiffuseFilteredTexture(lightIndex, L); // UVs for sampling the LUTs. float theta = acos(dot(V, worldNormal)); @@ -303,16 +313,16 @@ void CalculateAreaLight(in float3 worldPosition, half3 AmpDiffAmpSpecFresnel = tex2D(_AmpDiffAmpSpecFresnel, uv).rgb; half3 result = 0; -#if AREA_LIGHT_ENABLE_DIFFUSE - half diffuseTerm = TransformedPolygonRadiance(L, uv, _TransformInvDiffuse, AmpDiffAmpSpecFresnel.x); +#if defined(AREA_LIGHT_ENABLE_DIFFUSE) + half3 diffuseTerm = TransformedPolygonRadiance(lightIndex, L, uv, _TransformInvDiffuse, AmpDiffAmpSpecFresnel.x); result = diffuseTerm * baseColor; -#endif +#endif // AREA_LIGHT_ENABLE_DIFFUSE - half specularTerm = TransformedPolygonRadiance(L, uv, _TransformInvSpecular, AmpDiffAmpSpecFresnel.y); - half fresnelTerm = (half) (specularColor + (1.0 - specularColor) * AmpDiffAmpSpecFresnel.z); + half3 specularTerm = TransformedPolygonRadiance(lightIndex, L, uv, _TransformInvSpecular, AmpDiffAmpSpecFresnel.y); + half3 fresnelTerm = (half) (specularColor + (1.0 - specularColor) * AmpDiffAmpSpecFresnel.z); result += specularTerm * fresnelTerm * 3.14159265359; // Pi. - output = result * _AreaLightData[lightIndex].rgb * textureColor; + output = result * _AreaLightData[lightIndex].rgb; } /// @@ -360,12 +370,12 @@ void CalculateAreaLights(in float3 worldPosition, /// Entry point, call this from Shader Graph (half precision). /// void CalculateAreaLights_half(in float3 worldPosition, - in float3 worldCameraPosition, - in float3 worldNormal, - in half3 baseColor, - in half3 specularColor, - in half smoothness, - out half3 output) + in float3 worldCameraPosition, + in float3 worldNormal, + in half3 baseColor, + in half3 specularColor, + in half smoothness, + out half3 output) { CalculateAreaLights(worldPosition, worldCameraPosition, diff --git a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLightVisualize.shader b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLightVisualize.shader index 3eea9e10..2cab42b4 100644 --- a/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLightVisualize.shader +++ b/com.microsoft.mrtk.graphicstools.unity/Runtime/Experimental/AreaLight/Shaders/AreaLightVisualize.shader @@ -41,6 +41,8 @@ Shader "Hidden/Graphics Tools/Experimental/Area Light Visualize" CBUFFER_START(UnityPerMaterial) fixed4 _Color; float4 _MainTex_ST; + float _facing; + float _uvStartsAtTop; CBUFFER_END v2f vert (appdata_t input) @@ -55,7 +57,11 @@ Shader "Hidden/Graphics Tools/Experimental/Area Light Visualize" fixed4 frag (v2f input, bool facing : SV_IsFrontFace) : SV_Target { - fixed4 output = facing ? tex2D(_MainTex, input.texcoord) : fixed4(0.05, 0.05, 0.05, 1.0); + facing = _facing ? !facing : facing; + float2 texcoord = input.texcoord; + texcoord.x = abs(_facing - texcoord.x); + texcoord.y = abs(_uvStartsAtTop - texcoord.y); + fixed4 output = facing ? tex2D(_MainTex, texcoord) : fixed4(0.05, 0.05, 0.05, 1.0); UNITY_OPAQUE_ALPHA(output.a); return output * _Color; } diff --git a/com.microsoft.mrtk.graphicstools.unity/package.json b/com.microsoft.mrtk.graphicstools.unity/package.json index 6629e114..ef957935 100644 --- a/com.microsoft.mrtk.graphicstools.unity/package.json +++ b/com.microsoft.mrtk.graphicstools.unity/package.json @@ -1,6 +1,6 @@ { "name": "com.microsoft.mrtk.graphicstools.unity", - "version": "0.8.3", + "version": "0.8.4", "displayName": "MRTK Graphics Tools", "description": "Graphics tools and components for developing Mixed Reality applications in Unity.", "documentationUrl": "https://aka.ms/mrtk3graphics",