riversong code showcase
This commit is contained in:
50
Source/Riversong/Tools/BuildVersionPreprocessor.cs
Normal file
50
Source/Riversong/Tools/BuildVersionPreprocessor.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.Build.Reporting;
|
||||
#endif
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
public class BuildVersionPreprocessor : IPreprocessBuildWithReport
|
||||
{
|
||||
private const string AssetPath = "Assets/_Project/Config/BuildVersion.asset";
|
||||
|
||||
public int callbackOrder => 0;
|
||||
|
||||
public void OnPreprocessBuild(BuildReport report)
|
||||
{
|
||||
var buildVersion = AssetDatabase.LoadAssetAtPath<BuildVersionAsset>(AssetPath);
|
||||
|
||||
if (!buildVersion)
|
||||
throw new BuildFailedException($"Missing build version asset at '{AssetPath}'.");
|
||||
|
||||
var today = DateTime.Now;
|
||||
var dayKey = today.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
|
||||
|
||||
if (string.Equals(buildVersion.LastBuildDate, dayKey, StringComparison.Ordinal))
|
||||
buildVersion.LastBuildCounter++;
|
||||
else
|
||||
{
|
||||
buildVersion.LastBuildDate = dayKey;
|
||||
buildVersion.LastBuildCounter = 1;
|
||||
}
|
||||
|
||||
buildVersion.CurrentVersion = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0:00}.{1:00}.{2:0000}.{3}",
|
||||
today.Month,
|
||||
today.Day,
|
||||
today.Year,
|
||||
buildVersion.LastBuildCounter);
|
||||
|
||||
EditorUtility.SetDirty(buildVersion);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
48
Source/Riversong/Tools/IconMaker.cs
Normal file
48
Source/Riversong/Tools/IconMaker.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.IO;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class IconMaker : MonoBehaviour
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
[SerializeField]
|
||||
[PreviewField]
|
||||
private RenderTexture _iconTexture;
|
||||
|
||||
[Button(ButtonSizes.Large)]
|
||||
[GUIColor("cyan")]
|
||||
public void MakeIcon()
|
||||
{
|
||||
if (!_iconTexture) return;
|
||||
|
||||
var folderPath = "Assets/_Project/GameAssets/UI/Icons";
|
||||
if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath);
|
||||
|
||||
var fileName = $"Icon_{GUID.Generate()}.png";
|
||||
|
||||
RenderTexture.active = _iconTexture;
|
||||
|
||||
var temp = new Texture2D(_iconTexture.width, _iconTexture.height, TextureFormat.RGBA32, false);
|
||||
temp.ReadPixels(new Rect(0, 0, _iconTexture.width, _iconTexture.height), 0, 0);
|
||||
temp.Apply();
|
||||
|
||||
var fullPath = Path.Combine(folderPath, fileName);
|
||||
File.WriteAllBytes(fullPath, temp.EncodeToPNG());
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(fullPath);
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
importer.spriteImportMode = SpriteImportMode.Single;
|
||||
importer.maxTextureSize = 256;
|
||||
importer.mipmapEnabled = true;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
204
Source/Riversong/Tools/MeshBaker.cs
Normal file
204
Source/Riversong/Tools/MeshBaker.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
#endif
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class MeshBaker : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
private string _layer = "Default";
|
||||
|
||||
[SerializeField]
|
||||
private List<GameObject> _doNotBake = new();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[Button(ButtonSizes.Large)]
|
||||
[GUIColor("cyan")]
|
||||
private void BakeMesh()
|
||||
{
|
||||
var excluded = GetExcluded();
|
||||
|
||||
var combineInstancesByMaterial = CollectCombineInstances(excluded);
|
||||
if (combineInstancesByMaterial.ValueCount <= 0) return;
|
||||
|
||||
var bakedName = $"{gameObject.name.Replace("_Authoring", string.Empty)}_Baked";
|
||||
var bakedMesh = CreateBakedMesh(bakedName, combineInstancesByMaterial);
|
||||
var parentFolder = GetBakeOutputFolder(out var isPrefab);
|
||||
|
||||
AssetDatabase.CreateAsset(bakedMesh, $"{parentFolder}/{bakedName}.asset");
|
||||
|
||||
if (isPrefab) SaveBakedPrefab(parentFolder, bakedName, bakedMesh, combineInstancesByMaterial);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
private ListMultiDictionary<Material, CombineInstance> CollectCombineInstances(HashSet<Transform> excluded)
|
||||
{
|
||||
var meshRenderers = GetComponentsInChildren<MeshRenderer>();
|
||||
var combineInstancesByMaterial = new ListMultiDictionary<Material, CombineInstance>();
|
||||
|
||||
foreach (var childRenderer in meshRenderers)
|
||||
{
|
||||
if (excluded.Contains(childRenderer.transform)) continue;
|
||||
AddRendererCombineInstances(childRenderer, combineInstancesByMaterial);
|
||||
}
|
||||
|
||||
return combineInstancesByMaterial;
|
||||
}
|
||||
|
||||
private void AddRendererCombineInstances(MeshRenderer childRenderer, ListMultiDictionary<Material, CombineInstance> combineInstancesByMaterial)
|
||||
{
|
||||
var meshFilter = childRenderer.GetComponent<MeshFilter>();
|
||||
if (!meshFilter || !meshFilter.sharedMesh) return;
|
||||
|
||||
var mesh = meshFilter.sharedMesh;
|
||||
var materials = childRenderer.sharedMaterials;
|
||||
var subMeshCount = Mathf.Min(mesh.subMeshCount, materials.Length);
|
||||
|
||||
for (var i = 0; i < subMeshCount; i++)
|
||||
{
|
||||
var material = materials[i];
|
||||
if (!material) continue;
|
||||
|
||||
combineInstancesByMaterial.Add(
|
||||
material,
|
||||
new CombineInstance
|
||||
{
|
||||
mesh = mesh,
|
||||
subMeshIndex = i,
|
||||
transform = meshFilter.transform.localToWorldMatrix
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Mesh CreateBakedMesh(string bakedName, ListMultiDictionary<Material, CombineInstance> combineInstancesByMaterial)
|
||||
{
|
||||
var bakedMesh = new Mesh { name = bakedName };
|
||||
|
||||
if (combineInstancesByMaterial.KeyCount == 1)
|
||||
{
|
||||
CombineSingleMaterialMesh(bakedMesh, combineInstancesByMaterial);
|
||||
return bakedMesh;
|
||||
}
|
||||
|
||||
CombineMultiMaterialMesh(bakedMesh, combineInstancesByMaterial);
|
||||
return bakedMesh;
|
||||
}
|
||||
|
||||
private void CombineSingleMaterialMesh(Mesh bakedMesh, ListMultiDictionary<Material, CombineInstance> combineInstancesByMaterial)
|
||||
{
|
||||
foreach (var material in combineInstancesByMaterial.Keys)
|
||||
{
|
||||
var combineInstances = combineInstancesByMaterial[material];
|
||||
bakedMesh.CombineMeshes(combineInstances.ToArray(), true, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void CombineMultiMaterialMesh(Mesh bakedMesh, ListMultiDictionary<Material, CombineInstance> combineInstancesByMaterial)
|
||||
{
|
||||
var finalCombineInstances = new List<CombineInstance>();
|
||||
var tempMeshes = new List<Mesh>();
|
||||
|
||||
foreach (var material in combineInstancesByMaterial.Keys)
|
||||
{
|
||||
var tempMesh = new Mesh();
|
||||
tempMesh.CombineMeshes(combineInstancesByMaterial[material].ToArray(), true, true);
|
||||
|
||||
finalCombineInstances.Add(
|
||||
new CombineInstance
|
||||
{
|
||||
mesh = tempMesh,
|
||||
transform = Matrix4x4.identity
|
||||
});
|
||||
|
||||
tempMeshes.Add(tempMesh);
|
||||
}
|
||||
|
||||
bakedMesh.CombineMeshes(finalCombineInstances.ToArray(), false, false);
|
||||
|
||||
foreach (var mesh in tempMeshes) DestroyImmediate(mesh);
|
||||
}
|
||||
|
||||
private string GetBakeOutputFolder(out bool isPrefab)
|
||||
{
|
||||
var selectedPrefab = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject);
|
||||
if (string.IsNullOrEmpty(selectedPrefab))
|
||||
{
|
||||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||
if (prefabStage && prefabStage.prefabContentsRoot == gameObject) selectedPrefab = prefabStage.assetPath;
|
||||
}
|
||||
|
||||
isPrefab = !string.IsNullOrEmpty(selectedPrefab);
|
||||
return isPrefab ? Path.GetDirectoryName(selectedPrefab) : "Assets";
|
||||
}
|
||||
|
||||
private void SaveBakedPrefab(string parentFolder, string bakedName, Mesh bakedMesh, ListMultiDictionary<Material, CombineInstance> combineInstancesByMaterial)
|
||||
{
|
||||
var baked = new GameObject(bakedName) { layer = LayerMask.NameToLayer(_layer) };
|
||||
baked.AddComponent<MeshFilter>().sharedMesh = bakedMesh;
|
||||
|
||||
var bakedRenderer = baked.AddComponent<MeshRenderer>();
|
||||
bakedRenderer.sharedMaterials = new List<Material>(combineInstancesByMaterial.Keys).ToArray();
|
||||
|
||||
AddExcluded(baked.transform);
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(baked, $"{parentFolder}/{bakedName}.prefab");
|
||||
DestroyImmediate(baked);
|
||||
}
|
||||
|
||||
private HashSet<Transform> GetExcluded()
|
||||
{
|
||||
var excluded = new HashSet<Transform>();
|
||||
foreach (var obj in _doNotBake)
|
||||
{
|
||||
if (!obj) continue;
|
||||
ExcludeHierarchy(obj.transform, excluded);
|
||||
}
|
||||
|
||||
return excluded;
|
||||
}
|
||||
|
||||
private void ExcludeHierarchy(Transform root, HashSet<Transform> excluded)
|
||||
{
|
||||
excluded.Add(root);
|
||||
for (var i = 0; i < root.childCount; i++) ExcludeHierarchy(root.GetChild(i), excluded);
|
||||
}
|
||||
|
||||
private void AddExcluded(Transform root)
|
||||
{
|
||||
foreach (var obj in _doNotBake)
|
||||
{
|
||||
if (!obj) continue;
|
||||
var clone = Instantiate(obj, root);
|
||||
clone.name = obj.name;
|
||||
ApplyRelativeTransform(obj.transform, clone.transform);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRelativeTransform(Transform source, Transform target)
|
||||
{
|
||||
var relativeMatrix = transform.worldToLocalMatrix * source.localToWorldMatrix;
|
||||
|
||||
var localPosition = relativeMatrix.MultiplyPoint3x4(Vector3.zero);
|
||||
|
||||
var localForward = relativeMatrix.MultiplyVector(Vector3.forward);
|
||||
var localUp = relativeMatrix.MultiplyVector(Vector3.up);
|
||||
|
||||
var scaleX = relativeMatrix.GetColumn(0).magnitude;
|
||||
var scaleY = relativeMatrix.GetColumn(1).magnitude;
|
||||
var scaleZ = relativeMatrix.GetColumn(2).magnitude;
|
||||
|
||||
target.localPosition = localPosition;
|
||||
target.localRotation = Quaternion.LookRotation(localForward, localUp);
|
||||
target.localScale = new Vector3(scaleX, scaleY, scaleZ);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
91
Source/Riversong/Tools/WaterFlowMapBaker.cs
Normal file
91
Source/Riversong/Tools/WaterFlowMapBaker.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[RequireComponent(typeof(SplineContainer))]
|
||||
public class WaterFlowMapBaker : MonoBehaviour
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
public int WorldSize = 100;
|
||||
|
||||
public int MapSize = 512;
|
||||
|
||||
public float InfluenceRadius = 10;
|
||||
|
||||
public Texture2D OutputTexture;
|
||||
|
||||
[Button(ButtonSizes.Large)]
|
||||
[GUIColor("cyan")]
|
||||
public void BakeFlowMap()
|
||||
{
|
||||
var splineContainer = GetComponent<SplineContainer>();
|
||||
|
||||
var texture = new Texture2D(MapSize, MapSize, TextureFormat.RGBA32, false, true)
|
||||
{
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear
|
||||
};
|
||||
|
||||
for (var y = 0; y < MapSize; y++)
|
||||
for (var x = 0; x < MapSize; x++)
|
||||
{
|
||||
var c = new Color(0.5f, 0.5f, 0, 0);
|
||||
|
||||
var uv = new Vector2((x + 0.5f) / MapSize, (y + 0.5f) / MapSize);
|
||||
var world = new Vector3(uv.x * WorldSize, 0, uv.y * WorldSize);
|
||||
|
||||
var distance = SplineUtility.GetNearestPoint(splineContainer.Spline, world, out _, out var t);
|
||||
if (distance <= InfluenceRadius)
|
||||
{
|
||||
var tangent = splineContainer.Spline.EvaluateTangent(t);
|
||||
|
||||
var direction = new Vector2(tangent.x, tangent.z);
|
||||
if (direction.sqrMagnitude > 0.0001f) direction.Normalize();
|
||||
|
||||
var influence = Mathf.Clamp01(1 - distance / InfluenceRadius);
|
||||
|
||||
c.r = direction.x * 0.5f + 0.5f;
|
||||
c.g = direction.y * 0.5f + 0.5f;
|
||||
c.b = influence;
|
||||
}
|
||||
|
||||
texture.SetPixel(x, y, c);
|
||||
}
|
||||
|
||||
texture.Apply(false, false);
|
||||
|
||||
var textureDirty = false;
|
||||
if (!OutputTexture)
|
||||
{
|
||||
var path = EditorUtility.SaveFilePanelInProject("Save Flow Map", "WaterFlowMap", "asset", string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
texture.name = Path.GetFileNameWithoutExtension(path);
|
||||
AssetDatabase.CreateAsset(texture, path);
|
||||
OutputTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
||||
textureDirty = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
texture.name = OutputTexture.name;
|
||||
EditorUtility.CopySerialized(texture, OutputTexture);
|
||||
textureDirty = true;
|
||||
}
|
||||
|
||||
if (textureDirty)
|
||||
{
|
||||
EditorUtility.SetDirty(OutputTexture);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user