// 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. #if UNITY_EDITOR using Synty.SidekickCharacters.API; using Synty.SidekickCharacters.Database; using Synty.SidekickCharacters.Database.DTO; using Synty.SidekickCharacters.Enums; using Synty.SidekickCharacters.Filters; using Synty.SidekickCharacters.Serialization; using Synty.SidekickCharacters.SkinnedMesh; using Synty.SidekickCharacters.Synty.SidekickCharacters.Scripts.Editor.UI; using Synty.SidekickCharacters.Utils; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using Unity.VisualScripting.YamlDotNet.Serialization; using UnityEditor; using UnityEditor.Animations; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; using Random = UnityEngine.Random; namespace Synty.SidekickCharacters { public class ModularCharacterWindow : EditorWindow { private const string _AUTOSAVE_KEY = "SK_Autosave_character"; private const string _AUTOSAVE_MISSING_PARTS = "SK_Autosave_missing_parts"; private const string _AUTOSAVE_STATE = "SK_Autosave_state"; private const string _BASE_COLOR_SET_NAME = "Species"; private const string _BASE_COLOR_SET_PATH = "Assets/External/Models/SidekickCharacters/Resources/Species"; private const string _BASE_MESH_NAME = "Meshes/SK_BaseModel"; private const string _BASE_MATERIAL_NAME = "Materials/M_BaseMaterial"; 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 _AUTO_OPEN_STATE = "syntySkAutoOpenState"; private const string _OUTPUT_MODEL_NAME = "Combined Character"; private const string _PART_COUNT_BODY = " parts in library"; 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 static Queue _callbackQueue = new Queue(); private static bool _openWindowOnStart = true; private readonly List _visibleColorRows = new List(); private List _allColorRows = new List(); private List _allParts; private List _allSpecies; private ObjectField _animationField; private FilterGroup _appliedPartFilters; private bool _applyingPreset = false; private List _availablePartList; List _availablePresets; private bool _bakeBlends = true; private ObjectField _baseModelField; private Dictionary _blendShapeRigMovement = new Dictionary(); private Dictionary _blendShapeRigRotation = new Dictionary(); private ToolbarToggle _bodyPartsTab; private ToolbarToggle _bodyPresetTab; private ToolbarToggle _bodyShapeTab; private ScrollView _bodyShapeView; private float _bodySizeHeavyBlendValue; private float _bodySizeSkinnyBlendValue; private Slider _bodySizeSlider; private Slider _bodyTypeSlider; private VisualElement _colorSelectionRowView; private ToolbarToggle _colorSelectionTab; private ScrollView _colorSelectionView; private DropdownField _colorSetsDropdown; private bool _combineMeshes = true; private AnimatorController _currentAnimationController; private Dictionary _currentBodyPresetDictionary = new Dictionary(); private Dictionary _currentCharacter = new Dictionary(); private Dictionary _currentColorSpeciesPresetDictionary = new Dictionary(); private Dictionary _currentColorOutfitsPresetDictionary = new Dictionary(); private Dictionary _currentColorAttachmentsPresetDictionary = new Dictionary(); private Dictionary _currentColorMaterialsPresetDictionary = new Dictionary(); private Dictionary _currentColorElementsPresetDictionary = new Dictionary(); private SidekickColorSet _currentColorSet; private bool _currentGlobalLockStatus; private Material _currentMaterial; private Dictionary _previousPartSelections; private ColorPartType _currentPartType; private Dictionary _currentHeadPresetDictionary = new Dictionary(); private Dictionary _currentUpperBodyPresetDictionary = new Dictionary(); private Dictionary _currentLowerBodyPresetDictionary = new Dictionary(); private SidekickSpecies _currentSpecies; private TabView _currentTab; private Dictionary> _currentUVDictionary = new Dictionary>(); private List _currentUVList = new List(); private DatabaseManager _dbManager; private string _dbPath; private ToolbarToggle _decalSelectionTab; private ScrollView _decalSelectionView; private StyleSheet _editorStyle; private bool _showAllColourProperties = false; private float _bodyTypeBlendValue; private bool _loadingCharacter = false; private bool _loadingContent = false; private Image _loadingImage; private ObjectField _materialField; private float _musclesBlendValue; private Slider _musclesSlider; private GameObject _newModel; private VisualElement _newSetNameContainer; private ScrollView _optionSelectionView; private ToolbarToggle _optionTab; private Label _partCountLabel; private Dictionary _partDictionary; private Dictionary> _partLibrary; private Dictionary> _allPartsLibrary; private Dictionary> _partOutfitMap; private Dictionary, bool> _partLockMap; private Dictionary _partSelectionDictionary; private Foldout _partsFoldout; private Dictionary> _partSpeciesMap; private Dictionary _partPresetFilterToggleMap = new Dictionary(); private ScrollView _partView; private Dictionary _presetDefaultValues = new Dictionary(); private VisualElement _presetPartContainer; private ScrollView _presetView; private Toggle _previewToggle; private bool _processingSpeciesChange = false; private bool _requiresWrap = false; private VisualElement _root; private bool _showMissingPartsPopup = false; private SidekickRuntime _sidekickRuntime; private DropdownField _speciesField; private DropdownField _speciesPresetField; private PlayModeStateChange _stateChange; private bool _useAutoSaveAndLoad = false; private List _visibleColorSets = new List(); // Animation variables private Animator _currentAnimator; private string _defaultStateName = "Idle"; private float _defaultClipDurationFallback = 2f; private float _blendDuration = 0.5f; // seconds private List _otherStates = new List(); private string _currentState; private float _playbackTime; private float _stateDuration = 1f; private int _loopsRemaining = 0; private bool _playingDefault = true; private double _lastEditorTime; private Transform[] _animatorBones; private Dictionary _previousPosePos = new Dictionary(); private Dictionary _previousPoseRot = new Dictionary(); private float _blendElapsedTime = 0f; private bool _blending = false; /// private void Awake() { InitializeEditorWindow(); } /// private void OnDestroy() { if (_useAutoSaveAndLoad) { SerializedCharacter savedCharacter = CreateSerializedCharacter(_OUTPUT_MODEL_NAME); Serializer serializer = new Serializer(); string serializedCharacter = serializer.Serialize(savedCharacter); EditorPrefs.SetString(_AUTOSAVE_KEY, serializedCharacter); } // ensures we release the file lock on the database _dbManager?.CloseConnection(); } #if UNITY_EDITOR /// private void OnEnable() { EditorApplication.playModeStateChanged += StateChange; EditorApplication.update += AnimationUpdate; } private void OnDisable() { EditorApplication.update -= AnimationUpdate; } /// private void Update() { if (_loadingContent && _loadingImage != null) { Vector3 rotation = _loadingImage.transform.rotation.eulerAngles; rotation += new Vector3(0, 0, 0.5f * Time.deltaTime); _loadingImage.transform.rotation = Quaternion.Euler(rotation); } while (_callbackQueue.Count > 0) { Action action = null; lock (_callbackQueue) { if (_callbackQueue.Count > 0) { action = _callbackQueue.Dequeue(); } } action?.Invoke(); } } /// /// Sets up the animation controllers for playing during runtime. /// /// True if it is set up; otherwise false. private bool SetupAnimationControllers() { _loopsRemaining = 0; _currentAnimator = _newModel.GetComponent(); if (_currentAnimator != null) { _currentAnimationController = _currentAnimator.runtimeAnimatorController as AnimatorController; _sidekickRuntime.CurrentAnimationController = _currentAnimationController; } else { return false; } _animatorBones = _currentAnimator.GetComponentsInChildren(); CollectOtherStates(); if (string.IsNullOrEmpty(_currentState)) { StartDefaultState(); } else { SetState(_currentState); } _lastEditorTime = EditorApplication.timeSinceStartup; return true; } /// /// Plays the animation during edit time. /// private void AnimationUpdate() { if (Application.isPlaying) { return; } if (_animationField == null || _animationField.value == null) { return; } if (_currentAnimator == null || _currentAnimationController == null) { if (!SetupAnimationControllers()) { return; } } double currentTime = EditorApplication.timeSinceStartup; float deltaTime = (float)(currentTime - _lastEditorTime); _lastEditorTime = currentTime; _playbackTime += deltaTime; float normalizedTime = (_playbackTime % _stateDuration) / _stateDuration; _currentAnimator.Play(_currentState, 0, normalizedTime); _currentAnimator.Update(0f); // Blend from previous pose (if blending) if (_blending) { _blendElapsedTime += deltaTime; float t = Mathf.Clamp01(_blendElapsedTime / _blendDuration); foreach (var bone in _animatorBones) { if (bone == null) { continue; } if (_previousPosePos.TryGetValue(bone, out Vector3 prevPos)) { bone.localPosition = Vector3.Lerp(prevPos, bone.localPosition, t); } if (_previousPoseRot.TryGetValue(bone, out Quaternion prevRot)) { bone.localRotation = Quaternion.Slerp(prevRot, bone.localRotation, t); } } if (t >= +_blendDuration) { _blending = false; } } SceneView.RepaintAll(); if (_playbackTime >= _stateDuration) { _playbackTime = 0f; _loopsRemaining--; if (_loopsRemaining <= 0) { if (!_playingDefault) { StartDefaultState(); } else { _loopsRemaining = Int32.MaxValue; } } } } /// /// Collect other states from the animation contoller. /// private void CollectOtherStates() { _otherStates.Clear(); if (_currentAnimationController == null) { return; } foreach (var layer in _currentAnimationController.layers) { foreach (var state in layer.stateMachine.states) { string stateName = state.state.name; if (stateName != _defaultStateName) { _otherStates.Add(stateName); } } } } /// /// /// private void StartDefaultState() { SetState(_defaultStateName); } /// /// Sets the state of the animation controller. /// /// The name of the state to set. private void SetState(string stateName) { if (_currentAnimationController == null || _currentAnimator == null) { return; } CacheCurrentPose(); _currentState = stateName; _playbackTime = 0f; _blendElapsedTime = 0f; _blending = true; _loopsRemaining = stateName == _defaultStateName ? Int32.MaxValue : 1; _playingDefault = stateName == _defaultStateName; var sm = _currentAnimationController.layers[0].stateMachine; foreach (var state in sm.states) { if (state.state.name == stateName && state.state.motion is AnimationClip clip) { _stateDuration = Mathf.Max(clip.length, 0.01f); goto Found; } } _stateDuration = _defaultClipDurationFallback; Found: _currentAnimator.Play(stateName, 0, 0f); _currentAnimator.Update(0f); } /// /// Caches the current animation pose /// private void CacheCurrentPose() { _previousPosePos.Clear(); _previousPoseRot.Clear(); foreach (var bone in _animatorBones) { if (bone == null) continue; _previousPosePos[bone] = bone.localPosition; _previousPoseRot[bone] = bone.localRotation; } } /// /// Processes the callback from the play mode state change. /// /// The current PlayModeStateChange private void StateChange(PlayModeStateChange stateChange) { if (_useAutoSaveAndLoad && stateChange == PlayModeStateChange.ExitingEditMode || _useAutoSaveAndLoad && stateChange == PlayModeStateChange.ExitingPlayMode) { SerializedCharacter savedCharacter = CreateSerializedCharacter(_OUTPUT_MODEL_NAME); Serializer serializer = new Serializer(); string serializedCharacter = serializer.Serialize(savedCharacter); EditorPrefs.SetString(_AUTOSAVE_KEY, serializedCharacter); } if (_useAutoSaveAndLoad && stateChange == PlayModeStateChange.EnteredEditMode || _useAutoSaveAndLoad && stateChange == PlayModeStateChange.EnteredPlayMode) { _stateChange = stateChange; } } #endif /// public async void CreateGUI() { _loadingContent = true; InitializeEditorWindow(); _root = rootVisualElement; _root.Clear(); if (_editorStyle != null) { _root.styleSheets.Add(_editorStyle); } InitializeDatabase(); // if we still can't connect, something's gone wrong, don't keep building the GUI if (_dbManager?.GetCurrentDbConnection() == null) { _loadingContent = false; return; } // Maintains a linking to the model if the editor window is closed and re-opened. _newModel = GameObject.Find(_OUTPUT_MODEL_NAME); _previousPartSelections = new Dictionary(); foreach (CharacterPartType type in Enum.GetValues(typeof(CharacterPartType))) { _previousPartSelections.Add(type, "None"); } _partCountLabel = new Label("") { style = { unityTextAlign = TextAnchor.MiddleLeft } }; Image bannerImage = new Image { image = (Texture2D) Resources.Load("UI/T_SidekickTitle"), scaleMode = ScaleMode.ScaleToFit, }; VisualElement bannerLayout = new VisualElement { style = { backgroundColor = new Color(209f/256, 34f/256, 51f/256), minHeight = 150, paddingBottom = 5, paddingTop = 5, } }; bannerLayout.Add(bannerImage); _root.Add(bannerLayout); _presetView = new ScrollView(ScrollViewMode.Vertical); _partView = new ScrollView(ScrollViewMode.Vertical) { style = { display = DisplayStyle.None } }; _bodyShapeView = new ScrollView(ScrollViewMode.Vertical) { style = { display = DisplayStyle.None } }; _colorSelectionView = new ScrollView(ScrollViewMode.Vertical) { style = { display = DisplayStyle.None } }; // TODO: Uncomment when Decals added to the system. // _decalSelectionView = new ScrollView(ScrollViewMode.Vertical) // { // style = // { // display = DisplayStyle.None // } // }; _optionSelectionView = new ScrollView(ScrollViewMode.Vertical) { style = { display = DisplayStyle.None } }; // TODO: Replace this tabbed menu code with TabView when 2023 LTS in the minimum supported version. Toolbar tabBar = new Toolbar { style = { width = Length.Percent(100) } }; _bodyPresetTab = new ToolbarToggle { text = "Presets", tooltip = "Create a character using preset combinations of parts, body types and colors" }; _bodyPartsTab = new ToolbarToggle { text = "Parts", tooltip = "Edit individual character parts on your character" }; _bodyShapeTab = new ToolbarToggle { text = "Body", tooltip = "Edit the body type, size, musculature of your character" }; _colorSelectionTab = new ToolbarToggle { text = "Colors", tooltip = "Edit individual colors of your character" }; // TODO: Uncomment when Decals added to the system. // _decalSelectionTab = new ToolbarToggle // { // text = "Decals", // tooltip = "Edit the decals applied to your character" // }; _optionTab = new ToolbarToggle { text = "Options", tooltip = "Change the options of the tool" }; tabBar.Add(_bodyPresetTab); tabBar.Add(_bodyPartsTab); tabBar.Add(_bodyShapeTab); tabBar.Add(_colorSelectionTab); // TODO: Uncomment when Decals added to the system. // tabBar.Add(_decalSelectionTab); tabBar.Add(_optionTab); _root.Add(tabBar); _bodyPresetTab.RegisterValueChangedCallback( delegate { if (_currentTab != TabView.Preset && _bodyPresetTab.value) { SwitchToTab(TabView.Preset); } } ); _bodyPartsTab.RegisterValueChangedCallback( delegate { if (_currentTab != TabView.Parts && _bodyPartsTab.value) { SwitchToTab(TabView.Parts); } } ); _bodyShapeTab.RegisterValueChangedCallback( delegate { if (_currentTab != TabView.Body && _bodyShapeTab.value) { AddBodyShapeTabContent(_bodyShapeView); SwitchToTab(TabView.Body); } } ); _colorSelectionTab.RegisterValueChangedCallback( delegate { if (_currentTab != TabView.Colors && _colorSelectionTab.value) { // always re-populate the color rows with the latest when switching tabs if (_allColorRows.Count == 0) { PopulateColorRowsFromTextures(); } else { PopulatePartColorRows(); RefreshVisibleColorRows(); } SwitchToTab(TabView.Colors); } } ); // TODO: Uncomment when Decals added to the system. // _decalSelectionTab.RegisterValueChangedCallback( // delegate // { // if (_currentTab != TabView.Decals && _decalSelectionTab.value) // { // SwitchToTab(TabView.Decals); // } // } // ); _optionTab.RegisterValueChangedCallback( delegate { if (_currentTab != TabView.Options && _optionTab.value) { SwitchToTab(TabView.Options); } // If currently on this tab, and button is toggled to "off", toggle back to "on" else if (_currentTab == TabView.Options && !_optionTab.value) { _optionTab.value = true; } } ); _root.Add(_presetView); _root.Add(_partView); _root.Add(_bodyShapeView); _root.Add(_colorSelectionView); // TODO: Uncomment when Decals added to the system. // root.Add(_decalSelectionView); _root.Add(_optionSelectionView); AddOptionsTabContent(_optionSelectionView); // set the default colour set (this will change on various input triggers, but we need it to not be null) _currentColorSet = SidekickColorSet.GetDefault(_dbManager); _partDictionary = new Dictionary(); _currentCharacter = new Dictionary(); _sidekickRuntime = new SidekickRuntime((GameObject) _baseModelField.value, (Material) _materialField.value, _currentAnimationController, _dbManager); Label loadingLabel = new Label { text = "Loading Content..", style = { fontSize = 20, unityTextAlign = new StyleEnum(TextAnchor.MiddleCenter), marginTop = 20 } }; _presetView.Add(loadingLabel); _loadingImage = new Image { image = (Texture2D) Resources.Load("UI/T_LoadingCircle"), scaleMode = ScaleMode.ScaleToFit, style = { width = 28, height = 28, unityTextAlign = new StyleEnum(TextAnchor.MiddleCenter), alignContent = new StyleEnum(Align.Center), alignSelf = new StyleEnum(Align.Center) } }; _presetView.Add(_loadingImage); _bodyPresetTab.value = true; SwitchToTab(TabView.Preset); VisualElement saveLoadButtons = new VisualElement { style = { flexDirection = new StyleEnum(FlexDirection.Row), width = Length.Percent(100), alignContent = new StyleEnum(Align.FlexStart), alignItems = new StyleEnum(Align.FlexStart), flexWrap = new StyleEnum(Wrap.Wrap), justifyContent = new StyleEnum(Justify.SpaceBetween), minHeight = 30, marginTop = 20 } }; _root.Add(saveLoadButtons); Button loadCharacterButton = new Button(LoadCharacter) { text = "Load Character", style = { minHeight = 30, width = Length.Percent(48) } }; saveLoadButtons.Add(loadCharacterButton); Button saveCharacterButton = new Button(SaveCharacter) { text = "Save Character", style = { minHeight = 30, width = Length.Percent(48) } }; saveLoadButtons.Add(saveCharacterButton); Button createCharacterButton = new Button(CreateCharacterPrefab) { text = "Export Character as Prefab", style = { minHeight = 50, marginTop = 5 } }; _root.Add(createCharacterButton); // Populate the data in an async process, if not already populated try { await Task.Run( async () => { await SidekickRuntime.PopulateToolData(_sidekickRuntime); _callbackQueue.Enqueue(AddAllTabContent); if (_useAutoSaveAndLoad && _stateChange == PlayModeStateChange.EnteredEditMode || _useAutoSaveAndLoad && _stateChange == PlayModeStateChange.EnteredPlayMode) { _callbackQueue.Enqueue(ReloadCharacterFromStateChange); } } ); } catch { Debug.LogWarning("Failed to load tool data. Please try again.\nPlease note that data loading may take some time."); } } /// /// Reloads the character based on auto saved character /// private void ReloadCharacterFromStateChange() { if (_stateChange == PlayModeStateChange.EnteredEditMode) { GameObject existingModel = GameObject.Find(_OUTPUT_MODEL_NAME); while (existingModel != null) { GameObject.DestroyImmediate(existingModel); existingModel = GameObject.Find(_OUTPUT_MODEL_NAME); } } string serializedCharacterString = EditorPrefs.GetString(_AUTOSAVE_KEY, null); if (!string.IsNullOrEmpty(serializedCharacterString)) { Deserializer deserializer = new Deserializer(); SerializedCharacter serializedCharacter = deserializer.Deserialize(serializedCharacterString); LoadSerializedCharacter(serializedCharacter, _showAllColourProperties); } _stateChange = PlayModeStateChange.ExitingEditMode; } /// /// Adds the UI content to all tabs (excluding Options tab as it is already populated) /// private void AddAllTabContent() { if (_sidekickRuntime.PartCount < 15) { if (EditorUtility.DisplayDialog( "Unable to strat Sidekicks Tool", "There are not enough Sidekicks parts currently in your project. The tool is unable to start without the requisite parts", "Ok" )) { Close(); } } else { _allPartsLibrary = _sidekickRuntime.AllPartsLibrary; _allParts = SidekickPart.GetAll(_dbManager); _allSpecies = SidekickSpecies.GetAll(_dbManager); _currentSpecies = _allSpecies[0]; _sidekickRuntime.CurrentSpecies = _currentSpecies; _partCountLabel.text = _sidekickRuntime.PartCount + _PART_COUNT_BODY; _partOutfitMap = _sidekickRuntime.PartOutfitMap; AddBodyShapeTabContent(_bodyShapeView); AddColorTabContent(_colorSelectionView); // TODO: Uncomment when Decals added to the system. // _decalSelectionView.Add(new Label("Decal selections will go here.")); UpdatePartUVData(); PopulatePartUI(); PopulatePresetUI(); if (_currentSpecies == null) { _currentSpecies = _currentSpecies = _allSpecies.FirstOrDefault(species => species.Name == _speciesField.value); } if (_useAutoSaveAndLoad) { string serializedCharacterString = EditorPrefs.GetString(_AUTOSAVE_KEY, null); if (!string.IsNullOrEmpty(serializedCharacterString)) { try { Deserializer deserializer = new Deserializer(); SerializedCharacter serializedCharacter = deserializer.Deserialize(serializedCharacterString); LoadSerializedCharacter(serializedCharacter, _showAllColourProperties); } catch (Exception ex) { EditorUtility.DisplayDialog( "Something Went Wrong", "Unable to load saved character.", "OK" ); Debug.LogWarning(ex); _useAutoSaveAndLoad = false; EditorPrefs.SetBool(_AUTOSAVE_STATE, _useAutoSaveAndLoad); } } } _loadingContent = false; } } /// /// Initializes all the database setup, and provides the status label reference for users /// private void InitializeDatabase() { // TODO: always reinstantiate instead? if (_dbManager != null) { return; } _dbManager = new DatabaseManager(); _dbManager.GetDbConnection(true); } /// /// Initializes the editor window. /// private void InitializeEditorWindow() { _editorStyle = Resources.Load("Styles/EditorStyles"); _openWindowOnStart = EditorPrefs.GetBool(_AUTO_OPEN_STATE, true); _useAutoSaveAndLoad = EditorPrefs.GetBool(_AUTOSAVE_STATE, false); _showMissingPartsPopup = EditorPrefs.GetBool(_AUTOSAVE_MISSING_PARTS, false); } /// /// Adds the contents and change listeners for the body shape tab. /// /// The tabview to add the content to. private void AddBodyShapeTabContent(ScrollView view) { view.Clear(); _bodyTypeSlider = new Slider("Body Type", -100, 100) { value = _bodyTypeBlendValue, style = { maxWidth = new StyleLength(StyleKeyword.Auto) }, showInputField = true, tooltip = "Blend the body type of the character between masculine and feminine" }; VisualElement bodyTypeLabels = new VisualElement { style = { flexDirection = new StyleEnum(FlexDirection.Row), width = Length.Percent(100), paddingLeft = 155 } }; Label labelMasculine = new Label("Masculine") { style = { position = new StyleEnum(Position.Absolute), left = 155 } }; bodyTypeLabels.Add(labelMasculine); Label labelFeminine = new Label("Feminine") { style = { position = new StyleEnum(Position.Absolute), right = 58 } }; bodyTypeLabels.Add(labelFeminine); _bodySizeSlider = new Slider("Body Size", -100, 100) { value = _bodySizeSkinnyBlendValue > 0 ? -_bodySizeSkinnyBlendValue : _bodySizeHeavyBlendValue, style = { maxWidth = new StyleLength(StyleKeyword.Auto), marginTop = 30 }, showInputField = true, tooltip = "Blend the body size of the character between slim and heavy" }; VisualElement bodySizeLabels = new VisualElement { style = { flexDirection = new StyleEnum(FlexDirection.Row), width = Length.Percent(100), paddingLeft = 155 } }; Label labelSlim = new Label("Slim") { style = { position = new StyleEnum(Position.Absolute), left = 155 } }; bodySizeLabels.Add(labelSlim); Label labelHeavy = new Label("Heavy") { style = { position = new StyleEnum(Position.Absolute), right = 58 } }; bodySizeLabels.Add(labelHeavy); _musclesSlider = new Slider("Musculature", -100, 100) { value = _musclesBlendValue, style = { maxWidth = new StyleLength(StyleKeyword.Auto), marginTop = 30 }, showInputField = true, tooltip = "Blend the musculature of the character between lean and muscular" }; VisualElement muscleLabels = new VisualElement { style = { flexDirection = new StyleEnum(FlexDirection.Row), width = Length.Percent(100), height = 20, paddingLeft = 155 } }; Label labelLean = new Label("Lean") { style = { position = new StyleEnum(Position.Absolute), left = 155 } }; muscleLabels.Add(labelLean); Label labelBulk = new Label("Muscular") { style = { position = new StyleEnum(Position.Absolute), right = 58 } }; muscleLabels.Add(labelBulk); view.Add(_bodyTypeSlider); view.Add(bodyTypeLabels); view.Add(_bodySizeSlider); view.Add(bodySizeLabels); view.Add(_musclesSlider); view.Add(muscleLabels); _bodyTypeSlider.RegisterValueChangedCallback( evt => { _bodyTypeBlendValue = evt.newValue; _sidekickRuntime.BodyTypeBlendValue = evt.newValue; string body = "None"; if (_partSelectionDictionary.TryGetValue(CharacterPartType.Torso, out PartTypeControls bodySelection)) { body = bodySelection.PartDropdown.value; } if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection)) { if (body != "None" && _requiresWrap && _bodyTypeBlendValue > 0) { wrapSelection.PartDropdown.SetEnabled(true); wrapSelection.RandomisePartDropdownValue(); } else { wrapSelection.PartDropdown.SetEnabled(false); wrapSelection.SetPartDropdownValue(null); _currentCharacter.Remove(CharacterPartType.Wrap); } } if (_newModel == null) { _newModel = GenerateCharacter(false, true); UpdatePartUVData(); } _sidekickRuntime.UpdateBlendShapes(_newModel); _sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager)); _sidekickRuntime.ProcessBoneMovement(_newModel); } ); _bodySizeSlider.RegisterValueChangedCallback( evt => { float newValue = evt.newValue; if (newValue > 0) { _bodySizeHeavyBlendValue = newValue; _bodySizeSkinnyBlendValue = 0; _sidekickRuntime.BodySizeHeavyBlendValue = newValue; _sidekickRuntime.BodySizeSkinnyBlendValue = 0; } else if (newValue < 0) { _bodySizeHeavyBlendValue = 0; _bodySizeSkinnyBlendValue = -newValue; _sidekickRuntime.BodySizeHeavyBlendValue = 0; _sidekickRuntime.BodySizeSkinnyBlendValue = -newValue; } else { _bodySizeHeavyBlendValue = 0; _bodySizeSkinnyBlendValue = 0; _sidekickRuntime.BodySizeHeavyBlendValue = 0; _sidekickRuntime.BodySizeSkinnyBlendValue = 0; } if (_newModel == null) { _newModel = GenerateCharacter(false, true); UpdatePartUVData(); } _sidekickRuntime.UpdateBlendShapes(_newModel); _sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager)); _sidekickRuntime.ProcessBoneMovement(_newModel); } ); _musclesSlider.RegisterValueChangedCallback( evt => { _musclesBlendValue = evt.newValue; _sidekickRuntime.MusclesBlendValue = evt.newValue; if (_newModel == null) { _newModel = GenerateCharacter(false, true); UpdatePartUVData(); } _sidekickRuntime.UpdateBlendShapes(_newModel); _sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager)); _sidekickRuntime.ProcessBoneMovement(_newModel); } ); } /// /// Adds the content to the Color tab. /// /// The view to add the content to. private void AddColorTabContent(ScrollView view) { _colorSetsDropdown = new DropdownField { style = { maxWidth = Length.Percent(65) } }; Label filterPartsLabel = new Label("Filter - Parts") { tooltip = "Filter the displayed colors to focus on specific areas of the character and reduce the number of properties" }; view.Add(filterPartsLabel); DropdownField partTypeDropdown = new DropdownField(); string[] colorPartTypes = Enum.GetNames(typeof(ColorPartType)); // Enum names can't have spaces so we add in the space manually for display. for (int i = 0; i < colorPartTypes.Length; i++) { colorPartTypes[i] = StringUtils.AddSpacesBeforeCapitalLetters(colorPartTypes[i]); } partTypeDropdown.choices = colorPartTypes.ToList(); partTypeDropdown.value = colorPartTypes[0]; view.Add(partTypeDropdown); partTypeDropdown.RegisterValueChangedCallback( (evt) => { if (Enum.TryParse(typeof(ColorPartType), evt.newValue.Replace(" ", ""), out object newType)) { _currentPartType = (ColorPartType) newType; _colorSetsDropdown.value = "Custom"; } PopulatePartColorRows(); RefreshVisibleColorRows(); } ); _currentPartType = ColorPartType.AllParts; // TODO: Hidden due to early access, enable when feature complete // Label colorSetsLabel = new Label("Color Sets"); // view.Add(colorSetsLabel); // // VisualElement colorSetsRow = new VisualElement // { // style = // { // flexDirection = new StyleEnum(FlexDirection.Row), // width = Length.Percent(100), // alignContent = new StyleEnum(Align.FlexStart), // alignItems = new StyleEnum(Align.FlexStart), // flexWrap = new StyleEnum(Wrap.Wrap), // justifyContent = new StyleEnum(Justify.SpaceBetween), // marginBottom = 10 // } // }; UpdateVisibleColorSets(); // TODO: Hidden due to early access, enable when feature complete // colorSetsRow.Add(_colorSetsDropdown); // // _colorSetsDropdown.RegisterValueChangedCallback( // evt => // { // if (evt.newValue != "Custom") // { // SidekickColorSet newSet = _visibleColorSets.First(set => set.Name == evt.newValue); // List newRows = SidekickColorRow.GetAllBySet(_dbManager, newSet); // if (_currentPartType == ColorPartType.AllParts && _allColorRows.All(row => row.IsLocked == false)) // { // _currentColorSet = newSet; // _allColorRows = newRows; // if (_allColorRows.Count == 0) // { // PopulateColorRowsFromTextures(); // } // else // { // PopulatePartColorRows(); // RefreshVisibleColorRows(); // } // } // else // { // foreach (SidekickColorRow row in _visibleColorRows) // { // SidekickColorRow newRow = newRows.FirstOrDefault(r => r.ColorProperty.ID == row.ColorProperty.ID); // if (newRow != null) // { // row.NiceColor = newRow.NiceColor; // row.NiceMetallic = newRow.NiceMetallic; // row.NiceSmoothness = newRow.NiceSmoothness; // row.NiceReflection = newRow.NiceReflection; // row.NiceEmission = newRow.NiceEmission; // row.NiceOpacity = newRow.NiceOpacity; // } // } // // RefreshVisibleColorRows(); // } // // UpdateAllVisibleColors(); // } // } // ); // // Button previousSetButton = new Button( // () => // { // if (_colorSetsDropdown.index > 0) // { // _colorSetsDropdown.index -= 1; // } // } // ) // { // tooltip = "Previous Color Set" // }; // // previousSetButton.Add( // new Image // { // image = EditorGUIUtility.IconContent("tab_prev", "|Previous Color Set").image, // scaleMode = ScaleMode.ScaleToFit // } // ); // // colorSetsRow.Add(previousSetButton); // Button nextSetButton = new Button( // () => // { // if (_colorSetsDropdown.index < _colorSetsDropdown.choices.Count - 1) // { // _colorSetsDropdown.index += 1; // } // } // ) // { // tooltip = "Next Color Set" // }; // // nextSetButton.Add( // new Image // { // image = EditorGUIUtility.IconContent("tab_next", "|Next Color Set").image, // scaleMode = ScaleMode.ScaleToFit // } // ); // // colorSetsRow.Add(nextSetButton); // Button resetSetButton = new Button(ResetColorSet) // { // tooltip = "Reset Color Set from Disk" // }; // resetSetButton.Add( // new Image // { // image = EditorGUIUtility.IconContent("Refresh", "|Reset Color Set From Disk").image, // scaleMode = ScaleMode.ScaleToFit // } // ); // // colorSetsRow.Add(resetSetButton); // Button newSetButton = new Button(ShowCreateNewColorSet) // { // tooltip = "Create New Color Set" // }; // newSetButton.Add( // new Image // { // image = EditorGUIUtility.IconContent("Toolbar Plus", "|Create New Color Set").image, // scaleMode = ScaleMode.ScaleToFit // } // ); // // colorSetsRow.Add(newSetButton); // Button deleteSetButton = new Button(DeleteColorSet) // { // tooltip = "Delete Color Set" // }; // deleteSetButton.Add( // new Image // { // image = EditorGUIUtility.IconContent("close", "|Delete Color Set").image, // scaleMode = ScaleMode.ScaleToFit // } // ); // // colorSetsRow.Add(deleteSetButton); // Button saveSetButton = new Button(SaveColorSet) // { // tooltip = "Save Color Set" // }; // saveSetButton.Add( // new Image // { // image = EditorGUIUtility.IconContent("SaveAs", "|Save Color Set").image, // scaleMode = ScaleMode.ScaleToFit // } // ); // // colorSetsRow.Add(saveSetButton); // view.Add(colorSetsRow); // // _newSetNameContainer = new VisualElement // { // style = // { // flexDirection = new StyleEnum(FlexDirection.Row), // width = Length.Percent(100), // alignContent = new StyleEnum(Align.FlexStart), // alignItems = new StyleEnum(Align.FlexStart), // flexWrap = new StyleEnum(Wrap.Wrap), // marginBottom = 10, // display = DisplayStyle.None // } // }; // // TextField newNameField = new TextField("New Set Name") // { // style = // { // minWidth = Length.Percent(70) // } // }; // _newSetNameContainer.Add(newNameField); // // Button newSetCreateButton = new Button( // () => // { // CreateNewColorSet(newNameField.value); // List choices = _colorSetsDropdown.choices; // choices.Add(newNameField.value); // _colorSetsDropdown.choices = choices; // _colorSetsDropdown.value = newNameField.value; // _newSetNameContainer.style.display = DisplayStyle.None; // } // ) // { // text = "Create Set" // }; // // newNameField.RegisterValueChangedCallback( // evt => // { // newSetCreateButton.SetEnabled(!SidekickColorSet.DoesNameExist(_dbManager, evt.newValue)); // } // ); // // _newSetNameContainer.Add(newSetCreateButton); // view.Add(_newSetNameContainer); // // VisualElement allRow = new VisualElement(); // allRow.AddToClassList("colorSelectionRow"); // // Label allItemsLabel = new Label("All"); // allItemsLabel.AddToClassList("colorSelectionRowLabel"); // allRow.Add(allItemsLabel); // // VisualElement allRowContent = new VisualElement(); // allRowContent.AddToClassList("colorSelectionRowContent"); // allRow.Add(allRowContent); // // Button btnAllLock = new Button // { // style = // { // left = 0 // } // }; // allRowContent.Add(btnAllLock); // Image lockButtonImage = new Image // { // image = _currentGlobalLockStatus // ? EditorGUIUtility.IconContent("Locked").image // : EditorGUIUtility.IconContent("Unlocked").image, // scaleMode = ScaleMode.ScaleToFit // }; // // btnAllLock.Add(lockButtonImage); // // btnAllLock.clickable.clicked += () => // { // foreach (SidekickColorRow colorRow in _visibleColorRows) // { // colorRow.IsLocked = !_currentGlobalLockStatus; // if (colorRow.ButtonImage != null) // { // colorRow.ButtonImage.image = // colorRow.IsLocked // ? EditorGUIUtility.IconContent("Locked").image // : EditorGUIUtility.IconContent("Unlocked").image; // } // } // // _currentGlobalLockStatus = !_currentGlobalLockStatus; // lockButtonImage.image = _currentGlobalLockStatus // ? EditorGUIUtility.IconContent("Locked").image // : EditorGUIUtility.IconContent("Unlocked").image; // }; // // TODO: Uncomment once all colors options are re-enabled // Button randomAllButton = new Button // { // text = "R", // style = // { // right = 0 // } // }; // allRowContent.Add(randomAllButton); // // view.Add(allRow); _colorSelectionRowView = new VisualElement { style = { width = Length.Percent(100) } }; view.Add(_colorSelectionRowView); UpdateColorTabContent(); } /// /// Adds the content to the options tab. /// /// The view to add the content to. private void AddOptionsTabContent(VisualElement view) { Label baseAssetLabel = new Label { style = { marginTop = 5, marginLeft = 12, unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, text = "Base Assets", tooltip = "These assets are used to construct the character" }; view.Add(baseAssetLabel); _baseModelField = new ObjectField { style = { marginLeft = 15, marginRight = 15 }, tooltip = "The rigged character model used when constructing a character", objectType = typeof(GameObject), label = "Model" }; _baseModelField.RegisterCallback>( changeEvent => { // TODO: Check the model has a minimum of 1 SkinnedMeshRenderer as a child. } ); view.Add(_baseModelField); _baseModelField.value = Resources.Load(_BASE_MESH_NAME); _materialField = new ObjectField { tooltip = "The material used when constructing a character", objectType = typeof(Material), label = "Material", style = { marginLeft = 15, marginRight = 15 } }; view.Add(_materialField); _materialField.value = Resources.Load(_BASE_MATERIAL_NAME); _animationField = new ObjectField { tooltip = "The animation controller applied when constructing a character", objectType = typeof(AnimatorController), label = "Animation Controller", value = _currentAnimationController, style = { marginLeft = 15, marginRight = 15 } }; view.Add(_animationField); _animationField.RegisterValueChangedCallback( evt => { _currentAnimationController = (AnimatorController) evt.newValue; _sidekickRuntime.CurrentAnimationController = _currentAnimationController; } ); VisualElement updateLibraryLayout = new VisualElement { style = { minHeight = 20, display = DisplayStyle.Flex, flexDirection = FlexDirection.Row, marginBottom = 2, marginTop = 10, marginLeft = 15, marginRight = 2 } }; Button uploadLibraryButton = new Button() { text = "Update Part Library", tooltip = "Re-scans the project folders to update the parts list" }; uploadLibraryButton.clickable.clicked += async delegate { CreateGUI(); await Task.Run( async () => { await SidekickRuntime.PopulateToolData(_sidekickRuntime); _callbackQueue.Enqueue(AddAllTabContent); } ); }; updateLibraryLayout.Add(uploadLibraryButton); updateLibraryLayout.Add(_partCountLabel); view.Add(updateLibraryLayout); Label prefabOptions = new Label { style = { marginTop = 5, marginLeft = 12, unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, text = "Prefab Options", tooltip = "Options for how prefabs are created" }; view.Add(prefabOptions); Toggle combineToggle = new Toggle("Combine Character Meshes") { value = _combineMeshes, style = { marginTop = 10, marginLeft = 15 }, tooltip = "Whether or not to bake all the meshes down to a single mesh in the output model." }; combineToggle.RegisterValueChangedCallback( evt => { _combineMeshes = evt.newValue; } ); view.Add(combineToggle); Toggle bakeBlendsToggle = new Toggle("Combine Body Blend Shapes") { value = _bakeBlends, style = { marginTop = 10, marginLeft = 15 }, tooltip = "Whether or not to bake the body blend shapes into the mesh in the output model." }; bakeBlendsToggle.RegisterValueChangedCallback( evt => { _bakeBlends = evt.newValue; } ); view.Add(bakeBlendsToggle); Label toolOptions = new Label { style = { marginTop = 5, marginLeft = 12, unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, text = "Tool Options", tooltip = "Options for how the tool behaves" }; view.Add(toolOptions); _previewToggle = new Toggle("Auto Build Model") { value = true, style = { marginTop = 10, marginLeft = 15 } }; //view.Add(_previewToggle); Toggle filterColorsToggle = new Toggle("Show all color properties") { value = _showAllColourProperties, style = { marginTop = 10, marginLeft = 15 }, tooltip = "Display all color properties in the color tab rather than limited to only what the current character is using" }; filterColorsToggle.RegisterValueChangedCallback( evt => { _showAllColourProperties = evt.newValue; UpdateVisibleColorSets(); UpdateColorTabContent(); } ); view.Add(filterColorsToggle); Toggle autoOpenToggle = new Toggle("Open tool on startup") { value = _openWindowOnStart, style = { marginTop = 10, marginLeft = 15 }, tooltip = "Opens the Sidekick character tool on Unity startup" }; autoOpenToggle.RegisterValueChangedCallback( evt => { _openWindowOnStart = evt.newValue; EditorPrefs.SetBool(_AUTO_OPEN_STATE, _openWindowOnStart); } ); view.Add(autoOpenToggle); Toggle autoSaveToggle = new Toggle("Remember character") { value = _useAutoSaveAndLoad, style = { marginTop = 10, marginLeft = 15 }, tooltip = "Auto saves and loads character on run/stop and unity or tool open and close." }; autoSaveToggle.RegisterValueChangedCallback( evt => { _useAutoSaveAndLoad = evt.newValue; EditorPrefs.SetBool(_AUTOSAVE_STATE, _useAutoSaveAndLoad); } ); view.Add(autoSaveToggle); // TODO: Change to showing presets when parts are missing Toggle showMissingParts = new Toggle("Show missing parts warning") { value = _showMissingPartsPopup, style = { marginTop = 10, marginLeft = 15 }, tooltip = "Shows a popup for missing parts when selecting a preset." }; showMissingParts.RegisterValueChangedCallback( evt => { _showMissingPartsPopup = evt.newValue; EditorPrefs.SetBool(_AUTOSAVE_MISSING_PARTS, _showMissingPartsPopup); } ); view.Add(showMissingParts); VisualElement row = new VisualElement { style = { flexDirection = new StyleEnum(FlexDirection.Row), alignContent = new StyleEnum(Align.Center), justifyContent = new StyleEnum(Justify.SpaceAround), marginTop = 30 } }; Button documentationButton = new Button { text = "Documentation", style = { width = Length.Percent(30) }, tooltip = "Open documentation for Sidekick characters" }; documentationButton.clickable.clicked += delegate { string documentationPath = Path.Combine(DatabaseManager.GetPackageRootAbsolutePath() ?? string.Empty, "Documentation", "SidekickCharacters_UserGuide.pdf"); documentationPath = Path.GetFullPath(documentationPath); if (File.Exists(documentationPath)) { Application.OpenURL("file:" + documentationPath); } }; Button storeButton = new Button { text = "Synty Store", style = { width = Length.Percent(30) }, tooltip = "www.syntystore.com" }; storeButton.clickable.clicked += delegate { Application.OpenURL("https://syntystore.com"); }; Button tutorialButton = new Button { text = "Tutorials", style = { width = Length.Percent(30) }, tooltip = "Sidekick Characters - Quick start guide" }; tutorialButton.clickable.clicked += delegate { // TODO: Change to direct link to tutorial playlist, when available Application.OpenURL("https://www.youtube.com/@syntystudios"); }; row.Add(documentationButton); row.Add(storeButton); row.Add(tutorialButton); view.Add(row); } /// /// Reset the color rows for this color set back to the colors stored on the saved textures. /// private void ResetColorSet() { PopulateColorRowsFromTextures(); } /// /// Shows the name field and creation button for creating a new color set. /// private void ShowCreateNewColorSet() { _newSetNameContainer.style.display = DisplayStyle.Flex; } /// /// Creates a new color set with the given name. /// /// The name for the color set. private void CreateNewColorSet(string setName) { ResetCurrentColorSet(setName); SaveColorSet(); } /// /// Sets the current color set to a new, in-memory set of rows independent of the database /// /// Name of the new color set private void ResetCurrentColorSet(string setName = "Custom") { _currentColorSet.ID = -1; _currentColorSet.Species = _currentSpecies; _currentColorSet.Name = setName; foreach (SidekickColorRow row in _allColorRows) { row.ID = -1; row.ColorSet = _currentColorSet; } } /// /// Deletes a color set from the database. The color set will still be available in the app until the app is closed. /// private void DeleteColorSet() { List rowsToDelete = SidekickColorRow.GetAllBySet(_dbManager, _currentColorSet); foreach (SidekickColorRow row in rowsToDelete) { row.Delete(_dbManager); } _currentColorSet.Delete(_dbManager); UpdateVisibleColorSets(); ResetCurrentColorSet(); } /// /// Saves the current color row to the database. If it is a new color row, it is inserted into the DB; otherwise it is updated in the DB. /// private void SaveColorSet() { string baseColorSetPath = DatabaseManager.GetPackageAssetPath("Resources", "Species") ?? _BASE_COLOR_SET_PATH; string path = Path.Combine(baseColorSetPath, _currentSpecies.Name); path = Path.Combine(path, _currentColorSet.Name.Replace(" ", "_")); SaveTexturesToDisk(path); _currentColorSet.Save(_dbManager); foreach (SidekickColorRow row in _allColorRows) { row.Save(_dbManager); } UpdateVisibleColorSets(false); // TODO : refresh the project inspector window so the new textures show up } /// /// Saves texture files to disk at the given path. /// /// The path to save the textures to. private void SaveTexturesToDisk(string path, string additionalNaming = "") { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } // if no parts are selected, don't try and save a non-existent material, instead create one if (_currentMaterial == null) { _currentMaterial = (Material) _materialField.value; } string filename = _TEXTURE_PREFIX; if (!string.IsNullOrEmpty(additionalNaming)) { filename += additionalNaming; } string filePath = Path.Combine(path, filename + _TEXTURE_COLOR_NAME); Texture2D texture = (Texture2D) _currentMaterial.GetTexture(_COLOR_MAP); File.WriteAllBytes(filePath, texture.EncodeToPNG()); _currentColorSet.SourceColorPath = filePath; // TODO: Hidden due to early access, enable when feature complete // filePath = Path.Combine(path, filename + _TEXTURE_METALLIC_NAME); // texture = (Texture2D) _currentMaterial.GetTexture(_METALLIC_MAP); // File.WriteAllBytes(filePath, texture.EncodeToPNG()); // _currentColorSet.SourceMetallicPath = filePath; // filePath = Path.Combine(path, filename + _TEXTURE_SMOOTHNESS_NAME); // texture = (Texture2D) _currentMaterial.GetTexture(_SMOOTHNESS_MAP); // File.WriteAllBytes(filePath, texture.EncodeToPNG()); // _currentColorSet.SourceSmoothnessPath = filePath; // filePath = Path.Combine(path, filename + _TEXTURE_REFLECTION_NAME); // texture = (Texture2D) _currentMaterial.GetTexture(_REFLECTION_MAP); // File.WriteAllBytes(filePath, texture.EncodeToPNG()); // _currentColorSet.SourceReflectionPath = filePath; // filePath = Path.Combine(path, filename + _TEXTURE_EMISSION_NAME); // texture = (Texture2D) _currentMaterial.GetTexture(_EMISSION_MAP); // File.WriteAllBytes(filePath, texture.EncodeToPNG()); // _currentColorSet.SourceEmissionPath = filePath; // filePath = Path.Combine(path, filename + _TEXTURE_OPACITY_NAME); // texture = (Texture2D) _currentMaterial.GetTexture(_OPACITY_MAP); // File.WriteAllBytes(filePath, texture.EncodeToPNG()); // _currentColorSet.SourceOpacityPath = filePath; } /// /// Updates the color sets that are selectable in the dropdown on the colors tab /// /// Whether to set the color sets dropdown value to 'Custom' private void UpdateVisibleColorSets(bool setDropdownToCustom = true) { List sets = SidekickColorSet.GetAllBySpecies(_dbManager, _currentSpecies); if (sets.Count == 0) { sets.Add(SidekickColorSet.GetDefault(_dbManager)); } List setNames = sets.Select(set => set.Name).ToList(); _colorSetsDropdown.choices = setNames; _visibleColorSets = sets; if (setDropdownToCustom) { _colorSetsDropdown.value = "Custom"; } } /// /// Updates all the color rows currently visible in the UI. /// private void UpdateAllVisibleColors() { foreach (SidekickColorRow row in _visibleColorRows) { UpdateAllColors(row); } } /// /// Updates all the color types for a given color row. /// /// The color row to update. private void UpdateAllColors(SidekickColorRow colorRow) { foreach (ColorType colorType in Enum.GetValues(typeof(ColorType))) { _sidekickRuntime.UpdateColor(colorType, colorRow); } } /// /// Populates the part color rows based on the filter being used. /// private void PopulatePartColorRows() { List propertiesToShow = new List(); switch (_currentPartType) { case ColorPartType.Species: List speciesProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Species); foreach (SidekickColorProperty property in speciesProperties) { Vector2 uv = new Vector2(property.U, property.V); if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.Outfit: List outfitProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Outfits); foreach (SidekickColorProperty property in outfitProperties) { Vector2 uv = new Vector2(property.U, property.V); if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.Attachments: List attachmentProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Attachments); foreach (SidekickColorProperty property in attachmentProperties) { Vector2 uv = new Vector2(property.U, property.V); if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.Materials: List materialProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Materials); foreach (SidekickColorProperty property in materialProperties) { Vector2 uv = new Vector2(property.U, property.V); if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.Elements: List elementProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Elements); foreach (SidekickColorProperty property in elementProperties) { Vector2 uv = new Vector2(property.U, property.V); if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.CharacterHead: List headProperties = new List(); foreach (ColorPartType type in ColorPartType.CharacterHead.GetPartTypes()) { headProperties.AddRange(SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[type])); } foreach (SidekickColorProperty property in headProperties) { if (!propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.CharacterUpperBody: List upperProperties = new List(); foreach (ColorPartType type in ColorPartType.CharacterUpperBody.GetPartTypes()) { upperProperties.AddRange(SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[type])); } foreach (SidekickColorProperty property in upperProperties) { if (!propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.CharacterLowerBody: List lowerProperties = new List(); foreach (ColorPartType type in ColorPartType.CharacterLowerBody.GetPartTypes()) { lowerProperties.AddRange(SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[type])); } foreach (SidekickColorProperty property in lowerProperties) { if (!propertiesToShow.Contains(property)) { propertiesToShow.Add(property); } } break; case ColorPartType.Head: case ColorPartType.Hair: case ColorPartType.EyebrowLeft: case ColorPartType.EyebrowRight: case ColorPartType.EyeLeft: case ColorPartType.EyeRight: case ColorPartType.EarLeft: case ColorPartType.EarRight: case ColorPartType.FacialHair: case ColorPartType.Torso: case ColorPartType.ArmUpperLeft: case ColorPartType.ArmUpperRight: case ColorPartType.ArmLowerLeft: case ColorPartType.ArmLowerRight: case ColorPartType.HandLeft: case ColorPartType.HandRight: case ColorPartType.Hips: case ColorPartType.LegLeft: case ColorPartType.LegRight: case ColorPartType.FootLeft: case ColorPartType.FootRight: case ColorPartType.AttachmentHead: case ColorPartType.AttachmentFace: case ColorPartType.AttachmentBack: case ColorPartType.AttachmentHipsFront: case ColorPartType.AttachmentHipsBack: case ColorPartType.AttachmentHipsLeft: case ColorPartType.AttachmentHipsRight: case ColorPartType.AttachmentShoulderLeft: case ColorPartType.AttachmentShoulderRight: case ColorPartType.AttachmentElbowLeft: case ColorPartType.AttachmentElbowRight: case ColorPartType.AttachmentKneeLeft: case ColorPartType.AttachmentKneeRight: case ColorPartType.Nose: case ColorPartType.Teeth: case ColorPartType.Tongue: case ColorPartType.Wrap:/* case ColorPartType.AttachmentHandLeft: case ColorPartType.AttachmentHandRight: */ propertiesToShow = SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[_currentPartType]); break; case ColorPartType.AllParts: default: propertiesToShow = _showAllColourProperties || _currentUVList == null ? SidekickColorProperty.GetAll(_dbManager) : SidekickColorProperty.GetByUVs(_dbManager, _currentUVList); break; } _visibleColorRows.Clear(); // when filtering the view to a specific UV dictionary, we need to reset the property order propertiesToShow.Sort((a, b) => a.ID.CompareTo(b.ID)); foreach (SidekickColorProperty property in propertiesToShow) { foreach (SidekickColorRow row in _allColorRows.Where(row => row.ColorProperty.ID == property.ID)) { _visibleColorRows.Add(row); } } } /// /// Refreshes the visible color rows in the UI. /// private void RefreshVisibleColorRows() { _colorSelectionRowView.Clear(); foreach (ColorGroup group in Enum.GetValues(typeof(ColorGroup))) { List properties = _visibleColorRows .Select(row => row.ColorProperty) .Where(prop => prop.Group == group) .ToList(); string tooltipText = ""; switch (group) { case ColorGroup.Species: tooltipText = "Species colors make up the character as if it has no outfit on. (for example - skin, teeth, tongue, fingernails etc)"; break; case ColorGroup.Outfits: tooltipText = "Outfit colors make up the clothing on the character. (for example - Torso outfit, arm outfit, hand outfit etc)"; break; case ColorGroup.Attachments: tooltipText = "Attachment colors make up the additional parts attached to a character. (for example - a backpack, shoulder pads, elbow pads, knee pads etc)"; break; case ColorGroup.Materials: tooltipText = "material colors make up a collection of shared standard materials (for example - wood, metal, leather, paper, bone etc)"; break; } if (properties.Count > 0) { Label groupLabel = new Label(group.ToString()) { style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold), marginBottom = 4, marginTop = 6 }, tooltip = tooltipText }; _colorSelectionRowView.Add(groupLabel); VisualElement headerContainer = new VisualElement() { style = { flexDirection = new StyleEnum(FlexDirection.Row), fontSize = 10, unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) } }; Label colorHeader = new Label("Color") { style = { width = 103, marginLeft = 155 } }; Label metallicHeader = new Label("Metallic") { style = { width = 66 } }; Label smoothnessHeader = new Label("Smoothness") { style = { width = 66 } }; Label reflectionHeader = new Label("Reflection") { style = { width = 66 } }; Label emissionHeader = new Label("Emission") { style = { width = 66 } }; Label opacityHeader = new Label("Opacity") { style = { width = 66 } }; headerContainer.Add(colorHeader); // headerContainer.Add(metallicHeader); // headerContainer.Add(smoothnessHeader); // headerContainer.Add(reflectionHeader); // headerContainer.Add(emissionHeader); // headerContainer.Add(opacityHeader); _colorSelectionRowView.Add(headerContainer); foreach (SidekickColorProperty property in properties) { foreach (SidekickColorRow row in _visibleColorRows.Where(row => row.ColorProperty.ID == property.ID)) { CreateColorRow(_colorSelectionRowView, row); } } } } } /// /// Populates the color rows from texture files on the disk. /// private void PopulateColorRowsFromTextures() { TextureImporter textureImporter = null; Texture2D mainColor = AssetDatabase.LoadAssetAtPath(_currentColorSet.SourceColorPath); if (mainColor != null) { mainColor.filterMode = FilterMode.Point; if (!mainColor.isReadable) { textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceColorPath); textureImporter.isReadable = true; textureImporter.SaveAndReimport(); } } else { Material defaultMaterial = (Material) _materialField.value; mainColor = (Texture2D) defaultMaterial.mainTexture; } Texture2D metallic = AssetDatabase.LoadAssetAtPath(_currentColorSet.SourceMetallicPath); if (metallic != null) { metallic.filterMode = FilterMode.Point; if (!metallic.isReadable) { textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceMetallicPath); textureImporter.isReadable = true; textureImporter.SaveAndReimport(); } } Texture2D smoothness = AssetDatabase.LoadAssetAtPath(_currentColorSet.SourceSmoothnessPath); if (smoothness != null) { smoothness.filterMode = FilterMode.Point; if (!smoothness.isReadable) { textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceSmoothnessPath); textureImporter.isReadable = true; textureImporter.SaveAndReimport(); } } Texture2D reflection = AssetDatabase.LoadAssetAtPath(_currentColorSet.SourceReflectionPath); if (reflection != null) { reflection.filterMode = FilterMode.Point; if (!reflection.isReadable) { textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceReflectionPath); textureImporter.isReadable = true; textureImporter.SaveAndReimport(); } } Texture2D emission = AssetDatabase.LoadAssetAtPath(_currentColorSet.SourceEmissionPath); if (emission != null) { emission.filterMode = FilterMode.Point; if (!emission.isReadable) { textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceEmissionPath); textureImporter.isReadable = true; textureImporter.SaveAndReimport(); } } Texture2D opacity = AssetDatabase.LoadAssetAtPath(_currentColorSet.SourceOpacityPath); if (opacity != null) { opacity.filterMode = FilterMode.Point; if (!opacity.isReadable) { textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceOpacityPath); textureImporter.isReadable = true; textureImporter.SaveAndReimport(); } } List newColorRows = new List(); List currentSetColors = SidekickColorRow.GetAllBySet(_dbManager, _currentColorSet); // TODO : if textures don't exist BUT color rows exist in DB, ask user if they want to re-save the textures from the DB values, loop back and reimport // TODO : if textures don't exist AND color rows don't exist in DB, delete the colorset entry in DB/dropdown, advance to next on list and reload foreach (SidekickColorProperty property in SidekickColorProperty.GetAll(_dbManager)) { SidekickColorRow existingRow = currentSetColors.FirstOrDefault(row => row.ColorProperty.ID == property.ID); SidekickColorRow newRow = new SidekickColorRow { ID = existingRow?.ID ?? -1, ColorSet = _currentColorSet, ColorProperty = property, // TODO remove null checks when we know we have textures NiceColor = mainColor?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceColor ?? Color.red, // NiceMetallic = metallic?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceMetallic ?? Color.red, // NiceSmoothness = smoothness?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceSmoothness ?? Color.red, // NiceReflection = reflection?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceReflection ?? Color.red, // NiceEmission = emission?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceEmission ?? Color.red, // NiceOpacity = opacity?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceOpacity ?? Color.red }; newRow.Save(_dbManager); newColorRows.Add(newRow); } _allColorRows = newColorRows; PopulatePartColorRows(); RefreshVisibleColorRows(); } /// /// Adds a color row to the given view. /// /// The view to add the color row to. /// The color row to populate this UI element with. private void CreateColorRow(VisualElement view, SidekickColorRow colorRow) { VisualElement row = new VisualElement(); row.AddToClassList("colorSelectionRow"); Label rowLabel = new Label(colorRow.ColorProperty.Name); rowLabel.AddToClassList("colorSelectionRowLabel"); row.Add(rowLabel); VisualElement rowContent = new VisualElement(); rowContent.AddToClassList("colorSelectionRowContent"); row.Add(rowContent); // TODO: uncomment when locking is required. // Button btnLock = new Button(); // // Image lockImage = new Image // { // image = colorRow.IsLocked ? EditorGUIUtility.IconContent("Locked").image : EditorGUIUtility.IconContent("Unlocked").image, // scaleMode = ScaleMode.ScaleToFit // }; // // btnLock.Add(lockImage); // colorRow.ButtonImage = lockImage; // rowContent.Add(btnLock); // btnLock.clickable.clicked += () => // { // colorRow.IsLocked = !colorRow.IsLocked; // lockImage.image = colorRow.IsLocked // ? EditorGUIUtility.IconContent("Locked").image // : EditorGUIUtility.IconContent("Unlocked").image; // }; ColorField colorField = new ColorField { value = colorRow.NiceColor, tooltip = colorRow.ColorProperty.Name + " Color", style = { // TODO: shrink to 50 once all colors options are re-enabled width = 100 } }; rowContent.Add(colorField); colorField.RegisterValueChangedCallback( evt => { colorRow.NiceColor = evt.newValue; _sidekickRuntime.UpdateColor(ColorType.MainColor, colorRow); } ); // TODO: Uncomment once all colors options are re-enabled // ColorField metallicField = new ColorField // { // value = colorRow.NiceMetallic, // tooltip = colorRow.ColorProperty.Name + " Metallic", // style = // { // width = 60 // } // }; // rowContent.Add(metallicField); // metallicField.RegisterValueChangedCallback( // evt => // { // colorRow.NiceMetallic = evt.newValue; // _sidekickRuntime.UpdateColor(ColorType.Metallic, colorRow); // } // ); // // ColorField smoothnessField = new ColorField // { // value = colorRow.NiceSmoothness, // tooltip = colorRow.ColorProperty.Name + " Smoothness", // style = // { // width = 60 // } // }; // rowContent.Add(smoothnessField); // smoothnessField.RegisterValueChangedCallback( // evt => // { // colorRow.NiceSmoothness = evt.newValue; // _sidekickRuntime.UpdateColor(ColorType.Smoothness, colorRow); // } // ); // // ColorField reflectionField = new ColorField // { // value = colorRow.NiceReflection, // tooltip = colorRow.ColorProperty.Name + " Reflection", // style = // { // width = 60 // } // }; // rowContent.Add(reflectionField); // reflectionField.RegisterValueChangedCallback( // evt => // { // colorRow.NiceReflection = evt.newValue; // _sidekickRuntime.UpdateColor(ColorType.Reflection, colorRow); // } // ); // // ColorField emissionField = new ColorField // { // value = colorRow.NiceEmission, // tooltip = colorRow.ColorProperty.Name + " Emission", // style = // { // width = 60 // } // }; // rowContent.Add(emissionField); // emissionField.RegisterValueChangedCallback( // evt => // { // colorRow.NiceEmission = evt.newValue; // _sidekickRuntime.UpdateColor(ColorType.Emission, colorRow); // } // ); // // ColorField opacityField = new ColorField // { // value = colorRow.NiceOpacity, // tooltip = colorRow.ColorProperty.Name + " Opacity", // style = // { // width = 60 // } // }; // rowContent.Add(opacityField); // opacityField.RegisterValueChangedCallback( // evt => // { // colorRow.NiceOpacity = evt.newValue; // _sidekickRuntime.UpdateColor(ColorType.Opacity, colorRow); // } // ); // Button randomButton = new Button // { // text = "R", // style = // { // right = 0 // } // }; // rowContent.Add(randomButton); view.Add(row); } /// /// Switches the currently visible tab to the given tab. /// /// The tab to switch to. private void SwitchToTab(TabView newTab) { if (_currentTab == newTab) { return; } _currentTab = newTab; _bodyPresetTab.value = _currentTab == TabView.Preset; _bodyPartsTab.value = _currentTab == TabView.Parts; _bodyShapeTab.value = _currentTab == TabView.Body; _colorSelectionTab.value = _currentTab == TabView.Colors; // _decalSelectionTab.value = _currentTab == TabView.Decals; _optionTab.value = _currentTab == TabView.Options; _presetView.style.display = _bodyPresetTab.value ? DisplayStyle.Flex : DisplayStyle.None; _partView.style.display = _bodyPartsTab.value ? DisplayStyle.Flex : DisplayStyle.None; _bodyShapeView.style.display = _bodyShapeTab.value ? DisplayStyle.Flex : DisplayStyle.None; _colorSelectionView.style.display = _colorSelectionTab.value ? DisplayStyle.Flex : DisplayStyle.None; //_decalSelectionView.style.display = _decalSelectionTab.value ? DisplayStyle.Flex : DisplayStyle.None; _optionSelectionView.style.display = _optionTab.value ? DisplayStyle.Flex : DisplayStyle.None; } /// /// Populate the preset tab content. /// private void PopulatePresetUI() { _presetView.Clear(); Dictionary> dropdowns = new Dictionary>(); Foldout speciesFoldout = new Foldout { text = "Select - Species", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, }; List speciesNames = _allSpecies.Select(species => species.Name).ToList(); _speciesPresetField = new DropdownField { label = "Species", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Normal) }, tooltip = "Select the species of your character" }; _speciesPresetField.choices = speciesNames; _speciesPresetField.RegisterValueChangedCallback( evt => { _speciesField.value = _speciesPresetField.value; ProcessSpeciesChange(evt.newValue); } ); _speciesPresetField.index = _currentSpecies != null && speciesNames.Count > 0 ? _speciesPresetField.choices.IndexOf(_currentSpecies.Name) : 0; speciesFoldout.Add(_speciesPresetField); _presetView.Add(speciesFoldout); List allFilters = SidekickPresetFilter.GetAll(_dbManager); allFilters.Sort( (filterA, filterB) => String.CompareOrdinal(filterA.Term, filterB.Term) ); if (allFilters.Count > 0) { Foldout filterFoldout = new Foldout() { text = "Select - Preset Part Filter", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) } }; VisualElement filterContent = new VisualElement { style = { flexDirection = new StyleEnum(FlexDirection.Row), flexWrap = new StyleEnum(Wrap.Wrap) } }; Color borderColor = new Color(0.17f, 0.17f, 0.17f); Color backgroundColor = new Color(0.35f, 0.35f, 0.35f); List allFilterToggles = new List(); foreach (SidekickPresetFilter filter in allFilters) { Toggle outfitToggle = new Toggle(filter.Term) { value = !_partPresetFilterToggleMap.TryGetValue(filter, out bool toggleValue) || toggleValue, style = { width = 160, borderBottomWidth = 1, borderBottomColor = borderColor, paddingBottom = 2, borderLeftWidth = 1, borderLeftColor = borderColor, paddingLeft = 2, borderRightWidth = 1, borderRightColor = borderColor, paddingRight = 2, borderTopWidth = 1, borderTopColor = borderColor, paddingTop = 2, borderBottomLeftRadius = 3, borderBottomRightRadius = 3, borderTopLeftRadius = 3, borderTopRightRadius = 3, backgroundColor = backgroundColor, textOverflow = new StyleEnum(TextOverflow.Ellipsis) } }; allFilterToggles.Add(outfitToggle); if (outfitToggle.value) { _partPresetFilterToggleMap[filter] = outfitToggle.value; } outfitToggle.RegisterValueChangedCallback( evt => { _partPresetFilterToggleMap[filter] = evt.newValue; PopulatePresetPartDropdowns(dropdowns); } ); filterContent.Add(outfitToggle); } VisualElement buttonRow = new VisualElement() { style = { flexDirection = new StyleEnum(FlexDirection.Row) } }; Button selectAll = new Button( delegate { foreach (Toggle toggle in allFilterToggles) { toggle.value = true; } } ) { text = "Select All" }; Button selectNone = new Button( delegate { foreach (Toggle toggle in allFilterToggles) { toggle.value = false; } } ) { text = "Select None" }; buttonRow.Add(selectAll); buttonRow.Add(selectNone); filterFoldout.Add(buttonRow); filterFoldout.Add(filterContent); _presetView.Add(filterFoldout); } _availablePresets = SidekickPartPreset.GetAll(_dbManager); Foldout generateFoldout = new Foldout { text = "Randomize Character", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, //tooltip = "Create a character based on the selected species" }; Button generateButton = new Button() { style = { minHeight = 50, marginRight = 18, flexDirection = new StyleEnum(FlexDirection.Row), alignContent = new StyleEnum(Align.Center), alignItems = new StyleEnum(Align.Center), unityTextAlign = new StyleEnum(TextAnchor.MiddleCenter), justifyContent = new StyleEnum(Justify.Center) }, tooltip = "Generate a character at the push of a button" }; Texture2D randomImage = Resources.Load("UI/T_Random"); generateButton.Add( new Image { image = randomImage, scaleMode = ScaleMode.ScaleToFit, style = { paddingTop = new StyleLength(1), paddingBottom = new StyleLength(1), paddingRight = 5, alignSelf = new StyleEnum(Align.Center) } } ); generateButton.Add( new Label { text = "Randomize Character", style = { alignSelf = new StyleEnum(Align.Center) } } ); generateFoldout.Add(generateButton); _presetView.Add(generateFoldout); Foldout presetsFoldout = new Foldout() { text = "Presets", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, //tooltip = "Select from a number of collections of parts, body types and colors" }; Label partTitle = new Label("Parts") { style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, tooltip = "Collections of character parts ie. Head attachment, Torso, Nose that make up the character" }; string tooltipText = ""; presetsFoldout.Add(partTitle); _presetView.Add(presetsFoldout); _presetPartContainer = new VisualElement(); presetsFoldout.Add(_presetPartContainer); PopulatePresetPartDropdowns(dropdowns); Label bodyTitle = new Label("Body") { style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, tooltip = "Preset bodies in a number of types, sizes and musculature" }; presetsFoldout.Add(bodyTitle); _currentBodyPresetDictionary = new Dictionary(); List bodyShapes = SidekickBodyShapePreset.GetAll(_dbManager); List bodyShapeNames = bodyShapes.Select(b => b.Name).ToList(); for (int i = 0; i < bodyShapeNames.Count; i++) { _currentBodyPresetDictionary.Add(bodyShapeNames[i], bodyShapes[i]); } string bodyTypeLabel = "Body Type"; string bodyShapeDefaultValue = "Androgynous Medium"; if (bodyShapeNames.Count > 0) { bodyShapeDefaultValue = _presetDefaultValues.TryGetValue(bodyTypeLabel, out string bodyShapeValue) ? bodyShapeValue : bodyShapeDefaultValue; } bodyShapeNames.Sort(); tooltipText = "Select a body preset for you character - a body type preset is made up of combinations of body type, size and musculature."; dropdowns[bodyTypeLabel] = CreatePresetRow(presetsFoldout, bodyTypeLabel, tooltipText, bodyShapeNames, false, bodyShapeDefaultValue, PresetDropdownType.Body); Label colorTitle = new Label("Colors") { style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) }, tooltip = "Collections of character colors ie. Skin color, teeth color, eye color, hair color that make up the characters colors" }; SidekickSpecies unrestrictedSpecies = SidekickSpecies.GetByName(_dbManager, "Unrestricted"); presetsFoldout.Add(colorTitle); _currentColorSpeciesPresetDictionary = new Dictionary(); _currentColorOutfitsPresetDictionary = new Dictionary(); _currentColorAttachmentsPresetDictionary = new Dictionary(); _currentColorMaterialsPresetDictionary = new Dictionary(); _currentColorElementsPresetDictionary = new Dictionary(); foreach (ColorGroup colorGroup in Enum.GetValues(typeof(ColorGroup))) { // TODO: remove when Element colors are re-added if (colorGroup == ColorGroup.Elements) { continue; } List colorPresets = colorGroup is ColorGroup.Species && _currentSpecies.ID != unrestrictedSpecies.ID ? SidekickColorPreset.GetAllByColorGroupAndSpecies(_dbManager, colorGroup, _currentSpecies) : SidekickColorPreset.GetAllByColorGroup(_dbManager, colorGroup); List colorPresetNames = colorPresets.Select(cp => cp.Name).ToList(); for (int i = 0; i < colorPresetNames.Count; i++) { switch (colorGroup) { case ColorGroup.Species: _currentColorSpeciesPresetDictionary.Add(colorPresetNames[i], colorPresets[i]); tooltipText = "Select a species color preset for your character - a species color preset is made up of the colors that would make up the character if it had no outfit on. (for example - skin, teeth, tongue, fingernails etc)"; break; case ColorGroup.Outfits: _currentColorOutfitsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]); tooltipText = "Select an outfit color preset for your character - an outfit color preset is made up of the colors that make up the clothing on the character. (for example - torso outfit, arm outfit, hand outfit etc)"; break; case ColorGroup.Attachments: _currentColorAttachmentsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]); tooltipText = "Select an attachments color preset for your character - an attachments color preset is made up of the colors used on additional parts on the character. (for example - shoulder attachments, back attachments, hip attachments etc)"; break; case ColorGroup.Materials: _currentColorMaterialsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]); tooltipText = "Select a materials color preset for your character - a materials color preset is made up of the colors that make up general materials of the outfit and attachments. (for example - metal, wood, leather, plastic, bone etc)"; break; case ColorGroup.Elements: _currentColorElementsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]); break; } } string defaultValue = "None"; if (colorPresetNames.Count > 0) { defaultValue = _presetDefaultValues.TryGetValue(colorGroup.ToString(), out string value) ? value : defaultValue; } colorPresetNames.Sort(); if (_processingSpeciesChange && colorGroup == ColorGroup.Species && !_loadingCharacter) { defaultValue = colorPresetNames.Count > 0 ? colorPresetNames[0] : "None"; } dropdowns[colorGroup.ToString()] = CreatePresetRow(presetsFoldout, colorGroup.ToString(), tooltipText, colorPresetNames, true, defaultValue, PresetDropdownType.Color); } /* TODO: When decals are added (issue 1108), uncomment and update this section Label textureTitle = new Label("Textures") { style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) } }; presetsFoldout.Add(textureTitle); CreatePresetRow(presetsFoldout, "Skin", new List(), true, "None", PresetDropdownType.Texture); CreatePresetRow(presetsFoldout, "Outfit", new List(), true, "None", PresetDropdownType.Texture); */ generateButton.clickable.clicked += delegate { _applyingPreset = true; foreach (PopupField dropdown in dropdowns.Values) { List values = dropdown.choices; values.Remove("None"); string newValue = "None"; if (values.Count > 0) { newValue = values[Random.Range(0, values.Count - 1)]; } dropdown.value = newValue; } // if (_newModel != null) // { // DestroyImmediate(_newModel); // } _newModel = GenerateCharacter(false, true); UpdatePartUVData(); _applyingPreset = false; }; } /// /// Populates the preset part dropdowns. /// /// The list of all preset dropdowns to put the preset part dropdowns into once populated /// private void PopulatePresetPartDropdowns(Dictionary> dropdowns) { _presetPartContainer.Clear(); string tooltipText = ""; _currentHeadPresetDictionary = new Dictionary(); _currentUpperBodyPresetDictionary = new Dictionary(); _currentLowerBodyPresetDictionary = new Dictionary(); HashSet mappedPresets = new HashSet(); if (_partPresetFilterToggleMap.Count > 0) { foreach (KeyValuePair entry in _partPresetFilterToggleMap) { if (entry.Value) { if (_sidekickRuntime.MappedPresetFilterDictionary.TryGetValue(entry.Key.Term, out List presets)) { mappedPresets.UnionWith(presets); } } } if (_sidekickRuntime.MappedBasePresetDictionary.TryGetValue(_currentSpecies, out List basePresets)) { mappedPresets.UnionWith(basePresets); } } else { mappedPresets.UnionWith(_availablePresets); } List allPresets = mappedPresets.ToList(); SidekickSpecies unrestrictedSpecies = SidekickSpecies.GetByName(_dbManager, "Unrestricted"); foreach (PartGroup partGroup in Enum.GetValues(typeof(PartGroup))) { // only filter head part presets by species List presets = partGroup is PartGroup.Head && _currentSpecies.ID != unrestrictedSpecies?.ID ? allPresets.Where(preset => preset.Species.ID == _currentSpecies.ID && preset.PartGroup == partGroup).ToList() : allPresets.Where(preset => preset.PartGroup == partGroup).ToList(); List presetNames = new List(); foreach (SidekickPartPreset preset in presets) { switch (partGroup) { case PartGroup.Head: _currentHeadPresetDictionary.Add(preset.Name, preset); tooltipText = "Select a head preset for you character - a head preset is made up of parts like a head, nose, eyes and teeth etc."; break; case PartGroup.UpperBody: _currentUpperBodyPresetDictionary.Add(preset.Name, preset); tooltipText = "Select an upper body preset for you character - an upper body preset is made up of parts like a torso, arms, hands and a back attachment etc."; break; case PartGroup.LowerBody: _currentLowerBodyPresetDictionary.Add(preset.Name, preset); tooltipText = "Select a lower body preset for you character - a lower body preset is made up of parts like hips, legs, feet and hip attachments etc."; break; } presetNames.Add(preset.Name); } string defaultValue = "None"; if (presetNames.Count > 0) { if (_presetDefaultValues.TryGetValue(partGroup.ToString(), out string value)) { defaultValue = presetNames.Contains(value) ? value : "None"; if (_processingSpeciesChange && partGroup == PartGroup.Head && value != "None" && defaultValue == "None") { defaultValue = presetNames[Random.Range(0, presetNames.Count - 1)]; } } } presetNames.Sort(); dropdowns[partGroup.ToString()] = CreatePresetRow(_presetPartContainer, partGroup.ToString(), tooltipText, presetNames, true, defaultValue, PresetDropdownType.Part); } } /// /// Create a selection row for the preset tab with the given values. /// /// The view to add the row to. /// The label to put in the row. /// The tooltip to display for this row. /// The values for the dropdown in the row. /// Whether to include `None` as a value in the dropdown. /// The default value to select from the dropdown. /// What section of the preset UI this dropdown is part of. /// The dropdown selection UI element. private PopupField CreatePresetRow( VisualElement view, string rowLabel, string tooltipText, List dropdownValues, bool includeNoneValue, string defaultValue, PresetDropdownType dropdownType) { VisualElement partContainer = new VisualElement { style = { minHeight = 20, display = DisplayStyle.Flex, flexDirection = FlexDirection.Row, marginBottom = 2, marginTop = 2, marginLeft = 15, marginRight = 2, unityFontStyleAndWeight = new StyleEnum(FontStyle.Normal) } }; Label partTypeTitle = new Label(rowLabel.ToString()) { style = { unityTextAlign = TextAnchor.MiddleLeft, width = 150 }, tooltip = tooltipText }; Button removeButton = new Button() { tooltip = "Remove this preset, resets selection to None" }; removeButton.Add( new Image { image = Resources.Load("UI/T_Clear"), scaleMode = ScaleMode.ScaleToFit } ); int marginLeft = 0; if (dropdownType != PresetDropdownType.Part) { marginLeft = 36; } Button previousButton = new Button() { tooltip = "Select the previous preset", style = { marginLeft = marginLeft } }; previousButton.Add( new Image { image = EditorGUIUtility.IconContent("tab_prev", "|Previous Preset").image, scaleMode = ScaleMode.ScaleToFit } ); Button nextButton = new Button() { tooltip = "Select the next preset" }; nextButton.Add( new Image { image = EditorGUIUtility.IconContent("tab_next", "|Next Preset").image, scaleMode = ScaleMode.ScaleToFit } ); Button randomButton = new Button() { tooltip = "Randomly select a preset" }; Texture2D randomImage = Resources.Load("UI/T_Random"); randomButton.Add( new Image { image = randomImage, scaleMode = ScaleMode.ScaleToFit, style = { paddingTop = new StyleLength(1), paddingBottom = new StyleLength(1) } } ); if (!dropdownValues.Contains(defaultValue)) { defaultValue = includeNoneValue ? "None" : dropdownValues[0]; } List popupValues = new List(); if (includeNoneValue) { popupValues.Add("None"); }; popupValues.AddRange(dropdownValues); PopupField partSelection = new PopupField(popupValues, 0) { value = "None", style = { minWidth = 180 }, tooltip = tooltipText }; partSelection.RegisterValueChangedCallback( evt => { _presetDefaultValues[rowLabel] = evt.newValue; // Correctly enable/disable next and previous buttons based on selection previousButton.SetEnabled(partSelection.index > 0); nextButton.SetEnabled(partSelection.index < popupValues.Count - 1); switch (dropdownType) { case PresetDropdownType.Part: _applyingPreset = true; bool hasErrors = false; string errorMessage = "The following parts could not be found in your project:\n"; Enum.TryParse(rowLabel, out PartGroup group); List partTypesToRemove = group.GetPartTypes(); SidekickPartPreset currentPartPreset = null; List presetParts = new List(); // NOTE : need to ensure evt.newValue is always in the dictionary ahead of this, or change to GetValueOrDefault() if (evt.newValue != "None") { switch (group) { case PartGroup.Head: currentPartPreset = _currentHeadPresetDictionary[evt.newValue]; break; case PartGroup.UpperBody: currentPartPreset = _currentUpperBodyPresetDictionary[evt.newValue]; break; case PartGroup.LowerBody: currentPartPreset = _currentLowerBodyPresetDictionary[evt.newValue]; break; } presetParts = SidekickPartPresetRow.GetAllByPreset(_dbManager, currentPartPreset); } foreach (SidekickPartPresetRow presetPart in presetParts) { if (Enum.TryParse(CharacterPartTypeUtils.GetTypeNameFromShortcode(presetPart.PartType), out CharacterPartType partType)) { if (_partSelectionDictionary.TryGetValue(partType, out PartTypeControls currentField)) { if (presetPart.Part != null) { _currentCharacter[partType] = presetPart.Part; } UpdateResult result = new UpdateResult(errorMessage, hasErrors); if (partType == CharacterPartType.Wrap) { if (_partSelectionDictionary.TryGetValue(partType, out PartTypeControls wrapSelection)) { if (_requiresWrap && _bodyTypeBlendValue > 0) { wrapSelection.PartDropdown.SetEnabled(true); wrapSelection.RandomisePartDropdownValue(); } else { wrapSelection.PartDropdown.SetEnabled(false); wrapSelection.SetPartDropdownValue(null); _currentCharacter.Remove(CharacterPartType.Wrap); } } } else { result = UpdatePartDropdown( currentField, presetPart.Part?.Name ?? "None", errorMessage, hasErrors ); } hasErrors = result.HasErrors; errorMessage = result.ErrorMessage; partTypesToRemove.Remove(partType); } } } foreach (CharacterPartType partType in partTypesToRemove) { if (_partSelectionDictionary.TryGetValue(partType, out PartTypeControls currentField)) { UpdateResult result = UpdatePartDropdown(currentField, "None", errorMessage, hasErrors); hasErrors = result.HasErrors; errorMessage = result.ErrorMessage; } } if (hasErrors) { EditorUtility.DisplayDialog( "Assets Missing", errorMessage, "OK" ); } _applyingPreset = false; _newModel = GameObject.Find(_OUTPUT_MODEL_NAME); // if (_newModel != null) // { // DestroyImmediate(_newModel); // } _newModel = GenerateCharacter(false, true); UpdatePartUVData(); break; case PresetDropdownType.Body: SidekickBodyShapePreset bodyShapePreset = _currentBodyPresetDictionary[evt.newValue]; if (Math.Abs(_bodyTypeSlider.value - bodyShapePreset.BodyType) < 0.001f) { string body = "None"; if (_partSelectionDictionary.TryGetValue(CharacterPartType.Torso, out PartTypeControls bodySelection)) { body = bodySelection.PartDropdown.value; } if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection)) { if (body != "None" && _requiresWrap && _bodyTypeBlendValue > 0) { wrapSelection.PartDropdown.SetEnabled(true); wrapSelection.RandomisePartDropdownValue(); } else { wrapSelection.PartDropdown.SetEnabled(false); wrapSelection.SetPartDropdownValue(null); _currentCharacter.Remove(CharacterPartType.Wrap); } } } _bodyTypeSlider.value = bodyShapePreset.BodyType; _bodySizeSlider.value = bodyShapePreset.BodySize; _musclesSlider.value = bodyShapePreset.Musculature; break; case PresetDropdownType.Color: if (evt.newValue == "None") { return; } ResetCurrentColorSet(); Enum.TryParse(rowLabel, out ColorGroup colorGroup); SidekickColorPreset colorPreset = null; // NOTE : need to ensure evt.newValue is always in the dictionary ahead of this, or change to GetValueOrDefault() switch (colorGroup) { case ColorGroup.Species: colorPreset = _currentColorSpeciesPresetDictionary[evt.newValue]; break; case ColorGroup.Outfits: colorPreset = _currentColorOutfitsPresetDictionary[evt.newValue]; break; case ColorGroup.Attachments: colorPreset = _currentColorAttachmentsPresetDictionary[evt.newValue]; break; case ColorGroup.Materials: colorPreset = _currentColorMaterialsPresetDictionary[evt.newValue]; break;/* case ColorGroup.Elements: colorPreset = _currentColorElementsPresetDictionary[evt.newValue]; break;*/ } List presetColorRows = SidekickColorPresetRow.GetAllByPreset(_dbManager, colorPreset); foreach (SidekickColorPresetRow row in presetColorRows) { SidekickColorRow existingRow = _allColorRows.Find(r => r.ColorProperty.ID == row.ColorProperty.ID); if (existingRow == null) { existingRow = new SidekickColorRow() { ID = -1, ColorSet = _currentColorSet, ColorProperty = row.ColorProperty, NiceColor = row.NiceColor, NiceMetallic = row.NiceMetallic, NiceSmoothness = row.NiceSmoothness, NiceReflection = row.NiceReflection, NiceEmission = row.NiceEmission, NiceOpacity = row.NiceOpacity }; _allColorRows.Add(existingRow); } else { existingRow.NiceColor = row.NiceColor; existingRow.NiceMetallic = row.NiceMetallic; existingRow.NiceSmoothness = row.NiceSmoothness; existingRow.NiceReflection = row.NiceReflection; existingRow.NiceEmission = row.NiceEmission; existingRow.NiceOpacity = row.NiceOpacity; } UpdateAllColors(existingRow); } PopulatePartColorRows(); RefreshVisibleColorRows(); break; case PresetDropdownType.Texture: // TODO: Add texture setting functionality once decal system in place break; default: throw new ArgumentOutOfRangeException(nameof(dropdownType), dropdownType, null); } } ); removeButton.clickable.clicked += delegate { // TODO: Change to default character when available partSelection.value = "None"; }; previousButton.clickable.clicked += delegate { int newIndex = partSelection.index - 1; if (newIndex <= 0) { newIndex = 0; } partSelection.index = newIndex; }; nextButton.clickable.clicked += delegate { int newIndex = partSelection.index + 1; if (newIndex >= partSelection.choices.Count - 1) { newIndex = partSelection.choices.Count - 1; } partSelection.index = newIndex; }; randomButton.clickable.clicked += delegate { int currentIndex = partSelection.index; if (partSelection.choices.Count - 1 > 1) { while (partSelection.index == currentIndex) { partSelection.index = Random.Range(1, partSelection.choices.Count); } } }; partContainer.Add(partTypeTitle); if (dropdownType == PresetDropdownType.Part) { partContainer.Add(removeButton); } partContainer.Add(previousButton); partContainer.Add(nextButton); partContainer.Add(randomButton); partContainer.Add(partSelection); view.Add(partContainer); partSelection.value = defaultValue; return partSelection; } /// /// Updates the required parts of the UI on a species change. /// /// The name of the species being changed to. private void ProcessSpeciesChange(string newSpecies) { // don't need to re-process if multiple callbacks are triggered if (_currentSpecies.Name == newSpecies) { return; } _currentSpecies = _allSpecies.FirstOrDefault(species => species.Name == newSpecies); _sidekickRuntime.CurrentSpecies = _currentSpecies; UpdateVisibleColorSets(); ResetCurrentColorSet(); if (_allColorRows.Count == 0) { PopulateColorRowsFromTextures(); } _appliedPartFilters.ResetFiltersForSpeciesChange(); _processingSpeciesChange = true; UpdatePartDropdowns(); PopulatePresetUI(); _processingSpeciesChange = false; } /// /// Populates the parts UI. /// private void PopulatePartUI() { _partView.Clear(); _availablePartList = new List(); _partSelectionDictionary = new Dictionary(); _partLockMap = new Dictionary, bool>(); Foldout speciesFoldout = new Foldout { text = "Select - Species", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) } }; List speciesNames = _allSpecies.Select(species => species.Name).ToList(); _speciesField = new DropdownField { label = "Species", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Normal) }, tooltip = "Select the species of your character" }; _speciesField.choices = speciesNames; _speciesField.RegisterValueChangedCallback( evt => { _speciesPresetField.value = _speciesField.value; ProcessSpeciesChange(evt.newValue); } ); _speciesField.index = _currentSpecies != null && speciesNames.Count > 0 ? _speciesField.choices.IndexOf(_currentSpecies.Name) : 0; speciesFoldout.Add(_speciesField); _partView.Add(speciesFoldout); _partsFoldout = new Foldout() { text = "Select - Parts", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) } }; List outfitFilters = SidekickPartFilter.GetAllForFilterType(_dbManager, FilterType.Outfit); List orderedFilters = new List(); outfitFilters.Sort( (filterA, filterB) => String.CompareOrdinal(filterA.Term, filterB.Term) ); orderedFilters.AddRange(outfitFilters); Foldout filterFoldout = new Foldout() { text = "Select - Outfit Filter", style = { unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) } }; _appliedPartFilters = new FilterGroup() { Runtime = _sidekickRuntime, CombineType = FilterCombineType.Or }; VisualElement toggleList = new VisualElement() { style = { flexDirection = new StyleEnum(FlexDirection.Row), flexWrap = new StyleEnum(Wrap.Wrap) } }; List allFilterToggles = new List(); Color borderColor = new Color(0.17f, 0.17f, 0.17f); Color backgroundColor = new Color(0.35f, 0.35f, 0.35f); foreach (SidekickPartFilter filter in orderedFilters) { Toggle outfitToggle = new Toggle(filter.Term) { value = true, style = { width = 160, borderBottomWidth = 1, borderBottomColor = borderColor, paddingBottom = 2, borderLeftWidth = 1, borderLeftColor = borderColor, paddingLeft = 2, borderRightWidth = 1, borderRightColor = borderColor, paddingRight = 2, borderTopWidth = 1, borderTopColor = borderColor, paddingTop = 2, borderBottomLeftRadius = 3, borderBottomRightRadius = 3, borderTopLeftRadius = 3, borderTopRightRadius = 3, backgroundColor = backgroundColor, textOverflow = new StyleEnum(TextOverflow.Ellipsis) } }; FilterItem filterItem = new FilterItem(_sidekickRuntime, filter, FilterCombineType.Or); _appliedPartFilters.AddFilterItem(filterItem); outfitToggle.RegisterValueChangedCallback( evt => { if (evt.newValue) { _appliedPartFilters.AddFilterItem(filterItem); } else { _appliedPartFilters.RemoveFilterItem(filterItem); } UpdatePartDropdowns(); } ); toggleList.Add(outfitToggle); allFilterToggles.Add(outfitToggle); } VisualElement buttonRow = new VisualElement() { style = { flexDirection = new StyleEnum(FlexDirection.Row) } }; Button selectAll = new Button( delegate { foreach (Toggle toggle in allFilterToggles) { toggle.value = true; } } ) { text = "Select All" }; Button selectNone = new Button( delegate { foreach (Toggle toggle in allFilterToggles) { toggle.value = false; } } ) { text = "Select None" }; buttonRow.Add(selectAll); buttonRow.Add(selectNone); filterFoldout.Add(buttonRow); filterFoldout.Add(toggleList); _partView.Add(filterFoldout); foreach (PartGroup partGroup in Enum.GetValues(typeof(PartGroup))) { string labelText = StringUtils.AddSpacesBeforeCapitalLetters(partGroup.ToString()); Foldout partGroupFoldout = new Foldout { text = labelText, style = { marginLeft = 15, unityFontStyleAndWeight = new StyleEnum(FontStyle.Bold) } }; Button randomiseAllButton = new Button() { text = "Randomize " + labelText }; partGroupFoldout.Add(randomiseAllButton); foreach (CharacterPartType value in partGroup.GetPartTypes()) { VisualElement partContainer = new VisualElement { style = { minHeight = 20, display = DisplayStyle.Flex, flexDirection = FlexDirection.Row, marginBottom = 2, marginTop = 2, marginRight = 2, unityFontStyleAndWeight = new StyleEnum(FontStyle.Normal) } }; BuildPartDetails(value, partContainer); partGroupFoldout.Add(partContainer); } _partsFoldout.Add(partGroupFoldout); randomiseAllButton.clickable.clicked += delegate { foreach (CharacterPartType value in partGroup.GetPartTypes()) { PartTypeControls dropdown = _partSelectionDictionary[value]; bool locked = _partLockMap[dropdown.PartDropdown]; if (!locked) { if (dropdown.PartDropdown.choices.Count > 1) { dropdown.RandomisePartDropdownValue(); } else { dropdown.SetPartDropdownValue("None"); } } } }; } UpdatePartDropdowns(); _partView.Add(_partsFoldout); } /// /// Builds the UI details for each different part type. /// /// The type of the part to build the UI for. /// The container to add the UI to. private void BuildPartDetails(CharacterPartType type, VisualElement partContainer) { List partsList = new List(); foreach (SidekickPart part in _allPartsLibrary[type]) { if (_availablePartList.Any(p => p.ID == part.ID)) { partsList.Add(part); } } Label partTypeTitle = new Label(type.ToString()) { style = { unityTextAlign = TextAnchor.MiddleLeft, width = 150 }, tooltip = type.GetTooltipForPartType() }; Image lockImage = new Image { image = EditorGUIUtility.IconContent("LockIcon", "|Lock Part").image, scaleMode = ScaleMode.ScaleToFit, style = { alignSelf = new StyleEnum(Align.Center), width = 15, height = 15, paddingTop = 2 } }; Button lockButton = new Button() { tooltip = "Remove this part" }; lockButton.Add( lockImage ); Button removeButton = new Button() { tooltip = "Remove this part" }; removeButton.Add( new Image { image = Resources.Load("UI/T_Clear"), scaleMode = ScaleMode.ScaleToFit } ); Button previousButton = new Button() { tooltip = "Select the previous part" }; previousButton.Add( new Image { image = EditorGUIUtility.IconContent("tab_prev", "|Previous Part").image, scaleMode = ScaleMode.ScaleToFit } ); Button nextButton = new Button() { tooltip = "Select the next part" }; nextButton.Add( new Image { image = EditorGUIUtility.IconContent("tab_next", "|Next Part").image, scaleMode = ScaleMode.ScaleToFit } ); Button randomButton = new Button() { tooltip = "Randomly select a part" }; Texture2D randomImage = Resources.Load("UI/T_Random"); randomButton.Add( new Image { image = randomImage, scaleMode = ScaleMode.ScaleToFit, style = { paddingTop = new StyleLength(1), paddingBottom = new StyleLength(1) } } ); List popupValues = new List(); if (type != CharacterPartType.Wrap) { popupValues.Add("None"); } foreach (SidekickPart part in partsList) { SidekickPartSpeciesLink link = SidekickPartSpeciesLink.GetForSpeciesAndPart(_dbManager, _currentSpecies, part); if (link != null) { popupValues.Add(part.Name); } } _currentCharacter.TryGetValue(type, out SidekickPart selectedPart); string currentSelection = selectedPart?.Name ?? "None"; if (_processingSpeciesChange) { if (popupValues.Count < 1 || (popupValues.Count == 1 && popupValues[0] == "None")) { _previousPartSelections[type] = currentSelection; currentSelection = "None"; } else if (PartUtils.IsBaseSpeciesPart(currentSelection)) { _previousPartSelections[type] = currentSelection; currentSelection = popupValues.Find(n => n.Contains("BASE")) ?? "None"; } else if (currentSelection == "None" && _previousPartSelections[type] != "None" && popupValues.Count > 1) { currentSelection = _previousPartSelections[type]; _previousPartSelections[type] = "None"; } if (!popupValues.Contains(currentSelection)) { _previousPartSelections[type] = currentSelection; currentSelection = FindClosestPartMatch(popupValues, currentSelection); } } PopupField partSelection = new PopupField(popupValues, 0) { value = currentSelection }; PartTypeControls controls = new PartTypeControls { PartType = type, PartDropdown = partSelection, ClearButton = removeButton, NextButton = nextButton, PreviousButton = previousButton, RandomButton = randomButton }; partSelection.RegisterCallback>( changeEvent => { PartSelectionChangeEvent(changeEvent, type, controls); } ); _partLockMap[partSelection] = false; lockButton.clickable.clicked += delegate { bool newValue = !_partLockMap[partSelection]; _partLockMap[partSelection] = newValue; if (newValue) { partSelection.SetEnabled(false); removeButton.SetEnabled(false); nextButton.SetEnabled(false); previousButton.SetEnabled(false); randomButton.SetEnabled(false); lockImage.image = EditorGUIUtility.IconContent("LockIcon-On", "|Unlock Part").image; lockButton.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f); } else { partSelection.SetEnabled(true); removeButton.SetEnabled(true); nextButton.SetEnabled(true); previousButton.SetEnabled(true); randomButton.SetEnabled(true); PartSelectionChangeEvent(new ChangeEvent(), type, controls); lockImage.image = EditorGUIUtility.IconContent("LockIcon", "|Lock Part").image; lockButton.style.backgroundColor = new Color(0.345098f, 0.345098f, 0.345098f); } }; if (_processingSpeciesChange) { ChangeEvent changeEvent = ChangeEvent.GetPooled(_previousPartSelections[type], currentSelection); PartSelectionChangeEvent(changeEvent, type, controls); changeEvent.Dispose(); } _partSelectionDictionary.Add(type, controls); removeButton.clickable.clicked += delegate { partSelection.value = "None"; }; previousButton.clickable.clicked += delegate { int newIndex = partSelection.index - 1; if (newIndex <= 0) { newIndex = 0; } partSelection.index = newIndex; }; nextButton.clickable.clicked += delegate { int newIndex = partSelection.index + 1; if (newIndex >= partSelection.choices.Count - 1) { newIndex = partSelection.choices.Count - 1; } partSelection.index = newIndex; }; randomButton.clickable.clicked += delegate { int currentIndex = partSelection.index; if (partSelection.choices.Count - 1 > 1) { while (partSelection.index == currentIndex) { partSelection.index = Random.Range(1, partSelection.choices.Count); } } }; partContainer.Add(partTypeTitle); partContainer.Add(lockButton); partContainer.Add(removeButton); partContainer.Add(previousButton); partContainer.Add(nextButton); partContainer.Add(randomButton); partContainer.Add(partSelection); } private void UpdatePartDropdowns() { Dictionary> filteredParts = _appliedPartFilters.GetFilteredParts(); Dictionary> baseParts = _sidekickRuntime.MappedBasePartDictionary[_currentSpecies]; foreach (CharacterPartType type in Enum.GetValues(typeof(CharacterPartType))) { PartTypeControls controls = _partSelectionDictionary[type]; HashSet popupItems = new HashSet(); if (type != CharacterPartType.Wrap) { popupItems.Add("None"); }; HashSet itemList = baseParts.TryGetValue(type, out List items) ? items.ToHashSet() : new HashSet(); popupItems.UnionWith(itemList); itemList = filteredParts.TryGetValue(type, out List filteredItems) ? filteredItems.ToHashSet() : new HashSet(); popupItems.UnionWith(itemList); List popupValues = popupItems.ToList(); _currentCharacter.TryGetValue(type, out SidekickPart selectedPart); string currentSelection = selectedPart?.Name ?? "None"; if (_processingSpeciesChange) { if (popupValues.Count < 1 || (popupValues.Count == 1 && popupValues[0] == "None")) { _previousPartSelections[type] = currentSelection; currentSelection = "None"; } else if (PartUtils.IsBaseSpeciesPart(currentSelection)) { _previousPartSelections[type] = currentSelection; currentSelection = popupValues.Find(n => n.Contains("BASE")) ?? "None"; } else if (currentSelection == "None" && _previousPartSelections[type] != "None" && popupValues.Count > 1) { currentSelection = _previousPartSelections[type]; _previousPartSelections[type] = "None"; } if (!popupValues.Contains(currentSelection)) { _previousPartSelections[type] = currentSelection; currentSelection = FindClosestPartMatch(popupValues, currentSelection); } } if (type == CharacterPartType.Wrap && currentSelection == "None") { currentSelection = null; } controls.UpdateDropdownValues(popupValues); controls.SetPartDropdownValue(currentSelection); controls.UpdateControls(); } } /// /// Process the change event when a new part is selected. /// /// The change event to process. /// The type of part that has been changed. /// The UI PopupField the change event is happening for. private void PartSelectionChangeEvent(ChangeEvent changeEvent, CharacterPartType type, PartTypeControls partSelection) { try { if (_currentTab == TabView.Parts && _sidekickRuntime.MappedPartDictionary.ContainsKey(type) && changeEvent.newValue != null && _sidekickRuntime.MappedPartDictionary[type].TryGetValue(changeEvent.newValue, out SidekickPart selectedPart)) { GameObject partModel = selectedPart.GetPartModel(); SkinnedMeshRenderer selectedMesh = partModel.GetComponentInChildren(); _partDictionary[type] = selectedMesh; _currentCharacter[type] = selectedPart; if (!_processingSpeciesChange) { _previousPartSelections[type] = changeEvent.newValue; } if (type == CharacterPartType.Torso) { _requiresWrap = selectedPart.UsesWrap; if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection)) { if (_requiresWrap) { wrapSelection.PartDropdown.SetEnabled(true); wrapSelection.RandomisePartDropdownValue(); ; } else { wrapSelection.PartDropdown.SetEnabled(false); wrapSelection.SetPartDropdownValue(null); } } } } else if (changeEvent.newValue == "None") { _currentCharacter.Remove(type); _partDictionary.Remove(type); if (!_processingSpeciesChange) { _previousPartSelections[type] = "None"; } if (type == CharacterPartType.Torso) { if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection)) { wrapSelection.PartDropdown.SetEnabled(false); wrapSelection.SetPartDropdownValue(null); _currentCharacter.Remove(CharacterPartType.Wrap); } } } else { _partDictionary.Remove(type); } if (!_applyingPreset && _previewToggle.value) { // if (_combineMeshes && _newModel != null) // { // DestroyImmediate(_newModel); // } _newModel = GenerateCharacter(false, true); bool switchAnimation = SetupAnimationControllers(); if (switchAnimation && (type == CharacterPartType.HandLeft || type == CharacterPartType.HandRight)) { SetState("InspectHands"); } UpdatePartUVData(); } partSelection.UpdateControls(); } catch (Exception ex) { EditorUtility.DisplayDialog("Failed loading part", "Failed to load the following part\n" + changeEvent.newValue, "OK"); Debug.LogWarning(ex); } } /// /// Selects the first match, with the highest number of matching terms from a list of parts. /// /// The list of parts. /// The part to find the closest match for. /// The part with the closest match to the given part. private string FindClosestPartMatch(List availableParts, string existingPart) { string closestMatch = "None"; List partSections = existingPart.Split("_").ToList(); Dictionary matchCounts = new Dictionary(); foreach (string section in partSections) { foreach (string part in availableParts) { if (part.Contains(section)) { if (matchCounts.TryGetValue(part, out int count)) { matchCounts[part] = count + 1; } else { matchCounts[part] = 1; } } } } int currentMax = 0; foreach (KeyValuePair match in matchCounts) { if (match.Value > currentMax) { closestMatch = match.Key; currentMax = match.Value; } } return closestMatch; } /// /// Generates a character from the current selected parts. /// private GameObject GenerateCharacter(bool combineMesh, bool processBoneMovement) { List parts = new List(); foreach (KeyValuePair entry in _currentCharacter) { if (entry.Value != null) { // Only apply wrap when required if (entry.Key == CharacterPartType.Wrap && (!_requiresWrap || _bodyTypeBlendValue < 0)) { continue; } GameObject partModel = entry.Value.GetPartModel(); if (partModel == null) { if (_showMissingPartsPopup) { EditorUtility.DisplayDialog( "Error loading part", "Unable to load part: " + entry.Value.Name + ".", "Ok" ); } continue; } SkinnedMeshRenderer selectedMesh = partModel.GetComponentInChildren(); if (selectedMesh != null) { parts.Add(selectedMesh); } } } GameObject newModel = null; try { newModel = _sidekickRuntime.CreateCharacter( _OUTPUT_MODEL_NAME, parts, combineMesh, processBoneMovement, _newModel); _currentAnimator = null; } catch (Exception ex) { EditorUtility.DisplayDialog( "Error creating character", "Something went wrong when creating the character.\nPlease try again.", "Ok" ); Debug.LogWarning(ex); } return newModel; } /// /// Saves a character (Parts and Colors) out to a file which can be imported by the tool into any project. /// private void SaveCharacter() { try { SaveCharacter(null); } catch { Debug.LogWarning("Failed to save character. Please try again."); } } /// /// Saves a character (Parts and Colors) out to a file which can be imported by the tool into any project to a given path. /// /// The path to save the character to. private void SaveCharacter(string savePath) { bool showSuccessMessage = false; if (string.IsNullOrEmpty(savePath)) { savePath = ShowCharacterSaveDialog(); showSuccessMessage = true; } if (string.IsNullOrEmpty(savePath)) { // EditorUtility.DisplayDialog("Save Cancelled", "No save file selected. Saving cancelled.", "OK"); return; } string filename = Path.GetFileNameWithoutExtension(savePath); SerializedCharacter savedCharacter = CreateSerializedCharacter(filename); Serializer serializer = new Serializer(); File.WriteAllBytes(savePath, Encoding.ASCII.GetBytes(serializer.Serialize(savedCharacter))); if (showSuccessMessage) { EditorUtility.DisplayDialog("Save Successful", "Character successfully saved to " + Path.GetFileName(savePath), "OK"); } } /// /// Crates a serialized character from the current tool selections. /// /// The name to store in the serialized character. /// A SerializedCharacter from the selections in the tool. private SerializedCharacter CreateSerializedCharacter(string characterName) { SerializedCharacter savedCharacter = new SerializedCharacter { Species = _currentSpecies.ID, Name = characterName }; List usedParts = new List(); foreach (KeyValuePair entry in _currentCharacter) { // TODO: Update the part version to use actual version once the information is available. usedParts.Add(new SerializedPart(entry.Value.Name, entry.Key, "1")); } savedCharacter.Parts = usedParts; SerializedColorSet savedSet = new SerializedColorSet(); savedSet.PopulateFromSidekickColorSet(_currentColorSet, _currentSpecies); savedCharacter.ColorSet = savedSet; savedCharacter.BlendShapes = new SerializedBlendShapeValues() { BodyTypeValue = _bodyTypeSlider.value, BodySizeValue = _bodySizeSlider.value, MuscleValue = _musclesSlider.value }; List savedColorRows = new List(); foreach (SidekickColorRow row in _allColorRows) { savedColorRows.Add(new SerializedColorRow(row)); } savedCharacter.ColorRows = savedColorRows; return savedCharacter; } /// /// Loads a character (Parts and Colors) into the tool. /// private void LoadCharacter() { _loadingCharacter = true; bool showAllColors = _showAllColourProperties; _showAllColourProperties = true; string filePath = EditorUtility.OpenFilePanel("Load Character", "", "sk"); if (string.IsNullOrEmpty(filePath)) { EditorUtility.DisplayDialog("No File Chosen", "No file was chosen to load.", "OK"); return; } _bodyPartsTab.value = true; SwitchToTab(TabView.Parts); byte[] bytes = File.ReadAllBytes(filePath); string data = Encoding.ASCII.GetString(bytes); Deserializer deserializer = new Deserializer(); SerializedCharacter savedCharacter = deserializer.Deserialize(data); LoadSerializedCharacter(savedCharacter, showAllColors); _loadingCharacter = false; } /// /// Loads a character into the tool from a serialized character. /// /// The serialized character to load. /// Whether to show all colors or not. private void LoadSerializedCharacter(SerializedCharacter serializedCharacter, bool showAllColors) { SidekickSpecies species = SidekickSpecies.GetByID(_dbManager, serializedCharacter.Species); _speciesField.value = species.Name; ProcessSpeciesChange(species.Name); bool hasErrors = false; string errorMessage = "The following parts could not be found in your project:\n"; foreach (CharacterPartType currentType in Enum.GetValues(typeof(CharacterPartType))) { PartTypeControls currentField = _partSelectionDictionary[currentType]; SerializedPart part = serializedCharacter.Parts.FirstOrDefault(p => p.PartType == currentType); SidekickPart skPart = SidekickPart.SearchForByName(_dbManager, part?.Name); if (skPart != null) { UpdateResult result = UpdatePartDropdown(currentField, skPart.Name, errorMessage, hasErrors); hasErrors = result.HasErrors; errorMessage = result.ErrorMessage; } else { currentField.SetPartDropdownValue("None"); } } if (hasErrors) { EditorUtility.DisplayDialog( "Assets Missing", errorMessage, "OK" ); } LoadColorSet(serializedCharacter); if (serializedCharacter.BlendShapes != null) { LoadBlendShapes(serializedCharacter); } _showAllColourProperties = showAllColors; UpdateColorTabContent(); if (_combineMeshes && _newModel != null) { DestroyImmediate(_newModel); } _newModel = GenerateCharacter(_combineMeshes, true); UpdatePartUVData(); } /// /// Updates a part dropdown to select a new part, if the part is not in the dropdown values, `None` is selected instead. /// /// The dropdown field to update. /// The new part to select. /// The error message to update if the part is not available. /// The error flag to update if an error is encountered. /// A PartUpdateResult with the results of the update. private UpdateResult UpdatePartDropdown(PartTypeControls currentField, string partName, string errorMessage, bool hasErrors) { _partLockMap[currentField.PartDropdown] = false; if (partName == "None" || _allParts.Any(part => part.Name == partName)) { if (!currentField.PartDropdown.choices.Contains(partName) && PartUtils.IsBaseSpeciesPart(partName)) { currentField.SetPartDropdownValue(currentField.PartDropdown.choices.Find(n => n.Contains("BASE")) ?? "None"); if (currentField.PartDropdown.value == "None") { hasErrors = true; errorMessage += partName + "\n"; } } else { currentField.SetPartDropdownValue(partName); } } else if (PartUtils.IsBaseSpeciesPart(partName)) { currentField.SetPartDropdownValue(currentField.PartDropdown.choices.Find(n => n.Contains("BASE")) ?? "None"); if (currentField.PartDropdown.value == "None") { hasErrors = true; errorMessage += partName + "\n"; } } else { currentField.SetPartDropdownValue("None"); hasErrors = true; errorMessage += partName + "\n"; } return new UpdateResult(errorMessage, hasErrors); } /// /// Loads the color set for a saved character into memory. /// /// The character to load the color set for. private void LoadColorSet(SerializedCharacter savedCharacter) { _currentColorSet = savedCharacter.ColorSet.CreateSidekickColorSet(_dbManager); _colorSetsDropdown.value = "Custom"; List newRows = new List(); foreach (SerializedColorRow row in savedCharacter.ColorRows) { newRows.Add(row.CreateSidekickColorRow(_dbManager, _currentColorSet)); } _allColorRows = newRows; UpdateColorTabContent(); } /// /// Updates the content of the Color Tab. /// private void UpdateColorTabContent() { PopulatePartColorRows(); UpdateAllVisibleColors(); RefreshVisibleColorRows(); } /// /// Loads the blend shapes from a saved character into the tool. /// /// The character to load the blend shapes for. private void LoadBlendShapes(SerializedCharacter savedCharacter) { _bodyTypeSlider.value = savedCharacter.BlendShapes.BodyTypeValue; _bodySizeSlider.value = savedCharacter.BlendShapes.BodySizeValue; _musclesSlider.value = savedCharacter.BlendShapes.MuscleValue; } /// /// Shows the dialog box for where to save the character to, and also validates the save location and filename. /// /// The default path to save to. /// The full file path and filename to save the character to. private string ShowCharacterSaveDialog(string path = "") { string defaultName = _currentSpecies.Name + "-" + _currentColorSet.Name + ".sk"; string defaultDirectory = ""; if (!string.IsNullOrEmpty(path)) { defaultName = Path.GetFileName(path); defaultDirectory = Path.GetDirectoryName(path); } string savePath = EditorUtility.SaveFilePanel( "Save New Character", defaultDirectory, defaultName, "sk" ); // if (!string.IsNullOrEmpty(savePath) && File.Exists(savePath)) // { // int option = EditorUtility.DisplayDialogComplex( // "File Already Exists", // "A file already exists with the same name, are you sure you wish to overwrite it?\nThis cannot be undone.", // "Overwrite", // "Rename", // "Cancel" // ); // // switch (option) // { // // Overwrite. // case 0: // EditorUtility.DisplayDialog("Overwrite Accepted", "Existing file will be overwritten.", "OK"); // break; // // // Rename. // case 1: // savePath = ShowCharacterSaveDialog(savePath); // break; // // // Cancel. // case 2: // default: // savePath = null; // break; // } // } return savePath; } /// /// Saves a created character as a prefab. /// private void CreateCharacterPrefab() { try { string savePath = SelectPrefabSaveLocation(); if (string.IsNullOrEmpty(savePath)) { // EditorUtility.DisplayDialog("Save Cancelled", "No save file selected. Saving cancelled.", "OK"); return; } string baseFilename = Path.GetFileNameWithoutExtension(savePath); string directoryBase = Path.GetDirectoryName(savePath) ?? string.Empty; string directory = Path.Combine(directoryBase, baseFilename); savePath = Path.Combine(directory, Path.GetFileName(savePath)); string textureDirectory = Path.Combine(directory, "Textures"); string meshDirectory = Path.Combine(directory, "Meshes"); string materialDirectory = Path.Combine(directory, "Materials"); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } if (!Directory.Exists(meshDirectory)) { Directory.CreateDirectory(meshDirectory); } if (!Directory.Exists(materialDirectory)) { Directory.CreateDirectory(materialDirectory); } string savedCharacterPath = Path.Combine(directory, baseFilename + ".sk"); SaveCharacter(savedCharacterPath); //TODO textures are shared between exports! SaveTexturesToDisk(textureDirectory, baseFilename); // Ensure textures are written to disk before proceeding. As it seems this happens outside the main Unity loop, so can't be easily checked // for. int cutoff = 0; while (cutoff < 1000000000 && Directory.GetFiles(textureDirectory).Length <= 0) { cutoff++; } AssetDatabase.Refresh(); GameObject clonedModel = GenerateCharacter(_combineMeshes, false); List allRenderers = clonedModel.GetComponentsInChildren().ToList(); SkinnedMeshRenderer clonedRenderer = allRenderers[0]; if (_bakeBlends) { // Copy mesh, bone weights and bindposes before baking so the mesh can be re-skinned after baking. foreach (SkinnedMeshRenderer renderer in allRenderers) { if (clonedRenderer == null) { clonedRenderer = renderer; } Mesh clonedSkinnedMesh = MeshUtils.CopyMesh(renderer.sharedMesh); BoneWeight[] boneWeights = clonedSkinnedMesh.boneWeights; Matrix4x4[] bindposes = clonedSkinnedMesh.bindposes; List blendData = BlendShapeUtils.GetBlendShapeData( clonedSkinnedMesh, renderer, new string[] { "defaultHeavy", "defaultBuff", "defaultSkinny", "masculineFeminine" }, 0, new List() ); renderer.BakeMesh(clonedSkinnedMesh); // Re-skin the new baked mesh. clonedSkinnedMesh.boneWeights = boneWeights; clonedSkinnedMesh.bindposes = bindposes; // assign the new mesh to the renderer renderer.sharedMesh = clonedSkinnedMesh; BlendShapeUtils.RestoreBlendShapeData(blendData, clonedSkinnedMesh, renderer); } } // now do the bone movements! _sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager)); _sidekickRuntime.ProcessBoneMovement(clonedModel); Material newMaterial = CreateNewMaterialAssetFromSource( clonedRenderer.sharedMaterial, textureDirectory, materialDirectory, baseFilename, baseFilename ); foreach (SkinnedMeshRenderer renderer in allRenderers) { renderer.sharedMaterial = newMaterial; } CreatePrefab(clonedModel, meshDirectory, savePath, baseFilename); DestroyImmediate(clonedModel); } catch { Debug.LogWarning("Failed to create character prefab, please try again."); } } /// /// Prompts the user to select a path and prefab name within the project. /// /// The path and filename to use to save the prefab to. private string SelectPrefabSaveLocation() { string defaultName = _currentSpecies.Name + "-" + _currentColorSet.Name + ".prefab"; string savePath = EditorUtility.SaveFilePanelInProject( "Save Character Prefab", defaultName, "prefab", "Select where to save the prefab" ); return savePath; } /// /// Sets the material to use the textures at the given location. /// /// The material to set the textures on. /// The path to set the textures from. /// Additional naming for the textures, if applicable. /// The material with the paths set on it. private Material SetTextureLinkOnMaterial(Material material, string texturePath, string textureName = "") { string filename = _TEXTURE_PREFIX; if (!string.IsNullOrEmpty(textureName)) { filename += textureName; } material.SetTexture(_COLOR_MAP, null); LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_COLOR_NAME, _COLOR_MAP); // TODO: Uncomment when the shader has the these properties are enabled again // material.SetTexture(_METALLIC_MAP, null); // LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_METALLIC_NAME, _METALLIC_MAP); // material.SetTexture(_SMOOTHNESS_MAP, null); // LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_SMOOTHNESS_NAME, _SMOOTHNESS_MAP); // material.SetTexture(_REFLECTION_MAP, null); // LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_REFLECTION_NAME, _REFLECTION_MAP); // material.SetTexture(_EMISSION_MAP, null); // LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_EMISSION_NAME, _EMISSION_MAP); // material.SetTexture(_OPACITY_MAP, null); // LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_OPACITY_NAME, _OPACITY_MAP); return material; } /// /// Loads a texture from disk and assigns it to the material in the given texture ID. /// /// The material to assign the texture to. /// The path to load the texture from. /// The name of the texture to load. /// The texture ID to load the texture into on the material. private void LoadAndAssignTexture(Material material, string texturePath, string textureName, int textureID) { string filePath = Path.Combine(texturePath, textureName); while (material.GetTexture(textureID) == null) { TextureImporter textureImporter = AssetImporter.GetAtPath(filePath) as TextureImporter; if (textureImporter != null) { textureImporter.wrapMode = TextureWrapMode.Clamp; textureImporter.filterMode = FilterMode.Point; textureImporter.mipmapEnabled = false; textureImporter.SetPlatformTextureSettings(new TextureImporterPlatformSettings { maxTextureSize = 32, resizeAlgorithm = TextureResizeAlgorithm.Bilinear, format = TextureImporterFormat.RGB24 }); EditorUtility.SetDirty(textureImporter); textureImporter.SaveAndReimport(); } material.SetTexture(textureID, (Texture2D) AssetDatabase.LoadAssetAtPath(filePath, typeof(Texture2D))); } } /// /// Creates a new material to assign to the prefab. /// /// The existing material from the base model. /// The directory to save the textures into. /// The directory to save the material into. /// The base filename to use for all assets. /// Additional naming for the textures, if applicable. /// The new material cloned from sourceMaterial saved to the asset database. private Material CreateNewMaterialAssetFromSource( Material sourceMaterial, string textureDirectory, string materialDirectory, string baseFilename, string textureName = "" ) { Material clonedMaterial = new Material(sourceMaterial.shader); // NOTE: this is copying the texture slots from oldMaterial, so they need to be null'd afterward in SetTextureLinkOnMaterial() clonedMaterial.CopyPropertiesFromMaterial(sourceMaterial); clonedMaterial = SetTextureLinkOnMaterial(clonedMaterial, textureDirectory, textureName); string materialPath = Path.Combine(materialDirectory, baseFilename + ".mat"); // If the user has chosen to overwrite the prefab, delete the existing assets to replace them. if (File.Exists(materialPath)) { File.Delete(materialPath); } AssetDatabase.CreateAsset(clonedMaterial, materialPath); return clonedMaterial; } /// /// Creates a prefab and the required assets for the model to work as an independent asset. /// /// Root game object for the prefab. /// The directory to save the mesh and avatar assets to. /// The path to save the prefab to. /// The base filename to use for all assets. private void CreatePrefab( GameObject rootGameObject, string meshDirectory, string savePath, string baseFilename ) { List renderers = rootGameObject.GetComponentsInChildren().ToList(); foreach (SkinnedMeshRenderer renderer in renderers) { string type = null; if (renderer.name.Contains("_")) { type = Enum.GetName(typeof(CharacterPartType), _sidekickRuntime.ExtractPartType(renderer.name)); } Mesh sharedMesh = renderer.sharedMesh; string meshPath = type == null ? Path.Combine(meshDirectory, baseFilename + ".asset") : Path.Combine(meshDirectory, baseFilename + "-" + type + ".asset"); // If the user has chosen to overwrite the prefab, delete the existing assets to replace them. if (File.Exists(meshPath)) { File.Delete(meshPath); } AssetDatabase.CreateAsset(sharedMesh, meshPath); } Animator animator = rootGameObject.GetComponentInChildren(); Avatar existingAvatar = animator.avatar; Avatar newAvatar = Instantiate(existingAvatar); animator.avatar = newAvatar; string avatarPath = Path.Combine(meshDirectory, baseFilename + "-avatar.asset"); // If the user has chosen to overwrite the prefab, delete the existing assets to replace them. if (File.Exists(avatarPath)) { File.Delete(avatarPath); } AssetDatabase.CreateAsset(newAvatar, avatarPath); AssetDatabase.SaveAssets(); PrefabUtility.SaveAsPrefabAsset(rootGameObject, savePath); } /// /// 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. private string GetOutfitNameFromPartName(string partName) { if (string.IsNullOrEmpty(partName)) { return "None"; } return string.Join('_', partName.Substring(3).Split('_').Take(2)); } /// /// Updates the part UV data. /// private void UpdatePartUVData() { _currentUVDictionary = _sidekickRuntime.CurrentUVDictionary; _currentUVList = _sidekickRuntime.CurrentUVList; } /// /// The available tab views /// private enum TabView { Preset, Parts, Body, Colors, Decals, Options } /// /// The different types of preset dropdown /// private enum PresetDropdownType { Part, Body, Color, Texture } /// /// Encapsulates the result from a dropdown update attempt. /// private class UpdateResult { public string ErrorMessage { get; private set; } public bool HasErrors { get; private set; } public UpdateResult(string errorMessage, bool hasErrors) { ErrorMessage = errorMessage; HasErrors = hasErrors; } } } } #endif