Last active
September 18, 2023 01:23
-
-
Save kawashirov/458f6f03ecf8c8741652b5aa55cdff70 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System; | |
using UnityEngine; | |
#if UNITY_EDITOR | |
using UnityEditor; | |
using UnityEditor.Animations; | |
#endif | |
using VRC.SDK3.Avatars.ScriptableObjects; | |
using static VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu; | |
using Kawashirov; | |
public class TemmiePixiciGenerator : ScriptableObject { | |
#if UNITY_EDITOR | |
[CustomEditor(typeof(TemmiePixiciGenerator))] | |
class TemmiePixiciGeneratorEditor : Editor { | |
public override void OnInspectorGUI() { | |
base.OnInspectorGUI(); | |
EditorGUILayout.Space(); | |
if (GUILayout.Button("Generate")) { | |
var gen = target as TemmiePixiciGenerator; | |
gen.Generate(); | |
} | |
} | |
} | |
[MenuItem("Kawashirov/Create TemmiePixiciGenerator")] | |
internal static void CreateGenerator() { | |
var gen = new TemmiePixiciGenerator(); | |
var script = MonoScript.FromScriptableObject(gen); | |
var script_path = AssetDatabase.GetAssetPath(script); | |
var gen_path = Path.Combine(Path.GetDirectoryName(script_path), Path.GetFileNameWithoutExtension(script_path) + ".asset"); | |
AssetDatabase.CreateAsset(gen, gen_path); | |
} | |
public Color background = new Color32(11, 105, 193, 0); | |
public Vector2Int baseSize = new Vector2Int(18, 18); | |
public int[] specialIds = new int[] { 0, 255 }; | |
public static string CleanUpName(string name) { | |
foreach (var trim in new string[] { "_", ".", " ", "-" }) | |
name = name.Replace(trim, ""); | |
// name = name.ToLowerInvariant(); | |
return name; | |
} | |
internal class Pixici { | |
public TemmiePixiciGenerator Generator; | |
public int ID; | |
public string Name; | |
public string NameCleaned; | |
public string AssetPath; | |
public TextureImporter Importer; | |
public Texture2D Tex; | |
public Vector2Int AtlasGrid; | |
public RectInt AtlasRect; | |
public Vector4 AtlasST; | |
internal void ParseIDAndName() { | |
var filename = Path.GetFileNameWithoutExtension(AssetPath); | |
var parts = filename.Split(new char[] { '_' }, 2); | |
if (parts.Length != 2) | |
throw new ArgumentException($"Invalid filename {filename}: Can't parse ID and Name!"); | |
ID = int.Parse(parts[0]); | |
Name = parts[1]; | |
NameCleaned = CleanUpName(Name); | |
} | |
internal bool EnsureImporterCorrect() { | |
Importer = AssetImporter.GetAtPath(AssetPath) as TextureImporter; | |
if (Importer == null) | |
throw new ArgumentException($"There is no TextureImporter for {AssetPath}!"); | |
Importer.textureType = TextureImporterType.Default; | |
Importer.textureShape = TextureImporterShape.Texture2D; | |
Importer.sRGBTexture = true; | |
Importer.alphaSource = TextureImporterAlphaSource.FromInput; | |
Importer.alphaIsTransparency = true; | |
Importer.npotScale = TextureImporterNPOTScale.None; | |
Importer.isReadable = true; | |
Importer.mipmapEnabled = false; | |
Importer.mipmapEnabled = false; | |
Importer.wrapMode = TextureWrapMode.Clamp; | |
Importer.filterMode = FilterMode.Point; | |
if (EditorUtility.IsDirty(Importer)) { | |
Debug.Log($"Reimporting {AssetPath}...", Generator); | |
Importer.SaveAndReimport(); | |
return true; | |
} | |
return false; | |
} | |
internal void EnsureTextureCorrect() { | |
Tex = AssetDatabase.LoadAllAssetsAtPath(AssetPath).OfType<Texture2D>().Single(); | |
var size = Generator.baseSize; | |
if (Tex.width != size.x || Tex.height != size.y) | |
throw new ArgumentException($"Invalid size of {AssetPath}: Got {Tex.width}x{Tex.height}, must be {size.x}x{size.y}"); | |
} | |
internal Control MakeMenuControl() { | |
var control = new Control() { | |
name = Name, | |
icon = Tex, | |
type = Control.ControlType.Toggle, | |
parameter = new Control.Parameter() { name = "_Character" }, | |
value = ID, | |
}; | |
return control; | |
} | |
} | |
internal class PixiciNameComparer : IComparer<Pixici> { | |
public int Compare(Pixici x, Pixici y) => string.Compare(x.NameCleaned, y.NameCleaned); | |
} | |
internal static PixiciNameComparer pixiciNameCmp = new PixiciNameComparer(); | |
internal class TexelData { | |
public bool isUsed; | |
public bool isAlwaysUsed; | |
public Vector2 index; | |
public int boneIndex; | |
public GameObject bone; | |
} | |
internal class ControlNameComparer : IComparer<Control> { | |
public int Compare(Control x, Control y) => string.Compare(CleanUpName(x.name), CleanUpName(y.name)); | |
} | |
internal static ControlNameComparer controlNameCmp = new ControlNameComparer(); | |
internal List<Pixici> pixicies = new List<Pixici>(); | |
internal string basePath; | |
internal string sourcesPath; | |
internal string generatedPath; | |
internal TexelData[,] texels; | |
internal int usedPixelsCount; | |
internal int alwaysUsedPixelsCount; | |
internal List<GameObject> bones; | |
internal int gridSize; | |
internal Texture2D atlas; | |
internal string atlasPath; | |
internal Material material; | |
internal string materialPath; | |
internal GameObject contaier; | |
internal string contaierPath; | |
internal SkinnedMeshRenderer contaierRenderer; | |
internal Mesh mesh; | |
internal string meshPath; | |
internal Vector3[] meshVertices; | |
internal int meshVerticesIdx; | |
internal int[] meshTriangles; | |
internal int meshTrianglesIdx; | |
internal Vector2[] meshUV; | |
internal Vector3[] meshShapeCollapse; | |
internal BoneWeight[] meshBones; | |
internal AnimatorController controller; | |
internal string controllerPath; | |
internal VRCExpressionsMenu menu; | |
internal string menuPath; | |
internal void LoadPixicies() { | |
pixicies.Clear(); | |
Debug.Log($"Loading sprites from {sourcesPath}...", this); | |
foreach (var guid in AssetDatabase.FindAssets("", new string[] { sourcesPath })) { | |
var source = AssetDatabase.GUIDToAssetPath(guid); | |
var pixici = new Pixici { Generator = this, AssetPath = source }; | |
pixici.ParseIDAndName(); | |
pixicies.Add(pixici); | |
} | |
var names = string.Join(", ", pixicies.Select(p => $"#{p.ID} \"{p.Name}\"")); | |
Debug.Log($"Loaded {pixicies.Count} sprites: {names}", this); | |
} | |
internal void EnsureImportersCorrect() { | |
var is_asset_modified = false; | |
try { | |
AssetDatabase.StartAssetEditing(); | |
foreach (var pixici in pixicies) | |
is_asset_modified |= pixici.EnsureImporterCorrect(); | |
} finally { | |
AssetDatabase.StopAssetEditing(); | |
} | |
if (is_asset_modified) | |
throw new InvalidOperationException("Some assets was modified, need to re-do imports. Try again."); | |
} | |
internal void RepackAtlas() { | |
var BLOCK_SIZE = 4; | |
gridSize = Mathf.CeilToInt(Mathf.Sqrt(pixicies.Count)); | |
var width = Mathf.CeilToInt(baseSize.x * gridSize * 1f / BLOCK_SIZE) * BLOCK_SIZE; | |
var height = Mathf.CeilToInt(baseSize.y * gridSize * 1f / BLOCK_SIZE) * BLOCK_SIZE; | |
atlasPath = Path.Combine(generatedPath, "TemmiePixiciAtlas.asset"); | |
atlas = (Texture2D)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(Texture2D)); | |
if (atlas != null) | |
AssetDatabase.DeleteAsset(atlasPath); | |
Debug.Log($"Creating new Atlas: grid={gridSize}, {width}x{height}..."); | |
atlas = new Texture2D(width, height, TextureFormat.RGBA32, false); | |
AssetDatabase.CreateAsset(atlas, atlasPath); | |
atlas.alphaIsTransparency = true; | |
atlas.filterMode = FilterMode.Point; | |
atlas.wrapMode = TextureWrapMode.Clamp; | |
var fillColor = background; | |
fillColor.a = 0; | |
for (var x = 0; x < atlas.width; ++x) | |
for (var y = 0; y < atlas.height; ++y) | |
atlas.SetPixel(x, y, fillColor); | |
for (var i = 0; i < pixicies.Count; ++i) { | |
var pixici = pixicies[i]; | |
pixici.AtlasGrid = new Vector2Int(i % gridSize, i / gridSize); | |
var coord = pixici.AtlasGrid * baseSize; | |
pixici.AtlasRect = new RectInt(coord, baseSize); | |
Graphics.CopyTexture(pixici.Tex, 0, 0, 0, 0, baseSize.x, baseSize.y, atlas, 0, 0, coord.x, coord.y); | |
pixici.AtlasST.x = 1f * baseSize.x / atlas.width; | |
pixici.AtlasST.y = 1f * baseSize.y / atlas.height; | |
pixici.AtlasST.z = pixici.AtlasST.x * pixici.AtlasGrid.x; | |
pixici.AtlasST.w = pixici.AtlasST.y * pixici.AtlasGrid.y; | |
} | |
for (var x = 0; x < atlas.width; ++x) | |
for (var y = 0; y < atlas.height; ++y) | |
if (atlas.GetPixel(x, y).a == 0) | |
atlas.SetPixel(x, y, fillColor); | |
// EditorUtility.CompressTexture(atlas, TextureFormat.BC7, TextureCompressionQuality.Best); | |
atlas.Apply(true, false); | |
Debug.Log($"Created atlas {atlas}: {width}x{height}..."); | |
AssetDatabase.SaveAssets(); | |
} | |
internal void MakeMaterial() { | |
materialPath = Path.Combine(generatedPath, "TemmiePixiciMaterial.mat"); | |
material = AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material)) as Material; | |
//var shader = AssetDatabase.FindAssets("t:Shader") | |
// .Select(g => AssetDatabase.GUIDToAssetPath(g)) | |
// .Select(p => AssetDatabase.LoadAssetAtPath(p, typeof(Shader))) | |
// .OfType<Shader>() | |
// .Where(s => "VRChat/Mobile/Diffuse".Equals(s.name)) | |
// .First(); | |
var shader = Shader.Find("Standard"); | |
if (material == null) { | |
Debug.Log($"Creating new TextureAtlas Material..."); | |
material = new Material(shader); | |
AssetDatabase.CreateAsset(material, materialPath); | |
} | |
if (material.shader != shader) | |
material.shader = shader; | |
material.SetTexture("_MainTex", atlas); | |
AssetDatabase.SaveAssets(); | |
} | |
internal IEnumerable<Vector2Int> IterateTexelIndexes() { | |
// LINQ | |
for (var x = 0; x < baseSize.x; ++x) | |
for (var y = 0; y < baseSize.y; ++y) | |
yield return new Vector2Int(x, y); | |
} | |
internal void PrepareTexels() { | |
texels = new TexelData[baseSize.x, baseSize.y]; | |
for (var x = 0; x < baseSize.x; ++x) | |
for (var y = 0; y < baseSize.y; ++y) | |
texels[x, y] = new TexelData() { index = new Vector2Int(x, y), isAlwaysUsed = true }; | |
foreach (var pixici in pixicies) | |
for (var x = 0; x < baseSize.x; ++x) | |
for (var y = 0; y < baseSize.y; ++y) | |
if (pixici.Tex.GetPixel(x, y).a > 0.1f) | |
texels[x, y].isUsed = true; | |
else | |
texels[x, y].isAlwaysUsed = false; | |
usedPixelsCount = IterateTexelIndexes().Select(i => texels[i.x, i.y]).Where(t => t.isUsed).Count(); | |
alwaysUsedPixelsCount = IterateTexelIndexes().Select(i => texels[i.x, i.y]).Where(t => t.isAlwaysUsed).Count(); | |
var total = baseSize.x * baseSize.y; | |
Debug.Log($"Found {usedPixelsCount}/{total} used pixels, {alwaysUsedPixelsCount}/{total} always used pixels."); | |
} | |
internal void MakeContainerPrefab() { | |
contaierPath = Path.Combine(generatedPath, "TemmiePixiciTexels.prefab"); | |
contaier = File.Exists(contaierPath) ? PrefabUtility.LoadPrefabContents(contaierPath) : null; | |
if (contaier == null) { | |
contaier = new GameObject("TexelsContainer"); | |
PrefabUtility.SaveAsPrefabAsset(contaier, contaierPath); | |
} | |
contaier.transform.Reset(); | |
if (!contaier.TryGetComponent(out contaierRenderer)) { | |
contaierRenderer = contaier.AddComponent<SkinnedMeshRenderer>(); | |
} | |
var children = contaier.GetComponentsInChildren<Transform>(true); | |
foreach (var child in children) | |
if (child != contaier.transform) | |
DestroyImmediate(child.gameObject); | |
PrefabUtility.SaveAsPrefabAsset(contaier, contaierPath); | |
} | |
internal Mesh GetCubeMesh() { | |
var go = GameObject.CreatePrimitive(PrimitiveType.Cube); | |
var filter = go.GetComponent<MeshFilter>(); | |
var mesh = filter.sharedMesh; | |
DestroyImmediate(go); | |
return mesh; | |
} | |
void MakeMesh_PushQuad(Vector3[] position, Vector2[] uv, Vector3[] position_collapse, int bone_index) { | |
Array.Copy(position, 0, meshVertices, meshVerticesIdx, 4); | |
Array.Copy(position_collapse, 0, meshShapeCollapse, meshVerticesIdx, 4); | |
Array.Copy(uv, 0, meshUV, meshVerticesIdx, 4); | |
var bone_weight = new BoneWeight { boneIndex0 = Mathf.Max(0, bone_index), weight0 = bone_index < 0 ? 0 : 1 }; | |
Array.Copy(Enumerable.Repeat(bone_weight, 4).ToArray(), 0, meshBones, meshVerticesIdx, 4); | |
meshTriangles[meshTrianglesIdx + 0 + 0] = meshVerticesIdx + 0; | |
meshTriangles[meshTrianglesIdx + 0 + 1] = meshVerticesIdx + 1; | |
meshTriangles[meshTrianglesIdx + 0 + 2] = meshVerticesIdx + 2; | |
meshTriangles[meshTrianglesIdx + 3 + 0] = meshVerticesIdx + 2; | |
meshTriangles[meshTrianglesIdx + 3 + 1] = meshVerticesIdx + 3; | |
meshTriangles[meshTrianglesIdx + 3 + 2] = meshVerticesIdx + 0; | |
meshVerticesIdx += 4; | |
meshTrianglesIdx += 2 * 3; | |
} | |
void MakeMesh_ProcessTexel(TexelData texel) { | |
if (!texel.isUsed) | |
return; | |
// Debug.Log($"PushTexel {index.x},{index.y}: V={meshVerticesIdx}/{meshVertices.Length}, T={meshTrianglesIdx}/{meshTriangles.Length}"); | |
var center = new Vector3(texel.index.x + 0.5f - (baseSize.x * 0.5f), texel.index.y + 0.5f, 0); | |
// Когда мы смотрим на модель спереди у нас X в мире идет сПрава на Лево, а на текстуре сЛева на Право. | |
center.x *= -1; | |
// Кубик размером чуть больше 10см (что бы не было видно щелей) | |
var p = new Vector3[2, 2, 2]; | |
var eps1 = 1 + Vector3.kEpsilon; | |
for (var i = 0; i < 2; ++i) | |
for (var j = 0; j < 2; ++j) | |
for (var k = 0; k < 2; ++k) | |
p[i, j, k] = new Vector3(i == 0 ? -1 : 1, j == 0 ? -1 : 1, k == 0 ? -1 : 1) * (0.5f * eps1) + center; | |
// Квадратная UVшка | |
var uv = new Vector2[2, 2]; | |
uv[0, 0] = (texel.index + new Vector2(0.1f, 0.1f)) / baseSize; | |
uv[0, 1] = (texel.index + new Vector2(0.1f, 0.9f)) / baseSize; | |
uv[1, 1] = (texel.index + new Vector2(0.9f, 0.9f)) / baseSize; | |
uv[1, 0] = (texel.index + new Vector2(0.9f, 0.1f)) / baseSize; | |
var bone_index = -1; | |
if (true || !texel.isAlwaysUsed) { | |
var bone_obj = new GameObject($"texel_{texel.index.x:00}_{texel.index.y:00}"); | |
bone_obj.transform.Reset(); | |
bone_obj.transform.position = center; | |
texel.bone = bone_obj; | |
texel.boneIndex = bones.Count; | |
bones.Add(bone_obj); | |
bone_index = texel.boneIndex; | |
bone_obj.transform.parent = contaier.transform; | |
} | |
// Тут бы канешно не плохо зашли выделения массивов на стеке... | |
var collapsed = new Vector3[] { center, center, center, center }; | |
MakeMesh_PushQuad( // Front Z+ | |
new Vector3[] { p[1, 0, 1], p[1, 1, 1], p[0, 1, 1], p[0, 0, 1] }, | |
new Vector2[] { uv[0, 0], uv[0, 1], uv[1, 1], uv[1, 0] }, | |
collapsed, bone_index); | |
MakeMesh_PushQuad( // Back Z- | |
new Vector3[] { p[0, 0, 0], p[0, 1, 0], p[1, 1, 0], p[1, 0, 0] }, | |
new Vector2[] { uv[1, 0], uv[1, 1], uv[0, 1], uv[0, 0] }, | |
collapsed, bone_index); | |
MakeMesh_PushQuad( // Top Y+ | |
new Vector3[] { p[1, 1, 1], p[1, 1, 0], p[0, 1, 0], p[0, 1, 1] }, | |
new Vector2[] { uv[0, 1], uv[0, 1], uv[1, 1], uv[1, 1] }, | |
collapsed, bone_index); | |
MakeMesh_PushQuad( // Bottom Y- | |
new Vector3[] { p[1, 0, 0], p[1, 0, 1], p[0, 0, 1], p[0, 0, 0] }, | |
new Vector2[] { uv[0, 0], uv[0, 0], uv[1, 0], uv[1, 0] }, | |
collapsed, bone_index); | |
MakeMesh_PushQuad( // Right X+ | |
new Vector3[] { p[1, 0, 0], p[1, 1, 0], p[1, 1, 1], p[1, 0, 1] }, | |
new Vector2[] { uv[0, 0], uv[0, 1], uv[0, 1], uv[0, 0] }, | |
collapsed, bone_index); | |
MakeMesh_PushQuad( // Left X- | |
new Vector3[] { p[0, 0, 1], p[0, 1, 1], p[0, 1, 0], p[0, 0, 0] }, | |
new Vector2[] { uv[1, 0], uv[1, 1], uv[1, 1], uv[1, 0] }, | |
collapsed, bone_index); | |
} | |
internal void MakeMesh() { | |
meshPath = Path.Combine(generatedPath, "TemmiePixiciMesh.asset"); | |
mesh = AssetDatabase.LoadAssetAtPath(meshPath, typeof(Mesh)) as Mesh; | |
if (mesh == null) { | |
Debug.Log($"Creating new Mesh..."); | |
mesh = new Mesh(); | |
AssetDatabase.CreateAsset(mesh, meshPath); | |
} | |
mesh.Clear(); | |
// По 6 квадов на пиксель, по 4 точки на квад. | |
// Мы не можем делать общие вертексы, т.к. у них нормлали должны в другую сторону смотреть. | |
meshVertices = new Vector3[usedPixelsCount * 6 * 4]; | |
meshVerticesIdx = 0; | |
// По 6 квадов на пиксель, по 2 треугольник ана каждый квад | |
meshTriangles = new int[usedPixelsCount * 6 * 2 * 3]; | |
meshTrianglesIdx = 0; | |
meshUV = new Vector2[meshVertices.Length]; | |
meshShapeCollapse = new Vector3[meshVertices.Length]; | |
meshBones = new BoneWeight[meshVertices.Length]; | |
bones = new List<GameObject>(baseSize.x * baseSize.y); | |
for (var x = 0; x < baseSize.x; ++x) | |
for (var y = 0; y < baseSize.y; ++y) | |
MakeMesh_ProcessTexel(texels[x, y]); | |
// Бинд костей этой меши и скиннед-меш-рендерер контейнер префаб | |
//mesh.bindposes = bones.Select(b => b.transform.worldToLocalMatrix).ToArray(); | |
//contaierRenderer.bones = bones.Select(b => b.transform).ToArray(); | |
//contaierRenderer.sharedMesh = mesh; | |
//contaierRenderer.sharedMaterial = material; | |
//mesh.boneWeights = meshBones; | |
mesh.bindposes = null; | |
mesh.boneWeights = null; | |
mesh.vertices = meshVertices; | |
mesh.triangles = meshTriangles; | |
mesh.uv = meshUV; | |
var visemes = new Dictionary<string, Vector3>() { | |
// Тут все тупо как в CATS | |
{ "aa", new Vector3(0, 0, 0.9998f) }, | |
{ "ch", new Vector3(0, 0.9996f, 0) }, | |
{ "dd", new Vector3(0, 0.7f, 0.3f) }, | |
{ "e", new Vector3(0.3f, 0.7f, 0) }, | |
{ "ff", new Vector3(0, 0.4f, 0.2f) }, | |
{ "ih", new Vector3(0, 0.2f, 0.5f) }, | |
{ "kk", new Vector3(0, 0.4f, 0.7f) }, | |
{ "nn", new Vector3(0, 0.7f, 0.2f) }, | |
{ "oh", new Vector3(0.8f, 0, 0.2f) }, | |
{ "ou", new Vector3(0.9994f, 0, 0) }, | |
{ "pp", new Vector3(0.0004f, 0, 0.0004f) }, | |
{ "rr", new Vector3(0.3f, 0.5f, 0) }, | |
{ "ss", new Vector3(0, 0.8f, 0) }, | |
{ "th", new Vector3(0.15f, 0, 0.4f) }, | |
}; | |
for (var i = 0; i < meshVertices.Length; ++i) | |
// Конверсия абсолютных позиций в дельты. | |
meshShapeCollapse[i] -= meshVertices[i]; | |
mesh.AddBlendShapeFrame("vrc.v_sil", 100, Enumerable.Repeat(Vector3.zero, meshVertices.Length).ToArray(), null, null); | |
foreach (var viseme in visemes) | |
mesh.AddBlendShapeFrame($"vrc.v_{viseme.Key}", 100, meshShapeCollapse.Select(v => Vector3.Scale(v, viseme.Value) * 0.2f).ToArray(), null, null); | |
mesh.AddBlendShapeFrame("Collapse", 100, meshShapeCollapse, null, null); | |
mesh.AddBlendShapeFrame("Collapse X", 100, meshShapeCollapse.Select(v => Vector3.right * v.x).ToArray(), null, null); | |
mesh.AddBlendShapeFrame("Collapse Y", 100, meshShapeCollapse.Select(v => Vector3.up * v.y).ToArray(), null, null); | |
mesh.AddBlendShapeFrame("Collapse Z", 100, meshShapeCollapse.Select(v => Vector3.forward * v.z).ToArray(), null, null); | |
mesh.RecalculateNormals(); | |
mesh.RecalculateTangents(); | |
mesh.RecalculateBounds(); | |
mesh.Optimize(); | |
// Очистка референсов | |
meshVertices = null; | |
meshTriangles = null; | |
meshUV = null; | |
meshShapeCollapse = null; | |
bones.Clear(); | |
bones = null; | |
AssetDatabase.SaveAssets(); | |
} | |
internal void SavePrefab() { | |
PrefabUtility.SaveAsPrefabAsset(contaier, contaierPath); | |
PrefabUtility.UnloadPrefabContents(contaier); | |
} | |
internal void CleanupAnimator() { | |
while (true) { | |
var layers = controller.layers; | |
if (layers.Length < 1) | |
break; | |
controller.RemoveLayer(layers.Length - 1); | |
} | |
while (true) { | |
var parameters = controller.parameters; | |
if (parameters.Length < 1) | |
break; | |
controller.RemoveParameter(parameters.Length - 1); | |
} | |
foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(controllerPath)) | |
if (obj != controller) { | |
AssetDatabase.RemoveObjectFromAsset(obj); | |
DestroyImmediate(obj); | |
} | |
} | |
internal void MakeCharaLayer() { | |
var paramChara = new AnimatorControllerParameter { | |
name = "_Character", | |
type = AnimatorControllerParameterType.Int, | |
defaultInt = 0 | |
}; | |
controller.AddParameter(paramChara); | |
controller.AddLayer("Character"); | |
var layerCharacter = controller.layers.First(l => l.name == "Character"); | |
layerCharacter.defaultWeight = 1; | |
layerCharacter.stateMachine.anyStatePosition = new Vector3(-200, 200); | |
layerCharacter.stateMachine.exitPosition = new Vector3(200, 200); | |
layerCharacter.stateMachine.entryPosition = new Vector3(-200, -200); | |
for (var i = 0; i < pixicies.Count; ++i) { | |
var pixici = pixicies[i]; | |
//var pos = Matrix4x4.Rotate(Quaternion.Euler(0, 0, 360f * i / pixicies.Count)) | |
// .MultiplyPoint3x4(new Vector3(0, 500 + (i % 4) * 50, 0)); | |
var pos = new Vector3(pixici.AtlasGrid.x * 210, -pixici.AtlasGrid.y * 50); | |
var state = layerCharacter.stateMachine.AddState($"{pixici.ID} {pixici.AtlasGrid.x},{pixici.AtlasGrid.y} {pixici.NameCleaned}", pos); | |
state.writeDefaultValues = false; | |
var transition = layerCharacter.stateMachine.AddAnyStateTransition(state); | |
transition.name = state.name; | |
transition.canTransitionToSelf = false; | |
transition.duration = 0; | |
transition.hasExitTime = false; | |
transition.AddCondition(AnimatorConditionMode.Equals, pixici.ID, "_Character"); | |
if (pixici.ID == 0) | |
layerCharacter.stateMachine.defaultState = state; | |
var clip = new AnimationClip { name = state.name }; | |
clip.hideFlags |= HideFlags.NotEditable; | |
var curveSTx = AnimationCurve.Constant(0, 0, pixici.AtlasST.x); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._MainTex_ST.x", curveSTx); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._EmissionMap_ST.x", curveSTx); | |
var curveSTy = AnimationCurve.Constant(0, 0, pixici.AtlasST.y); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._MainTex_ST.y", curveSTy); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._EmissionMap_ST.y", curveSTy); | |
var curveSTz = AnimationCurve.Constant(0, 0, pixici.AtlasST.z); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._MainTex_ST.z", curveSTz); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._EmissionMap_ST.z", curveSTz); | |
var curveSTw = AnimationCurve.Constant(0, 0, pixici.AtlasST.w); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._MainTex_ST.w", curveSTw); | |
clip.SetCurve("Body", typeof(SkinnedMeshRenderer), "material._EmissionMap_ST.w", curveSTw); | |
state.motion = clip; | |
AssetDatabase.AddObjectToAsset(clip, controllerPath); | |
state.hideFlags |= HideFlags.NotEditable; | |
transition.hideFlags |= HideFlags.NotEditable; | |
} | |
} | |
internal void MakeUprightScaleLayer() { | |
var paramUpright = new AnimatorControllerParameter { | |
name = "Upright", | |
type = AnimatorControllerParameterType.Float, | |
defaultFloat = 1 | |
}; | |
controller.AddParameter(paramUpright); | |
controller.AddLayer("UprightScale"); | |
var layerUprightScale = controller.layers.First(l => l.name == "UprightScale"); | |
layerUprightScale.defaultWeight = 1; | |
layerUprightScale.stateMachine.anyStatePosition = new Vector3(-200, 200); | |
layerUprightScale.stateMachine.exitPosition = new Vector3(200, 200); | |
layerUprightScale.stateMachine.entryPosition = new Vector3(-200, -200); | |
var clipStand = new AnimationClip { name = "UprightStand" }; | |
clipStand.hideFlags |= HideFlags.NotEditable; | |
var curveStand = AnimationCurve.Constant(0, 0, 0.1f * 1.00f); | |
clipStand.SetCurve("Body", typeof(Transform), "localScale.x", curveStand); | |
clipStand.SetCurve("Body", typeof(Transform), "localScale.y", curveStand); | |
clipStand.SetCurve("Body", typeof(Transform), "localScale.z", curveStand); | |
AssetDatabase.AddObjectToAsset(clipStand, controllerPath); | |
var clipCrouch = new AnimationClip { name = "UprightCrouching" }; | |
clipCrouch.hideFlags |= HideFlags.NotEditable; | |
var curveCrouch = AnimationCurve.Constant(0, 0, 0.1f * 0.60f); | |
clipCrouch.SetCurve("Body", typeof(Transform), "localScale.x", curveCrouch); | |
clipCrouch.SetCurve("Body", typeof(Transform), "localScale.y", curveCrouch); | |
clipCrouch.SetCurve("Body", typeof(Transform), "localScale.z", curveCrouch); | |
AssetDatabase.AddObjectToAsset(clipCrouch, controllerPath); | |
var clipProne = new AnimationClip { name = "UprightProne" }; | |
clipProne.hideFlags |= HideFlags.NotEditable; | |
var curveProne = AnimationCurve.Constant(0, 0, 0.1f * 0.35f); | |
clipProne.SetCurve("Body", typeof(Transform), "localScale.x", curveProne); | |
clipProne.SetCurve("Body", typeof(Transform), "localScale.y", curveProne); | |
clipProne.SetCurve("Body", typeof(Transform), "localScale.z", curveProne); | |
AssetDatabase.AddObjectToAsset(clipProne, controllerPath); | |
var blendTree = new BlendTree { | |
name = "UprightScale", | |
blendType = BlendTreeType.Simple1D, | |
blendParameter = "Upright", | |
useAutomaticThresholds = false, | |
}; | |
blendTree.AddChild(clipProne, 0.35f); | |
blendTree.AddChild(clipCrouch, 0.60f); | |
blendTree.AddChild(clipStand, 1.00f); | |
AssetDatabase.AddObjectToAsset(blendTree, controllerPath); | |
var state = layerUprightScale.stateMachine.AddState($"UprightScale", new Vector3(0, 0)); | |
state.motion = blendTree; | |
state.hideFlags |= HideFlags.NotEditable; | |
layerUprightScale.stateMachine.defaultState = state; | |
layerUprightScale.defaultWeight = 1; | |
} | |
internal void MakeAnimator() { | |
controllerPath = Path.Combine(generatedPath, "TemmiePixiciController.controller"); | |
controller = AssetDatabase.LoadAssetAtPath(controllerPath, typeof(AnimatorController)) as AnimatorController; | |
if (controller == null) { | |
Debug.Log($"Creating new AnimatorController..."); | |
controller = new AnimatorController(); | |
AssetDatabase.CreateAsset(controller, controllerPath); | |
} | |
CleanupAnimator(); | |
MakeCharaLayer(); | |
MakeUprightScaleLayer(); | |
AssetDatabase.SaveAssets(); | |
} | |
string PackControls_FirstName(Control control, int letters) { | |
if (control.subMenu != null && control.subMenu.controls.Count > 0) | |
return PackControls_FirstName(control.subMenu.controls.First(), letters); | |
var n = CleanUpName(control.name); | |
return n.Length > letters ? n.Substring(0, letters) : n; | |
} | |
string PackControls_LastName(Control control, int letters) { | |
if (control.subMenu != null && control.subMenu.controls.Count > 0) | |
return PackControls_LastName(control.subMenu.controls.Last(), letters); | |
var n = CleanUpName(control.name); | |
return n.Length > letters ? n.Substring(0, letters) : n; | |
} | |
string PackControls_SubName(Control control, int letters) { | |
var first = PackControls_FirstName(control, letters); | |
var last = PackControls_LastName(control, letters); | |
return first.Equals(last) ? first : $"{first} ⋯ {last}"; | |
} | |
Texture2D PackControls_Icon(Control control) { | |
if (control.subMenu != null && control.subMenu.controls.Count > 0) | |
return PackControls_Icon(control.subMenu.controls[control.subMenu.controls.Count / 2]); | |
return control.icon; | |
} | |
internal Control PackControls(List<Control> controls, int maxtopLevel) { | |
int[] solution = null, suggested = new int[4]; | |
// Ебейший перебор за O(n^4) потому что мне лень курить деревья ради этой хуйни. | |
for (var a = 0; a < MAX_CONTROLS; ++a) { | |
suggested[0] = a; | |
for (var b = 0; b < MAX_CONTROLS; ++b) { | |
suggested[1] = b; | |
for (var c = 0; c < MAX_CONTROLS; ++c) { | |
suggested[2] = c; | |
for (var d = 0; d < MAX_CONTROLS; ++d) { | |
suggested[3] = d; | |
// Необходимые условия. | |
if (suggested.All(x => x > maxtopLevel)) | |
// Должно быть хотя бы одно с maxtopLevel или менее. | |
continue; | |
if (suggested.Any(x => x == 1)) | |
// Меню с 1 элементом не имеют смысла. | |
continue; | |
var sug_agg = suggested.Aggregate((agg, x) => x > 0 ? agg * x : agg); | |
if (sug_agg < controls.Count) | |
// Нужно иметь возможность поместить все контролы в иерархию. | |
continue; | |
if (solution == null) { | |
solution = new int[4]; | |
// Debug.Log($"First solution: {string.Join(",", suggested)}"); | |
Array.Copy(suggested, solution, 4); | |
continue; | |
} | |
// Оптимизационные условия. | |
if (suggested.Count(x => x != 0) > solution.Count(x => x != 0)) | |
// Меньше уровней = лучше. | |
continue; | |
//if (suggested.Max() < solution.Max()) | |
// continue; // Максимизируем объем последнего уровня иерархии. | |
//var sol_agg = solution.Aggregate((agg, x) => x > 0 ? agg * x : agg); | |
//if (sug_agg > sol_agg) | |
// continue; // Лучше найти максимально узкие уровни. | |
if (suggested.Sum() > solution.Sum()) | |
// Лучше найти максимально узкие уровни. | |
continue; | |
var max = suggested.Max(); | |
if (max == 0 || pixicies.Count % max == 1) | |
// Самый глубокий уровень меню, в котором окажется только 1 эл это кринж. | |
continue; | |
//Debug.Log($"{string.Join(",", solution)} -> {string.Join(",", suggested)}"); | |
Array.Copy(suggested, solution, 4); | |
} | |
} | |
} | |
} | |
var levels = solution.Where(x => x != 0).OrderBy(x => x).ToList(); | |
Debug.Log($"Got {levels.Count} layout layers ({string.Join(",", levels)}) for {controls.Count} controls."); | |
var queue = new Queue<Control>(controls); | |
Control BuildHierarchy(int depth) { | |
if (depth >= levels.Count) | |
return queue.Count > 0 ? queue.Dequeue() : null; | |
var width = levels[depth]; | |
var sub_controls = Enumerable.Range(0, width) | |
.Select(x => BuildHierarchy(depth + 1)) | |
.Where(x => x != null) | |
.ToList(); | |
if (sub_controls.Count < 1 && depth != 0) | |
return null; | |
if (sub_controls.Count == 1 && depth != 0) | |
return sub_controls.First(); | |
var sub_menu = CreateInstance<VRCExpressionsMenu>(); | |
sub_menu.name = "DUMMY"; | |
sub_menu.controls = sub_controls; | |
var sub_control = new Control { name = "DUMMY", type = Control.ControlType.SubMenu, subMenu = sub_menu }; | |
sub_control.name = sub_control.subMenu.name = PackControls_SubName(sub_control, 3); | |
sub_control.icon = PackControls_Icon(sub_control); | |
AssetDatabase.AddObjectToAsset(sub_menu, menuPath); | |
return sub_control; | |
} | |
return BuildHierarchy(0); | |
} | |
internal List<Control> UnpackControl(Control control) { | |
var packed = new List<Control>(); | |
if (control.subMenu != null && control.subMenu.controls != null) | |
packed.AddRange(control.subMenu.controls); | |
else | |
packed.Add(control); | |
return packed; | |
} | |
internal void MakeMenus() { | |
menuPath = Path.Combine(generatedPath, "TemmiePixiciMenus.asset"); | |
menu = AssetDatabase.LoadMainAssetAtPath(menuPath) as VRCExpressionsMenu; | |
if (menu == null) { | |
Debug.Log($"Creating new VRCExpressionsMenu..."); | |
menu = CreateInstance<VRCExpressionsMenu>(); | |
AssetDatabase.CreateAsset(menu, menuPath); | |
} | |
menu.controls.Clear(); | |
foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(menuPath)) | |
if (obj != menu) { | |
AssetDatabase.RemoveObjectFromAsset(obj); | |
DestroyImmediate(obj); | |
} | |
var specialControls = new List<Control>(); | |
var generalControls = new List<Control>(); | |
foreach (var pixici in pixicies) | |
(specialIds.Contains(pixici.ID) ? specialControls : generalControls).Add(pixici.MakeMenuControl()); | |
specialControls.Sort(controlNameCmp); | |
generalControls.Sort(controlNameCmp); | |
Debug.Log($"Collected {specialControls.Count} special controls, {generalControls.Count} general controls"); | |
// var special = PackControls(specialControls, 2); | |
// special.subMenu.name = special.name = "Special"; | |
// special.icon = pixicies.Where(p => p.ID == 255).First().Tex; | |
var special_sub_menu = CreateInstance<VRCExpressionsMenu>(); | |
special_sub_menu.name = "Special"; | |
special_sub_menu.controls = specialControls; | |
AssetDatabase.AddObjectToAsset(special_sub_menu, menuPath); | |
var special = new Control { | |
name = "Special", | |
type = Control.ControlType.SubMenu, | |
subMenu = special_sub_menu, | |
icon = pixicies.Where(p => p.ID == 255).First().Tex | |
}; | |
var general = PackControls(generalControls, MAX_CONTROLS - 1); | |
menu.controls.Add(special); | |
menu.controls.AddRange(UnpackControl(general)); | |
AssetDatabase.SaveAssets(); | |
} | |
internal void Generate() { | |
basePath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(this)); | |
sourcesPath = Path.Combine(basePath, "Sources"); | |
generatedPath = Path.Combine(basePath, "Generated"); | |
LoadPixicies(); | |
EnsureImportersCorrect(); | |
foreach (var pixici in pixicies) | |
pixici.EnsureTextureCorrect(); | |
RepackAtlas(); | |
MakeMaterial(); | |
PrepareTexels(); | |
MakeContainerPrefab(); | |
MakeMesh(); | |
SavePrefab(); | |
MakeAnimator(); | |
MakeMenus(); | |
} | |
#endif // UNITY_EDITOR | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment