// Copyright (c) 2024 Synty Studios Limited. All rights reserved. // // Use of this software is subject to the terms and conditions of the Synty Studios End User Licence Agreement (EULA) // available at: https://syntystore.com/pages/end-user-licence-agreement // // For additional details, see the LICENSE.MD file bundled with this software. using Synty.SidekickCharacters.Blendshapes; using Synty.SidekickCharacters.Database; using Synty.SidekickCharacters.Database.DTO; using Synty.SidekickCharacters.Enums; using Synty.SidekickCharacters.SkinnedMesh; using Synty.SidekickCharacters.Utils; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using UnityEngine; using Object = UnityEngine.Object; namespace Synty.SidekickCharacters.API { public class SidekickRuntime { private const string _BLEND_GENDER_NAME = "masculineFeminine"; private const string _BLEND_MUSCLE_NAME = "defaultBuff"; private const string _BLEND_SHAPE_HEAVY_NAME = "defaultHeavy"; private const string _BLEND_SHAPE_SKINNY_NAME = "defaultSkinny"; private const string _TEXTURE_COLOR_NAME = "ColorMap.png"; private const string _TEXTURE_METALLIC_NAME = "MetallicMap.png"; private const string _TEXTURE_SMOOTHNESS_NAME = "SmoothnessMap.png"; private const string _TEXTURE_REFLECTION_NAME = "ReflectionMap.png"; private const string _TEXTURE_EMISSION_NAME = "EmissionMap.png"; private const string _TEXTURE_OPACITY_NAME = "OpacityMap.png"; private const string _TEXTURE_PREFIX = "T_"; private static readonly int _COLOR_MAP = Shader.PropertyToID("_ColorMap"); private static readonly int _METALLIC_MAP = Shader.PropertyToID("_MetallicMap"); private static readonly int _SMOOTHNESS_MAP = Shader.PropertyToID("_SmoothnessMap"); private static readonly int _REFLECTION_MAP = Shader.PropertyToID("_ReflectionMap"); private static readonly int _EMISSION_MAP = Shader.PropertyToID("_EmissionMap"); private static readonly int _OPACITY_MAP = Shader.PropertyToID("_OpacityMap"); private DatabaseManager _dbManager; private GameObject _baseModel; private Material _currentMaterial; private RuntimeAnimatorController _currentAnimationController; private List _currentUVList; private Dictionary> _currentUVDictionary; private Dictionary _blendShapeRigMovement; private Dictionary _blendShapeRigRotation; private Dictionary> _partLibrary; private Dictionary> _allPartsLibrary; private Dictionary> _partOutfitMap; private Dictionary _partOutfitToggleMap; private Dictionary>>> _filterPartDictionary; private Dictionary> _mappedPartDictionary; private Dictionary> _mappedPartList; private Dictionary>> _mappedBasePartDictionary; private Dictionary _speciesDictionary; private Dictionary> _mappedPresetFilterDictionary; private Dictionary> _mappedBasePresetDictionary; private int _partCount; private SidekickSpecies _currentSpecies; private float _bodyTypeBlendValue; private float _bodySizeSkinnyBlendValue; private float _bodySizeHeavyBlendValue; private float _musclesBlendValue; public DatabaseManager DBManager { get => _dbManager; set => _dbManager = value; } public GameObject BaseModel { get => _baseModel; set => _baseModel = value; } public Material CurrentMaterial { get => _currentMaterial; set => _currentMaterial = value; } public RuntimeAnimatorController CurrentAnimationController { get => _currentAnimationController; set => _currentAnimationController = value; } public List CurrentUVList { get => _currentUVList; set => _currentUVList = value; } public Dictionary> CurrentUVDictionary { get => _currentUVDictionary; set => _currentUVDictionary = value; } public Dictionary> PartLibrary { get => _partLibrary; set => _partLibrary = value; } public int PartCount { get => _partCount; private set => _partCount = value; } public Dictionary> PartOutfitMap { get => _partOutfitMap; set => _partOutfitMap = value; } public Dictionary PartOutfitToggleMap { get => _partOutfitToggleMap; set => _partOutfitToggleMap = value; } public float BodyTypeBlendValue { get => _bodyTypeBlendValue; set => _bodyTypeBlendValue = value; } public float BodySizeSkinnyBlendValue { get => _bodySizeSkinnyBlendValue; set => _bodySizeSkinnyBlendValue = value; } public float BodySizeHeavyBlendValue { get => _bodySizeHeavyBlendValue; set => _bodySizeHeavyBlendValue = value; } public float MusclesBlendValue { get => _musclesBlendValue; set => _musclesBlendValue = value; } public SidekickSpecies CurrentSpecies { get => _currentSpecies; set => _currentSpecies = value; } public Dictionary>>> FilterPartDictionary { get => _filterPartDictionary; private set => _filterPartDictionary = value; } public Dictionary> MappedPartDictionary { get => _mappedPartDictionary; private set => _mappedPartDictionary = value; } public Dictionary>> MappedBasePartDictionary { get => _mappedBasePartDictionary; private set => _mappedBasePartDictionary = value; } public Dictionary> MappedPartList { get => _mappedPartList; private set => _mappedPartList = value; } public Dictionary> AllPartsLibrary { get => _allPartsLibrary; private set => _allPartsLibrary = value; } public Dictionary> MappedPresetFilterDictionary { get => _mappedPresetFilterDictionary; private set => _mappedPresetFilterDictionary = value; } public Dictionary> MappedBasePresetDictionary { get => _mappedBasePresetDictionary; private set => _mappedBasePresetDictionary = value; } /// /// Creates and instance of the SidekickRuntime with the given parameters. /// /// The base donor model to use. This is used to provide a base rig that parts can be added and removed from. /// The base material that will be applied to all parts that are added or removed from the character. /// The animation controller to apply to the created model. /// The Database Manager to use, if not provided a new connection will be created. public SidekickRuntime(GameObject model, Material material, RuntimeAnimatorController animationController = null, DatabaseManager dbManager = null) { _dbManager = dbManager ?? new DatabaseManager(); if (_dbManager.GetCurrentDbConnection() == null) { _dbManager.GetDbConnection(true); } _baseModel = model; _currentMaterial = material; _currentAnimationController = animationController; } public static async Task PopulateToolData(SidekickRuntime runtime) { await runtime.LoadPartLibrary(); await runtime.PopulatePresetLibrary(); } /// /// Takes all the parts selected in the window, and combines them into a single model in the scene. /// /// What to call the parent GameObject of the created character.; /// The list of SkinnedMeshes to combine to create the character. /// When true the character mesh will be combined into a single mesh. /// When true the bones will be moved to match the blend shape settings. /// A new character object. public GameObject CreateCharacter( string modelName, List toCombine, bool combineMesh, bool processBoneMovement, GameObject existingModel = null ) { PopulateUVDictionary(toCombine); GameObject newSpawn; if (combineMesh) { newSpawn = Combiner.CreateCombinedSkinnedMesh(toCombine, _baseModel, _currentMaterial); } else { newSpawn = CreateModelFromParts(toCombine, modelName, existingModel); } newSpawn.name = modelName; Renderer renderer = newSpawn.GetComponentInChildren(); if (renderer != null) { renderer.sharedMaterial = _currentMaterial; } if (newSpawn.GetComponent() == null) { if (existingModel == null) { Animator newModelAnimator = newSpawn.AddComponent(); Animator baseModelAnimator = _baseModel.GetComponentInChildren(); newModelAnimator.avatar = baseModelAnimator.avatar; newModelAnimator.Rebind(); if (_currentAnimationController != null) { newModelAnimator.runtimeAnimatorController = _currentAnimationController; } } else { Animator newModelAnimator = newSpawn.AddComponent(); Animator baseModelAnimator = existingModel.GetComponentInChildren(); newModelAnimator.avatar = baseModelAnimator.avatar; newModelAnimator.Rebind(); } } UpdateBlendShapes(newSpawn); if (processBoneMovement) { ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager)); ProcessBoneMovement(newSpawn); } return newSpawn; } /// /// Creates the model but with all parts as separate meshes. /// /// The parts to build into the character. /// What to call the parent GameObject of the created character. /// A new game object with all the part meshes and a single rig. public GameObject CreateModelFromParts( List parts, string outputModelName, GameObject existingModel = null ) { List allTypes = Enum.GetValues(typeof(CharacterPartType)).Cast().ToList(); GameObject partsModel = existingModel == null ? new GameObject(outputModelName) : existingModel; Transform modelRootBone = _baseModel.GetComponentInChildren().rootBone; GameObject newRootBone; if (existingModel != null) { GameObject oldRootBone = existingModel.transform.Find("root").gameObject; #if UNITY_EDITOR GameObject.DestroyImmediate(oldRootBone); #else GameObject.Destroy(oldRootBone); #endif } newRootBone = Object.Instantiate(modelRootBone.gameObject, partsModel.transform, true); newRootBone.name = modelRootBone.name; Hashtable boneNameMap = Combiner.CreateBoneNameMap(newRootBone); Transform[] bones = new Transform[boneNameMap.Count]; if (existingModel != null) { boneNameMap.Values.CopyTo(bones, 0); } Transform[] additionalBones = Combiner.FindAdditionalBones(boneNameMap, new List(parts)); if (additionalBones.Length > 0) { Combiner.JoinAdditionalBonesToBoneArray(bones, additionalBones, boneNameMap); // Need to redo the name map now that we have updated the bone array. boneNameMap = Combiner.CreateBoneNameMap(newRootBone); } for (int i = 0; i < parts.Count; i++) { SkinnedMeshRenderer part = parts[i]; allTypes.Remove(ExtractPartType(part.name)); if (existingModel != null && partsModel != null) { SkinnedMeshRenderer existingPart = partsModel.GetComponentsInChildren() .FirstOrDefault(go => go.name.Contains(ExtractPartTypeString(part.name))); if (existingPart != null) { #if UNITY_EDITOR GameObject.DestroyImmediate(existingPart.gameObject); #else GameObject.Destroy(existingModel.gameObject); #endif } } GameObject newPart = new GameObject(part.name); newPart.transform.parent = partsModel.transform; SkinnedMeshRenderer renderer = newPart.AddComponent(); renderer.updateWhenOffscreen = true; Transform[] oldBones = part.bones; Transform[] newBones = new Transform[part.bones.Length]; for (int j = 0; j < oldBones.Length; j++) { newBones[j] = (Transform) boneNameMap[oldBones[j].name]; } renderer.sharedMesh = MeshUtils.CopyMesh(part.sharedMesh); renderer.rootBone = (Transform) boneNameMap[part.rootBone.name]; Combiner.MergeAndGetAllBlendShapeDataOfSkinnedMeshRenderers( new[] { part }, renderer.sharedMesh, renderer ); renderer.bones = newBones; renderer.sharedMaterial = _currentMaterial; } foreach (CharacterPartType type in allTypes) { SkinnedMeshRenderer existingPart = partsModel.GetComponentsInChildren() .FirstOrDefault(go => go.name.Contains(CharacterPartTypeUtils.GetPartTypeString(type))); if (existingPart != null) { #if UNITY_EDITOR GameObject.DestroyImmediate(existingPart.gameObject); #else GameObject.Destroy(existingModel.gameObject); #endif } } return partsModel; } /// /// Populates the list of current UVs and UV part dictionary. /// public void PopulateUVDictionary(List usedParts) { _currentUVList = new List(); _currentUVDictionary = new Dictionary>(); foreach (ColorPartType type in Enum.GetValues(typeof(ColorPartType))) { _currentUVDictionary.Add(type, new List()); } foreach (SkinnedMeshRenderer skinnedMesh in usedParts) { ColorPartType type = Enum.Parse(ExtractPartType(skinnedMesh.name).ToString()); List partUVs = _currentUVDictionary[type]; foreach (Vector2 uv in skinnedMesh.sharedMesh.uv) { int scaledU = (int) Math.Floor(uv.x * 16); int scaledV = (int) Math.Floor(uv.y * 16); if (scaledU == 16) { scaledU = 15; } if (scaledV == 16) { scaledV = 15; } Vector2 scaledUV = new Vector2(scaledU, scaledV); // For the global UV list, we don't want any duplicates on a global level if (!_currentUVList.Contains(scaledUV)) { _currentUVList.Add(scaledUV); } // For the part specific UV list we may have UVs that are in the global list already, we don't want to exclude these, so check // them separately to the global list if (!partUVs.Contains(scaledUV)) { partUVs.Add(scaledUV); } } _currentUVDictionary[type] = partUVs; } } /// /// Updates the blend shape values of the combined model. /// public void UpdateBlendShapes(GameObject model) { if (model == null) { return; } List allMeshes = model.GetComponentsInChildren().ToList(); foreach (SkinnedMeshRenderer skinnedMesh in allMeshes) { Mesh sharedMesh = skinnedMesh.sharedMesh; for (int i = 0; i < sharedMesh.blendShapeCount; i++) { string blendName = sharedMesh.GetBlendShapeName(i); if (blendName.Contains(_BLEND_GENDER_NAME)) { skinnedMesh.SetBlendShapeWeight(i, (_bodyTypeBlendValue + 100) / 2); } else if (blendName.Contains(_BLEND_SHAPE_SKINNY_NAME)) { skinnedMesh.SetBlendShapeWeight(i, _bodySizeSkinnyBlendValue); } else if (blendName.Contains(_BLEND_SHAPE_HEAVY_NAME)) { skinnedMesh.SetBlendShapeWeight(i, _bodySizeHeavyBlendValue); } else if (blendName.Contains(_BLEND_MUSCLE_NAME)) { skinnedMesh.SetBlendShapeWeight(i, (_musclesBlendValue + 100) / 2); } } } } /// /// Populates the internal library of parts based on the files in the project. /// public Dictionary> PopulatePartLibrary() { _partLibrary = new Dictionary>(); _partOutfitMap = new Dictionary>(); _partOutfitToggleMap = new Dictionary(); _partCount = 0; List files = Directory.GetFiles("Assets", "SK_*_*_*_*_*.fbx", SearchOption.AllDirectories).ToList(); foreach (CharacterPartType partType in Enum.GetValues(typeof(CharacterPartType))) { Dictionary partLocationDictionary = new Dictionary(); foreach (string file in files) { FileInfo fileInfo = new FileInfo(file); string partName = fileInfo.Name; partName = partName.Substring(0, partName.IndexOf(".fbx", StringComparison.Ordinal)); CharacterPartType characterPartType = ExtractPartType(partName); if (characterPartType > 0 && characterPartType == partType && !partLocationDictionary.ContainsKey(partName)) { partLocationDictionary.Add(partName, file); _partCount++; // TODO: populate with actual outfit data when we have proper information about part outfits string tempPartOutfit = GetOutfitNameFromPartName(partName); List partNameList; if (_partOutfitMap.TryGetValue(tempPartOutfit, out List value)) { partNameList = value; partNameList.Add(partName); _partOutfitMap[tempPartOutfit] = partNameList; } else { partNameList = new List(); partNameList.Add(partName); _partOutfitMap.Add(tempPartOutfit, partNameList); _partOutfitToggleMap.Add(tempPartOutfit, true); } } } _partLibrary.Add(partType, partLocationDictionary); } return _partLibrary; } /// /// Populates the internal library of Presets into libraries based on filters and base parts. /// public Task PopulatePresetLibrary() { HashSet uniqueList = new HashSet(); _mappedPresetFilterDictionary = new Dictionary>(); _mappedBasePresetDictionary = new Dictionary>(); foreach (SidekickPresetFilter filter in SidekickPresetFilter.GetAll(_dbManager)) { List presets = filter.GetAllPresetsForFilter(_dbManager, true, true); _mappedPresetFilterDictionary[filter.Term] = presets; uniqueList.UnionWith(presets); } // Check for and add BASE only presets List allPresets = SidekickPartPreset.GetAll(_dbManager); List presetsNotInFilters = allPresets.Where(preset => !uniqueList.Contains(preset)).ToList(); foreach (SidekickPartPreset preset in presetsNotInFilters) { if (preset.HasOnlyBasePartsAndAllAvailable(_dbManager)) { if (_mappedBasePresetDictionary.TryGetValue(preset.Species, out List mappedPresets)) { mappedPresets.Add(preset); _mappedBasePresetDictionary[preset.Species] = mappedPresets; } else { List presets = new List { preset }; _mappedBasePresetDictionary.Add(preset.Species, presets); } } } return Task.CompletedTask; } /// /// Populates the internal library of parts based on the files in the project. /// public Task LoadPartLibrary() { _allPartsLibrary = new Dictionary>(); _mappedPartList = new Dictionary>(); _mappedPartDictionary = new Dictionary>(); _mappedBasePartDictionary = new Dictionary>>(); _speciesDictionary = new Dictionary(); _partCount = 0; List files = Directory.GetFiles("Assets", "SK_*_*_*_*_*.fbx", SearchOption.AllDirectories).ToList(); Dictionary filesOnDisk = new Dictionary(); foreach (string file in files) { FileInfo fileInfo = new FileInfo(file); string partName = fileInfo.Name; partName = partName.Substring(0, partName.IndexOf(".fbx", StringComparison.Ordinal)); filesOnDisk.TryAdd(partName, file); } foreach (CharacterPartType type in Enum.GetValues(typeof(CharacterPartType))) { _allPartsLibrary[type] = new List(); _mappedPartDictionary[type] = new Dictionary(); _mappedPartList[type] = new List(); } SidekickSpecies unrestrictedSpecies = null; foreach (SidekickSpecies species in SidekickSpecies.GetAll(_dbManager, false)) { _speciesDictionary[species.Name] = species; _mappedBasePartDictionary[species] = new Dictionary>(); if (species.Name == "Unrestricted") { unrestrictedSpecies = species; } } List allParts = SidekickPart.GetAll(_dbManager); foreach (SidekickPart part in allParts) { if (filesOnDisk.ContainsKey(part.Name)) { _partCount++; part.FileExists = true; List parts = _allPartsLibrary.TryGetValue(part.Type, out List value) ? value : new List(); parts.Add(part); _allPartsLibrary[part.Type] = parts; Dictionary partMap = _mappedPartDictionary[part.Type]; partMap[part.Name] = part; _mappedPartDictionary[part.Type] = partMap; List currentList = _mappedPartList[part.Type]; currentList.Add(part.Name); _mappedPartList[part.Type] = currentList; if (part.Name.Contains("_BASE_")) { if (!_mappedBasePartDictionary.ContainsKey(part.Species)) { continue; } Dictionary> basePartMap = _mappedBasePartDictionary[part.Species]; List partList = basePartMap.TryGetValue(part.Type, out List existingList) ? existingList : new List(); partList.Add(part.Name); basePartMap[part.Type] = partList; _mappedBasePartDictionary[part.Species] = basePartMap; if (unrestrictedSpecies != null) { Dictionary> unrestrictedBasePartMap = _mappedBasePartDictionary[unrestrictedSpecies]; List unrestrictedPartList = unrestrictedBasePartMap.TryGetValue(part.Type, out List unrestrictedList) ? unrestrictedList : new List(); unrestrictedPartList.Add(part.Name); unrestrictedBasePartMap[part.Type] = unrestrictedPartList; _mappedBasePartDictionary[unrestrictedSpecies] = unrestrictedBasePartMap; } } } else { part.FileExists = false; } } SidekickPart.UpdateAll(_dbManager, allParts); return Task.CompletedTask; } /// /// Gets the "outfit" name from the part name. /// TODO: This will be replaced once parts and outfits have a proper relationship. /// /// The part name to parse the "outfit" name from. /// The "outfit" name. public string GetOutfitNameFromPartName(string partName) { if (string.IsNullOrEmpty(partName)) { return "None"; } return string.Join('_', partName.Substring(3).Split('_').Take(2)); } /// /// Determines the part type from the part name. This will work as long as the naming format is correct. /// /// The name of the part. /// The part type. public CharacterPartType ExtractPartType(string partName) { string partTypeString = ExtractPartTypeString(partName); string partIndexString = "0"; if (partTypeString.Length > 2) { partIndexString = partTypeString.Substring(0, 2); } bool valueParsed = int.TryParse(partIndexString, out int index); return valueParsed ? (CharacterPartType) index : 0; } /// /// Extracts the part type string from the file name. /// /// The name of the part. /// The part type string public string ExtractPartTypeString(string partName) { return partName.Split('_').Reverse().ElementAt(1); } /// /// Processes the movement of rig joints based on blend shape changes. /// public void ProcessRigMovementOnBlendShapeChange(Dictionary> offsetLibrary) { Transform modelRootBone = _baseModel.transform.Find("root"); Hashtable boneNameMap = Combiner.CreateBoneNameMap(modelRootBone.gameObject); _blendShapeRigMovement = new Dictionary(); _blendShapeRigRotation = new Dictionary(); foreach (KeyValuePair entry in BlendshapeJointAdjustment.PART_TYPE_JOINT_MAP) { Transform bone = (Transform) boneNameMap[entry.Value]; float feminineBlendValue = (_bodyTypeBlendValue + 100) / 2 / 100; Vector3 allMovement = bone.localPosition; Quaternion allRotation = bone.localRotation; if (offsetLibrary.TryGetValue(entry.Key, out Dictionary blendOffsetLibrary)) { foreach (BlendShapeType blendType in Enum.GetValues(typeof(BlendShapeType))) { if (blendOffsetLibrary.TryGetValue(blendType, out SidekickBlendShapeRigMovement rigMovement)) { if (rigMovement == null) { continue; } switch (blendType) { case BlendShapeType.Feminine: allMovement += rigMovement.GetBlendedOffsetValue(feminineBlendValue); allRotation *= rigMovement.GetBlendedRotationValue(feminineBlendValue); break; case BlendShapeType.Heavy: allMovement += rigMovement.GetBlendedOffsetValue(_bodySizeHeavyBlendValue / 100); allRotation *= rigMovement.GetBlendedRotationValue(_bodySizeHeavyBlendValue / 100); break; case BlendShapeType.Skinny: allMovement += rigMovement.GetBlendedOffsetValue(_bodySizeSkinnyBlendValue / 100); allRotation *= rigMovement.GetBlendedRotationValue(_bodySizeSkinnyBlendValue / 100); break; case BlendShapeType.Bulk: allMovement += rigMovement.GetBlendedOffsetValue((_musclesBlendValue + 100) / 2 / 100); allRotation *= rigMovement.GetBlendedRotationValue((_musclesBlendValue + 100) / 2 / 100); break; } } } } _blendShapeRigMovement[entry.Value] = allMovement; _blendShapeRigRotation[entry.Value] = allRotation; } } /// /// Processes the movement of the rig with regards to the current blend shape settings. /// /// The model to process the movement on. public void ProcessBoneMovement(GameObject model) { if (model == null) { return; } Transform modelRootBone = model.transform.Find("root"); Hashtable boneNameMap = Combiner.CreateBoneNameMap(modelRootBone.gameObject); Combiner.ProcessBoneMovement(boneNameMap, _blendShapeRigMovement, _blendShapeRigRotation); } /// /// Updates the texture on the given color row for the specified color type. /// /// The color type to update. /// The color row to get the updated color from. public void UpdateColor(ColorType colorType, SidekickColorRow colorRow) { if (colorRow == null) { return; } if (_currentMaterial == null) { return; } switch (colorType) { case ColorType.Metallic: Texture2D metallic = (Texture2D) _currentMaterial.GetTexture(_METALLIC_MAP); UpdateTexture(metallic, colorRow.NiceMetallic, colorRow.ColorProperty.U, colorRow.ColorProperty.V); _currentMaterial.SetTexture(_METALLIC_MAP, metallic); break; case ColorType.Smoothness: Texture2D smoothness = (Texture2D) _currentMaterial.GetTexture(_SMOOTHNESS_MAP); UpdateTexture(smoothness, colorRow.NiceSmoothness, colorRow.ColorProperty.U, colorRow.ColorProperty.V); _currentMaterial.SetTexture(_SMOOTHNESS_MAP, smoothness); break; case ColorType.Reflection: Texture2D reflection = (Texture2D) _currentMaterial.GetTexture(_REFLECTION_MAP); UpdateTexture(reflection, colorRow.NiceReflection, colorRow.ColorProperty.U, colorRow.ColorProperty.V); _currentMaterial.SetTexture(_REFLECTION_MAP, reflection); break; case ColorType.Emission: Texture2D emission = (Texture2D) _currentMaterial.GetTexture(_EMISSION_MAP); UpdateTexture(emission, colorRow.NiceEmission, colorRow.ColorProperty.U, colorRow.ColorProperty.V); _currentMaterial.SetTexture(_EMISSION_MAP, emission); break; case ColorType.Opacity: Texture2D opacity = (Texture2D) _currentMaterial.GetTexture(_OPACITY_MAP); UpdateTexture(opacity, colorRow.NiceOpacity, colorRow.ColorProperty.U, colorRow.ColorProperty.V); _currentMaterial.SetTexture(_OPACITY_MAP, opacity); break; case ColorType.MainColor: default: Texture2D color = (Texture2D) _currentMaterial.GetTexture(_COLOR_MAP); UpdateTexture(color, colorRow.NiceColor, colorRow.ColorProperty.U, colorRow.ColorProperty.V); _currentMaterial.SetTexture(_COLOR_MAP, color); break; } } /// /// Updates the color on the texture with the given new color. /// /// The texture to update. /// The color to assign to the texture. /// The u positioning on the texture to update. /// The v positioning on the texture to update. public void UpdateTexture(Texture2D texture, Color newColor, int u, int v) { int scaledU = u * 2; int scaledV = v * 2; texture.SetPixel(scaledU, scaledV, newColor); texture.SetPixel(scaledU + 1, scaledV, newColor); texture.SetPixel(scaledU, scaledV + 1, newColor); texture.SetPixel(scaledU + 1, scaledV + 1, newColor); texture.Apply(); } } }