From 21cf2b51053f8f91eac1e6dc959508f3b635c385 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 13:34:44 +0900 Subject: [PATCH 1/8] test: add OpenLit Toon lighting contracts - Add OpenLit 1.0.2 direction, two-band SH, Gamma, and ForwardAdd diagnostic contracts. - Validate pure reference behavior and record expected RED results against the current Toon implementation. --- Tests/Config/shader-core-test-hosts.json | 42 ++ ...eringModeRenderingTests.SourceContracts.cs | 84 ++- ...reBaseToonLightingContractTests.OpenLit.cs | 496 ++++++++++++++++++ ...eToonLightingContractTests.OpenLit.cs.meta | 11 + ...reBaseToonLightingContractTests.Runtime.cs | 70 ++- ...ureBaseToonLightingContractTests.Shadow.cs | 9 +- .../PureBaseToonLightingContractTests.cs | 234 --------- .../Editor/ShaderCoreTestHostManifestTests.cs | 205 +++++++- Tests/Fixtures/Hosts/ToonOpenLit.meta | 8 + .../PureBaseTestToonOpenLitGamma.scshader | 98 ++++ ...PureBaseTestToonOpenLitGamma.scshader.meta | 10 + ...reBaseTestToonOpenLitGamma_properties.hlsl | 19 + ...eTestToonOpenLitGamma_properties.hlsl.meta | 7 + ...tests.shadercore.toonopenlitgamma.scmodule | 4 + ....shadercore.toonopenlitgamma.scmodule.meta | 7 + .../Hosts/ToonOpenLit/phase_shade.hlsl | 26 + .../Hosts/ToonOpenLit/phase_shade.hlsl.meta | 7 + .../Fixtures/Hosts/ToonOpenLit/sc_common.hlsl | 28 + .../Hosts/ToonOpenLit/sc_common.hlsl.meta | 7 + 19 files changed, 1066 insertions(+), 306 deletions(-) create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs.meta create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit.meta create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader.meta create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl.meta create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule.meta create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl.meta create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl create mode 100644 Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl.meta diff --git a/Tests/Config/shader-core-test-hosts.json b/Tests/Config/shader-core-test-hosts.json index bdcb6959..af954a48 100644 --- a/Tests/Config/shader-core-test-hosts.json +++ b/Tests/Config/shader-core-test-hosts.json @@ -346,6 +346,48 @@ "requireChannelAgreement": true } }, + { + "shaderName": "PureBase/Tests/ShaderCore/ToonOpenLitGamma", + "moduleUniqueId": "jp.penguin.purebase.tests.shadercore.toonopenlitgamma", + "expectedSentinels": [ + "PUREBASE_TEST_TOON_OPENLIT_GAMMA_SENTINEL_SHADE" + ], + "inactiveSentinels": [ + "PUREBASE_TEST_TOON_SHADOW_SENTINEL_LIGHT", + "PUREBASE_TEST_TOON_SHADOW_SENTINEL_MODIFYLIGHT", + "PUREBASE_TEST_TOON_SHADOW_SENTINEL_SHADE", + "PUREBASE_TEST_PHASE_SENTINEL_MORPH", + "PUREBASE_TEST_PHASE_SENTINEL_POSTVERTEX", + "PUREBASE_TEST_PHASE_SENTINEL_BASE", + "PUREBASE_TEST_PHASE_SENTINEL_LIGHT", + "PUREBASE_TEST_PHASE_SENTINEL_CUSTOMLIGHT", + "PUREBASE_TEST_PHASE_SENTINEL_MODIFYLIGHT", + "PUREBASE_TEST_PHASE_SENTINEL_SHADE", + "PUREBASE_TEST_PHASE_SENTINEL_REFLECTION", + "PUREBASE_TEST_PHASE_SENTINEL_ADD", + "PUREBASE_TEST_PHASE_SENTINEL_POSTPIXEL" + ], + "expectedPassSentinelCounts": { + "ForwardBase": 1, + "ForwardAdd": 1, + "ShadowCaster": 0, + "Meta": 0 + }, + "openLitRuntimeEvidence": { + "gammaMacro": "UNITY_COLORSPACE_GAMMA", + "productModelInclude": "Packages/jp.penguin.purebase/Shaders/Models/toon.hlsl", + "forwardBasePass": "ForwardBase", + "forwardAddPass": "ForwardAdd", + "absentPasses": [ + "ShadowCaster", + "Meta" + ], + "forwardAddSentinel": "PUREBASE_TEST_TOON_OPENLIT_GAMMA_SENTINEL_SHADE", + "injectedShSetCount": 2, + "requireGammaReadback": true, + "requireDirectOnlyForwardAdd": true + } + }, { "shaderName": "PureBase/Tests/ShaderCore/ModuleOrder", "moduleUniqueIds": [ diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs index fa81bf7b..71cf72af 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs @@ -188,6 +188,14 @@ public void ToonLightingOwnershipKeepsBinaryDirectTwoBandShaderCoreLightmapsAndF AssertBirpHostForwardAddAndLightmapContracts(host, shaderCoreLighting); AssertPbrAndHybridLightingOwnership(pbr, pbrBrdf, hybrid); AssertLightingPhaseOrder(host); + AssertOpenLitFallbackPrecedesNormalization(helper); + } + + /// Requires the Toon direction fallback to affect nondegenerate aggregates before normalization. + [Test] + public void ToonOpenLitFallbackIsAddedBeforeDirectionNormalization() + { + AssertOpenLitFallbackPrecedesNormalization(File.ReadAllText(ToonLightingHelperPath)); } /// Requires Toon to separate non-shadow attenuation from Unity effective visibility before Shader-Core light phases. @@ -481,11 +489,7 @@ private static void AssertToonHelperAndModelContracts(string toon, string helper helper, "The Toon helper must own fixed bright and dark environment band interpretation." ); - StringAssert.Contains( - "return lerp(dark, bright, step(0, dot(surfaceNormal, L)));", - helper, - "The Toon helper must select the environment from fixed bright and dark bands." - ); + AssertOpenLitToonEquationContracts(helper); StringAssert.Contains( "#include \"Packages/jp.penguin.purebase/Shaders/Common/toon_lighting.hlsl\"", toon, @@ -504,6 +508,7 @@ private static void AssertBirpHostForwardAddAndLightmapContracts(string host, st StringAssert.Contains("env = SCModelSelectEnvironmentLighting(env);", host); StringAssert.Contains("sd.lightColor = lightSum.color + env;", host); StringAssert.Contains("sd.lightColor = lightSum.color;", host); + AssertOpenLitHostGateContracts(host); Assert.That( RequireIndex(host, "#if defined(UNITY_PASS_FORWARDADD)"), Is.LessThan(RequireIndex(host, "env = SCModelSelectEnvironmentLighting(env);")), @@ -556,49 +561,42 @@ private static void AssertLightingPhaseOrder(string host) Assert.That(add, Is.LessThan(postPixel)); } - /// Requires the Toon-only dominant-direction and bright/dark SH helper without changing PBR or Hybrid ownership. - [Test] - public void ToonLightingRequiresFixedTwoBandHelperFallbackAndModelCallbackSeparation() + /// Requires the OpenLit-derived Toon equation and rejects the superseded scaled or inverted SH approximation. + /// The Toon-only lighting helper source. + private static void AssertOpenLitToonEquationContracts(string helper) + { + StringAssert.Contains("float3(0.22, 0.707, 0.071)", helper); + StringAssert.Contains("float3(0.0396819152, 0.458021790, 0.00609653955)", helper); + StringAssert.Contains("float3(0.001, 0.002, 0.001)", helper); + StringAssert.Contains("UNITY_COLORSPACE_GAMMA", helper); + Assert.That(Regex.IsMatch(helper, @"normalize\s*\(\s*shAr\.xyz\s*\+\s*shAg\.xyz\s*\+\s*shAb\.xyz\s*\)"), Is.True, "The dark L1 direction must derive from the summed SH coefficients."); + Assert.That(Regex.IsMatch(helper, @"float3\s+E\s*=\s*L\s*\*\s*0\.666666"), Is.False, "OpenLit bright L0/L2 and L1 must use unscaled V."); + Assert.That(Regex.IsMatch(helper, @"base\s*-\s*linearTerm"), Is.False, "OpenLit dark L1 must not invert the bright L1 term."); + Assert.That(Regex.IsMatch(helper, @"\bsd\.shadow\b"), Is.False, "Toon direction and SH evaluation must remain visibility-independent."); + } + + /// Requires the fixed fallback to enter the Toon direction sum before normalization. + /// The Toon-only lighting helper source. + private static void AssertOpenLitFallbackPrecedesNormalization(string helper) { - string toon = File.ReadAllText(ToonModelPath); Assert.That( - File.Exists(ToonLightingHelperPath), + Regex.IsMatch( + helper, + @"float3\s+directionVector\s*=\s*directAggregateDirection\s*\+\s*float3\s*\(\s*shDirection\.x\s*,\s*abs\s*\(\s*shDirection\.y\s*\)\s*,\s*shDirection\.z\s*\)\s*\+\s*float3\s*\(\s*0\.001\s*,\s*0\.002\s*,\s*0\.001\s*\)\s*;[\s\S]*?return\s+normalize\s*\(\s*directionVector\s*\)\s*;", + RegexOptions.Singleline + ), Is.True, - "The Toon-only dominant-direction and two-band SH helper must exist." + "Toon direction must add the fixed fallback to the direct and SH direction sum before normalization." ); - string helper = File.ReadAllText(ToonLightingHelperPath); - string host = File.ReadAllText(BirpHostPath); - string pbrBrdf = File.ReadAllText(PbrBrdfPath); + } - StringAssert.Contains( - "#include \"Packages/jp.penguin.purebase/Shaders/Common/toon_lighting.hlsl\"", - toon, - "The two-band SH helper must be included only by the Toon model." - ); - StringAssert.Contains("SCModelEvaluateDirectFactor", toon); - StringAssert.Contains("SCModelEvaluateAmbient", toon); - StringAssert.Contains("SCModelSelectEnvironmentLighting", toon); - StringAssert.Contains( - "float3 shDirection = (shAr.xyz + shAg.xyz + shAb.xyz) / 3", - helper - ); - StringAssert.Contains( - "float3(shDirection.x, abs(shDirection.y), shDirection.z)", - helper - ); - StringAssert.Contains("<= 0.000001", helper); - StringAssert.Contains("float3(0.001, 0.002, 0.001)", helper); - StringAssert.Contains("E = L * 0.666666", helper); - StringAssert.Contains("base + linear", helper); - StringAssert.Contains("base - linear", helper); - StringAssert.Contains("step(0, dot(surfaceNormal, L))", helper); - StringAssert.DoesNotContain("toon_lighting.hlsl", host); - StringAssert.DoesNotContain("toon_lighting.hlsl", pbrBrdf); - StringAssert.Contains( - "half diffuseNdotL = binaryDiffuse ? step(0.0, signedNdotL) : NdotL;", - pbrBrdf, - "The Toon helper must not replace Hybrid's inline binary direct-diffuse branch." - ); + /// Requires Toon-only SH gates and direct-only ForwardAdd direction publication in the shared host. + /// The common BIRP fragment host source. + private static void AssertOpenLitHostGateContracts(string host) + { + Assert.That(Regex.IsMatch(host, @"#if\s+defined\(PUREBASE_TOON_MODEL_INCLUDED\)\s*&&\s*!defined\(LIGHTMAP_ON\)"), Is.True, "Toon SH must remain disabled while Shader-Core owns lightmaps."); + Assert.That(Regex.IsMatch(host, @"#if[^\r\n]*\bUNITY_SHOULD_SAMPLE_SH\b"), Is.True, "Toon SH must obey Unity's SH sampling gate."); + Assert.That(Regex.IsMatch(host, @"#if\s+defined\(UNITY_PASS_FORWARDADD\)[\s\S]*?sd\.L\s*=\s*dot\(lightSum\.direction\s*,\s*lightSum\.direction\)\s*>\s*0\.000001\s*\?\s*normalize\(lightSum\.direction\)\s*:\s*(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)"), Is.True, "ForwardAdd must publish normalized direct direction or zero without SH fallback."); } /// Requires pass-bounded Stencil policy while preserving the existing pass and rendering-mode keyword ABI. diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs new file mode 100644 index 00000000..76275edd --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs @@ -0,0 +1,496 @@ +/* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Defines pure OpenLit 1.0.2 numerical contracts for Toon dominant direction and two-band SH evaluation. + +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +namespace PureBase.Tests.Daily +{ + /// Defines pure OpenLit 1.0.2 numerical contracts for Toon dominant direction and two-band SH evaluation. + [SuppressMessage("SonarAnalyzer.CSharp", "S2333", Justification = "This declaration remains partial so the pure OpenLit oracle stays separate from capture and shadow fixtures.")] + public sealed partial class PureBaseToonLightingContractTests + { + /// Identifies the fixed forced-Gamma Toon OpenLit diagnostic host. + private const string ToonOpenLitGammaShaderName = + "PureBase/Tests/ShaderCore/ToonOpenLitGamma"; + + /// Identifies the selected forced-Gamma Toon OpenLit diagnostic module. + private const string ToonOpenLitGammaModuleId = + "jp.penguin.purebase.tests.shadercore.toonopenlitgamma"; + + /// Identifies the forced-Gamma Toon OpenLit Shader-Core host asset. + private const string ToonOpenLitGammaHostAssetPath = + "Packages/jp.penguin.purebase/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader"; + + /// Requires OpenLit direction weighting to use color-space luminance, positive-Y SH, and an unconditional fallback. + [Test] + public void OpenLitDirectionUsesColorSpaceLuminanceShAndFallback() + { + Color directColor = new Color(0.7f, 0.2f, 0.1f, 1.0f); + Vector3 directDirection = Vector3.right; + Vector4 shAr = new Vector4(0.0f, -0.15f, 0.0f, 0.0f); + Vector4 shAg = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); + Vector4 shAb = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); + float gammaLuminance = EvaluateOpenLitLuminance(directColor, true); + float linearLuminance = EvaluateOpenLitLuminance(directColor, false); + Vector3 gammaDirection = EvaluateOpenLitDominantDirection( + EvaluateOpenLitDirectAggregate(directDirection, directColor, true), + shAr, + shAg, + shAb + ); + Vector3 linearDirection = EvaluateOpenLitDominantDirection( + EvaluateOpenLitDirectAggregate(directDirection, directColor, false), + shAr, + shAg, + shAb + ); + + Assert.That(gammaLuminance, Is.EqualTo(0.3025f).Within(OracleTolerance)); + Assert.That(linearLuminance, Is.EqualTo(0.11999135f).Within(OracleTolerance)); + Assert.That(gammaLuminance, Is.GreaterThan(linearLuminance + 0.1f)); + Assert.That(gammaDirection.y, Is.GreaterThan(0.0f), "Positive-Y SH direction must be included before normalization."); + Assert.That(linearDirection.y, Is.GreaterThan(0.0f), "Positive-Y SH direction must be included before normalization."); + Assert.That(Vector3.Distance(gammaDirection, linearDirection), Is.GreaterThan(0.01f)); + Assert.That(gammaDirection.z, Is.GreaterThan(0.0f), "The fixed fallback must contribute before normalization even for a nonzero aggregate."); + Assert.That(linearDirection.z, Is.GreaterThan(0.0f), "The fixed fallback must contribute before normalization even for a nonzero aggregate."); + } + + /// Requires unscaled bright L0/L2 plus L1 and SH-dominant dark L1 across every Unity SH coefficient input. + [Test] + public void OpenLitTwoBandShUsesUnscaledBrightAndShDominantDarkDirections() + { + ShCoefficients coefficients = CreateOpenLitCoefficients(); + Vector3 lightDirection = new Vector3(0.6f, 0.2f, Mathf.Sqrt(0.6f)); + Color bright = EvaluateOpenLitTwoBandSh(lightDirection, lightDirection, coefficients, false); + Color dark = EvaluateOpenLitTwoBandSh(-lightDirection, lightDirection, coefficients, false); + Color legacyBright = EvaluateLegacyScaledTwoBandSh(lightDirection, lightDirection, coefficients); + Color legacyDark = EvaluateLegacyScaledTwoBandSh(-lightDirection, lightDirection, coefficients); + + Assert.That(lightDirection.sqrMagnitude, Is.EqualTo(1.0f).Within(OracleTolerance)); + Assert.That(bright.r, Is.EqualTo(0.7244621f).Within(OracleTolerance)); + Assert.That(bright.g, Is.EqualTo(0.8120943f).Within(OracleTolerance)); + Assert.That(bright.b, Is.EqualTo(1.0022154f).Within(OracleTolerance)); + Assert.That(dark.r, Is.EqualTo(0.7016153f).Within(OracleTolerance)); + Assert.That(dark.g, Is.EqualTo(0.8667072f).Within(OracleTolerance)); + Assert.That(dark.b, Is.EqualTo(1.0255581f).Within(OracleTolerance)); + Assert.That(MaximumRgbDifference(bright, dark), Is.GreaterThan(0.05f)); + Assert.That(MaximumRgbDifference(legacyBright, bright), Is.GreaterThan(0.05f)); + Assert.That(MaximumRgbDifference(legacyDark, dark), Is.GreaterThan(0.05f)); + } + + /// Requires Linear SH to remain raw and Gamma SH to convert both assembled bands to sRGB. + [Test] + public void OpenLitTwoBandShDefinesLinearAndGammaResults() + { + ShCoefficients coefficients = CreateOpenLitCoefficients(); + Vector3 lightDirection = new Vector3(0.6f, 0.2f, Mathf.Sqrt(0.6f)); + Color linearBright = EvaluateOpenLitTwoBandSh(lightDirection, lightDirection, coefficients, false); + Color linearDark = EvaluateOpenLitTwoBandSh(-lightDirection, lightDirection, coefficients, false); + Color gammaBright = EvaluateOpenLitTwoBandSh(lightDirection, lightDirection, coefficients, true); + Color gammaDark = EvaluateOpenLitTwoBandSh(-lightDirection, lightDirection, coefficients, true); + + AssertColor(linearBright, EvaluateOpenLitTwoBandSh(lightDirection, lightDirection, coefficients, false), "OpenLit Linear bright band"); + AssertColor(linearDark, EvaluateOpenLitTwoBandSh(-lightDirection, lightDirection, coefficients, false), "OpenLit Linear dark band"); + Assert.That(gammaBright.r, Is.EqualTo(Mathf.LinearToGammaSpace(linearBright.r)).Within(OracleTolerance)); + Assert.That(gammaBright.g, Is.EqualTo(Mathf.LinearToGammaSpace(linearBright.g)).Within(OracleTolerance)); + Assert.That(gammaBright.b, Is.EqualTo(Mathf.LinearToGammaSpace(linearBright.b)).Within(OracleTolerance)); + Assert.That(gammaDark.r, Is.EqualTo(Mathf.LinearToGammaSpace(linearDark.r)).Within(OracleTolerance)); + Assert.That(gammaDark.g, Is.EqualTo(Mathf.LinearToGammaSpace(linearDark.g)).Within(OracleTolerance)); + Assert.That(gammaDark.b, Is.EqualTo(Mathf.LinearToGammaSpace(linearDark.b)).Within(OracleTolerance)); + Assert.That(MaximumRgbDifference(gammaBright, linearBright), Is.GreaterThan(0.1f)); + Assert.That(MaximumRgbDifference(gammaDark, linearDark), Is.GreaterThan(0.1f)); + } + + /// Requires finite fallback direction and finite zero dark L1 when all SH first-order direction coefficients are zero. + [Test] + public void OpenLitDegenerateDirectionsRemainFinite() + { + Vector3 direction = EvaluateOpenLitDominantDirection(Vector3.zero, Vector4.zero, Vector4.zero, Vector4.zero); + ShCoefficients coefficients = new ShCoefficients( + new Vector4(0.0f, 0.0f, 0.0f, 0.2f), + new Vector4(0.0f, 0.0f, 0.0f, 0.3f), + new Vector4(0.0f, 0.0f, 0.0f, 0.4f), + Vector4.zero, + Vector4.zero, + Vector4.zero, + Vector4.zero + ); + Color dark = EvaluateOpenLitTwoBandSh(-direction, direction, coefficients, false); + + AssertVector(new Vector3(0.40824829f, 0.81649658f, 0.40824829f), direction, "OpenLit fallback direction"); + Assert.That(IsFinite(direction), Is.True); + AssertFinite(dark, "OpenLit zero SH-dark direction"); + AssertColor(new Color(0.2f, 0.3f, 0.4f, 1.0f), dark, "OpenLit zero SH-dark L1"); + } + + /// Requires the Linear D3D11 product capture to follow the OpenLit oracle for top, side, and bottom samples across changed full SH inputs. + [Test] + public void ToonLinearD3D11RuntimeMatchesOpenLitTopSideBottomAndChangedShInputs() + { + Assert.That(SystemInfo.graphicsDeviceType, Is.EqualTo(GraphicsDeviceType.Direct3D11)); + Assert.That(QualitySettings.activeColorSpace, Is.EqualTo(ColorSpace.Linear)); + + ShCoefficients firstCoefficients = CreateOpenLitCoefficients(); + ShCoefficients secondCoefficients = CreateChangedOpenLitCoefficients(); + AssertLinearRuntimeBands(firstCoefficients, "first OpenLit SH set"); + AssertLinearRuntimeBands(secondCoefficients, "second OpenLit SH set"); + } + + /// Requires the fixed host to compile and read back the product Gamma branch against the Gamma OpenLit oracle. + [Test] + public void ToonOpenLitGammaHostReadbackMatchesGammaOracle() + { + ShCoefficients coefficients = CreateOpenLitCoefficients(); + Vector3 lightDirection = EvaluateOpenLitDominantDirection( + Vector3.zero, + coefficients.ar, + coefficients.ag, + coefficients.ab + ); + Color expected = EvaluateOpenLitTwoBandSh( + lightDirection, + lightDirection, + coefficients, + true + ); + + using ( + var selection = new ToonShadowHostSelectionScope( + ToonOpenLitGammaShaderName, + ToonOpenLitGammaModuleId, + ToonOpenLitGammaHostAssetPath + ) + ) + using (var capture = new ToonLightingCaptureScope()) + { + AssertImportedToonOpenLitGammaHost(); + Color actual = capture.Render( + ToonOpenLitGammaShaderName, + "ForwardBase", + lightDirection, + Vector4.zero, + new Vector4(lightDirection.x, lightDirection.y, lightDirection.z, 0.0f), + coefficients + ); + + AssertFinite(actual, "Toon OpenLit Gamma ForwardBase readback"); + AssertColor(expected, actual, "Toon OpenLit Gamma ForwardBase readback"); + } + } + + /// Requires the selected ForwardAdd diagnostic to publish only normalized direct direction or zero, independent of injected SH. + [Test] + public void ToonOpenLitForwardAddDiagnosticDecodesDirectOnlyDirectionForNonzeroAndZeroAggregates() + { + Vector4 coloredDirectLight = new Vector4(0.3f, 0.6f, 0.2f, 1.0f); + Vector3 expectedDirection = Vector3.forward; + + using ( + var selection = new ToonShadowHostSelectionScope( + ToonOpenLitGammaShaderName, + ToonOpenLitGammaModuleId, + ToonOpenLitGammaHostAssetPath + ) + ) + using (var capture = new ToonLightingCaptureScope()) + { + AssertImportedToonOpenLitGammaHost(); + Vector3 firstDirection = DecodeToonOpenLitForwardAddDirection( + RenderToonOpenLitForwardAddDiagnostic( + capture, + coloredDirectLight, + CreateOpenLitCoefficients() + ) + ); + Vector3 secondDirection = DecodeToonOpenLitForwardAddDirection( + RenderToonOpenLitForwardAddDiagnostic( + capture, + coloredDirectLight, + CreateChangedOpenLitCoefficients() + ) + ); + Vector3 zeroDirection = DecodeToonOpenLitForwardAddDirection( + RenderToonOpenLitForwardAddDiagnostic( + capture, + Vector4.zero, + CreateOpenLitCoefficients() + ) + ); + + AssertVector(expectedDirection, firstDirection, "Toon OpenLit ForwardAdd first SH direction"); + AssertVector(expectedDirection, secondDirection, "Toon OpenLit ForwardAdd second SH direction"); + AssertVector(Vector3.zero, zeroDirection, "Toon OpenLit ForwardAdd zero direct direction"); + } + } + + /// Renders one full SH input set at top, side, and bottom normals and compares the product readbacks with the Linear OpenLit oracle. + private static void AssertLinearRuntimeBands(ShCoefficients coefficients, string label) + { + Vector3 lightDirection = EvaluateOpenLitDominantDirection( + Vector3.zero, + coefficients.ar, + coefficients.ag, + coefficients.ab + ); + using (var capture = new ToonLightingCaptureScope()) + { + AssertLinearRuntimeBand(capture, lightDirection, lightDirection, coefficients, label + " top"); + AssertLinearRuntimeBand(capture, Vector3.right, lightDirection, coefficients, label + " side"); + AssertLinearRuntimeBand(capture, -lightDirection, lightDirection, coefficients, label + " bottom"); + } + } + + /// Renders one Linear product SH sample and compares it with the unconverted OpenLit reference band. + private static void AssertLinearRuntimeBand( + ToonLightingCaptureScope capture, + Vector3 normal, + Vector3 lightDirection, + ShCoefficients coefficients, + string label + ) + { + Color expected = EvaluateOpenLitTwoBandSh(normal, lightDirection, coefficients, false); + Color actual = capture.Render( + "PureBase/Toon", + "ForwardBase", + normal, + Vector4.zero, + new Vector4(lightDirection.x, lightDirection.y, lightDirection.z, 0.0f), + coefficients + ); + AssertFinite(actual, label + " readback"); + AssertColor(expected, actual, label + " readback"); + } + + /// Creates a second full SH input set whose first-order, L2, and C vectors differ from the primary OpenLit fixture. + private static ShCoefficients CreateChangedOpenLitCoefficients() + { + return new ShCoefficients( + new Vector4(-0.15f, 0.12f, 0.08f, 0.31f), + new Vector4(0.11f, -0.07f, 0.18f, 0.46f), + new Vector4(0.04f, 0.16f, -0.13f, 0.57f), + new Vector4(0.09f, -0.05f, 0.12f, 0.06f), + new Vector4(-0.04f, 0.14f, 0.07f, 0.02f), + new Vector4(0.13f, 0.01f, -0.08f, 0.05f), + new Vector4(0.06f, -0.02f, 0.04f, 0.0f) + ); + } + + /// Checks that the temporary forced-Gamma selection produced a supported compiler-clean shader. + private static void AssertImportedToonOpenLitGammaHost() + { + Shader shader = AssetDatabase.LoadAssetAtPath(ToonOpenLitGammaHostAssetPath); + Assert.That(shader, Is.Not.Null, "The temporary Toon OpenLit Gamma host import did not produce a shader."); + Assert.That(ShaderUtil.ShaderHasError(shader), Is.False, "The temporary Toon OpenLit Gamma host import produced shader compiler errors."); + Assert.That(shader.isSupported, Is.True, "The temporary Toon OpenLit Gamma host shader is unsupported."); + } + + /// Renders one ForwardAdd diagnostic using the selected host without changing any persistent Shader-Core selection. + private static Color RenderToonOpenLitForwardAddDiagnostic( + ToonLightingCaptureScope capture, + Vector4 lightColor, + ShCoefficients coefficients + ) + { + return capture.RenderLightWithCookie( + ToonOpenLitGammaShaderName, + "ForwardAdd", + new LightCaptureRequest + { + normal = Vector3.forward, + lightColor = lightColor, + lightPosition = new Vector4(0.0f, 0.0f, 1.0f, 0.0f), + coefficients = coefficients, + lightType = LightType.Directional, + lightCount = lightColor == Vector4.zero ? 0 : 1, + } + ); + } + + /// Decodes the host Shade diagnostic from its centered RGB representation. + private static Vector3 DecodeToonOpenLitForwardAddDirection(Color readback) + { + return new Vector3(readback.r, readback.g, readback.b) * 2.0f - Vector3.one; + } + + /// Builds the fixed nonzero coefficient set used to exercise every Unity SH input vector. + /// A seven-vector coefficient set with distinct linear and quadratic contributions. + private static ShCoefficients CreateOpenLitCoefficients() + { + return new ShCoefficients( + new Vector4(0.1f, 0.05f, 0.2f, 0.4f), + new Vector4(0.05f, 0.2f, 0.1f, 0.5f), + new Vector4(0.2f, 0.1f, 0.05f, 0.6f), + new Vector4(0.08f, 0.03f, 0.1f, 0.02f), + new Vector4(0.02f, 0.1f, 0.2f, 0.03f), + new Vector4(0.05f, 0.06f, 0.3f, 0.04f), + new Vector4(0.05f, 0.04f, 0.03f, 0.0f) + ); + } + + /// Evaluates OpenLit luminance for the requested output color-space branch. + /// The direct light RGB value. + /// Whether the Gamma coefficient set applies. + /// The OpenLit luminance weight. + private static float EvaluateOpenLitLuminance(Color color, bool isGamma) + { + Vector3 coefficients = isGamma + ? new Vector3(0.22f, 0.707f, 0.071f) + : new Vector3(0.0396819152f, 0.458021790f, 0.00609653955f); + return Vector3.Dot(new Vector3(color.r, color.g, color.b), coefficients); + } + + /// Builds the OpenLit direct aggregate from a light direction and color-space luminance weight. + /// The unnormalized direct-light direction. + /// The direct light RGB value. + /// Whether the Gamma luminance coefficients apply. + /// The luminance-weighted direct direction aggregate. + private static Vector3 EvaluateOpenLitDirectAggregate(Vector3 lightDirection, Color lightColor, bool isGamma) + { + return lightDirection * EvaluateOpenLitLuminance(lightColor, isGamma); + } + + /// Evaluates the fallback-inclusive OpenLit dominant direction from an already weighted direct aggregate. + /// The post-light luminance-weighted direct direction aggregate. + /// The Unity red first-order SH coefficient vector. + /// The Unity green first-order SH coefficient vector. + /// The Unity blue first-order SH coefficient vector. + /// The finite normalized dominant direction. + private static Vector3 EvaluateOpenLitDominantDirection(Vector3 directAggregateDirection, Vector4 shAr, Vector4 shAg, Vector4 shAb) + { + Vector3 shDirection = (new Vector3(shAr.x, shAr.y, shAr.z) + + new Vector3(shAg.x, shAg.y, shAg.z) + + new Vector3(shAb.x, shAb.y, shAb.z)) + / 3.0f; + Vector3 directionVector = directAggregateDirection + + new Vector3(shDirection.x, Mathf.Abs(shDirection.y), shDirection.z) + + new Vector3(0.001f, 0.002f, 0.001f); + return directionVector.normalized; + } + + /// Evaluates the unscaled OpenLit L0/L2 SH base term. + /// The unscaled OpenLit V direction. + /// All Unity SH coefficient vectors. + /// The L0/L2 base term shared by both bands. + private static Vector3 EvaluateOpenLitL0L2Base(Vector3 evaluationDirection, ShCoefficients coefficients) + { + Vector4 quadratic = new Vector4( + evaluationDirection.x * evaluationDirection.y, + evaluationDirection.y * evaluationDirection.z, + evaluationDirection.z * evaluationDirection.z, + evaluationDirection.z * evaluationDirection.x + ); + return new Vector3(coefficients.ar.w, coefficients.ag.w, coefficients.ab.w) + + new Vector3( + Vector4.Dot(coefficients.br, quadratic), + Vector4.Dot(coefficients.bg, quadratic), + Vector4.Dot(coefficients.bb, quadratic) + ) + + new Vector3(coefficients.c.x, coefficients.c.y, coefficients.c.z) + * (evaluationDirection.x * evaluationDirection.x - evaluationDirection.y * evaluationDirection.y); + } + + /// Evaluates the OpenLit bright L1 SH term along the unscaled V direction. + /// The unscaled OpenLit V direction. + /// All Unity SH coefficient vectors. + /// The bright-band L1 term. + private static Vector3 EvaluateOpenLitBrightL1(Vector3 evaluationDirection, ShCoefficients coefficients) + { + return EvaluateOpenLitL1(evaluationDirection, coefficients); + } + + /// Evaluates the finite OpenLit dark L1 SH term along the normalized summed first-order coefficients. + /// All Unity SH coefficient vectors. + /// The dark-band L1 term, or zero when the SH direction is degenerate. + private static Vector3 EvaluateOpenLitDarkL1(ShCoefficients coefficients) + { + Vector3 shDirection = new Vector3(coefficients.ar.x, coefficients.ar.y, coefficients.ar.z) + + new Vector3(coefficients.ag.x, coefficients.ag.y, coefficients.ag.z) + + new Vector3(coefficients.ab.x, coefficients.ab.y, coefficients.ab.z); + return shDirection.sqrMagnitude <= 0.000001f + ? Vector3.zero + : EvaluateOpenLitL1(shDirection.normalized, coefficients); + } + + /// Evaluates the selected OpenLit bright or dark SH band and performs Gamma-only linear-to-sRGB conversion. + /// The surface normal that selects the binary band. + /// The OpenLit V direction. + /// All Unity SH coefficient vectors. + /// Whether Gamma conversion applies after both bands are assembled. + /// The selected OpenLit SH color. + private static Color EvaluateOpenLitTwoBandSh(Vector3 surfaceNormal, Vector3 lightDirection, ShCoefficients coefficients, bool isGamma) + { + Vector3 baseTerm = EvaluateOpenLitL0L2Base(lightDirection, coefficients); + Vector3 bright = baseTerm + EvaluateOpenLitBrightL1(lightDirection, coefficients); + Vector3 dark = baseTerm + EvaluateOpenLitDarkL1(coefficients); + if (isGamma) + { + bright = ConvertLinearToSrgb(bright); + dark = ConvertLinearToSrgb(dark); + } + + Vector3 selected = Vector3.Dot(surfaceNormal, lightDirection) >= 0.0f ? bright : dark; + return new Color(selected.x, selected.y, selected.z, 1.0f); + } + + /// Evaluates the shared OpenLit first-order SH term along one supplied direction. + /// The direction used to evaluate L1. + /// All Unity SH coefficient vectors. + /// The three-channel L1 term. + private static Vector3 EvaluateOpenLitL1(Vector3 direction, ShCoefficients coefficients) + { + return new Vector3( + Vector3.Dot(new Vector3(coefficients.ar.x, coefficients.ar.y, coefficients.ar.z), direction), + Vector3.Dot(new Vector3(coefficients.ag.x, coefficients.ag.y, coefficients.ag.z), direction), + Vector3.Dot(new Vector3(coefficients.ab.x, coefficients.ab.y, coefficients.ab.z), direction) + ); + } + + /// Converts a three-channel Linear SH value to Unity-compatible sRGB. + /// The assembled Linear SH value. + /// The component-wise sRGB value. + private static Vector3 ConvertLinearToSrgb(Vector3 value) + { + return new Vector3( + Mathf.LinearToGammaSpace(value.x), + Mathf.LinearToGammaSpace(value.y), + Mathf.LinearToGammaSpace(value.z) + ); + } + + /// Evaluates the obsolete scaled and inverted two-band approximation for contrast with the OpenLit reference. + /// The surface normal that selects the binary band. + /// The dominant light direction. + /// All Unity SH coefficient vectors. + /// The obsolete selected SH approximation. + private static Color EvaluateLegacyScaledTwoBandSh(Vector3 surfaceNormal, Vector3 lightDirection, ShCoefficients coefficients) + { + Vector3 evaluationDirection = lightDirection * 0.666666f; + Vector3 baseTerm = EvaluateOpenLitL0L2Base(evaluationDirection, coefficients); + Vector3 linearTerm = EvaluateOpenLitBrightL1(evaluationDirection, coefficients); + Vector3 selected = Vector3.Dot(surfaceNormal, lightDirection) >= 0.0f + ? Vector3.Max(baseTerm + linearTerm, Vector3.zero) + : Vector3.Max(baseTerm - linearTerm, Vector3.zero); + return new Color(selected.x, selected.y, selected.z, 1.0f); + } + } +} diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs.meta b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs.meta new file mode 100644 index 00000000..2b966100 --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 542fec1b44eb9d4488468bdf6eb1ebeb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs index f853bc0e..40ea1aa2 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs @@ -1280,6 +1280,9 @@ private sealed class ToonShadowHostSelectionScope : IDisposable private const string ProjectSettingsRelativePath = "ProjectSettings/jp.lilxyzw.shadercore.asset"; private readonly UnityEngine.Object settings; + private readonly string shaderName; + private readonly string moduleId; + private readonly string hostAssetPath; private readonly ToonShadowSettingsRow originalRow; private readonly string projectSettingsHash; private bool temporarySelectionApplied; @@ -1287,7 +1290,19 @@ private sealed class ToonShadowHostSelectionScope : IDisposable /// Captures the original fixed-host row, applies a temporary selection, and synchronously imports only its host. public ToonShadowHostSelectionScope() + : this(ToonShadowShaderName, ToonShadowModuleId, ToonShadowHostAssetPath) { + } + + /// Captures one fixed-host row, applies its temporary selection, and synchronously imports only that host. + /// The unique fixed-host shader name whose selection row is temporary. + /// The sole diagnostic module selected for the fixed host. + /// The Shader-Core host asset imported after selection. + public ToonShadowHostSelectionScope(string shaderName, string moduleId, string hostAssetPath) + { + this.shaderName = shaderName; + this.moduleId = moduleId; + this.hostAssetPath = hostAssetPath; settings = GetProjectSettings(); projectSettingsHash = GetFileSha256(GetProjectSettingsPath()); try @@ -1295,14 +1310,14 @@ public ToonShadowHostSelectionScope() using (var serializedSettings = new SerializedObject(settings)) { SerializedProperty settingsProperty = GetShaderSettingsProperty(serializedSettings); - originalRow = ReadToonShadowRow(settingsProperty); - WriteTemporaryToonShadowRow(settingsProperty); + originalRow = ReadToonShadowRow(settingsProperty, shaderName); + WriteTemporaryToonShadowRow(settingsProperty, shaderName, moduleId); serializedSettings.ApplyModifiedPropertiesWithoutUndo(); temporarySelectionApplied = true; } AssetDatabase.ImportAsset( - ToonShadowHostAssetPath, + hostAssetPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate ); } @@ -1338,7 +1353,7 @@ private void RestoreAndAssertUnchanged() using (var serializedSettings = new SerializedObject(settings)) { SerializedProperty settingsProperty = GetShaderSettingsProperty(serializedSettings); - RestoreToonShadowRow(settingsProperty); + RestoreToonShadowRow(settingsProperty, shaderName); serializedSettings.ApplyModifiedPropertiesWithoutUndo(); } @@ -1346,17 +1361,18 @@ private void RestoreAndAssertUnchanged() Assert.That( GetFileSha256(GetProjectSettingsPath()), Is.EqualTo(projectSettingsHash), - "The temporary ToonShadow host selection must not persist Shader-Core ProjectSettings." + "The temporary fixed-host selection must not persist Shader-Core ProjectSettings." ); using (var serializedSettings = new SerializedObject(settings)) { ToonShadowSettingsRow restoredRow = ReadToonShadowRow( - GetShaderSettingsProperty(serializedSettings) + GetShaderSettingsProperty(serializedSettings), + shaderName ); Assert.That( restoredRow.Equals(originalRow), Is.True, - "The temporary ToonShadow host selection must restore only its original serialized row." + "The temporary fixed-host selection must restore only its original serialized row." ); } } @@ -1409,9 +1425,12 @@ private static SerializedProperty GetShaderSettingsProperty(SerializedObject ser } /// Reads only the original ToonShadow row, rejecting duplicate target rows before mutation. - private static ToonShadowSettingsRow ReadToonShadowRow(SerializedProperty settingsProperty) + private static ToonShadowSettingsRow ReadToonShadowRow( + SerializedProperty settingsProperty, + string shaderName + ) { - int rowIndex = FindToonShadowRowIndex(settingsProperty); + int rowIndex = FindToonShadowRowIndex(settingsProperty, shaderName); if (rowIndex < 0) { return ToonShadowSettingsRow.Missing; @@ -1426,9 +1445,13 @@ private static ToonShadowSettingsRow ReadToonShadowRow(SerializedProperty settin } /// Upserts only the target row with its required one-module selection. - private static void WriteTemporaryToonShadowRow(SerializedProperty settingsProperty) + private static void WriteTemporaryToonShadowRow( + SerializedProperty settingsProperty, + string shaderName, + string moduleId + ) { - int rowIndex = FindToonShadowRowIndex(settingsProperty); + int rowIndex = FindToonShadowRowIndex(settingsProperty, shaderName); if (rowIndex < 0) { rowIndex = settingsProperty.arraySize; @@ -1436,21 +1459,21 @@ private static void WriteTemporaryToonShadowRow(SerializedProperty settingsPrope } SerializedProperty row = settingsProperty.GetArrayElementAtIndex(rowIndex); - row.FindPropertyRelative(ShaderNameFieldName).stringValue = ToonShadowShaderName; - WriteStringArray(row.FindPropertyRelative(ModulesFieldName), new[] { ToonShadowModuleId }); + row.FindPropertyRelative(ShaderNameFieldName).stringValue = shaderName; + WriteStringArray(row.FindPropertyRelative(ModulesFieldName), new[] { moduleId }); WriteMultiModules(row.FindPropertyRelative(MultiModulesFieldName), Array.Empty()); } /// Restores only the target row to its captured presence and exact module collections. - private void RestoreToonShadowRow(SerializedProperty settingsProperty) + private void RestoreToonShadowRow(SerializedProperty settingsProperty, string shaderName) { - int rowIndex = FindToonShadowRowIndex(settingsProperty); + int rowIndex = FindToonShadowRowIndex(settingsProperty, shaderName); if (!originalRow.present) { Assert.That( rowIndex, Is.GreaterThanOrEqualTo(0), - "The temporary ToonShadow row disappeared before it could be removed." + "The temporary fixed-host row disappeared before it could be removed." ); settingsProperty.DeleteArrayElementAtIndex(rowIndex); return; @@ -1459,24 +1482,27 @@ private void RestoreToonShadowRow(SerializedProperty settingsProperty) Assert.That( rowIndex, Is.GreaterThanOrEqualTo(0), - "The original ToonShadow row disappeared before it could be restored." + "The original fixed-host row disappeared before it could be restored." ); SerializedProperty row = settingsProperty.GetArrayElementAtIndex(rowIndex); - row.FindPropertyRelative(ShaderNameFieldName).stringValue = ToonShadowShaderName; + row.FindPropertyRelative(ShaderNameFieldName).stringValue = shaderName; WriteStringArray(row.FindPropertyRelative(ModulesFieldName), originalRow.modules); WriteMultiModules(row.FindPropertyRelative(MultiModulesFieldName), originalRow.multiModules); } /// Finds the sole ToonShadow row without reading or changing unrelated module-selection rows. - private static int FindToonShadowRowIndex(SerializedProperty settingsProperty) + private static int FindToonShadowRowIndex( + SerializedProperty settingsProperty, + string shaderName + ) { var foundIndex = -1; for (var index = 0; index < settingsProperty.arraySize; index++) { - SerializedProperty shaderName = settingsProperty + SerializedProperty shaderNameProperty = settingsProperty .GetArrayElementAtIndex(index) .FindPropertyRelative(ShaderNameFieldName); - if (shaderName == null || shaderName.stringValue != ToonShadowShaderName) + if (shaderNameProperty == null || shaderNameProperty.stringValue != shaderName) { continue; } @@ -1484,7 +1510,7 @@ private static int FindToonShadowRowIndex(SerializedProperty settingsProperty) Assert.That( foundIndex, Is.EqualTo(-1), - "Shader-Core ProjectSettings contains duplicate ToonShadow rows." + "Shader-Core ProjectSettings contains duplicate fixed-host rows." ); foundIndex = index; } diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.Shadow.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.Shadow.cs index e399fe50..95685034 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.Shadow.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.Shadow.cs @@ -206,20 +206,21 @@ public void ToonShadowVisibilityOracleLeavesAggregateDirectionAndShBandUnchanged ToonShadowObservation shadowed = EvaluateToonShadowContract(shadowedInputs); Vector4 shAr = new Vector4(0.3f, 0.0f, 0.0f, 0.2f); Vector3 normal = (Vector3.forward - 0.5f * Vector3.right).normalized; - Vector3 visibleDirection = EvaluateDominantDirection( + Vector3 visibleDirection = EvaluateOpenLitDominantDirection( Vector3.forward * visible.directionWeight, shAr, Vector4.zero, Vector4.zero ); - Vector3 shadowedDirection = EvaluateDominantDirection( + Vector3 shadowedDirection = EvaluateOpenLitDominantDirection( Vector3.forward * shadowed.directionWeight, shAr, Vector4.zero, Vector4.zero ); - Color visibleBand = EvaluateTwoBandSh(normal, visibleDirection, shAr, Vector4.zero, Vector4.zero); - Color shadowedBand = EvaluateTwoBandSh(normal, shadowedDirection, shAr, Vector4.zero, Vector4.zero); + ShCoefficients coefficients = new ShCoefficients(shAr, Vector4.zero, Vector4.zero, Vector4.zero, Vector4.zero, Vector4.zero, Vector4.zero); + Color visibleBand = EvaluateOpenLitTwoBandSh(normal, visibleDirection, coefficients, false); + Color shadowedBand = EvaluateOpenLitTwoBandSh(normal, shadowedDirection, coefficients, false); Assert.That(shadowed.directRadiance, Is.LessThan(visible.directRadiance - 0.02f)); Assert.That(shadowed.directionWeight, Is.EqualTo(visible.directionWeight).Within(OracleTolerance)); diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.cs index dded6cdc..ab592f33 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.cs @@ -37,160 +37,6 @@ public sealed partial class PureBaseToonLightingContractTests /// Identifies the global keyword that selects the ForwardAdd point-light variant. private const string PointKeyword = "POINT"; - /// Requires the fixed dominant-direction two-band SH equation and rejects the current continuous normal evaluation. - [Test] - public void FixedTwoBandShOracleMatchesReferenceAndRejectsContinuousNormalEvaluation() - { - Vector4 shAr = new Vector4(0.3f, 0.0f, 0.0f, 0.2f); - Vector4 shAg = new Vector4(0.0f, 0.15f, 0.0f, 0.1f); - Vector4 shAb = new Vector4(0.0f, 0.0f, 0.45f, 0.3f); - Vector3 direction = EvaluateDominantDirection( - Vector3.zero, - shAr, - shAg, - shAb - ); - Color bright = EvaluateTwoBandSh(direction, direction, shAr, shAg, shAb); - Color dark = EvaluateTwoBandSh(-direction, direction, shAr, shAg, shAb); - - AssertVector( - new Vector3(0.53452248f, 0.26726124f, 0.80178373f), - direction, - "Fixed Toon SH dominant direction" - ); - AssertColor( - new Color(0.30690450f, 0.12672612f, 0.54053512f, 1.0f), - bright, - "Fixed Toon SH bright band" - ); - AssertColor( - new Color(0.09309550f, 0.07327388f, 0.05946488f, 1.0f), - dark, - "Fixed Toon SH dark band" - ); - - Color oldContinuousBright = EvaluateContinuousNormalSh(direction, shAr, shAg, shAb); - Color oldContinuousDark = EvaluateContinuousNormalSh(-direction, shAr, shAg, shAb); - Assert.That( - MaximumRgbDifference(oldContinuousBright, bright), - Is.GreaterThan(0.02f), - "The old continuous normal-evaluated SH must not satisfy the fixed bright-band oracle." - ); - Assert.That( - MaximumRgbDifference(oldContinuousDark, dark), - Is.GreaterThan(0.02f), - "The old continuous normal-evaluated SH must not satisfy the fixed dark-band oracle." - ); - } - - /// Requires the fixed nonzero fallback before dominant-direction normalization. - [Test] - public void DegenerateDominantDirectionUsesFixedFiniteFallback() - { - Vector3 direction = EvaluateDominantDirection( - Vector3.zero, - Vector4.zero, - Vector4.zero, - Vector4.zero - ); - - AssertVector( - new Vector3(0.40824829f, 0.81649658f, 0.40824829f), - direction, - "Fixed Toon SH degenerate-direction fallback" - ); - Assert.That(IsFinite(direction), Is.True, "Fallback direction must remain finite."); - } - - /// Requires Toon ForwardBase readbacks to select fixed bright and dark SH bands instead of continuous normal SH. - [Test] - public void ToonForwardBaseShOnlyReadbackRequiresFixedBrightAndDarkBands() - { - ShCoefficients coefficients = ShCoefficients.FixedOracle; - Vector3 direction = EvaluateDominantDirection( - Vector3.zero, - coefficients.ar, - coefficients.ag, - coefficients.ab - ); - Color expectedBright = EvaluateTwoBandSh( - direction, - direction, - coefficients.ar, - coefficients.ag, - coefficients.ab - ); - Color expectedDark = EvaluateTwoBandSh( - -direction, - direction, - coefficients.ar, - coefficients.ag, - coefficients.ab - ); - - using (var capture = new ToonLightingCaptureScope()) - { - Color bright = capture.Render( - "PureBase/Toon", - "ForwardBase", - direction, - Vector4.zero, - Vector4.zero, - coefficients - ); - Color dark = capture.Render( - "PureBase/Toon", - "ForwardBase", - -direction, - Vector4.zero, - Vector4.zero, - coefficients - ); - - AssertFinite(bright, "Toon SH-only bright readback"); - AssertFinite(dark, "Toon SH-only dark readback"); - AssertColor(expectedBright, bright, "Toon SH-only bright readback"); - AssertColor(expectedDark, dark, "Toon SH-only dark readback"); - } - } - - /// Requires direct aggregate direction to participate in Toon SH-band selection while direct binary lighting remains additive. - [Test] - public void ToonForwardBaseDirectAggregateParticipatesInShBandDirection() - { - ShCoefficients coefficients = ShCoefficients.FixedOracle; - Vector4 directLight = new Vector4(0.2f, 0.2f, 0.2f, 1.0f); - Vector4 directionalPosition = new Vector4(0.0f, 0.0f, 1.0f, 0.0f); - Vector3 direction = EvaluateDominantDirection( - new Vector3(0.0f, 0.0f, 0.2f), - coefficients.ar, - coefficients.ag, - coefficients.ab - ); - Color expectedBright = EvaluateTwoBandSh( - direction, - direction, - coefficients.ar, - coefficients.ag, - coefficients.ab - ) + new Color(0.2f, 0.2f, 0.2f, 0.0f); - - using (var capture = new ToonLightingCaptureScope()) - { - Color actual = capture.Render( - "PureBase/Toon", - "ForwardBase", - direction, - directLight, - directionalPosition, - coefficients - ); - - AssertFinite(actual, "Toon direct-plus-SH readback"); - AssertColor(expectedBright, actual, "Toon direct-plus-SH readback"); - } - } - /// Requires injected SH to leave a point-light ForwardAdd readback unchanged while its RGB and alpha remain valid. [Test] public void ToonForwardAddSecondPointLightIgnoresInjectedShAndPreservesDestinationAlpha() @@ -425,86 +271,6 @@ private sealed class ToonLightingCaptureScope : ToonLightingCaptureRuntimeScope { } - /// Evaluates the fixed direct-plus-SH dominant direction with the required degenerate fallback. - /// The grayscale-weighted direct-light direction aggregate. - /// The Unity red first-order SH coefficient vector. - /// The Unity green first-order SH coefficient vector. - /// The Unity blue first-order SH coefficient vector. - /// The finite normalized Toon scene-light direction. - private static Vector3 EvaluateDominantDirection( - Vector3 directAggregateDirection, - Vector4 shAr, - Vector4 shAg, - Vector4 shAb - ) - { - Vector3 shDirection = (new Vector3(shAr.x, shAr.y, shAr.z) - + new Vector3(shAg.x, shAg.y, shAg.z) - + new Vector3(shAb.x, shAb.y, shAb.z)) - / 3.0f; - Vector3 directionVector = directAggregateDirection - + new Vector3(shDirection.x, Mathf.Abs(shDirection.y), shDirection.z); - if (Vector3.Dot(directionVector, directionVector) <= 0.000001f) - { - directionVector = new Vector3(0.001f, 0.002f, 0.001f); - } - - return directionVector.normalized; - } - - /// Evaluates the fixed Toon bright or dark SH band selected by the supplied surface normal. - /// The normalized world-space surface normal. - /// The fixed dominant scene-light direction. - /// The Unity red first-order SH coefficient vector. - /// The Unity green first-order SH coefficient vector. - /// The Unity blue first-order SH coefficient vector. - /// The selected nonnegative SH band. - private static Color EvaluateTwoBandSh( - Vector3 surfaceNormal, - Vector3 lightDirection, - Vector4 shAr, - Vector4 shAg, - Vector4 shAb - ) - { - Vector3 evaluationDirection = lightDirection * 0.666666f; - Vector3 baseTerm = new Vector3(shAr.w, shAg.w, shAb.w); - Vector3 linear = new Vector3( - Vector3.Dot(new Vector3(shAr.x, shAr.y, shAr.z), evaluationDirection), - Vector3.Dot(new Vector3(shAg.x, shAg.y, shAg.z), evaluationDirection), - Vector3.Dot(new Vector3(shAb.x, shAb.y, shAb.z), evaluationDirection) - ); - Vector3 bright = Vector3.Max(baseTerm + linear, Vector3.zero); - Vector3 dark = Vector3.Max(baseTerm - linear, Vector3.zero); - Vector3 selected = Vector3.Dot(surfaceNormal, lightDirection) >= 0.0f ? bright : dark; - return new Color(selected.x, selected.y, selected.z, 1.0f); - } - - /// Evaluates the pre-change Toon continuous normal SH implementation with zero quadratic coefficients. - /// The normalized world-space surface normal. - /// The Unity red first-order SH coefficient vector. - /// The Unity green first-order SH coefficient vector. - /// The Unity blue first-order SH coefficient vector. - /// The pre-change continuous nonnegative SH result. - private static Color EvaluateContinuousNormalSh( - Vector3 surfaceNormal, - Vector4 shAr, - Vector4 shAg, - Vector4 shAb - ) - { - Vector4 normal = new Vector4(surfaceNormal.x, surfaceNormal.y, surfaceNormal.z, 1.0f); - Vector3 ambient = Vector3.Max( - new Vector3( - Vector4.Dot(shAr, normal), - Vector4.Dot(shAg, normal), - Vector4.Dot(shAb, normal) - ), - Vector3.zero - ); - return new Color(ambient.x, ambient.y, ambient.z, 1.0f); - } - /// Asserts a fixed Vector3 reference within the half/float-compatible tolerance. /// The fixed reference vector. /// The observed vector. diff --git a/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs b/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs index 3c2f7a6f..0cd059e7 100644 --- a/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs +++ b/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs @@ -69,19 +69,40 @@ public sealed class ShaderCoreTestHostManifestTests /// Ensures every fixed host has one non-empty shader name and module selection. [Test] - public void ManifestContainsTwelveUniqueFixedHostSelections() + public void ManifestContainsThirteenUniqueFixedHostSelections() { var manifest = JsonUtility.FromJson(File.ReadAllText(GetManifestPath())); Assert.That(manifest, Is.Not.Null); Assert.That(manifest.schemaVersion, Is.EqualTo(1)); Assert.That(manifest.hosts, Is.Not.Null); - Assert.That(manifest.hosts.Length, Is.EqualTo(12)); + Assert.That(manifest.hosts.Length, Is.EqualTo(13)); AssertHostSelections(manifest.hosts); AssertManifestRuntimeContracts(manifest.hosts); } + /// Requires the forced-Gamma Toon host to keep its product includes and direct-only diagnostic in their intended passes. + [Test] + public void ToonOpenLitGeneratedSourcePreservesGammaPlacementAndDirectOnlyDiagnostic() + { + HostManifestEntry host = LoadManifest().hosts.Single(entry => + string.Equals( + entry.shaderName, + "PureBase/Tests/ShaderCore/ToonOpenLitGamma", + StringComparison.Ordinal + ) + ); + string assetPath = FindHostAssetPath(host.shaderName); + Shader shader = AssetDatabase.LoadAssetAtPath(assetPath); + AssertImportedShaderIsUsable(host.shaderName, shader); + + string source = LoadGeneratedShaderSource(assetPath, host.shaderName); + AssertExpectedSentinelCounts(host, source); + AssertInactiveSentinelsAreAbsent(host, source); + AssertOpenLitGeneratedSourceContract(host, source); + } + /// Asserts unique shader names and required module and sentinel configuration for every host entry. /// The manifest host entries to inspect. private static void AssertHostSelections(HostManifestEntry[] hosts) @@ -114,6 +135,11 @@ private static void AssertManifestRuntimeContracts(HostManifestEntry[] hosts) Is.EqualTo(1), "Only the Toon shadow host must declare one valid phase-shadow runtime contract." ); + Assert.That( + hosts.Count(HasConfiguredOpenLitRuntimeEvidence), + Is.EqualTo(1), + "Only the Toon OpenLit host must declare one valid Gamma and ForwardAdd runtime contract." + ); } /// Asserts that one host has required unique module and sentinel configuration. @@ -166,6 +192,11 @@ public void ImportedHostsMatchGeneratedSourceContracts() AssertExpectedSentinelCounts(host, source); AssertInactiveSentinelsAreAbsent(host, source); + if (HasConfiguredOpenLitRuntimeEvidence(host)) + { + AssertOpenLitGeneratedSourceContract(host, source); + } + if (HasConfiguredModuleOrder(host)) { AssertModuleOrder(host, source); @@ -280,7 +311,7 @@ private static HostManifest LoadManifest() ); Assert.That(manifest, Is.Not.Null); Assert.That(manifest.schemaVersion, Is.EqualTo(1)); - Assert.That(manifest.hosts, Is.Not.Null.And.Length.EqualTo(12)); + Assert.That(manifest.hosts, Is.Not.Null.And.Length.EqualTo(13)); return manifest; } @@ -330,6 +361,30 @@ private static bool HasConfiguredRuntimeEvidence(HostManifestEntry host) return runtimeEvidence.requireFinite && runtimeEvidence.requireChannelAgreement; } + /// Returns whether the manifest declares the complete forced-Gamma Toon OpenLit runtime contract. + private static bool HasConfiguredOpenLitRuntimeEvidence(HostManifestEntry host) + { + OpenLitRuntimeEvidence runtimeEvidence = host.openLitRuntimeEvidence; + return runtimeEvidence != null + && string.Equals(runtimeEvidence.gammaMacro, "UNITY_COLORSPACE_GAMMA", StringComparison.Ordinal) + && string.Equals( + runtimeEvidence.productModelInclude, + "Packages/jp.penguin.purebase/Shaders/Models/toon.hlsl", + StringComparison.Ordinal + ) + && string.Equals(runtimeEvidence.forwardBasePass, "ForwardBase", StringComparison.Ordinal) + && string.Equals(runtimeEvidence.forwardAddPass, "ForwardAdd", StringComparison.Ordinal) + && runtimeEvidence.absentPasses.SequenceEqual(new[] { "ShadowCaster", "Meta" }) + && string.Equals( + runtimeEvidence.forwardAddSentinel, + "PUREBASE_TEST_TOON_OPENLIT_GAMMA_SENTINEL_SHADE", + StringComparison.Ordinal + ) + && runtimeEvidence.injectedShSetCount == 2 + && runtimeEvidence.requireGammaReadback + && runtimeEvidence.requireDirectOnlyForwardAdd; + } + /// Returns whether the manifest declares the required red, green, and blue phase channels. /// The phase-channel declaration to inspect. /// Whether every phase channel has its expected semantic color. @@ -503,6 +558,12 @@ private static void AssertPassSentinelCount( int expectedCount ) { + if (IsConfiguredAbsentOpenLitPass(host, passName)) + { + Assert.That(expectedCount, Is.Zero); + return; + } + string passSource = GetPassSource(source, passName, host.shaderName); Assert.That( CountOccurrences(passSource, sentinel), @@ -511,6 +572,13 @@ int expectedCount ); } + /// Returns whether a named generated pass is intentionally absent from the fixed Toon OpenLit host. + private static bool IsConfiguredAbsentOpenLitPass(HostManifestEntry host, string passName) + { + return HasConfiguredOpenLitRuntimeEvidence(host) + && host.openLitRuntimeEvidence.absentPasses.Contains(passName); + } + /// Checks that every inactive sentinel is absent from the entire generated source. private static void AssertInactiveSentinelsAreAbsent(HostManifestEntry host, string source) { @@ -583,6 +651,101 @@ int expectedCount ); } + /// Asserts forced-Gamma macro placement and the ForwardAdd-only OpenLit direction diagnostic in generated host source. + private static void AssertOpenLitGeneratedSourceContract( + HostManifestEntry host, + string source + ) + { + Assert.That(HasConfiguredOpenLitRuntimeEvidence(host), Is.True); + OpenLitRuntimeEvidence runtimeEvidence = host.openLitRuntimeEvidence; + string forwardBaseSource = GetPassSource( + source, + runtimeEvidence.forwardBasePass, + host.shaderName + ); + string forwardAddSource = GetPassSource( + source, + runtimeEvidence.forwardAddPass, + host.shaderName + ); + AssertGammaPrecedesProductInclude(host, forwardBaseSource); + AssertGammaPrecedesProductInclude(host, forwardAddSource); + AssertForwardAddDiagnosticIsPreprocessorGated( + host, + forwardBaseSource, + forwardAddSource + ); + AssertConfiguredPassesAreAbsent(host, source); + } + + /// Asserts that the fixed OpenLit host emits only the configured ForwardBase and ForwardAdd product passes. + private static void AssertConfiguredPassesAreAbsent(HostManifestEntry host, string source) + { + foreach (string absentPass in host.openLitRuntimeEvidence.absentPasses) + { + Assert.That( + source.IndexOf($"Name \"{absentPass}\"", StringComparison.Ordinal), + Is.EqualTo(-1), + $"Host '{host.shaderName}' must not emit the unsupported '{absentPass}' pass." + ); + } + } + + /// Asserts that a generated pass defines forced Gamma before identifying the product Toon model include. + private static void AssertGammaPrecedesProductInclude(HostManifestEntry host, string passSource) + { + OpenLitRuntimeEvidence runtimeEvidence = host.openLitRuntimeEvidence; + Match gammaMacro = Regex.Match( + passSource, + @"#\s*define\s+UNITY_COLORSPACE_GAMMA\s+1\b", + RegexOptions.CultureInvariant + ); + Match productModelInclude = Regex.Match( + passSource, + @"#\s*define\s+PUREBASE_MODEL_INCLUDE\s+\""Packages/jp\.penguin\.purebase/Shaders/Models/toon\.hlsl\""", + RegexOptions.CultureInvariant + ); + Assert.That( + gammaMacro.Success, + Is.True, + $"Host '{host.shaderName}' must define '{runtimeEvidence.gammaMacro}' in every configured product pass." + ); + Assert.That( + productModelInclude.Success, + Is.True, + $"Host '{host.shaderName}' must identify '{runtimeEvidence.productModelInclude}' in every configured product pass." + ); + Assert.That( + gammaMacro.Index, + Is.LessThan(productModelInclude.Index), + $"Host '{host.shaderName}' must define '{runtimeEvidence.gammaMacro}' before the product Toon model include." + ); + } + + /// Asserts that generated diagnostic source remains inactive outside the compiled ForwardAdd pass. + private static void AssertForwardAddDiagnosticIsPreprocessorGated( + HostManifestEntry host, + string forwardBaseSource, + string forwardAddSource + ) + { + OpenLitRuntimeEvidence runtimeEvidence = host.openLitRuntimeEvidence; + string forwardAddGuardPattern = + @"#\s*if\s+defined\s*\(\s*UNITY_PASS_FORWARDADD\s*\)[\s\S]*?" + + Regex.Escape(runtimeEvidence.forwardAddSentinel); + Assert.That( + Regex.IsMatch(forwardBaseSource, forwardAddGuardPattern, RegexOptions.CultureInvariant), + Is.True, + $"Host '{host.shaderName}' must guard '{runtimeEvidence.forwardAddSentinel}' with UNITY_PASS_FORWARDADD when the source is emitted in ForwardBase." + ); + Assert.That( + CountOccurrences(forwardAddSource, runtimeEvidence.forwardAddSentinel), + Is.EqualTo(1), + $"Host '{host.shaderName}' must emit '{runtimeEvidence.forwardAddSentinel}' exactly once in ForwardAdd." + ); + } + /// Asserts the configured gate-on versus gate-off metric direction and magnitude. private static void AssertRuntimeDelta( HostManifestEntry host, @@ -811,6 +974,9 @@ private sealed class HostManifestEntry /// Gets the Toon phase-shadow runtime evidence requirements. public RuntimeEvidence runtimeEvidence; + + /// Gets the forced-Gamma Toon OpenLit runtime evidence requirements. + public OpenLitRuntimeEvidence openLitRuntimeEvidence; } /// Stores selected sentinel counts for every generated ShaderLab pass. @@ -888,6 +1054,39 @@ private sealed class RuntimeEvidence public bool requireChannelAgreement; } + /// Stores the forced-Gamma Toon OpenLit generated-source and runtime readback requirements. + [Serializable] + [SuppressMessage("SonarAnalyzer.CSharp", "S3459", Justification = "Unity JsonUtility populates these public fields from the fixed-host manifest.")] + private sealed class OpenLitRuntimeEvidence + { + /// Gets the forced Unity Gamma macro name. + public string gammaMacro; + + /// Gets the product Toon model include path. + public string productModelInclude; + + /// Gets the ForwardBase pass that must compile the Gamma branch. + public string forwardBasePass; + + /// Gets the ForwardAdd pass that exposes the direct-only direction diagnostic. + public string forwardAddPass; + + /// Gets generated passes intentionally excluded from the fixed two-pass host. + public string[] absentPasses; + + /// Gets the Shade diagnostic sentinel emitted only by ForwardAdd. + public string forwardAddSentinel; + + /// Gets the count of injected SH sets required for ForwardAdd isolation. + public int injectedShSetCount; + + /// Gets whether the fixed host requires a Gamma readback contract. + public bool requireGammaReadback; + + /// Gets whether the fixed host requires direct-only ForwardAdd direction evidence. + public bool requireDirectOnlyForwardAdd; + } + /// Stores the RGB channel published by every selected Toon phase. [Serializable] [SuppressMessage("SonarAnalyzer.CSharp", "S3459", Justification = "Unity JsonUtility populates these public fields, and optional reference fields must remain null when their JSON sections are absent.")] diff --git a/Tests/Fixtures/Hosts/ToonOpenLit.meta b/Tests/Fixtures/Hosts/ToonOpenLit.meta new file mode 100644 index 00000000..56b51801 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6d0fa392fd09eff44b65c7b2b8775edd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader new file mode 100644 index 00000000..401b85ff --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader @@ -0,0 +1,98 @@ +Shader "PureBase/Tests/ShaderCore/ToonOpenLitGamma" +{ + // The Apache notice follows because Shader-Core importer discovery requires Shader first. + /* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + // Defines a forced-Gamma product Toon host for OpenLit lighting contracts. + Properties + { + __SC_SHADERLAB_properties__ + [HideInInspector] _SrcBlend ("", Float) = 1 + [HideInInspector] _DstBlend ("", Float) = 0 + [HideInInspector] _ZWrite ("", Float) = 1 + [HideInInspector] _AddSrcBlend ("", Float) = 1 + [HideInInspector] _AddDstBlend ("", Float) = 1 + } + + HLSLINCLUDE + __SC_SHADERKEYWORDS__ + #pragma shader_feature_local _ PUREBASE_RENDERING_OPAQUE PUREBASE_RENDERING_TRANSPARENT + ENDHLSL + + SubShader + { + Tags { "RenderType" = "TransparentCutout" "Queue" = "AlphaTest" } + + Pass + { + Name "ForwardBase" + Tags { "LightMode" = "ForwardBase" } + Stencil + { + Ref [_StencilRef] + ReadMask [_StencilReadMask] + WriteMask [_StencilWriteMask] + Comp [_StencilComp] + Pass [_StencilPass] + Fail [_StencilFail] + ZFail [_StencilZFail] + } + Cull [_Cull] + ZWrite [_ZWrite] + ZTest LEqual + Blend [_SrcBlend] [_DstBlend] + + HLSLPROGRAM + #pragma target 5.0 + #define UNITY_COLORSPACE_GAMMA 1 + #define PUREBASE_MODEL_INCLUDE "Packages/jp.penguin.purebase/Shaders/Models/toon.hlsl" + #include "Packages/jp.lilxyzw.shadercore/ShaderLibrary/birp_forward.hlsl" + #include "Packages/jp.penguin.purebase/Shaders/Common/birp_host.hlsl" + ENDHLSL + } + + Pass + { + Name "ForwardAdd" + Tags { "LightMode" = "ForwardAdd" } + Stencil + { + Ref [_StencilRef] + ReadMask [_StencilReadMask] + WriteMask 0 + Pass Keep + Fail Keep + ZFail Keep + } + Cull [_Cull] + ZWrite Off + ZTest LEqual + Blend [_AddSrcBlend] [_AddDstBlend] + ColorMask RGB + + HLSLPROGRAM + #pragma target 5.0 + #define UNITY_COLORSPACE_GAMMA 1 + #define PUREBASE_MODEL_INCLUDE "Packages/jp.penguin.purebase/Shaders/Models/toon.hlsl" + #include "Packages/jp.lilxyzw.shadercore/ShaderLibrary/birp_forwardadd.hlsl" + #include "Packages/jp.penguin.purebase/Shaders/Common/birp_host.hlsl" + ENDHLSL + } + } + + CustomEditor "SCMaterialEditor" +} diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader.meta b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader.meta new file mode 100644 index 00000000..ed0eec54 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma.scshader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 7a93a6ea2cc84fd3b690a8993bd1d46f +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 11c23ed6ad66fef4699c7e3c88c88784, type: 3} diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl new file mode 100644 index 00000000..22ab6469 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl @@ -0,0 +1,19 @@ +SC_Texture2D(_BaseTexture, "white", [SCMainTexture], "Base Texture", "") +SC_SamplerState(sampler_BaseTexture) +SC_ScaleOffset(_BaseTexture) +SC_color(_BaseColor, (1,1,1,1), [], "Base Color", "") +SC_Texture2D(_SharedMask, "white", [SCMask], "__SharedMask", "") +SC_Texture2DArray(_SharedGradients, "white", [SCGradients], "__SharedGradients", "") +SC_uint(_RenderingMode, 1, [PureBaseRenderingMode], "Rendering Mode", "") +SC_float(_Cutoff, 0.5, [PureBaseCutoff][SCRange(-0.001,1.001)], "Cutoff", "") +SC_float(_Cull, 2, [SCEnum(Off, 0, Front, 1, Back, 2)], "Cull", "") +SC_float(_StencilRef, 0, [SCRangeInt(0,255)], "Stencil Reference", "") +SC_float(_StencilReadMask, 255, [SCRangeInt(0,255)], "Stencil Read Mask", "") +SC_float(_StencilWriteMask, 255, [SCRangeInt(0,255)], "Stencil Write Mask", "") +SC_float(_StencilComp, 8, [SCEnum(UnityEngine.Rendering.CompareFunction)], "Stencil Comparison", "") +SC_float(_StencilPass, 0, [SCEnum(UnityEngine.Rendering.StencilOp)], "Stencil Pass", "") +SC_float(_StencilFail, 0, [SCEnum(UnityEngine.Rendering.StencilOp)], "Stencil Fail", "") +SC_float(_StencilZFail, 0, [SCEnum(UnityEngine.Rendering.StencilOp)], "Stencil Z Fail", "") +SC_Texture2D(_NormalMap, "bump", [], "Normal Map", "") +SC_SamplerState(sampler_NormalMap) +SC_float(_NormalScale, 1, [SCRange(0,2)], "Normal Scale", "") diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl.meta b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl.meta new file mode 100644 index 00000000..810a2637 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/PureBaseTestToonOpenLitGamma_properties.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 56f8de75254e4b0c9fa35fc8ad615c32 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule b/Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule new file mode 100644 index 00000000..113cf0bc --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule @@ -0,0 +1,4 @@ +{ + "name": "PureBase Test Toon OpenLit Gamma", + "uniqueID": "jp.penguin.purebase.tests.shadercore.toonopenlitgamma" +} diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule.meta b/Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule.meta new file mode 100644 index 00000000..db8857b9 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/jp.penguin.purebase.tests.shadercore.toonopenlitgamma.scmodule.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8db1434b1c5a40e9ae62187647f24c1d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl b/Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl new file mode 100644 index 00000000..2ab456b7 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Encodes the direct-only ForwardAdd aggregate direction for fixture readback. + +#if defined(UNITY_PASS_FORWARDADD) +/// Identifies the ForwardAdd direct-direction Shade diagnostic source. +#define PUREBASE_TEST_TOON_OPENLIT_GAMMA_SENTINEL_SHADE 1 + +sd.col.rgb = sd.L * half(0.5) + half(0.5); +sd.add = 0; +sd.postadd = 0; +#endif diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl.meta b/Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl.meta new file mode 100644 index 00000000..9f9f6978 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/phase_shade.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1194cdfa6b9c4fdbb2b8cf75e6fd720e +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl b/Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl new file mode 100644 index 00000000..292078f6 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Resolves Shader-Core's host-local common include through the product Toon support path. + +#ifndef PUREBASE_TEST_TOON_OPENLIT_GAMMA_SC_COMMON_INCLUDED +#define PUREBASE_TEST_TOON_OPENLIT_GAMMA_SC_COMMON_INCLUDED + +#ifndef PUREBASE_MODEL_INCLUDE +#define PUREBASE_MODEL_INCLUDE "Packages/jp.penguin.purebase/Shaders/Models/toon.hlsl" +#endif + +#include "Packages/jp.penguin.purebase/Shaders/sc_common.hlsl" + +#endif diff --git a/Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl.meta b/Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl.meta new file mode 100644 index 00000000..d582f401 --- /dev/null +++ b/Tests/Fixtures/Hosts/ToonOpenLit/sc_common.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3f65c620a183442c85cad85afeb31a2c +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 23badfb14d718ee5471534294457d143bbb4b401 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 15:41:36 +0900 Subject: [PATCH 2/8] fix: align Toon SH with OpenLit - Align Toon direction weighting and two-band SH evaluation with the OpenLit-derived contract. - Validate focused lighting contracts and the clean batchmode Daily regression lane. --- Shaders/Common/birp_host.hlsl | 21 ++- Shaders/Common/toon_lighting.hlsl | 61 +++++-- Shaders/Models/toon.hlsl | 3 +- ...deRenderingTests.OpenLitSourceContracts.cs | 89 ++++++++++ ...deringTests.OpenLitSourceContracts.cs.meta | 11 ++ ...eringModeRenderingTests.SourceContracts.cs | 38 ---- ...reBaseToonLightingContractTests.OpenLit.cs | 33 +--- ...LightingContractTests.OpenLitRegression.cs | 167 ++++++++++++++++++ ...ingContractTests.OpenLitRegression.cs.meta | 11 ++ ...oonLightingContractTests.OpenLitRuntime.cs | 39 ++++ ...ghtingContractTests.OpenLitRuntime.cs.meta | 11 ++ ...reBaseToonLightingContractTests.Runtime.cs | 2 +- 12 files changed, 403 insertions(+), 83 deletions(-) create mode 100644 Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs create mode 100644 Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs.meta create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs.meta create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs.meta diff --git a/Shaders/Common/birp_host.hlsl b/Shaders/Common/birp_host.hlsl index acf46f70..bd975bbb 100644 --- a/Shaders/Common/birp_host.hlsl +++ b/Shaders/Common/birp_host.hlsl @@ -25,6 +25,10 @@ #define SCModelSelectAggregateLightDirection(directAggregateDirection, shAr, shAg, shAb) (dot(directAggregateDirection, directAggregateDirection) == 0 ? half3(0, 0, 0) : normalize(directAggregateDirection)) #endif +#ifndef SCModelEvaluateLightDirectionWeight + #define SCModelEvaluateLightDirectionWeight(lightColor) dot(lightColor, half3(0.333333, 0.333333, 0.333333)) +#endif + /// Accumulates a BIRP light after the Shader-Core per-light phase. void SCCalculateLight(inout SCLightData lightSum, inout SCShadingData sd, inout SCCustomData cd, SCVertexData vertex, SCLightData light) { @@ -40,17 +44,22 @@ void SCCalculateLight(inout SCLightData lightSum, inout SCShadingData sd, inout __SC_PHASE_light__ - lightSum.direction += light.direction * dot(light.color, half3(0.333333, 0.333333, 0.333333)); + lightSum.direction += light.direction * SCModelEvaluateLightDirectionWeight(light.color); lightSum.color += light.color * SCModelEvaluateDirectFactor(sd, light); } -/// Publishes the aggregate light direction and applies the selected model's ambient SH response. +/// Publishes the fallback-inclusive Toon direct direction in ForwardBase and adds Toon SH only when Unity permits it. void SCCalculateEnvironmentLight(inout SCLightData lightSum, inout half3 env, inout SCShadingData sd, inout SCCustomData cd, SCVertexData vertex, half4 shAr, half4 shAg, half4 shAb, half4 shBr, half4 shBg, half4 shBb, half4 shC) { - #if defined(PUREBASE_TOON_MODEL_INCLUDED) && !defined(LIGHTMAP_ON) - sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb); - #if !defined(UNITY_PASS_FORWARDADD) - env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC); + #if defined(PUREBASE_TOON_MODEL_INCLUDED) + #if defined(UNITY_PASS_FORWARDADD) + sd.L = dot(lightSum.direction, lightSum.direction) > 0.000001 ? normalize(lightSum.direction) : half3(0, 0, 0); + #else + sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0)); + #if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH + sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb); + env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC); + #endif #endif #else sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, shAr, shAg, shAb); diff --git a/Shaders/Common/toon_lighting.hlsl b/Shaders/Common/toon_lighting.hlsl index f5bc9d73..b1444281 100644 --- a/Shaders/Common/toon_lighting.hlsl +++ b/Shaders/Common/toon_lighting.hlsl @@ -26,28 +26,67 @@ half PureBaseToonEvaluateDirectFactor(float3 surfaceNormal, float3 lightDirectio return step(0, dot(surfaceNormal, lightDirection)); } -/// Builds a finite Toon band direction from direct-light and spherical-harmonics aggregates. +/// Evaluates the OpenLit-derived direct-light luminance for the active Unity color-space branch. +float PureBaseToonLuminance(float3 rgb) +{ + #if defined(UNITY_COLORSPACE_GAMMA) + return dot(rgb, float3(0.22, 0.707, 0.071)); + #else + return dot(rgb, float3(0.0396819152, 0.458021790, 0.00609653955)); + #endif +} + +/// Builds a finite fallback-inclusive OpenLit Toon band direction from post-light direct and first-order SH aggregates. float3 PureBaseToonComputeLightDirection(float3 directAggregateDirection, float4 shAr, float4 shAg, float4 shAb) { float3 shDirection = (shAr.xyz + shAg.xyz + shAb.xyz) / 3; - float3 directionVector = directAggregateDirection + float3(shDirection.x, abs(shDirection.y), shDirection.z); - if (dot(directionVector, directionVector) <= 0.000001) + float3 fallbackDirection = float3(0.001, 0.002, 0.001); + float3 directionVector = directAggregateDirection + float3(shDirection.x, abs(shDirection.y), shDirection.z) + fallbackDirection; + if (all(directionVector == 0) || !all(isfinite(directionVector))) { - directionVector = float3(0.001, 0.002, 0.001); + directionVector = fallbackDirection; } return normalize(directionVector); } -/// Evaluates the fixed bright and dark spherical-harmonics bands for a Toon surface. +/// Evaluates the OpenLit-derived L0/L2 SH base shared by the bright and dark Toon bands. +float3 PureBaseToonEvaluateShL0L2(float3 V, float4 shAr, float4 shAg, float4 shAb, float4 shBr, float4 shBg, float4 shBb, float4 shC) +{ + float4 quadratic = V.xyzz * V.yzzx; + return float3(shAr.w, shAg.w, shAb.w) + + float3(dot(shBr, quadratic), dot(shBg, quadratic), dot(shBb, quadratic)) + + shC.rgb * (V.x * V.x - V.y * V.y); +} + +/// Evaluates a first-order SH term along the supplied direction. +float3 PureBaseToonEvaluateShL1(float3 direction, float4 shAr, float4 shAg, float4 shAb) +{ + return float3(dot(shAr.rgb, direction), dot(shAg.rgb, direction), dot(shAb.rgb, direction)); +} + +/// Evaluates the finite dark-band L1 term along the summed first-order SH direction. +float3 PureBaseToonEvaluateDarkShL1(float4 shAr, float4 shAg, float4 shAb) +{ + float3 shDirection = shAr.xyz + shAg.xyz + shAb.xyz; + if (all(shDirection == 0) || !all(isfinite(shDirection))) + { + return float3(0, 0, 0); + } + + return PureBaseToonEvaluateShL1(normalize(shDirection), shAr, shAg, shAb); +} + +/// Evaluates the OpenLit-derived bright and dark SH bands before selecting the Toon surface-facing band. float3 PureBaseToonEvaluateTwoBandSh(float3 surfaceNormal, float3 L, float4 shAr, float4 shAg, float4 shAb, float4 shBr, float4 shBg, float4 shBb, float4 shC) { - float3 E = L * 0.666666; - float4 quadratic = E.xyzz * E.yzzx; - float3 base = float3(shAr.w, shAg.w, shAb.w) + float3(dot(shBr, quadratic), dot(shBg, quadratic), dot(shBb, quadratic)) + shC.rgb * (E.x * E.x - E.y * E.y); - float3 linearTerm = float3(dot(shAr.xyz, E), dot(shAg.xyz, E), dot(shAb.xyz, E)); - float3 bright = max(base + linearTerm, 0); - float3 dark = max(base - linearTerm, 0); + float3 base = PureBaseToonEvaluateShL0L2(L, shAr, shAg, shAb, shBr, shBg, shBb, shC); + float3 bright = base + PureBaseToonEvaluateShL1(L, shAr, shAg, shAb); + float3 dark = base + PureBaseToonEvaluateDarkShL1(shAr, shAg, shAb); + #if defined(UNITY_COLORSPACE_GAMMA) + bright = LinearToGammaSpace(bright); + dark = LinearToGammaSpace(dark); + #endif return lerp(dark, bright, step(0, dot(surfaceNormal, L))); } diff --git a/Shaders/Models/toon.hlsl b/Shaders/Models/toon.hlsl index d24e8eb4..c59fb609 100644 --- a/Shaders/Models/toon.hlsl +++ b/Shaders/Models/toon.hlsl @@ -64,7 +64,7 @@ half3 SCModelSelectMainLightDirection(SCVertexData vertex, half3 lightDirection) return lightDirection; } -/// Evaluates the supplied Unity spherical-harmonics coefficients as fixed bright and dark Toon bands. +/// Evaluates the supplied Unity spherical-harmonics coefficients as OpenLit-derived bright and dark Toon bands. half3 SCModelEvaluateAmbient(SCShadingData shadingData, half4 shAr, half4 shAg, half4 shAb, half4 shBr, half4 shBg, half4 shBb, half4 shC) { return PureBaseToonEvaluateTwoBandSh(shadingData.N, shadingData.L, shAr, shAg, shAb, shBr, shBg, shBb, shC); @@ -95,5 +95,6 @@ half4 SCModelAddSurfaceColor(SCShadingData shadingData, SCCustomData customData, } #define SCModelSelectAggregateLightDirection(directAggregateDirection, shAr, shAg, shAb) PureBaseToonComputeLightDirection(directAggregateDirection, shAr, shAg, shAb) +#define SCModelEvaluateLightDirectionWeight(lightColor) PureBaseToonLuminance(lightColor) #endif \ No newline at end of file diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs new file mode 100644 index 00000000..197986b1 --- /dev/null +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs @@ -0,0 +1,89 @@ +/* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Defines focused source contracts for the OpenLit-derived Toon lighting integration. + +using System; +using System.Text.RegularExpressions; +using NUnit.Framework; + +namespace PureBase.Tests.Daily +{ + /// Defines focused source contracts for the OpenLit-derived Toon lighting integration. + public sealed partial class PureBaseRenderingModeRenderingTests + { + /// Requires the OpenLit-derived Toon equation and rejects the superseded scaled or inverted SH approximation. + /// The Toon-only lighting helper source. + private static void AssertOpenLitToonEquationContracts(string helper) + { + StringAssert.Contains("float3(0.22, 0.707, 0.071)", helper); + StringAssert.Contains("float3(0.0396819152, 0.458021790, 0.00609653955)", helper); + StringAssert.Contains("float3(0.001, 0.002, 0.001)", helper); + StringAssert.Contains("UNITY_COLORSPACE_GAMMA", helper); + Assert.That(Regex.IsMatch(helper, @"normalize\s*\(\s*shDirection\s*\)"), Is.True, "The dark L1 direction must derive from the summed SH coefficients."); + Assert.That(Regex.IsMatch(helper, @"if\s*\(\s*all\s*\(\s*shDirection\s*==\s*0\s*\)\s*\|\|\s*!all\s*\(\s*isfinite\s*\(\s*shDirection\s*\)\s*\)\s*\)"), Is.True, "The dark L1 direction must only reject exact-zero or nonfinite summed SH coefficients."); + Assert.That(Regex.IsMatch(helper, @"dot\s*\(\s*shDirection\s*,\s*shDirection\s*\)\s*<=\s*0\.000001"), Is.False, "The dark L1 direction must not discard finite near-cancellation residuals."); + Assert.That(Regex.IsMatch(helper, @"float3\s+E\s*=\s*L\s*\*\s*0\.666666"), Is.False, "OpenLit bright L0/L2 and L1 must use unscaled V."); + Assert.That(Regex.IsMatch(helper, @"base\s*-\s*linearTerm"), Is.False, "OpenLit dark L1 must not invert the bright L1 term."); + Assert.That(Regex.IsMatch(helper, @"\bsd\.shadow\b"), Is.False, "Toon direction and SH evaluation must remain visibility-independent."); + } + + /// Requires the fixed fallback to enter the Toon direction sum before normalization. + /// The Toon-only lighting helper source. + private static void AssertOpenLitFallbackPrecedesNormalization(string helper) + { + StringAssert.Contains("float3 fallbackDirection = float3(0.001, 0.002, 0.001);", helper); + Assert.That( + Regex.IsMatch( + helper, + @"float3\s+directionVector\s*=\s*directAggregateDirection\s*\+\s*float3\s*\(\s*shDirection\.x\s*,\s*abs\s*\(\s*shDirection\.y\s*\)\s*,\s*shDirection\.z\s*\)\s*\+\s*fallbackDirection\s*;[\s\S]*?return\s+normalize\s*\(\s*directionVector\s*\)\s*;", + RegexOptions.Singleline + ), + Is.True, + "Toon direction must add the fixed fallback to the direct and SH direction sum before normalization." + ); + } + + /// Requires Toon-only SH gates and direct-only ForwardAdd direction publication in the shared host. + /// The common BIRP fragment host source. + private static void AssertOpenLitHostGateContracts(string host) + { + const string fallbackDirectDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0));"; + const string shDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb);"; + const string toonAmbient = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; + int fallbackDirectIndex = RequireIndex(host, fallbackDirectDirection); + int shGateIndex = RequireIndex(host, "#if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH"); + int shDirectionIndex = RequireIndex(host, shDirection); + int toonAmbientIndex = RequireIndex(host, toonAmbient); + + Assert.That(Regex.IsMatch(host, @"#if\s+defined\(PUREBASE_TOON_MODEL_INCLUDED\)\s*&&\s*!defined\(LIGHTMAP_ON\)"), Is.False, "Fallback-inclusive direct direction must not be owned by the obsolete combined Toon/lightmap gate."); + Assert.That(fallbackDirectIndex, Is.LessThan(shGateIndex), "ForwardBase must publish fallback-inclusive direct direction before deciding whether Toon SH is available."); + Assert.That(shGateIndex, Is.LessThan(shDirectionIndex), "Toon SH direction must remain inside the nested no-lightmap Unity SH gate."); + Assert.That(shDirectionIndex, Is.LessThan(toonAmbientIndex), "The Toon ambient band must remain after its SH direction contribution inside the nested gate."); + Assert.That( + Regex.IsMatch( + host, + @"#else\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\)\);\s*#if\s*!defined\(LIGHTMAP_ON\)\s*&&\s*UNITY_SHOULD_SAMPLE_SH\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb\);\s*env\s*\+=\s*SCModelEvaluateAmbient\(sd,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb,\s*unity_SHBr,\s*unity_SHBg,\s*unity_SHBb,\s*unity_SHC\);\s*#endif", + RegexOptions.Singleline + ), + Is.True, + "Only the nested ForwardBase no-lightmap Unity SH gate may add Toon SH direction and ambient bands." + ); + Assert.That(Regex.IsMatch(host, @"#if\s+defined\(UNITY_PASS_FORWARDADD\)[\s\S]*?sd\.L\s*=\s*dot\(lightSum\.direction\s*,\s*lightSum\.direction\)\s*>\s*0\.000001\s*\?\s*normalize\(lightSum\.direction\)\s*:\s*(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)"), Is.True, "ForwardAdd must publish normalized direct direction or zero without SH fallback."); + Assert.That(Regex.Matches(host, @"\bsd\.L\s*=\s*[^;]*?(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)").Count, Is.EqualTo(1), "Only ForwardAdd may reset sd.L to zero; lightmap and SH-disabled ForwardBase branches must retain fallback-inclusive direct direction."); + } + } +} diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs.meta b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs.meta new file mode 100644 index 00000000..fa494d8f --- /dev/null +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35d75250ec4e4421bc6be8b67d2ea3cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs index 71cf72af..ef25ce1a 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs @@ -561,44 +561,6 @@ private static void AssertLightingPhaseOrder(string host) Assert.That(add, Is.LessThan(postPixel)); } - /// Requires the OpenLit-derived Toon equation and rejects the superseded scaled or inverted SH approximation. - /// The Toon-only lighting helper source. - private static void AssertOpenLitToonEquationContracts(string helper) - { - StringAssert.Contains("float3(0.22, 0.707, 0.071)", helper); - StringAssert.Contains("float3(0.0396819152, 0.458021790, 0.00609653955)", helper); - StringAssert.Contains("float3(0.001, 0.002, 0.001)", helper); - StringAssert.Contains("UNITY_COLORSPACE_GAMMA", helper); - Assert.That(Regex.IsMatch(helper, @"normalize\s*\(\s*shAr\.xyz\s*\+\s*shAg\.xyz\s*\+\s*shAb\.xyz\s*\)"), Is.True, "The dark L1 direction must derive from the summed SH coefficients."); - Assert.That(Regex.IsMatch(helper, @"float3\s+E\s*=\s*L\s*\*\s*0\.666666"), Is.False, "OpenLit bright L0/L2 and L1 must use unscaled V."); - Assert.That(Regex.IsMatch(helper, @"base\s*-\s*linearTerm"), Is.False, "OpenLit dark L1 must not invert the bright L1 term."); - Assert.That(Regex.IsMatch(helper, @"\bsd\.shadow\b"), Is.False, "Toon direction and SH evaluation must remain visibility-independent."); - } - - /// Requires the fixed fallback to enter the Toon direction sum before normalization. - /// The Toon-only lighting helper source. - private static void AssertOpenLitFallbackPrecedesNormalization(string helper) - { - Assert.That( - Regex.IsMatch( - helper, - @"float3\s+directionVector\s*=\s*directAggregateDirection\s*\+\s*float3\s*\(\s*shDirection\.x\s*,\s*abs\s*\(\s*shDirection\.y\s*\)\s*,\s*shDirection\.z\s*\)\s*\+\s*float3\s*\(\s*0\.001\s*,\s*0\.002\s*,\s*0\.001\s*\)\s*;[\s\S]*?return\s+normalize\s*\(\s*directionVector\s*\)\s*;", - RegexOptions.Singleline - ), - Is.True, - "Toon direction must add the fixed fallback to the direct and SH direction sum before normalization." - ); - } - - /// Requires Toon-only SH gates and direct-only ForwardAdd direction publication in the shared host. - /// The common BIRP fragment host source. - private static void AssertOpenLitHostGateContracts(string host) - { - Assert.That(Regex.IsMatch(host, @"#if\s+defined\(PUREBASE_TOON_MODEL_INCLUDED\)\s*&&\s*!defined\(LIGHTMAP_ON\)"), Is.True, "Toon SH must remain disabled while Shader-Core owns lightmaps."); - Assert.That(Regex.IsMatch(host, @"#if[^\r\n]*\bUNITY_SHOULD_SAMPLE_SH\b"), Is.True, "Toon SH must obey Unity's SH sampling gate."); - Assert.That(Regex.IsMatch(host, @"#if\s+defined\(UNITY_PASS_FORWARDADD\)[\s\S]*?sd\.L\s*=\s*dot\(lightSum\.direction\s*,\s*lightSum\.direction\)\s*>\s*0\.000001\s*\?\s*normalize\(lightSum\.direction\)\s*:\s*(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)"), Is.True, "ForwardAdd must publish normalized direct direction or zero without SH fallback."); - } - /// Requires pass-bounded Stencil policy while preserving the existing pass and rendering-mode keyword ABI. [Test] public void ProductGeneratedSourcesExposeStencilPassContractsWithoutNewVariantsOrPasses() diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs index 76275edd..72562e8b 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLit.cs @@ -278,7 +278,7 @@ string label coefficients ); AssertFinite(actual, label + " readback"); - AssertColor(expected, actual, label + " readback"); + AssertRuntimeColor(expected, actual, label + " readback"); } /// Creates a second full SH input set whose first-order, L2, and C vectors differ from the primary OpenLit fixture. @@ -304,28 +304,6 @@ private static void AssertImportedToonOpenLitGammaHost() Assert.That(shader.isSupported, Is.True, "The temporary Toon OpenLit Gamma host shader is unsupported."); } - /// Renders one ForwardAdd diagnostic using the selected host without changing any persistent Shader-Core selection. - private static Color RenderToonOpenLitForwardAddDiagnostic( - ToonLightingCaptureScope capture, - Vector4 lightColor, - ShCoefficients coefficients - ) - { - return capture.RenderLightWithCookie( - ToonOpenLitGammaShaderName, - "ForwardAdd", - new LightCaptureRequest - { - normal = Vector3.forward, - lightColor = lightColor, - lightPosition = new Vector4(0.0f, 0.0f, 1.0f, 0.0f), - coefficients = coefficients, - lightType = LightType.Directional, - lightCount = lightColor == Vector4.zero ? 0 : 1, - } - ); - } - /// Decodes the host Shade diagnostic from its centered RGB representation. private static Vector3 DecodeToonOpenLitForwardAddDirection(Color readback) { @@ -426,9 +404,12 @@ private static Vector3 EvaluateOpenLitDarkL1(ShCoefficients coefficients) Vector3 shDirection = new Vector3(coefficients.ar.x, coefficients.ar.y, coefficients.ar.z) + new Vector3(coefficients.ag.x, coefficients.ag.y, coefficients.ag.z) + new Vector3(coefficients.ab.x, coefficients.ab.y, coefficients.ab.z); - return shDirection.sqrMagnitude <= 0.000001f - ? Vector3.zero - : EvaluateOpenLitL1(shDirection.normalized, coefficients); + if (!IsFinite(shDirection) || (shDirection.x == 0.0f && shDirection.y == 0.0f && shDirection.z == 0.0f)) + { + return Vector3.zero; + } + + return EvaluateOpenLitL1(shDirection.normalized, coefficients); } /// Evaluates the selected OpenLit bright or dark SH band and performs Gamma-only linear-to-sRGB conversion. diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs new file mode 100644 index 00000000..f7fa3883 --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs @@ -0,0 +1,167 @@ +/* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Defines focused OpenLit regression and runtime-readback contracts for Toon lighting. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using NUnit.Framework; +using UnityEngine; + +namespace PureBase.Tests.Daily +{ + /// Defines focused OpenLit regression and runtime-readback contracts for Toon lighting. + [SuppressMessage("SonarAnalyzer.CSharp", "S2333", Justification = "This declaration remains partial so focused OpenLit regressions stay separate from the numerical oracle.")] + public sealed partial class PureBaseToonLightingContractTests + { + /// Defines the D3D11 Linear product readback tolerance for the Shader-Core half and float execution boundary. + private const float RuntimeReadbackTolerance = 0.002f; + + /// Identifies the product Toon lighting include relative to the Unity project root. + private const string ToonLightingRelativePath = "Packages/jp.penguin.purebase/Shaders/Common/toon_lighting.hlsl"; + + /// Identifies the product BIRP host include relative to the Unity project root. + private const string BirpHostRelativePath = "Packages/jp.penguin.purebase/Shaders/Common/birp_host.hlsl"; + + /// Requires finite nonzero direct and SH cancellation residuals to normalize without being replaced by the fallback direction. + [Test] + public void OpenLitDirectionNormalizesFiniteNonzeroCancellationResidual() + { + Vector3 fallbackDirection = new Vector3(0.001f, 0.002f, 0.001f); + Vector3 directAggregateDirection = -fallbackDirection + new Vector3(0.0005f, 0.0f, 0.0f); + Vector3 actual = EvaluateOpenLitDominantDirection( + directAggregateDirection, + Vector4.zero, + Vector4.zero, + Vector4.zero + ); + string source = ReadPackageSource(ToonLightingRelativePath); + + AssertVector(Vector3.right, actual, "OpenLit near-cancellation residual direction"); + StringAssert.Contains("if (all(directionVector == 0) || !all(isfinite(directionVector)))", source); + StringAssert.DoesNotContain("dot(directionVector, directionVector) <= 0.000001", source); + } + + /// Requires every finite nonzero summed first-order SH direction to contribute to the OpenLit dark L1 band. + [Test] + public void OpenLitDarkL1NormalizesFiniteNearCancellation() + { + ShCoefficients coefficients = new ShCoefficients( + new Vector4(1.0f, 0.0f, 0.0f, 0.0f), + new Vector4(-0.9995f, 0.0f, 0.0f, 0.0f), + Vector4.zero, + Vector4.zero, + Vector4.zero, + Vector4.zero, + Vector4.zero + ); + Vector3 expectedDarkL1 = new Vector3(1.0f, -0.9995f, 0.0f); + Vector3 darkL1 = EvaluateOpenLitDarkL1(coefficients); + Color darkBand = EvaluateOpenLitTwoBandSh(-Vector3.right, Vector3.right, coefficients, false); + + AssertVector(expectedDarkL1, darkL1, "OpenLit finite near-cancellation dark L1"); + AssertColor(new Color(expectedDarkL1.x, expectedDarkL1.y, expectedDarkL1.z, 1.0f), darkBand, "OpenLit finite near-cancellation dark band"); + } + + /// Requires Toon ForwardBase lightmap variants to retain fallback-inclusive direct direction without adding Toon SH direction or ambient bands. + [Test] + public void ToonForwardBaseLightmapPublishesFallbackDirectDirectionWithoutToonShBand() + { + string source = ReadPackageSource(BirpHostRelativePath); + + AssertForwardBaseDirectDirectionPrecedesToonShGate(source, "LIGHTMAP_ON"); + } + + /// Requires Toon ForwardBase SH-disabled variants to retain fallback-inclusive direct direction without adding Toon SH direction or ambient bands. + [Test] + public void ToonForwardBaseShDisabledPublishesFallbackDirectDirectionWithoutToonShBand() + { + string source = ReadPackageSource(BirpHostRelativePath); + + AssertForwardBaseDirectDirectionPrecedesToonShGate(source, "UNITY_SHOULD_SAMPLE_SH"); + } + + /// Renders one ForwardAdd diagnostic using the selected host without changing any persistent Shader-Core selection. + /// The isolated Toon lighting capture. + /// The diagnostic directional light color. + /// The injected spherical-harmonic coefficients. + /// The isolated ForwardAdd diagnostic readback. + private static Color RenderToonOpenLitForwardAddDiagnostic( + ToonLightingCaptureScope capture, + Vector4 lightColor, + ShCoefficients coefficients + ) + { + return capture.RenderForwardAddLightDifference( + ToonOpenLitGammaShaderName, + new LightCaptureRequest + { + normal = Vector3.forward, + lightColor = lightColor, + lightPosition = new Vector4(0.0f, 0.0f, 1.0f, 0.0f), + coefficients = coefficients, + lightType = LightType.Directional, + lightCount = lightColor == Vector4.zero ? 0 : 1, + } + ); + } + + /// Asserts the fallback direct direction is assigned before the gate that permits Toon SH direction and ambient evaluation. + /// The BIRP host source. + /// The disabled feature whose gate must retain direct direction. + private static void AssertForwardBaseDirectDirectionPrecedesToonShGate(string source, string disabledFeature) + { + const string directAssignment = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0));"; + const string toonShGate = "#if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH"; + const string toonShDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb);"; + const string toonAmbientBand = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; + int directIndex = source.IndexOf(directAssignment, StringComparison.Ordinal); + int gateIndex = source.IndexOf(toonShGate, StringComparison.Ordinal); + int shDirectionIndex = source.IndexOf(toonShDirection, StringComparison.Ordinal); + int ambientBandIndex = source.IndexOf(toonAmbientBand, StringComparison.Ordinal); + + StringAssert.Contains(disabledFeature, toonShGate); + Assert.That(directIndex, Is.GreaterThanOrEqualTo(0), "ForwardBase must initialize sd.L from the fallback-inclusive direct aggregate."); + Assert.That(gateIndex, Is.GreaterThan(directIndex), "Only Toon SH augmentation may be gated after the direct direction is published."); + Assert.That(shDirectionIndex, Is.GreaterThan(gateIndex), "Toon SH direction must remain inside the SH gate."); + Assert.That(ambientBandIndex, Is.GreaterThan(gateIndex), "Toon ambient band evaluation must remain inside the SH gate."); + } + + /// Asserts a Linear product readback using the bounded D3D11 Shader-Core half and float tolerance. + /// The oracle color. + /// The product readback color. + /// The diagnostic assertion label. + private static void AssertRuntimeColor(Color expected, Color actual, string label) + { + Assert.That(actual.r, Is.EqualTo(expected.r).Within(RuntimeReadbackTolerance), label + " red"); + Assert.That(actual.g, Is.EqualTo(expected.g).Within(RuntimeReadbackTolerance), label + " green"); + Assert.That(actual.b, Is.EqualTo(expected.b).Within(RuntimeReadbackTolerance), label + " blue"); + } + + /// Reads one package-owned source file from Unity's project root. + /// The slash-separated path relative to the Unity project root. + /// The source file contents. + private static string ReadPackageSource(string relativePath) + { + string projectRoot = Directory.GetParent(Application.dataPath).FullName; + string path = Path.Combine(projectRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); + + Assert.That(File.Exists(path), Is.True, "Required package source file is missing: " + relativePath); + return File.ReadAllText(path); + } + } +} diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs.meta b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs.meta new file mode 100644 index 00000000..f9de58e5 --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a9c5e2835f94bbd8d675aa3e91b40f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs new file mode 100644 index 00000000..521780a0 --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Defines the OpenLit-specific ForwardAdd runtime capture operation. + +using UnityEngine; + +namespace PureBase.Tests.Daily +{ + /// Defines the OpenLit-specific ForwardAdd runtime capture operation. + public sealed partial class PureBaseToonLightingContractTests + { + /// Owns the OpenLit-specific runtime capture extensions. + private partial class ToonLightingCaptureRuntimeScope + { + /// Renders the isolated second-light ForwardAdd contribution for a named diagnostic shader. + /// The required diagnostic shader name. + /// The coherent directional light and spherical-harmonic input. + /// The isolated ForwardAdd center linear readback. + public Color RenderForwardAddLightDifference(string shaderName, LightCaptureRequest request) + { + return RenderLightDifference(CreateProductMaterial(shaderName, "ForwardAdd", 0.0f), request); + } + } + } +} diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs.meta b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs.meta new file mode 100644 index 00000000..0884638b --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f5369bf48f44e16a41ca7ed608bd7a3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs index 40ea1aa2..589d5ba5 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.Runtime.cs @@ -150,7 +150,7 @@ public LightCaptureRequest(Texture cookie = null) } /// Owns one isolated regular-render fixture and restores every Unity global it changes. - private class ToonLightingCaptureRuntimeScope : IDisposable + private partial class ToonLightingCaptureRuntimeScope : IDisposable { /// Stores the dedicated layer used by the preview-scene renderer and lights. private const int FixtureLayer = 31; From 20f259c9d0baf067b3a8e5edbc333aff902d03dc Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 16:14:16 +0900 Subject: [PATCH 3/8] docs: document OpenLit Toon lighting - Document the OpenLit-derived Toon lighting contract and attribution boundaries in English and Japanese. - Add transient lilToon classification observation and validate the Daily lighting integration suite. --- Docs/pure-base-shader-contract.md | 14 ++ Docs/technical-information.ja.md | 12 + Docs/technical-information.md | 12 + NOTICE | 4 +- ...ightingContractTests.LilToonObservation.cs | 223 ++++++++++++++++++ ...ngContractTests.LilToonObservation.cs.meta | 2 + 6 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs create mode 100644 Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs.meta diff --git a/Docs/pure-base-shader-contract.md b/Docs/pure-base-shader-contract.md index 15b19102..1ae3a556 100644 --- a/Docs/pure-base-shader-contract.md +++ b/Docs/pure-base-shader-contract.md @@ -151,6 +151,20 @@ Unlit returns the base surface without host direct, baked, ambient, or environme Toon evaluates a binary direct diffuse response from the surface normal and light direction. Its `ForwardBase` direction combines the Shader-Core direct aggregate with the first-order SH direction, and its ambient result selects a fixed bright or dark SH band from that direction. Shader-Core continues to provide the lightmap input; when Shader-Core supplies the lightmap aggregate, Toon does not synthesize an additional baked-light contribution. `ForwardAdd` contributes direct light only. +### OpenLit-derived Toon direction and SH bands + +The module-free Toon host uses a bounded Pure Base adaptation of selected OpenLit 1.0.2 BIRP concepts. It retains Shader-Core's post-`light` aggregation boundary rather than copying OpenLit's `ComputeLights` or replacing Shader-Core light enumeration. + +- The post-`light` direct aggregate weights each `light.color` with the OpenLit color-space luminance coefficients: `(0.22, 0.707, 0.071)` under `UNITY_COLORSPACE_GAMMA`, or `(0.0396819152, 0.458021790, 0.00609653955)` in Linear. Module changes made through the established `light` phase therefore remain part of the Toon aggregate. +- The scene direction adds the positive-Y first-order SH direction to that direct aggregate. The direction vector is `directAggregate + ((shAr.rgb + shAg.rgb + shAb.rgb) / 3)` with the Y component made positive, plus the fixed fallback `(0.001, 0.002, 0.001)`. The fallback is added before normalization, including for a nonzero aggregate. Exact-zero or nonfinite direction vectors use the fallback; finite near-cancellation residuals are normalized normally. +- In the supported normal BIRP scope, OpenLit `GetV` is the identity: the SH evaluator uses the selected scene direction without a camera or world-position dependency. Light Volumes, direction override, and other expanded OpenLit scope are not implemented. +- The bright band evaluates the unscaled identity-BIRP `V` using the L0/L2 base plus the L1 term along `V`. The dark band reuses the same L0/L2 base and evaluates L1 along the normalized SH RGB direction `normalize(shAr.rgb + shAg.rgb + shAb.rgb)`. An exact-zero or nonfinite SH direction contributes zero dark-band L1 so the result remains finite. +- Both bands are assembled before color-space conversion. Gamma converts both assembled bands with Unity's Linear-to-sRGB conversion; Linear leaves both assembled bands unconverted. The selected band remains the binary result of `step(0, dot(surfaceNormal, lightDirection))`. + +These equations are Pure Base's narrow reimplementation of inspected OpenLit 1.0.2 concepts as used by lilToon 2.3.4. They do not copy upstream function bodies, add an OpenLit dependency, or imply an official lilToon/OpenLit association. + +The ownership boundaries remain explicit: `ForwardAdd` publishes and uses only normalized direct aggregate direction when its squared length is greater than `0.000001`, otherwise zero; it adds no SH direction, fallback, or environment band. `LIGHTMAP_ON` and disabled Unity SH sampling preserve the direct direction but suppress Toon-generated SH bands, while Shader-Core owns lightmap decoding and Mixed/Subtractive handling. `sd.shadow` affects host-managed direct Toon radiance exactly once and never direction or SH evaluation. PBR, Hybrid, and Unlit retain their existing lighting paths and do not inherit Toon's OpenLit-derived helper. + ### Toon direct-light visibility contract For `PureBase/Toon`, the per-light `light.color` exposed to the `light` phase is the scene/direct light color multiplied by non-shadow distance, spot, and cookie attenuation. Unity effective visibility is published separately as `sd.shadow` before the `light` phase, so the same value is available to the `modifylight` and `shade` phases. This contract applies across the supported Unity light-kind branches, including directional, point, spot, point-cookie, and directional-cookie inputs. diff --git a/Docs/technical-information.ja.md b/Docs/technical-information.ja.md index cd82839c..71420678 100644 --- a/Docs/technical-information.ja.md +++ b/Docs/technical-information.ja.md @@ -121,6 +121,18 @@ Hybrid は PBR の経路の中にある既存の2値化直接拡散反射の式 この固定されたホスト動作によって、公開項目、キーワード、パス、バリアント、依存関係は増えません。公開プロパティ ABI は変わらないため、既存のマテリアルを移行する必要はなく、自動的にこの動作を受け取ります。 +### OpenLit 由来の Toon 動作と provenance + +Pure Base の追加モジュールなし Toon ライティングは、lilToon 2.3.4 を通して確認した OpenLit 1.0.2 の Built-in Render Pipeline 向け概念のうち、限定した部分を適応したものです。Shader-Core の `light` 後の直接光集計境界を維持し、OpenLit の `ComputeLights` をコピーしたり、Shader-Core のライト列挙を置き換えたりしません。 + +- Toon は `light` フェーズ後の直接光集計を、色空間に応じた OpenLit の luminance で重み付けします。Gamma では `(0.22, 0.707, 0.071)`、Linear では `(0.0396819152, 0.458021790, 0.00609653955)` を使用します。そのため、既存の `light` フェーズでモジュールが変更した値も集計に含まれます。 +- 方向はその集計、正の Y を持つ1次 SH の方向、固定 fallback `(0.001, 0.002, 0.001)` を加えてから正規化します。fallback は常に和へ含め、合計が完全なゼロまたは有限でない場合はこのベクトルを使います。有限な近相殺の残差は通常どおり正規化します。 +- 通常の BIRP では、OpenLit の `GetV` に相当する動作はホストが選んだ方向をそのまま使う identity です。カメラやワールド座標への依存、Light Volumes、方向の上書き、その他の拡張された OpenLit 範囲はありません。 +- 明るい SH 帯は unscaled な `V` による L0/L2 の基底と L1 を使います。暗い SH 帯は同じ L0/L2 の基底を再利用し、正規化した SH RGB 方向に沿って L1 を評価します。SH 方向がゼロまたは有限でない場合、暗い帯の L1 をゼロにして結果を有限に保ちます。 +- Gamma では組み立て済みの両方の帯へ Unity の Linear-to-sRGB 変換を適用し、Linear では変換しません。明暗の選択は、表面法線とシーン方向の内積の符号による2値判定です。 + +`ForwardAdd` は直接光だけを扱います。直接光集計の二乗長が `0.000001` より大きい場合は正規化した方向を使い、それ以外ではゼロとし、SH、fallback、環境光の帯は加えません。ライトマップのデコードと Mixed/Subtractive の処理は Shader-Core が担当し、`LIGHTMAP_ON` または Unity の SH サンプリング無効時は Toon が生成する SH を抑制します。`sd.shadow` は Toon のホスト管理直接放射輝度へ1回だけ作用し、集計方向や SH には作用しません。PBR、Hybrid、Unlit の動作は変更されません。 + ### Toon の直接光と可視性の契約 `PureBase/Toon` の `light` 差し込み位置へ渡す各ライトの `light.color` は、シーンの直接光の色に、影以外の距離・スポット・クッキー減衰を乗じた値です。Unity のライト単位の実効可視性は `sd.shadow` として分離して公開され、`light` の前に設定されるため、`modifylight` と `shade` からも同じ値を参照できます。この分離は、対応する方向ライト、ポイントライト、スポットライト、ポイントクッキー、方向クッキーの各ライト分岐に適用されます。 diff --git a/Docs/technical-information.md b/Docs/technical-information.md index 5216437e..b1a704a4 100644 --- a/Docs/technical-information.md +++ b/Docs/technical-information.md @@ -121,6 +121,18 @@ Hybrid retains its unchanged binary direct-diffuse equation inside the PBR path. This fixed host behavior adds no public property, keyword, pass, variant, or dependency. The public property ABI is unchanged, so existing materials need no migration and receive the behavior automatically. +### OpenLit-derived Toon behavior and provenance + +Pure Base's module-free Toon lighting is a bounded adaptation of selected OpenLit 1.0.2 BIRP concepts inspected through lilToon 2.3.4. It keeps Shader-Core's post-`light` direct-light aggregation and does not copy OpenLit's `ComputeLights` or replace Shader-Core light enumeration. + +- Toon weights the post-`light` direct aggregate with color-space-specific OpenLit luminance: `(0.22, 0.707, 0.071)` for Gamma and `(0.0396819152, 0.458021790, 0.00609653955)` for Linear. The aggregate therefore continues to include module-authored `light` changes. +- Its direction combines that aggregate, positive-Y first-order SH, and the fixed fallback `(0.001, 0.002, 0.001)` before normalization. The fallback is always part of the sum; exact-zero or nonfinite totals fall back to that vector, while finite near-cancellation residuals remain normalizable. +- In normal BIRP, OpenLit `GetV` is represented by the identity direction used by the host. There is no camera/position dependency, Light Volumes behavior, direction override, or other expanded OpenLit scope. +- The bright SH band uses unscaled `V` for the L0/L2 base and L1. The dark band reuses that L0/L2 base and uses L1 along the normalized SH RGB direction. A zero or nonfinite SH direction contributes zero dark L1 to keep the result finite. +- Gamma applies Unity's Linear-to-sRGB conversion to both assembled bands; Linear does not convert them. Binary band selection uses the sign of the surface-normal and scene-direction dot product. + +`ForwardAdd` is direct-only: it uses normalized direct aggregate direction above the `0.000001` squared-length threshold and zero otherwise, without SH, fallback, or environment contribution. Lightmap decoding and Mixed/Subtractive handling remain Shader-Core-owned, and Toon-generated SH is suppressed for `LIGHTMAP_ON` or disabled Unity SH sampling. `sd.shadow` changes direct Toon radiance once, never the aggregate direction or SH. PBR, Hybrid, and Unlit are unchanged. + ### Toon direct-light visibility contract For `PureBase/Toon`, `light.color` in the `light` phase is the scene/direct light color multiplied by non-shadow distance, spot, and cookie attenuation. Unity effective per-light visibility is exposed independently through `sd.shadow` before `light`, `modifylight`, and `shade`. The same split is used for the supported directional, point, spot, point-cookie, and directional-cookie light branches. diff --git a/NOTICE b/NOTICE index fe4e33b6..33f9b4d3 100644 --- a/NOTICE +++ b/NOTICE @@ -20,11 +20,11 @@ License This project is licensed under the Apache-2.0 License - see the LICENSE file for details Third-Party Lighting Concept Provenance -PureBase's minimal Toon lighting helper is a narrow adapted/reimplemented use of the identified lilToon 2.3.4/OpenLit 1.0.2 lighting concepts, including the corresponding SH bright/dark constants and operation structure. It is not a broad lilToon or OpenLit integration or a full upstream code copy. This acknowledgement does not imply endorsement, sponsorship, contributor status, or participation by lilToon or OpenLit developers. +Pure Base's minimal Toon lighting helper is a narrow, independently written reimplementation of selected OpenLit 1.0.2 BIRP concepts inspected through lilToon 2.3.4: color-space luminance, positive-Y SH direction and fallback handling, and the bright/dark SH term structure. The Pure Base adaptation retains Shader-Core's post-light aggregation and does not copy `ComputeLights` or any upstream function body. It adds no OpenLit or lilToon dependency and is not an official lilToon or OpenLit integration. This acknowledgement does not imply endorsement, sponsorship, contributor status, or participation by lilToon or OpenLit developers. Inspected lilToon sources -- `Shader/Includes/openlit_core.hlsl`: `ComputeLightDirection`, `ShadeSH9ToonDouble`, and `ComputeLights` +- `Shader/Includes/openlit_core.hlsl`: `GetV`, `OpenLitLuminance`, `ComputeLightDirection`, `ShadeSH9ToonDouble`, `ComputeSHLightsAndDirection`, and `ComputeLights` - `Shader/Includes/lil_common_functions.hlsl`: SH direction and bright/dark decomposition - `Shader/Includes/lil_common_macro.hlsl`: ForwardAdd and lightmap ownership context diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs new file mode 100644 index 00000000..34525d97 --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs @@ -0,0 +1,223 @@ +/* + * Copyright 2026 Penguin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Records a transient lilToon 2.3.4 BIRP bright/dark classification observation for the OpenLit runtime inputs. + +using System.IO; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +namespace PureBase.Tests.Daily +{ + /// Records a transient lilToon 2.3.4 BIRP bright/dark classification observation for the OpenLit runtime inputs. + public sealed partial class PureBaseToonLightingContractTests + { + /// Identifies the installed lilToon BIRP shader observed by this supplemental test. + private const string LilToonShaderName = "lilToon"; + + /// Identifies the installed lilToon package manifest relative to the Unity project root. + private const string LilToonPackageManifestRelativePath = "Packages/jp.lilxyzw.liltoon/package.json"; + + /// Identifies the VRC Light Volumes enable global that must remain disabled for this observation. + private const string UdonLightVolumeEnabledGlobalName = "_UdonLightVolumeEnabled"; + + /// Records top, side, and bottom lilToon output classifications without treating final lilToon RGB as a parity result. + [Test] + public void LilToon234BirpObservationRecordsTopSideBottomClassificationAndTransitionOrientation() + { + Assert.That(SystemInfo.graphicsDeviceType, Is.EqualTo(GraphicsDeviceType.Direct3D11)); + Assert.That(QualitySettings.activeColorSpace, Is.EqualTo(ColorSpace.Linear)); + AssertInstalledLilToon234(); + + ShCoefficients coefficients = CreateOpenLitCoefficients(); + Vector3 lightDirection = EvaluateOpenLitDominantDirection( + Vector3.zero, + coefficients.ar, + coefficients.ag, + coefficients.ab + ); + + using (var capture = new ToonLightingCaptureScope()) + { + Color top = capture.RenderLilToonOpenLitObservation(lightDirection, lightDirection, coefficients); + Color side = capture.RenderLilToonOpenLitObservation(Vector3.right, lightDirection, coefficients); + Color bottom = capture.RenderLilToonOpenLitObservation(-lightDirection, lightDirection, coefficients); + + AssertFinite(top, "lilToon 2.3.4 top BIRP observation"); + AssertFinite(side, "lilToon 2.3.4 side BIRP observation"); + AssertFinite(bottom, "lilToon 2.3.4 bottom BIRP observation"); + RecordLilToonObservation(lightDirection, coefficients, top, side, bottom); + } + } + + /// Requires the read-only comparison target to be the installed lilToon 2.3.4 package. + private static void AssertInstalledLilToon234() + { + string projectRoot = Directory.GetParent(Application.dataPath).FullName; + string manifestPath = Path.Combine(projectRoot, LilToonPackageManifestRelativePath.Replace('/', Path.DirectorySeparatorChar)); + + Assert.That(File.Exists(manifestPath), Is.True, "The installed lilToon package manifest is unavailable for the BIRP observation."); + StringAssert.Contains("\"version\": \"2.3.4\"", File.ReadAllText(manifestPath)); + } + + /// Writes the reproducible classification observation and its known non-parity RGB boundaries to the NUnit result. + /// The normalized OpenLit direction used for all three samples. + /// The seven Unity SH vectors shared with the formal OpenLit runtime test. + /// The top-normal lilToon BIRP readback. + /// The side-normal lilToon BIRP readback. + /// The bottom-normal lilToon BIRP readback. + private static void RecordLilToonObservation( + Vector3 lightDirection, + ShCoefficients coefficients, + Color top, + Color side, + Color bottom + ) + { + float topLuminance = EvaluateObservationLuminance(top); + float sideLuminance = EvaluateObservationLuminance(side); + float bottomLuminance = EvaluateObservationLuminance(bottom); + float midpoint = (topLuminance + bottomLuminance) * 0.5f; + + TestContext.WriteLine( + "lilToon 2.3.4 BIRP OpenLit observation | " + + "directColor=(0,0,0), direction=" + lightDirection + + ", SHAr=" + coefficients.ar + ", SHAg=" + coefficients.ag + ", SHAb=" + coefficients.ab + + ", SHBr=" + coefficients.br + ", SHBg=" + coefficients.bg + ", SHBb=" + coefficients.bb + ", SHC=" + coefficients.c + + ", top=" + DescribeObservation(top, topLuminance, midpoint) + + ", side=" + DescribeObservation(side, sideLuminance, midpoint) + + ", bottom=" + DescribeObservation(bottom, bottomLuminance, midpoint) + + ", orientation=" + DescribeTransitionOrientation(topLuminance, sideLuminance, bottomLuminance) + + "; RGB is descriptive only: lilToon CorrectLights and shade threshold/color stages are outside Pure Base parity." + ); + } + + /// Calculates the Linear luminance used only to label a supplemental lilToon observation. + /// The finite linear BIRP readback. + /// The observation-only luminance. + private static float EvaluateObservationLuminance(Color color) + { + return color.r * 0.2126f + color.g * 0.7152f + color.b * 0.0722f; + } + + /// Formats one color readback with its observed bright, dark, or transition classification. + /// The observed linear BIRP color. + /// The observation-only luminance. + /// The midpoint between top and bottom luminance. + /// A stable NUnit output fragment. + private static string DescribeObservation(Color color, float luminance, float midpoint) + { + const float classificationTolerance = 0.001f; + string classification = luminance > midpoint + classificationTolerance + ? "bright" + : luminance < midpoint - classificationTolerance + ? "dark" + : "transition"; + return "rgb=" + color + ", luminance=" + luminance + ", classification=" + classification; + } + + /// Describes the observed top-to-bottom transition without asserting a lilToon final-color expectation. + /// The top-normal luminance. + /// The side-normal luminance. + /// The bottom-normal luminance. + /// The observed monotonic orientation or its explicit non-monotonic state. + private static string DescribeTransitionOrientation(float top, float side, float bottom) + { + const float transitionTolerance = 0.001f; + if (top >= side - transitionTolerance + && side >= bottom - transitionTolerance + && top > bottom + transitionTolerance) + { + return "top-to-bottom darkening"; + } + + if (top <= side + transitionTolerance + && side <= bottom + transitionTolerance + && top < bottom - transitionTolerance) + { + return "top-to-bottom brightening"; + } + + return Mathf.Abs(top - bottom) <= transitionTolerance ? "flat" : "non-monotonic"; + } + + /// Owns the lilToon-specific extension of the existing transient BIRP capture scope. + private partial class ToonLightingCaptureRuntimeScope + { + /// Renders one lilToon ForwardBase observation with all available nonessential correction controls neutralized. + /// The uniform world-space normal for the observed surface. + /// The normalized OpenLit direction retained across every sample. + /// The seven injected Unity SH vectors. + /// The center linear readback used only for observation output. + public Color RenderLilToonOpenLitObservation(Vector3 normal, Vector3 lightDirection, ShCoefficients coefficients) + { + float originalLightVolumeEnabled = Shader.GetGlobalFloat(UdonLightVolumeEnabledGlobalName); + Material material = CreateLilToonObservationMaterial(); + try + { + ConfigureLilToonObservationMaterial(material); + Shader.SetGlobalFloat(UdonLightVolumeEnabledGlobalName, 0.0f); + return RenderWithLights( + material, + CreateDirectionalLightCaptureRequest( + normal, + Vector4.zero, + new Vector4(lightDirection.x, lightDirection.y, lightDirection.z, 0.0f), + coefficients + ) + ); + } + finally + { + Shader.SetGlobalFloat(UdonLightVolumeEnabledGlobalName, originalLightVolumeEnabled); + } + } + + /// Creates a registered transient lilToon material without imposing a Pure Base named-pass contract. + /// The capture-owned lilToon material for BIRP Forward rendering. + private Material CreateLilToonObservationMaterial() + { + Shader shader = Shader.Find(LilToonShaderName); + Assert.That(shader, Is.Not.Null, "Installed lilToon 2.3.4 BIRP shader is unavailable."); + Assert.That(ShaderUtil.ShaderHasError(shader), Is.False, "Installed lilToon 2.3.4 BIRP shader has compiler errors."); + + var material = new Material(shader) { hideFlags = HideFlags.HideAndDontSave }; + materials.Add(material); + ConfigureMaterial(material, 0.0f); + return material; + } + + /// Neutralizes material-local lilToon corrections while retaining its shade stage for classification observation. + /// The capture-owned transient lilToon material. + private static void ConfigureLilToonObservationMaterial(Material material) + { + material.SetTexture("_MainTex", Texture2D.whiteTexture); + material.SetColor("_Color", Color.white); + material.SetFloat("_LightMinLimit", 0.0f); + material.SetFloat("_LightMaxLimit", 10.0f); + material.SetFloat("_MonochromeLighting", 0.0f); + material.SetFloat("_AsUnlit", 0.0f); + material.SetFloat("_lilDirectionalLightStrength", 1.0f); + material.SetFloat("_UseShadow", 1.0f); + material.SetVector("_LightDirectionOverride", new Vector4(0.001f, 0.002f, 0.001f, 0.0f)); + material.DisableKeyword("LTCGI"); + material.DisableKeyword("LIL_LTCGI"); + } + } + } +} diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs.meta b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs.meta new file mode 100644 index 00000000..2a604b7d --- /dev/null +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e821ce8c79ec4d5ea4ebbbb9862f03fe From 3bcfa716f40ecdc61cf8ab8011f663b4c621c01e Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 17:30:43 +0900 Subject: [PATCH 4/8] test: tolerate absent lilToon observation - Record missing optional lilToon as a passing supplemental observation before rendering. - Verify installed and CI-equivalent Daily runs pass with no skipped tests. --- ...ightingContractTests.LilToonObservation.cs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs index 34525d97..f3523dde 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs @@ -24,7 +24,7 @@ namespace PureBase.Tests.Daily { - /// Records a transient lilToon 2.3.4 BIRP bright/dark classification observation for the OpenLit runtime inputs. + /// Records lilToon 2.3.4 BIRP availability and, when installed, a transient bright/dark classification observation for the OpenLit runtime inputs. public sealed partial class PureBaseToonLightingContractTests { /// Identifies the installed lilToon BIRP shader observed by this supplemental test. @@ -36,14 +36,17 @@ public sealed partial class PureBaseToonLightingContractTests /// Identifies the VRC Light Volumes enable global that must remain disabled for this observation. private const string UdonLightVolumeEnabledGlobalName = "_UdonLightVolumeEnabled"; - /// Records top, side, and bottom lilToon output classifications without treating final lilToon RGB as a parity result. + /// Records lilToon availability and, when available, top, side, and bottom output classifications without treating final lilToon RGB as a parity result. [Test] public void LilToon234BirpObservationRecordsTopSideBottomClassificationAndTransitionOrientation() { + if (!AssertInstalledLilToon234()) + { + return; + } + Assert.That(SystemInfo.graphicsDeviceType, Is.EqualTo(GraphicsDeviceType.Direct3D11)); Assert.That(QualitySettings.activeColorSpace, Is.EqualTo(ColorSpace.Linear)); - AssertInstalledLilToon234(); - ShCoefficients coefficients = CreateOpenLitCoefficients(); Vector3 lightDirection = EvaluateOpenLitDominantDirection( Vector3.zero, @@ -65,14 +68,23 @@ public void LilToon234BirpObservationRecordsTopSideBottomClassificationAndTransi } } - /// Requires the read-only comparison target to be the installed lilToon 2.3.4 package. - private static void AssertInstalledLilToon234() + /// Records optional package unavailability and requires version 2.3.4 when the package is installed. + /// True when the lilToon observation should run; otherwise false after recording package unavailability. + private static bool AssertInstalledLilToon234() { string projectRoot = Directory.GetParent(Application.dataPath).FullName; string manifestPath = Path.Combine(projectRoot, LilToonPackageManifestRelativePath.Replace('/', Path.DirectorySeparatorChar)); - Assert.That(File.Exists(manifestPath), Is.True, "The installed lilToon package manifest is unavailable for the BIRP observation."); + if (!File.Exists(manifestPath)) + { + TestContext.WriteLine( + "The supplemental lilToon 2.3.4 BIRP observation was not run because the optional package is unavailable." + ); + return false; + } + StringAssert.Contains("\"version\": \"2.3.4\"", File.ReadAllText(manifestPath)); + return true; } /// Writes the reproducible classification observation and its known non-parity RGB boundaries to the NUnit result. From a3f69a64fdf1f6ffea5d0c1bc8eedd98fda7ae88 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 19:12:42 +0900 Subject: [PATCH 5/8] test: refine Codacy test contracts - Normalize generated-source assertions and nest OpenLit contract helpers without changing test fixture behavior. - Verify focused Daily tests and local static analysis pass with no new findings. --- ...deRenderingTests.OpenLitSourceContracts.cs | 114 +++++++++--------- ...eringModeRenderingTests.SourceContracts.cs | 8 +- .../Editor/ShaderCoreTestHostManifestTests.cs | 2 +- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs index 197986b1..9de0b8cc 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs @@ -25,65 +25,69 @@ namespace PureBase.Tests.Daily /// Defines focused source contracts for the OpenLit-derived Toon lighting integration. public sealed partial class PureBaseRenderingModeRenderingTests { - /// Requires the OpenLit-derived Toon equation and rejects the superseded scaled or inverted SH approximation. - /// The Toon-only lighting helper source. - private static void AssertOpenLitToonEquationContracts(string helper) + /// Provides source-contract assertions for the OpenLit-derived Toon lighting integration. + private static class OpenLitSourceContractAssertions { - StringAssert.Contains("float3(0.22, 0.707, 0.071)", helper); - StringAssert.Contains("float3(0.0396819152, 0.458021790, 0.00609653955)", helper); - StringAssert.Contains("float3(0.001, 0.002, 0.001)", helper); - StringAssert.Contains("UNITY_COLORSPACE_GAMMA", helper); - Assert.That(Regex.IsMatch(helper, @"normalize\s*\(\s*shDirection\s*\)"), Is.True, "The dark L1 direction must derive from the summed SH coefficients."); - Assert.That(Regex.IsMatch(helper, @"if\s*\(\s*all\s*\(\s*shDirection\s*==\s*0\s*\)\s*\|\|\s*!all\s*\(\s*isfinite\s*\(\s*shDirection\s*\)\s*\)\s*\)"), Is.True, "The dark L1 direction must only reject exact-zero or nonfinite summed SH coefficients."); - Assert.That(Regex.IsMatch(helper, @"dot\s*\(\s*shDirection\s*,\s*shDirection\s*\)\s*<=\s*0\.000001"), Is.False, "The dark L1 direction must not discard finite near-cancellation residuals."); - Assert.That(Regex.IsMatch(helper, @"float3\s+E\s*=\s*L\s*\*\s*0\.666666"), Is.False, "OpenLit bright L0/L2 and L1 must use unscaled V."); - Assert.That(Regex.IsMatch(helper, @"base\s*-\s*linearTerm"), Is.False, "OpenLit dark L1 must not invert the bright L1 term."); - Assert.That(Regex.IsMatch(helper, @"\bsd\.shadow\b"), Is.False, "Toon direction and SH evaluation must remain visibility-independent."); - } + /// Requires the OpenLit-derived Toon equation and rejects the superseded scaled or inverted SH approximation. + /// The Toon-only lighting helper source. + private static void AssertOpenLitToonEquationContracts(string helper) + { + StringAssert.Contains("float3(0.22, 0.707, 0.071)", helper); + StringAssert.Contains("float3(0.0396819152, 0.458021790, 0.00609653955)", helper); + StringAssert.Contains("float3(0.001, 0.002, 0.001)", helper); + StringAssert.Contains("UNITY_COLORSPACE_GAMMA", helper); + Assert.That(Regex.IsMatch(helper, @"normalize\s*\(\s*shDirection\s*\)"), Is.True, "The dark L1 direction must derive from the summed SH coefficients."); + Assert.That(Regex.IsMatch(helper, @"if\s*\(\s*all\s*\(\s*shDirection\s*==\s*0\s*\)\s*\|\|\s*!all\s*\(\s*isfinite\s*\(\s*shDirection\s*\)\s*\)\s*\)"), Is.True, "The dark L1 direction must only reject exact-zero or nonfinite summed SH coefficients."); + Assert.That(Regex.IsMatch(helper, @"dot\s*\(\s*shDirection\s*,\s*shDirection\s*\)\s*<=\s*0\.000001"), Is.False, "The dark L1 direction must not discard finite near-cancellation residuals."); + Assert.That(Regex.IsMatch(helper, @"float3\s+E\s*=\s*L\s*\*\s*0\.666666"), Is.False, "OpenLit bright L0/L2 and L1 must use unscaled V."); + Assert.That(Regex.IsMatch(helper, @"base\s*-\s*linearTerm"), Is.False, "OpenLit dark L1 must not invert the bright L1 term."); + Assert.That(Regex.IsMatch(helper, @"\bsd\.shadow\b"), Is.False, "Toon direction and SH evaluation must remain visibility-independent."); + } - /// Requires the fixed fallback to enter the Toon direction sum before normalization. - /// The Toon-only lighting helper source. - private static void AssertOpenLitFallbackPrecedesNormalization(string helper) - { - StringAssert.Contains("float3 fallbackDirection = float3(0.001, 0.002, 0.001);", helper); - Assert.That( - Regex.IsMatch( - helper, - @"float3\s+directionVector\s*=\s*directAggregateDirection\s*\+\s*float3\s*\(\s*shDirection\.x\s*,\s*abs\s*\(\s*shDirection\.y\s*\)\s*,\s*shDirection\.z\s*\)\s*\+\s*fallbackDirection\s*;[\s\S]*?return\s+normalize\s*\(\s*directionVector\s*\)\s*;", - RegexOptions.Singleline - ), - Is.True, - "Toon direction must add the fixed fallback to the direct and SH direction sum before normalization." - ); - } + /// Requires the fixed fallback to enter the Toon direction sum before normalization. + /// The Toon-only lighting helper source. + private static void AssertOpenLitFallbackPrecedesNormalization(string helper) + { + StringAssert.Contains("float3 fallbackDirection = float3(0.001, 0.002, 0.001);", helper); + Assert.That( + Regex.IsMatch( + helper, + @"float3\s+directionVector\s*=\s*directAggregateDirection\s*\+\s*float3\s*\(\s*shDirection\.x\s*,\s*abs\s*\(\s*shDirection\.y\s*\)\s*,\s*shDirection\.z\s*\)\s*\+\s*fallbackDirection\s*;[\s\S]*?return\s+normalize\s*\(\s*directionVector\s*\)\s*;", + RegexOptions.Singleline + ), + Is.True, + "Toon direction must add the fixed fallback to the direct and SH direction sum before normalization." + ); + } - /// Requires Toon-only SH gates and direct-only ForwardAdd direction publication in the shared host. - /// The common BIRP fragment host source. - private static void AssertOpenLitHostGateContracts(string host) - { - const string fallbackDirectDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0));"; - const string shDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb);"; - const string toonAmbient = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; - int fallbackDirectIndex = RequireIndex(host, fallbackDirectDirection); - int shGateIndex = RequireIndex(host, "#if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH"); - int shDirectionIndex = RequireIndex(host, shDirection); - int toonAmbientIndex = RequireIndex(host, toonAmbient); + /// Requires Toon-only SH gates and direct-only ForwardAdd direction publication in the shared host. + /// The common BIRP fragment host source. + private static void AssertOpenLitHostGateContracts(string host) + { + const string fallbackDirectDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0));"; + const string shDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb);"; + const string toonAmbient = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; + int fallbackDirectIndex = RequireIndex(host, fallbackDirectDirection); + int shGateIndex = RequireIndex(host, "#if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH"); + int shDirectionIndex = RequireIndex(host, shDirection); + int toonAmbientIndex = RequireIndex(host, toonAmbient); - Assert.That(Regex.IsMatch(host, @"#if\s+defined\(PUREBASE_TOON_MODEL_INCLUDED\)\s*&&\s*!defined\(LIGHTMAP_ON\)"), Is.False, "Fallback-inclusive direct direction must not be owned by the obsolete combined Toon/lightmap gate."); - Assert.That(fallbackDirectIndex, Is.LessThan(shGateIndex), "ForwardBase must publish fallback-inclusive direct direction before deciding whether Toon SH is available."); - Assert.That(shGateIndex, Is.LessThan(shDirectionIndex), "Toon SH direction must remain inside the nested no-lightmap Unity SH gate."); - Assert.That(shDirectionIndex, Is.LessThan(toonAmbientIndex), "The Toon ambient band must remain after its SH direction contribution inside the nested gate."); - Assert.That( - Regex.IsMatch( - host, - @"#else\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\)\);\s*#if\s*!defined\(LIGHTMAP_ON\)\s*&&\s*UNITY_SHOULD_SAMPLE_SH\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb\);\s*env\s*\+=\s*SCModelEvaluateAmbient\(sd,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb,\s*unity_SHBr,\s*unity_SHBg,\s*unity_SHBb,\s*unity_SHC\);\s*#endif", - RegexOptions.Singleline - ), - Is.True, - "Only the nested ForwardBase no-lightmap Unity SH gate may add Toon SH direction and ambient bands." - ); - Assert.That(Regex.IsMatch(host, @"#if\s+defined\(UNITY_PASS_FORWARDADD\)[\s\S]*?sd\.L\s*=\s*dot\(lightSum\.direction\s*,\s*lightSum\.direction\)\s*>\s*0\.000001\s*\?\s*normalize\(lightSum\.direction\)\s*:\s*(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)"), Is.True, "ForwardAdd must publish normalized direct direction or zero without SH fallback."); - Assert.That(Regex.Matches(host, @"\bsd\.L\s*=\s*[^;]*?(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)").Count, Is.EqualTo(1), "Only ForwardAdd may reset sd.L to zero; lightmap and SH-disabled ForwardBase branches must retain fallback-inclusive direct direction."); + Assert.That(Regex.IsMatch(host, @"#if\s+defined\(PUREBASE_TOON_MODEL_INCLUDED\)\s*&&\s*!defined\(LIGHTMAP_ON\)"), Is.False, "Fallback-inclusive direct direction must not be owned by the obsolete combined Toon/lightmap gate."); + Assert.That(fallbackDirectIndex, Is.LessThan(shGateIndex), "ForwardBase must publish fallback-inclusive direct direction before deciding whether Toon SH is available."); + Assert.That(shGateIndex, Is.LessThan(shDirectionIndex), "Toon SH direction must remain inside the nested no-lightmap Unity SH gate."); + Assert.That(shDirectionIndex, Is.LessThan(toonAmbientIndex), "The Toon ambient band must remain after its SH direction contribution inside the nested gate."); + Assert.That( + Regex.IsMatch( + host, + @"#else\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\)\);\s*#if\s*!defined\(LIGHTMAP_ON\)\s*&&\s*UNITY_SHOULD_SAMPLE_SH\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb\);\s*env\s*\+=\s*SCModelEvaluateAmbient\(sd,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb,\s*unity_SHBr,\s*unity_SHBg,\s*unity_SHBb,\s*unity_SHC\);\s*#endif", + RegexOptions.Singleline + ), + Is.True, + "Only the nested ForwardBase no-lightmap Unity SH gate may add Toon SH direction and ambient bands." + ); + Assert.That(Regex.IsMatch(host, @"#if\s+defined\(UNITY_PASS_FORWARDADD\)[\s\S]*?sd\.L\s*=\s*dot\(lightSum\.direction\s*,\s*lightSum\.direction\)\s*>\s*0\.000001\s*\?\s*normalize\(lightSum\.direction\)\s*:\s*(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)"), Is.True, "ForwardAdd must publish normalized direct direction or zero without SH fallback."); + Assert.That(Regex.Matches(host, @"\bsd\.L\s*=\s*[^;]*?(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)").Count, Is.EqualTo(1), "Only ForwardAdd may reset sd.L to zero; lightmap and SH-disabled ForwardBase branches must retain fallback-inclusive direct direction."); + } } } } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs index ef25ce1a..49f9c7f6 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs @@ -188,14 +188,14 @@ public void ToonLightingOwnershipKeepsBinaryDirectTwoBandShaderCoreLightmapsAndF AssertBirpHostForwardAddAndLightmapContracts(host, shaderCoreLighting); AssertPbrAndHybridLightingOwnership(pbr, pbrBrdf, hybrid); AssertLightingPhaseOrder(host); - AssertOpenLitFallbackPrecedesNormalization(helper); + OpenLitSourceContractAssertions.AssertOpenLitFallbackPrecedesNormalization(helper); } /// Requires the Toon direction fallback to affect nondegenerate aggregates before normalization. [Test] public void ToonOpenLitFallbackIsAddedBeforeDirectionNormalization() { - AssertOpenLitFallbackPrecedesNormalization(File.ReadAllText(ToonLightingHelperPath)); + OpenLitSourceContractAssertions.AssertOpenLitFallbackPrecedesNormalization(File.ReadAllText(ToonLightingHelperPath)); } /// Requires Toon to separate non-shadow attenuation from Unity effective visibility before Shader-Core light phases. @@ -489,7 +489,7 @@ private static void AssertToonHelperAndModelContracts(string toon, string helper helper, "The Toon helper must own fixed bright and dark environment band interpretation." ); - AssertOpenLitToonEquationContracts(helper); + OpenLitSourceContractAssertions.AssertOpenLitToonEquationContracts(helper); StringAssert.Contains( "#include \"Packages/jp.penguin.purebase/Shaders/Common/toon_lighting.hlsl\"", toon, @@ -508,7 +508,7 @@ private static void AssertBirpHostForwardAddAndLightmapContracts(string host, st StringAssert.Contains("env = SCModelSelectEnvironmentLighting(env);", host); StringAssert.Contains("sd.lightColor = lightSum.color + env;", host); StringAssert.Contains("sd.lightColor = lightSum.color;", host); - AssertOpenLitHostGateContracts(host); + OpenLitSourceContractAssertions.AssertOpenLitHostGateContracts(host); Assert.That( RequireIndex(host, "#if defined(UNITY_PASS_FORWARDADD)"), Is.LessThan(RequireIndex(host, "env = SCModelSelectEnvironmentLighting(env);")), diff --git a/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs b/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs index 0cd059e7..cef9efe7 100644 --- a/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs +++ b/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs @@ -703,7 +703,7 @@ private static void AssertGammaPrecedesProductInclude(HostManifestEntry host, st ); Match productModelInclude = Regex.Match( passSource, - @"#\s*define\s+PUREBASE_MODEL_INCLUDE\s+\""Packages/jp\.penguin\.purebase/Shaders/Models/toon\.hlsl\""", + @"#\s*define\s+PUREBASE_MODEL_INCLUDE\s+""Packages/jp\.penguin\.purebase/Shaders/Models/toon\.hlsl""", RegexOptions.CultureInvariant ); Assert.That( From 2c741a7ec0eab7fe1dc3b0c5b1a43eb7aaf73408 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 20:19:55 +0900 Subject: [PATCH 6/8] test: repair OpenLit contract checks - Keep OpenLit source-contract helpers private to the fixture while allowing partial callers to compile. - Restore Toon ambient assertions and move OpenLit tests into their fixture partial for Codacy analysis. --- ...deRenderingTests.OpenLitSourceContracts.cs | 36 ++++++++++++++++--- ...eringModeRenderingTests.SourceContracts.cs | 26 -------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs index 9de0b8cc..16f704a9 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs @@ -17,6 +17,7 @@ // Defines focused source contracts for the OpenLit-derived Toon lighting integration. using System; +using System.IO; using System.Text.RegularExpressions; using NUnit.Framework; @@ -30,7 +31,7 @@ private static class OpenLitSourceContractAssertions { /// Requires the OpenLit-derived Toon equation and rejects the superseded scaled or inverted SH approximation. /// The Toon-only lighting helper source. - private static void AssertOpenLitToonEquationContracts(string helper) + internal static void AssertOpenLitToonEquationContracts(string helper) { StringAssert.Contains("float3(0.22, 0.707, 0.071)", helper); StringAssert.Contains("float3(0.0396819152, 0.458021790, 0.00609653955)", helper); @@ -46,7 +47,7 @@ private static void AssertOpenLitToonEquationContracts(string helper) /// Requires the fixed fallback to enter the Toon direction sum before normalization. /// The Toon-only lighting helper source. - private static void AssertOpenLitFallbackPrecedesNormalization(string helper) + internal static void AssertOpenLitFallbackPrecedesNormalization(string helper) { StringAssert.Contains("float3 fallbackDirection = float3(0.001, 0.002, 0.001);", helper); Assert.That( @@ -62,11 +63,11 @@ private static void AssertOpenLitFallbackPrecedesNormalization(string helper) /// Requires Toon-only SH gates and direct-only ForwardAdd direction publication in the shared host. /// The common BIRP fragment host source. - private static void AssertOpenLitHostGateContracts(string host) + internal static void AssertOpenLitHostGateContracts(string host) { const string fallbackDirectDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0));"; const string shDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb);"; - const string toonAmbient = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; + const string toonAmbient = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; int fallbackDirectIndex = RequireIndex(host, fallbackDirectDirection); int shGateIndex = RequireIndex(host, "#if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH"); int shDirectionIndex = RequireIndex(host, shDirection); @@ -88,6 +89,33 @@ private static void AssertOpenLitHostGateContracts(string host) Assert.That(Regex.IsMatch(host, @"#if\s+defined\(UNITY_PASS_FORWARDADD\)[\s\S]*?sd\.L\s*=\s*dot\(lightSum\.direction\s*,\s*lightSum\.direction\)\s*>\s*0\.000001\s*\?\s*normalize\(lightSum\.direction\)\s*:\s*(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)"), Is.True, "ForwardAdd must publish normalized direct direction or zero without SH fallback."); Assert.That(Regex.Matches(host, @"\bsd\.L\s*=\s*[^;]*?(?:half|float)3\(0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\)").Count, Is.EqualTo(1), "Only ForwardAdd may reset sd.L to zero; lightmap and SH-disabled ForwardBase branches must retain fallback-inclusive direct direction."); } + + } + + /// Requires Toon-owned binary direct and two-band environment lighting with Shader-Core lightmap and ForwardAdd isolation. + [Test] + public void ToonLightingOwnershipKeepsBinaryDirectTwoBandShaderCoreLightmapsAndForwardAddIsolation() + { + string toon = File.ReadAllText(ToonModelPath); + string helper = File.ReadAllText(ToonLightingHelperPath); + string host = File.ReadAllText(BirpHostPath); + string shaderCoreLighting = File.ReadAllText(ShaderCoreBirpLightingPath); + string pbr = File.ReadAllText(PbrModelPath); + string pbrBrdf = File.ReadAllText(PbrBrdfPath); + string hybrid = File.ReadAllText(HybridModelPath); + + AssertToonHelperAndModelContracts(toon, helper); + AssertBirpHostForwardAddAndLightmapContracts(host, shaderCoreLighting); + AssertPbrAndHybridLightingOwnership(pbr, pbrBrdf, hybrid); + AssertLightingPhaseOrder(host); + OpenLitSourceContractAssertions.AssertOpenLitFallbackPrecedesNormalization(helper); + } + + /// Requires the Toon direction fallback to affect nondegenerate aggregates before normalization. + [Test] + public void ToonOpenLitFallbackIsAddedBeforeDirectionNormalization() + { + OpenLitSourceContractAssertions.AssertOpenLitFallbackPrecedesNormalization(File.ReadAllText(ToonLightingHelperPath)); } } } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs index 49f9c7f6..626f7420 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs @@ -172,32 +172,6 @@ public void BirpHostPreservesModeAlphaFogPostPixelAndForwardAddSourceOrder() StringAssert.Contains("sd.col.a = half(0.25)", File.ReadAllText(PostPixelProbePath)); } - /// Requires Toon-owned binary direct and two-band environment lighting with Shader-Core lightmap and ForwardAdd isolation. - [Test] - public void ToonLightingOwnershipKeepsBinaryDirectTwoBandShaderCoreLightmapsAndForwardAddIsolation() - { - string toon = File.ReadAllText(ToonModelPath); - string helper = File.ReadAllText(ToonLightingHelperPath); - string host = File.ReadAllText(BirpHostPath); - string shaderCoreLighting = File.ReadAllText(ShaderCoreBirpLightingPath); - string pbr = File.ReadAllText(PbrModelPath); - string pbrBrdf = File.ReadAllText(PbrBrdfPath); - string hybrid = File.ReadAllText(HybridModelPath); - - AssertToonHelperAndModelContracts(toon, helper); - AssertBirpHostForwardAddAndLightmapContracts(host, shaderCoreLighting); - AssertPbrAndHybridLightingOwnership(pbr, pbrBrdf, hybrid); - AssertLightingPhaseOrder(host); - OpenLitSourceContractAssertions.AssertOpenLitFallbackPrecedesNormalization(helper); - } - - /// Requires the Toon direction fallback to affect nondegenerate aggregates before normalization. - [Test] - public void ToonOpenLitFallbackIsAddedBeforeDirectionNormalization() - { - OpenLitSourceContractAssertions.AssertOpenLitFallbackPrecedesNormalization(File.ReadAllText(ToonLightingHelperPath)); - } - /// Requires Toon to separate non-shadow attenuation from Unity effective visibility before Shader-Core light phases. [Test] public void ToonShadowSeparationRequiresSplitInputsModelPreparationAndUnchangedNonToonOwnership() From 43bd04c0d877538215537011bc481b9ea600a4f4 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 22:33:05 +0900 Subject: [PATCH 7/8] test: harden host manifest assertions - Guard optional manifest evidence and centralize imported host source validation. - Validate exact lilToon version parsing with focused Unity tests and Codacy analysis. --- Shaders/Common/birp_host.hlsl | 4 ++-- ...ightingContractTests.LilToonObservation.cs | 11 ++++++++- .../Editor/ShaderCoreTestHostManifestTests.cs | 24 +++++++++++-------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Shaders/Common/birp_host.hlsl b/Shaders/Common/birp_host.hlsl index bd975bbb..be071205 100644 --- a/Shaders/Common/birp_host.hlsl +++ b/Shaders/Common/birp_host.hlsl @@ -57,8 +57,8 @@ void SCCalculateEnvironmentLight(inout SCLightData lightSum, inout half3 env, in #else sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0)); #if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH - sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb); - env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC); + sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, shAr, shAg, shAb); + env += SCModelEvaluateAmbient(sd, shAr, shAg, shAb, shBr, shBg, shBb, shC); #endif #endif #else diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs index f3523dde..82d3a740 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.LilToonObservation.cs @@ -17,6 +17,7 @@ // Records a transient lilToon 2.3.4 BIRP bright/dark classification observation for the OpenLit runtime inputs. using System.IO; +using System.Text.RegularExpressions; using NUnit.Framework; using UnityEditor; using UnityEngine; @@ -83,7 +84,15 @@ private static bool AssertInstalledLilToon234() return false; } - StringAssert.Contains("\"version\": \"2.3.4\"", File.ReadAllText(manifestPath)); + Assert.That( + Regex.IsMatch( + File.ReadAllText(manifestPath), + "\"version\"\\s*:\\s*\"2\\.3\\.4\"", + RegexOptions.CultureInvariant + ), + Is.True, + "The installed lilToon package manifest must declare the exact version 2.3.4." + ); return true; } diff --git a/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs b/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs index cef9efe7..58bfb7d4 100644 --- a/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs +++ b/Tests/Daily/Editor/ShaderCoreTestHostManifestTests.cs @@ -93,11 +93,7 @@ public void ToonOpenLitGeneratedSourcePreservesGammaPlacementAndDirectOnlyDiagno StringComparison.Ordinal ) ); - string assetPath = FindHostAssetPath(host.shaderName); - Shader shader = AssetDatabase.LoadAssetAtPath(assetPath); - AssertImportedShaderIsUsable(host.shaderName, shader); - - string source = LoadGeneratedShaderSource(assetPath, host.shaderName); + string source = LoadImportedHostSource(host); AssertExpectedSentinelCounts(host, source); AssertInactiveSentinelsAreAbsent(host, source); AssertOpenLitGeneratedSourceContract(host, source); @@ -184,11 +180,7 @@ public void ImportedHostsMatchGeneratedSourceContracts() HostManifest manifest = LoadManifest(); foreach (HostManifestEntry host in manifest.hosts) { - string assetPath = FindHostAssetPath(host.shaderName); - Shader shader = AssetDatabase.LoadAssetAtPath(assetPath); - AssertImportedShaderIsUsable(host.shaderName, shader); - - string source = LoadGeneratedShaderSource(assetPath, host.shaderName); + string source = LoadImportedHostSource(host); AssertExpectedSentinelCounts(host, source); AssertInactiveSentinelsAreAbsent(host, source); @@ -374,6 +366,7 @@ private static bool HasConfiguredOpenLitRuntimeEvidence(HostManifestEntry host) ) && string.Equals(runtimeEvidence.forwardBasePass, "ForwardBase", StringComparison.Ordinal) && string.Equals(runtimeEvidence.forwardAddPass, "ForwardAdd", StringComparison.Ordinal) + && runtimeEvidence.absentPasses != null && runtimeEvidence.absentPasses.SequenceEqual(new[] { "ShadowCaster", "Meta" }) && string.Equals( runtimeEvidence.forwardAddSentinel, @@ -470,6 +463,17 @@ private static string FindHostAssetPath(string shaderName) ); } + /// Loads and validates one imported fixed host and returns its generated source. + /// The manifest entry for the fixed host to validate. + /// The non-empty generated Shader Source subasset text. + private static string LoadImportedHostSource(HostManifestEntry host) + { + string assetPath = FindHostAssetPath(host.shaderName); + Shader shader = AssetDatabase.LoadAssetAtPath(assetPath); + AssertImportedShaderIsUsable(host.shaderName, shader); + return LoadGeneratedShaderSource(assetPath, host.shaderName); + } + /// Asserts that an already imported Shader-Core shader compiled and is supported. private static void AssertImportedShaderIsUsable(string shaderName, Shader shader) { From aa9f8ac2ed94dfacebf63d352812f15c7b9d5844 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Sat, 15 Aug 2026 23:29:47 +0900 Subject: [PATCH 8/8] test: align Toon SH source contracts - Require caller-provided SH coefficients in Toon generated-source assertions. - Verify the complete Daily lane passes without skips or protected-file changes. --- ...RenderingModeRenderingTests.OpenLitSourceContracts.cs | 9 ++++++--- ...ureBaseToonLightingContractTests.OpenLitRegression.cs | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs index 16f704a9..cccdce0e 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.OpenLitSourceContracts.cs @@ -66,13 +66,16 @@ internal static void AssertOpenLitFallbackPrecedesNormalization(string helper) internal static void AssertOpenLitHostGateContracts(string host) { const string fallbackDirectDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0));"; - const string shDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb);"; - const string toonAmbient = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; + const string shDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, shAr, shAg, shAb);"; + const string toonAmbient = "env += SCModelEvaluateAmbient(sd, shAr, shAg, shAb, shBr, shBg, shBb, shC);"; int fallbackDirectIndex = RequireIndex(host, fallbackDirectDirection); int shGateIndex = RequireIndex(host, "#if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH"); int shDirectionIndex = RequireIndex(host, shDirection); int toonAmbientIndex = RequireIndex(host, toonAmbient); + Match toonBranch = Regex.Match(host, @"#if\s+defined\(PUREBASE_TOON_MODEL_INCLUDED\)[\s\S]*?#endif\s*#endif\s*#else", RegexOptions.Singleline); + Assert.That(toonBranch.Success, Is.True, "The Toon host branch must retain its ForwardAdd and no-lightmap structure."); + Assert.That(Regex.IsMatch(toonBranch.Value, @"\bunity_SH(?:Ar|Ag|Ab|Br|Bg|Bb|C)\b"), Is.False, "Toon SH evaluation must use supplied SCCalculateEnvironmentLight parameters instead of Unity SH globals."); Assert.That(Regex.IsMatch(host, @"#if\s+defined\(PUREBASE_TOON_MODEL_INCLUDED\)\s*&&\s*!defined\(LIGHTMAP_ON\)"), Is.False, "Fallback-inclusive direct direction must not be owned by the obsolete combined Toon/lightmap gate."); Assert.That(fallbackDirectIndex, Is.LessThan(shGateIndex), "ForwardBase must publish fallback-inclusive direct direction before deciding whether Toon SH is available."); Assert.That(shGateIndex, Is.LessThan(shDirectionIndex), "Toon SH direction must remain inside the nested no-lightmap Unity SH gate."); @@ -80,7 +83,7 @@ internal static void AssertOpenLitHostGateContracts(string host) Assert.That( Regex.IsMatch( host, - @"#else\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\)\);\s*#if\s*!defined\(LIGHTMAP_ON\)\s*&&\s*UNITY_SHOULD_SAMPLE_SH\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb\);\s*env\s*\+=\s*SCModelEvaluateAmbient\(sd,\s*unity_SHAr,\s*unity_SHAg,\s*unity_SHAb,\s*unity_SHBr,\s*unity_SHBg,\s*unity_SHBb,\s*unity_SHC\);\s*#endif", + @"#else\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\),\s*half4\(0,\s*0,\s*0,\s*0\)\);\s*#if\s*!defined\(LIGHTMAP_ON\)\s*&&\s*UNITY_SHOULD_SAMPLE_SH\s*sd\.L\s*=\s*SCModelSelectAggregateLightDirection\(lightSum\.direction,\s*shAr,\s*shAg,\s*shAb\);\s*env\s*\+=\s*SCModelEvaluateAmbient\(sd,\s*shAr,\s*shAg,\s*shAb,\s*shBr,\s*shBg,\s*shBb,\s*shC\);\s*#endif", RegexOptions.Singleline ), Is.True, diff --git a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs index f7fa3883..0ec5dc87 100644 --- a/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs +++ b/Tests/Daily/Editor/PureBaseToonLightingContractTests.OpenLitRegression.cs @@ -127,14 +127,19 @@ private static void AssertForwardBaseDirectDirectionPrecedesToonShGate(string so { const string directAssignment = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, half4(0, 0, 0, 0), half4(0, 0, 0, 0), half4(0, 0, 0, 0));"; const string toonShGate = "#if !defined(LIGHTMAP_ON) && UNITY_SHOULD_SAMPLE_SH"; - const string toonShDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, unity_SHAr, unity_SHAg, unity_SHAb);"; - const string toonAmbientBand = "env += SCModelEvaluateAmbient(sd, unity_SHAr, unity_SHAg, unity_SHAb, unity_SHBr, unity_SHBg, unity_SHBb, unity_SHC);"; + const string toonShDirection = "sd.L = SCModelSelectAggregateLightDirection(lightSum.direction, shAr, shAg, shAb);"; + const string toonAmbientBand = "env += SCModelEvaluateAmbient(sd, shAr, shAg, shAb, shBr, shBg, shBb, shC);"; int directIndex = source.IndexOf(directAssignment, StringComparison.Ordinal); int gateIndex = source.IndexOf(toonShGate, StringComparison.Ordinal); int shDirectionIndex = source.IndexOf(toonShDirection, StringComparison.Ordinal); int ambientBandIndex = source.IndexOf(toonAmbientBand, StringComparison.Ordinal); StringAssert.Contains(disabledFeature, toonShGate); + Assert.That( + source.IndexOf("unity_SH", StringComparison.Ordinal), + Is.LessThan(0), + "Toon SH evaluation must use supplied parameters instead of Unity SH globals." + ); Assert.That(directIndex, Is.GreaterThanOrEqualTo(0), "ForwardBase must initialize sd.L from the fallback-inclusive direct aggregate."); Assert.That(gateIndex, Is.GreaterThan(directIndex), "Only Toon SH augmentation may be gated after the direct direction is published."); Assert.That(shDirectionIndex, Is.GreaterThan(gateIndex), "Toon SH direction must remain inside the SH gate.");