This commit is contained in:
2026-07-09 21:33:33 +02:00
commit 84a2a365f3
2364 changed files with 950134 additions and 0 deletions
@@ -0,0 +1,350 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities;
using UnityEditor;
using UnityEngine;
using VOID.Editor.Utils;
using VOID.Generic.World;
namespace VOID.Editor.EditorWindow.World
{
public class ComplexAreaRotateVariantGenerator : OdinEditorWindow
{
[MenuItem("VOID RPG/World/Complex Area Rotate Variant Generator")]
private static void OpenWindow() => GetWindow<ComplexAreaRotateVariantGenerator>().Show();
// Input and Output for user
[ShowInInspector, AssetsOnly] private List<ComplexArea> sourceComplexAreas = new();
[ShowInInspector, ReadOnly] private List<ComplexArea> generatedComplexAreas = new();
// Rotation variants to create
private static readonly float[] Rotations = { 90, 180, 270 };
// Temporary variables
private ComplexArea _targetComplexArea;
private string _sourcePrefabPath;
private string _sourceFolder;
private float _variantRotation;
private string _targetPrefabPath;
private string _targetFolder;
public static void GenerateRotatedVariants(ComplexArea complexArea)
{
// Only ASSETS for this generation!
var prefab = complexArea.gameObject.GetPrefabAsset();
if (prefab == null)
{
Debug.LogError("Only prefab asset is valid to variant generation!");
return;
}
// Just to be sure - save given prefab for user before generation
PrefabUtility.SavePrefabAsset(complexArea.gameObject);
var isOpen = HasOpenInstances<ComplexAreaRotateVariantGenerator>();
var variantGenerator = GetWindow<ComplexAreaRotateVariantGenerator>(false, null, false);
variantGenerator.sourceComplexAreas = new List<ComplexArea> { prefab.GetComponent<ComplexArea>() };
variantGenerator.GenerateRotatedVariants();
if (!isOpen) variantGenerator.Close();
}
[Button]
private void GenerateRotatedVariants()
{
if (sourceComplexAreas.IsNullOrEmpty())
{
Debug.LogWarning("ComplexArea not selected");
return;
}
generatedComplexAreas.Clear();
sourceComplexAreas.ForEach(sourceComplexArea =>
{
var variants = Rotations.Select(rotation => GenerateRotatedVariant(sourceComplexArea, rotation)).ToList();
sourceComplexArea.isVariant = false;
sourceComplexArea.variants = variants;
sourceComplexArea.original = null;
PrefabUtility.SavePrefabAsset(sourceComplexArea.gameObject);
generatedComplexAreas.AddRange(variants);
});
sourceComplexAreas.Clear();
}
private ComplexArea GenerateRotatedVariant(ComplexArea sourceComplexArea, float rotation)
{
_sourcePrefabPath = AssetDatabase.GetAssetPath(sourceComplexArea.gameObject);
_sourceFolder = Path.GetDirectoryName(_sourcePrefabPath)?.Replace("\\", "/") ?? "";
_variantRotation = rotation;
_targetFolder = $"{_sourceFolder}/{sourceComplexArea.gameObject.name}_v{_variantRotation}";
if (Directory.Exists(_targetFolder)) Directory.Delete(_targetFolder, true);
Directory.CreateDirectory(_targetFolder);
DuplicatePrefab();
DuplicateTerrains();
RotatePrefab();
UpdateTerrainsHeight();
UpdateTerrainsAlphas();
UpdateTerrainsDetails();
UpdateTerrainsTrees();
UpdateComplexArea();
UpdateComplexAreaOriginalVariant(sourceComplexArea);
PrefabUtility.SaveAsPrefabAsset(_targetComplexArea.gameObject, _targetPrefabPath);
PrefabUtility.UnloadPrefabContents(_targetComplexArea.gameObject);
return AssetDatabase.LoadAssetAtPath<GameObject>(_targetPrefabPath).GetComponent<ComplexArea>();
}
private void DuplicatePrefab()
{
// Duplicate prefab asset in sub directory
_targetPrefabPath = $"{_targetFolder}/{Path.GetFileNameWithoutExtension(_sourcePrefabPath)}_v{_variantRotation}.prefab";
AssetDatabase.CopyAsset(_sourcePrefabPath, _targetPrefabPath);
// "Open" duplicated prefab for editing
_targetComplexArea = PrefabUtility.LoadPrefabContents(_targetPrefabPath).GetComponent<ComplexArea>();
}
private void DuplicateTerrains()
{
// Duplicate TerrainData and place them in relative directiory (if not possible put them next to prefab)
_targetComplexArea.GetComponentsInChildren<Terrain>().ForEach(terrain =>
{
var dataPath = AssetDatabase.GetAssetPath(terrain.terrainData);
var dataPathNew = GetNewAssetPath(dataPath);
Directory.CreateDirectory(_targetFolder);
AssetDatabase.CopyAsset(dataPath, dataPathNew);
var terrainData = AssetDatabase.LoadAssetAtPath<TerrainData>(dataPathNew);
terrain.GetComponent<Terrain>().terrainData = terrainData;
terrain.GetComponent<TerrainCollider>().terrainData = terrainData;
});
// Update asset database
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
private void RotatePrefab()
{
// Terrain rotating FIX - instead we will rotate dummies
var terrainPositionDummyList = new Dictionary<GameObject, Terrain>();
_targetComplexArea.gameObject.GetComponentsInChildren<Terrain>().ForEach(terrain =>
{
var dummy = new GameObject();
var sizeOffset = Vector3.Scale(new Vector3(.5f, 0, .5f), terrain.terrainData.size);
dummy.transform.parent = terrain.transform.parent;
dummy.transform.position = terrain.transform.position + sizeOffset;
terrainPositionDummyList.Add(dummy, terrain);
});
// All children of prefab
var prefabChildren = new List<Transform>();
for (var i = 0; i < _targetComplexArea.gameObject.transform.childCount; i++)
prefabChildren.Add(_targetComplexArea.gameObject.transform.GetChild(i));
// Put empty dummy
var dummyRotate = new GameObject();
dummyRotate.transform.parent = _targetComplexArea.gameObject.transform;
dummyRotate.transform.position = Vector3.zero;
// Put all children into dummy
prefabChildren.ForEach(child => child.parent = dummyRotate.transform);
// Rotate dummy with all children
dummyRotate.transform.Rotate(Vector3.up, _variantRotation);
// Take out all children from dummy
prefabChildren.ForEach(child => child.parent = _targetComplexArea.gameObject.transform);
// Remove dummy
DestroyImmediate(dummyRotate);
// Terrain rotating FIX - Put terrains in good positions
terrainPositionDummyList.ForEach(pair =>
{
var dummy = pair.Key;
var terrain = pair.Value;
var sizeOffset = Vector3.Scale(new Vector3(.5f, 0, .5f), terrain.terrainData.size);
terrain.transform.position = pair.Key.transform.position - sizeOffset;
terrain.transform.rotation = Quaternion.identity;
DestroyImmediate(pair.Key);
});
// Update asset database
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
private void UpdateTerrainsHeight()
{
_targetComplexArea.gameObject.GetComponentsInChildren<Terrain>().ForEach(terrain =>
{
var rotationTimes = Mathf.RoundToInt(_variantRotation / 90);
var terrainData = terrain.terrainData;
var heightMapSize = terrainData.heightmapResolution;
var heights = terrainData.GetHeights(0, 0, heightMapSize, heightMapSize);
var rotatedHeights = new float[heightMapSize, heightMapSize];
// Perform rotating N times
for (var i = 0; i < rotationTimes; i++)
{
// Rotating whole two-dimensional height array by 90 degree
for (var y = 0; y < heightMapSize; y++)
for (var x = 0; x < heightMapSize; x++)
rotatedHeights[y, x] = heights[x, heightMapSize - y - 1];
// Rotated by 90 degree, prepare starting point for next rotating
heights = (float[,])rotatedHeights.Clone();
}
// Update terrain
terrainData.SetHeights(0, 0, rotatedHeights);
});
}
private void UpdateTerrainsAlphas()
{
_targetComplexArea.gameObject.GetComponentsInChildren<Terrain>().ForEach(terrain =>
{
var rotationTimes = Mathf.RoundToInt(_variantRotation / 90);
var terrainData = terrain.terrainData;
var alphaSize = terrainData.alphamapResolution;
var alphaCount = terrainData.alphamapLayers;
var alphas = terrainData.GetAlphamaps(0, 0, alphaSize, alphaSize);
var rotatedAlphas = new float[alphaSize, alphaSize, alphaCount];
// Perform rotating N times
for (var i = 0; i < rotationTimes; i++)
{
// Rotating whole two-dimensional array for each alpha map by 90 degree
for (var y = 0; y < alphaSize; y++)
for (var x = 0; x < alphaSize; x++)
for (var a = 0; a < alphaCount; a++)
rotatedAlphas[y, x, a] = alphas[x, alphaSize - y - 1, a];
// Rotated by 90 degree, prepare starting point for next rotating
alphas = (float[,,])rotatedAlphas.Clone();
}
// Update terrain
terrainData.SetAlphamaps(0, 0, rotatedAlphas);
});
}
private void UpdateTerrainsDetails()
{
_targetComplexArea.gameObject.GetComponentsInChildren<Terrain>().ForEach(terrain =>
{
var rotationTimes = Mathf.RoundToInt(_variantRotation / 90);
var terrainData = terrain.terrainData;
var detailSize = terrainData.detailResolution;
var detailLayerCount = terrainData.detailPrototypes.Length;
var rotatedDetails = new int[detailSize, detailSize];
// Rotate ALL detail layers
for (var a = 0; a < detailLayerCount; a++)
{
// Get current detail layer at index
var details = terrainData.GetDetailLayer(0, 0, detailSize, detailSize, a);
// Perform rotating N times
for (var i = 0; i < rotationTimes; i++)
{
// Rotating whole dimensional array by 90 degree
for (var y = 0; y < detailSize; y++)
for (var x = 0; x < detailSize; x++)
rotatedDetails[y, x] = details[x, detailSize - y - 1];
// Rotated by 90 degree, prepare starting point for next rotating
details = (int[,])rotatedDetails.Clone();
}
// Update terrain
terrainData.SetDetailLayer(0, 0, a, rotatedDetails);
}
});
}
private void UpdateTerrainsTrees()
{
_targetComplexArea.gameObject.GetComponentsInChildren<Terrain>().ForEach(terrain =>
{
var terrainData = terrain.terrainData;
// New empty GameObject for further rotating trees
var terrainDummy = new GameObject();
terrainDummy.transform.position = terrain.transform.position + terrainData.size/2;
// Map dummy GameObjects to each tree for rotating purposes, by making it child to terrain dummy
var treeList = terrainData.treeInstances;
var dummyList = new GameObject[treeList.Length];
for (var i = 0; i < treeList.Length; i++)
{
dummyList[i] = new GameObject();
dummyList[i].transform.parent = terrainDummy.transform;
dummyList[i].transform.position = terrain.transform.position + Vector3.Scale(treeList[i].position, terrainData.size);
}
// Rotate dummy terrain (and all tree dummies)
terrainDummy.transform.Rotate(Vector3.up, _variantRotation);
// Corrent positions and rotations of all trees, by using created dummies
for (var i = 0; i < treeList.Length; i++)
{
var relativePosition = dummyList[i].transform.position - terrain.transform.position;
treeList[i].position = new Vector3(relativePosition.x / terrainData.size.x, 0, relativePosition.z / terrainData.size.z);
treeList[i].rotation = dummyList[i].transform.rotation.eulerAngles.y;
}
// Update Terrain
terrainData.SetTreeInstances(treeList, true);
// Remove dummies
dummyList.ForEach(DestroyImmediate);
DestroyImmediate(terrainDummy);
});
}
private void UpdateComplexArea()
{
_targetComplexArea.areas.ForEach(area =>
{
// Areas should not be rotated
area.transform.rotation = Quaternion.identity;
// Swap connection clockwise - N times of needed rotation
var rotationTimes = Mathf.RoundToInt(_variantRotation / 90);
for (var i = 0; i < rotationTimes; i++)
{
var tempConnection = area.forwardConnection;
area.forwardConnection = area.leftConnection;
area.leftConnection = area.backConnection;
area.backConnection = area.rightConnection;
area.rightConnection = tempConnection;
}
});
// Run all inside updates and fixes
_targetComplexArea.UpdateAll();
_targetComplexArea.AutoFixAll();
}
private void UpdateComplexAreaOriginalVariant(ComplexArea sourceComplexArea)
{
_targetComplexArea.isVariant = true;
_targetComplexArea.variants.Clear();
_targetComplexArea.original = sourceComplexArea;
}
private string GetNewAssetPath(string path)
{
if (path.StartsWith(_sourceFolder) == false) return $"{_targetFolder}/{Path.GetFileName(path)}";
var relativePath = path.Substring(_sourceFolder.Length).TrimStart('/');
return $"{_targetFolder}/{relativePath}";
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f3e35552b4d52364b8ddd710111b0378