diff --git a/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportOnButtonClickBase.cs b/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportOnButtonClickBase.cs index a65012e7d..ba0b96089 100644 --- a/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportOnButtonClickBase.cs +++ b/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportOnButtonClickBase.cs @@ -33,8 +33,8 @@ protected virtual void Awake() _saveFolderProvider = Singletons.GetSingleton(); _selection = this.GetCharacterCreatorComponent(); _dataRepo = this.GetComponentInParent(); - _tooltip = GetComponent(); - // These GameObjects have multiple of the same component on them, so we need this ugly code to find the right ones. + _tooltip = GetComponent(); + // These GameObjects have multiple of the same component on them, so we need this ugly code to find the right ones. ApplySliderAsScaleBase[] applySliders = _headSizeApplyGameObject.GetComponents(); foreach (ApplySliderAsScaleBase slider in applySliders) { @@ -109,13 +109,38 @@ public float GetHeadEarLength() protected string GetSavePath() { - var selected = _selection.Selected; - var name = (selected != null) ? selected.CachedData.Name : "UnnamedYinglet"; - var sanitizedName = System.Text.RegularExpressions.Regex.Replace(name, "[^a-zA-Z0-9_-]", ""); - var folderName = sanitizedName + "-" + System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + CachedYingletReference yingRef = _selection.Selected; + string name = (yingRef != null) ? yingRef.CachedData.Name : "UnnamedYinglet"; + string sanitizedName = System.Text.RegularExpressions.Regex.Replace(name, "[^a-zA-Z0-9_-]", ""); + string folderName = sanitizedName + "-" + System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); return System.IO.Path.Combine(_saveFolderProvider.ExportsFolderPath, folderName); } + protected Texture2D GetThumbnailTexture() + { + CachedYingletReference yingRef = _selection.Selected; + IYingSnapshotManager snapshotManager = Singletons.GetSingleton(); + using (IYingSnapshotRenderTexture snapshot = snapshotManager.GetRenderTexture(yingRef)) + { + RenderTexture renderTex = snapshot.RenderTexture; + if (renderTex == null) + { + return null; + } + // Convert RenderTexture to Texture2D + RenderTexture.active = renderTex; + // Hard-code square thumbnail size. The VRM specification says: "The thumbnail image must be square". + int newSize = renderTex.height * 3 / 4; + int offsetX = (renderTex.width - newSize) / 2; + int offsetY = 0; // Align to bottom (Unity's texture coordinate system is bottom-left origin). + Texture2D thumbnail = new Texture2D(newSize, newSize, TextureFormat.RGBA32, false); + thumbnail.ReadPixels(new Rect(offsetX, offsetY, newSize, newSize), 0, 0); + thumbnail.Apply(); + RenderTexture.active = null; + return thumbnail; + } + } + protected void EmitExportEvent() { OnExport.Invoke(); diff --git a/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportToModelOnButtonClick.cs b/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportToModelOnButtonClick.cs index dab8f1efa..c6c0ff2b3 100644 --- a/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportToModelOnButtonClick.cs +++ b/Assets/Scripts/Entities/Character/Creator/UI/Export/ExportToModelOnButtonClick.cs @@ -5,10 +5,10 @@ public class ExportToModelOnButtonClick : ExportOnButtonClickBase private enum ExportJSONModelFormat { NONE, - G3MF, // .g3b or .g3tf if shift is pressed - GLTF, // .glb or .gltf if shift is pressed - VRM_0_x, // .vrm 0.x or .gltf if shift is pressed - VRM_1_0, // .vrm 1.0 or .gltf if shift is pressed + G3MF, // .g3b or .g3tf if shift is pressed + GLTF, // .glb or .gltf if shift is pressed + VRM_0_x, // .vrm 0.x or .gltf if shift is pressed + VRM_1_0, // .vrm 1.0 or .gltf if shift is pressed } [SerializeField] private ExportJSONModelFormat _exportFormat = ExportJSONModelFormat.NONE; [SerializeField] private JigglePhysics.JiggleRigBuilder _jiggleRigBuilder; @@ -61,12 +61,12 @@ protected override void OnExportButtonClicked() { Debug.LogError("Yinglet root is not assigned."); return; - } - // Prepare data. + } + // Prepare data. ModelMaterial.pupilTexture = _pupilTexture; MeshObjectOptimization meshObjOpt = (MeshObjectOptimization)_meshOptimizationDropdown.value; - EyeExpression eyeExp = GetEyeExpression(); - // Begin export. + EyeExpression eyeExp = GetEyeExpression(); + // Begin export. ModelDocument yinglet = ModelDocument.CreateFromYingletSkeleton(_skeletonHips); yinglet.SetExplicitBoneLengths(GetHeadAntennaeLength(), GetHeadEarLength()); yinglet.ConvertYingletMeshes(_yingletRoot, meshObjOpt, eyeExp); @@ -93,13 +93,14 @@ protected override void OnExportButtonClicked() SetFloatPrecisionForModelAccessors(baseFormat, _floatPrecisionDropdown.value); yinglet.PerformOptionalCleanups(); yinglet.EncodeMeshDataIntoAccessors(baseFormat); - yinglet.ExportTextures(_imageFormatDropdown.value); yinglet.EncodeAnimationAccessors(baseFormat); - string savePath = GetSavePath(); - // Note: The non-VRM 0.x formats all include the VRM 1.0 metadata. - // This is because VRM 1.0 is a clean superset of the standard rig, so - // there is no downside to including the data, and it could be used outside - // of VRM applications, like reading the toon materials or spring bone data. + yinglet.EncodeTextures(_imageFormatDropdown.value); + yinglet.EncodeThumbnail(GetThumbnailTexture(), _imageFormatDropdown.value); + string savePath = GetSavePath(); + // Note: The non-VRM 0.x formats all include the VRM 1.0 metadata. + // This is because VRM 1.0 is a clean superset of the standard rig, so + // there is no downside to including the data, and it could be used outside + // of VRM applications, like reading the toon materials or spring bone data. switch (_exportFormat) { case ExportJSONModelFormat.NONE: @@ -123,16 +124,16 @@ protected override void OnExportButtonClicked() private static void SetFloatPrecisionForModelAccessors(ModelBaseFormat format, int floatPrecisionDropdownValue) { - if (floatPrecisionDropdownValue == 0) // Automatic - { - // Automatic based on format: G3MF = float16, glTF = float32. + if (floatPrecisionDropdownValue == 0) // Automatic + { + // Automatic based on format: G3MF = float16, glTF = float32. floatPrecisionDropdownValue = (format == ModelBaseFormat.G3MF) ? 1 : 2; } - if (floatPrecisionDropdownValue == 1) // Force 16-bit (half) + if (floatPrecisionDropdownValue == 1) // Force 16-bit (half) { ModelAccessor.preferredFloatComponentType = "float16"; } - else if (floatPrecisionDropdownValue == 2) // Force 32-bit (single) + else if (floatPrecisionDropdownValue == 2) // Force 32-bit (single) { ModelAccessor.preferredFloatComponentType = "float32"; } diff --git a/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelDocument.cs b/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelDocument.cs index d62709617..0e53fdc12 100644 --- a/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelDocument.cs +++ b/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelDocument.cs @@ -64,6 +64,7 @@ public class ModelDocument /// private HashSet uniqueNames = new HashSet(); private Dictionary nodeIndexToJointMap = null; + private int thumbnailTextureAndImageIndex = -1; /// /// If true, inject extra LowerLeg nodes between UpperLeg and LowerLeg1 to improve compatibility @@ -458,6 +459,18 @@ public string ReserveUniqueName(string desiredName) return uniqueName; } + public ModelAnimation FindAnimationByName(string name) + { + for (int i = 0; i < animations.Count; i++) + { + if (animations[i].name == name) + { + return animations[i]; + } + } + return null; + } + public int FindNodeIndexByName(string name) { for (int i = 0; i < nodes.Count; i++) @@ -548,7 +561,15 @@ public void EncodeMeshDataIntoAccessors(ModelBaseFormat format) } } - public void ExportTextures(int imageFormat) + public void EncodeAnimationAccessors(ModelBaseFormat format) + { + for (int i = 0; i < animations.Count; i++) + { + animations[i].EncodeZeroTimeAnimationAccessors(this, format); + } + } + + public void EncodeTextures(int imageFormat) { for (int i = 0; i < materials.Count; i++) { @@ -563,12 +584,14 @@ public void ExportTextures(int imageFormat) } } - public void EncodeAnimationAccessors(ModelBaseFormat format) + public void EncodeThumbnail(Texture2D thumbnailTexture, int imageFormat) { - for (int i = 0; i < animations.Count; i++) + if (thumbnailTexture == null) { - animations[i].EncodeZeroTimeAnimationAccessors(this, format); + Debug.LogError("No thumbnail texture provided for export."); + return; } + thumbnailTextureAndImageIndex = ModelTexture.FromUnityTexture2D(this, thumbnailTexture, "thumbnail", imageFormat); } public void ExportToG3MF(string path) @@ -583,9 +606,19 @@ public void ExportToG3MF(string path) json.Append("\"KHR_character\",\"KHR_character_expression\",\"KHR_character_skeleton_mapping\""); json.Append(",\"KHR_materials_unlit\",\"KHR_texture_transform\",\"KHR_xmp_json_ld\""); // VRM extension declarations. - json.Append(",\"VRMC_materials_mtoon\",\"VRMC_springBone\",\"VRMC_vrm\""); - json.Append("]"); // End extensionsUsed - json.Append(",\"generator\":\"Yinglet Creator\",\"specification\":\"https://github.com/godot-dimensions/g4mf\"}"); + //json.Append(",\"VRMC_materials_mtoon\""); + json.Append(",\"VRMC_springBone\",\"VRMC_vrm\""); + json.Append("]"); // End extensionsUsed. + json.Append(",\"generator\":\""); + json.Append(GetGeneratorText()); + json.Append("\",\"specification\":\"https://github.com/godot-dimensions/g4mf\""); + if (thumbnailTextureAndImageIndex >= 0) + { + json.Append(",\"thumbnail\":"); + json.Append(thumbnailTextureAndImageIndex); + } + // End asset header. + json.Append("}"); // Accessors. ExportCollectionToJSON(json, "accessors", accessors, ModelBaseFormat.G3MF); // Animations. @@ -675,7 +708,9 @@ public void ExportToGLTF(string path, int vrmVersion = 1) json.Append("{\"asset\":{"); json.Append("\"copyright\":\"" + System.DateTime.UtcNow.ToString("yyyy") + " Yinglet Creator\""); json.Append(",\"extensions\":{\"KHR_xmp_json_ld\":{\"packet\":0}}"); // Packet applies to asset header / entire file. - json.Append(",\"generator\":\"Yinglet Creator\",\"version\":\"2.0\"}"); + json.Append(",\"generator\":\""); + json.Append(GetGeneratorText()); + json.Append("\",\"version\":\"2.0\"}"); // Accessors. ExportCollectionToJSON(json, "accessors", accessors, ModelBaseFormat.GLTF); // Animations. @@ -727,7 +762,8 @@ public void ExportToGLTF(string path, int vrmVersion = 1) } else { - json.Append(",\"VRMC_materials_mtoon\",\"VRMC_springBone\",\"VRMC_vrm\""); + //json.Append(",\"VRMC_materials_mtoon\""); + json.Append(",\"VRMC_springBone\",\"VRMC_vrm\""); } json.Append("]"); // End extensionsUsed // Encode images of textures. @@ -906,8 +942,19 @@ private void ExportKhrCharacterSkeletonMappingExtension(StringBuilder json) private void ExportVRM0xTopLevelExtensions(StringBuilder json) { json.Append("\"VRM\":{"); - json.Append("\"exporterVersion\":\"Yinglet Creator\""); - json.Append(",\"humanoid\":{"); + json.Append("\"exporterVersion\":\""); + json.Append(GetGeneratorText()); + json.Append("\""); + json.Append(",\"firstPerson\":{"); + { + // Hard-coded values designed to work well with the exported Yinglet eye bones. + json.Append("\"lookAtHorizontalInner\":{\"xRange\":50.0,\"yRange\":3.0},"); + json.Append("\"lookAtHorizontalOuter\":{\"xRange\":50.0,\"yRange\":6.0},"); + json.Append("\"lookAtTypeName\":\"Bone\","); + json.Append("\"lookAtVerticalDown\":{\"xRange\":40.0,\"yRange\":3.0},"); + json.Append("\"lookAtVerticalUp\":{\"xRange\":40.0,\"yRange\":3.0}"); + } + json.Append("},\"humanoid\":{"); { json.Append("\"humanBones\":["); int vrmHumanBoneIndex = 0; @@ -946,6 +993,12 @@ private void ExportVRM0xTopLevelExtensions(StringBuilder json) json.Append("\"licenseName\":\"CC_BY_NC_SA\","); json.Append("\"otherLicenseUrl\":\"https://github.com/TBartl/YingletCreator/blob/master/LICENSE\","); json.Append("\"sexualUssageName\":\"Allow\","); + if (thumbnailTextureAndImageIndex >= 0) + { + json.Append("\"texture\":"); // VRM 0.x has the dumbest names I swear. + json.Append(thumbnailTextureAndImageIndex); + json.Append(","); + } json.Append("\"title\":\""); json.Append(nodes[0].name); json.Append("\","); @@ -985,7 +1038,23 @@ private void ExportVRM10TopLevelExtensions(StringBuilder json) } json.Append("},\"VRMC_vrm\":{"); { - json.Append("\"humanoid\":{"); + json.Append("\"expressions\":{"); + { + json.Append("\"custom\":{"); + ModelAnimation winceAnim = FindAnimationByName("EyesWince"); + AnimationToVRM10ExpressionJSON(json, winceAnim, "wince"); + json.Append("},\"preset\":{"); + int expressionIndex = 0; + foreach (var kvp in vrmExpressionToAnimationName) + { + ModelAnimation animation = FindAnimationByName(kvp.Value); + if (expressionIndex > 0) json.Append(","); + AnimationToVRM10ExpressionJSON(json, animation, kvp.Key); + expressionIndex++; + } + json.Append("}"); + } + json.Append("},\"humanoid\":{"); { json.Append("\"humanBones\":{"); int vrmHumanBoneIndex = 0; @@ -999,6 +1068,15 @@ private void ExportVRM10TopLevelExtensions(StringBuilder json) } json.Append("}"); } + json.Append("},\"lookAt\":{"); + { + // Hard-coded values designed to work well with the exported Yinglet eye bones. + json.Append("\"rangeMapHorizontalInner\":{\"inputMaxValue\":50.0,\"outputScale\":3.0},"); + json.Append("\"rangeMapHorizontalOuter\":{\"inputMaxValue\":50.0,\"outputScale\":6.0},"); + json.Append("\"rangeMapVerticalDown\":{\"inputMaxValue\":40.0,\"outputScale\":3.0},"); + json.Append("\"rangeMapVerticalUp\":{\"inputMaxValue\":40.0,\"outputScale\":3.0},"); + json.Append("\"type\":\"bone\""); + } json.Append("},\"meta\":{"); { json.Append("\"allowAntisocialOrHateUsage\":true,"); @@ -1017,6 +1095,12 @@ private void ExportVRM10TopLevelExtensions(StringBuilder json) json.Append(nodes[0].name); json.Append("\","); json.Append("\"otherLicenseUrl\":\"https://github.com/TBartl/YingletCreator/blob/master/LICENSE\","); + if (thumbnailTextureAndImageIndex >= 0) + { + json.Append("\"thumbnailImage\":"); + json.Append(thumbnailTextureAndImageIndex); + json.Append(","); + } json.Append("\"version\":\"0.0\""); } json.Append("},"); @@ -1025,6 +1109,22 @@ private void ExportVRM10TopLevelExtensions(StringBuilder json) json.Append("}"); } + private void AnimationToVRM10ExpressionJSON(StringBuilder json, ModelAnimation animation, string vrmName) + { + json.Append("\""); + json.Append(vrmName); + json.Append("\":{\"isBinary\":true,\"textureTransformBinds\":[{"); + json.Append("\"material\":"); + json.Append(animation.targetMaterialIndex); + json.Append(",\"offset\":["); + json.Append(ModelItem.Flt(animation.offset.x)); + json.Append(","); + json.Append(ModelItem.Flt(animation.offset.y)); + json.Append("]"); // Close offset + json.Append(",\"scale\":[1.0,1.0]"); // HACK: UniVRM crashes when scale is missing, even though it's optional in the spec. + json.Append("}]}"); // Close textureTransformBinds, close expression. + } + private void ExportCollectionToJSON(StringBuilder json, string name, List items, ModelBaseFormat baseFormat) where T : ModelItem { json.Append(",\"" + name + "\":["); @@ -1064,4 +1164,23 @@ private int EncodeInverseBindMatrices() } return ModelAccessor.EncodeMatrix4x4s(this, inverseBindMatrices); } + + private static string GetGeneratorText() + { + return "Yinglet Creator " + Application.version; + } + + /// + /// VRM 1.0's expression names have a decent mapping for everything except "EyesWince". + /// + private static readonly Dictionary vrmExpressionToAnimationName = new Dictionary + { + {"angry", "EyesAngry"}, + {"blink", "EyesBlink"}, + {"happy", "EyesPleased"}, + {"neutral", "EyesNeutral"}, + {"relaxed", "EyesSquint"}, + {"sad", "EyesSad"}, + {"surprised", "EyesShocked"}, + }; } diff --git a/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelMaterial.cs b/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelMaterial.cs index 6550c1fe1..7586aad85 100644 --- a/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelMaterial.cs +++ b/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelMaterial.cs @@ -207,7 +207,7 @@ public override string ModelItemToJSON(ModelBaseFormat format) json.Append("\"alphaMode\":\"MASK\","); json.Append("\"extensions\":{"); json.Append("\"KHR_materials_unlit\":{}"); - json.Append(",\"VRMC_materials_mtoon\":{\"specVersion\":\"1.0\"}"); + //json.Append(",\"VRMC_materials_mtoon\":{\"specVersion\":\"1.0\"}"); json.Append("}"); // End extensions json.Append(",\"name\":\"" + name + "\""); json.Append(",\"pbrMetallicRoughness\":{\"baseColorTexture\":{"); @@ -232,7 +232,7 @@ public override string ModelItemToJSON(ModelBaseFormat format) json.Append("}"); // End baseColor json.Append(",\"extensions\":{"); json.Append("\"KHR_materials_unlit\":{}"); - json.Append(",\"VRMC_materials_mtoon\":{\"specVersion\":\"1.0\"}"); + //json.Append(",\"VRMC_materials_mtoon\":{\"specVersion\":\"1.0\"}"); json.Append("}"); // End extensions json.Append(",\"name\":\"" + name + "\""); } diff --git a/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelTexture.cs b/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelTexture.cs index f21d9346d..804d932d0 100644 --- a/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelTexture.cs +++ b/Assets/Scripts/Entities/Character/Creator/UI/Export/JSONBasedModels/ModelTexture.cs @@ -19,13 +19,18 @@ public static int FromMaterial(ModelDocument doc, ModelMaterial material, int im return i; } } + return FromUnityTexture2D(doc, material.unityTexture, material.name, imageFormat); + } + + public static int FromUnityTexture2D(ModelDocument doc, Texture2D unityTexture, string baseName, int imageFormat) + { // Make a new texture. ModelTexture modelTexture = new ModelTexture(); - // Convert the material name to snake_case for the texture name. - modelTexture.name = System.Text.RegularExpressions.Regex.Replace(PascalToSnake(material.name), "[^a-z0-9]", "_"); + // Convert the base name to snake_case for the texture name. + modelTexture.name = System.Text.RegularExpressions.Regex.Replace(PascalToSnake(baseName), "[^a-z0-9]", "_"); modelTexture.name = modelTexture.name.Replace("__", "_").Replace("mat_", "tex_").Replace("material", "texture"); modelTexture.name = doc.ReserveUniqueName(modelTexture.name); - modelTexture.unityTexture = material.unityTexture; + modelTexture.unityTexture = unityTexture; if (imageFormat == 0) // PNG { modelTexture.imageMimeType = "image/png"; @@ -41,7 +46,7 @@ public static int FromMaterial(ModelDocument doc, ModelMaterial material, int im modelTexture.bufferViewIndex = ModelBufferView.FromByteArrayIntoDoc(doc, jpgData); } int texIndex = doc.textures.Count; - modelTexture.imageIndex = texIndex; // For simplicity, these will be in sync, excluding deduplicating the eyes at the end. + modelTexture.imageIndex = texIndex; // For simplicity, these will be in sync. doc.textures.Add(modelTexture); return texIndex; } diff --git a/Assets/Scripts/Entities/Character/Creator/UI/MainPage/YingPortraits/YingSnapshotManager.cs b/Assets/Scripts/Entities/Character/Creator/UI/MainPage/YingPortraits/YingSnapshotManager.cs index 8371cc6f8..a14cc7ed4 100644 --- a/Assets/Scripts/Entities/Character/Creator/UI/MainPage/YingPortraits/YingSnapshotManager.cs +++ b/Assets/Scripts/Entities/Character/Creator/UI/MainPage/YingPortraits/YingSnapshotManager.cs @@ -1,154 +1,154 @@ -using Character.Creator; -using Reactivity; -using Snapshotter; -using System; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - - -/// -/// Responsible for -/// -public interface IYingSnapshotManager -{ - /// - /// Gets a render texture that will be updated when possible - /// This will be shared by other sources trying to get the same texture - /// This should be disposed to ensure the render texture is cleaned up when no longer needed - /// - IYingSnapshotRenderTexture GetRenderTexture(CachedYingletReference yingletData); -} - -interface IYingSnapshotManagerReferences -{ - ISnapshotterReferences References { get; } - SnapshotterCameraPosition CameraPosition { get; } - ICompositeResourceLoader ResourceLoader { get; } - Coroutine StartCoroutine(IEnumerator routine); -} - -public interface IYingSnapshotRenderTexture : IDisposable -{ - RenderTexture RenderTexture { get; } -} - - -public class YingSnapshotManager : MonoBehaviour, IYingSnapshotManager, IYingSnapshotManagerReferences -{ - [SerializeField] SnapshotterReferences _references; - [SerializeField] AssetReferenceT _cameraPositionReference; - - Dictionary _snapshots = new(); - private ICompositeResourceLoader _resourceLoader; - - public ISnapshotterReferences References => _references; - public SnapshotterCameraPosition CameraPosition => _cameraPositionReference.LoadSync(); - public ICompositeResourceLoader ResourceLoader => _resourceLoader; - - private void Awake() - { - _resourceLoader = Singletons.GetSingleton(); - } - - public IYingSnapshotRenderTexture GetRenderTexture(CachedYingletReference yingletData) - { - DictValue dictValue = null; - if (_snapshots.TryGetValue(yingletData, out var cachedDictValue)) - { - dictValue = cachedDictValue; - } - if (dictValue == null) - { - dictValue = new DictValue(this, yingletData); - _snapshots.Add(yingletData, dictValue); - } - - dictValue.Watchers++; - - return new YingSnapshotRenderTexture(dictValue.RenderTexture, () => - { - dictValue.Watchers--; - if (dictValue.Watchers <= 0) - { - dictValue.Dispose(); - _snapshots.Remove(yingletData); - } - }); - } - - - sealed class DictValue : IDisposable - { - public int Watchers { get; set; } = 0; - IYingSnapshotManagerReferences _snapshotReferences; - CachedYingletReference _yingReference; - public RenderTexture RenderTexture { get; private set; } - Reflector _reflector; - - public DictValue(IYingSnapshotManagerReferences snapshotReferences, CachedYingletReference yingReference) - { - _snapshotReferences = snapshotReferences; - _yingReference = yingReference; - RenderTexture = SnapshotterUtils.CreateRenderTexture(snapshotReferences.References); - _reflector = new Reflector(Reflect); - } - - public void Dispose() - { - RenderTexture = null; - _reflector.Destroy(); - } - - private void Reflect() - { - var cachedData = _yingReference.CachedData; // Not actually used; just for reflection - RunThrottled(Snapshot); - } - - void Snapshot() - { - var observableData = new ObservableCustomizationData(_yingReference.CachedData, _snapshotReferences.ResourceLoader); - RenderTexture = SnapshotterUtils.Snapshot( - _snapshotReferences.References, - new SnapshotterParams(_snapshotReferences.CameraPosition, observableData), - RenderTexture); - } - - static Coroutine currentChain; - void RunThrottled(Action action) - { - IEnumerator Chain() - { - // Wait until the current chain is done - if (currentChain != null) - yield return currentChain; - - yield return null; - - action(); - - currentChain = null; - } - - currentChain = _snapshotReferences.StartCoroutine(Chain()); - } - - } - - sealed class YingSnapshotRenderTexture : IYingSnapshotRenderTexture - { - private Action _dispose; - public YingSnapshotRenderTexture(RenderTexture renderTexture, Action dispose) - { - RenderTexture = renderTexture; - _dispose = dispose; - } - public RenderTexture RenderTexture { get; } - - public void Dispose() - { - _dispose(); - } - } -} +using Character.Creator; +using Reactivity; +using Snapshotter; +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + + +/// +/// Responsible for +/// +public interface IYingSnapshotManager +{ + /// + /// Gets a render texture that will be updated when possible + /// This will be shared by other sources trying to get the same texture + /// This should be disposed to ensure the render texture is cleaned up when no longer needed + /// + IYingSnapshotRenderTexture GetRenderTexture(CachedYingletReference yingletData); +} + +interface IYingSnapshotManagerReferences +{ + ISnapshotterReferences References { get; } + SnapshotterCameraPosition CameraPosition { get; } + ICompositeResourceLoader ResourceLoader { get; } + Coroutine StartCoroutine(IEnumerator routine); +} + +public interface IYingSnapshotRenderTexture : IDisposable +{ + RenderTexture RenderTexture { get; } +} + + +public class YingSnapshotManager : MonoBehaviour, IYingSnapshotManager, IYingSnapshotManagerReferences +{ + [SerializeField] SnapshotterReferences _references; + [SerializeField] AssetReferenceT _cameraPositionReference; + + Dictionary _snapshots = new(); + private ICompositeResourceLoader _resourceLoader; + + public ISnapshotterReferences References => _references; + public SnapshotterCameraPosition CameraPosition => _cameraPositionReference.LoadSync(); + public ICompositeResourceLoader ResourceLoader => _resourceLoader; + + private void Awake() + { + _resourceLoader = Singletons.GetSingleton(); + } + + public IYingSnapshotRenderTexture GetRenderTexture(CachedYingletReference yingletData) + { + DictValue dictValue = null; + if (_snapshots.TryGetValue(yingletData, out var cachedDictValue)) + { + dictValue = cachedDictValue; + } + if (dictValue == null) + { + dictValue = new DictValue(this, yingletData); + _snapshots.Add(yingletData, dictValue); + } + + dictValue.Watchers++; + + return new YingSnapshotRenderTexture(dictValue.RenderTexture, () => + { + dictValue.Watchers--; + if (dictValue.Watchers <= 0) + { + dictValue.Dispose(); + _snapshots.Remove(yingletData); + } + }); + } + + + sealed class DictValue : IDisposable + { + public int Watchers { get; set; } = 0; + IYingSnapshotManagerReferences _snapshotReferences; + CachedYingletReference _yingReference; + public RenderTexture RenderTexture { get; private set; } + Reflector _reflector; + + public DictValue(IYingSnapshotManagerReferences snapshotReferences, CachedYingletReference yingReference) + { + _snapshotReferences = snapshotReferences; + _yingReference = yingReference; + RenderTexture = SnapshotterUtils.CreateRenderTexture(snapshotReferences.References); + _reflector = new Reflector(Reflect); + } + + public void Dispose() + { + RenderTexture = null; + _reflector.Destroy(); + } + + private void Reflect() + { + var cachedData = _yingReference.CachedData; // Not actually used; just for reflection + RunThrottled(Snapshot); + } + + void Snapshot() + { + var observableData = new ObservableCustomizationData(_yingReference.CachedData, _snapshotReferences.ResourceLoader); + RenderTexture = SnapshotterUtils.Snapshot( + _snapshotReferences.References, + new SnapshotterParams(_snapshotReferences.CameraPosition, observableData), + RenderTexture); + } + + static Coroutine currentChain; + void RunThrottled(Action action) + { + IEnumerator Chain() + { + // Wait until the current chain is done + if (currentChain != null) + yield return currentChain; + + yield return null; + + action(); + + currentChain = null; + } + + currentChain = _snapshotReferences.StartCoroutine(Chain()); + } + + } + + sealed class YingSnapshotRenderTexture : IYingSnapshotRenderTexture + { + private Action _dispose; + public YingSnapshotRenderTexture(RenderTexture renderTexture, Action dispose) + { + RenderTexture = renderTexture; + _dispose = dispose; + } + public RenderTexture RenderTexture { get; } + + public void Dispose() + { + _dispose(); + } + } +}