Skip to content

Instantly share code, notes, and snippets.

@kawashirov
Last active September 18, 2023 01:23
Show Gist options
  • Save kawashirov/458f6f03ecf8c8741652b5aa55cdff70 to your computer and use it in GitHub Desktop.
Save kawashirov/458f6f03ecf8c8741652b5aa55cdff70 to your computer and use it in GitHub Desktop.
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