Files
Hasebe/Library/PackageCache/com.unity.render-pipelines.universal@0ffb3d4f1461/Tests/Editor/UniversalTerrainLitCodeGenTests.cs
T
2026-09-25 00:42:08 +03:00

104 lines
5.5 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NUnit.Framework;
using UnityEditor.Rendering.Universal.ShaderGraph;
using UnityEditor.ShaderGraph;
namespace UnityEditor.Rendering.Universal.Tests
{
[TestFixture]
class UniversalTerrainLitCodeGenTests
{
internal enum VertexContext
{
WithPositionBlock,
Empty,
}
// TerrainLit emits the primary shader plus the hidden _AddPass and _BaseMapGen shaders, so
// assertions have to cover all of them, not just Generator.generatedShader.
static List<(string name, string code)> GenerateShaders(VertexContext vertexContext, GenerationMode mode)
{
var target = new UniversalTarget();
Assert.IsTrue(target.TrySetActiveSubTarget(typeof(UniversalTerrainLitSubTarget)),
"TerrainLit subtarget is not registered on UniversalTarget");
var blocks = vertexContext == VertexContext.WithPositionBlock
? new[] { BlockFields.VertexDescription.Position, BlockFields.SurfaceDescription.BaseColor }
: new[] { BlockFields.SurfaceDescription.BaseColor };
var graph = new GraphData();
graph.AddContexts();
graph.InitializeOutputs(new Target[] { target }, blocks);
// outputNode stays null for a non-subgraph. A non-null one would make Generator swap in
// PreviewTarget and drop the terrain subtarget entirely.
var shaders = new Generator(graph, graph.outputNode, mode, "TerrainLitCodeGen").allGeneratedShaders
.Select(shader => (shader.shaderName, shader.codeString))
.ToList();
Assert.IsNotEmpty(shaders, "TerrainLit generated no shaders");
// The template preprocessor treats '$' as a command marker even inside a comment, and on a
// malformed directive it splices a marker into the shader instead of failing. Without this
// check a broken template would leave the assertions below passing on text that is wrong.
// Every marker is a comment line carrying a caret under the offending column. Matching the
// caret rather than the word ERROR also catches Expect(), which reports "^ Expected ')'".
foreach (var (name, code) in shaders)
StringAssert.DoesNotMatch(@"(?m)^\s*//\s*\^ ", code, $"template preprocessor error in {name}");
return shaders;
}
// Asserting mere presence would be satisfied by a single pass out of fifteen, so a new terrain
// pass whose template forgot the include would slip through. Every terrain pass emits exactly one
// UNIVERSAL_TERRAIN_ENABLED define and should carry exactly one copy of the function.
static void AssertDefinedInEveryTerrainPass(string function, VertexContext vertexContext, GenerationMode mode)
{
foreach (var (name, code) in GenerateTerrainShaders(vertexContext, mode))
{
var passes = Regex.Matches(code, @"#define UNIVERSAL_TERRAIN_ENABLED 1").Count;
var definitions = Regex.Matches(code, Regex.Escape(function) + @"\(").Count;
Assert.AreEqual(passes, definitions, $"{function} in {name}");
}
}
// Preview generation emits the _AddPass shader with no passes at all, and a shader with no
// terrain pass has nothing to define. Key off the define the terrain passes themselves add.
static List<(string name, string code)> GenerateTerrainShaders(VertexContext vertexContext, GenerationMode mode)
{
var shaders = GenerateShaders(vertexContext, mode)
.Where(shader => shader.code.Contains("UNIVERSAL_TERRAIN_ENABLED"))
.ToList();
Assert.IsNotEmpty(shaders, "no generated shader turns on the terrain path");
return shaders;
}
// Varyings.hlsl calls TerrainVaryingGeneration outside of any FEATURES_GRAPH_VERTEX guard, so the
// terrain passes have to emit it even when the graph's Vertex context is empty. That state is one
// Delete away: TerrainLit's only active vertex block is Position, it is unconnected by default, and
// removing it does not trigger the auto re-add. Gating the definition on graphVertex breaks every pass.
[TestCase(VertexContext.WithPositionBlock, GenerationMode.ForReals)]
[TestCase(VertexContext.Empty, GenerationMode.ForReals)]
[TestCase(VertexContext.WithPositionBlock, GenerationMode.Preview)]
[TestCase(VertexContext.Empty, GenerationMode.Preview)]
public void TerrainShadersDefineTerrainVaryingGeneration(VertexContext vertexContext, GenerationMode mode)
{
AssertDefinedInEveryTerrainPass("TerrainVaryingGeneration", vertexContext, mode);
}
// Same for ConstructTerrainTangent, which the forward and gbuffer passes call under
// ENABLE_TERRAIN_PERPIXEL_NORMAL.
[TestCase(VertexContext.WithPositionBlock, GenerationMode.ForReals)]
[TestCase(VertexContext.Empty, GenerationMode.ForReals)]
[TestCase(VertexContext.WithPositionBlock, GenerationMode.Preview)]
[TestCase(VertexContext.Empty, GenerationMode.Preview)]
public void TerrainShadersDefineConstructTerrainTangent(VertexContext vertexContext, GenerationMode mode)
{
AssertDefinedInEveryTerrainPass("ConstructTerrainTangent", vertexContext, mode);
}
}
}