490 lines
20 KiB
C#
490 lines
20 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Runtime.Serialization.Formatters.Binary;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.Experimental.TerrainAPI;
|
|
using UnityEngine.TestTools;
|
|
using static UnityEditor.Experimental.TerrainAPI.BaseBrushUIGroup;
|
|
|
|
namespace UnityEditor.Experimental.TerrainAPI
|
|
{
|
|
[TestFixture]
|
|
public class BrushPlaybackTests
|
|
{
|
|
private const string k_TerrainToolsApiPrefix = "UnityEditor.Experimental.TerrainAPI.";
|
|
const string k_TerrainToolsApiSuffix = ", Unity.TerrainTools.Editor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
|
|
private Terrain terrainObj;
|
|
private Bounds terrainBounds;
|
|
private Queue<OnPaintOccurrence> onPaintHistory;
|
|
private int m_PrevRTHandlesCount;
|
|
private ulong m_PrevTextureMemory;
|
|
|
|
private Type onSceneGUIContextType, terrainToolType, onPaintType;
|
|
|
|
private static BindingFlags s_bindingFlags = BindingFlags.Public |
|
|
BindingFlags.NonPublic |
|
|
BindingFlags.Static |
|
|
BindingFlags.Instance |
|
|
BindingFlags.FlattenHierarchy;
|
|
private float[,] startHeightArr;
|
|
|
|
private object terrainToolInstance;
|
|
private MethodInfo onPaintMethod, onSceneGUIMethod;
|
|
private Type baseBrushUIGroupType, brushRotationType, brushSizeType, brushStrengthType;
|
|
private BaseBrushUIGroup commonUIInstance;
|
|
private PropertyInfo brushRotationProperty, brushSizeProperty, brushStrengthProperty;
|
|
|
|
private static string GetApiString(string str)
|
|
{
|
|
return $"{k_TerrainToolsApiPrefix}{str}{k_TerrainToolsApiSuffix}";
|
|
}
|
|
|
|
private void InitTerrainTypesWithReflection(string paintToolName) {
|
|
|
|
terrainToolType = Type.GetType("UnityEditor.Experimental.TerrainAPI." + paintToolName + ", " +
|
|
"Unity.TerrainTools.Editor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
|
|
onPaintType = Type.GetType("UnityEditor.Experimental.TerrainAPI.OnPaintContext, " +
|
|
"UnityEditor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
|
|
onSceneGUIContextType = Type.GetType("UnityEditor.Experimental.TerrainAPI.OnSceneGUIContext, " +
|
|
"UnityEditor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
|
|
|
|
// Get the method and instance for the current tool being tested
|
|
PropertyInfo propertyInfo = terrainToolType.GetProperty("instance", s_bindingFlags);
|
|
MethodInfo methodInfo = propertyInfo.GetGetMethod();
|
|
terrainToolInstance = methodInfo.Invoke(null, null);
|
|
|
|
onPaintMethod = terrainToolType.GetMethod("OnPaint");
|
|
onSceneGUIMethod = terrainToolType.GetMethod("OnSceneGUI");
|
|
|
|
MethodInfo loadSettingsInfo = terrainToolType.GetMethod("LoadSettings", s_bindingFlags);
|
|
|
|
if (loadSettingsInfo != null)
|
|
{
|
|
loadSettingsInfo.Invoke(terrainToolInstance, null);
|
|
}
|
|
|
|
// LOAD TOOL SETTINGS
|
|
baseBrushUIGroupType = typeof(BaseBrushUIGroup);
|
|
brushSizeType = typeof(IBrushSizeController);
|
|
brushStrengthType = typeof(IBrushStrengthController);
|
|
brushRotationType = typeof(IBrushRotationController);
|
|
|
|
FieldInfo baseBrushUIGroupFieldInfo = terrainToolType.GetField("commonUI", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
if (baseBrushUIGroupFieldInfo == null)
|
|
{
|
|
PropertyInfo baseBrushUIGroupPropertyInfo = terrainToolType.GetProperty("commonUI", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
if (baseBrushUIGroupPropertyInfo != null)
|
|
{
|
|
commonUIInstance = baseBrushUIGroupPropertyInfo.GetValue(terrainToolInstance) as BaseBrushUIGroup;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
commonUIInstance = baseBrushUIGroupFieldInfo.GetValue(terrainToolInstance) as BaseBrushUIGroup;
|
|
}
|
|
|
|
if (commonUIInstance == null)
|
|
{
|
|
throw new Exception("The commonUI of the brush can't be found - does it have one?");
|
|
}
|
|
|
|
brushSizeProperty = baseBrushUIGroupType.GetProperty("brushSize", BindingFlags.Public | BindingFlags.Instance);
|
|
brushStrengthProperty = baseBrushUIGroupType.GetProperty("brushStrength", BindingFlags.Public | BindingFlags.Instance);
|
|
brushRotationProperty = baseBrushUIGroupType.GetProperty("brushRotation", BindingFlags.Public | BindingFlags.Instance);
|
|
}
|
|
|
|
// Triggered once per frame while the test is running
|
|
void OnSceneGUI(SceneView sceneView)
|
|
{
|
|
if (onPaintHistory == null || onPaintHistory.Count == 0 || terrainObj == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnPaintOccurrence paintOccurrence = onPaintHistory.Dequeue();
|
|
|
|
// Generate a raycast from the relative UV and terrain size
|
|
Vector3 rayOrigin = new Vector3(
|
|
Mathf.Lerp(terrainBounds.min.x, terrainBounds.max.x, paintOccurrence.xPos),
|
|
1000,
|
|
Mathf.Lerp(terrainBounds.min.z, terrainBounds.max.z, paintOccurrence.yPos)
|
|
);
|
|
|
|
Physics.Raycast(new Ray(rayOrigin, Vector3.down), out RaycastHit hit);
|
|
|
|
Texture brushTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(paintOccurrence.brushTextureAssetPath) as Texture;
|
|
|
|
// Instantiate a null SceneGUIContext with the above raycast
|
|
object onSceneGUIContextInstance = Activator.CreateInstance(
|
|
onSceneGUIContextType,
|
|
null, hit, brushTexture, paintOccurrence.brushStrength, paintOccurrence.brushSize
|
|
);
|
|
|
|
// set context info in case tool uses that instead of brush ui group
|
|
MethodInfo setInfo = onSceneGUIContextType.GetMethod("Set", BindingFlags.Public | BindingFlags.Instance);
|
|
setInfo.Invoke(onSceneGUIContextInstance,
|
|
new object[]
|
|
{
|
|
sceneView, true, hit,
|
|
brushTexture,
|
|
paintOccurrence.brushStrength,
|
|
paintOccurrence.brushSize
|
|
});
|
|
|
|
brushSizeProperty.SetValue(commonUIInstance, paintOccurrence.brushSize);
|
|
brushStrengthProperty.SetValue(commonUIInstance, paintOccurrence.brushStrength);
|
|
brushRotationProperty.SetValue(commonUIInstance, paintOccurrence.brushRotation);
|
|
|
|
onSceneGUIMethod.Invoke(terrainToolInstance, new object[] { terrainObj, onSceneGUIContextInstance });
|
|
|
|
// Set the brush strength via commonUI
|
|
commonUIInstance.brushStrength = paintOccurrence.brushStrength;
|
|
commonUIInstance.brushSize = paintOccurrence.brushSize;
|
|
|
|
object onPaintContext = Activator.CreateInstance(
|
|
onPaintType,
|
|
hit,
|
|
brushTexture,
|
|
new Vector2(paintOccurrence.xPos, paintOccurrence.yPos),
|
|
paintOccurrence.brushStrength,
|
|
paintOccurrence.brushSize
|
|
);
|
|
onPaintMethod.Invoke(terrainToolInstance, new object[] { terrainObj, onPaintContext });
|
|
}
|
|
|
|
private void ResetTerrainHeight(Terrain terrain)
|
|
{
|
|
float[,] heights = GetFullTerrainHeights(terrain);
|
|
|
|
for (int x = 0; x < terrain.terrainData.heightmapResolution; x++) {
|
|
for (int y = 0; y < terrain.terrainData.heightmapResolution; y++) {
|
|
heights[x, y] = 0;
|
|
}
|
|
}
|
|
|
|
terrain.terrainData.SetHeights(0, 0, heights);
|
|
}
|
|
|
|
private Queue<OnPaintOccurrence> LoadDataFile(string recordingFileName, bool expectNull = false) {
|
|
// Discover path to data file
|
|
string[] assets = AssetDatabase.FindAssets(recordingFileName);
|
|
if (assets.Length == 0) {
|
|
Debug.LogError("No asset with name " + recordingFileName + " found");
|
|
}
|
|
string assetPath = AssetDatabase.GUIDToAssetPath(assets[0]);
|
|
|
|
// Load data file as a List<paintHistory>
|
|
FileStream file = File.OpenRead(assetPath);
|
|
BinaryFormatter bf = new BinaryFormatter();
|
|
Queue<OnPaintOccurrence> paintHistory = new Queue<OnPaintOccurrence>(bf.Deserialize(file) as List<OnPaintOccurrence>);
|
|
|
|
file.Close();
|
|
|
|
if (paintHistory.Count == 0 && !expectNull)
|
|
{
|
|
throw new InconclusiveException("The loaded file contains no recordings");
|
|
}
|
|
|
|
return paintHistory;
|
|
}
|
|
|
|
private float[,] GetFullTerrainHeights(Terrain terrain)
|
|
{
|
|
int terrainWidth = terrain.terrainData.heightmapResolution;
|
|
int terrainHeight = terrain.terrainData.heightmapResolution;
|
|
return terrain.terrainData.GetHeights(
|
|
0, 0,
|
|
terrainWidth,
|
|
terrainHeight
|
|
);
|
|
}
|
|
|
|
private bool AreHeightsEqual(float[,] arr1, float[,] arr2)
|
|
{
|
|
if(arr1.Rank != arr2.Rank)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if(arr1.Rank > 1 && arr2.Rank > 1)
|
|
{
|
|
if(arr1.GetLength(0) != arr2.GetLength(0) ||
|
|
arr1.GetLength(1) != arr2.GetLength(1))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int xlen = arr1.GetLength(0);
|
|
int ylen = arr1.GetLength(1);
|
|
|
|
for(int x = 0; x < xlen; ++x)
|
|
{
|
|
for(int y = 0; y < ylen; ++y)
|
|
{
|
|
if(arr1[x,y] != arr2[x,y])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool AreHeightsNotEqual(float[,] arr1, float[,] arr2)
|
|
{
|
|
return !AreHeightsEqual(arr1, arr2);
|
|
}
|
|
|
|
public void SetupTerrain(string terrainName) {
|
|
TerrainData td = new TerrainData();
|
|
td.size = new Vector3(1000, 600, 1000);
|
|
td.heightmapResolution = 513;
|
|
td.baseMapResolution = 1024;
|
|
td.SetDetailResolution(1024, 32);
|
|
|
|
// Generate terrain
|
|
GameObject terrainGo = Terrain.CreateTerrainGameObject(td);
|
|
terrainObj = terrainGo.GetComponent<Terrain>();
|
|
terrainBounds = terrainGo.GetComponent<TerrainCollider>().bounds;
|
|
Selection.activeObject = terrainGo;
|
|
|
|
ResetTerrainHeight(terrainObj);
|
|
|
|
Selection.activeObject = terrainGo;
|
|
|
|
startHeightArr = GetFullTerrainHeights(terrainObj);
|
|
}
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
EditorWindow.GetWindow<SceneView>().Focus();
|
|
|
|
m_PrevTextureMemory = Texture.totalTextureMemory;
|
|
m_PrevRTHandlesCount = RTUtils.GetHandleCount();
|
|
|
|
// Enables the core brush playback on OnSceneGUI
|
|
SceneView.duringSceneGui -= OnSceneGUI;
|
|
SceneView.duringSceneGui += OnSceneGUI;
|
|
}
|
|
|
|
[TearDown]
|
|
public void Cleanup()
|
|
{
|
|
SceneView.duringSceneGui -= OnSceneGUI;
|
|
Selection.activeObject = null;
|
|
if (onPaintHistory != null)
|
|
onPaintHistory.Clear();
|
|
|
|
// delete test resources
|
|
commonUIInstance?.brushMaskFilterStack?.Clear(true);
|
|
PaintContext.ApplyDelayedActions(); // apply before destroying terrain and terrainData
|
|
if (terrainObj != null)
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(terrainObj.terrainData);
|
|
UnityEngine.Object.DestroyImmediate(terrainObj.gameObject);
|
|
}
|
|
|
|
// check Texture memory and RTHandle count
|
|
// var currentTextureMemory = Texture.totalTextureMemory;
|
|
// Assert.True(m_PrevTextureMemory == currentTextureMemory, $"Texture memory leak. Was {m_PrevTextureMemory} but is now {currentTextureMemory}. Diff = {currentTextureMemory - m_PrevTextureMemory}");
|
|
var currentRTHandlesCount = RTUtils.GetHandleCount();
|
|
Assert.True(m_PrevRTHandlesCount == RTUtils.GetHandleCount(), $"RTHandle leak. Was {m_PrevRTHandlesCount} but is now {currentRTHandlesCount}. Diff = {currentRTHandlesCount - m_PrevRTHandlesCount}");
|
|
}
|
|
|
|
[UnityTest]
|
|
[TestCase("PaintHeightHistory", "Terrain", ExpectedResult = null)]
|
|
public IEnumerator Test_PaintHeight_Playback(string recordingFilePath, string targetTerrainName) {
|
|
yield return null;
|
|
SetupTerrain(targetTerrainName);
|
|
InitTerrainTypesWithReflection("PaintHeightTool");
|
|
onPaintHistory = LoadDataFile(recordingFilePath);
|
|
SetupTerrain(targetTerrainName);
|
|
|
|
while (onPaintHistory.Count > 0)
|
|
{
|
|
// Force a SceneView update for OnSceneGUI to be triggered
|
|
SceneView.RepaintAll();
|
|
yield return null;
|
|
}
|
|
|
|
if(terrainObj.drawInstanced)
|
|
{
|
|
terrainObj.terrainData.SyncHeightmap();
|
|
}
|
|
|
|
Assert.That(AreHeightsNotEqual(startHeightArr, GetFullTerrainHeights(terrainObj)), Is.True, "Brush didn't make changes to terrain heightmap");
|
|
}
|
|
|
|
[UnityTest]
|
|
[TestCase("SetHeightHistory", 204f, ExpectedResult = null)]
|
|
public IEnumerator Test_SetHeight_Playback(string recordingFilePath, float targetHeight) {
|
|
yield return null;
|
|
|
|
SetupTerrain("Terrain");
|
|
InitTerrainTypesWithReflection("SetHeightTool");
|
|
onPaintHistory = LoadDataFile(recordingFilePath);
|
|
|
|
// Set the height parameter
|
|
FieldInfo heightField = terrainToolType.GetField("m_TargetHeight", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
heightField.SetValue(terrainToolInstance, targetHeight);
|
|
|
|
while (onPaintHistory.Count > 0)
|
|
{
|
|
// Force a SceneView update for OnSceneGUI to be triggered
|
|
SceneView.RepaintAll();
|
|
yield return null;
|
|
}
|
|
|
|
if(terrainObj.drawInstanced)
|
|
{
|
|
terrainObj.terrainData.SyncHeightmap();
|
|
}
|
|
|
|
Assert.That(AreHeightsNotEqual(startHeightArr, GetFullTerrainHeights(terrainObj)), Is.True, "Brush didn't make changes to terrain heightmap");
|
|
}
|
|
|
|
|
|
[UnityTest]
|
|
[TestCase("StampToolHistory", 500.0f, ExpectedResult = null)]
|
|
public IEnumerator Test_StampTerrain_Playback(string recordingFilePath, float stampHeight) {
|
|
yield return null;
|
|
|
|
SetupTerrain("Terrain");
|
|
InitTerrainTypesWithReflection("StampTool");
|
|
onPaintHistory = LoadDataFile(recordingFilePath);
|
|
|
|
// Set the height parameter
|
|
FieldInfo propertiesField = terrainToolType.GetField("stampToolProperties", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
object props = propertiesField.GetValue(terrainToolInstance);
|
|
FieldInfo heightField = props.GetType().GetField("m_StampHeight");
|
|
heightField.SetValue(props, stampHeight); // Use 20 b/c why not
|
|
|
|
while (onPaintHistory.Count > 0)
|
|
{
|
|
// Force a SceneView update for OnSceneGUI to be triggered
|
|
SceneView.RepaintAll();
|
|
yield return null;
|
|
}
|
|
|
|
if(terrainObj.drawInstanced)
|
|
{
|
|
terrainObj.terrainData.SyncHeightmap();
|
|
}
|
|
|
|
Assert.That(AreHeightsNotEqual(startHeightArr, GetFullTerrainHeights(terrainObj)), Is.True, "Brush didn't make changes to terrain heightmap");
|
|
}
|
|
|
|
[UnityTest]
|
|
[TestCase("NoiseHeightHistory", "Terrain", ExpectedResult = null)]
|
|
public IEnumerator Test_PaintNoiseHeight_Playback(string recordingFilePath, string targetTerrainName) {
|
|
yield return null;
|
|
|
|
SetupTerrain(targetTerrainName);
|
|
InitTerrainTypesWithReflection("NoiseHeightTool");
|
|
onPaintHistory = LoadDataFile(recordingFilePath);
|
|
|
|
while (onPaintHistory.Count > 0)
|
|
{
|
|
// Force a SceneView update for OnSceneGUI to be triggered
|
|
SceneView.RepaintAll();
|
|
yield return null;
|
|
}
|
|
|
|
if(terrainObj.drawInstanced)
|
|
{
|
|
terrainObj.terrainData.SyncHeightmap();
|
|
}
|
|
|
|
Assert.That(AreHeightsNotEqual(startHeightArr, GetFullTerrainHeights(terrainObj)), Is.True, "Brush didn't make changes to terrain heightmap");
|
|
}
|
|
|
|
// Used to check for texture matrix regressions
|
|
[UnityTest]
|
|
[TestCase("NoiseHeightHistory", "Terrain", ExpectedResult = null)]
|
|
public IEnumerator Test_PaintTexture_Playback(string recordingFilePath, string targetTerrainName) {
|
|
yield return null;
|
|
|
|
SetupTerrain(targetTerrainName);
|
|
InitTerrainTypesWithReflection("PaintTextureTool");
|
|
onPaintHistory = LoadDataFile(recordingFilePath);
|
|
|
|
TerrainLayer tl1 = new TerrainLayer(), tl2 = new TerrainLayer();
|
|
tl1.diffuseTexture = Resources.Load<Texture2D>("testGradientCircle");
|
|
tl2.diffuseTexture = Resources.Load<Texture2D>("testGradientCircle");
|
|
terrainObj.terrainData.terrainLayers = new TerrainLayer[] { tl1, tl2 };
|
|
|
|
PaintTextureTool paintTextureTool = terrainToolInstance as PaintTextureTool;
|
|
FieldInfo selectedTerrainLayerInfo = typeof(PaintTextureTool).GetField("m_SelectedTerrainLayer",
|
|
s_bindingFlags);
|
|
selectedTerrainLayerInfo.SetValue(paintTextureTool, tl2);
|
|
|
|
while (onPaintHistory.Count > 0) {
|
|
// Force a SceneView update for OnSceneGUI to be triggered
|
|
SceneView.RepaintAll();
|
|
yield return null;
|
|
}
|
|
|
|
Assert.Pass("Matrix stack regression not found!");
|
|
}
|
|
|
|
[UnityTest]
|
|
[TestCase("PaintHeightHistory", "Terrain", ExpectedResult = null)]
|
|
public IEnumerator Test_PaintHeight_With_BrushMaskFilters_Playback(string recordingFilePath, string targetTerrainName)
|
|
{
|
|
yield return null;
|
|
|
|
InitTerrainTypesWithReflection("PaintHeightTool");
|
|
onPaintHistory = LoadDataFile(recordingFilePath);
|
|
SetupTerrain(targetTerrainName);
|
|
|
|
commonUIInstance.brushMaskFilterStack.Clear(true);
|
|
|
|
var filterCount = FilterUtility.GetFilterTypeCount();
|
|
for(int i = 0; i < filterCount; ++i)
|
|
{
|
|
commonUIInstance.brushMaskFilterStack.Add(FilterUtility.CreateInstance(FilterUtility.GetFilterType(i)));
|
|
}
|
|
|
|
while (onPaintHistory.Count > 0)
|
|
{
|
|
// Force a SceneView update for OnSceneGUI to be triggered
|
|
SceneView.RepaintAll();
|
|
PaintContext.ApplyDelayedActions();
|
|
yield return null;
|
|
}
|
|
|
|
if(terrainObj.drawInstanced)
|
|
{
|
|
terrainObj.terrainData.SyncHeightmap();
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Test_MemoryLeaks()
|
|
{
|
|
yield return null;
|
|
InitTerrainTypesWithReflection("PaintHeightTool");
|
|
SetupTerrain("Terrain");
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Test_SetHeight_FlattenTile()
|
|
{
|
|
yield return null;
|
|
|
|
InitTerrainTypesWithReflection("SetHeightTool");
|
|
SetupTerrain("Terrain");
|
|
|
|
var fillHeightFunc = terrainToolType.GetMethod("Flatten", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
fillHeightFunc.Invoke(terrainToolInstance, new[] {terrainObj});
|
|
}
|
|
}
|
|
}
|